diff --git a/Assets/Prefabs/Hider.prefab b/Assets/Prefabs/Hider.prefab
index c340875..28684b9 100644
--- a/Assets/Prefabs/Hider.prefab
+++ b/Assets/Prefabs/Hider.prefab
@@ -10,7 +10,7 @@ PrefabInstance:
m_Modifications:
- target: {fileID: 273411532115212066, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: scale
- value: 0.5
+ value: 0.7
objectReference: {fileID: 0}
- target: {fileID: 1557561356852236266, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: GlobalObjectIdHash
@@ -22,15 +22,15 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 6997860362608943113, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: m_LocalScale.x
- value: 0.5
+ value: 0.7
objectReference: {fileID: 0}
- target: {fileID: 6997860362608943113, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: m_LocalScale.y
- value: 0.5
+ value: 0.7
objectReference: {fileID: 0}
- target: {fileID: 6997860362608943113, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: m_LocalScale.z
- value: 0.5
+ value: 0.7
objectReference: {fileID: 0}
- target: {fileID: 6997860362608943113, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: m_LocalPosition.x
diff --git a/Assets/Prefabs/Hunter.prefab b/Assets/Prefabs/Hunter.prefab
index e2fd8ec..91589cb 100644
--- a/Assets/Prefabs/Hunter.prefab
+++ b/Assets/Prefabs/Hunter.prefab
@@ -10,7 +10,7 @@ PrefabInstance:
m_Modifications:
- target: {fileID: 273411532115212066, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: scale
- value: 2.5
+ value: 2.4
objectReference: {fileID: 0}
- target: {fileID: 1035623159758896108, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: sprintSpeedMultiplier
@@ -26,15 +26,15 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 6997860362608943113, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: m_LocalScale.x
- value: 2.5
+ value: 2.4
objectReference: {fileID: 0}
- target: {fileID: 6997860362608943113, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: m_LocalScale.y
- value: 2.5
+ value: 2.4
objectReference: {fileID: 0}
- target: {fileID: 6997860362608943113, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: m_LocalScale.z
- value: 2.5
+ value: 2.4
objectReference: {fileID: 0}
- target: {fileID: 6997860362608943113, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
propertyPath: m_LocalPosition.x
@@ -76,6 +76,10 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
+ - target: {fileID: 8574096554986372233, guid: 97017ed3bb40aff4e885440d9a7336ee, type: 3}
+ propertyPath: role
+ value: 1
+ objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity
index 7002168..06c81c5 100644
--- a/Assets/Scenes/SampleScene.unity
+++ b/Assets/Scenes/SampleScene.unity
@@ -1251,6 +1251,9 @@ MonoBehaviour:
m_EditorClassIdentifier: Assembly-CSharp::NetworkBootstrap
showGui: 1
enableHotkeys: 1
+ hunterPrefab: {fileID: 3510981371713521867, guid: cf153a3aea3d5da4f9d4cdbff0be8180, type: 3}
+ hiderPrefab: {fileID: 5508335275054701003, guid: 6b0b58e8352625343951db00beb8cb0a, type: 3}
+ selectedRole: 0
--- !u!114 &284910515
MonoBehaviour:
m_ObjectHideFlags: 0
diff --git a/Assets/Scripts/GameModeManager.cs b/Assets/Scripts/GameModeManager.cs
index 0f1a3b2..5c6f7ad 100644
--- a/Assets/Scripts/GameModeManager.cs
+++ b/Assets/Scripts/GameModeManager.cs
@@ -6,9 +6,10 @@ using UnityEngine.InputSystem;
#endif
///
-/// Switches the game between high-level modes (Player Control, Paint, Free Cam). The switch
-/// key (Tab) cycles to the next registered mode. Register additional modes in
-/// . Attach to a single, persistent scene GameObject.
+/// Switches the game between high-level modes. The switch key (Tab) cycles to the next registered
+/// mode. The mode set is scoped to the local avatar's when it spawns
+/// (Hunter: First Person + Paint; Hider: Player Control + Paint + Free Cam); a non-networked scene
+/// uses the full set. Attach to a single, persistent scene GameObject.
///
[DisallowMultipleComponent]
public class GameModeManager : MonoBehaviour
@@ -86,30 +87,40 @@ public class GameModeManager : MonoBehaviour
SwitchTo(Mathf.Clamp(startModeIndex, 0, _modes.Count - 1));
}
- /// Registers all available modes. Add new modes here.
- private void BuildModes()
+ /// Registers the modes for the current context. Without a role (no networked avatar yet,
+ /// or a non-networked scene) the full set is built from whichever rigs are present. With a role the
+ /// set is scoped to that side and ordered so index 0 is its intended start mode. Add new modes here.
+ private void BuildModes() => BuildModes(null);
+
+ private void BuildModes(PlayerRole? role)
{
_modes.Clear();
- // Player Control only joins the Tab cycle when a gravity player is present, so a scene
- // running only the insect walker doesn't trap input in a dead, player-freezing mode.
- if (player)
- _modes.Add(new PlayerControlMode());
+ switch (role)
+ {
+ case PlayerRole.Hunter:
+ // Hunter: first person + paint. Index 0 (First Person, if its rig exists) is the start mode.
+ if (firstPerson) _modes.Add(new FirstPersonMode());
+ _modes.Add(new PaintMode());
+ break;
- _modes.Add(new PaintMode());
+ case PlayerRole.Hider:
+ // Hider: third person (player control) + paint + free cam. Index 0 (Player Control) is the start.
+ if (player) _modes.Add(new PlayerControlMode());
+ _modes.Add(new PaintMode());
+ if (freeCam) _modes.Add(new FreeCamMode());
+ break;
- // Free Cam only joins the cycle when a freecam rig is present, so scenes without one are unchanged.
- if (freeCam)
- _modes.Add(new FreeCamMode());
-
- // First Person only joins the cycle when a first-person rig is present, so scenes without one are unchanged.
- if (firstPerson)
- _modes.Add(new FirstPersonMode());
-
- // Insect mode only joins the Tab cycle when a surface walker is present, so a scene
- // without one behaves exactly as before (no dead, player-freezing mode in the cycle).
- //if (insect)
- // _modes.Add(new InsectMode());
+ default:
+ // No role: legacy full set, gated on whichever rigs exist. Player Control only joins when a
+ // gravity player is present (player-less pre-spawn stays in Paint, not a dead frozen mode);
+ // Free Cam / First Person only when their rigs exist, so non-networked scenes are unchanged.
+ if (player) _modes.Add(new PlayerControlMode());
+ _modes.Add(new PaintMode());
+ if (freeCam) _modes.Add(new FreeCamMode());
+ if (firstPerson) _modes.Add(new FirstPersonMode());
+ break;
+ }
foreach (var m in _modes)
m.Bind(this);
@@ -142,13 +153,16 @@ public class GameModeManager : MonoBehaviour
if (firstPerson)
firstPerson.SetPlayer(localPlayer);
- // Tear down the pre-spawn (player-less) mode and rebuild with Player Control available.
+ // Tear down the pre-spawn (player-less) mode and rebuild scoped to this avatar's role.
ActiveMode?.Exit();
_activeIndex = -1;
- BuildModes();
- int idx = _modes.FindIndex(m => m is PlayerControlMode);
- SwitchTo(idx >= 0 ? idx : Mathf.Clamp(startModeIndex, 0, _modes.Count - 1));
+ var setup = localPlayer.GetComponent();
+ PlayerRole role = setup ? setup.Role : PlayerRole.Hider;
+ BuildModes(role);
+
+ // Each role list is ordered so index 0 is its start mode (Hunter -> First Person, Hider -> Player Control).
+ SwitchTo(0);
}
private void Update()
diff --git a/Assets/Scripts/NetworkBootstrap.cs b/Assets/Scripts/NetworkBootstrap.cs
index 6ec9573..3423d68 100644
--- a/Assets/Scripts/NetworkBootstrap.cs
+++ b/Assets/Scripts/NetworkBootstrap.cs
@@ -6,10 +6,11 @@ using UnityEngine.InputSystem;
///
/// Dev-only connection harness for local multiplayer testing across the junction-mirror clones:
-/// one instance clicks Host, the others click Client (loopback 127.0.0.1 via the configured
-/// UnityTransport). Draws a small on-screen panel and exposes hotkeys (H = Host, C = Client,
-/// J = Server, K = Shutdown). Purely a testing scaffold — replace with real lobby/menu UI later.
-/// Attach to the same GameObject as the NetworkManager.
+/// pick a role (Hunter/Hider) in the panel, then one instance clicks Host and the others click
+/// Client (loopback 127.0.0.1 via the configured UnityTransport). The chosen role rides along as the
+/// connection-approval payload, so the server spawns that role's prefab variant for the connection.
+/// Hotkeys: H = Host, C = Client, J = Server, K = Shutdown. Purely a testing scaffold - replace with
+/// real lobby/menu UI later. Attach to the same GameObject as the NetworkManager.
///
[RequireComponent(typeof(NetworkManager))]
[DisallowMultipleComponent]
@@ -21,9 +22,51 @@ public class NetworkBootstrap : MonoBehaviour
[Tooltip("Allow the H/C/J/K keyboard shortcuts.")]
public bool enableHotkeys = true;
+ [Header("Role prefabs")]
+ [Tooltip("Prefab spawned for a connection that picked Hunter. Must be registered as a network prefab.")]
+ [SerializeField] private GameObject hunterPrefab;
+
+ [Tooltip("Prefab spawned for a connection that picked Hider. Must be registered as a network prefab.")]
+ [SerializeField] private GameObject hiderPrefab;
+
+ [Tooltip("Role this instance connects as. Toggle it in the dev panel before hosting/joining.")]
+ [SerializeField] private PlayerRole selectedRole = PlayerRole.Hider;
+
private NetworkManager _net;
- private void Awake() => _net = GetComponent();
+ private void Awake()
+ {
+ _net = GetComponent();
+
+ // Per-connection prefab selection needs connection approval: the callback maps the role byte
+ // the joiner sent to the matching prefab hash. Enabled here so the scene asset needn't carry it.
+ _net.NetworkConfig.ConnectionApproval = true;
+ _net.ConnectionApprovalCallback = ApprovalCheck;
+ }
+
+ /// Stamps the chosen role into the connection payload, then starts the given mode.
+ /// StartHost runs approval on its own client too, so the host's role is read from this same payload.
+ private void StartAs(System.Func start)
+ {
+ _net.NetworkConfig.ConnectionData = new[] { (byte)selectedRole };
+ start();
+ }
+
+ private void ApprovalCheck(NetworkManager.ConnectionApprovalRequest request,
+ NetworkManager.ConnectionApprovalResponse response)
+ {
+ PlayerRole role = (request.Payload != null && request.Payload.Length > 0)
+ ? (PlayerRole)request.Payload[0]
+ : PlayerRole.Hider;
+
+ GameObject prefab = role == PlayerRole.Hunter ? hunterPrefab : hiderPrefab;
+ var netObj = prefab ? prefab.GetComponent() : null;
+
+ response.Approved = true;
+ response.CreatePlayerObject = true;
+ // Null prefab/ref falls back to the NetworkManager's default PlayerPrefab.
+ if (netObj) response.PlayerPrefabHash = netObj.PrefabIdHash;
+ }
private void Update()
{
@@ -33,9 +76,9 @@ public class NetworkBootstrap : MonoBehaviour
if (kb == null || _net == null) return;
if (!_net.IsListening)
{
- if (kb.hKey.wasPressedThisFrame) _net.StartHost();
- else if (kb.cKey.wasPressedThisFrame) _net.StartClient();
- else if (kb.jKey.wasPressedThisFrame) _net.StartServer();
+ if (kb.hKey.wasPressedThisFrame) StartAs(_net.StartHost);
+ else if (kb.cKey.wasPressedThisFrame) StartAs(_net.StartClient);
+ else if (kb.jKey.wasPressedThisFrame) StartAs(_net.StartServer);
}
else if (kb.kKey.wasPressedThisFrame)
{
@@ -48,18 +91,25 @@ public class NetworkBootstrap : MonoBehaviour
{
if (!showGui || _net == null) return;
- GUILayout.BeginArea(new Rect(10, 10, 220, 160), GUI.skin.box);
+ GUILayout.BeginArea(new Rect(10, 10, 220, 200), GUI.skin.box);
if (!_net.IsListening)
{
GUILayout.Label("Netcode (dev)");
- if (GUILayout.Button("Host (H)")) _net.StartHost();
- if (GUILayout.Button("Client (C)")) _net.StartClient();
- if (GUILayout.Button("Server (J)")) _net.StartServer();
+
+ GUILayout.BeginHorizontal();
+ GUILayout.Label("Role:", GUILayout.Width(38));
+ if (GUILayout.Toggle(selectedRole == PlayerRole.Hider, "Hider", GUI.skin.button)) selectedRole = PlayerRole.Hider;
+ if (GUILayout.Toggle(selectedRole == PlayerRole.Hunter, "Hunter", GUI.skin.button)) selectedRole = PlayerRole.Hunter;
+ GUILayout.EndHorizontal();
+
+ if (GUILayout.Button("Host (H)")) StartAs(_net.StartHost);
+ if (GUILayout.Button("Client (C)")) StartAs(_net.StartClient);
+ if (GUILayout.Button("Server (J)")) StartAs(_net.StartServer);
}
else
{
string role = _net.IsHost ? "Host" : _net.IsServer ? "Server" : "Client";
- GUILayout.Label($"Role: {role}");
+ GUILayout.Label($"Role: {role} ({selectedRole})");
GUILayout.Label($"ClientId: {_net.LocalClientId}");
GUILayout.Label($"Clients: {(_net.IsServer ? _net.ConnectedClients.Count : (_net.IsConnectedClient ? 1 : 0))}");
if (GUILayout.Button("Shutdown (K)")) _net.Shutdown();
diff --git a/Assets/Scripts/NetworkPlayerSetup.cs b/Assets/Scripts/NetworkPlayerSetup.cs
index ab10d8c..1923119 100644
--- a/Assets/Scripts/NetworkPlayerSetup.cs
+++ b/Assets/Scripts/NetworkPlayerSetup.cs
@@ -21,6 +21,14 @@ public class NetworkPlayerSetup : NetworkBehaviour
[Tooltip("Slots per ring before the radius steps outward, so spacing stays roughly constant as more join.")]
[SerializeField] private int spawnRingSlots = 8;
+ [Header("Role")]
+ [Tooltip("Which side this avatar plays. Baked per prefab variant (Hider on the base/Hider prefab, " +
+ "Hunter on the Hunter prefab); GameModeManager reads it on spawn to build this role's mode set.")]
+ [SerializeField] private PlayerRole role = PlayerRole.Hider;
+
+ /// The side this avatar plays (Hunter/Hider), baked into the prefab variant.
+ public PlayerRole Role => role;
+
public override void OnNetworkSpawn()
{
var controller = GetComponent();
diff --git a/Assets/Scripts/PlayerRole.cs b/Assets/Scripts/PlayerRole.cs
new file mode 100644
index 0000000..421c8b8
--- /dev/null
+++ b/Assets/Scripts/PlayerRole.cs
@@ -0,0 +1,10 @@
+///
+/// Which side a spawned avatar plays. Picked at connect time (NetworkBootstrap) and sent as the
+/// connection-approval payload so the server spawns the matching prefab variant, then baked into
+/// that prefab and read back by GameModeManager to build the role's mode set.
+///
+public enum PlayerRole
+{
+ Hider = 0,
+ Hunter = 1,
+}
diff --git a/Assets/Scripts/PlayerRole.cs.meta b/Assets/Scripts/PlayerRole.cs.meta
new file mode 100644
index 0000000..132c444
--- /dev/null
+++ b/Assets/Scripts/PlayerRole.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4c3a46956c5cf5446ab2698d8da6b2d8
\ No newline at end of file