Netcode Bootstrap
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user