// PlayerDriver.cs — JANKENBOTS Milestone 1, step 2: the replicated player-driven capsule. // // WHAT THIS IS (feel over fidelity — throwaway prototype code): // A single free-roaming capsule that one player owns and drives around the empty // networked scene. It exists purely to prove out the M1 netcode loop before we bolt // on the shared bot: "owning client reads input -> host simulates physics -> NGO // replicates the transform to everyone." If this capsule slides around smoothly on // a friend's screen over Relay, the host-authoritative pipeline works. // // HOST-AUTHORITATIVE, NO PREDICTION (decided for this throwaway only): // The host owns the Rigidbody and is the ONLY peer that applies force. The owning // client never moves itself locally — it just ships its input up to the server every // FixedUpdate via an [Rpc(SendTo.Server)]. The server caches that input and, in its // own FixedUpdate, pushes the Rigidbody. A server-authoritative NetworkTransform then // streams the resulting position/rotation back down to all clients. Because there's // no client-side prediction, the local player feels one round-trip of lag on their own // capsule — that's fine and expected for M1; we're testing coordination feel, not twitch. // // REQUIRED PREFAB SETUP (assumed wired in the Editor — this script does NOT add them): // * Rigidbody — the simulated body. Only the host actually integrates it; // NetworkRigidbody/NetworkTransform force isKinematic=true on // client copies, so DON'T touch isKinematic yourself. // * NetworkObject — makes it a spawnable networked entity; register the prefab in // NetworkManager -> Network Prefabs (or set it as the PlayerPrefab). // * NetworkTransform — server-authoritative by default (leave Interpolate = true). // This is what actually replicates our host-driven motion to clients. // (A NetworkRigidbody is optional for a slow capsule; add one — with UseRigidBodyForMotion // for the fast shove-ball later — if the plain NetworkTransform sync looks jittery.) // // Unity 6.x notes baked in below: Rigidbody.linearVelocity (was .velocity); RPC method // names MUST end in "Rpc"; physics is gated behind IsServer and only ever runs in FixedUpdate. using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; namespace Jankenbots.Net { [RequireComponent(typeof(Rigidbody))] public class PlayerDriver : NetworkBehaviour { [Header("Drive feel (host applies these as literal forces)")] [Tooltip("Force pushing the capsule along the input direction, in Newtons-ish. Tune for a floaty, cartoony glide — this is a party game, not a sim.")] [SerializeField] float moveForce = 40f; [Tooltip("Extra damping the host applies when there's no input, so the capsule coasts to a stop instead of drifting forever on the frictionless-ish floor.")] [SerializeField] float idleDamping = 4f; [Tooltip("Clamp on horizontal speed so a friend can't rocket the capsule off the arena.")] [SerializeField] float maxSpeed = 8f; // The Rigidbody we drive. Present on every copy, but only the HOST integrates it. Rigidbody rb; // --- Host-side input cache ------------------------------------------------------- // The owning client streams its raw stick/keys into these fields via the ServerRpc. // The host reads them in FixedUpdate. On client copies these just sit unused. // Stored as an already-normalized planar direction (x = strafe, z = forward). Vector2 cachedInput; void Awake() { rb = GetComponent(); } public override void OnNetworkSpawn() { // Belt-and-suspenders: physics must only be integrated on the authority (host). // NetworkTransform/NetworkRigidbody normally kinematic-lock client copies for us, // but if this capsule is used with a bare NetworkTransform we make doubly sure the // non-server copies never fight the replicated transform with their own gravity. if (!IsServer) rb.isKinematic = true; } // ------------------------------------------------------------------------------------- // OWNING CLIENT: read input every frame and ship it to the host. // We sample in Update (once per rendered frame — cheap, responsive) and send the RPC. // The host applies it on its own FixedUpdate clock, so exact send cadence doesn't matter. // ------------------------------------------------------------------------------------- void Update() { // Only the player who OWNS this capsule may drive it. Everyone else (including the // host, for capsules it doesn't own) skips input entirely — IsOwner is the gate. if (!IsOwner) return; // Throwaway-prototype input via the new Input System. The project ships New-only, so // legacy UnityEngine.Input.GetAxis would THROW at runtime — read WASD / arrow keys // straight off the current keyboard instead. No action asset needed for an M1 smoke test. var kb = Keyboard.current; float strafe = 0f, fwd = 0f; if (kb != null) { if (kb.aKey.isPressed || kb.leftArrowKey.isPressed) strafe -= 1f; // left if (kb.dKey.isPressed || kb.rightArrowKey.isPressed) strafe += 1f; // right if (kb.wKey.isPressed || kb.upArrowKey.isPressed) fwd += 1f; // forward if (kb.sKey.isPressed || kb.downArrowKey.isPressed) fwd -= 1f; // back } Vector2 input = new Vector2(strafe, fwd); // Normalize so diagonal input isn't ~1.4x faster; keep magnitude for analog sticks. if (input.sqrMagnitude > 1f) input.Normalize(); // Fire the input up to the server. Sending every frame (even zero) is fine for a // handful of M1 players — it keeps the host's cache fresh and lets it damp to a stop. SubmitMoveInputRpc(input); } // ------------------------------------------------------------------------------------- // CLIENT -> HOST input channel. [Rpc(SendTo.Server)] is the NGO 6.x universal RPC form; // the method name MUST end in "Rpc". RequireOwnership defaults to true, which is exactly // what we want: only this capsule's owner may push input into it. // ------------------------------------------------------------------------------------- [Rpc(SendTo.Server)] void SubmitMoveInputRpc(Vector2 input, RpcParams _ = default) { // Runs on the host. Just cache — never apply force here. All physics happens in the // host's FixedUpdate so it's on the fixed timestep the Rigidbody is integrated on. cachedInput = input; } // ------------------------------------------------------------------------------------- // HOST-ONLY physics. Guarded by IsServer so client copies never integrate — they only // receive the replicated NetworkTransform. Everything here runs on the fixed timestep. // ------------------------------------------------------------------------------------- void FixedUpdate() { if (!IsServer) return; // Turn the cached 2D input into a world-space push on the XZ plane. We drive in world // axes (not capsule-local) so a capsule with no meaningful facing still moves intuitively // relative to the arena — good enough for the M1 "does it replicate?" test. Vector3 dir = new Vector3(cachedInput.x, 0f, cachedInput.y); if (dir.sqrMagnitude > 0.0001f) { rb.AddForce(dir * moveForce, ForceMode.Force); } else { // No input: bleed off horizontal drift so the capsule settles. Cheap manual damping // on the planar velocity only (leave gravity on Y alone). Vector3 v = rb.linearVelocity; // Unity 6: .linearVelocity (was .velocity) Vector3 planar = new Vector3(v.x, 0f, v.z); planar = Vector3.MoveTowards(planar, Vector3.zero, idleDamping * Time.fixedDeltaTime); rb.linearVelocity = new Vector3(planar.x, v.y, planar.z); } // Clamp planar speed so nobody launches the capsule out of the arena. Vector3 vel = rb.linearVelocity; Vector3 flat = new Vector3(vel.x, 0f, vel.z); if (flat.magnitude > maxSpeed) { flat = flat.normalized * maxSpeed; rb.linearVelocity = new Vector3(flat.x, vel.y, flat.z); } } } }