Core Game Loop Additions
This commit is contained in:
@@ -35,5 +35,9 @@ namespace ProjectM.Simulation
|
||||
|
||||
/// <summary>Integrated distance travelled (predicted on client + authoritative on server). Not replicated.</summary>
|
||||
public float DistanceTravelled;
|
||||
|
||||
/// <summary>This tick's travel step (Speed*dt), written by ProjectileMoveSystem so a plain-group harvest
|
||||
/// sweep is tunnelling-safe without depending on its own variable-frame clock. Server-local; not replicated.</summary>
|
||||
public float LastStep;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace ProjectM.Simulation
|
||||
.WithAll<Simulate>())
|
||||
{
|
||||
float step = projectile.ValueRO.Speed * dt;
|
||||
projectile.ValueRW.LastStep = step;
|
||||
float3 dir = new float3(projectile.ValueRO.Direction.x, 0f, projectile.ValueRO.Direction.y);
|
||||
|
||||
transform.ValueRW.Position += dir * step;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 655041f384f27064e82ddc6dec87ce86
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Tag marking the single GLOBAL shared-resource ledger — the entity whose [GhostField]
|
||||
/// <see cref="StorageEntry"/> buffer holds harvested resources (Aether/ore/biomass) replicated to ALL
|
||||
/// connections. It lives on the ownerless interpolated CycleDirector ghost, which carries NO
|
||||
/// <see cref="RegionTag"/>, so GhostRelevancy (SetIsIrrelevant) keeps it relevant to players in EVERY
|
||||
/// region — base AND expedition — unlike the region-tagged base storage container (which relevancy hides
|
||||
/// from expedition players). Server systems resolve the ledger via
|
||||
/// <c>GetSingletonEntity<ResourceLedger>()</c> then <c>GetBuffer<StorageEntry>()</c> — NEVER
|
||||
/// <c>GetSingleton<StorageEntry></c> (the base container owns a second StorageEntry buffer, so a
|
||||
/// buffer-typed singleton query would throw "multiple instances").
|
||||
/// </summary>
|
||||
public struct ResourceLedger : IComponentData { }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78ea3121ee0b2db4992b1a2c5dd8d34b
|
||||
@@ -0,0 +1,21 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Baked singleton holding the resource-node ghost prefab + field shape. ExpeditionFieldSystem reads it to
|
||||
/// scatter <see cref="Count"/> nodes within <see cref="Radius"/> of the expedition region origin on each
|
||||
/// Expedition phase entry (seeded by the cycle number). Mirrors <see cref="StorageSpawner"/>.
|
||||
/// </summary>
|
||||
public struct ResourceFieldSpawner : IComponentData
|
||||
{
|
||||
/// <summary>Baked resource-node ghost prefab to instantiate.</summary>
|
||||
public Entity Prefab;
|
||||
|
||||
/// <summary>Number of nodes to scatter per expedition.</summary>
|
||||
public int Count;
|
||||
|
||||
/// <summary>Scatter radius (world units) around the expedition region origin.</summary>
|
||||
public float Radius;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 120cf21540ce86640b32921fa224b974
|
||||
@@ -0,0 +1,41 @@
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>Resource-type ids for harvested materials (a byte, not an enum, per the cross-assembly enum-in-Burst hazard).</summary>
|
||||
public static class ResourceId
|
||||
{
|
||||
/// <summary>Unused / empty sentinel (aligns with StorageMath's 0-itemId no-op).</summary>
|
||||
public const byte None = 0;
|
||||
|
||||
/// <summary>Magic energy — powers abilities / charging.</summary>
|
||||
public const byte Aether = 1;
|
||||
|
||||
/// <summary>Raw ore — structures / building.</summary>
|
||||
public const byte Ore = 2;
|
||||
|
||||
/// <summary>Biomass — misc / crafting.</summary>
|
||||
public const byte Biomass = 3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A harvestable resource node in the procedural expedition field — an ownerless INTERPOLATED ghost
|
||||
/// (region-tagged Expedition) that clients see and shoot. The server-only ResourceHarvestSystem sweeps
|
||||
/// projectiles against it; each hit deposits <see cref="HarvestPerHit"/> of <see cref="ResourceId"/> into
|
||||
/// the GLOBAL resource ledger and decrements <see cref="Remaining"/>; the node despawns at <= 0.
|
||||
/// ResourceId/Remaining are [GhostField] so clients can tint by type and (later) show depletion;
|
||||
/// HarvestPerHit is baked, server-only.
|
||||
/// </summary>
|
||||
public struct ResourceNode : IComponentData
|
||||
{
|
||||
/// <summary>Which resource this node yields (see <see cref="ResourceId"/>).</summary>
|
||||
[GhostField] public byte ResourceId;
|
||||
|
||||
/// <summary>Remaining resource units; the node despawns when this reaches 0.</summary>
|
||||
[GhostField] public int Remaining;
|
||||
|
||||
/// <summary>Units yielded per projectile hit (baked; server-only).</summary>
|
||||
public float HarvestPerHit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d8504e2af2e9a694d9a7dc30c61cc69a
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee67f9c921637c84b92bfae0edb3d3e9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,61 @@
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Macro-loop state for "The Aether Cycle": which phase the run is in, the cycle number, and the server
|
||||
/// tick the current (timed) phase ends. Server-authoritative, maintained by CyclePhaseSystem. Currently a
|
||||
/// server-side singleton; the [GhostField]s below are inert until it is moved onto the runtime-spawned
|
||||
/// CycleDirector ghost (when the client HUD is wired), at which point the same struct replicates unchanged.
|
||||
/// The Defend phase is NOT timed — it ends when the base-defense wave is cleared — so PhaseEndTick is only
|
||||
/// meaningful in Expedition/Build (0 during Defend).
|
||||
/// </summary>
|
||||
public struct CycleState : IComponentData
|
||||
{
|
||||
/// <summary>Current phase (see <see cref="CyclePhase"/>).</summary>
|
||||
[GhostField] public byte Phase;
|
||||
|
||||
/// <summary>1-based cycle counter (increments when a new Expedition begins).</summary>
|
||||
[GhostField] public int CycleNumber;
|
||||
|
||||
/// <summary>Server tick the current timed phase ends (Expedition/Build only; 0 in Defend).</summary>
|
||||
[GhostField] public uint PhaseEndTick;
|
||||
}
|
||||
|
||||
/// <summary>Phase constants for <see cref="CycleState.Phase"/> (a byte, not an enum, for trivial Burst/serialization).</summary>
|
||||
public static class CyclePhase
|
||||
{
|
||||
/// <summary>Out in the procedural field gathering resources (timed).</summary>
|
||||
public const byte Expedition = 0;
|
||||
|
||||
/// <summary>The base is under assault by a Husk wave (ends when the wave is cleared).</summary>
|
||||
public const byte Defend = 1;
|
||||
|
||||
/// <summary>Calm at base: spend resources to build/upgrade (timed).</summary>
|
||||
public const byte Build = 2;
|
||||
|
||||
/// <summary>Expedition phase duration in server ticks (SimulationTickRate = 60). Tunable; short for the M6 slice.</summary>
|
||||
public const uint ExpeditionTicks = 3600; // ~60s cap (early return via the gate ends it sooner)
|
||||
|
||||
/// <summary>Build phase duration in server ticks.</summary>
|
||||
public const uint BuildTicks = 1200; // ~20s
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-only bookkeeping for the cycle state machine that must NOT replicate (kept separate from the
|
||||
/// replicated <see cref="CycleState"/>). Records the wave number captured when the Defend phase began so
|
||||
/// the director can detect "this Defend's wave has now been spawned and cleared".
|
||||
/// </summary>
|
||||
public struct CycleRuntime : IComponentData
|
||||
{
|
||||
/// <summary>WaveState.WaveNumber captured at the moment the current Defend phase started.</summary>
|
||||
public int DefendStartWave;
|
||||
|
||||
/// <summary>Cycle phase from the previous tick — lets ExpeditionFieldSystem edge-detect entering/leaving Expedition.</summary>
|
||||
public byte PrevPhase;
|
||||
|
||||
/// <summary>CycleNumber the expedition field was last seeded for (compared by int equality, never tick math).</summary>
|
||||
public int LastSpawnedCycle;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca714d222c4d2ed48aaaad7bbe6ec8fc
|
||||
@@ -0,0 +1,16 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton baked into the gameplay subscene holding the cycle-director ghost prefab. A one-shot server
|
||||
/// system (<c>CycleDirectorSpawnSystem</c>) instantiates the prefab — the GLOBAL <see cref="CycleState"/>
|
||||
/// + shared resource-ledger ghost — exactly once, then destroys this singleton. Mirrors
|
||||
/// <see cref="StorageSpawner"/>. Carries no transform; only the prefab needs one.
|
||||
/// </summary>
|
||||
public struct CycleDirectorSpawner : IComponentData
|
||||
{
|
||||
/// <summary>Baked cycle-director ghost prefab to instantiate.</summary>
|
||||
public Entity Prefab;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e0fcb1bcbea82cc419d4f134c9589619
|
||||
@@ -0,0 +1,28 @@
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// A walk-in travel gate between world regions. A baked entity (visible mesh + this component) at a fixed
|
||||
/// position; the server <c>ExpeditionGateSystem</c> transits a player who walks within <see cref="Radius"/>
|
||||
/// and whose region matches <see cref="FromRegion"/> to <see cref="ToRegion"/>, placing them at
|
||||
/// <see cref="ArrivalPos"/> (offset from the destination gate so they do not immediately re-trigger).
|
||||
/// Returning to the base during the Expedition phase also starts Defend early (the "timer cap + early
|
||||
/// return" pacing).
|
||||
/// </summary>
|
||||
public struct ExpeditionGate : IComponentData
|
||||
{
|
||||
/// <summary>Region a player must currently be in for this gate to act on them (see <see cref="RegionId"/>).</summary>
|
||||
public byte FromRegion;
|
||||
|
||||
/// <summary>Region the player is transited to.</summary>
|
||||
public byte ToRegion;
|
||||
|
||||
/// <summary>Planar (XZ) trigger radius in world units.</summary>
|
||||
public float Radius;
|
||||
|
||||
/// <summary>World position the player arrives at in the destination region.</summary>
|
||||
public float3 ArrivalPos;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed28d6b4a4f0b0844b851cecaadeb93f
|
||||
@@ -0,0 +1,49 @@
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies which world REGION an entity belongs to. M6 splits the single server world into two
|
||||
/// spatial regions at a large coordinate offset — the persistent home <see cref="RegionId.Base"/> and
|
||||
/// the procedurally-arranged <see cref="RegionId.Expedition"/> — and uses per-connection GhostRelevancy
|
||||
/// to replicate each region only to the connections whose player is currently in it. Server-side only
|
||||
/// (NOT a [GhostField]; the server makes all relevancy decisions). Added to players on spawn and to
|
||||
/// every region-scoped ghost the server spawns. Untagged ghosts are global (relevant to everyone).
|
||||
/// </summary>
|
||||
public struct RegionTag : IComponentData
|
||||
{
|
||||
/// <summary>Region id (see <see cref="RegionId"/>): 0 = base, 1 = expedition.</summary>
|
||||
public byte Region;
|
||||
}
|
||||
|
||||
/// <summary>Region ids for <see cref="RegionTag.Region"/> (a byte, not an enum, to keep server/Burst code trivial).</summary>
|
||||
public static class RegionId
|
||||
{
|
||||
/// <summary>The persistent, shared home base.</summary>
|
||||
public const byte Base = 0;
|
||||
|
||||
/// <summary>The procedural expedition field (offset far from the base on +X).</summary>
|
||||
public const byte Expedition = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic mapping of a region id to its world-space origin. The base region keeps the existing
|
||||
/// home-base coordinates; the expedition region lives at a large +X offset so the two never overlap in
|
||||
/// the single shared PhysicsWorld. Pure (no RNG/wall-clock) — server-authoritative teleports and field
|
||||
/// spawners resolve region positions through here.
|
||||
/// </summary>
|
||||
public static class RegionMath
|
||||
{
|
||||
/// <summary>World-space X offset of the expedition region from the base region.</summary>
|
||||
public const float ExpeditionOffsetX = 1000f;
|
||||
|
||||
/// <summary>World-space origin of <paramref name="region"/>, given the base center (BaseGridMath.PlotCenter).</summary>
|
||||
public static float3 RegionOrigin(byte region, float3 baseCenter)
|
||||
{
|
||||
return region == RegionId.Expedition
|
||||
? baseCenter + new float3(ExpeditionOffsetX, 0f, 0f)
|
||||
: baseCenter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09694e58455f41b439cdf791c3c421d8
|
||||
@@ -0,0 +1,17 @@
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Client -> server request to move the sender's player between world regions (base <-> expedition).
|
||||
/// A one-off action, so an RPC (not a per-tick predicted input), mirroring <see cref="StorageOpRequest"/>.
|
||||
/// TargetRegion is a byte (see <see cref="RegionId"/>) to keep the generated serializer trivial. The
|
||||
/// server teleports the sender's player to the region origin and flips its <see cref="RegionTag"/>, which
|
||||
/// re-scopes GhostRelevancy so the client gains the target region's ghosts and drops the old region's.
|
||||
/// </summary>
|
||||
public struct RegionTransitRequest : IRpcCommand
|
||||
{
|
||||
/// <summary>Destination region id (see <see cref="RegionId"/>).</summary>
|
||||
public byte TargetRegion;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32c76b46284ed1845a412ca030de2499
|
||||
Reference in New Issue
Block a user