Files
zfxaction26_1/groundwork.md
2026-04-14 02:22:05 +02:00

22 KiB

Side Scroller Shooter Groundwork

Goal

This document defines the technical foundation for a classic 2D side scroller shooter built with Godot 4 and C#. The framework must support the whole genre family, not a single game design: player movement, weapons, enemies, bosses, pickups, scrolling levels, scripted encounters, layered music, overlapping sound effects, and strong debugging tools. Final game-specific rules and content will be built later on top of this groundwork.

The non-negotiable constraint is that all authoritative gameplay simulation lives in one pure .NET project with no Godot dependency. Godot is the host for rendering, input capture, authoring UX, audio playback, and debug tooling. The same simulation assembly is referenced by both xUnit tests and the Godot host so all gameplay logic can be verified without engine or multimedia interference.

Hard Constraints

  1. Single pure simulation project. Do not split runtime gameplay and content definitions into separate assemblies unless a real dependency problem appears later.
  2. Deterministic fixed-step simulation. The same content, seed, and input action stream must produce the same state and hashes every run.
  3. Serializable input actions. The simulation consumes serializable action batches so recorded runs can be replayed exactly.
  4. Serializable full state. The entire simulation state must be serializable for save/load, replay checkpoints, and desync verification.
  5. Thin Godot host. Godot must not become gameplay authority for physics, damage, AI, triggers, or progression.
  6. xUnit-first verification. The simulation project is expected to reach 100% gameplay coverage; exclusions are acceptable only for clearly non-logic code such as generated tables or serializer boilerplate.

Repository Layout

The Godot project should not live at repository root. With SDK-style .NET projects, a root-level Godot .csproj will glob in files from subfolders, which makes it too easy to accidentally compile tests and simulation sources into the Godot assembly.

Recommended layout:

/SideScrollerGame.sln
/src/SideScrollerGame.Sim/
    SideScrollerGame.Sim.csproj
    /FixPoint/                     # preferred final home
    /Definitions/
    /Runtime/
    /Systems/
    /Serialization/
    /Replay/
    /Verification/
/tests/SideScrollerGame.Sim.Tests/
    SideScrollerGame.Sim.Tests.csproj
/godot/
    project.godot
    SideScrollerGame.Godot.csproj
    /.godot/
    /scenes/
    /scripts/host/
    /assets/
    /addons/
/content/compiled/
/tools/

Current repository note:

  • The existing root FixPoint/ folder is acceptable during bootstrap.
  • The preferred end state is to move it under src/SideScrollerGame.Sim/FixPoint/ and adjust namespaces once the simulation project is created.
  • The Godot project should be moved under /godot before the real solution structure grows.

Dependency rules:

Godot Host -> SideScrollerGame.Sim
Tests      -> SideScrollerGame.Sim
Sim        -> no Godot references

Within the simulation project, separate concerns by namespaces and folders, not by extra assemblies:

  • SideScrollerGame.Sim.Math
  • SideScrollerGame.Sim.Definitions
  • SideScrollerGame.Sim.Runtime
  • SideScrollerGame.Sim.Systems
  • SideScrollerGame.Sim.Serialization
  • SideScrollerGame.Sim.Replay
  • SideScrollerGame.Sim.Verification

Assembly Responsibilities

SideScrollerGame.Sim

  • fixed-step world simulation
  • immutable gameplay and level definitions
  • deterministic numeric types and RNG
  • physics and collision
  • AI and behavior execution
  • input action ingestion
  • save/load serialization
  • replay support
  • debug hashes and verification hooks

SideScrollerGame.Godot

  • editor bootstrapping
  • converting physical input into simulation actions
  • fixed-step runner and interpolation
  • rendering and presentation node lifecycle
  • animation mapping
  • audio playback, music queue, and cross-fading
  • authoring and validation tools
  • development-time timeline controls

Simulation Boundaries

The simulation owns:

  • world state
  • movement and collision
  • weapon firing and ammo
  • enemy behavior
  • spawn logic
  • damage, death, score, drops, checkpoints
  • scripted triggers
  • camera gameplay rules
  • music state requests

The Godot host owns:

  • drawing sprites, meshes, parallax, particles, and UI
  • collecting OS input and converting it to actions
  • audio stream playback and mixing
  • interpolation between snapshots
  • editor tooling and authoring UX
  • developer overlays and transport controls

Godot physics, animation trees, collision layers, and timers must never become authoritative for gameplay.

Deterministic Simulation Model

Use a fixed rate of 60 simulation ticks per second. This is appropriate for classic side scroller shooters and is easy to reason about. Rendering can run faster or slower; simulation must not.

Determinism rules:

  • no floating-point math in simulation logic
  • no use of DateTime, wall clock, threads, async work, GUIDs, or nondeterministic iteration order
  • stable entity iteration order
  • immutable definitions after load
  • deterministic RNG with explicit persisted state
  • all simulation outputs derived only from definitions, current state, seed, and action stream

Numerics:

  • use fixed-point math throughout the simulation
  • represent position, velocity, and acceleration in deterministic units
  • keep map sizes and tile sizes as integer values

The current FixPoint/ folder should be treated as simulation infrastructure, not Godot host code.

Simulation API

The simulation should expose a narrow runner API that is safe for tests, replays, and Godot hosting:

public sealed class Simulation
{
    public Simulation(GameDefinition gameDefinition, SimulationConfig config, int seed);

    public int CurrentTick { get; }
    public SimulationState CurrentState { get; }
    public WorldSnapshot PreviousSnapshot { get; }
    public WorldSnapshot CurrentSnapshot { get; }

    public TickResult Step(in TickActionBatch actions);

    public byte[] SaveState();
    public static Simulation LoadState(byte[] data, GameDefinition gameDefinition, SimulationConfig config);
}

TickResult should contain:

  • gameplay events
  • presentation events
  • sound requests
  • music requests
  • spawn/despawn notifications
  • per-tick debug hash
  • optional verification report

Input Model and Replay Format

The simulation should receive user intent through serializable actions, not direct polling state.

Recommended approach:

  • Godot host converts raw keyboard/gamepad input into SimulationAction records
  • each tick receives a TickActionBatch
  • continuous controls are handled by edge-triggered actions plus persistent input state inside the simulation

Example action model:

public abstract record SimulationAction;

public sealed record MoveAxisChanged(PlayerId PlayerId, sbyte X, sbyte Y) : SimulationAction;
public sealed record AimAxisChanged(PlayerId PlayerId, short X, short Y) : SimulationAction;
public sealed record ButtonChanged(PlayerId PlayerId, InputButton Button, bool IsPressed) : SimulationAction;
public sealed record WeaponSlotSelected(PlayerId PlayerId, int SlotIndex) : SimulationAction;

public sealed record TickActionBatch(int Tick, ImmutableArray<SimulationAction> Actions);

Why this matters:

  • the exact action stream can be recorded to disk
  • bug reports can attach a replay instead of describing a scenario
  • tests can load recorded runs and verify hashes
  • stepping to tick N is just replay plus optional checkpoint restore

Replay file contents:

  • content hash or version id
  • simulation config
  • seed
  • initial level/start condition
  • per-tick action batches
  • optional periodic serialized state checkpoints

Full State Serialization

The entire simulation state must be serializable. This is not just for player save games. It is also required for:

  • replay checkpoints
  • fast-forward and jump-to-tick tooling
  • save/load correctness verification
  • deterministic debugging

SimulationState should include:

  • current tick
  • active level and checkpoint
  • all runtime entities and subsystem state
  • timers and cooldowns
  • RNG state
  • squad and flock coordination state
  • scripted trigger state
  • camera gameplay state
  • pending spawn queues
  • music/gameplay progression state
  • player input hold state

Serialization guidance:

  • use an explicit versioned serializer owned by the simulation project
  • avoid relying on Godot serialization
  • keep format deterministic and stable
  • support both compact runtime format and optional debug-friendly dump format if useful

Built-In Desync Verification

During development, each step should optionally verify save/load correctness and step determinism by round-tripping a clone.

Recommended VerificationMode:

  • None
  • RoundTripState
  • RoundTripAndStepClone

RoundTripAndStepClone algorithm for each tick:

  1. serialize the live simulation
  2. deserialize a clone from those bytes
  3. compare live and clone state hashes or normalized snapshots
  4. apply the same TickActionBatch to both instances
  5. compare resulting states again
  6. report any mismatch with tick number, subsystem, and serialized artifacts

This catches two different problems:

  • save/load desync: serialization fails to reconstruct the same state
  • step desync: cloned and live instances diverge when stepping the same input

This mode is expensive and should be optional, but it is exactly the right tool for early framework development.

Runtime Debug Time Controls

Development UX must support stepping and replay inspection from day one.

Required transport controls:

  • Restart
  • Play/Pause
  • Advance One Step
  • Fast Forward To Step X

Recommended supporting features:

  • current tick display
  • current replay seed
  • state hash display
  • verification on/off toggle
  • playback speed multiplier
  • jump to checkpoint
  • scrub to nearest saved checkpoint, then replay to requested tick

Implementation guidance:

  • maintain a RunController in the Godot host
  • cache serialized checkpoints every N ticks to make fast-forward practical
  • treat restart as reloading the initial seed, content, and replay
  • do not let presentation-only pause state alter simulation state

World Representation

Avoid a generic ECS. A domain-specific, data-oriented model is a better fit for this genre.

Recommended runtime state groups:

  • PlayerState
  • EnemyState
  • ProjectileState
  • PickupState
  • HazardState
  • PlatformState
  • TriggerState
  • CameraState
  • LevelRuntimeState
  • SquadState

Cross-cutting concepts:

  • EntityId
  • faction/team
  • transform
  • velocity
  • health and armor
  • hurtboxes and hitboxes
  • timers and modifiers
  • presentation state keys

Keep definitions immutable and runtime state mutable.

Fixed Tick Pipeline

Each tick should run in a fixed order:

  1. apply incoming actions to player input state
  2. advance timers, cooldowns, modifiers, and scripted sequences
  3. update AI, squad logic, and flocking decisions
  4. resolve weapon intents and spawn projectiles/effects
  5. integrate movement
  6. resolve map, platform, and environment collision
  7. resolve projectile, melee, and contact hits
  8. apply damage, knockback, deaths, drops, score, and checkpoint changes
  9. process triggers, camera rules, wave progression, and music state transitions
  10. build events, snapshots, and hashes

This order must remain explicit and stable. New systems should be inserted deliberately, not opportunistically.

Physics and Collision

The simulation owns all gameplay physics.

Recommended model:

  • 2D kinematic actors
  • deterministic AABB-based collision for actors
  • tile-grid collision for level geometry
  • one-way platforms
  • ladders and climb volumes
  • hazard and water zones
  • deterministic moving platforms
  • sweep or raycast projectile movement to avoid tunneling

Movement design targets:

  • grounded movement
  • coyote time and jump buffering
  • crouch and stance-dependent hitboxes
  • knockback and stun
  • moving platform carry behavior
  • optional slopes later if needed

Do not use Godot rigid bodies as gameplay authority.

Frame Interpolation

Rendering should interpolate between two authoritative simulation snapshots.

Godot host loop:

  1. accumulate real frame time
  2. run zero or more fixed simulation steps
  3. compute interpolation alpha
  4. render between PreviousSnapshot and CurrentSnapshot

Interpolate:

  • position
  • aim direction
  • camera anchor
  • recoil offsets

Do not interpolate:

  • deaths
  • pickups
  • state machine transitions
  • damage flashes
  • spawn and despawn boundaries

Presentation nodes should be mapped by EntityId and recreated only on tick boundaries.

Animation Model

The simulation should emit semantic animation state, not Godot clip names.

Example semantic outputs:

  • locomotion: Idle, Run, JumpRise, JumpFall, Land, Crouch, Climb
  • combat: Fire, Reload, Charge, Melee, Overheat
  • damage: Hurt, Invulnerable, Dead
  • modifiers: facing, aim sector, grounded, wet, frozen, heavy_weapon

The Godot host maps semantic state to AnimationTree, sprite animations, blend spaces, or custom timelines.

Audio Architecture

The simulation emits logical audio requests; the Godot host performs actual playback.

Overlapping Sound Effects

Simulation should emit SoundEvent data:

  • cue id
  • source entity or world position
  • category/bus
  • priority
  • optional pitch/variant seed

The Godot host should:

  • pool multiple audio players
  • allow overlapping playback for repeated weapons and explosions
  • cap voices per cue and bus
  • support both positional and UI sounds

Cross-Fading Music Queue

The simulation should emit logical music requests, not manipulate players directly.

Recommended requests:

  • Queue(trackId)
  • Replace(trackId, fadeOutTicks, fadeInTicks)
  • PushPriority(trackId)
  • PopPriority(trackId)
  • Stop(fadeOutTicks)

The Godot host should implement a MusicDirector with:

  • at least two music players for cross-fading
  • support for intro, loop, and optional outro segments
  • deterministic transitions triggered from simulation ticks
  • queue inspection in the debug UI

Gameplay Definitions

Definitions live inside the simulation project under Definitions/ and are loaded from compiled engine-agnostic data.

HeroDefinition

Include:

  • id, tags, display info
  • collider and stance shapes
  • health, armor, lives, invulnerability ticks
  • movement profile
  • weapon loadout
  • pickup interaction rules
  • animation profile id
  • sound profile id

EnemyDefinition

Include:

  • id, archetype, faction, tags
  • collider and hurtboxes
  • health and contact damage
  • movement profile
  • behavior definition id
  • squad role support
  • weapon set
  • score reward and drop table
  • boss phase data when needed

WeaponDefinition

Include:

  • slot type
  • fire mode
  • cadence and burst settings
  • ammo and reload rules
  • muzzle offsets
  • projectile or hitscan profile
  • recoil, spread, and status effects
  • animation and audio ids

ProjectileDefinition

Include:

  • speed and lifetime
  • gravity scale
  • collider
  • collision mask
  • pierce behavior
  • hit response
  • damage payload
  • explosion or follow-up spawn ids

PowerUpDefinition

Include:

  • pickup type
  • effect payload
  • duration
  • stacking policy
  • despawn rules
  • magnet behavior
  • audio and presentation ids

Modifier Model

Use a shared modifier system for buffs and debuffs:

  • additive and multiplicative stat changes
  • capability flags
  • timed effects
  • stack limits
  • refresh behavior

Behavior Framework

Use a data-driven hierarchical state machine model with reusable sensors and actions. Do not rely on Godot scripts for enemy logic.

Reusable sensors:

  • target in range
  • line of sight
  • ground ahead
  • wall ahead
  • recently damaged
  • timer elapsed
  • squad signal received

Reusable actions:

  • move
  • stop
  • jump
  • fire
  • dodge
  • retreat
  • strafe
  • hover
  • call squad action

Squadrons and Flocking

The behavior framework must also support coordinated enemies, not just isolated actors.

Required concepts:

  • SquadDefinition
  • SquadState
  • leader/follower roles
  • formation anchor and slots
  • shared target selection
  • squad orders such as attack, regroup, retreat, flank
  • separation, cohesion, and alignment style flocking for flyers or swarms

Use flocking as a tunable subsystem, not a hard-coded enemy type. The simulation should support:

  • loose flying swarms
  • tight escort formations
  • attack waves that break formation temporarily
  • rejoin logic after disruption

Level Data Structure

Levels must support both authored geometry and deterministic encounter scripting.

Recommended LevelDefinition:

  • metadata and dimensions
  • tile layers and collision/material map
  • spawn markers
  • pickups and hazards
  • moving platforms and paths
  • checkpoints
  • trigger zones
  • encounter and wave definitions
  • camera zones and locks
  • music zones and scripted transitions
  • scripted event graph

Keep authored Godot scenes as input to a compile/export step. The simulation and tests should run on compiled runtime data, not raw scene trees.

Level Editing UX in Godot

Godot should be used as the authoring front-end, but authored scenes must compile into engine-agnostic runtime definitions.

Recommended workflow:

  1. author level scenes in Godot
  2. place custom marker nodes for spawns, triggers, checkpoints, patrol paths, squad anchors, and music zones
  3. validate the authoring scene
  4. compile to runtime content under content/compiled/
  5. run game and tests against compiled data

Required authoring UX:

  • custom inspector editors for definitions and references
  • gizmos for triggers, camera bounds, paths, and squad formations
  • validation panel for bad ids, overlapping markers, broken references, and invalid encounter setups
  • buttons for Validate, Compile, and Play From Here
  • preview of wave timing, camera rules, and music transitions where practical

Content Pipeline

There is still a distinction between authoring content and runtime content, but it does not justify a separate assembly.

Authoring content:

  • Godot scenes and resources
  • marker nodes and editor-only metadata

Compiled runtime content:

  • engine-agnostic serialized definitions
  • loaded by the simulation project
  • used identically by tests and the Godot host

Compilation should:

  • resolve references
  • validate identifiers and links
  • flatten authored data into deterministic DTOs
  • assign stable content hashes
  • produce clear compile errors

Save/Load, Replay, and Debugging

These three systems should be designed together.

Save/Load

Save files should contain:

  • content hash/version
  • serialized SimulationState
  • optional metadata such as timestamp or user-visible slot info

Replay

Replay files should contain:

  • content hash/version
  • simulation config
  • seed
  • initial start state or entry point
  • action batches per tick
  • optional periodic state checkpoints

Debugging

A bug report should ideally include:

  • replay file
  • final tick number
  • expected vs actual state hash
  • verification failure artifacts if present

This allows deterministic reproduction without stepping through engine code.

Testing Strategy

Use xUnit for all automated simulation tests.

Test categories:

  • unit tests for movement, collision, damage, weapons, modifiers, and trigger logic
  • scenario tests for encounters and level scripts
  • replay tests using recorded action streams
  • serialization roundtrip tests for full simulation state
  • desync verification tests for clone-step comparison
  • property or fuzz tests for invariants where useful

Coverage expectations:

  • target 100% line and branch coverage for gameplay logic in SideScrollerGame.Sim
  • allow narrow exclusions only for generated lookup tables, source-generated serializers, or similar non-logic infrastructure
  • do not use exclusions to hide untested gameplay systems

Recommended tooling:

  • xUnit
  • coverlet
  • deterministic test fixtures and builders

Implementation Phases

Phase 1: Project Restructure

  • move the Godot project under /godot
  • create src/SideScrollerGame.Sim
  • create tests/SideScrollerGame.Sim.Tests
  • temporarily link the existing root FixPoint/ sources into the simulation project if needed

Phase 2: Simulation Foundation

  • integrate fixed-point numerics and deterministic RNG
  • define SimulationState, snapshots, ids, events, and core step runner
  • define serialization contracts

Phase 3: Replay and Verification

  • define serializable action model
  • implement replay recording and playback
  • implement per-step roundtrip and clone-step verification
  • add debug hashes

Phase 4: Movement and Collision

  • implement actor movement, platforms, ladders, hazards, and projectile sweeps
  • build exhaustive xUnit coverage for collision edge cases

Phase 5: Combat and Behaviors

  • add hero, enemy, weapon, projectile, powerup, and modifier definitions
  • implement behavior graphs, squads, flocking, and boss phase support

Phase 6: Levels and Authoring

  • implement level definitions, triggers, checkpoints, and scripted graph
  • build Godot authoring nodes, validators, and compile/export flow

Phase 7: Presentation Host

  • add interpolation, presenter registry, animation mapping, audio playback, and music director
  • add runtime transport controls and replay inspection UI

Key Decisions to Hold

  • keep all gameplay authority in the pure simulation project
  • keep content definitions in that same project unless a real dependency issue emerges
  • keep the Godot host in its own subfolder and project
  • keep replay, save/load, and verification as first-class features
  • keep fixed-point determinism as the baseline
  • keep the debug transport controls available throughout development

If these constraints are maintained, the framework will remain flexible enough for later game design work without collapsing into engine-driven logic or untestable behavior.