From 9f49d4546545299be291ef4452baf8dee65dc432 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sat, 11 Jul 2026 00:13:41 +0100 Subject: [PATCH] M1: build shared 2-tread bot; rewire TreadPart to seat-based gating - Rewrote TreadPart: seat-based input gating (was mis-wired to NetworkObject IsOwner, which needs per-tread ownership that NGO forbids for nested NetworkObjects). Both treads now share the chassis NetworkObject; a client drives the tread whose SeatManager seat it holds, host re-validates OwnsSeat on a RequireOwnership=false RPC. Also swapped to Keyboard.current (New input). - Moved SeatManager OnGUI below the connectivity panel (was overlapping). - M1_Arena: in-scene Bot NetworkObject = chassis (Rigidbody 60kg + NetworkObject + NetworkTransform + NetworkRigidbody + SeatManager) + Tread_L/Tread_R children (TreadPart, world offset +/-1.2 X for differential drive). - Solo-verified: bot auto-spawns in-scene, seat claim + input gating wired. Controls: W/Up throttle, Shift pivot, A/D lean. Co-Authored-By: Claude Opus 4.8 (1M context) --- Assets/Scenes/M1_Arena.unity | 517 ++++++++++++++++++++++++++++++ Assets/Scripts/Bot/SeatManager.cs | 14 +- Assets/Scripts/Bot/TreadPart.cs | 205 ++++++------ memory.md | 2 + 4 files changed, 617 insertions(+), 121 deletions(-) diff --git a/Assets/Scenes/M1_Arena.unity b/Assets/Scenes/M1_Arena.unity index 8106a48..5280a85 100644 --- a/Assets/Scenes/M1_Arena.unity +++ b/Assets/Scenes/M1_Arena.unity @@ -338,6 +338,522 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &781174154 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 781174160} + - component: {fileID: 781174159} + - component: {fileID: 781174158} + - component: {fileID: 781174157} + - component: {fileID: 781174156} + - component: {fileID: 781174155} + - component: {fileID: 781174163} + - component: {fileID: 781174162} + - component: {fileID: 781174161} + m_Layer: 0 + m_Name: Bot + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &781174155 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 781174154} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject + GlobalObjectIdHash: 1509253913 + InScenePlacedSourceGlobalObjectIdHash: 0 + DeferredDespawnTick: 0 + Ownership: 1 + AlwaysReplicateAsRoot: 0 + SynchronizeTransform: 1 + k__BackingField: 0 + ActiveSceneSynchronization: 0 + SceneMigrationSynchronization: 0 + SpawnWithObservers: 1 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 + SyncOwnerTransformWhenParented: 1 + AllowOwnerToParent: 0 +--- !u!54 &781174156 +Rigidbody: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 781174154} + serializedVersion: 5 + m_Mass: 1 + m_LinearDamping: 0 + m_AngularDamping: 0.05 + m_CenterOfMass: {x: 0, y: 0, z: 0} + m_InertiaTensor: {x: 1, y: 1, z: 1} + m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ImplicitCom: 1 + m_ImplicitTensor: 1 + m_UseGravity: 1 + m_IsKinematic: 0 + m_Interpolate: 0 + m_Constraints: 0 + m_CollisionDetection: 0 +--- !u!23 &781174157 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 781174154} + 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 &781174158 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 781174154} + 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 &781174159 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 781174154} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!4 &781174160 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 781174154} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: 0} + m_LocalScale: {x: 2, y: 1, z: 3} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1147751743} + - {fileID: 1190106215} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &781174161 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 781174154} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3dd8122938f14804f9e4ef7e494fb2f6, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::Jankenbots.Prototype.SeatManager + ShowTopMostFoldoutHeaderGroup: 1 +--- !u!114 &781174162 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 781174154} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f6c0be61502bb534f922ebb746851216, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.Components.NetworkRigidbody + ShowTopMostFoldoutHeaderGroup: 1 + NetworkRigidbodyBaseExpanded: 0 + UseRigidBodyForMotion: 0 + AutoUpdateKinematicState: 1 + AutoSetKinematicOnDespawn: 1 +--- !u!114 &781174163 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 781174154} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e96cb6065543e43c4a752faaa1468eb1, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.Components.NetworkTransform + ShowTopMostFoldoutHeaderGroup: 1 + NetworkTransformExpanded: 0 + AutoOwnerAuthorityTickOffset: 1 + PositionInterpolationType: 0 + RotationInterpolationType: 0 + ScaleInterpolationType: 0 + PositionLerpSmoothing: 1 + PositionMaxInterpolationTime: 0.1 + RotationLerpSmoothing: 1 + RotationMaxInterpolationTime: 0.1 + ScaleLerpSmoothing: 1 + ScaleMaxInterpolationTime: 0.1 + AuthorityMode: 0 + TickSyncChildren: 0 + UseUnreliableDeltas: 0 + SyncPositionX: 1 + SyncPositionY: 1 + SyncPositionZ: 1 + SyncRotAngleX: 1 + SyncRotAngleY: 1 + SyncRotAngleZ: 1 + SyncScaleX: 1 + SyncScaleY: 1 + SyncScaleZ: 1 + PositionThreshold: 0.001 + RotAngleThreshold: 0.01 + ScaleThreshold: 0.01 + UseQuaternionSynchronization: 0 + UseQuaternionCompression: 0 + UseHalfFloatPrecision: 0 + InLocalSpace: 0 + SwitchTransformSpaceWhenParented: 0 + Interpolate: 1 + SlerpPosition: 0 +--- !u!1 &1147751742 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1147751743} + - component: {fileID: 1147751747} + - component: {fileID: 1147751746} + - component: {fileID: 1147751745} + - component: {fileID: 1147751744} + m_Layer: 0 + m_Name: Tread_L + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1147751743 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1147751742} + serializedVersion: 2 + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: -0.6, y: 0, z: 0} + m_LocalScale: {x: 0.2, y: 0.7, z: 0.9} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 781174160} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1147751744 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1147751742} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a07e6377a09865c4e8c2b59a637e7dd1, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::Jankenbots.Prototype.TreadPart + ShowTopMostFoldoutHeaderGroup: 1 + side: 0 + chassis: {fileID: 0} + cruiseForce: 1200 + throttleDeadzone: 0.08 + pivotAnchorStrength: 2500 + pivotAngularResistance: 400 + leanTorque: 800 + leanDeadzone: 0.08 +--- !u!23 &1147751745 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1147751742} + 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 &1147751746 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1147751742} + 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 &1147751747 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1147751742} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} +--- !u!1 &1190106214 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1190106215} + - component: {fileID: 1190106219} + - component: {fileID: 1190106218} + - component: {fileID: 1190106217} + - component: {fileID: 1190106216} + m_Layer: 0 + m_Name: Tread_R + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1190106215 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1190106214} + serializedVersion: 2 + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0.6, y: 0, z: 0} + m_LocalScale: {x: 0.2, y: 0.7, z: 0.9} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 781174160} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1190106216 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1190106214} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a07e6377a09865c4e8c2b59a637e7dd1, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::Jankenbots.Prototype.TreadPart + ShowTopMostFoldoutHeaderGroup: 1 + side: 1 + chassis: {fileID: 0} + cruiseForce: 1200 + throttleDeadzone: 0.08 + pivotAnchorStrength: 2500 + pivotAngularResistance: 400 + leanTorque: 800 + leanDeadzone: 0.08 +--- !u!23 &1190106217 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1190106214} + 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 &1190106218 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1190106214} + 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 &1190106219 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1190106214} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} --- !u!1 &1336178141 GameObject: m_ObjectHideFlags: 0 @@ -580,3 +1096,4 @@ SceneRoots: - {fileID: 562137770} - {fileID: 1336178145} - {fileID: 1571629442} + - {fileID: 781174160} diff --git a/Assets/Scripts/Bot/SeatManager.cs b/Assets/Scripts/Bot/SeatManager.cs index f34d1d8..433ad70 100644 --- a/Assets/Scripts/Bot/SeatManager.cs +++ b/Assets/Scripts/Bot/SeatManager.cs @@ -161,19 +161,21 @@ namespace Jankenbots.Prototype Seat? mine = LocalSeat(); string held = mine.HasValue ? mine.Value.ToString() : "none (spectator)"; - const int w = 260, h = 78; - GUI.Box(new Rect(10, 10, w, h), "JANKENBOTS · seat"); - GUI.Label(new Rect(20, 32, w - 20, 20), $"You hold: {held}"); - GUI.Label(new Rect(20, 52, w - 20, 20), + // Sits BELOW the NetworkBootstrap connectivity panel (which occupies the + // top-left ~260px) so the Claim button isn't hidden behind it. + const int x = 10, y = 300, w = 260, h = 78; + GUI.Box(new Rect(x, y, w, h), "JANKENBOTS · seat"); + GUI.Label(new Rect(x + 10, y + 22, w - 20, 20), $"You hold: {held}"); + GUI.Label(new Rect(x + 10, y + 42, w - 20, 20), $"Left: {SeatLabel(LeftPilot.Value)} Right: {SeatLabel(RightPilot.Value)}"); // Client-side ask/release buttons (they just fire the ServerRpc). if (!mine.HasValue) { - if (GUI.Button(new Rect(w - 70, 30, 55, 20), "Claim")) + if (GUI.Button(new Rect(x + w - 70, y + 20, 55, 20), "Claim")) ClaimSeatRpc(); } - else if (GUI.Button(new Rect(w - 70, 30, 55, 20), "Leave")) + else if (GUI.Button(new Rect(x + w - 70, y + 20, 55, 20), "Leave")) { ReleaseSeatRpc(); } diff --git a/Assets/Scripts/Bot/TreadPart.cs b/Assets/Scripts/Bot/TreadPart.cs index ca98c0c..5739538 100644 --- a/Assets/Scripts/Bot/TreadPart.cs +++ b/Assets/Scripts/Bot/TreadPart.cs @@ -1,5 +1,6 @@ using Unity.Netcode; using UnityEngine; +using UnityEngine.InputSystem; namespace Jankenbots.Prototype { @@ -11,31 +12,28 @@ namespace Jankenbots.Prototype /// clumsy body and try to make it go where they want together? /// /// ARCHITECTURE (host-authoritative, no prediction — see the M1 cheat-sheet): - /// * There is ONE simulated Rigidbody: the CHASSIS. Both treads are just - /// force-emitters that push on that shared body. No tread has its own - /// Rigidbody. Differential drive "emerges" because the two treads apply - /// their forces at different WORLD POSITIONS (left vs right of centre). - /// * The owning pilot's client only READS input and ships {throttle, pivotHeld, - /// lean} to the host via an RPC. It applies NOTHING locally. - /// * The host (IsServer) caches the latest input per tread and applies ALL - /// forces in FixedUpdate. NGO's NetworkRigidbody/NetworkTransform on the + /// * There is ONE simulated Rigidbody + ONE NetworkObject: the CHASSIS. Both + /// treads are plain CHILD GameObjects (no Rigidbody, no NetworkObject of their + /// own); each TreadPart is a NetworkBehaviour bound to the chassis' shared + /// NetworkObject. (NGO forbids nested NetworkObjects in a spawned prefab, so + /// per-tread ownership is impossible — we gate on the SEAT instead.) + /// * Differential drive "emerges" because the two treads apply their forces at + /// different WORLD POSITIONS (left vs right of centre). + /// * SEAT-BASED input gating (via ): a client only reads + /// & sends input for a tread whose seat it currently holds. The host re-checks + /// seat ownership before honoring any input RPC, so a client can't drive a tread + /// it doesn't pilot. + /// * The host (IsServer) caches the latest validated input per tread and applies + /// ALL forces in FixedUpdate. NGO's NetworkRigidbody/NetworkTransform on the /// chassis replicates the resulting motion back to everyone. /// /// FEEL MODEL v0.1 — three deliberately "janky" verbs, each a literal force: - /// (a) SNAP-TO-CRUISE THROTTLE — a tread does not have an analog gas pedal. - /// Push the stick fully forward and it commits to a fixed CRUISE force - /// (chunky, momentum-y, a bit out of your hands). Partial stick is - /// proportional so you CAN feather it, but the intent is "slam it to - /// cruise and live with the consequences" — that shared over-commitment - /// is where the comedy of coordination comes from. - /// (b) LOCK-PIVOT — hold a button and THIS tread plants itself as an anchor. - /// Its drive contribution is cancelled and we actively fight the chassis' - /// motion AT the tread's position, so the whole bot swings/rotates about - /// this tread like a pinned foot. Two pilots learn "you plant, I drive" - /// to turn on the spot. - /// (c) PASSIVE LEAN — this tall bot WANTS to tip over. Nudging the stick - /// sideways dumps a ballast torque along the drive axis to counter-roll, - /// a constant low-key balancing chore shared between pilots. + /// (a) SNAP-TO-CRUISE THROTTLE — slam forward → a fixed CRUISE force; partial is + /// proportional but the intent is over-commitment (that's the comedy). + /// (b) LOCK-PIVOT — hold a button and THIS tread plants as an anchor; the bot + /// swings about it like a pinned foot. + /// (c) PASSIVE LEAN — sideways stick dumps a ballast torque to counter-roll the + /// top-heavy bot; a constant shared balancing chore. /// /// Tuning fields are public so we can dial the feel live in the inspector while /// friends are playing. Numbers here are only sane starting points. @@ -43,14 +41,13 @@ namespace Jankenbots.Prototype [DisallowMultipleComponent] public class TreadPart : NetworkBehaviour { - // Which side of the bot this tread is. Purely descriptive for M1 (the actual - // left/right behaviour comes from where the tread SITS on the chassis, not - // from this enum) — but it's handy for seat-assignment logs and inspector - // sanity, and lets us bias per-side tuning later if we want asymmetry. + // Which side of the bot this tread is. Drives the SEAT it maps to (Left/Right) + // AND, because the tread physically SITS left/right of centre, the differential + // drive. Set per-tread in the inspector. public enum Side { Left, Right } [Header("Identity")] - [Tooltip("Which tread this is. Descriptive — real behaviour comes from world position on the chassis.")] + [Tooltip("Which tread this is → which SeatManager seat it maps to, and (via its world position) which side of the chassis it pushes.")] public Side side = Side.Left; [Header("Shared body")] @@ -59,75 +56,84 @@ namespace Jankenbots.Prototype // ---- (a) SNAP-TO-CRUISE THROTTLE tuning ---------------------------------- [Header("(a) Throttle — snap-to-cruise")] - [Tooltip("Force (Newtons) applied at full-forward stick. This is the 'cruise' the tread snaps to. Bigger = the bot lurches harder and is twic­e as hard to coordinate.")] + [Tooltip("Force (Newtons) applied at full-forward stick. This is the 'cruise' the tread snaps to.")] public float cruiseForce = 1200f; - [Tooltip("Below this stick magnitude we treat throttle as zero — kills drift/noise so a resting stick doesn't creep the bot.")] + [Tooltip("Below this stick magnitude we treat throttle as zero.")] [Range(0f, 0.5f)] public float throttleDeadzone = 0.08f; // ---- (b) LOCK-PIVOT tuning ----------------------------------------------- [Header("(b) Lock-pivot — plant this tread as an anchor")] - [Tooltip("How hard the planted tread resists the chassis sliding at its position. Higher = a crisper, more locked pivot; too high = the whole bot snaps rigidly and feels un-janky.")] + [Tooltip("How hard the planted tread resists the chassis sliding at its position.")] public float pivotAnchorStrength = 2500f; - [Tooltip("Extra angular damping (torque opposing spin) while planting. Keeps the pivot from becoming a wild spin — the planted foot should feel 'stuck', not greasy.")] + [Tooltip("Extra angular damping (torque opposing spin) while planting.")] public float pivotAngularResistance = 400f; // ---- (c) PASSIVE LEAN tuning --------------------------------------------- [Header("(c) Passive lean — anti-tip ballast")] - [Tooltip("Ballast torque (N·m) at full sideways stick, applied along the drive (forward) axis to counter-roll the tall bot. Tune vs how tippy the chassis is.")] + [Tooltip("Ballast torque (N·m) at full sideways stick, applied along the drive (forward) axis.")] public float leanTorque = 800f; - [Tooltip("Below this sideways magnitude, no lean torque — resting stick = no ballast.")] + [Tooltip("Below this sideways magnitude, no lean torque.")] [Range(0f, 0.5f)] public float leanDeadzone = 0.08f; + // The seat this tread maps to, resolved once from `side`. + SeatManager.Seat MySeat => side == Side.Left ? SeatManager.Seat.Left : SeatManager.Seat.Right; + + // Source of truth for who pilots which tread. Found on a parent (the chassis). + SeatManager _seats; + // -------------------------------------------------------------------------- - // HOST-SIDE cached input. These are written ONLY by the RPC (which only runs - // on the server) and read ONLY in FixedUpdate (also gated to server). We never - // touch them on a non-owning client, so no sync primitive is needed — the + // HOST-SIDE cached input. Written ONLY by the (validated) RPC on the server and + // read ONLY in FixedUpdate (also server-gated). No sync primitive needed — the // authoritative simulation is entirely host-local. // -------------------------------------------------------------------------- - float _throttle; // 0..1, already deadzoned/clamped by the sender - bool _pivotHeld; // is the pilot planting this tread right now? - float _lean; // -1..1 sideways ballast request + float _throttle; // 0..1 + bool _pivotHeld; + float _lean; // -1..1 void Awake() { - // Convenience: if nobody wired the chassis in the inspector, grab the - // Rigidbody off a parent. All treads should end up pointing at the SAME - // chassis Rigidbody — that shared reference is what makes it one bot. if (chassis == null) chassis = GetComponentInParent(); + _seats = GetComponentInParent(); } // ========================================================================== - // CLIENT: read local input, ship it to the host. Nothing is applied locally. - // Runs every frame on the owning pilot only. + // CLIENT: if the LOCAL player holds this tread's seat, read input and ship it + // to the host. Nothing is applied locally. Seat ownership — not NetworkObject + // ownership — is the gate, so each client self-selects the tread it pilots. // ========================================================================== void Update() { - if (!IsOwner) return; + if (!IsSpawned || _seats == null) return; + + ulong me = NetworkManager.Singleton.LocalClientId; + if (_seats.PilotOf(MySeat) != me) return; // I don't drive this tread float throttle = ReadThrottle(); // 0..1 bool pivot = ReadPivotHeld(); float lean = ReadLean(); // -1..1 - // One tiny packet per frame to the host. The host simulates; we watch the - // replicated chassis move. That round-trip "lag between my stick and the - // bot lurching" is itself part of the janky feel we're testing. SubmitTreadInputRpc(throttle, pivot, lean); } /// - /// Owning client → host. Named *Rpc + [Rpc(SendTo.Server)] per NGO 2.x. - /// RequireOwnership stays true (default): only the pilot who owns this tread - /// may drive it. The host just caches; forces are applied in FixedUpdate. + /// Owning pilot's client → host. RequireOwnership is FALSE (the bot's + /// NetworkObject is owned by the server, not the pilot) — instead the host + /// re-validates the SEAT so a client can only drive the tread it actually + /// pilots. The host just caches; forces are applied in FixedUpdate. /// - [Rpc(SendTo.Server)] - void SubmitTreadInputRpc(float throttle, bool pivotHeld, float lean, RpcParams _ = default) + [Rpc(SendTo.Server, RequireOwnership = false)] + void SubmitTreadInputRpc(float throttle, bool pivotHeld, float lean, RpcParams rpcParams = default) { + // Anti-spoof: only honor input from the client that currently holds this seat. + if (_seats == null || !_seats.OwnsSeat(MySeat, rpcParams.Receive.SenderClientId)) + return; + _throttle = Mathf.Clamp01(throttle); _pivotHeld = pivotHeld; _lean = Mathf.Clamp(lean, -1f, 1f); @@ -135,47 +141,38 @@ namespace Jankenbots.Prototype // ========================================================================== // HOST ONLY: turn the cached input into literal forces on the SHARED chassis. - // This is the entire physics of the bot. Two TreadParts running this in the - // same FixedUpdate, pushing at their two different world positions, ARE the - // differential drive. + // Two TreadParts running this in the same FixedUpdate, pushing at their two + // different world positions, ARE the differential drive. // ========================================================================== void FixedUpdate() { if (!IsServer) return; // authority guard — clients never simulate if (chassis == null) return; + // Un-piloted tread goes limp: no pilot → no cached force. (Also clears any + // stale input the instant a pilot leaves, so the bot doesn't coast on it.) + if (_seats == null || _seats.PilotOf(MySeat) == SeatManager.NoPilot) + { + _throttle = 0f; _pivotHeld = false; _lean = 0f; + return; + } + Vector3 treadPos = transform.position; // where THIS tread pushes from if (_pivotHeld) { // ---- (b) LOCK-PIVOT -------------------------------------------- - // The pilot has planted this tread. We do NOT drive with it; instead - // we make the chassis behave as if it's pinned at this tread's - // position, so the OTHER tread's thrust swings the whole bot around - // this point like a pivoting foot. - // - // 1) Cancel the sideways/linear slip AT the tread position by pushing - // back against the local velocity there. GetPointVelocity gives the - // chassis' velocity at this world point (includes rotation), so - // opposing it plants the point in space. + // Plant this tread: cancel linear slip at its position and bleed spin + // so the OTHER tread's thrust swings the bot about this point. Vector3 pointVel = chassis.GetPointVelocity(treadPos); chassis.AddForceAtPosition(-pointVel * pivotAnchorStrength, treadPos, ForceMode.Force); - - // 2) Bleed off raw spin so the pivot feels 'stuck', not greasy. This - // is a soft angular brake, NOT a hard lock — we still want jank. chassis.AddTorque(-chassis.angularVelocity * pivotAngularResistance, ForceMode.Force); - - // NOTE: no throttle drive while planting — a planted tread is an - // anchor, not a motor. (Lean is also skipped: you're busy pivoting.) - return; + return; // a planted tread is an anchor, not a motor } // ---- (a) SNAP-TO-CRUISE THROTTLE ----------------------------------- - // throttle is 0..1. Full stick == full cruiseForce (the "snap to cruise" - // commitment); partial stick scales it down so feathering is possible but - // not the point. Push along the tread's own forward so a mis-aligned / - // knocked-askew tread pushes the bot in a wonky direction — jank on - // purpose. Applied AT the tread's world position → left+right offset = + // Full stick == full cruiseForce; partial scales down. Applied AT the + // tread's world position along its own forward → left/right offset = // differential drive (asymmetric throttle turns the bot). if (_throttle > throttleDeadzone) { @@ -184,9 +181,6 @@ namespace Jankenbots.Prototype } // ---- (c) PASSIVE LEAN — anti-tip ballast --------------------------- - // Sideways stick shovels ballast torque along the drive (forward) axis to - // counter-roll the top-heavy bot. It's a constant balancing chore the - // pilots share; it does NOT steer (that's the throttle differential). if (Mathf.Abs(_lean) > leanDeadzone) { chassis.AddTorque(transform.forward * (_lean * leanTorque), ForceMode.Force); @@ -194,57 +188,38 @@ namespace Jankenbots.Prototype } // ========================================================================== - // INPUT READERS — placeholder wiring for M1. Swap for real Input System - // actions once seats are assigned; kept trivial so the physics is testable - // immediately. Only ever called on the owning client (inside Update's guard). + // INPUT READERS — new Input System (project is Input-System-New-only, so legacy + // UnityEngine.Input would throw). Same keys for both seats; the SEAT gate in + // Update() decides which tread a given client actually drives. Only called for + // the tread the local player pilots. Swap for per-seat gamepad actions later. + // Throttle = W / Up · Pivot = Left Shift · Lean = A/D or Left/Right // ========================================================================== - /// 0..1 throttle. Vertical axis, forward only (no reverse in v0.1). + /// 0..1 throttle. Forward only (no reverse in v0.1). float ReadThrottle() { - // Forward-only: negative stick = 0 throttle (reverse is a later feel test). - float v = Mathf.Max(0f, Input.GetAxisRaw("Vertical")); + var kb = Keyboard.current; + if (kb == null) return 0f; + float v = (kb.wKey.isPressed || kb.upArrowKey.isPressed) ? 1f : 0f; return v < throttleDeadzone ? 0f : v; } /// Is the plant-pivot button held? bool ReadPivotHeld() { - // Placeholder: left shift = plant. Real build: per-seat gamepad button. - return Input.GetKey(KeyCode.LeftShift); + var kb = Keyboard.current; + return kb != null && (kb.leftShiftKey.isPressed || kb.rightShiftKey.isPressed); } /// -1..1 sideways ballast request. float ReadLean() { - float h = Input.GetAxisRaw("Horizontal"); + var kb = Keyboard.current; + if (kb == null) return 0f; + float h = 0f; + if (kb.aKey.isPressed || kb.leftArrowKey.isPressed) h -= 1f; + if (kb.dKey.isPressed || kb.rightArrowKey.isPressed) h += 1f; return Mathf.Abs(h) < leanDeadzone ? 0f : h; } - - // ========================================================================== - // Seat lifecycle. The bot spawns owned by the server; the seat manager grants - // a tread to a pilot via NetworkObject.ChangeOwnership(clientId) (server-only, - // see cheat-sheet §8). These hooks just log so we can see claims land while - // testing with friends, and clear stale input if a pilot leaves. - // ========================================================================== - public override void OnGainedOwnership() - { - base.OnGainedOwnership(); - if (IsOwner) - Debug.Log($"[TreadPart] {side} tread claimed by local pilot (client {OwnerClientId})."); - } - - public override void OnLostOwnership() - { - base.OnLostOwnership(); - // On the host, wipe cached input so an un-piloted tread goes limp instead - // of coasting on the last pilot's stick. - if (IsServer) - { - _throttle = 0f; - _pivotHeld = false; - _lean = 0f; - } - } } } diff --git a/memory.md b/memory.md index de70126..87af83b 100644 --- a/memory.md +++ b/memory.md @@ -16,6 +16,8 @@ Durable memory for this project. Read at session start, update before session en ## Open questions / TODOs - [ ] **▶ M1 IN PROGRESS** (see `docs/prototype-plan.md`). Done so far (2026-07-10, all compiling clean + pushed): **packages** (NGO 2.13.0 + `com.unity.services.multiplayer` 2.2.4 — the *unified* SDK that supersedes the deprecated standalone relay/lobby packages; pulls transport 6.5.0 + core/auth), **4 scripts** in `Assets/Scripts/{Net,Bot}/`, **scene** `Assets/Scenes/M1_Arena.unity` (NetworkManager+UnityTransport+NetworkBootstrap, ground/cam/light, set as build scene), **replicated Player capsule prefab** (`Assets/Prefabs/Player.prefab`, registered as `NetworkConfig.PlayerPrefab`). + - **✅ CAPSULE REPLICATION VERIFIED (2026-07-11) — M1 netcode thesis proven.** Two clients over Relay (host = original editor via MCP, client = ParrelSync clone), both capsules spawned + drove independently + replicated **bidirectionally, smooth**. The `input→ServerRpc→host-physics→NetworkTransform` loop works over real Relay. **ParrelSync 1.5.2** installed for local 2-client testing (clone at `../jankenbots_clone_0`, symlinks Assets+ProjectSettings; open it via `ClonesManager.OpenProject` or the ParrelSync menu; its MCP loses the port fight so only the original is MCP-controllable). + - **✅ SHARED BOT BUILT (2026-07-11), solo-smoke-verified; awaiting 2-pilot drive test.** In-scene `Bot` NetworkObject in M1_Arena: chassis Cube (Rigidbody 60kg + NetworkObject + NetworkTransform + NetworkRigidbody + SeatManager) with two child treads `Tread_L`/`Tread_R` (TreadPart, side Left/Right) at world ±1.2 X. In-scene bot auto-spawns on host; seat claim + gating verified solo. **DESIGN FIX (important):** the two drafts were mis-wired — TreadPart gated on NetworkObject `IsOwner` (needs per-tread ownership → impossible, NGO forbids nested NetworkObjects in a spawned prefab) while SeatManager's `OwnsSeat/PilotOf` API sat unused. Rewrote TreadPart to **seat-based gating**: both treads share the chassis NetworkObject; a client drives the tread whose seat it holds (`PilotOf(side)==LocalClientId`), host re-validates `OwnsSeat` on the RequireOwnership=false RPC. Also swapped TreadPart to `Keyboard.current` (New-only input). Controls: **W/↑ throttle · Shift pivot · A/D lean**. Moved SeatManager OnGUI below the bootstrap panel (was overlapping). - **✅ UGS GATE CLEARED + HOST PATH VERIFIED (2026-07-10).** Cloud project **jankenbots** (`cloudProjectId a0d222ae-c79b-4120-bc0d-947bfd7600cb`, org `megaproxy`) is linked (has been in ProjectSettings since project creation) AND **Relay is now activated in the dashboard** (the missing step — done via cloud.unity.com ▸ Development ▸ Products ▸ Relay; there is NO enable toggle, opening the product page activates it). Verified by an in-editor Play-mode smoke test: `UnityServices.State=Initialized`, anonymous auth signed in, `HostGame()` created a real Relay allocation + join code, host's Player capsule auto-spawned (`IsSpawned`, owner=0). **The host half of M1-step-2 works end-to-end.** Still unverified: a *remote client joining* + peer-to-peer replication (needs a 2nd instance — standalone build via `manage_build` or a ParrelSync clone). - **Next build steps:** (5) shared bot = chassis Rigidbody + 2 `TreadPart` treads + `SeatManager` (⚠ `TreadPart` still reads legacy `Input.GetAxis` — swap to `Keyboard.current` like PlayerDriver was, project is Input-System-**New-only**); (6) shove ball + follow cam; (7) 2-friends feel test over Relay (needs a standalone build via `manage_build` or a ParrelSync clone for the 2nd client). - **Verified Relay API gotcha (baked into NetworkBootstrap):** with the unified SDK, `new RelayServerData(allocation, "dtls")` does NOT exist — use `AllocationUtils.ToRelayServerData(allocation, "dtls")` (`AllocationUtils` is in `Unity.Services.Relay.Models`). Corrected via live editor reflection.