using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; namespace Jankenbots.Prototype { /// /// JANKENBOTS — networked run-around PLAYER avatar (the rigged low-poly /// character). This is the foundation for the timed junkyard "gather parts" /// phase: each player controls one of these, runs around, and (later) hauls /// scrap back to build the bot. Driving the finished bot happens from player /// stands, so the avatar and the tread controls are separate systems. /// /// NETCODE: OWNER-authoritative. Only the owning client reads input and moves /// its CharacterController; an on the /// same object replicates the resulting motion to everyone. No host round-trip /// on your own movement → it feels responsive. /// /// ANIMATION: derived LOCALLY on every client from the avatar's measured /// horizontal speed (position delta / dt), fed into the Animator's "Speed" /// float, which drives an Idle→Walk→Run 1-D blend tree. Because observers /// measure the same replicated motion the owner produces, the animation stays /// in sync with zero extra network traffic — no NetworkAnimator needed. /// /// NOTE: reads WASD, same keys as the bot's TreadPart — fine while testing the /// avatar in isolation; the real game separates "running" from "driving" via /// the (future) player-stand flow, so a player is never doing both at once. /// [RequireComponent(typeof(CharacterController))] public class PlayerAvatar : NetworkBehaviour { [Header("Movement")] [Tooltip("Run speed in m/s.")] public float moveSpeed = 5f; [Tooltip("How fast the avatar turns to face its movement direction (deg/s).")] public float turnSpeed = 720f; [Tooltip("Gravity (m/s^2, negative). Keeps the CharacterController grounded.")] public float gravity = -20f; [Header("Animation")] [Tooltip("Speed value the Animator treats as full-run, for normalising the blend.")] public float runAnimSpeed = 5f; CharacterController _cc; Animator _anim; float _vy; // vertical velocity (gravity) Vector3 _lastPos; float _animSpeed; static readonly int SpeedHash = Animator.StringToHash("Speed"); void Awake() { _cc = GetComponent(); _anim = GetComponentInChildren(); _lastPos = transform.position; } public override void OnNetworkSpawn() { _lastPos = transform.position; // Fan owners out a little so multiple players don't stack on the origin. if (IsOwner) { float a = OwnerClientId * 1.3f; transform.position += new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a)) * (2f + OwnerClientId); _lastPos = transform.position; } } void Update() { if (!IsSpawned) return; if (IsOwner) OwnerMove(); // Animation on EVERY client, from measured horizontal speed. Vector3 delta = transform.position - _lastPos; delta.y = 0f; float measured = delta.magnitude / Mathf.Max(Time.deltaTime, 1e-4f); _lastPos = transform.position; _animSpeed = Mathf.Lerp(_animSpeed, measured, 12f * Time.deltaTime); if (_anim != null) _anim.SetFloat(SpeedHash, _animSpeed); } void OwnerMove() { var kb = Keyboard.current; float h = 0f, v = 0f; if (kb != null) { if (kb.aKey.isPressed || kb.leftArrowKey.isPressed) h -= 1f; if (kb.dKey.isPressed || kb.rightArrowKey.isPressed) h += 1f; if (kb.wKey.isPressed || kb.upArrowKey.isPressed) v += 1f; if (kb.sKey.isPressed || kb.downArrowKey.isPressed) v -= 1f; } Vector3 input = new Vector3(h, 0f, v); if (input.sqrMagnitude > 1f) input.Normalize(); Vector3 move = input * moveSpeed; if (_cc.isGrounded && _vy < 0f) _vy = -2f; _vy += gravity * Time.deltaTime; _cc.Move((move + new Vector3(0f, _vy, 0f)) * Time.deltaTime); if (move.sqrMagnitude > 0.01f) { Quaternion target = Quaternion.LookRotation(new Vector3(move.x, 0f, move.z)); transform.rotation = Quaternion.RotateTowards(transform.rotation, target, turnSpeed * Time.deltaTime); } } } }