Files
Project-M/Docs/Vault/07_Sessions/2026/2026-06-02_GameFeel_Identity.md
T
kronic e362aaeb43 Import art/VFX asset packs + game-feel systems; normalize texture extensions to lowercase for LFS
Add BefourStudios SciFi environment packs, Gabriel Aguiar VFX, and the
ShaderCrew Toon Shader embedded packages, plus combat/enemy/wave/death
gameplay systems and supporting vault docs/screenshots.

Rename 11 vendor textures from uppercase .PNG/.HDR to lowercase so the
case-sensitive Git LFS filters (*.png/*.hdr) match on case-sensitive
filesystems (Linux CI, case-sensitive macOS), not just locally where
core.ignorecase=true masks the gap. Each .meta moved with its asset so
GUID references are preserved. All ~1000 binaries tracked via LFS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 22:50:43 -07:00

10 KiB
Raw Blame History

date, type, tags, permalink
date type tags permalink
2026-06-02 session
session
dots
netcode
game-feel
identity
enemy-ai
presentation
hud
urp
m5
gamevault/07-sessions/2026/2026-06-02-gamefeel-identity

Session 2026-06-02 — "First Blood": game-feel & identity slice

Goal

Before more systems work, make the project feel like a game — in feel, look, and play — and give it an identity. Diagnosis: M1M5 are systems-complete but identity-less — pillars are mechanical (no fiction), and there is no feedback layer (grep confirmed zero audio/particles/VFX/shake/UI), the only enemy is an inert dummy, and there's no death/respawn or HUD. Operator chose: identity = sci-fi frontier colony, first slice = the full game-feel pass ("First Blood"), proceed plan→approve→build. Locked in DR-009_GameFeel_Identity_FirstBlood; fiction in Identity.

Process

  • Explore + propose (read-only): diagnosed the look/feel/play gap against Pillars + the M1M5 vault, surfaced the "mechanics without a fiction or a feedback layer" core, and proposed a spine (Identity) + 3 axes (Feel/Look/Play) with a recommended first slice. Operator picked the fiction + full slice via a 3-question gate.
  • context7 WAS reachable this session (vs DR-008_M5_HomeBase_BaseLayer_Storage). A 4-agent ctx7 verification workflow (Netcode / Entities-enableable / URP / DOTS→GameObject presentation) returned high-confidence verified API shapes + a key correction: an interpolated enemy ghost must be moved server-only in the plain SimulationSystemGroup, NOT in the predicted loop, and Dead is cleanest derived from replicated Health (no replicated enabled bit).
  • Implementation (sequential via MCP create_script/script_apply_edits — single editor, domain-reload-ordered; NOT a parallel swarm) in 5 stages, each compiled + console-checked + (where applicable) EditMode-tested + runtime-validated before the next.
  • Adversarial review workflow over the diff (12 agents: 4 dimensions → per-finding adversarial verification). It verified the architecture correct — the Husk is a properly interpolated/ownerless ghost (HasOwner=0, DefaultGhostMode=Interpolated), the derived Dead is rollback-safe (no GhostField), the local player's Health is never client-predicted, and presentation randomness is outside the sim — every netcode-desync suspicion came back not-a-bug. 3 confirmed low-severity findings, all the same root: the uint cooldown "0 = ready" sentinel can collide at tick wraparound (RespawnMath already guarded it; the three cooldown writers + the HUD didn't). Fixed via a shared TickUtil.NonZero guard at the three writes (EnemyAISystem/EnemySpawnSystem/AbilityFireSystem) and a wrap-safe NetworkTick compare in the HUD cooldown bar.

Done

Stage 1 — Husk enemy (ProjectM.Simulation/Combat, ProjectM.Server/Combat, ProjectM.Authoring/Combat): EnemyTag/EnemyStats/EnemyAttackCooldown/EnemySpawner + pure EnemyAIMath (SeekVelocity/InAttackRange/RingPosition); EnemyAISystem (server-only, plain SimulationSystemGroup [UpdateBefore(PredictedSimulationSystemGroup)] — seeks nearest living player, writes LocalTransform, appends DamageEvent on a NetworkTick-gated strike); EnemySpawnSystem (persistent server spawner, sustains MaxAlive on a deterministic ring around BaseAnchor); HealthApplyDamageSystem +EnemyTag death; EnemyAuthoring + EnemySpawnerAuthoring. Enemy.prefab duplicated from UpgradePickup.prefab (interpolated ownerless ghost) → EnemyAuthoring; EnemySpawner GameObject added to Gameplay.unity.

Stage 2 — Player death/respawn (ProjectM.Simulation/Player, ProjectM.Server/Combat): Dead (enableable, derived from Health) + RespawnState + pure RespawnMath; PlayerDeathStateSystem (both worlds, predicted, before movement/aim/fire — sets Dead + zeroes velocity); PlayerRespawnSystem (server-only, after the predicted group — schedule on death, refill+reposition on due). PlayerControlSystem/PlayerAimSystem/AbilityFireSystem gained .WithDisabled<Dead>(); PlayerAuthoring bakes Dead (disabled) + RespawnState.

Stage 3 — Combat juice (ProjectM.Client/Presentation): CombatFeedbackSystem (client-only managed SystemBase, PresentationSystemGroup) — edge-detects replicated Health → pooled billboard TextMesh damage numbers + runtime ParticleSystem hit/death/muzzle bursts + procedural AudioClip SFX + camera shake; Husk death via despawn-prune; local-fire muzzle via AbilityCooldown edge. PrototypeCameraRig +AddShake (presentation-only decayed shake).

Stage 4 — HUD (ProjectM.Client/Presentation): HudSystem (code-built uGUI overlay — RawImage health + cooldown bars over Texture2D.whiteTexture, legacy Text threat count + DOWNED/RESPAWNING overlay) driven from the local player ghost. ProjectM.Client.asmdef +UnityEngine.UI.

Stage 5 — Look (assets/editor): URP global Volume (Bloom/ACES Tonemapping/ColorAdjustments/Vignette) + dark flat ambient + exp fog + cool key light + URP HDR color grading; emissive re-tint (teal crew, cyan projectiles, orange Husks via new M_Husk, dark-metal walls/ground). Husk balance tuned (dmg 8→5, strike 36→48 ticks, speed 3.5→3.0).

Docs: Identity (the fiction) + linked from Pillars/Home; DR-009_GameFeel_Identity_FirstBlood.

Validation

  • EditMode 74/74 green (+12 vs M5's 62 — 7 EnemyAIMath, 5 RespawnMath). Console clean of code/Burst/ghost errors after every stage.
  • Runtime (single in-editor client, 6.4.7): Husks spawn (6) + replicate server↔client (6==6), chase from the ring to the player, deal contact damage (player → 0 HP). Death→respawn loop verified by polling: Health=0/Dead=True/RespawnTick scheduled → recovered at base full-HP/Dead=False. HUD renders (health bar, "HUSKS N", DOWNED overlay). Look confirmed via screenshots: dark metal world + cyan crew + orange Husks + bloom (after fixing the M_Dummy-as-wall-material orange-walls bug).
  • Adversarial review (12 agents): architecture verified correct; 3 confirmed low-severity findings (the uint cooldown 0-sentinel wraparound), all fixed (TickUtil.NonZero + HUD NetworkTick compare). EditMode 74/74 after fixes, console clean.

Diagnosis notes (for future me)

  • Interpolated ghost ≠ predicted. Move an ownerless interpolated enemy ghost server-only in plain SimulationSystemGroup ([WorldSystemFilter(ServerSimulation)]), NOT in PredictedSimulationSystemGroup. [UpdateAfter(PredictedSimulationSystemGroup)] (NOT [UpdateBefore] — the predicted group is OrderFirst, so UpdateBefore is silently ignored; corrected in the deepening pass) so a contact DamageEvent drains the following tick (~16ms, fine for melee). Stock LocalTransform replication carries its position — no hand-written [GhostField].
  • Derive enableable state, don't replicate it, when it's a pure function of already-replicated data. Dead = Health<=0 derived every predicted tick in both worlds is rollback-correct and dodges the [GhostEnabledBit] surface — same idiom as EffectiveCharacterStats. To toggle a disabled enableable component you must still visit the entity: .WithPresent<Dead>() (write) vs .WithDisabled<Dead>() (alive-only run). Simulate (also enableable) ANDs independently — keep .WithAll<Simulate>().
  • A Baker can bake an enableable component DISABLED (AddComponent<Dead>(e); SetComponentEnabled<Dead>(e, false);) → instances spawn alive with zero first-tick work (instantiated entities inherit the prefab's enabled state).
  • Presentation belongs in a client-only managed SystemBase in PresentationSystemGroup (once per frame, no rollback double-fire). Read ECS state via SystemAPI.Query INSIDE OnUpdate (registers job deps) + EntityManager.CompleteDependencyBeforeRO<T>() — NOT from a MonoBehaviour LateUpdate (the job-safety exception the camera rig hit). Entity is a stable client dict key for a ghost's lifetime; prune the cache each frame (a pruned enemy = a kill → death VFX at its last position; never DestroyEntity a ghost from the client — GhostDespawnSystem owns that).
  • Netcode-safe "hit-stop" = a camera punch, never Time.timeScale (which would corrupt the deterministic sim).
  • Code-built uGUI with no assets: RawImage over Texture2D.whiteTexture (anchor-driven fill, no sprite) + legacy Text with Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf"). Procedural AudioClip.Create + runtime ParticleSystem (Sprites/Default material, HDR start color to bloom) keep the slice asset-free.
  • Watch shared-material bleed when re-tinting: the walls glowed orange because M_Dummy doubled as the wall material — gave Husks their own M_Husk instead. Editing a prefab asset's component in code needs PrefabUtility.LoadPrefabContents → modify → SaveAsPrefabAsset (NOT SavePrefabAsset, which rejects the contents root) → UnloadPrefabContents.
  • ACES tonemapping needs the URP asset's color grading mode = HDR (m_ColorGradingMode = 1), else HDR bloom/emission won't tonemap right.
  • Editor is_focused matters again: play-enter domain reload + boot took ~1015s while unfocused; entity inspection via execute_code had to be retried until worlds booted. (Application.runInBackground keeps Play ticking but Edit mode still throttles unfocused.)

Open / deferred

  • Generalize AbilityFireSystem auto-target from TrainingDummyTag to EnemyTag (Husks are hittable via raw-aim swept hit; only the aim-assist snap is missing).
  • Respawn safety (brief invuln / Husk push-back) so respawn isn't instant re-death.
  • Real assets (audio, stylized crew/structure models, biomech Husks); SSAO renderer feature.
  • Enemy variety + a wave/threat director (ranged Husk, brute, broodmaker).
  • Multi-client validation of remote-player death VFX + per-client HUD.

Next

Either deepen the loop (enemy variety + wave director + respawn safety + auto-target on Husks) to make the combat sustain, or resume the milestone track at M6 — server-authoritative grid build placement (DR-008_M5_HomeBase_BaseLayer_Storage). The presentation + server-AI patterns from this slice are the reusable foundation for the former.