M1: add networked rigged player avatar (run-around character)
- Import Hodaart Low Poly Character Collection 3 (humanoid-rigged, LFS) - PlayerAvatar.cs: owner-authoritative CharacterController movement, animation derived from measured speed on every client (no NetworkAnimator needed) - OwnerAuthNetworkTransform.cs: owner-authoritative NetworkTransform - PlayerLocomotion.controller: Idle/Walk/Run 1D blend tree on Speed (reuses Hodaart clips) - PlayerCharacter.prefab (Character 01 + CC + NetworkObject + owner NT + PlayerAvatar + Animator w/ humanoid avatar); registered as PlayerPrefab - Verified in play: spawns, visible, Idle pose (rig animates), owner-auth Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0306045697
commit
beb17eacdf
141 changed files with 287727 additions and 1 deletions
8
Assets/Scripts/Player.meta
Normal file
8
Assets/Scripts/Player.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 362506bdd55ca664c925adc3f1b19a38
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
18
Assets/Scripts/Player/OwnerAuthNetworkTransform.cs
Normal file
18
Assets/Scripts/Player/OwnerAuthNetworkTransform.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using Unity.Netcode.Components;
|
||||
|
||||
namespace Jankenbots.Prototype
|
||||
{
|
||||
/// <summary>
|
||||
/// Owner-authoritative NetworkTransform. The default NetworkTransform is
|
||||
/// SERVER-authoritative (right for the host-simulated bot/ball). For a
|
||||
/// run-around PLAYER avatar we want the owning client to be the source of
|
||||
/// truth so their own movement is lag-free — the owner writes its transform
|
||||
/// and NGO replicates it to everyone else. This is the canonical NGO pattern
|
||||
/// (override OnIsServerAuthoritative → false).
|
||||
/// </summary>
|
||||
[UnityEngine.DisallowMultipleComponent]
|
||||
public class OwnerAuthNetworkTransform : NetworkTransform
|
||||
{
|
||||
protected override bool OnIsServerAuthoritative() => false;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player/OwnerAuthNetworkTransform.cs.meta
Normal file
2
Assets/Scripts/Player/OwnerAuthNetworkTransform.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 46734e697cca0e1448ddd4dcac81ebd2
|
||||
115
Assets/Scripts/Player/PlayerAvatar.cs
Normal file
115
Assets/Scripts/Player/PlayerAvatar.cs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace Jankenbots.Prototype
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="OwnerAuthNetworkTransform"/> 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.
|
||||
/// </summary>
|
||||
[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<CharacterController>();
|
||||
_anim = GetComponentInChildren<Animator>();
|
||||
_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player/PlayerAvatar.cs.meta
Normal file
2
Assets/Scripts/Player/PlayerAvatar.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ea986826a02cd05489ba9c17036c4a0d
|
||||
Loading…
Add table
Add a link
Reference in a new issue