Netcode Bootstrap
This commit is contained in:
@@ -19,7 +19,7 @@ MonoBehaviour:
|
||||
m_StripRuntimeDebugShaders: 1
|
||||
m_URPShaderStrippingSetting:
|
||||
m_Version: 0
|
||||
m_StripUnusedPostProcessingVariants: 1
|
||||
m_StripUnusedPostProcessingVariants: 0
|
||||
m_StripUnusedVariants: 1
|
||||
m_StripScreenCoordOverrideVariants: 1
|
||||
m_ShaderVariantLogLevel: 0
|
||||
@@ -60,16 +60,21 @@ MonoBehaviour:
|
||||
- rid: 6670095613664165896
|
||||
- rid: 6670095613664165897
|
||||
- rid: 6670095613664165898
|
||||
- rid: 6670095613664165899
|
||||
- rid: 6670095613664165900
|
||||
- rid: -2
|
||||
- rid: -2
|
||||
- rid: 6670095613664165901
|
||||
- rid: 6670095613664165902
|
||||
- rid: 6670095613664165903
|
||||
- rid: 6670095613664165904
|
||||
- rid: 6670095613664165905
|
||||
- rid: -2
|
||||
- rid: -2
|
||||
- rid: -2
|
||||
- rid: 6670095651985424593
|
||||
- rid: 6670095651985424594
|
||||
m_RuntimeSettings:
|
||||
m_List: []
|
||||
m_AssetVersion: 10
|
||||
m_AssetVersion: 11
|
||||
m_ObsoleteDefaultVolumeProfile: {fileID: 0}
|
||||
m_RenderingLayerNames:
|
||||
- Light Layer default
|
||||
@@ -99,6 +104,8 @@ MonoBehaviour:
|
||||
references:
|
||||
version: 2
|
||||
RefIds:
|
||||
- rid: -2
|
||||
type: {class: , ns: , asm: }
|
||||
- rid: 6670095613664165891
|
||||
type: {class: RayTracingRenderPipelineResources, ns: UnityEngine.Rendering.UnifiedRayTracing, asm: Unity.UnifiedRayTracing.Runtime}
|
||||
data:
|
||||
@@ -209,23 +216,6 @@ MonoBehaviour:
|
||||
data:
|
||||
version: 1
|
||||
useReflectionProbeRotation: 0
|
||||
- rid: 6670095613664165899
|
||||
type: {class: ScreenSpaceAmbientOcclusionDynamicResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
||||
data:
|
||||
m_BlueNoise256Textures:
|
||||
- {fileID: 2800000, guid: 36f118343fc974119bee3d09e2111500, type: 3}
|
||||
- {fileID: 2800000, guid: 4b7b083e6b6734e8bb2838b0b50a0bc8, type: 3}
|
||||
- {fileID: 2800000, guid: c06cc21c692f94f5fb5206247191eeee, type: 3}
|
||||
- {fileID: 2800000, guid: cb76dd40fa7654f9587f6a344f125c9a, type: 3}
|
||||
- {fileID: 2800000, guid: e32226222ff144b24bf3a5a451de54bc, type: 3}
|
||||
- {fileID: 2800000, guid: 3302065f671a8450b82c9ddf07426f3a, type: 3}
|
||||
- {fileID: 2800000, guid: 56a77a3e8d64f47b6afe9e3c95cb57d5, type: 3}
|
||||
m_Version: 0
|
||||
- rid: 6670095613664165900
|
||||
type: {class: ScreenSpaceAmbientOcclusionPersistentResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
||||
data:
|
||||
m_Shader: {fileID: 4800000, guid: 0849e84e3d62649e8882e9d6f056a017, type: 3}
|
||||
m_Version: 0
|
||||
- rid: 6670095613664165901
|
||||
type: {class: URPTerrainShaderSetting, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
||||
data:
|
||||
@@ -278,11 +268,28 @@ MonoBehaviour:
|
||||
_skyBoxMesh: {fileID: 4300000, guid: 0529e6c5f6dea8c4a8c2835ed7de57cb, type: 2}
|
||||
_sixFaceSkyBoxMesh: {fileID: 4300000, guid: a80925ceebd011741b42509226cefc74, type: 2}
|
||||
_buildLightGridShader: {fileID: 7200000, guid: 16e47c1641bd0104e92b624601457bb0, type: 3}
|
||||
- rid: 6670095651985424593
|
||||
type: {class: ScreenSpaceAmbientOcclusionDynamicResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
||||
data:
|
||||
m_BlueNoise256Textures:
|
||||
- {fileID: 2800000, guid: 36f118343fc974119bee3d09e2111500, type: 3}
|
||||
- {fileID: 2800000, guid: 4b7b083e6b6734e8bb2838b0b50a0bc8, type: 3}
|
||||
- {fileID: 2800000, guid: c06cc21c692f94f5fb5206247191eeee, type: 3}
|
||||
- {fileID: 2800000, guid: cb76dd40fa7654f9587f6a344f125c9a, type: 3}
|
||||
- {fileID: 2800000, guid: e32226222ff144b24bf3a5a451de54bc, type: 3}
|
||||
- {fileID: 2800000, guid: 3302065f671a8450b82c9ddf07426f3a, type: 3}
|
||||
- {fileID: 2800000, guid: 56a77a3e8d64f47b6afe9e3c95cb57d5, type: 3}
|
||||
m_Version: 0
|
||||
- rid: 6670095651985424594
|
||||
type: {class: ScreenSpaceAmbientOcclusionPersistentResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
||||
data:
|
||||
m_Shader: {fileID: 4800000, guid: 0849e84e3d62649e8882e9d6f056a017, type: 3}
|
||||
m_Version: 0
|
||||
- rid: 6852985685364965376
|
||||
type: {class: URPShaderStrippingSetting, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
||||
data:
|
||||
m_Version: 0
|
||||
m_StripUnusedPostProcessingVariants: 1
|
||||
m_StripUnusedPostProcessingVariants: 0
|
||||
m_StripUnusedVariants: 1
|
||||
m_StripScreenCoordOverrideVariants: 1
|
||||
- rid: 6852985685364965377
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d457270f9cc024880891ec4f93a2366b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,148 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &2218851646297572645
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 194292233036670670}
|
||||
- component: {fileID: 7298513067680060886}
|
||||
- component: {fileID: 5990132956580680787}
|
||||
- component: {fileID: 8290370249712404227}
|
||||
- component: {fileID: 304484164735584996}
|
||||
- component: {fileID: 1666753161128106451}
|
||||
m_Layer: 0
|
||||
m_Name: Player
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &194292233036670670
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2218851646297572645}
|
||||
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!33 &7298513067680060886
|
||||
MeshFilter:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2218851646297572645}
|
||||
m_Mesh: {fileID: 10208, guid: 0000000000000000e000000000000000, type: 0}
|
||||
--- !u!23 &5990132956580680787
|
||||
MeshRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2218851646297572645}
|
||||
m_Enabled: 1
|
||||
m_CastShadows: 1
|
||||
m_ReceiveShadows: 1
|
||||
m_DynamicOccludee: 1
|
||||
m_StaticShadowCaster: 0
|
||||
m_MotionVectors: 1
|
||||
m_LightProbeUsage: 1
|
||||
m_ReflectionProbeUsage: 1
|
||||
m_RayTracingMode: 2
|
||||
m_RayTraceProcedural: 0
|
||||
m_RayTracingAccelStructBuildFlagsOverride: 0
|
||||
m_RayTracingAccelStructBuildFlags: 1
|
||||
m_SmallMeshCulling: 1
|
||||
m_ForceMeshLod: -1
|
||||
m_MeshLodSelectionBias: 0
|
||||
m_RenderingLayerMask: 1
|
||||
m_RendererPriority: 0
|
||||
m_Materials:
|
||||
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
|
||||
m_StaticBatchInfo:
|
||||
firstSubMesh: 0
|
||||
subMeshCount: 0
|
||||
m_StaticBatchRoot: {fileID: 0}
|
||||
m_ProbeAnchor: {fileID: 0}
|
||||
m_LightProbeVolumeOverride: {fileID: 0}
|
||||
m_ScaleInLightmap: 1
|
||||
m_ReceiveGI: 1
|
||||
m_PreserveUVs: 1
|
||||
m_IgnoreNormalsForChartDetection: 0
|
||||
m_ImportantGI: 0
|
||||
m_StitchLightmapSeams: 1
|
||||
m_SelectedEditorRenderState: 3
|
||||
m_MinimumChartSize: 4
|
||||
m_AutoUVMaxDistance: 0.5
|
||||
m_AutoUVMaxAngle: 89
|
||||
m_LightmapParameters: {fileID: 0}
|
||||
m_GlobalIlluminationMeshLod: 0
|
||||
m_SortingLayerID: 0
|
||||
m_SortingLayer: 0
|
||||
m_SortingOrder: 0
|
||||
m_MaskInteraction: 0
|
||||
m_AdditionalVertexStreams: {fileID: 0}
|
||||
--- !u!114 &8290370249712404227
|
||||
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: 766c44362be2b4fcaa872e6fb44fc42f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.PlayerAuthoring
|
||||
MoveSpeed: 6
|
||||
TurnRateDegreesPerSec: 720
|
||||
--- !u!114 &304484164735584996
|
||||
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: c16549610bfe4458aa9389201d072bb6, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.Entities.Hybrid::Unity.Entities.Hybrid.Baking.LinkedEntityGroupAuthoring
|
||||
--- !u!114 &1666753161128106451
|
||||
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: 7c79d771cedb4794bf100ce60df5f764, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.NetCode.Authoring.Hybrid::Unity.NetCode.GhostAuthoringComponent
|
||||
HasOwner: 1
|
||||
SupportAutoCommandTarget: 1
|
||||
TrackInterpolationDelay: 0
|
||||
GhostGroup: 0
|
||||
UsePreSerialization: 0
|
||||
UseSingleBaseline: 0
|
||||
RollbackPredictedSpawnedGhostState: 0
|
||||
RollbackPredictionOnStructuralChanges: 1
|
||||
DefaultGhostMode: 2
|
||||
SupportedGhostModes: 3
|
||||
OptimizationMode: 0
|
||||
Importance: 1
|
||||
MaxSendRate: 0
|
||||
SingleWorldHostInterpolationSmoothing: 1
|
||||
prefabId:
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a27bbed2662454377bd25279ee4a14d2
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e118095ea29346b188e4c17f8d29181
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,37 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Authoring for the player ghost prefab. Bakes the gameplay components onto the entity and
|
||||
/// exposes movement tunables for designers. Ghost replication, <c>GhostOwner</c> and
|
||||
/// AutoCommandTarget are supplied by the GhostAuthoringComponent added on the same prefab
|
||||
/// GameObject. <c>GetEntity(TransformUsageFlags.Dynamic)</c> ensures a runtime-mutable
|
||||
/// LocalTransform exists.
|
||||
/// </summary>
|
||||
public class PlayerAuthoring : MonoBehaviour
|
||||
{
|
||||
[Min(0f)] public float MoveSpeed = 6f;
|
||||
[Min(0f)] public float TurnRateDegreesPerSec = 720f;
|
||||
|
||||
private class PlayerBaker : Baker<PlayerAuthoring>
|
||||
{
|
||||
public override void Bake(PlayerAuthoring authoring)
|
||||
{
|
||||
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
|
||||
|
||||
AddComponent<PlayerTag>(entity);
|
||||
AddComponent(entity, new PlayerMoveStats
|
||||
{
|
||||
MoveSpeed = authoring.MoveSpeed,
|
||||
TurnRateRadiansPerSec = math.radians(authoring.TurnRateDegreesPerSec)
|
||||
});
|
||||
AddComponent<PlayerFacing>(entity);
|
||||
AddComponent<PlayerInput>(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 766c44362be2b4fcaa872e6fb44fc42f
|
||||
@@ -4,6 +4,8 @@
|
||||
"references": [
|
||||
"ProjectM.Simulation",
|
||||
"Unity.Entities",
|
||||
"Unity.Entities.Hybrid",
|
||||
"Unity.Collections",
|
||||
"Unity.Mathematics",
|
||||
"Unity.NetCode"
|
||||
],
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a5cd4882c5ffe499fb63c8c385ddf8d2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,34 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Authoring placed once in the gameplay subscene. Bakes a <see cref="PlayerSpawner"/> singleton
|
||||
/// holding the player ghost prefab entity and a spawn point, which the server's
|
||||
/// GoInGameServerSystem instantiates on each connect.
|
||||
/// </summary>
|
||||
public class PlayerSpawnerAuthoring : MonoBehaviour
|
||||
{
|
||||
[Tooltip("The Player ghost prefab to spawn for each connected client.")]
|
||||
public GameObject PlayerPrefab;
|
||||
|
||||
public Vector3 SpawnPoint = Vector3.zero;
|
||||
|
||||
private class PlayerSpawnerBaker : Baker<PlayerSpawnerAuthoring>
|
||||
{
|
||||
public override void Bake(PlayerSpawnerAuthoring authoring)
|
||||
{
|
||||
// The spawner itself needs no transform; it is a data singleton.
|
||||
var entity = GetEntity(authoring, TransformUsageFlags.None);
|
||||
|
||||
AddComponent(entity, new PlayerSpawner
|
||||
{
|
||||
PlayerPrefab = GetEntity(authoring.PlayerPrefab, TransformUsageFlags.Dynamic),
|
||||
SpawnPoint = authoring.SpawnPoint
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e56c91ba352644bd900142035ff2799
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2913bc88b49f3421c82223b996a66a4b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,47 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-side connection handshake: for every connection that has been assigned a
|
||||
/// <see cref="NetworkId"/> but is not yet <see cref="NetworkStreamInGame"/>, mark it in-game and
|
||||
/// fire a <see cref="GoInGameRequest"/> RPC so the server spawns this client's player ghost.
|
||||
/// Adding NetworkStreamInGame is what gates snapshot/command flow on. Mirrors the netcode
|
||||
/// "networked-cube" go-in-game sample.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
|
||||
public partial struct GoInGameClientSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
var builder = new EntityQueryBuilder(Allocator.Temp)
|
||||
.WithAll<NetworkId>()
|
||||
.WithNone<NetworkStreamInGame>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(builder));
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (_, connection) in
|
||||
SystemAPI.Query<RefRO<NetworkId>>().WithNone<NetworkStreamInGame>().WithEntityAccess())
|
||||
{
|
||||
ecb.AddComponent<NetworkStreamInGame>(connection);
|
||||
|
||||
var request = ecb.CreateEntity();
|
||||
ecb.AddComponent<GoInGameRequest>(request);
|
||||
ecb.AddComponent(request, new SendRpcCommandRequest { TargetConnection = connection });
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2b0386021d7340d0b023ab1d954ce14
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb249fc7c6daa49478fb318eb553b719
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,68 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-only twin-stick input gather. Samples the Input System once per frame (WASD /
|
||||
/// left-stick -> Move, right-stick -> Aim) and writes <see cref="PlayerInput"/> on the
|
||||
/// locally-owned player ghost (filtered to <see cref="GhostOwnerIsLocal"/>). Runs in
|
||||
/// <see cref="GhostInputSystemGroup"/> — NOT the prediction loop — so devices are read once per
|
||||
/// frame, never re-read during rollback. Implemented as a non-Burst <see cref="ISystem"/>
|
||||
/// because it reads the managed Input System.
|
||||
/// <para>
|
||||
/// NOTE: the Input System device types are fully qualified rather than imported via
|
||||
/// <c>using UnityEngine.InputSystem;</c> on purpose — that namespace also defines a
|
||||
/// <c>PlayerInput</c> type which would collide with <see cref="ProjectM.Simulation.PlayerInput"/>
|
||||
/// and make the Entities source generator bind <c>RefRW<PlayerInput></c> to the managed
|
||||
/// class (a spurious CS8377 "must be unmanaged").
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[UpdateInGroup(typeof(GhostInputSystemGroup))]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
|
||||
public partial struct PlayerInputGatherSystem : ISystem
|
||||
{
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<PlayerInput>();
|
||||
}
|
||||
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
float2 move = float2.zero;
|
||||
float2 aim = float2.zero;
|
||||
|
||||
var keyboard = UnityEngine.InputSystem.Keyboard.current;
|
||||
if (keyboard != null)
|
||||
{
|
||||
if (keyboard.wKey.isPressed) move.y += 1f;
|
||||
if (keyboard.sKey.isPressed) move.y -= 1f;
|
||||
if (keyboard.dKey.isPressed) move.x += 1f;
|
||||
if (keyboard.aKey.isPressed) move.x -= 1f;
|
||||
}
|
||||
|
||||
var gamepad = UnityEngine.InputSystem.Gamepad.current;
|
||||
if (gamepad != null)
|
||||
{
|
||||
float2 leftStick = gamepad.leftStick.ReadValue();
|
||||
if (math.lengthsq(leftStick) > math.lengthsq(move))
|
||||
move = leftStick;
|
||||
|
||||
aim = gamepad.rightStick.ReadValue();
|
||||
}
|
||||
|
||||
// Right-stick deadzone: a resting stick yields zero Aim so PlayerAimSystem falls back to
|
||||
// the movement heading (controller-first directional aim).
|
||||
if (math.lengthsq(aim) < 0.04f)
|
||||
aim = float2.zero;
|
||||
|
||||
foreach (var input in SystemAPI.Query<RefRW<PlayerInput>>().WithAll<GhostOwnerIsLocal>())
|
||||
{
|
||||
input.ValueRW.Move = move;
|
||||
input.ValueRW.Aim = aim;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 27c80caff08534f239e39ff4732fbdfd
|
||||
@@ -8,7 +8,8 @@
|
||||
"Unity.Mathematics",
|
||||
"Unity.Burst",
|
||||
"Unity.NetCode",
|
||||
"Unity.Entities.Graphics"
|
||||
"Unity.Entities.Graphics",
|
||||
"Unity.InputSystem"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e45f23db9544e4b1d94a4914c10f35d3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,60 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-authoritative player spawn. On each received <see cref="GoInGameRequest"/>: mark the
|
||||
/// source connection in-game, instantiate the player ghost from the baked
|
||||
/// <see cref="PlayerSpawner"/>, stamp <see cref="GhostOwner"/> with the connection's
|
||||
/// <see cref="NetworkId"/>, place it at the spawn point, and link it to the connection's
|
||||
/// LinkedEntityGroup so it auto-despawns on disconnect. Mirrors the netcode "networked-cube"
|
||||
/// ModifiedGoInGameServer sample. All structural changes are batched through an
|
||||
/// <see cref="EntityCommandBuffer"/>.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct GoInGameServerSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<PlayerSpawner>();
|
||||
|
||||
var builder = new EntityQueryBuilder(Allocator.Temp)
|
||||
.WithAll<GoInGameRequest, ReceiveRpcCommandRequest>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(builder));
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var spawner = SystemAPI.GetSingleton<PlayerSpawner>();
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (receive, requestEntity) in
|
||||
SystemAPI.Query<RefRO<ReceiveRpcCommandRequest>>().WithAll<GoInGameRequest>().WithEntityAccess())
|
||||
{
|
||||
var connection = receive.ValueRO.SourceConnection;
|
||||
ecb.AddComponent<NetworkStreamInGame>(connection);
|
||||
|
||||
var networkId = SystemAPI.GetComponent<NetworkId>(connection);
|
||||
|
||||
var player = ecb.Instantiate(spawner.PlayerPrefab);
|
||||
ecb.SetComponent(player, LocalTransform.FromPosition(spawner.SpawnPoint));
|
||||
ecb.SetComponent(player, new GhostOwner { NetworkId = networkId.Value });
|
||||
|
||||
// Auto-despawn the player when its owning connection is removed.
|
||||
ecb.AppendToBuffer(connection, new LinkedEntityGroup { Value = player });
|
||||
|
||||
ecb.DestroyEntity(requestEntity);
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c89f67860a64492bb09af8cc38f3c83
|
||||
@@ -4,6 +4,7 @@
|
||||
"references": [
|
||||
"ProjectM.Simulation",
|
||||
"Unity.Entities",
|
||||
"Unity.Transforms",
|
||||
"Unity.Collections",
|
||||
"Unity.Mathematics",
|
||||
"Unity.Burst",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 08533a9dddc374862b7e3259cb8f872d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Client -> server one-off request to enter gameplay. On receipt the server adds
|
||||
/// NetworkStreamInGame to the connection (enabling snapshot/command flow) and spawns the
|
||||
/// client's player ghost. Lives in Simulation so both worlds see the type for RPC source-gen.
|
||||
/// </summary>
|
||||
public struct GoInGameRequest : IRpcCommand { }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b633994e24f874e0796d1fa93cc46679
|
||||
@@ -15,8 +15,11 @@ namespace ProjectM.Simulation
|
||||
{
|
||||
public override bool Initialize(string defaultWorldName)
|
||||
{
|
||||
// 0 = do not auto-connect; worlds are still created. Set a port later to auto-connect.
|
||||
AutoConnectPort = 0;
|
||||
// Auto-connect in-editor: the server listens and the in-process client connects (over
|
||||
// IPC) on the default BinaryWorlds host mode — one process hosts both worlds (M1 listen
|
||||
// server). M3 replaces this with an explicit Unity Relay host/join flow. Set to 0 to
|
||||
// disable auto-connect.
|
||||
AutoConnectPort = 7979;
|
||||
CreateDefaultClientServerWorlds();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78b348213a4864001bf105954525fbda
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,41 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Predicted aim/facing: writes <see cref="PlayerFacing"/> from twin-stick Aim, falling back to
|
||||
/// the movement direction when Aim is zero (controller-first directional aim). Also turns the
|
||||
/// ghost transform toward the facing direction for top-down presentation. When there is no input
|
||||
/// this tick the previous facing is held. Deterministic (pure math); filtered to
|
||||
/// <see cref="Simulate"/> so it runs only for predicted ghosts.
|
||||
/// </summary>
|
||||
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
|
||||
[BurstCompile]
|
||||
public partial struct PlayerAimSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
foreach (var (facing, transform, input) in
|
||||
SystemAPI.Query<RefRW<PlayerFacing>, RefRW<LocalTransform>, RefRO<PlayerInput>>()
|
||||
.WithAll<Simulate>())
|
||||
{
|
||||
float2 aim = input.ValueRO.Aim;
|
||||
if (math.lengthsq(aim) < 1e-6f)
|
||||
aim = input.ValueRO.Move; // fall back to movement heading
|
||||
if (math.lengthsq(aim) < 1e-6f)
|
||||
continue; // no input this tick: keep last facing
|
||||
|
||||
aim = math.normalize(aim);
|
||||
facing.ValueRW.Direction = aim;
|
||||
|
||||
float3 forward = new float3(aim.x, 0f, aim.y);
|
||||
transform.ValueRW.Rotation = quaternion.LookRotationSafe(forward, math.up());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a274d036dc034cbaa70f6a782d8784a
|
||||
@@ -0,0 +1,18 @@
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Authoritative aim/facing direction, decoupled from movement heading so twin-stick aim is
|
||||
/// independent of travel direction. Written in predicted sim from PlayerInput.Aim; consumed by
|
||||
/// presentation now and ability systems (M2). Replicated so interpolated remote players show
|
||||
/// the correct facing.
|
||||
/// </summary>
|
||||
public struct PlayerFacing : IComponentData
|
||||
{
|
||||
/// <summary>Normalized planar facing direction (world XZ mapped to float2 x,y).</summary>
|
||||
[GhostField(Quantization = 1000)] public float2 Direction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d0adf0e4def842a89a3017de243aca9
|
||||
@@ -0,0 +1,30 @@
|
||||
using Unity.Collections;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Twin-stick player input (server-authoritative, input-only clients). Gathered once per frame
|
||||
/// on the owning client in <see cref="GhostInputSystemGroup"/> and streamed to the server via
|
||||
/// AutoCommandTarget. Netcode source-gen produces InputBufferData<PlayerInput> plus the
|
||||
/// copy/apply systems from this type. The [GhostField]s let remote owners be predicted; the
|
||||
/// data replays deterministically under rollback.
|
||||
/// </summary>
|
||||
public struct PlayerInput : IInputComponentData
|
||||
{
|
||||
/// <summary>WASD / left-stick movement, normalized to roughly -1..1 per axis.</summary>
|
||||
[GhostField(Quantization = 1000)] public float2 Move;
|
||||
|
||||
/// <summary>Right-stick / cursor aim direction (normalized). Zero => face movement direction.</summary>
|
||||
[GhostField(Quantization = 1000)] public float2 Aim;
|
||||
|
||||
public FixedString512Bytes ToFixedString()
|
||||
{
|
||||
var s = new FixedString512Bytes();
|
||||
s.Append(Move.x); s.Append(','); s.Append(Move.y); s.Append(';');
|
||||
s.Append(Aim.x); s.Append(','); s.Append(Aim.y);
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 72ac3666802bf49f891f49a0d03201e5
|
||||
@@ -0,0 +1,17 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-player movement tunables, baked from authoring. Identical on client (re-prediction) and
|
||||
/// server so movement is deterministic. Not replicated.
|
||||
/// </summary>
|
||||
public struct PlayerMoveStats : IComponentData
|
||||
{
|
||||
/// <summary>Planar movement speed in units/second.</summary>
|
||||
public float MoveSpeed;
|
||||
|
||||
/// <summary>Max turn rate (radians/second) when rotating toward the facing direction.</summary>
|
||||
public float TurnRateRadiansPerSec;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf5fc79d6c67d4ef39ba4e7e9457dd85
|
||||
@@ -0,0 +1,39 @@
|
||||
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.
|
||||
/// </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<PlayerMoveStats>>()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35cd3eacccc2f4172b557b2807a6df22
|
||||
@@ -0,0 +1,10 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Zero-size marker identifying a player-character ghost. Lets movement/aim/ability systems
|
||||
/// query players without coupling to other gameplay components. Added by PlayerBaker.
|
||||
/// </summary>
|
||||
public struct PlayerTag : IComponentData { }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f967358256a44853bfc5be66e13bf3b
|
||||
@@ -3,6 +3,7 @@
|
||||
"rootNamespace": "ProjectM.Simulation",
|
||||
"references": [
|
||||
"Unity.Entities",
|
||||
"Unity.Transforms",
|
||||
"Unity.Collections",
|
||||
"Unity.Mathematics",
|
||||
"Unity.Burst",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 37f2ed4a443ac4a18aba505897f9e6fa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton baked into the gameplay subscene, holding the baked player ghost prefab entity so
|
||||
/// the server spawn system can instantiate it on connect. Mirrors the netcode CubeSpawner sample.
|
||||
/// </summary>
|
||||
public struct PlayerSpawner : IComponentData
|
||||
{
|
||||
public Entity PlayerPrefab;
|
||||
public float3 SpawnPoint;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d1945139846b49a7943b7149188aa45
|
||||
@@ -119,7 +119,54 @@ NavMeshSettings:
|
||||
debug:
|
||||
m_Flags: 0
|
||||
m_NavMeshData: {fileID: 0}
|
||||
--- !u!1 &1498433570
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1498433572}
|
||||
- component: {fileID: 1498433571}
|
||||
m_Layer: 0
|
||||
m_Name: PlayerSpawner
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!114 &1498433571
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1498433570}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 5e56c91ba352644bd900142035ff2799, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.PlayerSpawnerAuthoring
|
||||
PlayerPrefab: {fileID: 2218851646297572645, guid: a27bbed2662454377bd25279ee4a14d2, type: 3}
|
||||
SpawnPoint: {x: 0, y: 1, z: 0}
|
||||
--- !u!4 &1498433572
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1498433570}
|
||||
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!1660057539 &9223372036854775807
|
||||
SceneRoots:
|
||||
m_ObjectHideFlags: 0
|
||||
m_Roots: []
|
||||
m_Roots:
|
||||
- {fileID: 1498433572}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
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 + PlayerMoveStats + LocalTransform + enabled Simulate), injects
|
||||
/// a fixed delta-time, ticks N times, and asserts the position advanced by exactly
|
||||
/// MoveSpeed * dt * N. Version-independent and netcode-free, mirroring HeartbeatSystemTests.
|
||||
/// </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(PlayerMoveStats), 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 PlayerMoveStats { MoveSpeed = moveSpeed, TurnRateRadiansPerSec = 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(PlayerMoveStats), typeof(LocalTransform), typeof(Simulate));
|
||||
em.SetComponentData(e, LocalTransform.FromPosition(float3.zero));
|
||||
em.SetComponentData(e, new PlayerMoveStats { MoveSpeed = 3f, TurnRateRadiansPerSec = 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aed328b85e7264aa9aedca0a628c6169
|
||||
@@ -4,8 +4,10 @@
|
||||
"references": [
|
||||
"ProjectM.Simulation",
|
||||
"Unity.Entities",
|
||||
"Unity.Transforms",
|
||||
"Unity.Collections",
|
||||
"Unity.Mathematics",
|
||||
"Unity.NetCode",
|
||||
"UnityEngine.TestRunner",
|
||||
"UnityEditor.TestRunner"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user