From e92be317f51490bf398344a1d4d75f7345e701bd Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sun, 12 Jul 2026 20:44:18 +0100 Subject: [PATCH] M1: player stand + control handoff + Cinemachine camera system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- Assets/Scenes/M1_Arena.unity | 228 +++++++++++++++++++ Assets/Scripts/Camera/CameraDirector.cs | 143 ++++++++++++ Assets/Scripts/Camera/CameraDirector.cs.meta | 2 + Assets/Scripts/Player/PlayerAvatar.cs | 113 +++++++-- Packages/manifest.json | 1 + Packages/packages-lock.json | 28 +++ 6 files changed, 490 insertions(+), 25 deletions(-) create mode 100644 Assets/Scripts/Camera/CameraDirector.cs create mode 100644 Assets/Scripts/Camera/CameraDirector.cs.meta diff --git a/Assets/Scenes/M1_Arena.unity b/Assets/Scenes/M1_Arena.unity index a9f7918..2983be5 100644 --- a/Assets/Scenes/M1_Arena.unity +++ b/Assets/Scenes/M1_Arena.unity @@ -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} diff --git a/Assets/Scripts/Camera/CameraDirector.cs b/Assets/Scripts/Camera/CameraDirector.cs new file mode 100644 index 0000000..ecc6e50 --- /dev/null +++ b/Assets/Scripts/Camera/CameraDirector.cs @@ -0,0 +1,143 @@ +using Unity.Cinemachine; +using UnityEngine; +using UnityEngine.InputSystem; + +namespace Jankenbots.Prototype +{ + /// + /// 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 + /// (). 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. + /// + [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() == null) gameObject.AddComponent(); + // Retire the old bespoke bot-only follow cam if it's still on this camera. + var legacy = GetComponent(); + 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(); + 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(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; + } + } +} diff --git a/Assets/Scripts/Camera/CameraDirector.cs.meta b/Assets/Scripts/Camera/CameraDirector.cs.meta new file mode 100644 index 0000000..1e54c67 --- /dev/null +++ b/Assets/Scripts/Camera/CameraDirector.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cd21368c9a8b1064d913c375fb607c0b \ No newline at end of file diff --git a/Assets/Scripts/Player/PlayerAvatar.cs b/Assets/Scripts/Player/PlayerAvatar.cs index 6c9eaaf..097acd6 100644 --- a/Assets/Scripts/Player/PlayerAvatar.cs +++ b/Assets/Scripts/Player/PlayerAvatar.cs @@ -1,4 +1,5 @@ using Unity.Netcode; +using Unity.Netcode.Components; using UnityEngine; using UnityEngine.InputSystem; @@ -6,25 +7,24 @@ 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. + /// character), PLUS the on-foot ↔ driving control handoff. /// - /// 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. + /// CONTROL MODEL (the spine): a player is in exactly one mode, derived from a + /// single fact — do I currently hold a bot seat? (). + /// • 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 ). + /// 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). /// [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"); + /// True while the LOCAL owner is seated/driving — read by the camera director. + public bool IsSeatedLocal => _seated; + void Awake() { _cc = GetComponent(); _anim = GetComponentInChildren(); + _nt = GetComponent(); _lastPos = transform.position; } public override void OnNetworkSpawn() { _lastPos = transform.position; + _seats = FindFirstObjectByType(); + 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)); diff --git a/Packages/manifest.json b/Packages/manifest.json index 8eb26be..d4e8afd 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -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", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 5639ded..67e0777 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -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,