CC Package and Physics
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0",
|
||||||
|
"components": [
|
||||||
|
"Microsoft.VisualStudio.Workload.ManagedGame"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -14,6 +14,8 @@ GameObject:
|
|||||||
- component: {fileID: 8290370249712404227}
|
- component: {fileID: 8290370249712404227}
|
||||||
- component: {fileID: 304484164735584996}
|
- component: {fileID: 304484164735584996}
|
||||||
- component: {fileID: 1666753161128106451}
|
- component: {fileID: 1666753161128106451}
|
||||||
|
- component: {fileID: 3388430386408803303}
|
||||||
|
- component: {fileID: 2040450355011915060}
|
||||||
m_Layer: 0
|
m_Layer: 0
|
||||||
m_Name: Player
|
m_Name: Player
|
||||||
m_TagString: Untagged
|
m_TagString: Untagged
|
||||||
@@ -147,3 +149,66 @@ MonoBehaviour:
|
|||||||
Importance: 1
|
Importance: 1
|
||||||
MaxSendRate: 0
|
MaxSendRate: 0
|
||||||
prefabId:
|
prefabId:
|
||||||
|
--- !u!136 &3388430386408803303
|
||||||
|
CapsuleCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2218851646297572645}
|
||||||
|
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: 2
|
||||||
|
m_Radius: 0.5
|
||||||
|
m_Height: 2
|
||||||
|
m_Direction: 1
|
||||||
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!114 &2040450355011915060
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2218851646297572645}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: e344ebd143cbf68439540a537e6ba4e9, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.PlayerCharacterAuthoring
|
||||||
|
GroundedMovementSharpness: 15
|
||||||
|
CharacterProperties:
|
||||||
|
CustomPhysicsBodyTags:
|
||||||
|
Tag00: 0
|
||||||
|
Tag01: 0
|
||||||
|
Tag02: 0
|
||||||
|
Tag03: 0
|
||||||
|
Tag04: 0
|
||||||
|
Tag05: 0
|
||||||
|
Tag06: 0
|
||||||
|
Tag07: 0
|
||||||
|
InterpolatePosition: 1
|
||||||
|
InterpolateRotation: 0
|
||||||
|
EvaluateGrounding: 1
|
||||||
|
SnapToGround: 1
|
||||||
|
GroundSnappingDistance: 0.5
|
||||||
|
EnhancedGroundPrecision: 0
|
||||||
|
MaxGroundedSlopeAngle: 60
|
||||||
|
DetectMovementCollisions: 1
|
||||||
|
DecollideFromOverlaps: 1
|
||||||
|
ProjectVelocityOnInitialOverlaps: 0
|
||||||
|
MaxContinuousCollisionsIterations: 8
|
||||||
|
MaxOverlapDecollisionIterations: 2
|
||||||
|
DiscardMovementWhenExceedMaxIterations: 1
|
||||||
|
KillVelocityWhenExceedMaxIterations: 1
|
||||||
|
DetectObstructionsForParentBodyMovement: 0
|
||||||
|
SimulateDynamicBody: 1
|
||||||
|
Mass: 1
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using ProjectM.Simulation;
|
||||||
|
using Unity.CharacterController;
|
||||||
|
using Unity.Entities;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace ProjectM.Authoring
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Authoring for the kinematic-character half of the player ghost (M5b: Unity Character Controller). Added
|
||||||
|
/// to the SAME prefab GameObject as <c>PlayerAuthoring</c> + the GhostAuthoringComponent (multiple bakers
|
||||||
|
/// per GameObject is supported; both resolve the same Entity). The baker calls
|
||||||
|
/// <see cref="KinematicCharacterUtilities.BakeCharacter"/>, which adds the CC runtime components/buffers
|
||||||
|
/// (KinematicCharacterProperties, KinematicCharacterBody, StoredKinematicCharacterData, the four CC
|
||||||
|
/// buffers, kinematic PhysicsVelocity/PhysicsMass, PhysicsGravityFactor, CharacterInterpolation) and bakes
|
||||||
|
/// the GameObject's CapsuleCollider into a PhysicsCollider. We then add our
|
||||||
|
/// <see cref="CharacterComponent"/> + <see cref="CharacterControl"/>.
|
||||||
|
/// <para>
|
||||||
|
/// IMPORTANT: BakeCharacter aborts (logs an error, adds nothing) if the GameObject has a Rigidbody and
|
||||||
|
/// requires uniform (1,1,1) scale — so the M5 Rigidbody MUST be removed from the prefab (the M5
|
||||||
|
/// CapsuleCollider stays — it is what gets baked into the character's PhysicsCollider).
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class PlayerCharacterAuthoring : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Tooltip("Sharpness of ground velocity smoothing (higher = snappier).")]
|
||||||
|
public float GroundedMovementSharpness = 15f;
|
||||||
|
|
||||||
|
public AuthoringKinematicCharacterProperties CharacterProperties = AuthoringKinematicCharacterProperties.GetDefault();
|
||||||
|
|
||||||
|
private class PlayerCharacterBaker : Baker<PlayerCharacterAuthoring>
|
||||||
|
{
|
||||||
|
public override void Bake(PlayerCharacterAuthoring authoring)
|
||||||
|
{
|
||||||
|
// Top-down planar character: no gravity (handled in the processor), stay on the plane.
|
||||||
|
var props = authoring.CharacterProperties;
|
||||||
|
props.SnapToGround = false; // no floor entity to snap to; planar
|
||||||
|
props.EvaluateGrounding = true; // floor/obstacle contact still reads as grounded
|
||||||
|
props.InterpolatePosition = true; // smooth fixed-step position for presentation
|
||||||
|
props.InterpolateRotation = false; // rotation owned by PlayerAimSystem
|
||||||
|
props.SimulateDynamicBody = false; // players don't physically shove each other (keep simple)
|
||||||
|
|
||||||
|
KinematicCharacterUtilities.BakeCharacter(this, authoring.gameObject, props);
|
||||||
|
|
||||||
|
var entity = GetEntity(TransformUsageFlags.Dynamic | TransformUsageFlags.WorldSpace);
|
||||||
|
|
||||||
|
AddComponent(entity, new CharacterComponent
|
||||||
|
{
|
||||||
|
GroundedMovementSharpness = authoring.GroundedMovementSharpness,
|
||||||
|
StepAndSlopeHandling = BasicStepAndSlopeHandlingParameters.GetDefault(),
|
||||||
|
});
|
||||||
|
AddComponent(entity, new CharacterControl());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e344ebd143cbf68439540a537e6ba4e9
|
||||||
@@ -7,7 +7,9 @@
|
|||||||
"Unity.Entities.Hybrid",
|
"Unity.Entities.Hybrid",
|
||||||
"Unity.Collections",
|
"Unity.Collections",
|
||||||
"Unity.Mathematics",
|
"Unity.Mathematics",
|
||||||
"Unity.NetCode"
|
"Unity.NetCode",
|
||||||
|
"Unity.Physics",
|
||||||
|
"Unity.CharacterController"
|
||||||
],
|
],
|
||||||
"includePlatforms": [],
|
"includePlatforms": [],
|
||||||
"excludePlatforms": [],
|
"excludePlatforms": [],
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace ProjectM.Simulation
|
|||||||
/// AbilityRef / CharacterStatsRef) with its replicated StatModifier buffer into the
|
/// AbilityRef / CharacterStatsRef) with its replicated StatModifier buffer into the
|
||||||
/// EffectiveAbilityStats / EffectiveCharacterStats components - every predicted tick, on both worlds.
|
/// EffectiveAbilityStats / EffectiveCharacterStats components - every predicted tick, on both worlds.
|
||||||
///
|
///
|
||||||
/// Runs at the head of the predicted group (UpdateBefore PlayerAimSystem and PlayerMoveSystem;
|
/// Runs at the head of the predicted group (UpdateBefore PlayerAimSystem;
|
||||||
/// AbilityFireSystem runs after PlayerAimSystem, so it sees fresh values too). Recompute is
|
/// AbilityFireSystem runs after PlayerAimSystem, so it sees fresh values too). Recompute is
|
||||||
/// unconditional every tick: it is a pure function of (blob base + replicated buffer), both of which
|
/// unconditional every tick: it is a pure function of (blob base + replicated buffer), both of which
|
||||||
/// are restored on rollback, so predicted and server results always agree. A dirty-flag / change
|
/// are restored on rollback, so predicted and server results always agree. A dirty-flag / change
|
||||||
@@ -19,7 +19,6 @@ namespace ProjectM.Simulation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
|
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
|
||||||
[UpdateBefore(typeof(PlayerAimSystem))]
|
[UpdateBefore(typeof(PlayerAimSystem))]
|
||||||
[UpdateBefore(typeof(PlayerMoveSystem))]
|
|
||||||
[BurstCompile]
|
[BurstCompile]
|
||||||
public partial struct StatRecomputeSystem : ISystem
|
public partial struct StatRecomputeSystem : ISystem
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using Unity.CharacterController;
|
||||||
|
using Unity.Entities;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tunables for the top-down kinematic character (M5b: Unity Character Controller package). Twin-stick /
|
||||||
|
/// planar: NO jump, NO air control, NO gravity, NO view pitch. The per-tick target speed comes from the
|
||||||
|
/// data-driven <see cref="EffectiveCharacterStats.MoveSpeed"/> via <see cref="PlayerControlSystem"/>; this
|
||||||
|
/// component only carries the smoothing sharpness + step/slope handling the CC processor needs. Authored
|
||||||
|
/// on the player prefab by <c>PlayerCharacterAuthoring</c>. Not a ghost — movement replicates through the
|
||||||
|
/// owner-predicted LocalTransform (re-simulated on the owner, interpolated on remotes).
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public struct CharacterComponent : IComponentData
|
||||||
|
{
|
||||||
|
/// <summary>How quickly RelativeVelocity is lerped toward the target velocity on the ground.</summary>
|
||||||
|
public float GroundedMovementSharpness;
|
||||||
|
|
||||||
|
/// <summary>Step/slope handling params (defaults; planar ground so mostly inert).</summary>
|
||||||
|
public BasicStepAndSlopeHandlingParameters StepAndSlopeHandling;
|
||||||
|
|
||||||
|
public static CharacterComponent GetDefault()
|
||||||
|
{
|
||||||
|
return new CharacterComponent
|
||||||
|
{
|
||||||
|
GroundedMovementSharpness = 15f,
|
||||||
|
StepAndSlopeHandling = BasicStepAndSlopeHandlingParameters.GetDefault(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-tick, NON-replicated control for the CC processor, derived each predicted tick from the replicated
|
||||||
|
/// <see cref="PlayerInput"/> by <see cref="PlayerControlSystem"/>. Derived (not authored, not a ghost): a
|
||||||
|
/// pure function of replicated input + recomputed stats, so it reproduces identically on server, owning
|
||||||
|
/// client, and across rollback re-simulation.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public struct CharacterControl : IComponentData
|
||||||
|
{
|
||||||
|
/// <summary>World-space desired velocity (already scaled by MoveSpeed, Y == 0). The processor lerps
|
||||||
|
/// RelativeVelocity toward this each tick.</summary>
|
||||||
|
public float3 MoveVelocity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 69a9d5a5573f2804d987602849f43554
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using Unity.Mathematics;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pure, Burst-friendly math for top-down character control, factored out for EditMode unit testing
|
||||||
|
/// (no World/system needed). Maps a twin-stick move axis + speed to a planar world velocity.
|
||||||
|
/// </summary>
|
||||||
|
public static class CharacterControlMath
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Twin-stick move (x,y) on the XZ plane scaled by <paramref name="speed"/>. Input longer than unit
|
||||||
|
/// length is clamped (so diagonals aren't faster than cardinals); shorter is left proportional
|
||||||
|
/// (analog sticks). Returns a world velocity with Y == 0.
|
||||||
|
/// </summary>
|
||||||
|
public static float3 DesiredMovement(float2 move, float speed)
|
||||||
|
{
|
||||||
|
float lenSq = math.lengthsq(move);
|
||||||
|
if (lenSq > 1f)
|
||||||
|
move = math.normalize(move);
|
||||||
|
return new float3(move.x, 0f, move.y) * speed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8ada5ffb04316ba48851d44f5b3340f2
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Unity.CharacterController;
|
||||||
|
using Unity.Entities;
|
||||||
|
using Unity.NetCode;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ghost-variant registration for the kinematic character (M5b). <see cref="CharacterInterpolation"/> is
|
||||||
|
/// presentation-only fixed-step smoothing that must exist ONLY on predicted clients: on the server it
|
||||||
|
/// would interfere with LocalToWorld, and on interpolated remote ghosts it would double up with netcode's
|
||||||
|
/// own snapshot interpolation. <c>BakeCharacter</c> adds it to every prefab version, so we force it
|
||||||
|
/// predicted-client-only here.
|
||||||
|
/// <para>
|
||||||
|
/// We deliberately do NOT override the default variants for <c>LocalTransform</c> or
|
||||||
|
/// <c>PhysicsVelocity</c> (the CC sample's DontSerializeVariant on LocalTransform is global and would
|
||||||
|
/// break the non-character ghosts in this project — projectiles, dummies, pickups — which rely on stock
|
||||||
|
/// LocalTransform replication). The character's position therefore replicates via the normal
|
||||||
|
/// owner-predicted LocalTransform path, like every other ghost.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class CharacterGhostVariantsSystem : DefaultVariantSystemBase
|
||||||
|
{
|
||||||
|
protected override void RegisterDefaultVariants(Dictionary<ComponentType, Rule> defaultVariants)
|
||||||
|
{
|
||||||
|
defaultVariants.Add(typeof(CharacterInterpolation), Rule.ForAll(typeof(CharacterInterpolation_GhostVariant)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[GhostComponentVariation(typeof(CharacterInterpolation))]
|
||||||
|
[GhostComponent(PrefabType = GhostPrefabType.PredictedClient)]
|
||||||
|
public struct CharacterInterpolation_GhostVariant
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6383b28a0d1f44649b514ece99ee4915
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
using Unity.Burst;
|
||||||
|
using Unity.Burst.Intrinsics;
|
||||||
|
using Unity.CharacterController;
|
||||||
|
using Unity.Entities;
|
||||||
|
using Unity.Physics;
|
||||||
|
using Unity.Transforms;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fixed-step kinematic character update. Runs in <see cref="KinematicCharacterPhysicsUpdateGroup"/>
|
||||||
|
/// (which sits after the physics build); under Netcode-for-Entities the physics groups are relocated into
|
||||||
|
/// <see cref="Unity.NetCode.PredictedFixedStepSimulationSystemGroup"/>, so the character integrates inside
|
||||||
|
/// the predicted loop — deterministic, rollback-safe, identical on server + owning client. Builds a
|
||||||
|
/// <see cref="KinematicCharacterDataAccess"/> per entity and runs the processor's PhysicsUpdate. Filtered
|
||||||
|
/// to <see cref="Simulate"/> so only predicted ghosts step.
|
||||||
|
/// </summary>
|
||||||
|
[UpdateInGroup(typeof(KinematicCharacterPhysicsUpdateGroup))]
|
||||||
|
[BurstCompile]
|
||||||
|
public partial struct CharacterPhysicsUpdateSystem : ISystem
|
||||||
|
{
|
||||||
|
EntityQuery m_CharacterQuery;
|
||||||
|
CharacterUpdateContext m_Context;
|
||||||
|
KinematicCharacterUpdateContext m_BaseContext;
|
||||||
|
|
||||||
|
[BurstCompile]
|
||||||
|
public void OnCreate(ref SystemState state)
|
||||||
|
{
|
||||||
|
m_CharacterQuery = KinematicCharacterUtilities.GetBaseCharacterQueryBuilder()
|
||||||
|
.WithAll<CharacterComponent, CharacterControl>()
|
||||||
|
.Build(ref state);
|
||||||
|
|
||||||
|
m_Context = new CharacterUpdateContext();
|
||||||
|
m_Context.OnSystemCreate(ref state);
|
||||||
|
m_BaseContext = new KinematicCharacterUpdateContext();
|
||||||
|
m_BaseContext.OnSystemCreate(ref state);
|
||||||
|
|
||||||
|
state.RequireForUpdate(m_CharacterQuery);
|
||||||
|
state.RequireForUpdate<PhysicsWorldSingleton>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[BurstCompile]
|
||||||
|
public void OnUpdate(ref SystemState state)
|
||||||
|
{
|
||||||
|
m_Context.OnSystemUpdate(ref state);
|
||||||
|
m_BaseContext.OnSystemUpdate(ref state, SystemAPI.Time, SystemAPI.GetSingleton<PhysicsWorldSingleton>());
|
||||||
|
|
||||||
|
var job = new CharacterPhysicsUpdateJob
|
||||||
|
{
|
||||||
|
Context = m_Context,
|
||||||
|
BaseContext = m_BaseContext,
|
||||||
|
};
|
||||||
|
state.Dependency = job.Schedule(state.Dependency);
|
||||||
|
}
|
||||||
|
|
||||||
|
[BurstCompile]
|
||||||
|
[WithAll(typeof(Simulate))]
|
||||||
|
public partial struct CharacterPhysicsUpdateJob : IJobEntity, IJobEntityChunkBeginEnd
|
||||||
|
{
|
||||||
|
public CharacterUpdateContext Context;
|
||||||
|
public KinematicCharacterUpdateContext BaseContext;
|
||||||
|
|
||||||
|
void Execute(
|
||||||
|
Entity entity,
|
||||||
|
RefRW<LocalTransform> localTransform,
|
||||||
|
RefRW<KinematicCharacterProperties> characterProperties,
|
||||||
|
RefRW<KinematicCharacterBody> characterBody,
|
||||||
|
RefRW<PhysicsCollider> physicsCollider,
|
||||||
|
RefRW<CharacterComponent> characterComponent,
|
||||||
|
RefRW<CharacterControl> characterControl,
|
||||||
|
DynamicBuffer<KinematicCharacterHit> characterHitsBuffer,
|
||||||
|
DynamicBuffer<StatefulKinematicCharacterHit> statefulHitsBuffer,
|
||||||
|
DynamicBuffer<KinematicCharacterDeferredImpulse> deferredImpulsesBuffer,
|
||||||
|
DynamicBuffer<KinematicVelocityProjectionHit> velocityProjectionHits)
|
||||||
|
{
|
||||||
|
var processor = new CharacterProcessor
|
||||||
|
{
|
||||||
|
CharacterDataAccess = new KinematicCharacterDataAccess(
|
||||||
|
entity,
|
||||||
|
localTransform,
|
||||||
|
characterProperties,
|
||||||
|
characterBody,
|
||||||
|
physicsCollider,
|
||||||
|
characterHitsBuffer,
|
||||||
|
statefulHitsBuffer,
|
||||||
|
deferredImpulsesBuffer,
|
||||||
|
velocityProjectionHits),
|
||||||
|
CharacterComponent = characterComponent,
|
||||||
|
CharacterControl = characterControl,
|
||||||
|
};
|
||||||
|
|
||||||
|
processor.PhysicsUpdate(ref Context, ref BaseContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OnChunkBegin(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask)
|
||||||
|
{
|
||||||
|
BaseContext.EnsureCreationOfTmpCollections();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnChunkEnd(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in v128 chunkEnabledMask, bool chunkWasExecuted) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 70d14da3eeeaec24896586d71fd22fdc
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
using Unity.CharacterController;
|
||||||
|
using Unity.Entities;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
using Unity.Physics;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Per-update "global" data the character processor needs (lookups, singletons). Empty for the minimal
|
||||||
|
/// top-down character — kept so the CC update signature is satisfied and future lookups have a home.
|
||||||
|
/// </summary>
|
||||||
|
public struct CharacterUpdateContext
|
||||||
|
{
|
||||||
|
public void OnSystemCreate(ref SystemState state) { }
|
||||||
|
public void OnSystemUpdate(ref SystemState state) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Top-down kinematic character processor (CC 1.4.2 pattern: <see cref="IKinematicCharacterProcessor{T}"/>
|
||||||
|
/// + a <see cref="KinematicCharacterDataAccess"/> built in the job, driving the static
|
||||||
|
/// <see cref="KinematicCharacterUtilities"/> Update_* sequence). Stripped of all FPS features: no jump,
|
||||||
|
/// no air movement, no gravity, no view. <see cref="PhysicsUpdate"/> runs the canonical CC update
|
||||||
|
/// sequence; <see cref="HandleVelocityControl"/> lerps RelativeVelocity toward the desired planar
|
||||||
|
/// velocity <see cref="PlayerControlSystem"/> wrote. Rotation/facing is owned by <see cref="PlayerAimSystem"/>.
|
||||||
|
/// </summary>
|
||||||
|
public struct CharacterProcessor : IKinematicCharacterProcessor<CharacterUpdateContext>
|
||||||
|
{
|
||||||
|
public KinematicCharacterDataAccess CharacterDataAccess;
|
||||||
|
public RefRW<CharacterComponent> CharacterComponent;
|
||||||
|
public RefRW<CharacterControl> CharacterControl;
|
||||||
|
|
||||||
|
public void PhysicsUpdate(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext)
|
||||||
|
{
|
||||||
|
ref CharacterComponent characterComponent = ref CharacterComponent.ValueRW;
|
||||||
|
ref KinematicCharacterBody characterBody = ref CharacterDataAccess.CharacterBody.ValueRW;
|
||||||
|
ref float3 characterPosition = ref CharacterDataAccess.LocalTransform.ValueRW.Position;
|
||||||
|
|
||||||
|
KinematicCharacterUtilities.Update_Initialize(
|
||||||
|
in this, ref context, ref baseContext,
|
||||||
|
ref characterBody,
|
||||||
|
CharacterDataAccess.CharacterHitsBuffer,
|
||||||
|
CharacterDataAccess.DeferredImpulsesBuffer,
|
||||||
|
CharacterDataAccess.VelocityProjectionHits,
|
||||||
|
baseContext.Time.DeltaTime);
|
||||||
|
|
||||||
|
KinematicCharacterUtilities.Update_ParentMovement(
|
||||||
|
in this, ref context, ref baseContext,
|
||||||
|
CharacterDataAccess.CharacterEntity,
|
||||||
|
ref characterBody,
|
||||||
|
CharacterDataAccess.CharacterProperties.ValueRO,
|
||||||
|
CharacterDataAccess.PhysicsCollider.ValueRO,
|
||||||
|
CharacterDataAccess.LocalTransform.ValueRO,
|
||||||
|
ref characterPosition,
|
||||||
|
characterBody.WasGroundedBeforeCharacterUpdate);
|
||||||
|
|
||||||
|
KinematicCharacterUtilities.Update_Grounding(
|
||||||
|
in this, ref context, ref baseContext,
|
||||||
|
ref characterBody,
|
||||||
|
CharacterDataAccess.CharacterEntity,
|
||||||
|
CharacterDataAccess.CharacterProperties.ValueRO,
|
||||||
|
CharacterDataAccess.PhysicsCollider.ValueRO,
|
||||||
|
CharacterDataAccess.LocalTransform.ValueRO,
|
||||||
|
CharacterDataAccess.VelocityProjectionHits,
|
||||||
|
CharacterDataAccess.CharacterHitsBuffer,
|
||||||
|
ref characterPosition);
|
||||||
|
|
||||||
|
HandleVelocityControl(ref context, ref baseContext);
|
||||||
|
|
||||||
|
KinematicCharacterUtilities.Update_PreventGroundingFromFutureSlopeChange(
|
||||||
|
in this, ref context, ref baseContext,
|
||||||
|
CharacterDataAccess.CharacterEntity,
|
||||||
|
ref characterBody,
|
||||||
|
CharacterDataAccess.CharacterProperties.ValueRO,
|
||||||
|
CharacterDataAccess.PhysicsCollider.ValueRO,
|
||||||
|
in characterComponent.StepAndSlopeHandling);
|
||||||
|
|
||||||
|
KinematicCharacterUtilities.Update_GroundPushing(
|
||||||
|
in this, ref context, ref baseContext,
|
||||||
|
ref characterBody,
|
||||||
|
CharacterDataAccess.CharacterProperties.ValueRO,
|
||||||
|
CharacterDataAccess.LocalTransform.ValueRO,
|
||||||
|
CharacterDataAccess.DeferredImpulsesBuffer,
|
||||||
|
float3.zero);
|
||||||
|
|
||||||
|
KinematicCharacterUtilities.Update_MovementAndDecollisions(
|
||||||
|
in this, ref context, ref baseContext,
|
||||||
|
CharacterDataAccess.CharacterEntity,
|
||||||
|
ref characterBody,
|
||||||
|
CharacterDataAccess.CharacterProperties.ValueRO,
|
||||||
|
CharacterDataAccess.PhysicsCollider.ValueRO,
|
||||||
|
CharacterDataAccess.LocalTransform.ValueRO,
|
||||||
|
CharacterDataAccess.VelocityProjectionHits,
|
||||||
|
CharacterDataAccess.CharacterHitsBuffer,
|
||||||
|
CharacterDataAccess.DeferredImpulsesBuffer,
|
||||||
|
ref characterPosition);
|
||||||
|
|
||||||
|
KinematicCharacterUtilities.Update_MovingPlatformDetection(ref baseContext, ref characterBody);
|
||||||
|
KinematicCharacterUtilities.Update_ParentMomentum(ref baseContext, ref characterBody,
|
||||||
|
CharacterDataAccess.LocalTransform.ValueRO.Position);
|
||||||
|
KinematicCharacterUtilities.Update_ProcessStatefulCharacterHits(
|
||||||
|
CharacterDataAccess.CharacterHitsBuffer,
|
||||||
|
CharacterDataAccess.StatefulHitsBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleVelocityControl(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext)
|
||||||
|
{
|
||||||
|
float deltaTime = baseContext.Time.DeltaTime;
|
||||||
|
ref KinematicCharacterBody characterBody = ref CharacterDataAccess.CharacterBody.ValueRW;
|
||||||
|
ref CharacterComponent characterComponent = ref CharacterComponent.ValueRW;
|
||||||
|
CharacterControl characterControl = CharacterControl.ValueRO;
|
||||||
|
|
||||||
|
// Planar twin-stick: smoothly approach the desired world velocity (already speed-scaled, Y==0).
|
||||||
|
float3 targetVelocity = characterControl.MoveVelocity;
|
||||||
|
CharacterControlUtilities.StandardGroundMove_Interpolated(
|
||||||
|
ref characterBody.RelativeVelocity,
|
||||||
|
targetVelocity,
|
||||||
|
characterComponent.GroundedMovementSharpness,
|
||||||
|
deltaTime,
|
||||||
|
math.up(),
|
||||||
|
math.up());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void VariableUpdate(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext) { }
|
||||||
|
|
||||||
|
public void UpdateGroundingUp(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext)
|
||||||
|
{
|
||||||
|
ref KinematicCharacterBody characterBody = ref CharacterDataAccess.CharacterBody.ValueRW;
|
||||||
|
KinematicCharacterUtilities.Default_UpdateGroundingUp(
|
||||||
|
ref characterBody,
|
||||||
|
CharacterDataAccess.LocalTransform.ValueRO.Rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanCollideWithHit(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext, in BasicHit hit)
|
||||||
|
{
|
||||||
|
return PhysicsUtilities.IsCollidable(hit.Material);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsGroundedOnHit(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext, in BasicHit hit, int groundingEvaluationType)
|
||||||
|
{
|
||||||
|
CharacterComponent characterComponent = CharacterComponent.ValueRO;
|
||||||
|
return KinematicCharacterUtilities.Default_IsGroundedOnHit(
|
||||||
|
in this, ref context, ref baseContext,
|
||||||
|
CharacterDataAccess.CharacterEntity,
|
||||||
|
CharacterDataAccess.PhysicsCollider.ValueRO,
|
||||||
|
CharacterDataAccess.CharacterBody.ValueRO,
|
||||||
|
CharacterDataAccess.CharacterProperties.ValueRO,
|
||||||
|
in hit,
|
||||||
|
in characterComponent.StepAndSlopeHandling,
|
||||||
|
groundingEvaluationType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnMovementHit(
|
||||||
|
ref CharacterUpdateContext context,
|
||||||
|
ref KinematicCharacterUpdateContext baseContext,
|
||||||
|
ref KinematicCharacterHit hit,
|
||||||
|
ref float3 remainingMovementDirection,
|
||||||
|
ref float remainingMovementLength,
|
||||||
|
float3 originalVelocityDirection,
|
||||||
|
float hitDistance)
|
||||||
|
{
|
||||||
|
ref KinematicCharacterBody characterBody = ref CharacterDataAccess.CharacterBody.ValueRW;
|
||||||
|
ref float3 characterPosition = ref CharacterDataAccess.LocalTransform.ValueRW.Position;
|
||||||
|
CharacterComponent characterComponent = CharacterComponent.ValueRO;
|
||||||
|
|
||||||
|
KinematicCharacterUtilities.Default_OnMovementHit(
|
||||||
|
in this, ref context, ref baseContext,
|
||||||
|
ref characterBody,
|
||||||
|
CharacterDataAccess.CharacterEntity,
|
||||||
|
CharacterDataAccess.CharacterProperties.ValueRO,
|
||||||
|
CharacterDataAccess.PhysicsCollider.ValueRO,
|
||||||
|
CharacterDataAccess.LocalTransform.ValueRO,
|
||||||
|
ref characterPosition,
|
||||||
|
CharacterDataAccess.VelocityProjectionHits,
|
||||||
|
ref hit,
|
||||||
|
ref remainingMovementDirection,
|
||||||
|
ref remainingMovementLength,
|
||||||
|
originalVelocityDirection,
|
||||||
|
hitDistance,
|
||||||
|
characterComponent.StepAndSlopeHandling.StepHandling,
|
||||||
|
characterComponent.StepAndSlopeHandling.MaxStepHeight,
|
||||||
|
characterComponent.StepAndSlopeHandling.CharacterWidthForStepGroundingCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OverrideDynamicHitMasses(
|
||||||
|
ref CharacterUpdateContext context,
|
||||||
|
ref KinematicCharacterUpdateContext baseContext,
|
||||||
|
ref PhysicsMass characterMass,
|
||||||
|
ref PhysicsMass otherMass,
|
||||||
|
BasicHit hit)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProjectVelocityOnHits(
|
||||||
|
ref CharacterUpdateContext context,
|
||||||
|
ref KinematicCharacterUpdateContext baseContext,
|
||||||
|
ref float3 velocity,
|
||||||
|
ref bool characterIsGrounded,
|
||||||
|
ref BasicHit characterGroundHit,
|
||||||
|
in DynamicBuffer<KinematicVelocityProjectionHit> velocityProjectionHits,
|
||||||
|
float3 originalVelocityDirection)
|
||||||
|
{
|
||||||
|
CharacterComponent characterComponent = CharacterComponent.ValueRO;
|
||||||
|
KinematicCharacterUtilities.Default_ProjectVelocityOnHits(
|
||||||
|
ref velocity,
|
||||||
|
ref characterIsGrounded,
|
||||||
|
ref characterGroundHit,
|
||||||
|
in velocityProjectionHits,
|
||||||
|
originalVelocityDirection,
|
||||||
|
characterComponent.StepAndSlopeHandling.ConstrainVelocityToGroundPlane,
|
||||||
|
in CharacterDataAccess.CharacterBody.ValueRO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d2bcdc39be82b4c4dbc85b799da194f3
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using Unity.Burst;
|
||||||
|
using Unity.Entities;
|
||||||
|
using Unity.NetCode;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Bridges replicated <see cref="PlayerInput"/> + data-driven <see cref="EffectiveCharacterStats"/> into
|
||||||
|
/// the CC processor's per-tick <see cref="CharacterControl"/>. Replaces the M5 <c>PlayerMoveSystem</c>:
|
||||||
|
/// instead of writing PhysicsVelocity, it writes the desired world velocity that
|
||||||
|
/// <see cref="CharacterProcessor"/> lerps toward inside the fixed-step character update. Runs in
|
||||||
|
/// <see cref="PredictedSimulationSystemGroup"/> after <see cref="StatRecomputeSystem"/> (fresh MoveSpeed)
|
||||||
|
/// and before the predicted fixed-step group (where the character physics steps). Derived purely from
|
||||||
|
/// replicated input + recomputed stats → identical on server, owning client, and across rollback.
|
||||||
|
/// Filtered to <see cref="Simulate"/> for predicted ghosts only.
|
||||||
|
/// </summary>
|
||||||
|
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
|
||||||
|
[UpdateAfter(typeof(StatRecomputeSystem))]
|
||||||
|
[BurstCompile]
|
||||||
|
public partial struct PlayerControlSystem : ISystem
|
||||||
|
{
|
||||||
|
[BurstCompile]
|
||||||
|
public void OnUpdate(ref SystemState state)
|
||||||
|
{
|
||||||
|
foreach (var (control, input, stats) in
|
||||||
|
SystemAPI.Query<RefRW<CharacterControl>, RefRO<PlayerInput>, RefRO<EffectiveCharacterStats>>()
|
||||||
|
.WithAll<Simulate>())
|
||||||
|
{
|
||||||
|
control.ValueRW.MoveVelocity =
|
||||||
|
CharacterControlMath.DesiredMovement(input.ValueRO.Move, stats.ValueRO.MoveSpeed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d70cdf70e3ae53440aeb7274bdb27793
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
using Unity.Burst;
|
|
||||||
using Unity.Entities;
|
|
||||||
using Unity.Mathematics;
|
|
||||||
using Unity.NetCode;
|
|
||||||
using Unity.Transforms;
|
|
||||||
|
|
||||||
namespace ProjectM.Simulation
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Canonical predicted system: advances each player's planar (XZ) position from twin-stick Move
|
|
||||||
/// input. Runs inside the prediction loop on the owning client (re-simulated on rollback) and
|
|
||||||
/// once per tick on the server; filtered to <see cref="Simulate"/> so only predicted ghosts move.
|
|
||||||
/// Deterministic by construction: uses <c>SystemAPI.Time.DeltaTime</c> (the fixed tick step)
|
|
||||||
/// only — no wall-clock, no <c>System.Random</c>. Move is clamped to unit length so diagonal
|
|
||||||
/// keyboard movement is not faster than cardinal. Move speed is the data-driven
|
|
||||||
/// <see cref="EffectiveCharacterStats.MoveSpeed"/> (authored base + active modifiers), recomputed
|
|
||||||
/// each tick by <see cref="StatRecomputeSystem"/> which runs before this system.
|
|
||||||
/// </summary>
|
|
||||||
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
|
|
||||||
[BurstCompile]
|
|
||||||
public partial struct PlayerMoveSystem : ISystem
|
|
||||||
{
|
|
||||||
[BurstCompile]
|
|
||||||
public void OnUpdate(ref SystemState state)
|
|
||||||
{
|
|
||||||
float dt = SystemAPI.Time.DeltaTime;
|
|
||||||
|
|
||||||
foreach (var (transform, input, stats) in
|
|
||||||
SystemAPI.Query<RefRW<LocalTransform>, RefRO<PlayerInput>, RefRO<EffectiveCharacterStats>>()
|
|
||||||
.WithAll<Simulate>())
|
|
||||||
{
|
|
||||||
float2 move = input.ValueRO.Move;
|
|
||||||
if (math.lengthsq(move) > 1f)
|
|
||||||
move = math.normalize(move);
|
|
||||||
|
|
||||||
float3 delta = new float3(move.x, 0f, move.y) * stats.ValueRO.MoveSpeed * dt;
|
|
||||||
transform.ValueRW.Position += delta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 35cd3eacccc2f4172b557b2807a6df22
|
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
"Unity.Mathematics",
|
"Unity.Mathematics",
|
||||||
"Unity.Burst",
|
"Unity.Burst",
|
||||||
"Unity.Physics",
|
"Unity.Physics",
|
||||||
"Unity.NetCode"
|
"Unity.NetCode",
|
||||||
|
"Unity.CharacterController"
|
||||||
],
|
],
|
||||||
"includePlatforms": [],
|
"includePlatforms": [],
|
||||||
"excludePlatforms": [],
|
"excludePlatforms": [],
|
||||||
|
|||||||
@@ -169,6 +169,118 @@ Transform:
|
|||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1 &691660672
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 691660676}
|
||||||
|
- component: {fileID: 691660675}
|
||||||
|
- component: {fileID: 691660674}
|
||||||
|
- component: {fileID: 691660673}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Wall_North
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!23 &691660673
|
||||||
|
MeshRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 691660672}
|
||||||
|
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: f8df9cd33fb974460a903e35a6fce3c9, 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 &691660674
|
||||||
|
BoxCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 691660672}
|
||||||
|
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 &691660675
|
||||||
|
MeshFilter:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 691660672}
|
||||||
|
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
--- !u!4 &691660676
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 691660672}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 1.25, z: 6}
|
||||||
|
m_LocalScale: {x: 8, y: 2.5, z: 0.5}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &1379903944
|
--- !u!1 &1379903944
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -214,6 +326,57 @@ Transform:
|
|||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1 &1435545286
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1435545288}
|
||||||
|
- component: {fileID: 1435545287}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: PhysicsConfig
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!114 &1435545287
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1435545286}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 5bd6bf266b32e49ac8c5951317b6b250, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Unity.NetCode.Physics.Hybrid::Unity.NetCode.NetCodePhysicsConfig
|
||||||
|
PhysicGroupRunMode: 1
|
||||||
|
EnableLagCompensation: 0
|
||||||
|
ServerHistorySize: 0
|
||||||
|
ClientHistorySize: 1
|
||||||
|
ClientNonGhostWorldIndex: 0
|
||||||
|
DeepCopyDynamicColliders: 1
|
||||||
|
DeepCopyStaticColliders: 0
|
||||||
|
--- !u!4 &1435545288
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1435545286}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &1498433570
|
--- !u!1 &1498433570
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -245,6 +408,8 @@ MonoBehaviour:
|
|||||||
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.PlayerSpawnerAuthoring
|
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.PlayerSpawnerAuthoring
|
||||||
PlayerPrefab: {fileID: 2218851646297572645, guid: a27bbed2662454377bd25279ee4a14d2, type: 3}
|
PlayerPrefab: {fileID: 2218851646297572645, guid: a27bbed2662454377bd25279ee4a14d2, type: 3}
|
||||||
SpawnPoint: {x: 0, y: 1, z: 0}
|
SpawnPoint: {x: 0, y: 1, z: 0}
|
||||||
|
SpawnRingRadius: 2.5
|
||||||
|
RingSlots: 4
|
||||||
--- !u!4 &1498433572
|
--- !u!4 &1498433572
|
||||||
Transform:
|
Transform:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -308,6 +473,230 @@ Transform:
|
|||||||
m_Children: []
|
m_Children: []
|
||||||
m_Father: {fileID: 0}
|
m_Father: {fileID: 0}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1 &1694504171
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1694504175}
|
||||||
|
- component: {fileID: 1694504174}
|
||||||
|
- component: {fileID: 1694504173}
|
||||||
|
- component: {fileID: 1694504172}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Pillar_Center
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!23 &1694504172
|
||||||
|
MeshRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1694504171}
|
||||||
|
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: f8df9cd33fb974460a903e35a6fce3c9, 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 &1694504173
|
||||||
|
BoxCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1694504171}
|
||||||
|
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 &1694504174
|
||||||
|
MeshFilter:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1694504171}
|
||||||
|
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
--- !u!4 &1694504175
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1694504171}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 1.25, z: 0}
|
||||||
|
m_LocalScale: {x: 1.5, y: 2.5, z: 1.5}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!1 &1936735558
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 1936735562}
|
||||||
|
- component: {fileID: 1936735561}
|
||||||
|
- component: {fileID: 1936735560}
|
||||||
|
- component: {fileID: 1936735559}
|
||||||
|
m_Layer: 0
|
||||||
|
m_Name: Wall_East
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!23 &1936735559
|
||||||
|
MeshRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1936735558}
|
||||||
|
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: f8df9cd33fb974460a903e35a6fce3c9, 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 &1936735560
|
||||||
|
BoxCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1936735558}
|
||||||
|
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 &1936735561
|
||||||
|
MeshFilter:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1936735558}
|
||||||
|
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
--- !u!4 &1936735562
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 1936735558}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 6, y: 1.25, z: 0}
|
||||||
|
m_LocalScale: {x: 0.5, y: 2.5, z: 8}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
--- !u!1 &2143686865
|
--- !u!1 &2143686865
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -365,3 +754,7 @@ SceneRoots:
|
|||||||
- {fileID: 2143686867}
|
- {fileID: 2143686867}
|
||||||
- {fileID: 409538539}
|
- {fileID: 409538539}
|
||||||
- {fileID: 1527461113}
|
- {fileID: 1527461113}
|
||||||
|
- {fileID: 1435545288}
|
||||||
|
- {fileID: 1694504175}
|
||||||
|
- {fileID: 1936735562}
|
||||||
|
- {fileID: 691660676}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using ProjectM.Simulation;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
|
||||||
|
namespace ProjectM.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pure-helper unit tests for <see cref="CharacterControlMath.DesiredMovement"/> (M5b: Unity Character
|
||||||
|
/// Controller). Replaces the M5 PlayerMoveSystemTests: the character now moves via collide-and-slide
|
||||||
|
/// inside the predicted physics loop, so the unit-testable seam is the input→world-velocity mapping that
|
||||||
|
/// <see cref="PlayerControlSystem"/> feeds the CC processor. The actual sweep/collision/replication is
|
||||||
|
/// covered by the Play Mode runtime check (a full PhysicsWorld is not built in a bare EditMode world).
|
||||||
|
/// Netcode-free and version-independent.
|
||||||
|
/// </summary>
|
||||||
|
public class CharacterControlMathTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void DesiredMovement_Maps_Cardinal_To_Planar_Velocity()
|
||||||
|
{
|
||||||
|
const float speed = 5f;
|
||||||
|
var v = CharacterControlMath.DesiredMovement(new float2(1f, 0f), speed);
|
||||||
|
|
||||||
|
Assert.AreEqual(speed, v.x, 1e-4f, "Move=(1,0) -> +X scaled by speed.");
|
||||||
|
Assert.AreEqual(0f, v.y, 1e-4f, "Movement is planar; Y must be zero.");
|
||||||
|
Assert.AreEqual(0f, v.z, 1e-4f, "Move=(1,0) maps to +X only; Z must be zero.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DesiredMovement_Clamps_Diagonal_To_Unit_Speed()
|
||||||
|
{
|
||||||
|
const float speed = 6f;
|
||||||
|
var v = CharacterControlMath.DesiredMovement(new float2(1f, 1f), speed);
|
||||||
|
|
||||||
|
float planarSpeed = math.length(new float2(v.x, v.z));
|
||||||
|
Assert.AreEqual(speed, planarSpeed, 1e-3f, "Diagonal input clamped to unit length -> speed == MoveSpeed.");
|
||||||
|
Assert.AreEqual(0f, v.y, 1e-4f, "Linear Y must stay zero.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DesiredMovement_Sub_Unit_Input_Is_Proportional()
|
||||||
|
{
|
||||||
|
const float speed = 10f;
|
||||||
|
var v = CharacterControlMath.DesiredMovement(new float2(0.5f, 0f), speed);
|
||||||
|
|
||||||
|
Assert.AreEqual(0.5f * speed, v.x, 1e-3f, "Sub-unit analog input scales speed proportionally (not renormalised).");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void DesiredMovement_Is_Deterministic_And_Maps_Z()
|
||||||
|
{
|
||||||
|
var a = CharacterControlMath.DesiredMovement(new float2(0f, 1f), 3f);
|
||||||
|
var b = CharacterControlMath.DesiredMovement(new float2(0f, 1f), 3f);
|
||||||
|
|
||||||
|
Assert.AreEqual(a.z, b.z, 0f, "Deterministic: identical args -> identical result.");
|
||||||
|
Assert.AreEqual(3f, a.z, 1e-4f, "Move=(0,1) maps to +Z scaled by speed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 051c0bdf57f9cca41963e3c2ed60b2bf
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
using NUnit.Framework;
|
|
||||||
using ProjectM.Simulation;
|
|
||||||
using Unity.Core;
|
|
||||||
using Unity.Entities;
|
|
||||||
using Unity.Mathematics;
|
|
||||||
using Unity.Transforms;
|
|
||||||
|
|
||||||
namespace ProjectM.Tests
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Plain-Entities determinism test for <see cref="PlayerMoveSystem"/> (the M1 predicted move
|
|
||||||
/// system). Boots a bare ECS world, registers the system in the SimulationSystemGroup, creates a
|
|
||||||
/// synthetic player (PlayerInput + EffectiveCharacterStats + LocalTransform + enabled Simulate),
|
|
||||||
/// injects a fixed delta-time, ticks N times, and asserts the position advanced by exactly
|
|
||||||
/// MoveSpeed * dt * N. As of M3 move speed is read from EffectiveCharacterStats (the data-driven
|
|
||||||
/// effective stat) rather than the removed PlayerMoveStats. Version-independent and netcode-free.
|
|
||||||
/// </summary>
|
|
||||||
public class PlayerMoveSystemTests
|
|
||||||
{
|
|
||||||
[Test]
|
|
||||||
public void PlayerMove_Advances_By_MoveSpeed_Times_Dt_Each_Tick()
|
|
||||||
{
|
|
||||||
using var world = new World("PlayerMoveTestWorld");
|
|
||||||
var simulationGroup = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
|
||||||
var moveSystem = world.GetOrCreateSystem<PlayerMoveSystem>();
|
|
||||||
simulationGroup.AddSystemToUpdateList(moveSystem);
|
|
||||||
simulationGroup.SortSystems();
|
|
||||||
|
|
||||||
var em = world.EntityManager;
|
|
||||||
var entity = em.CreateEntity(
|
|
||||||
typeof(PlayerInput), typeof(EffectiveCharacterStats), typeof(LocalTransform), typeof(Simulate));
|
|
||||||
|
|
||||||
const float moveSpeed = 5f;
|
|
||||||
const float dt = 0.1f;
|
|
||||||
const int ticks = 10;
|
|
||||||
|
|
||||||
em.SetComponentData(entity, LocalTransform.FromPosition(float3.zero));
|
|
||||||
em.SetComponentData(entity, new EffectiveCharacterStats { MoveSpeed = moveSpeed, TurnRateRadiansPerSec = 0f, MaxHealth = 0f });
|
|
||||||
em.SetComponentData(entity, new PlayerInput { Move = new float2(1f, 0f), Aim = float2.zero });
|
|
||||||
|
|
||||||
for (int i = 0; i < ticks; i++)
|
|
||||||
{
|
|
||||||
// Fixed delta so the predicted move is fully deterministic (no wall-clock).
|
|
||||||
world.SetTime(new TimeData(elapsedTime: dt * (i + 1), deltaTime: dt));
|
|
||||||
simulationGroup.Update();
|
|
||||||
}
|
|
||||||
|
|
||||||
var position = em.GetComponentData<LocalTransform>(entity).Position;
|
|
||||||
|
|
||||||
Assert.AreEqual(moveSpeed * dt * ticks, position.x, 1e-3f,
|
|
||||||
"X should advance by MoveSpeed * dt each tick for Move=(1,0).");
|
|
||||||
Assert.AreEqual(0f, position.y, 1e-3f, "Movement is planar; Y should stay 0.");
|
|
||||||
Assert.AreEqual(0f, position.z, 1e-3f, "Move=(1,0) maps to +X only; Z should stay 0.");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void PlayerMove_Is_Idempotent_Across_Equal_Tick_Batches()
|
|
||||||
{
|
|
||||||
// Determinism/idempotence: the same inputs and dt must yield the same result regardless
|
|
||||||
// of how the ticks are grouped (mirrors the prediction loop re-simulating a tick).
|
|
||||||
float3 RunTicks(int ticks)
|
|
||||||
{
|
|
||||||
using var world = new World("PlayerMoveDetWorld");
|
|
||||||
var group = world.GetOrCreateSystemManaged<SimulationSystemGroup>();
|
|
||||||
group.AddSystemToUpdateList(world.GetOrCreateSystem<PlayerMoveSystem>());
|
|
||||||
group.SortSystems();
|
|
||||||
|
|
||||||
var em = world.EntityManager;
|
|
||||||
var e = em.CreateEntity(
|
|
||||||
typeof(PlayerInput), typeof(EffectiveCharacterStats), typeof(LocalTransform), typeof(Simulate));
|
|
||||||
em.SetComponentData(e, LocalTransform.FromPosition(float3.zero));
|
|
||||||
em.SetComponentData(e, new EffectiveCharacterStats { MoveSpeed = 3f, TurnRateRadiansPerSec = 0f, MaxHealth = 0f });
|
|
||||||
em.SetComponentData(e, new PlayerInput { Move = new float2(0f, 1f), Aim = float2.zero });
|
|
||||||
|
|
||||||
for (int i = 0; i < ticks; i++)
|
|
||||||
{
|
|
||||||
world.SetTime(new TimeData(0.05f * (i + 1), 0.05f));
|
|
||||||
group.Update();
|
|
||||||
}
|
|
||||||
|
|
||||||
return em.GetComponentData<LocalTransform>(e).Position;
|
|
||||||
}
|
|
||||||
|
|
||||||
var a = RunTicks(20);
|
|
||||||
var b = RunTicks(20);
|
|
||||||
Assert.AreEqual(a.z, b.z, 1e-4f, "Two identical runs must produce identical positions.");
|
|
||||||
Assert.AreEqual(3f * 0.05f * 20f, a.z, 1e-3f, "Move=(0,1) maps to +Z by MoveSpeed*dt*N.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: aed328b85e7264aa9aedca0a628c6169
|
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"Unity.Transforms",
|
"Unity.Transforms",
|
||||||
"Unity.Collections",
|
"Unity.Collections",
|
||||||
"Unity.Mathematics",
|
"Unity.Mathematics",
|
||||||
|
"Unity.Physics",
|
||||||
"Unity.NetCode",
|
"Unity.NetCode",
|
||||||
"UnityEngine.TestRunner",
|
"UnityEngine.TestRunner",
|
||||||
"UnityEditor.TestRunner"
|
"UnityEditor.TestRunner"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Multiplayer game on **Unity DOTS (Entities) + Netcode for Entities** — server-
|
|||||||
| `com.unity.collections` | 6.4.0 | (transitive) |
|
| `com.unity.collections` | 6.4.0 | (transitive) |
|
||||||
| `com.unity.netcode` | **1.13.2** | Netcode **for Entities** (ECS). NOT `com.unity.netcode.gameobjects`. Independent 1.x line on Unity 6.4. |
|
| `com.unity.netcode` | **1.13.2** | Netcode **for Entities** (ECS). NOT `com.unity.netcode.gameobjects`. Independent 1.x line on Unity 6.4. |
|
||||||
| `com.unity.physics` | **1.4.6** | Unity Physics (DOTS). Independent 1.x line on Unity 6.4. |
|
| `com.unity.physics` | **1.4.6** | Unity Physics (DOTS). Independent 1.x line on Unity 6.4. |
|
||||||
|
| `com.unity.charactercontroller` | **1.4.2** | Unity Character Controller (DOTS, kinematic collide-and-slide). Player movement foundation (M5b). Declares `entities`/`physics` 1.3.15 but resolves on our 6.4.0/1.4.6 via SemVer floor (no downgrade). |
|
||||||
| `com.unity.transport` | 2.7.2 | (transitive) |
|
| `com.unity.transport` | 2.7.2 | (transitive) |
|
||||||
| `com.unity.burst` | 1.8.29 | (transitive) |
|
| `com.unity.burst` | 1.8.29 | (transitive) |
|
||||||
| `com.unity.mathematics` | 1.3.3 | (transitive) |
|
| `com.unity.mathematics` | 1.3.3 | (transitive) |
|
||||||
@@ -50,6 +51,26 @@ Root namespace: **`ProjectM`**. Code lives under `Assets/_Project/Scripts/` in f
|
|||||||
- **In-editor input injection needs a focused Game view — unless you change two settings.** By default the Input System ignores injected/real device input while the Game view is unfocused, so headless (MCP `execute_code`) keypress simulation won't drive `IInputComponentData`. Fix (both now set in this project): `InputSettings.editorInputBehaviorInPlayMode = AllDeviceInputAlwaysGoesToGameView` + `Application.runInBackground = true`. For deterministic, device-independent validation prefer the editor-only **`DebugInputInjectionSystem`** (`ProjectM.Client`, `#if UNITY_EDITOR`): poke its statics from `execute_code` — `DebugInputInjectionSystem.Fire()` / `.SetMove(x,z)` / `.SetAim(x,z)` / `.Stop()` — to drive the local player's `PlayerInput` through the authentic command→prediction pipeline. (Validated: `SetMove` drives + replicates movement. One-shot `Fire` propagation needs a healthy editor — tick-batching under a degraded/corrupt-Burst editor drops one-shot `InputEvent`s while continuous values survive.)
|
- **In-editor input injection needs a focused Game view — unless you change two settings.** By default the Input System ignores injected/real device input while the Game view is unfocused, so headless (MCP `execute_code`) keypress simulation won't drive `IInputComponentData`. Fix (both now set in this project): `InputSettings.editorInputBehaviorInPlayMode = AllDeviceInputAlwaysGoesToGameView` + `Application.runInBackground = true`. For deterministic, device-independent validation prefer the editor-only **`DebugInputInjectionSystem`** (`ProjectM.Client`, `#if UNITY_EDITOR`): poke its statics from `execute_code` — `DebugInputInjectionSystem.Fire()` / `.SetMove(x,z)` / `.SetAim(x,z)` / `.Stop()` — to drive the local player's `PlayerInput` through the authentic command→prediction pipeline. (Validated: `SetMove` drives + replicates movement. One-shot `Fire` propagation needs a healthy editor — tick-batching under a degraded/corrupt-Burst editor drops one-shot `InputEvent`s while continuous values survive.)
|
||||||
- **Prototype presentation glue lives in `ProjectM.Client` as MonoBehaviours.** `PrototypeCameraRig` (on the Main Camera) is a tunable player-following ARPG cam (default mid 3/4 ~45° perspective) that reads the local player ghost's `LocalTransform` each LateUpdate. Bright prototype URP-Lit materials are in `Assets/_Project/Materials/` (player cyan, dummy red, projectile yellow, ground grey). `ProjectM.Client` now references `Unity.Transforms` directly (the rig reads `LocalTransform`).
|
- **Prototype presentation glue lives in `ProjectM.Client` as MonoBehaviours.** `PrototypeCameraRig` (on the Main Camera) is a tunable player-following ARPG cam (default mid 3/4 ~45° perspective) that reads the local player ghost's `LocalTransform` each LateUpdate. Bright prototype URP-Lit materials are in `Assets/_Project/Materials/` (player cyan, dummy red, projectile yellow, ground grey). `ProjectM.Client` now references `Unity.Transforms` directly (the rig reads `LocalTransform`).
|
||||||
|
|
||||||
|
### Build gotchas (learned — M5 physics-in-prediction, 2026-06-01)
|
||||||
|
|
||||||
|
- **Editing Assets `.cs` with the raw `Write` tool does NOT reliably trigger a Unity recompile** on an unfocused editor — `refresh_unity` did a domain reload *without* recompiling, so tests + `execute_code` ran a **stale assembly** (symptom: behaviour that exists in *neither* the old nor new source). **Always edit Assets `.cs` via MCP `apply_text_edits` / `create_script`** (Unity's own scripting pipeline) — never `Write`. (`Write`/`Edit` are fine for non-asset files: vault, asmdef JSON, etc.) See [[2026-06-01_M5_Physics_In_Prediction]].
|
||||||
|
- **Predicted physics is implicit — there is no `PredictedPhysics` toggle.** With the netcode-physics package present (`Unity.NetCode.Physics`, `…Physics.Hybrid`) and predicted ghosts carrying physics components, Netcode relocates `PhysicsSystemGroup` into the **`PredictedFixedStepSimulationSystemGroup`** (a child of `PredictedSimulationSystemGroup`, marked **OrderFirst**). `NetCodePhysicsConfig` only tunes lag-comp / run-mode / history. Put one in the gameplay subscene with `PhysicGroupRunMode = LagCompensationEnabledOrAnyPhysicsEntities` so the group runs whenever physics entities exist.
|
||||||
|
- **Unity Physics 1.x bakes built-in `UnityEngine` colliders + `Rigidbody`** — the old `PhysicsShapeAuthoring`/`PhysicsBodyAuthoring` (Physics 0.x) are **gone** (`unity_reflect` finds neither). Author a dynamic body with a `CapsuleCollider`/`BoxCollider` + `Rigidbody` (`useGravity=false` → planar/`PhysicsGravityFactor`=0; `isKinematic=false`; `interpolation=Interpolate` → `PhysicsGraphicalSmoothing`). Static colliders = collider, no Rigidbody, baked into the subscene (present identically in server + client worlds, deterministic, no replication).
|
||||||
|
- **`PhysicsVelocity` auto-replicates** — Netcode ships `PhysicsVelocityDefaultVariant` + a generated serializer, so a predicted-physics ghost needs **no hand-written `[GhostField]`** for velocity (`LocalTransform` is already replicated). Drive the character by writing `PhysicsVelocity.Linear`, not by teleporting `LocalTransform`.
|
||||||
|
- **`Rigidbody.FreezeRotation` is NOT honored by the DOTS baker** (baked `PhysicsMass.InverseInertia` stays non-zero). Hold a top-down character's facing by zeroing angular velocity each tick + writing rotation directly (`PlayerAimSystem`); set `PhysicsMass.InverseInertia = float3.zero` in a baker/system if a hard lock is needed.
|
||||||
|
- **Gravity-off bodies accumulate vertical contact impulses permanently** (a capsule rides up a box edge and floats away — looks like tunnelling, isn't). Pin players to the movement plane *after* the physics step: a system in `PredictedSimulationSystemGroup` `[UpdateAfter(PredictedFixedStepSimulationSystemGroup)]` clamping Y to `PlayerSpawner.SpawnPoint.y` + zeroing `Linear.y` (`PlayerPlanarConstraintSystem`).
|
||||||
|
- **The predicted physics group is OrderFirst**, so a system in `PredictedSimulationSystemGroup` with `[UpdateBefore(PredictedFixedStepSimulationSystemGroup)]` is **ignored** (OrderFirst/OrderLast wins) → 1-tick velocity offset (consistent across server/client/rollback — prediction stays in sync). For same-tick application, put the system *inside* `PredictedFixedStepSimulationSystemGroup` `[UpdateBefore(Unity.Physics.Systems.PhysicsSystemGroup)]` (verified to sort before the step) — but expect cosmetic "invalid UpdateBefore" warnings from the relocation.
|
||||||
|
|
||||||
|
### Build gotchas (learned — M5b Unity Character Controller, 2026-06-01)
|
||||||
|
|
||||||
|
- **The player is now a Unity Character Controller kinematic character, NOT a dynamic Rigidbody.** `PlayerMoveSystem` + `PlayerPlanarConstraintSystem` (M5) are **deleted**. Movement: `PlayerControlSystem` maps `PlayerInput.Move` × `EffectiveCharacterStats.MoveSpeed` → `CharacterControl` (via the unit-tested `CharacterControlMath.DesiredMovement`); `CharacterProcessor` (collide-and-slide) consumes it in `CharacterPhysicsUpdateSystem` (`[UpdateInGroup(KinematicCharacterPhysicsUpdateGroup)]`, relocated into the predicted loop). The DR-006 predicted-physics infra (`NetCodePhysicsConfig`, baked static walls) is **kept** — the CC character sweeps against that same PhysicsWorld.
|
||||||
|
- **A package declaring an older `com.unity.entities`/`com.unity.physics` dependency can still resolve on our renumbered stack** — Unity treats the dep as a SemVer **floor**, so Entities 6.4.0 satisfies a `1.3.15` requirement and is NOT downgraded. Don't trust a version-string mismatch as "incompatible": **probe** (add the package, confirm `packages-lock.json` kept Entities 6.4.0 / Physics 1.4.6 / Netcode 1.13.2 + a clean compile; rollback if not). CC 1.4.2 verified this way.
|
||||||
|
- **CC 1.4.2 API shape = `IKinematicCharacterProcessor<T>` + `KinematicCharacterDataAccess` + static `KinematicCharacterUtilities.Update_*`.** The legacy `KinematicCharacterAspect` (IAspect, instance `Update_*`) also exists but is NOT what the 1.4.x samples use — verify the installed shape with `unity_reflect`, don't assume. (A sub-agent's package-cache read disagreed with reflect; reflect + first-try clean compile won.)
|
||||||
|
- **`KinematicCharacterUtilities.BakeCharacter` aborts** (logs an error, adds nothing) **if the GameObject has a `Rigidbody`** and requires uniform (1,1,1) scale. The player prefab keeps its `CapsuleCollider` (baked into `PhysicsCollider`) but the M5 `Rigidbody` was removed. Two bakers on one prefab GameObject (`PlayerAuthoring` + `PlayerCharacterAuthoring`) is fine — both resolve the same entity.
|
||||||
|
- **`CharacterInterpolation` must be PredictedClient-only.** `BakeCharacter` adds it to all prefab versions; a `DefaultVariantSystemBase` registers `CharacterInterpolation → [GhostComponent(PrefabType = GhostPrefabType.PredictedClient)]` so it's stripped from server + interpolated-client prefabs (else double-interp on remotes). Verified: server ghost has no `CharacterInterpolation`, client ghost does.
|
||||||
|
- **Do NOT copy the CC sample's global `LocalTransform → DontSerializeVariant`.** It is project-wide and would break the non-character ghosts here (projectiles/dummies/pickups) that rely on stock `LocalTransform` replication. Our CC character replicates position via the normal owner-predicted `LocalTransform` path; only the `CharacterInterpolation` variant is registered.
|
||||||
|
- **Top-down CC config (planar, no gravity):** `AuthoringKinematicCharacterProperties` with `SnapToGround=false`, `InterpolateRotation=false` (rotation owned by `PlayerAimSystem`), `SimulateDynamicBody=false` (players don't physically push each other); gravity is handled in the processor by feeding `float3.zero` to `Update_GroundPushing` and never adding a gravity term. Result: stays on the spawn plane (y≈1) with no planar-pin system.
|
||||||
|
|
||||||
## Bootstrap & worlds
|
## Bootstrap & worlds
|
||||||
|
|
||||||
- `ProjectM.Simulation.GameBootstrap : ClientServerBootstrap` → overrides `Initialize`, sets `AutoConnectPort = 7979` (in-editor auto-connect over IPC; set in M1 — was 0), calls `CreateDefaultClientServerWorlds()`. Entering Play Mode creates separate `ServerWorld` (`WorldFlags.GameServer`) and `ClientWorld` (`WorldFlags.GameClient`) — verified.
|
- `ProjectM.Simulation.GameBootstrap : ClientServerBootstrap` → overrides `Initialize`, sets `AutoConnectPort = 7979` (in-editor auto-connect over IPC; set in M1 — was 0), calls `CreateDefaultClientServerWorlds()`. Entering Play Mode creates separate `ServerWorld` (`WorldFlags.GameServer`) and `ClientWorld` (`WorldFlags.GameClient`) — verified.
|
||||||
|
|||||||
@@ -28,4 +28,14 @@ Unordered pool of candidate work. Promote to a [[Milestones|milestone]] when com
|
|||||||
- [ ] **M3 follow-up — multi-prefab abilities** (a per-ability *different* projectile ghost) needs `ProjectileClassificationSystem` generalized beyond the single shared prefab.
|
- [ ] **M3 follow-up — multi-prefab abilities** (a per-ability *different* projectile ghost) needs `ProjectileClassificationSystem` generalized beyond the single shared prefab.
|
||||||
- [ ] **M3 follow-up — standalone-server debug modifier path** via `IRpcCommand` (current `DebugModifierInjectionSystem` is in-editor single-process only).
|
- [ ] **M3 follow-up — standalone-server debug modifier path** via `IRpcCommand` (current `DebugModifierInjectionSystem` is in-editor single-process only).
|
||||||
- [ ] **M3 follow-up — rate-limited turning** (`PlayerAimSystem` still snaps rotation; `EffectiveCharacterStats.TurnRate` is wired but unused).
|
- [ ] **M3 follow-up — rate-limited turning** (`PlayerAimSystem` still snaps rotation; `EffectiveCharacterStats.TurnRate` is wired but unused).
|
||||||
- [ ] **M3 polish — pickup visuals** (primitive sphere/default material currently); pickup auto-grant feel (continuous overlap).
|
- [ ] **M3 polish — pickup visuals** (primitive sphere/default material currently); pickup auto-grant feel (continuous overlap).
|
||||||
|
- [ ] **M5 — base subscene streaming** (the other half of M5): persistent home-base subscene that streams in/out around players. Physics-in-prediction slice is done ([[DR-006_M5_Physics_In_Prediction]]).
|
||||||
|
- [ ] ~~**M5 follow-up — same-tick movement**~~ — moot after M5b (the CC character runs in the predicted fixed-step group; M5's `PlayerMoveSystem` is deleted). See [[DR-007_M5b_Character_Controller_Package]].
|
||||||
|
- [ ] **M5b follow-up — multi-client CC interpolation validation**: live two-build / thin-client run to confirm remote-peer interpolation smoothness (predicted-only `CharacterInterpolation` variant is in; only single-client validated). Pairs with the deferred M4 real-LAN two-build test.
|
||||||
|
- [ ] **M5b follow-up — player-vs-player collision**: currently non-physical (`SimulateDynamicBody=false`); enable + handle masses if mutual push is wanted.
|
||||||
|
- [ ] **M5b follow-up — KinematicCharacterBody velocity ghost variant** (replicate RelativeVelocity/IsGrounded) if owner-prediction reconciliation looks jittery under latency.
|
||||||
|
- [ ] **M5b follow-up — gravity/verticality**: CC character is gravity-free + planar (no floor collider). Add gravity in the processor + a ground collider + reconsider `SnapToGround` if terrain/verticality is introduced.
|
||||||
|
- [ ] **M5b follow-up — CC package version**: 1.4.2 declares `entities/physics@1.3.x` (resolves via SemVer floor on our 6.4.0/1.4.6). Move to a CC build explicitly targeting Entities 6.x when published.
|
||||||
|
- [ ] **M5 follow-up — physics lag compensation** (`NetCodePhysicsConfig.EnableLagCompensation` + `PhysicsWorldHistorySingleton`) if/when physics-based hit detection replaces the swept-segment server check.
|
||||||
|
- [ ] **M5 follow-up — projectiles as physics bodies** (currently kinematic + swept hit); convert for projectile-vs-world collision.
|
||||||
|
- [ ] **M5 follow-up — hard rotation lock** (`PhysicsMass.InverseInertia=0`); `Rigidbody.FreezeRotation` is not honored by the DOTS baker, so rotation is held by per-tick angular-zero + aim write.
|
||||||
@@ -15,7 +15,7 @@ permalink: gamevault/06-roadmap/milestones
|
|||||||
| **M2 — Combat** | Directional ability fire + deterministic soft auto-target; server-authoritative damage/health | ✅ Done 2026-05-31 — runtime-validated on 6.4.7: input→fire→**predicted projectile**→**swept hit**→server damage→`Health` `[GhostField]` replicated server→client; movement + fire confirmed live; EditMode 22/22. Predicted-projectile + server auto-target + non-Burst classifier — [[DR-003_M2_Combat_Netcode_Architecture]], [[2026-05-31_M2_Combat]]. (Projectile ghost-map errors were later root-caused to a `[ReadOnly]`-write in `ProjectileClassificationSystem` — fixed 2026-06-01, see [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]] — NOT two-editor tick-batching as first thought.) |
|
| **M2 — Combat** | Directional ability fire + deterministic soft auto-target; server-authoritative damage/health | ✅ Done 2026-05-31 — runtime-validated on 6.4.7: input→fire→**predicted projectile**→**swept hit**→server damage→`Health` `[GhostField]` replicated server→client; movement + fire confirmed live; EditMode 22/22. Predicted-projectile + server auto-target + non-Burst classifier — [[DR-003_M2_Combat_Netcode_Architecture]], [[2026-05-31_M2_Combat]]. (Projectile ghost-map errors were later root-caused to a `[ReadOnly]`-write in `ProjectileClassificationSystem` — fixed 2026-06-01, see [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]] — NOT two-editor tick-batching as first thought.) |
|
||||||
| **M3 — Data-driven abilities & modifiers** | Ability **and** character stats authored in ScriptableObjects, baked to DOTS **blob assets**; runtime **flat + % modifier** stacks (upgrades/buffs) → effective stats, server-authoritative + prediction-correct. Pattern slice: refactor the current projectile ability + 1–2 sample abilities onto the data model. | ✅ Done 2026-05-31 — runtime-validated on 6.4.7: blob DB baked into both worlds; data-driven base + replicated `StatModifier` ghost buffer → **identical effective stats on server & owner-predicted client** (held under tick-batching); data-only ability swap; real pickup grant; EditMode 38/38. Blob DB + replicated modifier buffer + every-tick effective recompute — [[DR-004_M3_DataDriven_Abilities_Modifiers]], [[2026-05-31_M3_Data_Driven_Abilities]]. |
|
| **M3 — Data-driven abilities & modifiers** | Ability **and** character stats authored in ScriptableObjects, baked to DOTS **blob assets**; runtime **flat + % modifier** stacks (upgrades/buffs) → effective stats, server-authoritative + prediction-correct. Pattern slice: refactor the current projectile ability + 1–2 sample abilities onto the data model. | ✅ Done 2026-05-31 — runtime-validated on 6.4.7: blob DB baked into both worlds; data-driven base + replicated `StatModifier` ghost buffer → **identical effective stats on server & owner-predicted client** (held under tick-batching); data-only ability swap; real pickup grant; EditMode 38/38. Blob DB + replicated modifier buffer + every-tick effective recompute — [[DR-004_M3_DataDriven_Abilities_Modifiers]], [[2026-05-31_M3_Data_Driven_Abilities]]. |
|
||||||
| **M4 — Co-op** | 2–4 players; client-hosted listen-server (Direct IP/LAN now, Unity Relay later) | 🚧 In progress 2026-06-01 — **LAN slice done + runtime-validated**: no-auto-connect `ConnectionConfig` + request-component host/join, editor auto-host + thin clients, deterministic ring spawn; 3 clients (1 real + 2 thin) connect→spawn (distinct slots)→replicate→clean disconnect; `ConnectionUI` for builds; EditMode 45/45. **Unity Relay + real two-build LAN join deferred** — [[DR-005_M4_Connection_Model_Direct_IP]], [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]]. |
|
| **M4 — Co-op** | 2–4 players; client-hosted listen-server (Direct IP/LAN now, Unity Relay later) | 🚧 In progress 2026-06-01 — **LAN slice done + runtime-validated**: no-auto-connect `ConnectionConfig` + request-component host/join, editor auto-host + thin clients, deterministic ring spawn; 3 clients (1 real + 2 thin) connect→spawn (distinct slots)→replicate→clean disconnect; `ConnectionUI` for builds; EditMode 45/45. **Unity Relay + real two-build LAN join deferred** — [[DR-005_M4_Connection_Model_Direct_IP]], [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]]. |
|
||||||
| **M5 — Home base + physics** | Persistent base subscene streaming + Unity Physics in the predicted loop | ⬜ |
|
| **M5 — Home base + physics** | Persistent base subscene streaming + Unity Physics in the predicted loop | 🚧 In progress 2026-06-01 — **physics-in-prediction slice done + runtime-validated** on 6.4.7: player is a velocity-driven dynamic Unity Physics body in the predicted loop (built-in `CapsuleCollider`+`Rigidbody` bake; `PhysicsVelocity` auto-replicated), collides with baked static walls (stops at the surface, no tunnel/climb-over), planar-pinned, **server == client** with no desync; EditMode 51/51. **Base subscene streaming deferred** to a later pass — [[DR-006_M5_Physics_In_Prediction]], [[2026-06-01_M5_Physics_In_Prediction]]. **M5b (same day): player movement re-founded on the Unity Character Controller package** (`com.unity.charactercontroller` 1.4.2) — kinematic collide-and-slide, owner-predicted, data-driven speed; replaces the dynamic-Rigidbody mover (keeps the DR-006 predicted-physics infra). Runtime-validated (collide-and-slide, planar, server==client, CharacterInterpolation predicted-only); EditMode 47/47 — [[DR-007_M5b_Character_Controller_Package]], [[2026-06-01_M5b_Character_Controller]]. |
|
||||||
| **M6 — Build/placement** | Server-authoritative grid build placement via RPC | ⬜ |
|
| **M6 — Build/placement** | Server-authoritative grid build placement via RPC | ⬜ |
|
||||||
| **M7 — Automation** | Self-running tick-based production chains (deterministic offline catch-up) | ⬜ |
|
| **M7 — Automation** | Self-running tick-based production chains (deterministic offline catch-up) | ⬜ |
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
date: 2026-06-01
|
||||||
|
type: session
|
||||||
|
tags: [session, dots, netcode, physics, prediction, m5]
|
||||||
|
permalink: gamevault/07-sessions/2026/2026-06-01-m5-physics-in-prediction
|
||||||
|
---
|
||||||
|
|
||||||
|
# Session 2026-06-01 — M5 physics-in-prediction (velocity-driven character)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Start **M5 — Home base + physics** ([[Milestones]]). Operator scoped this pass to **physics-in-prediction first**: bring Unity Physics (1.4.6) into Netcode's predicted loop as a deterministic, rollback-safe foundation before base-streaming. Deliverable: the player becomes a dynamic physics body driven by input velocity, colliding with static world geometry + other players, replicating server→client with no desync. Projectiles stay kinematic. Architecture locked in [[DR-006_M5_Physics_In_Prediction]].
|
||||||
|
|
||||||
|
## Done
|
||||||
|
|
||||||
|
- **Velocity-driven movement.** `PlayerMoveSystem` rewritten from teleporting `LocalTransform.Position` → writing `PhysicsVelocity.Linear = dir * EffectiveCharacterStats.MoveSpeed` (Y zeroed, angular zeroed); Unity Physics integrates + resolves contacts. Kept in `PredictedSimulationSystemGroup` (the predicted physics group is OrderFirst, so velocity applies on the next step — a 1-tick offset that is consistent across server/client/rollback). `Unity.Transforms`/`Unity.Physics.Systems` usings dropped.
|
||||||
|
- **Player physics body (built-in authoring).** `Player.prefab` got a `CapsuleCollider` (r 0.5, h 2) + `Rigidbody` (`useGravity=false`, dynamic, `FreezeRotation`, `interpolation=Interpolate`) → bakes `PhysicsCollider`/`PhysicsVelocity`/`PhysicsMass`/`PhysicsGravityFactor`(0)/`PhysicsGraphicalSmoothing`. Unity Physics 1.x bakes built-in colliders + `Rigidbody`; the old `PhysicsShape/BodyAuthoring` are gone.
|
||||||
|
- **Planar pin.** New `PlayerPlanarConstraintSystem` (after the fixed-step physics group) clamps player Y to `PlayerSpawner.SpawnPoint.y` + zeroes vertical velocity — stops gravity-free capsules from riding up box edges and floating away (the actual cause of the first "tunnelling" symptom; see below).
|
||||||
|
- **Subscene content.** `Gameplay.unity` gained a `NetCodePhysicsConfig` singleton (`PhysicGroupRunMode = LagCompensationEnabledOrAnyPhysicsEntities`, `EnableLagCompensation = false`) and 3 static box-collider obstacles (`Pillar_Center`, `Wall_East`, `Wall_North`, red). Re-baked.
|
||||||
|
- **Tests.** `ProjectM.Tests.EditMode` asmdef gained `Unity.Physics`; `PlayerMoveSystemTests` rewritten from teleport-position asserts → velocity-mapping asserts (cardinal, diagonal unit-clamp, sub-unit proportional, determinism + angular-zero).
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- **EditMode 51/51 green** (47 prior suites + 4 rewritten PlayerMove tests); M1–M4 suites unaffected.
|
||||||
|
- **Runtime (single in-editor client, 6.4.7):** player baked with all physics components on **server + client**; CollisionWorld holds 4 collider bodies (player + 3 static), raycast confirms solid walls. Driving +X into `Wall_East` (face x≈5.75) → capsule **stops at x=5.25 = face − radius** on both worlds, **holds under continuous push** (no tunnel, no creep), **Y pinned at 1.00** (no climb-over), and **releases to free movement** on reverse. **Server == client exactly** (prediction in sync). Console clean of all five M2/M4 cascade signatures + Burst "not a known entry point"; only the known unfocused-editor `Server Tick Batching` artifact remains.
|
||||||
|
|
||||||
|
## Diagnosis notes (for future me)
|
||||||
|
|
||||||
|
- **First symptom looked like tunnelling; it was climb-over.** Colliders were solid (raycast hit), but with gravity off the capsule took a vertical contact impulse, rose above the short (y[0,1]) walls, and floated away at constant velocity. Fix = planar pin + taller walls. Not a collision-detection bug.
|
||||||
|
- **A whole detour was caused by `Write`-tool edits to `.cs` not triggering a Unity recompile** on this unfocused editor — tests/`execute_code` ran a **stale assembly** (velocity showed `MoveSpeed*dt`, angular not zeroed: code that existed in neither old nor new source). Switching to MCP `apply_text_edits` (Unity's own scripting pipeline) fixed it immediately. **Always edit Assets `.cs` via `apply_text_edits`/`create_script`, never the raw `Write` tool.**
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
- [[DR-006_M5_Physics_In_Prediction]] — implicit predicted physics (no toggle); built-in collider+Rigidbody authoring; velocity-driven move; `PhysicsVelocity` auto-replication; planar pin; baked static world. Physics-first ordering (1-tick offset); `FreezeRotation` not baked → angular-zero held in code.
|
||||||
|
|
||||||
|
## Open / deferred
|
||||||
|
- **Base subscene streaming** — the other half of M5 (persistent home base, stream in/out around players). Not started this pass.
|
||||||
|
- **Lag compensation** — `EnableLagCompensation=false`; revisit if/when physics-based hit detection replaces the swept-segment server check.
|
||||||
|
- **Same-tick movement** — currently a 1-tick velocity offset (physics group is OrderFirst). Move `PlayerMoveSystem` into the fixed-step group `[UpdateBefore(PhysicsSystemGroup)]` if responsiveness needs it (costs 2 cosmetic warnings).
|
||||||
|
- **Projectiles as physics bodies** — still kinematic + swept-hit; convert if projectile-vs-world collision is wanted.
|
||||||
|
- **Rotation lock** — relying on per-tick angular-zero + aim write; set `PhysicsMass.InverseInertia=0` if a hard lock is needed.
|
||||||
|
- **Player-vs-player collision** is server-authoritative (both dynamic on server); on clients others are interpolated, so peer collisions are approximate and server-corrected — fine for 2–4.
|
||||||
|
|
||||||
|
## Next
|
||||||
|
Either (a) the **base subscene streaming** half of M5, or (b) layer **Unity Relay** onto M4's `ConnectionConfig` to make co-op remote-playable. Recommend (a) to complete M5.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
date: 2026-06-01
|
||||||
|
type: session
|
||||||
|
tags: [session, dots, netcode, physics, character-controller, prediction, m5]
|
||||||
|
permalink: gamevault/07-sessions/2026/2026-06-01-m5b-character-controller
|
||||||
|
---
|
||||||
|
|
||||||
|
# Session 2026-06-01 — M5b: adopt the Unity Character Controller package
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Operator decision: replace the M5 dynamic-Rigidbody player ([[DR-006_M5_Physics_In_Prediction]]) with Unity's **Character Controller package** (kinematic, collide-and-slide, DOTS, netcode-predicted) as the player's movement foundation. Run via `/dots-dev` with a multi-agent research workflow. Architecture locked in [[DR-007_M5b_Character_Controller_Package]].
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
- **Read-only research workflow** (6 parallel Explore agents → synthesis): compatibility (two angles), CC netcode setup, CC core API, codebase impact. Returned a unified plan + a hard compatibility verdict.
|
||||||
|
- **Compatibility gate (the make-or-break):** research said CC 1.4.2 declares `entities/physics@1.3.15` vs our Entities 6.4.0 (Unity-6 renumber) — *likely incompatible*. Resolved by an **empirical install probe** (operator-gated): added the package, confirmed the lock kept Entities 6.4.0 / Physics 1.4.6 / Netcode 1.13.2 (no downgrade) and compiled clean. The floors were satisfied → **it works**. Probe → pause → operator "go".
|
||||||
|
- **Port:** a sub-agent fetched the OnlineFPS netcode-character sample verbatim; I reconciled its API against the **installed** 1.4.2 via `unity_reflect` (the canonical path is `IKinematicCharacterProcessor<T>` + `KinematicCharacterDataAccess` + static `KinematicCharacterUtilities.Update_*`; the legacy `KinematicCharacterAspect` also exists but isn't what the samples use). Authored a minimal top-down adaptation.
|
||||||
|
|
||||||
|
## Done
|
||||||
|
|
||||||
|
- **New (`ProjectM.Simulation`):** `CharacterComponent` + `CharacterControl` (CharacterComponents.cs); `CharacterControlMath.DesiredMovement` (unit-test seam); `CharacterProcessor` (the IKinematicCharacterProcessor running the Update_* sequence, gravity-free, velocity-controlled); `CharacterPhysicsUpdateSystem` (`KinematicCharacterPhysicsUpdateGroup`); `PlayerControlSystem` (PlayerInput × MoveSpeed → CharacterControl); `CharacterGhostVariants` (CharacterInterpolation → PredictedClient-only).
|
||||||
|
- **New (`ProjectM.Authoring`):** `PlayerCharacterAuthoring` — baker calls `KinematicCharacterUtilities.BakeCharacter` (top-down props) + adds CharacterComponent/CharacterControl.
|
||||||
|
- **Modified:** `ProjectM.Simulation.asmdef` + `ProjectM.Authoring.asmdef` (+`Unity.CharacterController`, Authoring +`Unity.Physics`); `Player.prefab` (removed M5 Rigidbody, kept CapsuleCollider, added PlayerCharacterAuthoring, scale 1); `StatRecomputeSystem` (dropped the now-dangling `[UpdateBefore(PlayerMoveSystem)]`).
|
||||||
|
- **Deleted:** `PlayerMoveSystem`, `PlayerPlanarConstraintSystem` (CC + no-gravity stays planar — no constraint needed); `PlayerMoveSystemTests` → `CharacterControlMathTests`.
|
||||||
|
- **Kept:** PlayerInput + input gather + DebugInputInjectionSystem, EffectiveCharacterStats/StatRecompute, GhostOwner/spawn/co-op ring, PlayerAimSystem, combat/health, the `NetCodePhysicsConfig` + baked static walls from DR-006.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- **EditMode 47/47 green** (4 new CharacterControlMath tests replace the 4 old PhysicsVelocity-mapping tests).
|
||||||
|
- **Runtime (single in-editor client, 6.4.7):** player bakes the full CC set (`KinematicCharacterBody/Properties`, `PhysicsCollider`, `CharacterControl`, `CharacterComponent`, `CharacterInterpolation`, the 4 CC buffers) on **both** worlds; spawns at the ring slot (2.5,1,0). Driving +X stops the capsule at **x=5.24** (= `Wall_East` face − radius) on **server and client**; diagonal input **slides along** the wall and rounds its finite end (collide-and-slide, no tunnel); **Y holds 1.00 with no planar-pin system**; **server == client**. `CharacterInterpolation` confirmed on the client ghost, **absent on the server** (predicted-only variant works). Console clean of CC/Burst/cascade signatures — only the known unfocused-editor tick-batching artifact.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
- [[DR-007_M5b_Character_Controller_Package]] — CC 1.4.2 resolves on our stack (SemVer floors, no downgrade); kinematic collide-and-slide owner-predicted character via the static-utilities pattern; data-driven velocity from existing PlayerInput/stats; CharacterInterpolation predicted-only; do NOT globally DontSerialize LocalTransform (protects non-character ghosts); supersedes the DR-006 dynamic-Rigidbody mover, keeps the DR-006 predicted-physics infra.
|
||||||
|
|
||||||
|
## Diagnosis notes (for future me)
|
||||||
|
- **The compatibility "no" was a false alarm** — a package declaring an older `entities@1.3.x` dependency still resolves against the renumbered Entities 6.4.0 (SemVer floor); always **probe** rather than trust the version-string mismatch. Verify the lock didn't downgrade + the package compiles.
|
||||||
|
- **Trust `unity_reflect` over sub-agent package-cache reads** for the installed API shape: reflect showed both the aspect (legacy) and the static-utilities path; the samples use the latter, which compiled first-try.
|
||||||
|
- Continued to edit Assets `.cs` exclusively via MCP `create_script`/`apply_text_edits` (see [[2026-06-01_M5_Physics_In_Prediction]] / the Write-tool stale-assembly trap).
|
||||||
|
|
||||||
|
## Open / deferred
|
||||||
|
- **Multi-client co-op interpolation** — validate remote-peer smoothness with a live two-build / thin-client run (predicted-only `CharacterInterpolation` variant is in place; single-client validated).
|
||||||
|
- **Player-vs-player non-physical** (`SimulateDynamicBody=false`); **gravity-free/no floor** (planar); **CC declares older deps** (revisit on CC bump); optional **KinematicCharacterBody velocity ghost variant** for tighter reconciliation under latency.
|
||||||
|
|
||||||
|
## Next
|
||||||
|
Recommend a **real two-build LAN co-op smoke test** (also closes the M4 deferral) to validate remote CC interpolation, then resume the **base subscene streaming** half of M5.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
id: DR-006
|
||||||
|
title: M5 physics-in-prediction — velocity-driven character on Unity Physics in the predicted loop; planar pin
|
||||||
|
status: accepted
|
||||||
|
date: 2026-06-01
|
||||||
|
tags:
|
||||||
|
- decision
|
||||||
|
- netcode
|
||||||
|
- physics
|
||||||
|
- prediction
|
||||||
|
- m5
|
||||||
|
permalink: gamevault/07-sessions/decisions/dr-006-m5-physics-in-prediction
|
||||||
|
---
|
||||||
|
|
||||||
|
# DR-006 — M5 Physics-in-Prediction (velocity-driven character; planar pin)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
M5 ([[Milestones]]) = "home base + physics". The operator scoped this first pass to **physics-in-prediction**: stand up Unity Physics (1.4.6) *inside* Netcode's predicted simulation loop before any base-streaming work, because deterministic, rollback-safe physics is the highest-risk netcode surface in the roadmap. Through M4 the player moved by **teleporting `LocalTransform.Position`** (`PlayerMoveSystem`, kinematic) — no collision, no bodies. `Unity.Physics` was already referenced by `ProjectM.Simulation`, and the netcode-physics integration assemblies (`Unity.NetCode.Physics`, `…Physics.Hybrid`) ship with our stack. Validated against Unity package docs + `unity_reflect` against the installed packages (context7 MCP was unavailable this session — fell back to official docs + reflection). Extends [[DR-005_M4_Connection_Model_Direct_IP]].
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. **Predicted physics is implicit — no toggle.** With the netcode-physics package present and predicted ghosts carrying physics components, Netcode relocates `PhysicsSystemGroup` into the `PredictedFixedStepSimulationSystemGroup` (a child of `PredictedSimulationSystemGroup`, marked **OrderFirst**). `NetCodePhysicsConfig` has **no `PredictedPhysics` field** — it only tunes lag-comp / run-mode / history. A single `NetCodePhysicsConfig` lives in the `Gameplay` subscene with `PhysicGroupRunMode = LagCompensationEnabledOrAnyPhysicsEntities` (so the predicted physics group runs whenever physics entities exist) and `EnableLagCompensation = false` (our damage is swept-segment + server-side; lag comp deferred).
|
||||||
|
2. **The player is a velocity-driven dynamic body, authored with built-in components.** Unity Physics **1.x bakes built-in `UnityEngine` colliders + `Rigidbody`** — the old `PhysicsBodyAuthoring`/`PhysicsShapeAuthoring` (Physics 0.x) are gone. `Player.prefab` gets a `CapsuleCollider` (r 0.5, h 2) + a `Rigidbody` (`useGravity=false` → planar, `isKinematic=false`, `FreezeRotation`, `interpolation=Interpolate`). These bake to `PhysicsCollider` / `PhysicsVelocity` / `PhysicsMass` / `PhysicsGravityFactor`(0) / `PhysicsGraphicalSmoothing`. `PlayerMoveSystem` now writes `PhysicsVelocity.Linear = dir * MoveSpeed` (Y zeroed, angular zeroed) instead of teleporting; the solver integrates + resolves contacts.
|
||||||
|
3. **`PhysicsVelocity` auto-replicates.** Netcode ships `PhysicsVelocityDefaultVariant` + a generated serializer, so no hand-written `[GhostField]` is needed; `LocalTransform` is already replicated on the player ghost. Owner-predicted player (`DefaultGhostMode=OwnerPredicted`) → owner predicts physics, other clients interpolate (Simulate disabled), server is authoritative.
|
||||||
|
4. **Players are pinned to the movement plane after the step.** `PlayerPlanarConstraintSystem` (`PredictedSimulationSystemGroup`, `[UpdateAfter(PredictedFixedStepSimulationSystemGroup)]`) clamps each predicted player's Y to `PlayerSpawner.SpawnPoint.y` (single source) and zeroes vertical velocity. Required because with gravity off, any vertical contact impulse (a capsule riding a box edge) is **permanent** — without the pin the character climbs over obstacles and floats away.
|
||||||
|
5. **Static world geometry is baked, not ghosted.** Three static box colliders (`Pillar_Center`, `Wall_East`, `Wall_North`, red `M_Dummy`) added to the `Gameplay` subscene — present identically in server + client worlds (deterministic, no replication). Projectiles stay kinematic this pass.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Validated end to end on 6.4.7.** EditMode 51/51 (4 new `PlayerMoveSystemTests`: cardinal map, diagonal unit-clamp, sub-unit proportional, determinism+angular-zero). Runtime: player baked with all physics components on **both** worlds; 4 `PhysicsCollider` bodies; driving +X into `Wall_East` (face x≈5.75) stops the capsule at **x=5.25** (= face − radius) on server **and** client, holds there under continuous push (no tunnel, no creep), Y pinned at 1.00 (no climb-over), and releases to free movement on reverse. **Server == client exactly** — prediction in sync, console clean of all five M2/M4 cascade signatures and Burst errors.
|
||||||
|
- **Ordering: physics runs *before* `PlayerMoveSystem` (1-tick velocity offset).** The fixed-step group is **OrderFirst**, so `[UpdateBefore]` against it is ignored (the attribute was removed to keep the console warning-free). The offset is identical on server + owning client + rollback, so prediction stays consistent and the lag is absorbed by prediction. If same-tick responsiveness is ever needed, move `PlayerMoveSystem` *into* `PredictedFixedStepSimulationSystemGroup` `[UpdateBefore(PhysicsSystemGroup)]` (verified to sort correctly) — at the cost of two cosmetic "invalid UpdateBefore" warnings from the netcode physics relocation.
|
||||||
|
- **`Rigidbody.FreezeRotation` is NOT honored by the DOTS baker** — baked `PhysicsMass.InverseInertia` stayed non-zero. Rotation is instead held by zeroing angular velocity each tick (`PlayerMoveSystem`) + `PlayerAimSystem` writing the facing directly. If precise rotation lock is needed later, set `PhysicsMass.InverseInertia = 0` in a baker/system.
|
||||||
|
- **The known unfocused-editor `Server Tick Batching` artifact persists** (1.25–1.75 ticks/frame) — clears when the Game view is focused / in a build (see [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]]). It does not desync the physics in-session.
|
||||||
|
- **Determinism caveat (deferred):** Unity Physics is same-binary deterministic, which is what client re-simulation needs; cross-platform float determinism is not guaranteed but the server is authoritative and corrects via snapshots + `PhysicsGraphicalSmoothing`. No lag compensation yet (`EnableLagCompensation=false`).
|
||||||
|
|
||||||
|
Mirrors the server-authoritative + client-prediction pillars from [[Pillars]]; unblocks the base-streaming half of M5.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
id: DR-007
|
||||||
|
title: M5b — adopt Unity Character Controller package for the predicted player (replaces the dynamic-Rigidbody mover)
|
||||||
|
status: accepted
|
||||||
|
date: 2026-06-01
|
||||||
|
tags:
|
||||||
|
- decision
|
||||||
|
- netcode
|
||||||
|
- physics
|
||||||
|
- character-controller
|
||||||
|
- prediction
|
||||||
|
- m5
|
||||||
|
permalink: gamevault/07-sessions/decisions/dr-007-m5b-character-controller-package
|
||||||
|
---
|
||||||
|
|
||||||
|
# DR-007 — M5b Unity Character Controller (kinematic, owner-predicted)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The M5 player ([[DR-006_M5_Physics_In_Prediction]]) was a **dynamic Unity Physics Rigidbody** driven by setting `PhysicsVelocity` each tick. It worked, but only with workarounds the kinematic approach avoids by design: a planar-pin system (gravity-off bodies accumulate vertical contact impulses and climb over walls), per-tick angular-zeroing, and `Rigidbody.FreezeRotation` (which the DOTS baker ignores). The operator chose to adopt Unity's **Character Controller package** (`com.unity.charactercontroller`) — a DOTS, collide-and-slide kinematic controller with first-class Netcode-for-Entities prediction — as the player's movement foundation. Researched via a read-only workflow + verified against the installed API with `unity_reflect`. Supersedes the **movement** half of DR-006; the predicted-physics infrastructure DR-006 stood up (predicted physics group, `NetCodePhysicsConfig`, baked static walls) is **kept** and is exactly what the CC character sweeps against.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. **CC version: `com.unity.charactercontroller` 1.4.2 — verified to resolve on our stack.** It declares `com.unity.entities@1.3.15` / `com.unity.physics@1.3.15`, but Unity treats these as SemVer **floors**: the resolver kept our **Entities 6.4.0 / Physics 1.4.6 / Netcode 1.13.2 / Collections 6.4.0** with **no downgrade**, and CC 1.4.2 compiled clean against the renumbered Entities 6.x. (Empirical install probe was the gate; the docs' "installation will fail" prediction was wrong.)
|
||||||
|
2. **Kinematic collide-and-slide, owner-predicted.** The player is a `KinematicCharacterBody` (no Rigidbody). `BakeCharacter` adds the CC component/buffer set + bakes the prefab's `CapsuleCollider` into `PhysicsCollider`. Top-down config: `SnapToGround=false`, gravity handled in the processor (fed `float3.zero`), `InterpolateRotation=false` (rotation stays owned by `PlayerAimSystem`), `SimulateDynamicBody=false`.
|
||||||
|
3. **CC API pattern (1.4.2): `IKinematicCharacterProcessor<T>` + `KinematicCharacterDataAccess` + static `KinematicCharacterUtilities.Update_*`.** The legacy `KinematicCharacterAspect` (IAspect, instance `Update_*`) still exists but the static-utilities path is what the 1.4.x samples use — confirmed via reflect. The processor runs the canonical Update sequence (Initialize→ParentMovement→Grounding→[velocity control]→PreventGrounding→GroundPushing→MovementAndDecollisions→…) in `CharacterPhysicsUpdateSystem` (`[UpdateInGroup(KinematicCharacterPhysicsUpdateGroup)]`, which netcode relocates into the predicted fixed-step loop).
|
||||||
|
4. **Data-driven + reuse the existing input.** `PlayerControlSystem` (`PredictedSimulationSystemGroup`, `[UpdateAfter(StatRecomputeSystem)]`) maps the replicated `PlayerInput.Move` × `EffectiveCharacterStats.MoveSpeed` → a non-replicated `CharacterControl.MoveVelocity` (clamp via the unit-tested `CharacterControlMath.DesiredMovement`); the processor lerps `RelativeVelocity` toward it. No second input component; spawn / GhostOwner / co-op ring unchanged.
|
||||||
|
5. **Ghost variants: only `CharacterInterpolation` → PredictedClient-only.** `BakeCharacter` adds `CharacterInterpolation` to every prefab version; a `DefaultVariantSystemBase` + `[GhostComponentVariation(typeof(CharacterInterpolation))] [GhostComponent(PrefabType = GhostPrefabType.PredictedClient)]` strips it from server + interpolated-client prefabs (presentation-only; double-interp on remotes otherwise). **We deliberately do NOT register the CC sample's global `LocalTransform → DontSerializeVariant`** — that is project-wide and would break our non-character ghosts (projectiles/dummies/pickups) that rely on stock `LocalTransform` replication. The character replicates position via the normal owner-predicted `LocalTransform` path.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Validated on 6.4.7.** EditMode 47/47 (the 4 M5 PhysicsVelocity-mapping tests replaced by 4 `CharacterControlMath` tests). Runtime (single in-editor client): player bakes the full CC set on **both** worlds; driving into `Wall_East` (face x≈5.75) stops the capsule at **x=5.24** = face − radius on server **and** client; diagonal input **slides along** the wall and rounds its finite end (true collide-and-slide, not tunnelling); **Y holds at 1.00 with no planar-pin system** (kinematic + no gravity stays on plane); **server == client**; `CharacterInterpolation` confirmed present on the client ghost and **absent on the server** (variant works). Console clean of CC/Burst/cascade errors — only the known unfocused-editor tick-batching artifact.
|
||||||
|
- **Net code simplification.** Deleted `PlayerMoveSystem` + `PlayerPlanarConstraintSystem` (and removed `StatRecomputeSystem`'s `[UpdateBefore(PlayerMoveSystem)]`). The climb-over / rotation-lock / planar-pin workarounds from DR-006 are gone — the kinematic controller handles it natively.
|
||||||
|
- **Asmdefs:** `ProjectM.Simulation` and `ProjectM.Authoring` now reference `Unity.CharacterController` (Authoring also `Unity.Physics` for `BakeCharacter`).
|
||||||
|
- **Stack:** new dependency `com.unity.charactercontroller@1.4.2` in `manifest.json` / lock; core DOTS versions unchanged.
|
||||||
|
|
||||||
|
## Open / deferred
|
||||||
|
|
||||||
|
- **Multi-client co-op interpolation not headlessly validated.** Single owner-predicted client is validated; remote interpolation smoothness (and the predicted-only `CharacterInterpolation` variant's effect on a *real* second client) needs a live two-build / thin-client check. The variant is in place per Unity's documented setup; CC 1.4.2 also guards `CharacterInterpolationSystem` to enabled-`Simulate` entities only.
|
||||||
|
- **Player-vs-player is non-physical** (`SimulateDynamicBody=false`): kinematic characters don't shove each other. Revisit if mutual push is wanted (set true + handle masses in `OverrideDynamicHitMasses`).
|
||||||
|
- **Gravity-free / no floor collider** — planar top-down; if verticality/terrain is added, give the character gravity in the processor + a ground collider and reconsider `SnapToGround`.
|
||||||
|
- **CC 1.4.2 declares older package deps** (entities/physics 1.3.15). Re-check on any CC bump; a CC build explicitly targeting Entities 6.x ("1.5+") would be preferable when available.
|
||||||
|
- **`KinematicCharacterBody` velocity ghost variant** (replicate `RelativeVelocity`/`IsGrounded` for tighter reconciliation) was left at defaults; add if owner-prediction reconciliation looks jittery under latency.
|
||||||
|
|
||||||
|
Mirrors the server-authoritative + client-prediction + small-co-op pillars from [[Pillars]]. Builds on [[DR-006_M5_Physics_In_Prediction]] (kept infra) and [[DR-005_M4_Connection_Model_Direct_IP]] (spawn/co-op).
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#main",
|
"com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#main",
|
||||||
"com.unity.ai.navigation": "2.0.12",
|
"com.unity.ai.navigation": "2.0.12",
|
||||||
|
"com.unity.charactercontroller": "1.4.2",
|
||||||
"com.unity.cinemachine": "3.1.6",
|
"com.unity.cinemachine": "3.1.6",
|
||||||
"com.unity.collab-proxy": "2.12.4",
|
"com.unity.collab-proxy": "2.12.4",
|
||||||
"com.unity.entities": "6.5.0",
|
"com.unity.entities": "6.5.0",
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
"com.unity.test-framework": "1.8.0",
|
"com.unity.test-framework": "1.8.0",
|
||||||
"com.unity.timeline": "1.8.12",
|
"com.unity.timeline": "1.8.12",
|
||||||
"com.unity.ugui": "2.6.0",
|
"com.unity.ugui": "2.6.0",
|
||||||
"com.unity.visualeffectgraph": "17.6.0",
|
"com.unity.visualeffectgraph": "17.4.0",
|
||||||
"com.unity.visualscripting": "1.9.11",
|
"com.unity.visualscripting": "1.9.11",
|
||||||
"com.unity.modules.accessibility": "1.0.0",
|
"com.unity.modules.accessibility": "1.0.0",
|
||||||
"com.unity.modules.adaptiveperformance": "1.0.0",
|
"com.unity.modules.adaptiveperformance": "1.0.0",
|
||||||
|
|||||||
Generated
+12
@@ -37,6 +37,18 @@
|
|||||||
},
|
},
|
||||||
"url": "https://packages.unity.com"
|
"url": "https://packages.unity.com"
|
||||||
},
|
},
|
||||||
|
"com.unity.charactercontroller": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"depth": 0,
|
||||||
|
"source": "registry",
|
||||||
|
"dependencies": {
|
||||||
|
"com.unity.physics": "1.3.15",
|
||||||
|
"com.unity.entities": "1.3.15",
|
||||||
|
"com.unity.modules.physics": "1.0.0",
|
||||||
|
"com.unity.modules.uielements": "1.0.0"
|
||||||
|
},
|
||||||
|
"url": "https://packages.unity.com"
|
||||||
|
},
|
||||||
"com.unity.cinemachine": {
|
"com.unity.cinemachine": {
|
||||||
"version": "3.1.6",
|
"version": "3.1.6",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &1
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 53
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 6762c37d3236e2a4382cd018dcdf221e, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Unity.Multiplayer.Tools.Common::Unity.Multiplayer.Tools.Common.Visualization.CustomColorSettings
|
||||||
|
colors:
|
||||||
|
m_Keys:
|
||||||
|
m_Values: []
|
||||||
Reference in New Issue
Block a user