CC Package and Physics
This commit is contained in:
@@ -14,6 +14,8 @@ GameObject:
|
||||
- component: {fileID: 8290370249712404227}
|
||||
- component: {fileID: 304484164735584996}
|
||||
- component: {fileID: 1666753161128106451}
|
||||
- component: {fileID: 3388430386408803303}
|
||||
- component: {fileID: 2040450355011915060}
|
||||
m_Layer: 0
|
||||
m_Name: Player
|
||||
m_TagString: Untagged
|
||||
@@ -147,3 +149,66 @@ MonoBehaviour:
|
||||
Importance: 1
|
||||
MaxSendRate: 0
|
||||
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.Collections",
|
||||
"Unity.Mathematics",
|
||||
"Unity.NetCode"
|
||||
"Unity.NetCode",
|
||||
"Unity.Physics",
|
||||
"Unity.CharacterController"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace ProjectM.Simulation
|
||||
/// AbilityRef / CharacterStatsRef) with its replicated StatModifier buffer into the
|
||||
/// 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
|
||||
/// 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
|
||||
@@ -19,7 +19,6 @@ namespace ProjectM.Simulation
|
||||
/// </summary>
|
||||
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
|
||||
[UpdateBefore(typeof(PlayerAimSystem))]
|
||||
[UpdateBefore(typeof(PlayerMoveSystem))]
|
||||
[BurstCompile]
|
||||
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.Burst",
|
||||
"Unity.Physics",
|
||||
"Unity.NetCode"
|
||||
"Unity.NetCode",
|
||||
"Unity.CharacterController"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
|
||||
@@ -169,6 +169,118 @@ Transform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 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
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -214,6 +326,57 @@ Transform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 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
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -245,6 +408,8 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.PlayerSpawnerAuthoring
|
||||
PlayerPrefab: {fileID: 2218851646297572645, guid: a27bbed2662454377bd25279ee4a14d2, type: 3}
|
||||
SpawnPoint: {x: 0, y: 1, z: 0}
|
||||
SpawnRingRadius: 2.5
|
||||
RingSlots: 4
|
||||
--- !u!4 &1498433572
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -308,6 +473,230 @@ Transform:
|
||||
m_Children: []
|
||||
m_Father: {fileID: 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
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -365,3 +754,7 @@ SceneRoots:
|
||||
- {fileID: 2143686867}
|
||||
- {fileID: 409538539}
|
||||
- {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.Collections",
|
||||
"Unity.Mathematics",
|
||||
"Unity.Physics",
|
||||
"Unity.NetCode",
|
||||
"UnityEngine.TestRunner",
|
||||
"UnityEditor.TestRunner"
|
||||
|
||||
Reference in New Issue
Block a user