447 lines
22 KiB
Markdown
447 lines
22 KiB
Markdown
# CLAUDE.md
|
|
|
|
Guidance for working in this Unity project. **These rules are mandatory** — follow them
|
|
exactly when reading, writing, or refactoring any C# script.
|
|
|
|
---
|
|
|
|
## 1. Project overview
|
|
|
|
First-person survival/exploration game built in **Unity 6000.3.8f1** with **URP**.
|
|
The game is **online co-op multiplayer** (a single-player build is just a host with no
|
|
clients — there is no separate offline mode). Core systems already in place: first-person
|
|
locomotion, camera, stats (health/hunger/thirst), inventory + hotbar, crafting, cooking,
|
|
harvesting/tools, procedural scatter placement, audio (footsteps by surface + music),
|
|
seasons, and a full UI/menu/settings stack.
|
|
|
|
**Networking stack** (see §3):
|
|
- **FishNet 4.7.2** (in `Assets/FishNet/`) — the netcode.
|
|
- **Steamworks.NET** + **FishySteamworks** transport — Steam relay (SteamNetworkingSockets,
|
|
no port forwarding). Sessions are joined by a shareable code (host SteamID64) or Steam invite.
|
|
|
|
Context for current work:
|
|
- **This is final, production-quality code — not a prototype.** Aim for clean, well-named,
|
|
well-architected code that respects the patterns in this document. No quick hacks,
|
|
no "temporary" shortcuts; if something is worth doing, do it properly.
|
|
- **Multiplayer co-op is a hard constraint** — when touching any player, world or shared
|
|
system, assume two+ players exist (see §3).
|
|
- **Player body is split by viewpoint** — the **local owner** sees a **first-person arms
|
|
viewmodel** (his own hands/tools); **remote players** are drawn as a **full third-person
|
|
body**. Each client renders its own arms *and* everyone else's full body. The owner's own
|
|
third-person body is hidden from his first-person camera (owner-only vs remote-only gating
|
|
lives in `PlayerNetworkController` — see §3). HUD/audio feedback still applies on top.
|
|
- Animations use **DOTween** (tweens, not Animator) wherever possible.
|
|
- Shaders are **hand-written HLSL/ShaderLab** — do not propose Shader Graph.
|
|
|
|
---
|
|
|
|
## 2. Architecture — the golden rule
|
|
|
|
**Everything talks through the static event bus `PlayerEvents`**
|
|
(`Assets/GAME/Script/Player/Events/PlayerEvents.cs`).
|
|
|
|
- `PlayerEvents` is the source of truth for the **local** player's cross-system state
|
|
(`IsDead`, `IsGrounded`, `IsInventoryOpen`, `IsPaused`, `InputLocked`, …). Here "shared"
|
|
means shared *between systems on one client* — never *between players* (see §3).
|
|
- Producers call a `Raise*` method; the raiser updates the matching `IsX` state, logs
|
|
(under the `PLAYER_EVENT_LOG` define) and invokes the event.
|
|
- Consumers subscribe to the `event` in `OnEnable` and **unsubscribe in `OnDisable`**.
|
|
- **Never** reach across systems with `FindObjectOfType` or hard references to read state
|
|
that the bus already exposes. Add a new event/state to `PlayerEvents` instead — **except**
|
|
per-player networked state, which belongs on a `NetworkBehaviour` (SyncVar/RPC), not the
|
|
bus (see §3).
|
|
|
|
**Each script owns one concern — but "concern" means one reason to change, not one tiny
|
|
action.** A script reads its own inputs/state from the bus, does its job, and raises events for
|
|
others. Do not let a script mutate another system's internal data directly. If two systems need
|
|
to coordinate, route it through `PlayerEvents`.
|
|
|
|
Group by **cohesion, not by a per-action counter.** Splitting a feature into many micro-scripts
|
|
is its own anti-pattern (scattered logic, high coupling, inspector-wiring noise) just as much as a
|
|
god-object is. Keep together what changes together and is used together; split only at a **real
|
|
seam** — a genuinely different reason to change, a different collaborator set, or a file growing
|
|
unwieldy. The seam that always matters: **local player UX vs shared networked state** (see §3) —
|
|
never fuse across it. Reference: the build system splits into `BuildManager` (all the *local*
|
|
build UX — catalog, menu, placement) and `BuildRegistry` (the *networked* side — committed
|
|
structures + the ghost NetworkObject), one clean local/network seam, not a script per
|
|
sub-responsibility.
|
|
|
|
Data that is authored in the editor lives in **ScriptableObjects** (`ItemData`,
|
|
`CraftingRecipe`, `SurfaceSoundProfile`, `ScatterProfile`, `MusicPlaylist`,
|
|
`ToolDropTable`, …). Logic lives in `MonoBehaviour`s. Keep them separate.
|
|
|
|
Singletons exist where a single instance is genuinely global (`PlayerInventory.Instance`,
|
|
`PlayerStats.Instance`, …); guard every access for `null` — they may not exist yet or in
|
|
every scene.
|
|
|
|
**UI is a dumb view — a manager owns the data and populates it.** A UI component (a panel,
|
|
a card, a list row, …) must **never** reach for domain data itself: no catalog, no
|
|
`ScriptableObject` list, no scanning a database/model to decide *what* to show. It exposes a
|
|
`Populate(...)` / `Set(...)` method that takes a lightweight **view payload** (plain
|
|
icon / label / number data — never the authored `ScriptableObject`) plus a selection
|
|
callback, and renders exactly what it is handed. A dedicated **manager** owns the real data
|
|
(the catalog, the model), builds the view payload, pushes it into the UI, and handles the
|
|
callback — i.e. it decides what a click *does*. The view reports interactions back by
|
|
**index or token**, never by resolving the underlying data. Data flows one way (manager →
|
|
view), interactions flow back one way (view → manager via callback), and views stay
|
|
swappable and data-agnostic. Reference implementation: `BuildManager` (owns the catalog,
|
|
populates + handles picks) + `BuildMenuUI` / `BuildMenuCardUI` (pure views) +
|
|
`BuildCardView` (the view payload).
|
|
|
|
---
|
|
|
|
## 3. Multiplayer — the second golden rule
|
|
|
|
The game runs on **FishNet** (co-op, **client-authoritative** — friendly play, *not*
|
|
anti-cheat). Networking code lives in `Assets/GAME/Script/Network/`. When you write or
|
|
change anything that involves a player, the world, or shared state, obey these rules.
|
|
|
|
### `PlayerEvents` is the LOCAL player's bus only
|
|
|
|
The static `PlayerEvents` (state + events) represents **the local owned player**, never
|
|
remote players. Only owner-side code may call `Raise*`. Remote player copies must **never**
|
|
drive the static bus — they only update their own instance state (e.g. via SyncVar hooks).
|
|
Per-player networked state (health, position, …) belongs on the player's `NetworkBehaviour`
|
|
(SyncVar / RPC), not in `PlayerEvents`.
|
|
|
|
### Owner gating
|
|
|
|
`PlayerNetworkController` (on the player root) disables every owner-only component
|
|
(input, locomotion, camera, tools, **stats**, inventory, hotbar, interactor, audio,
|
|
lifecycle) and owner-only object (cameras, audio listener, tool holder) on non-owners, then
|
|
raises `LocalPlayerSpawned`. **Any new player component that simulates or reads local input
|
|
must be added to that `ownerOnlyBehaviours` list.** Scene/UI code must bind to the *local*
|
|
player — get it from `PlayerInventory.Instance` / `PlayerStats.Instance` (owner-only
|
|
singletons), **never** `FindObjectOfType<PlayerX>()`, which also matches disabled remote
|
|
puppets (their root GameObject stays active) and targets the wrong player.
|
|
|
|
### Keep systems OFF the player — it holds only its own intrinsic state
|
|
|
|
**Default to a scene object/registry, not the player prefab.** The player carries *only what is
|
|
intrinsically the local player itself*: its owner-only local simulation (input, locomotion, camera,
|
|
tools, animation driver) and its own per-player networked state (health/stats, inventory, the body).
|
|
**Everything that touches the world or shared state — and anything that does not need per-player
|
|
networking — lives OUTSIDE the player.** This is logical: a build, a placed structure, a shared
|
|
station, even a *per-player preview of a world action* (the build ghost), is world-facing, so it
|
|
belongs to a scene `NetworkBehaviour` (a `SyncList<…>` registry, or a NetworkObject it spawns),
|
|
**never bolted onto the player**. Do not reflexively put per-player networked state on the player
|
|
just because it is "per player" — ask first *"is this the player, or is it the world?"*. If it is
|
|
world/shared, it is a scene object. Reference implementations: `WorldObjectRegistry`, `BuildRegistry`
|
|
(committed structures + the spawned ghost preview), `CookingStation`. The player-hosted pattern
|
|
below is only for state that genuinely *is* the player.
|
|
|
|
### Visible body — first-person arms (owner) + third-person body (remotes)
|
|
|
|
Every player is drawn twice, split by viewpoint (see §1):
|
|
|
|
- **First-person arms** — the owner's own hands/tools, seen *only* by the owner. The arms mesh
|
|
goes in `PlayerNetworkController.ownerOnlyObjects` (hidden on remotes); it is **never**
|
|
networked because no one else renders it.
|
|
- **Third-person body** — the full character everyone else sees. It stays active on *all*
|
|
clients so its `Animator` keeps running. `PlayerBodyVisibility` (a `NetworkBehaviour` on the
|
|
player root) relayers the **owner's own** body onto a `LocalBody` layer that the owner's
|
|
first-person camera excludes from its culling mask — so the owner never sees his own body
|
|
clip the view, yet every remote still renders it. **Layers are local and never networked**,
|
|
so hiding your body from yourself does not hide it from anyone else.
|
|
|
|
**Animation is a bus consumer, replicated by `NetworkAnimator`.** `PlayerAnimatorDriver`
|
|
(`Player/Animation/`) drives an `Animator` purely from `PlayerEvents` (`VelocityChanged`,
|
|
`GroundedChanged`, `Jumped`, `AttackSwung`, `CrouchChanged`, …). The same component drives both
|
|
rigs from that one shared source, so arms and body stay in lockstep. Each instance is owner-only
|
|
local simulation → **add every `PlayerAnimatorDriver` to `ownerOnlyBehaviours`**. On the body,
|
|
a sibling FishNet **`NetworkAnimator`** replicates the owner's animation to remotes (whose driver
|
|
is disabled and whose body is animated by the replicated parameters instead). Need a new
|
|
animation cue? Add an event to `PlayerEvents` and have the driver subscribe — never poll another
|
|
system's internals.
|
|
|
|
### Networking a player system — the canonical pattern (mirror `PlayerInventory`/`PlayerStats`)
|
|
|
|
- Make it a `NetworkBehaviour`. **No destructive `Awake` singleton guard** (it would destroy
|
|
spawned remote players). Set the `Instance` only for the owner, in `OnStartNetwork`
|
|
(`if (base.Owner.IsLocalClient) Instance = this;`), clear it in `OnStopNetwork`.
|
|
- Owner simulates locally; replicate a lightweight snapshot via SyncVar (server writes,
|
|
pushed from the owner through a `[ServerRpc]`, throttled + immediate on key events).
|
|
- SyncVar `OnChange` updates remote copies' local state only — guard `if (base.IsOwner) return;`
|
|
and never touch `PlayerEvents` there.
|
|
|
|
### World state
|
|
|
|
World objects (pickables, harvestables) are **not** NetworkObjects. They use the
|
|
`WorldObject` + `WorldObjectRegistry` pattern: one networked registry per type per scene
|
|
owns a synced set of inactive ids + server-side authority (health, loot, drops) via RPC.
|
|
Add new harvestable/pickable-like world state here, not as per-object NetworkObjects.
|
|
`ItemData` cannot cross the wire — map items to ids through `ItemDatabase`
|
|
(**Tools ▸ Ashwild ▸ Rebuild Item Database** after adding items). `BuildRegistry` +
|
|
`BuildableDatabase` mirror this for placed structures (which also accumulate to high counts).
|
|
|
|
This "no NetworkObjects" rule targets **high-count** world state (thousands of scatter objects,
|
|
hundreds of builds) — making those NetworkObjects would flood the network. It is **not** absolute:
|
|
a **transient, low-count** object that moves and needs smooth replication — the build ghost, one
|
|
per player — is the case where a real **NetworkObject + client-authoritative NetworkTransform**
|
|
is the better tool, and `BuildRegistry` spawns it as such. Pick by count and lifetime, not dogma.
|
|
|
|
### Shared interactables (cooking stations, future chests/benches)
|
|
|
|
Anything two players can use at once needs server-authoritative state + RPCs — local-only
|
|
MonoBehaviour state will let players clobber each other. `CookingStation` is the reference:
|
|
the server owns the simulation (fuel burn + cooking timers), replicates a compact view
|
|
(`SyncList<SlotView>` + a `SyncVar<int>` log count), and mutates only through
|
|
`[ServerRpc(RequireOwnership = false)]` requests that refund the item on failure via
|
|
`PlayerInventory.GrantItemFromServer`. Mirror it for chests/benches.
|
|
|
|
### Not yet networked — do not rely on these in co-op
|
|
|
|
These still run locally and will desync between clients until migrated:
|
|
|
|
- **`GrassClearer`** — modifies the terrain detail layer locally, no RPC → others keep the grass.
|
|
- **`SeasonManager`** — the global `_Season` shader value is not synced → visual desync.
|
|
|
|
### Reference implementations
|
|
|
|
Copy these patterns rather than inventing new ones: `PlayerInventory.cs` and `PlayerStats.cs`
|
|
(networked player system), `HarvestableRegistry.cs` / `WorldObjectRegistry.cs` (world state),
|
|
`CookingStation.cs` (server-authoritative shared interactable), `PlayerNetworkController.cs`
|
|
(owner gating).
|
|
|
|
---
|
|
|
|
## 4. Folder map (`Assets/GAME/Script/`)
|
|
|
|
| Folder | Namespace | Responsibility |
|
|
| --- | --- | --- |
|
|
| `Player/Events/` | `Ashwild.Player` | `PlayerEvents` — the central bus. Start here to understand the game. |
|
|
| `Player/Input/` | `Ashwild.Player` | `PlayerInputRouter` — the **only** Input System receiver. |
|
|
| `Player/Locomotion/` | `Ashwild.Player` | Movement, ground/slope checks, jump, slide, crouch. |
|
|
| `Player/Camera/` | `Ashwild.Player` | First-person camera, look, head height. |
|
|
| `Player/Tools/` | `Ashwild.Player` | Tool equip/swing logic and tool holder. |
|
|
| `Player/` (root) | `Ashwild.Player` | `PlayerStats`, `PlayerInteractor`, `PlayerLifecycle`, `PlayerAudio`. |
|
|
| `Network/` | `Ashwild.Network` | FishNet layer — session host/join, player spawn, owner gating, `WorldObject`/registry world-state sync. |
|
|
| `Inventory/` | `Ashwild.Inventory` | Inventory model, slots, hotbar, pickables, inventory UI. |
|
|
| `Crafting/` | `Ashwild.Crafting` | Recipes, ingredients, crafting manager and its UI. |
|
|
| `Cooking/` | `Ashwild.Cooking` | Cooking stations (food + fuel → cooked results). |
|
|
| `Harvesting/` | `Ashwild.Harvesting` | Harvestables, tools, resource drops, scatter placement. |
|
|
| `Audio/` | `Ashwild.Audio` | Music manager/playlist, surface sound profiles. |
|
|
| `Environment/` | `Ashwild.Environment` | Season manager. |
|
|
| `GrassClearer/` | `Ashwild.GrassClearer` | Detail/grass removal around placed objects. |
|
|
| `Interaction/` | `Ashwild.Interaction` | `IInteractable` and interaction contracts. |
|
|
| `Settings/` | `Ashwild.Settings` | Settings manager, panels and sub-panels, brightness. |
|
|
| `UI/` | `Ashwild.UI` | HUD, panels, notifications, crosshair, death/splash/pause screens. |
|
|
| `Integrations/` | `Ashwild.Integrations` | Third-party integrations (e.g. Discord Rich Presence). |
|
|
| `Editor/` | `Ashwild.EditorTools` | Custom inspectors, property drawers, tooling (editor-only). |
|
|
|
|
### Namespaces — one per top-level system folder
|
|
|
|
Every script lives in a namespace **`Ashwild.<TopLevelFolder>`** — flat, one per
|
|
top-level system folder. Sub-folders do **not** get their own namespace: everything under
|
|
`Player/` (incl. `Player/Camera/`, `Player/Input/`, `Player/Tools/`, `Player/HeldItems/`)
|
|
is `Ashwild.Player`. This flatness is deliberate — it keeps cross-references short and
|
|
avoids a sub-namespace shadowing a Unity type (a namespace `Ashwild.Player.Camera` would
|
|
mask `UnityEngine.Camera` inside it).
|
|
|
|
Rules when adding or moving a script:
|
|
|
|
- New file → wrap it in the namespace of its top-level folder. Add `using Ashwild.X;` for
|
|
every other Ashwild namespace whose types it references.
|
|
- **`Editor/` is the one exception to the `Ashwild.<Folder>` rule** — its namespace is
|
|
**`Ashwild.EditorTools`**, *not* `Ashwild.Editor`, because a namespace segment named
|
|
`Editor` shadows the `UnityEditor.Editor` base type that every custom inspector derives
|
|
from (`CS0118`).
|
|
- **Never** put `using Ashwild.EditorTools;` in a runtime (non-`Editor/`) script. Editor
|
|
code compiles into the separate `Assembly-CSharp-Editor` assembly, which runtime code
|
|
cannot reference (`CS0234`). Editor scripts may freely `using` runtime namespaces, not
|
|
the reverse.
|
|
|
|
---
|
|
|
|
## 5. Code conventions — non-negotiable
|
|
|
|
### 5.1 XML summaries
|
|
Every **class, method, and non-obvious public member** gets a `/// <summary>` written in
|
|
**English**, describing intent (the *why*), not restating the signature.
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// Adds an item to the inventory, stacking into existing slots first.
|
|
/// Returns false (and logs) when the item is null or no room remains.
|
|
/// </summary>
|
|
public bool AddItem(ItemData item, int quantity = 1) { ... }
|
|
```
|
|
|
|
**Use the 3-line form** — opening `/// <summary>`, the text, and the closing
|
|
`/// </summary>` each on their own line; never collapse a summary onto one line.
|
|
|
|
Scope (this matches the real codebase — see `PlayerInventory`/`PlayerStats`):
|
|
- **Always:** every class, every method (incl. Unity lifecycle and event handlers), and any
|
|
public member whose intent isn't obvious from its name.
|
|
- **Not required:** trivial getters / `=>` passthroughs and `[SerializeField]` fields — group
|
|
the latter under `[Header("...")]` and clarify individual ones with `[Tooltip("...")]`
|
|
(or a summary) only when the name alone doesn't convey intent.
|
|
|
|
### 5.1b No in-body comments — explain in the summary above the method
|
|
**Never write explanatory comments *inside* a method body.** All rationale — the *why*, the
|
|
gotchas, the edge cases — goes in the `/// <summary>` above the method (extend it with as much
|
|
prose as needed). The body stays pure code. If a block feels like it needs an inline comment to
|
|
be understood, that's a signal to either name things better or extract it into its own
|
|
well-summarized method. The only inline text allowed in a body is a `// TODO:` / `// HACK:`
|
|
marker for tracked future work — not an explanation of what the current code does.
|
|
|
|
```csharp
|
|
// ❌ wrong — explanation lives inside the body
|
|
private void Step()
|
|
{
|
|
// Subtract instead of zeroing so the leftover carries into the next step.
|
|
distanceTravelled -= stepDistance;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fires a footstep and carries the leftover distance into the next step. Subtracting
|
|
/// (instead of zeroing) keeps the cadence even at sprint speed, where the per-frame
|
|
/// overshoot would otherwise randomly stretch steps and drift out of sync.
|
|
/// </summary>
|
|
private void Step()
|
|
{
|
|
distanceTravelled -= stepDistance;
|
|
}
|
|
```
|
|
|
|
### 5.2 Specific `#region`s — never one generic blob
|
|
Group members into **named, specific** regions reflecting their role. Do **not** dump
|
|
everything under a single `#region Methods`. Typical regions for a MonoBehaviour:
|
|
|
|
```csharp
|
|
#region Serialized Fields
|
|
#region State
|
|
#region Unity Lifecycle // Awake / OnEnable / OnDisable / Start / Update
|
|
#region Event Handlers // bus callbacks
|
|
#region Public API
|
|
#region Internal Helpers
|
|
#endregion
|
|
```
|
|
|
|
Pick regions that match what the script actually does (e.g. `#region Ground Check`,
|
|
`#region Surface Detection`, `#region Playback`). The goal: a reader scans the region
|
|
names and knows the script's shape instantly.
|
|
|
|
### 5.3 Event subscription lifecycle
|
|
Subscribe in `OnEnable`, unsubscribe in `OnDisable` — always paired, same order.
|
|
Never subscribe without a matching unsubscribe (leaks + double-fire after reload).
|
|
|
|
### 5.4 DOTween hygiene
|
|
Cache tweeners in fields and `Kill()` them in `OnDestroy` (and before restarting them).
|
|
A tween targeting a destroyed object throws — never leave one running on teardown.
|
|
|
|
### 5.5 Defensive logging instead of crashes
|
|
Guard nullable inputs and log a clear, prefixed error with the **context object** so
|
|
clicking the console message selects the culprit in the hierarchy:
|
|
|
|
```csharp
|
|
if (itemData == null)
|
|
{
|
|
Debug.LogError($"[Pickable] '{name}' has no ItemData assigned — cannot pick up.", this);
|
|
return;
|
|
}
|
|
```
|
|
|
|
Prefix logs with `[ClassName]`. Pass `this` as the second arg whenever possible.
|
|
|
|
### 5.6 Input naming — avoid the `On<Action>` trap
|
|
The `PlayerInput` component uses **Send Messages**, so Unity invokes `On<ActionName>`
|
|
methods by reflection on the player's GameObject and its components.
|
|
**Only `PlayerInputRouter` may declare `On<Action>` methods.** Any other component with a
|
|
method named like an input action (e.g. `OnHotbarScroll`) will be invoked by the Input
|
|
System and crash on a signature mismatch. Name bus handlers `Handle<Thing>` instead.
|
|
|
|
### 5.7 General practices
|
|
- `private` fields exposed to the inspector use `[SerializeField] private`, grouped under
|
|
`[Header("...")]`. No public fields just to show them in the inspector.
|
|
- One class per file; file name matches the type.
|
|
- Prefer `OnEnable/OnDisable` over `Start/OnDestroy` for subscription symmetry.
|
|
- No magic numbers in logic that designers tune — promote them to serialized fields.
|
|
- Don't add `FindObjectOfType` in hot paths (`Update`/`FixedUpdate`); cache references.
|
|
- Keep `Update`/`FixedUpdate` cheap; cache expensive lookups (see `PlayerAudio`'s surface
|
|
cache for the pattern).
|
|
|
|
---
|
|
|
|
## 6. Canonical script template
|
|
|
|
```csharp
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// One-sentence description of this component's single responsibility.
|
|
/// </summary>
|
|
[DisallowMultipleComponent]
|
|
public class ExampleSystem : MonoBehaviour
|
|
{
|
|
#region Serialized Fields
|
|
|
|
[Header("Tuning")]
|
|
[SerializeField] private float duration = 1f;
|
|
|
|
#endregion
|
|
|
|
#region State
|
|
|
|
private bool isActive;
|
|
|
|
#endregion
|
|
|
|
#region Unity Lifecycle
|
|
|
|
/// <summary>
|
|
/// Subscribes to the bus events this system reacts to.
|
|
/// </summary>
|
|
private void OnEnable()
|
|
{
|
|
PlayerEvents.Died += HandleDied;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unsubscribes — must mirror OnEnable exactly.
|
|
/// </summary>
|
|
private void OnDisable()
|
|
{
|
|
PlayerEvents.Died -= HandleDied;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Event Handlers
|
|
|
|
/// <summary>
|
|
/// Resets the system when the player dies.
|
|
/// </summary>
|
|
private void HandleDied() => isActive = false;
|
|
|
|
#endregion
|
|
|
|
#region Public API
|
|
|
|
/// <summary>
|
|
/// Starts the effect; ignored if already running.
|
|
/// </summary>
|
|
public void Activate()
|
|
{
|
|
if (isActive) return;
|
|
isActive = true;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Before you finish
|
|
|
|
- New cross-system communication → add an event to `PlayerEvents`, don't bypass the bus.
|
|
- Touched a `MonoBehaviour` that subscribes? Verify `OnEnable`/`OnDisable` stay paired.
|
|
- Touched a player system? Confirm it's owner-gated (in `ownerOnlyBehaviours`) and that a
|
|
remote copy never drives `PlayerEvents` or simulates itself (see §3).
|
|
- New networked prefab? Add a `NetworkObject` and register it in `DefaultPrefabObjects`.
|
|
- Added a tween? Verify it's killed on teardown.
|
|
- The project must compile with **zero errors**; treat new warnings as something to fix
|
|
or justify.
|
|
- Editor-only code stays under `Editor/`.
|