12 KiB
12 KiB
Unity Tower Defense — Project Context
Purpose
A snapshot of where the project is and how it works — the authoritative reference for current architecture, implemented systems, conventions, and known debt. It pairs with Project_Roadmap.md, which is the forward-looking plan. When the two disagree, this document describes what exists today; the roadmap describes what's planned next.
Last substantial update: 2026-06-24.
Game overview
A co-op tower-defense / roguelike hybrid for up to 9 players.
- Maze defense (Wintermaul-style): players build towers to force enemies along a longer path through their own zone. Lives are a shared pool; gold is per-player.
- Roguelike layer (now core to the design): every player starts with the same three towers and builds out a personal "deck"/"build" through a 3-option draft presented at match start and after every wave. Choices span new towers, systemic upgrades, builder abilities, enemy debuffs, and relic quests. See
Project_Roadmap.mdfor the full design. - Target platform: Steam (Windows / Linux / Steam Deck).
- Visual direction (aspirational): "painted tabletop miniature" look with Spider-Verse-style stepped (on-2s) enemy animation. Current visuals are placeholder (primitive meshes / cones, sourced creature models).
Engine & tech
Unity 6.4 (6000.4.4f1), URP, IL2CPP, .NET Standard 2.1, Linear color space, new Input System, Force Text serialization. Netcode: Netcode for GameObjects (NGO) 2.x.
Repository & collaboration
- Self-hosted Forgejo:
https://git.marlboro-bc.duckdns.org/yeahweregames/UnityTowerDefense(git remoteorigin). A legacy GitHub mirror exists as remotegithub. - Multiple contributors work in parallel on feature branches merged to
mainvia PRs. Expectmainto move between sessions; rebase/branch off the latest. - IP guardrail: the prototype uses Games-Workshop-adjacent placeholder content (race names, sourced models). The repo stays private, no public builds/demos, until that content is replaced. Maintain a plain-text asset manifest of IP-derived assets to swap before any public release.
Architecture & conventions
- Server-authoritative gameplay; local-only UI/visual state. Only gameplay-meaningful state is networked. Selection, placement ghosts, animation, and the paint cursor are client-local.
- Data-driven via ScriptableObjects:
TowerDefinition,EnemyDefinition,WaveDefinition,RaceDefinition,GoldConfig,BuffDefinition/BuffCategory,DraftOption(+NewTowerDraftOption). Designers tune stats in assets, not code. - Per-player state pattern:
NetworkBehaviours on the Player prefab with a staticGetForClient(clientId)/Localregistry. Current set:PlayerGoldManager,PlayerMatchState,PlayerBuffManager,PlayerTowerDeck,PlayerDraft. - Networked identifiers are catalog indices.
TowerTypeIdindexesTowerPlacementManager.towerDefinitions[];DraftOptionIdindexesDraftPool. Stable within a match, not across sessions — cross-match persistence will need stable IDs (asset GUID or a serialized StableId). - Namespaces:
TD.Core(enums, palettes, grid math),TD.Gameplay(builder, placement, match/wave/pathfinding state, economy, enemies, deck),TD.Gameplay.Draft(draft system),TD.Combat(TowerCombat/Projectile),TD.Levels(in-engine authoring + bake),TD.UI,TD.Net. - Scenes:
MainMenu→Lobby→ a Match level (9Player,Main).
Engineering principles (carried across sessions)
- Debug to root cause. No defensive workarounds or per-frame state-correcting hacks — find and fix the actual cause.
- Restate design intent before coding. Confirmed valuable repeatedly; prevents rework.
- Server owns gameplay; clients render. Authoritative state on the server, visualization derived locally on each peer.
Implemented systems
Match foundation
MatchState(NetworkBehaviour scene singleton): phase (Lobby/CountDown/Playing/Victory/Defeat), current wave, shared lives.PlayerMatchState(per-player): slot allocation, race selection, ready state. Authoritative client-id →PlayerSlotmapping.
Combat & towers
TowerCombat(server targeting loop;OnTargetAcquired/OnTargetLost/OnFire/OnAreaFiredevents),Projectile, hitscan + projectile paths, target types Single/Splash/Chain/AllInRange, damage types incl. Electric, range indicator. Air-targeting done (GroundedOnlyskips flyers). Range checks are 3D.TowerPlacementManager(server-validated placement queue) —SpawnTowernow seats towers flush on the ground from their mesh bounds (SeatOnGround), so any pivot works (replaced the old hard-coded y=0.5).Builderbuild queue with staged construction/pause/cancel/refund, RTS selection, owner tinting.- Construction-phase build visuals:
BuildSiteVisualswaps between an ordered set of phase prefabs as a tower builds —ConstructionPhaseSetSO + optional per-towerTowerDefinition.ConstructionPhases, with a project-default set and a legacy cube Y-scale fallback.BuildSiteVisualresolves its def viaTowerTypeId(catalog), likeTowerInstance. - Tesla Coil tower: Electric,
AllInRange(zaps every enemy in range each 0.5s tick).TeslaArcVisualdraws arced, crackling, HDRLineRendererbolts that flicker, plus real point lights (emitter flash + per-impact lights at struck enemies, capped) so the electricity actually illuminates the scene — emission/bloom alone don't cast light.
Enemies & pathfinding
EnemyHealth(replicated HP, damage types,IsFlying, held state),EnemyStatus(lingering effects: slow/DoT),EnemyMovement(A* path following, zone-leak attribution, death/sink sequence).PathfindingService(A* on the runtime walkability grid, octile heuristic, corner-cut prevention, line-of-sight path smoothing; re-paths on tower placement/removal).- Flying enemies: path on the baked terrain grid (
LevelLoader.IsBaseWalkable) so they soar over towers; compute once, never re-path; spawn elevated byEnemyDefinition.FlightHeight. - Content: ~10 enemy definitions/prefabs (Crystal Golem / Cyclops / Ent variants, Undead Drake = the flying test enemy), 10 wave definitions.
Lobby, connection & races
MainMenu+Lobbyscenes; Direct-IP viaNetworkBootstrap(the single seam for the deferred Steam swap) +UnityTransport;LobbyService,SessionFlow; Quick Start dev shortcut.- Race data + selection UI:
RaceDefinition,RaceRegistry, 16-slotRaceId,RaceSelectionOverlay. Two placeholder races (both use the default builder). Note: "Race" is the code term; the roguelike design calls this concept "Builder" — naming reconciliation is an open decision.
Economy
PlayerGoldManager(per-player, server-validated spend/award),GoldConfig(starting gold, per-wave kill rewards, completion + no-leak bonuses),WaveManagerorchestration (prep countdown, per-zone spawning, kill-gold attribution, lives pool, Victory/Defeat).
Roguelike — tower deck & draft (the current focus)
PlayerTowerDeck(per-player): the growable set of unlockedTowerTypeIds. Starts at the base set (seeded byWaveManagerfromTowerPlacementManager.startingDeck); placement is gated server-side (TowerNotInDeckrejection); the HUD build grid reads the local deck and rebuilds live on change. Merged tomain.- Draft system (Slice 1 — spine + "new tower"):
DraftOption(abstract SO) /NewTowerDraftOption,DraftPool(scene singleton catalog),PlayerDraft(per-player offered set + pick/buy-roll RPCs),DraftService(server weighted-random generation + offer/auto-resolve). The prep phase is the draft window (wave 1's prep = the match-start draft); unpicked drafts auto-resolve at prep end. Gold "Buy Roll" purchases an extra roll any time. Non-modal HUD overlay. On branchfeature/draft-system, verified working in-engine; pending commit/merge as of session end. - Draft Slice 1 is merged (it's in
main/the active branch line, not justfeature/draft-system). Tower content available to draft: Basic Arrow (ground+air single), Siege Cannon (ground-only splash), Wall (2×2 maze-marker), Tesla Coil (Electric AllInRange). Arrow/Siege have cone meshes; Tesla is a ProBuilder mesh. So the "new tower" draft finally has real content — starting deck vs. draftable pool is a live tuning knob.
Paint system (PAUSED)
- In-match paint (R/G/B + Reset) recolors owned towers and drives effects (Red=Splash, Green=Poison, Blue=Cold), server-authoritative. Frozen pending the roguelike reconciliation decision (it overlaps with systemic damage-type upgrades).
Level authoring
- In-engine
TD.Levelsauthoring volumes (player zones, spawners, leak exits, goals) +LevelData+ a bake pipeline producing walkability/placement/owner grids.
Rendering & post-processing
- URP with 6 quality-tier RP assets in
Assets/_Project/Settings/Render Settings/(Mobile, PC, URP-Low/Medium/High/Ultra). HDR must be on per-asset (Quality section) for bloom/glow. - Project-wide post-processing via the
DefaultVolumeProfile(Edit→Project Settings→Graphics→Default Profile) — a grimdark grade (ACES tonemapping, desaturate/contrast, Split Toning, Bloom, Vignette, Film Grain). It has no per-property checkboxes (it's the always-on baseline). Gotcha: scene Volumes (e.g. the URP-templateSampleSceneProfilein9Player) override the default — remove/redirect them so the default applies everywhere.
HUD & dev tools
HUDController(UI Toolkit): gold/wave/lives/leaks, scoreboard, minimap, selection portrait + context panel, build/paint command grid, build progress, match-end overlay, chat, buff menu, and the new draft overlay.DevWaveControls: F9 / "Force Next Wave", F8 / "Grant Next Tower" (dev stand-in for deck growth).
Known placeholders & technical debt
- Visuals are placeholder throughout (primitive/cone towers, sourced enemy models). See roadmap art TODOs.
- Enemies lack idle animation support — needs adding across all enemy prefabs.
- Tower footprint visuals don't fill their 2×2 space — meshes read smaller than the tile area they occupy; should visually reflect the full footprint.
- Grimdark grade is in, but the scene fights it — the post-processing reads correctly, yet the bright/flat-lit ice map (
9Player) undercuts the mood. Grimdark needs a lighting pass (low ambient, hard moody key) and ultimately darker level art; post alone can't carry it. - URP additional-lights budget now matters — Tesla flash + per-impact lights stack across towers; watch the URP asset's Additional Lights "Per Object Limit" / Per-Pixel setting.
- Tower catalog is now a single source of truth:
TowerPlacementManager.towerDefinitions[](by-index, =TowerTypeId).TowerInstancereplicates that index and resolves viaTowerPlacementManager.GetDefinition. (The old by-nameTowerRegistrywas removed 2026-06-24 — add new towers only totowerDefinitions+DraftPool.) - Catalog index 0 is a reserved sentinel (valid
TowerTypeIds start at 1) — easy to forget when wiring the catalog in the inspector. - Catalog-index identifiers aren't session-stable — blocks cross-match persistence until a stable ID is added.
- Paint system frozen; Race vs Builder naming unresolved; the gold "Buy Roll available any time" rule is provisional and may change.
- Stubbed/unbuilt: tower Upgrade/Sell actions (HUD buttons disabled), enemy resistances/weaknesses, in-match race-pick countdown.