jankenbots/Assets/Scripts/Net/PlayerDriver.cs
megaproxy 351a7f1f82 M1: add NGO + Multiplayer Services packages and prototype scripts (compiling)
Netcode for GameObjects 2.13.0 + Unity Multiplayer Services 2.2.4 (unified
Relay/Lobby SDK; supersedes deprecated standalone relay/lobby). Four M1
scripts, verified against live editor reflection and compiling clean:
- Net/NetworkBootstrap.cs  Relay host/join-by-code harness (IMGUI)
- Net/PlayerDriver.cs       replicated host-auth capsule (input->ServerRpc->host)
- Bot/TreadPart.cs          v0.1 tread feel: snap-to-cruise/lock-pivot/lean
- Bot/SeatManager.cs        left/right tread seat claim + leaver-safety

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:24:47 +01:00

152 lines
8.5 KiB
C#

// 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;
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<Rigidbody>();
}
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: legacy Input axes are perfectly fine here. WASD / arrows
// / left stick map to Horizontal + Vertical out of the box. (The project ships the new
// Input System, but its compatibility mode feeds these same axes, so this Just Works
// for M1 without authoring an action asset.)
float strafe = Input.GetAxisRaw("Horizontal"); // A/D, left-stick X
float fwd = Input.GetAxisRaw("Vertical"); // W/S, left-stick Y
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);
}
}
}
}