Core Game Loop Additions
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only, one-shot spawner for the GLOBAL cycle-director ghost (mirrors SharedStorageSpawnSystem,
|
||||
/// but MINUS the RegionTag — the director must stay global so GhostRelevancy keeps it relevant to every
|
||||
/// region). On its first update it reads the baked <see cref="CycleDirectorSpawner"/> + NetworkTime,
|
||||
/// instantiates the ghost, initializes <see cref="CycleState"/> (Expedition, cycle 1, PhaseEndTick =
|
||||
/// now + <see cref="CyclePhase.ExpeditionTicks"/>), adds the server-only <see cref="CycleRuntime"/>, and
|
||||
/// places it at the base center (preserving the prefab's baked LocalTransform scale — FromPosition would
|
||||
/// reset the replicated Scale GhostField), then destroys the spawner so it idles.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct CycleDirectorSpawnSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<CycleDirectorSpawner>();
|
||||
state.RequireForUpdate<NetworkTime>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
||||
if (!serverTick.IsValid)
|
||||
return;
|
||||
uint now = serverTick.TickIndexForValidTick;
|
||||
|
||||
var spawnerEntity = SystemAPI.GetSingletonEntity<CycleDirectorSpawner>();
|
||||
var spawner = SystemAPI.GetComponent<CycleDirectorSpawner>(spawnerEntity);
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
if (spawner.Prefab != Entity.Null)
|
||||
{
|
||||
var director = ecb.Instantiate(spawner.Prefab);
|
||||
|
||||
// Place at the base center, preserving the prefab's baked scale/rotation.
|
||||
var xform = SystemAPI.GetComponent<LocalTransform>(spawner.Prefab);
|
||||
if (SystemAPI.TryGetSingleton<BaseAnchor>(out var anchor))
|
||||
xform.Position = BaseGridMath.PlotCenter(anchor);
|
||||
ecb.SetComponent(director, xform);
|
||||
|
||||
// Override the baked CycleState with the real start tick; add server-only bookkeeping.
|
||||
ecb.SetComponent(director, new CycleState
|
||||
{
|
||||
Phase = CyclePhase.Expedition,
|
||||
CycleNumber = 1,
|
||||
PhaseEndTick = TickUtil.NonZero(now + CyclePhase.ExpeditionTicks),
|
||||
});
|
||||
ecb.AddComponent(director, new CycleRuntime { DefendStartWave = 0 });
|
||||
}
|
||||
|
||||
// One-shot: remove the spawner so RequireForUpdate fails and the system idles.
|
||||
ecb.DestroyEntity(spawnerEntity);
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bdfd3e27e8a3e924c93d6af962e0df05
|
||||
@@ -0,0 +1,93 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-authoritative macro-loop director for "The Aether Cycle": Expedition (timed) -> Defend
|
||||
/// (wave-driven) -> Build (timed) -> next cycle. Maintains the <see cref="CycleState"/> singleton and gates
|
||||
/// <see cref="WaveSystem"/> so the base-defense wave only spawns during Defend. Runs in the plain server
|
||||
/// SimulationSystemGroup (NOT prediction) before <see cref="WaveSystem"/>. All timing is wrap-safe
|
||||
/// NetworkTick math (<see cref="TickUtil.NonZero"/> + <see cref="Unity.NetCode.NetworkTick.IsNewerThan"/>),
|
||||
/// never raw uint compares. The CycleState/CycleRuntime live on the runtime-spawned CycleDirector ghost.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateBefore(typeof(WaveSystem))]
|
||||
public partial struct CyclePhaseSystem : ISystem
|
||||
{
|
||||
EntityQuery m_AliveHusks;
|
||||
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<NetworkTime>();
|
||||
state.RequireForUpdate<CycleState>();
|
||||
m_AliveHusks = state.GetEntityQuery(ComponentType.ReadOnly<EnemyTag>());
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
||||
if (!serverTick.IsValid)
|
||||
return;
|
||||
uint now = serverTick.TickIndexForValidTick;
|
||||
|
||||
var cycleEntity = SystemAPI.GetSingletonEntity<CycleState>();
|
||||
|
||||
var cycle = SystemAPI.GetComponent<CycleState>(cycleEntity);
|
||||
var runtime = SystemAPI.GetComponent<CycleRuntime>(cycleEntity);
|
||||
|
||||
bool timedPhaseDue =
|
||||
cycle.PhaseEndTick != 0 && !new NetworkTick(cycle.PhaseEndTick).IsNewerThan(serverTick);
|
||||
|
||||
switch (cycle.Phase)
|
||||
{
|
||||
case CyclePhase.Expedition:
|
||||
if (timedPhaseDue)
|
||||
{
|
||||
cycle.Phase = CyclePhase.Defend;
|
||||
cycle.PhaseEndTick = 0; // Defend is wave-driven, not timed.
|
||||
runtime.DefendStartWave =
|
||||
SystemAPI.TryGetSingleton<WaveState>(out var w) ? w.WaveNumber : 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case CyclePhase.Defend:
|
||||
if (DefendCleared(ref state, runtime.DefendStartWave))
|
||||
{
|
||||
cycle.Phase = CyclePhase.Build;
|
||||
cycle.PhaseEndTick = TickUtil.NonZero(now + CyclePhase.BuildTicks);
|
||||
}
|
||||
break;
|
||||
|
||||
case CyclePhase.Build:
|
||||
if (timedPhaseDue)
|
||||
{
|
||||
cycle.Phase = CyclePhase.Expedition;
|
||||
cycle.CycleNumber += 1;
|
||||
cycle.PhaseEndTick = TickUtil.NonZero(now + CyclePhase.ExpeditionTicks);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
SystemAPI.SetComponent(cycleEntity, cycle);
|
||||
SystemAPI.SetComponent(cycleEntity, runtime);
|
||||
}
|
||||
|
||||
// The Defend wave has run for this phase (WaveNumber advanced past the captured start), is fully
|
||||
// spawned, and no Husks remain alive.
|
||||
bool DefendCleared(ref SystemState state, int defendStartWave)
|
||||
{
|
||||
if (!SystemAPI.TryGetSingleton<WaveState>(out var wave))
|
||||
return false;
|
||||
return wave.WaveNumber > defendStartWave
|
||||
&& wave.RemainingToSpawn == 0
|
||||
&& m_AliveHusks.CalculateEntityCount() == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c325c252dce9fba4a938d5c8db903042
|
||||
@@ -0,0 +1,85 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only walk-in gate transit: a player who walks within a gate's radius (and whose region matches the
|
||||
/// gate's <see cref="ExpeditionGate.FromRegion"/>) is transited to the gate's ToRegion at its ArrivalPos
|
||||
/// (RegionTag flipped + LocalTransform teleported — GhostRelevancy re-scopes their ghosts, as in
|
||||
/// <c>RegionTransitSystem</c>). Returning to the BASE during the Expedition phase expires the Expedition
|
||||
/// timer so Defend starts early ("timer cap + early return"). Plain server SimulationSystemGroup
|
||||
/// <c>[UpdateAfter(CyclePhaseSystem)]</c>. Arrival points are offset from the destination gate so a transited
|
||||
/// player does not immediately re-trigger.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(CyclePhaseSystem))]
|
||||
public partial struct ExpeditionGateSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<ExpeditionGate>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
// Snapshot gates once.
|
||||
var gateFrom = new NativeList<byte>(Allocator.Temp);
|
||||
var gateTo = new NativeList<byte>(Allocator.Temp);
|
||||
var gateRadiusSq = new NativeList<float>(Allocator.Temp);
|
||||
var gatePos = new NativeList<float2>(Allocator.Temp);
|
||||
var gateArrival = new NativeList<float3>(Allocator.Temp);
|
||||
foreach (var (gate, xform) in SystemAPI.Query<RefRO<ExpeditionGate>, RefRO<LocalTransform>>())
|
||||
{
|
||||
gateFrom.Add(gate.ValueRO.FromRegion);
|
||||
gateTo.Add(gate.ValueRO.ToRegion);
|
||||
gateRadiusSq.Add(gate.ValueRO.Radius * gate.ValueRO.Radius);
|
||||
gatePos.Add(xform.ValueRO.Position.xz);
|
||||
gateArrival.Add(gate.ValueRO.ArrivalPos);
|
||||
}
|
||||
|
||||
bool returnedToBase = false;
|
||||
foreach (var (region, xform) in
|
||||
SystemAPI.Query<RefRW<RegionTag>, RefRW<LocalTransform>>().WithAll<PlayerTag>())
|
||||
{
|
||||
byte r = region.ValueRO.Region;
|
||||
float2 pp = xform.ValueRO.Position.xz;
|
||||
for (int i = 0; i < gateFrom.Length; i++)
|
||||
{
|
||||
if (gateFrom[i] != r) continue;
|
||||
if (math.distancesq(pp, gatePos[i]) > gateRadiusSq[i]) continue;
|
||||
region.ValueRW.Region = gateTo[i];
|
||||
xform.ValueRW.Position = gateArrival[i];
|
||||
if (gateTo[i] == RegionId.Base)
|
||||
returnedToBase = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
gateFrom.Dispose();
|
||||
gateTo.Dispose();
|
||||
gateRadiusSq.Dispose();
|
||||
gatePos.Dispose();
|
||||
gateArrival.Dispose();
|
||||
|
||||
// Early return: a player came back to base mid-Expedition -> expire the Expedition timer (-> Defend).
|
||||
if (returnedToBase && SystemAPI.TryGetSingletonEntity<CycleState>(out var cycleEntity))
|
||||
{
|
||||
var cs = SystemAPI.GetComponent<CycleState>(cycleEntity);
|
||||
if (cs.Phase == CyclePhase.Expedition)
|
||||
{
|
||||
cs.PhaseEndTick = 1; // CyclePhaseSystem sees timedPhaseDue next tick -> Defend
|
||||
SystemAPI.SetComponent(cycleEntity, cs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4292536f663eb5c4d92688f6c5bb0368
|
||||
@@ -0,0 +1,69 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-authoritative per-connection ghost relevancy for the base/expedition region split. Each tick:
|
||||
/// build a connection -> region map from the region-tagged player ghosts, then mark every region-tagged
|
||||
/// ghost IRRELEVANT to each connection whose player is in a DIFFERENT region. Uses
|
||||
/// <see cref="GhostRelevancyMode.SetIsIrrelevant"/> so untagged/global ghosts (e.g. the cycle director)
|
||||
/// stay relevant to everyone for free — only cross-region ghosts are hidden. Runs in the
|
||||
/// <see cref="GhostSimulationSystemGroup"/> (before GhostSendSystem reads the set). The set holds
|
||||
/// (connection, ghost) pairs for the CURRENT simulated tick only, so it is cleared and repopulated every
|
||||
/// update. A connection with no spawned player yet is absent from the map and simply sees everything.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(GhostSimulationSystemGroup))]
|
||||
public partial struct RegionRelevancySystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<GhostRelevancy>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
// Map each in-game connection (by NetworkId) to its player's region.
|
||||
var connRegion = new NativeHashMap<int, byte>(8, Allocator.Temp);
|
||||
foreach (var (owner, region) in
|
||||
SystemAPI.Query<RefRO<GhostOwner>, RefRO<RegionTag>>().WithAll<PlayerTag>())
|
||||
{
|
||||
connRegion[owner.ValueRO.NetworkId] = region.ValueRO.Region;
|
||||
}
|
||||
|
||||
ref var relevancy = ref SystemAPI.GetSingletonRW<GhostRelevancy>().ValueRW;
|
||||
relevancy.GhostRelevancyMode = GhostRelevancyMode.SetIsIrrelevant;
|
||||
var set = relevancy.GhostRelevancySet;
|
||||
set.Clear();
|
||||
|
||||
if (!connRegion.IsEmpty)
|
||||
{
|
||||
var conns = connRegion.GetKeyValueArrays(Allocator.Temp);
|
||||
foreach (var (ghost, region) in
|
||||
SystemAPI.Query<RefRO<GhostInstance>, RefRO<RegionTag>>())
|
||||
{
|
||||
int ghostId = ghost.ValueRO.ghostId;
|
||||
if (ghostId == 0)
|
||||
continue; // ghost id not assigned yet this tick
|
||||
|
||||
byte ghostRegion = region.ValueRO.Region;
|
||||
for (int i = 0; i < conns.Keys.Length; i++)
|
||||
{
|
||||
if (conns.Values[i] != ghostRegion)
|
||||
set.Add(new RelevantGhostForConnection { Connection = conns.Keys[i], Ghost = ghostId }, 1);
|
||||
}
|
||||
}
|
||||
conns.Dispose();
|
||||
}
|
||||
|
||||
connRegion.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db2f33e6ce7ce9346b3dfbcd5e562ed6
|
||||
@@ -0,0 +1,71 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-authoritative handler for <see cref="RegionTransitRequest"/> RPCs. Resolves the sender's player
|
||||
/// (via the source connection's <see cref="NetworkId"/> -> <see cref="GhostOwner"/>), flips its
|
||||
/// <see cref="RegionTag"/> to the requested region, and teleports it to that region's origin
|
||||
/// (<see cref="RegionMath.RegionOrigin"/>, centered on the base via <see cref="BaseGridMath.PlotCenter"/>).
|
||||
/// Runs in the default server SimulationSystemGroup (NOT the prediction loop) so the transit applies once;
|
||||
/// the next snapshot reconciles the owner-predicted client and <see cref="RegionRelevancySystem"/> re-scopes
|
||||
/// which region's ghosts the connection receives. Mirrors the <see cref="StorageOpReceiveSystem"/> RPC shape.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial struct RegionTransitSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<BaseAnchor>();
|
||||
|
||||
var builder = new EntityQueryBuilder(Allocator.Temp)
|
||||
.WithAll<RegionTransitRequest, ReceiveRpcCommandRequest>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(builder));
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var baseCenter = BaseGridMath.PlotCenter(SystemAPI.GetSingleton<BaseAnchor>());
|
||||
|
||||
// Map connection NetworkId -> player entity.
|
||||
var playerByConn = new NativeHashMap<int, Entity>(8, Allocator.Temp);
|
||||
foreach (var (owner, entity) in
|
||||
SystemAPI.Query<RefRO<GhostOwner>>().WithAll<PlayerTag, RegionTag>().WithEntityAccess())
|
||||
{
|
||||
playerByConn[owner.ValueRO.NetworkId] = entity;
|
||||
}
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (request, receive, requestEntity) in
|
||||
SystemAPI.Query<RefRO<RegionTransitRequest>, RefRO<ReceiveRpcCommandRequest>>().WithEntityAccess())
|
||||
{
|
||||
var connEntity = receive.ValueRO.SourceConnection;
|
||||
if (SystemAPI.HasComponent<NetworkId>(connEntity))
|
||||
{
|
||||
int connId = SystemAPI.GetComponent<NetworkId>(connEntity).Value;
|
||||
if (playerByConn.TryGetValue(connId, out var player))
|
||||
{
|
||||
byte target = request.ValueRO.TargetRegion;
|
||||
SystemAPI.GetComponentRW<RegionTag>(player).ValueRW.Region = target;
|
||||
SystemAPI.GetComponentRW<LocalTransform>(player).ValueRW.Position =
|
||||
RegionMath.RegionOrigin(target, baseCenter);
|
||||
}
|
||||
}
|
||||
|
||||
ecb.DestroyEntity(requestEntity);
|
||||
}
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
playerByConn.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44d6ce89f189d984c83cd213d86d4b02
|
||||
Reference in New Issue
Block a user