M1: player stand + control handoff + Cinemachine camera system
- Install Cinemachine 3.1.7 - Stand (raised platform behind north wall) with StandPoint_0/_1 - PlayerAvatar: seat-ownership drives the mode — hold a seat => teleport to your stand, freeze, refuse input (control transfers/locks to your tread); release => teleport back, resume on-foot. Resolves the WASD overlap off one flag. - CameraDirector on Main Camera: 4 Cinemachine vcams (AvatarFollow / RobotFollow / StandView / CinematicOrbit), Brain blends; V cycles; default auto-follows the seat flag (on-foot=Avatar, driving=Robot). Retires the old FollowCamera. - Verified in play: claim->teleport+freeze+RobotFollow, release->return+AvatarFollow Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e93e6e76a3
commit
e92be317f5
6 changed files with 490 additions and 25 deletions
|
|
@ -469,6 +469,118 @@ Transform:
|
|||
m_CorrespondingSourceObject: {fileID: 400000, guid: 463e9d66ec2b7ad428d27c80a2b2d1db, type: 3}
|
||||
m_PrefabInstance: {fileID: 67875927}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1 &101108170
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 101108171}
|
||||
- component: {fileID: 101108174}
|
||||
- component: {fileID: 101108173}
|
||||
- component: {fileID: 101108172}
|
||||
m_Layer: 0
|
||||
m_Name: Stand_Platform
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 2147483647
|
||||
m_IsActive: 1
|
||||
--- !u!4 &101108171
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 101108170}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 1.5, z: 0}
|
||||
m_LocalScale: {x: 12, y: 3, z: 5}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1532509717}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!23 &101108172
|
||||
MeshRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 101108170}
|
||||
m_Enabled: 1
|
||||
m_CastShadows: 1
|
||||
m_ReceiveShadows: 1
|
||||
m_DynamicOccludee: 1
|
||||
m_StaticShadowCaster: 0
|
||||
m_MotionVectors: 1
|
||||
m_LightProbeUsage: 1
|
||||
m_ReflectionProbeUsage: 1
|
||||
m_RayTracingMode: 2
|
||||
m_RayTraceProcedural: 0
|
||||
m_RayTracingAccelStructBuildFlagsOverride: 0
|
||||
m_RayTracingAccelStructBuildFlags: 1
|
||||
m_SmallMeshCulling: 1
|
||||
m_ForceMeshLod: -1
|
||||
m_MeshLodSelectionBias: 0
|
||||
m_RenderingLayerMask: 1
|
||||
m_RendererPriority: 0
|
||||
m_Materials:
|
||||
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
|
||||
m_StaticBatchInfo:
|
||||
firstSubMesh: 0
|
||||
subMeshCount: 0
|
||||
m_StaticBatchRoot: {fileID: 0}
|
||||
m_ProbeAnchor: {fileID: 0}
|
||||
m_LightProbeVolumeOverride: {fileID: 0}
|
||||
m_ScaleInLightmap: 1
|
||||
m_ReceiveGI: 1
|
||||
m_PreserveUVs: 1
|
||||
m_IgnoreNormalsForChartDetection: 0
|
||||
m_ImportantGI: 0
|
||||
m_StitchLightmapSeams: 1
|
||||
m_SelectedEditorRenderState: 3
|
||||
m_MinimumChartSize: 4
|
||||
m_AutoUVMaxDistance: 0.5
|
||||
m_AutoUVMaxAngle: 89
|
||||
m_LightmapParameters: {fileID: 0}
|
||||
m_GlobalIlluminationMeshLod: 0
|
||||
m_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_MaskInteraction: 0
|
||||
m_AdditionalVertexStreams: {fileID: 0}
|
||||
--- !u!65 &101108173
|
||||
BoxCollider:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 101108170}
|
||||
m_Material: {fileID: 0}
|
||||
m_IncludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_ExcludeLayers:
|
||||
serializedVersion: 2
|
||||
m_Bits: 0
|
||||
m_LayerOverridePriority: 0
|
||||
m_IsTrigger: 0
|
||||
m_ProvidesContacts: 0
|
||||
m_Enabled: 1
|
||||
serializedVersion: 3
|
||||
m_Size: {x: 1, y: 1, z: 1}
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
--- !u!33 &101108174
|
||||
MeshFilter:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 101108170}
|
||||
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
||||
--- !u!1001 &130342001
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -1372,6 +1484,37 @@ BoxCollider:
|
|||
serializedVersion: 3
|
||||
m_Size: {x: 150.5, y: 4, z: 0.5}
|
||||
m_Center: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &506345465
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 506345466}
|
||||
m_Layer: 0
|
||||
m_Name: StandPoint_1
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &506345466
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 506345465}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 1, z: 0, w: -0.00000004371139}
|
||||
m_LocalPosition: {x: 2.5, y: 3, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1532509717}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &562137767
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -1384,6 +1527,7 @@ GameObject:
|
|||
- component: {fileID: 562137769}
|
||||
- component: {fileID: 562137768}
|
||||
- component: {fileID: 562137771}
|
||||
- component: {fileID: 562137772}
|
||||
m_Layer: 0
|
||||
m_Name: Main Camera
|
||||
m_TagString: MainCamera
|
||||
|
|
@ -1483,6 +1627,24 @@ MonoBehaviour:
|
|||
positionSmoothTime: 0.25
|
||||
lookAtDegreesPerSecond: 180
|
||||
lookAtHeightOffset: 1
|
||||
--- !u!114 &562137772
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 562137767}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: cd21368c9a8b1064d913c375fb607c0b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Jankenbots.Prototype.CameraDirector
|
||||
avatarOffset: {x: 0, y: 4, z: -6}
|
||||
robotOffset: {x: 0, y: 8, z: -12}
|
||||
orbitRadius: 20
|
||||
orbitHeight: 9
|
||||
orbitSpeed: 18
|
||||
lookHeight: 1.2
|
||||
--- !u!1001 &579422633
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -3779,6 +3941,37 @@ Transform:
|
|||
m_CorrespondingSourceObject: {fileID: 400000, guid: 5161c2c79aa7dc44b8a812f624bf37d7, type: 3}
|
||||
m_PrefabInstance: {fileID: 893957807}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1 &903945605
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 903945606}
|
||||
m_Layer: 0
|
||||
m_Name: StandPoint_0
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &903945606
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 903945605}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 1, z: 0, w: -0.00000004371139}
|
||||
m_LocalPosition: {x: -2.5, y: 3, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1532509717}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1001 &912005198
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -6914,6 +7107,40 @@ Transform:
|
|||
m_CorrespondingSourceObject: {fileID: 400000, guid: 034ba005b2423b84ebc087e917744363, type: 3}
|
||||
m_PrefabInstance: {fileID: 1531216050}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1 &1532509716
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1532509717}
|
||||
m_Layer: 0
|
||||
m_Name: Stand
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1532509717
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1532509716}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 82}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 101108171}
|
||||
- {fileID: 903945606}
|
||||
- {fileID: 506345466}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1001 &1537562849
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -8460,3 +8687,4 @@ SceneRoots:
|
|||
- {fileID: 1378796740}
|
||||
- {fileID: 850707848}
|
||||
- {fileID: 326006527}
|
||||
- {fileID: 1532509717}
|
||||
|
|
|
|||
143
Assets/Scripts/Camera/CameraDirector.cs
Normal file
143
Assets/Scripts/Camera/CameraDirector.cs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
using Unity.Cinemachine;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace Jankenbots.Prototype
|
||||
{
|
||||
/// <summary>
|
||||
/// JANKENBOTS — client-local camera director. Owns four Cinemachine cameras and
|
||||
/// lets the local player cycle between them; the CinemachineBrain on this Camera
|
||||
/// blends smoothly on every switch.
|
||||
///
|
||||
/// • AvatarFollow — 3rd-person chase on YOUR character (on-foot).
|
||||
/// • RobotFollow — chase cam on the bot (driving; the robot-follow cam).
|
||||
/// • StandView — fixed view from your player stand, looking at the bot.
|
||||
/// • CinematicOrbit— slow auto-orbit of the bot (spectate / flavor).
|
||||
///
|
||||
/// The default mode auto-follows the control state: on foot → AvatarFollow, seated
|
||||
/// (driving) → RobotFollow — reading the SAME seat flag as the control handoff
|
||||
/// (<see cref="PlayerAvatar.IsSeatedLocal"/>). Press V to cycle manually; claiming
|
||||
/// or leaving a seat re-asserts the sensible default.
|
||||
///
|
||||
/// The vcams are intentionally "bare" (no procedural body/aim) — the director poses
|
||||
/// their transforms each LateUpdate from the live targets, and the Brain just blends
|
||||
/// between whichever is prioritised. Cameras are purely client-local, never networked.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Camera))]
|
||||
public class CameraDirector : MonoBehaviour
|
||||
{
|
||||
public enum Mode { AvatarFollow, RobotFollow, StandView, CinematicOrbit }
|
||||
|
||||
[Header("Framing")]
|
||||
public Vector3 avatarOffset = new Vector3(0f, 4f, -6f);
|
||||
public Vector3 robotOffset = new Vector3(0f, 8f, -12f);
|
||||
public float orbitRadius = 20f;
|
||||
public float orbitHeight = 9f;
|
||||
public float orbitSpeed = 18f; // deg/sec
|
||||
public float lookHeight = 1.2f; // aim a bit above the target pivot
|
||||
|
||||
Transform _bot, _standOverlook;
|
||||
CinemachineCamera _avatar, _robot, _stand, _orbit;
|
||||
CinemachineCamera[] _all;
|
||||
Mode _mode = Mode.AvatarFollow;
|
||||
bool _userOverride;
|
||||
bool _lastSeated;
|
||||
float _orbitAngle;
|
||||
PlayerAvatar _local;
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (GetComponent<CinemachineBrain>() == null) gameObject.AddComponent<CinemachineBrain>();
|
||||
// Retire the old bespoke bot-only follow cam if it's still on this camera.
|
||||
var legacy = GetComponent<FollowCamera>();
|
||||
if (legacy != null) legacy.enabled = false;
|
||||
|
||||
var b = GameObject.Find("Bot"); if (b != null) _bot = b.transform;
|
||||
var s = GameObject.Find("StandPoint_0"); if (s != null) _standOverlook = s.transform;
|
||||
|
||||
var holder = new GameObject("CameraRigs").transform;
|
||||
_avatar = Make(holder, "CM_AvatarFollow");
|
||||
_robot = Make(holder, "CM_RobotFollow");
|
||||
_stand = Make(holder, "CM_StandView");
|
||||
_orbit = Make(holder, "CM_CinematicOrbit");
|
||||
_all = new[] { _avatar, _robot, _stand, _orbit };
|
||||
Apply(_mode);
|
||||
}
|
||||
|
||||
CinemachineCamera Make(Transform parent, string name)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
go.transform.SetParent(parent, false);
|
||||
var vc = go.AddComponent<CinemachineCamera>();
|
||||
vc.Priority = 0;
|
||||
return vc;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (_local == null) _local = FindLocal();
|
||||
|
||||
var kb = Keyboard.current;
|
||||
if (kb != null && kb.vKey.wasPressedThisFrame)
|
||||
{
|
||||
_mode = (Mode)(((int)_mode + 1) % _all.Length);
|
||||
_userOverride = true;
|
||||
Apply(_mode);
|
||||
}
|
||||
|
||||
// A seat claim/leave re-asserts the sensible default (clears a manual override).
|
||||
if (_local != null && _local.IsSeatedLocal != _lastSeated)
|
||||
{
|
||||
_lastSeated = _local.IsSeatedLocal;
|
||||
_userOverride = false;
|
||||
}
|
||||
|
||||
if (!_userOverride && _local != null)
|
||||
{
|
||||
Mode want = _local.IsSeatedLocal ? Mode.RobotFollow : Mode.AvatarFollow;
|
||||
if (want != _mode) { _mode = want; Apply(_mode); }
|
||||
}
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
if (_local != null) PoseFollow(_avatar, _local.transform, avatarOffset);
|
||||
|
||||
if (_bot != null)
|
||||
{
|
||||
PoseFollow(_robot, _bot, robotOffset);
|
||||
|
||||
_orbitAngle += orbitSpeed * Time.deltaTime;
|
||||
Vector3 off = Quaternion.Euler(0f, _orbitAngle, 0f) * new Vector3(0f, orbitHeight, -orbitRadius);
|
||||
PoseFollow(_orbit, _bot, off);
|
||||
|
||||
if (_standOverlook != null)
|
||||
{
|
||||
_stand.transform.position = _standOverlook.position + Vector3.up * 1.5f;
|
||||
_stand.transform.rotation = Quaternion.LookRotation(
|
||||
(_bot.position + Vector3.up * lookHeight) - _stand.transform.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PoseFollow(CinemachineCamera vc, Transform target, Vector3 worldOffset)
|
||||
{
|
||||
Vector3 pos = target.position + worldOffset;
|
||||
vc.transform.position = pos;
|
||||
vc.transform.rotation = Quaternion.LookRotation((target.position + Vector3.up * lookHeight) - pos);
|
||||
}
|
||||
|
||||
PlayerAvatar FindLocal()
|
||||
{
|
||||
foreach (var a in FindObjectsByType<PlayerAvatar>(FindObjectsSortMode.None))
|
||||
if (a.IsOwner) return a;
|
||||
return null;
|
||||
}
|
||||
|
||||
void Apply(Mode m)
|
||||
{
|
||||
for (int i = 0; i < _all.Length; i++)
|
||||
_all[i].Priority = ((int)m == i) ? 20 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Camera/CameraDirector.cs.meta
Normal file
2
Assets/Scripts/Camera/CameraDirector.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: cd21368c9a8b1064d913c375fb607c0b
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using Unity.Netcode;
|
||||
using Unity.Netcode.Components;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
|
|
@ -6,25 +7,24 @@ 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.
|
||||
/// character), PLUS the on-foot ↔ driving control handoff.
|
||||
///
|
||||
/// 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.
|
||||
/// CONTROL MODEL (the spine): a player is in exactly one mode, derived from a
|
||||
/// single fact — do I currently hold a bot seat? (<see cref="SeatManager"/>).
|
||||
/// • ON FOOT (no seat) → WASD drives THIS avatar; free-roaming.
|
||||
/// • DRIVING (holds a seat)→ avatar TELEPORTS to its player stand and FREEZES;
|
||||
/// it refuses all input, so control transfers and
|
||||
/// locks to the claimed tread (TreadPart already only
|
||||
/// listens when you hold its seat). Release the seat →
|
||||
/// teleport back and resume on-foot control.
|
||||
/// This is what resolves the WASD overlap: PlayerAvatar and TreadPart become
|
||||
/// mutually exclusive off one flag — never both live at once.
|
||||
///
|
||||
/// 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.
|
||||
/// NETCODE: OWNER-authoritative movement (see <see cref="OwnerAuthNetworkTransform"/>).
|
||||
/// Teleports use NetworkTransform.Teleport so observers don't see a slide.
|
||||
///
|
||||
/// 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.
|
||||
/// ANIMATION: derived locally on every client from measured horizontal speed →
|
||||
/// Animator "Speed" → Idle/Walk/Run blend (stays in sync with no extra traffic).
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(CharacterController))]
|
||||
public class PlayerAvatar : NetworkBehaviour
|
||||
|
|
@ -45,28 +45,41 @@ namespace Jankenbots.Prototype
|
|||
[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;
|
||||
NetworkTransform _nt;
|
||||
SeatManager _seats;
|
||||
Transform _standLeft, _standRight; // where each pilot teleports (found by name)
|
||||
|
||||
float _vy; // vertical velocity (gravity)
|
||||
Vector3 _horizVel; // smoothed horizontal velocity (ramped, not instant)
|
||||
Vector3 _lastPos;
|
||||
float _animSpeed;
|
||||
|
||||
bool _seated; // am I (owner) currently piloting a tread?
|
||||
Vector3 _returnPos; // where to drop me back when I leave the seat
|
||||
bool _hasReturn;
|
||||
|
||||
static readonly int SpeedHash = Animator.StringToHash("Speed");
|
||||
|
||||
/// <summary>True while the LOCAL owner is seated/driving — read by the camera director.</summary>
|
||||
public bool IsSeatedLocal => _seated;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_cc = GetComponent<CharacterController>();
|
||||
_anim = GetComponentInChildren<Animator>();
|
||||
_nt = GetComponent<NetworkTransform>();
|
||||
_lastPos = transform.position;
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
_lastPos = transform.position;
|
||||
_seats = FindFirstObjectByType<SeatManager>();
|
||||
var sl = GameObject.Find("StandPoint_0"); if (sl != null) _standLeft = sl.transform;
|
||||
var sr = GameObject.Find("StandPoint_1"); if (sr != null) _standRight = sr.transform;
|
||||
|
||||
// Fan owners out a little so multiple players don't stack on the origin.
|
||||
if (IsOwner)
|
||||
{
|
||||
|
|
@ -80,7 +93,20 @@ namespace Jankenbots.Prototype
|
|||
{
|
||||
if (!IsSpawned) return;
|
||||
|
||||
if (IsOwner) OwnerMove();
|
||||
if (IsOwner)
|
||||
{
|
||||
// Derive mode from seat ownership and handle the transition.
|
||||
SeatManager.Seat? seat = LocalSeat();
|
||||
bool wantSeated = seat.HasValue;
|
||||
if (wantSeated != _seated)
|
||||
{
|
||||
if (wantSeated) EnterSeated(seat.Value);
|
||||
else ExitSeated();
|
||||
}
|
||||
|
||||
// On foot → drive the avatar. Seated → do nothing (control is on the tread).
|
||||
if (!_seated) OwnerMove();
|
||||
}
|
||||
|
||||
// Animation on EVERY client, from measured horizontal speed.
|
||||
Vector3 delta = transform.position - _lastPos;
|
||||
|
|
@ -91,6 +117,46 @@ namespace Jankenbots.Prototype
|
|||
if (_anim != null) _anim.SetFloat(SpeedHash, _animSpeed);
|
||||
}
|
||||
|
||||
// Which seat (if any) the local player holds — the single source of truth for mode.
|
||||
SeatManager.Seat? LocalSeat()
|
||||
{
|
||||
if (_seats == null || NetworkManager.Singleton == null) return null;
|
||||
ulong me = NetworkManager.Singleton.LocalClientId;
|
||||
if (_seats.PilotOf(SeatManager.Seat.Left) == me) return SeatManager.Seat.Left;
|
||||
if (_seats.PilotOf(SeatManager.Seat.Right) == me) return SeatManager.Seat.Right;
|
||||
return null;
|
||||
}
|
||||
|
||||
void EnterSeated(SeatManager.Seat seat)
|
||||
{
|
||||
_seated = true;
|
||||
_returnPos = transform.position; // remember where I left the arena
|
||||
_hasReturn = true;
|
||||
_horizVel = Vector3.zero; _vy = 0f;
|
||||
|
||||
Transform sp = seat == SeatManager.Seat.Left ? _standLeft : _standRight;
|
||||
if (sp != null) TeleportTo(sp.position, sp.rotation);
|
||||
}
|
||||
|
||||
void ExitSeated()
|
||||
{
|
||||
_seated = false;
|
||||
_horizVel = Vector3.zero; _vy = 0f;
|
||||
if (_hasReturn) TeleportTo(_returnPos + Vector3.up * 0.2f, transform.rotation);
|
||||
}
|
||||
|
||||
// Interpolation-free teleport (CharacterController must be off to move it directly;
|
||||
// NetworkTransform.Teleport keeps observers from seeing a slide across the map).
|
||||
void TeleportTo(Vector3 pos, Quaternion rot)
|
||||
{
|
||||
bool ccWas = _cc.enabled;
|
||||
_cc.enabled = false;
|
||||
transform.SetPositionAndRotation(pos, rot);
|
||||
_cc.enabled = ccWas;
|
||||
_lastPos = pos;
|
||||
if (_nt != null) _nt.Teleport(pos, rot, transform.localScale);
|
||||
}
|
||||
|
||||
void OwnerMove()
|
||||
{
|
||||
var kb = Keyboard.current;
|
||||
|
|
@ -107,8 +173,7 @@ namespace Jankenbots.Prototype
|
|||
if (input.sqrMagnitude > 1f) input.Normalize();
|
||||
Vector3 desired = input * moveSpeed;
|
||||
|
||||
// Ramp velocity toward the target instead of snapping — this is what kills
|
||||
// the "jerky/jarring" feel and lets the anim blend Idle→Walk→Run smoothly.
|
||||
// Ramp velocity toward the target instead of snapping (smooth start/stop).
|
||||
float rate = input.sqrMagnitude > 0.001f ? acceleration : deceleration;
|
||||
_horizVel = Vector3.MoveTowards(_horizVel, desired, rate * Time.deltaTime);
|
||||
|
||||
|
|
@ -117,8 +182,6 @@ namespace Jankenbots.Prototype
|
|||
|
||||
_cc.Move((_horizVel + new Vector3(0f, _vy, 0f)) * Time.deltaTime);
|
||||
|
||||
// Face the way we're actually moving (uses the smoothed velocity, so turns
|
||||
// ease too rather than snapping on a key change).
|
||||
if (_horizVel.sqrMagnitude > 0.05f)
|
||||
{
|
||||
Quaternion target = Quaternion.LookRotation(new Vector3(_horizVel.x, 0f, _horizVel.z));
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"dependencies": {
|
||||
"com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#main",
|
||||
"com.unity.ai.navigation": "2.0.13",
|
||||
"com.unity.cinemachine": "3.1.7",
|
||||
"com.unity.collab-proxy": "2.12.4",
|
||||
"com.unity.ide.rider": "3.0.38",
|
||||
"com.unity.ide.visualstudio": "2.0.26",
|
||||
|
|
|
|||
|
|
@ -36,6 +36,16 @@
|
|||
},
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.cinemachine": {
|
||||
"version": "3.1.7",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
"com.unity.splines": "2.0.0",
|
||||
"com.unity.modules.imgui": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.collab-proxy": {
|
||||
"version": "2.12.4",
|
||||
"depth": 0,
|
||||
|
|
@ -242,6 +252,13 @@
|
|||
},
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.settings-manager": {
|
||||
"version": "2.1.1",
|
||||
"depth": 2,
|
||||
"source": "registry",
|
||||
"dependencies": {},
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.shadergraph": {
|
||||
"version": "17.5.0",
|
||||
"depth": 1,
|
||||
|
|
@ -251,6 +268,17 @@
|
|||
"com.unity.searcher": "4.9.3"
|
||||
}
|
||||
},
|
||||
"com.unity.splines": {
|
||||
"version": "2.9.0",
|
||||
"depth": 1,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
"com.unity.mathematics": "1.2.1",
|
||||
"com.unity.modules.imgui": "1.0.0",
|
||||
"com.unity.settings-manager": "1.0.3"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.test-framework": {
|
||||
"version": "1.7.0",
|
||||
"depth": 0,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue