23 KiB
Race Fields Architecture
Этот документ фиксирует текущую архитектуру проекта по фактическим сценам, prefab'ам и скриптам.
Главная цель: видеть не просто граф зависимостей, а слои проекта, ответственность каждого блока и места, где архитектура сейчас хрупкая.
Как Читать Диаграммы
Главная диаграмма читается сверху вниз:
Entry / Scene Layer- откуда стартует игра.Scene Bootstrap Layer- кто собирает игровую сцену.Runtime Entity Layer- реальные игровые объекты.Subsystem Layer- системы, которые обслуживают player/enemies.Asset / Data Layer- prefab'ы, sprites, render textures, static state.
Связи на главной диаграмме показывают только важные runtime-зависимости. Детальные связи вынесены ниже в отдельные диаграммы.
Current Architecture Overview
flowchart TB
subgraph L1["ENTRY / SCENE LAYER"]
SelectionScene["Selection Scene"]
DemoScene["Demo Scene"]
end
subgraph L2["SCENE BOOTSTRAP LAYER"]
SelectionMenu["SelectionMenuController\nвыбор машины / режима / карты"]
SelectedVehicleSpawner["SelectedVehicleSpawner\nсоздает player и биндует сцену"]
end
subgraph L3["RUNTIME ENTITY LAYER"]
Player["Player Vehicle Instance\nBuggy prefab clone"]
Harvester["Harvester Enemy\nHarvester prefab instance"]
Terrain["Terrain\nTerrainCollider"]
end
subgraph L4["PLAYER SUBSYSTEMS"]
VehicleControl["ArcadeBuggyController\ninput + physics + wheels + sound + effects"]
WheelRig["WheelColliders / WheelVisuals"]
DamageCore["VehicleDamageController\nмодули здоровья"]
ImpactDamage["VehicleImpactDamageReceiver\nterrain / landing / collision damage"]
DamageHitboxes["DamageHitbox\nзоны урона"]
end
subgraph L5["SCENE SUBSYSTEMS"]
FollowCamera["BuggyFollowCamera"]
RearViewCamera["RearViewMirrorCamera"]
DamageHUD["VehicleDamageHud"]
HarvesterAI["HarvesterChaseController"]
BladeDamage["BladeDamageSource"]
BladeVisual["HarvesterBladeSpinner"]
end
subgraph L6["ASSET / DATA LAYER"]
GameSelectionState["GameSelectionState static\nвыбор между сценами"]
VehiclePrefabs["Player Vehicle Prefabs\nBuggy / Buggy_1 / Buggy_Player"]
HarvesterPrefab["HarvesterEnemy Prefab"]
DamageUISprites["MDP_Buggy_1.psb\nHUD base + overlay sprites"]
RearViewRT["RearViewMirror RenderTexture"]
end
SelectionScene --> SelectionMenu
SelectionMenu --> GameSelectionState
SelectionMenu --> DemoScene
DemoScene --> SelectedVehicleSpawner
DemoScene --> Terrain
DemoScene --> Harvester
SelectedVehicleSpawner --> GameSelectionState
SelectedVehicleSpawner --> VehiclePrefabs
SelectedVehicleSpawner --> Player
SelectedVehicleSpawner --> FollowCamera
SelectedVehicleSpawner --> RearViewCamera
SelectedVehicleSpawner --> DamageHUD
VehiclePrefabs --> Player
Player --> VehicleControl
Player --> WheelRig
Player --> DamageCore
Player --> ImpactDamage
Player --> DamageHitboxes
VehicleControl --> WheelRig
ImpactDamage --> DamageCore
ImpactDamage --> WheelRig
DamageHitboxes --> DamageCore
DamageCore --> DamageHUD
DamageUISprites --> DamageHUD
HarvesterPrefab --> Harvester
Harvester --> HarvesterAI
Harvester --> BladeVisual
Harvester --> BladeDamage
HarvesterAI --> Player
BladeDamage --> DamageHitboxes
FollowCamera --> Player
RearViewCamera --> Player
RearViewCamera --> RearViewRT
Main Runtime Flow
Это основной путь от выбора машины до gameplay.
sequenceDiagram
participant PlayerInput as Player Input
participant SelectionMenu as SelectionMenuController
participant SelectionState as GameSelectionState
participant UnityScene as Unity SceneManager
participant DemoScene as Demo Scene
participant Spawner as SelectedVehicleSpawner
participant Vehicle as Player Vehicle
participant Camera as Cameras
participant HUD as VehicleDamageHud
participant Enemy as Harvester
PlayerInput->>SelectionMenu: left/right/space
SelectionMenu->>SelectionMenu: choose vehicle, mode, map
SelectionMenu->>SelectionState: SetSelection(vehiclePrefab, mode, sceneName)
SelectionMenu->>UnityScene: LoadScene(sceneName)
UnityScene->>DemoScene: load gameplay scene
DemoScene->>Spawner: Start()
Spawner->>SelectionState: read selected prefab
Spawner->>Vehicle: Instantiate(prefab at spawn point)
Spawner->>Camera: SetTarget(vehicle)
Spawner->>HUD: Bind(vehicle.VehicleDamageController)
Enemy->>Vehicle: auto find / chase target
Проблема этого flow:
GameSelectionState- static.SelectedVehicleSpawnerуже не просто spawner, а bootstrap сцены.- Камеры, HUD и враг частично умеют искать player сами, частично получают его от spawner.
Scene Composition
Selection Scene
flowchart LR
subgraph SelectionScene["Selection Scene"]
MainCamera["Main Camera"]
Light["Directional Light"]
PreviewRoot["VehiclePreviewRoot"]
PreviewFloor["Preview Floor"]
Canvas["Selection Canvas"]
EventSystem["EventSystem\nInputSystemUIInputModule"]
Menu["SelectionMenuController"]
end
Menu --> PreviewRoot
Menu --> Canvas
Menu -->|"spawns preview prefab"| PreviewVehicle["Runtime Preview Vehicle"]
Menu -->|"writes selected data"| GameSelectionState["GameSelectionState"]
Menu -->|"loads selected map"| SceneManager["SceneManager.LoadScene"]
Что важно:
- Preview использует настоящий gameplay prefab.
- После spawn preview отключаются gameplay-компоненты.
- Это быстро, но preview и gameplay связаны слишком плотно.
Demo Scene
flowchart LR
subgraph DemoScene["Demo Scene"]
Terrain["Terrain + TerrainCollider"]
ExistingBuggy["Buggy_1 scene instance\nтестовый объект"]
SpawnPoint["Selected Vehicle Spawn Point"]
Spawner["SelectedVehicleSpawner"]
FollowCamera["Buggy_ThirdPersonCamera"]
RearCamera["RearViewMirrorCamera"]
HudCanvas["Gameplay HUD Canvas"]
Harvester["Harvester"]
MachineGun["MachineGun"]
end
Spawner --> SpawnPoint
Spawner -->|"destroys existing ArcadeBuggyController if enabled"| ExistingBuggy
Spawner -->|"spawns selected prefab"| SpawnedPlayer["Buggy(Clone)"]
Spawner --> FollowCamera
Spawner --> RearCamera
Spawner --> HudCanvas
Harvester -->|"chases"| SpawnedPlayer
Что важно:
- В сцене есть и тестовая машина
Buggy_1, и spawner. - Spawner может удалить старую машину и создать выбранную.
- Для финальной архитектуры лучше оставить один путь создания player.
Vehicle Prefab Composition
Все три player-prefab'а сейчас должны иметь одну и ту же структуру:
Buggy.prefabBuggy_1.prefabBuggy_Player.prefab
flowchart TB
subgraph VehicleRoot["PLAYER VEHICLE PREFAB ROOT"]
Transform["Transform"]
Rigidbody["Rigidbody"]
BoxCollider["BoxCollider"]
end
subgraph Control["CONTROL / PHYSICS"]
ArcadeBuggyController["ArcadeBuggyController"]
BuggyWheelSetup["BuggyWheelSetup"]
WheelColliders["WheelCollider_FL / FR / RL / RR"]
WheelVisuals["Wheel visuals"]
VehicleAudio["EngineSound / TireScreechSound"]
VehicleVFX["Smoke / Skid Trails"]
end
subgraph Damage["DAMAGE"]
VehicleDamageController["VehicleDamageController"]
VehicleImpactDamageReceiver["VehicleImpactDamageReceiver"]
DamageModules["DamageModules"]
DamageHitboxes["DamageHitboxes"]
end
subgraph Modules["MODULES"]
Body["Body Module"]
Engine["Engine Module"]
FL["FrontLeftWheel Module"]
FR["FrontRightWheel Module"]
RL["RearLeftWheel Module"]
RR["RearRightWheel Module"]
end
subgraph Hitboxes["HITBOXES"]
BodyBox["Body Hitbox"]
EngineBox["Engine Hitbox"]
FLBox["FrontLeftWheel Hitbox"]
FRBox["FrontRightWheel Hitbox"]
RLBox["RearLeftWheel Hitbox"]
RRBox["RearRightWheel Hitbox"]
end
VehicleRoot --> Control
VehicleRoot --> Damage
ArcadeBuggyController --> WheelColliders
ArcadeBuggyController --> WheelVisuals
ArcadeBuggyController --> VehicleAudio
ArcadeBuggyController --> VehicleVFX
BuggyWheelSetup --> WheelColliders
VehicleDamageController --> DamageModules
DamageModules --> Modules
DamageHitboxes --> Hitboxes
DamageHitboxes --> VehicleDamageController
VehicleImpactDamageReceiver --> VehicleDamageController
VehicleImpactDamageReceiver --> ArcadeBuggyController
FL -->|"destroyed"| ArcadeBuggyController
FR -->|"destroyed"| ArcadeBuggyController
RL -->|"destroyed"| ArcadeBuggyController
RR -->|"destroyed"| ArcadeBuggyController
Ключевая проблема:
Damage зависит от ArcadeBuggyController. Это работает только пока все машины устроены как багги. Для нормальной архитектуры нужен wheel adapter/interface.
Damage Architecture
flowchart TB
subgraph DamageInput["DAMAGE INPUT SOURCES"]
BladeDamageSource["BladeDamageSource\ntrigger damage"]
ImpactReceiver["VehicleImpactDamageReceiver\nterrain / landing / collision"]
FutureWeapons["Future Weapons\nbullets / mines / rockets"]
end
subgraph Routing["DAMAGE ROUTING"]
DamageHitbox["DamageHitbox\nexternal hit -> module type"]
DamageHit["DamageHit\namount / type / point / direction / source"]
end
subgraph State["DAMAGE STATE"]
DamageController["VehicleDamageController"]
ModuleBase["DamageableVehicleModule"]
WheelModule["VehicleWheelDamageModule"]
end
subgraph Output["DAMAGE OUTPUT"]
Hud["VehicleDamageHud"]
WheelDetach["Wheel detach\nvisual + collider + rigidbody"]
ControllerEffect["ArcadeBuggyController.SetWheelEnabled(false)"]
end
BladeDamageSource --> DamageHitbox
FutureWeapons --> DamageHitbox
DamageHitbox --> DamageHit
ImpactReceiver --> DamageHit
DamageHit --> DamageController
DamageController --> ModuleBase
ModuleBase --> WheelModule
DamageController --> Hud
WheelModule --> WheelDetach
WheelModule --> ControllerEffect
Damage Rules Сейчас
External damage:
BladeDamageSourceищетDamageHitbox.DamageHitboxзнаетVehicleModuleType.VehicleDamageControllerпередает damage в конкретный module.
Terrain / impact damage:
VehicleImpactDamageReceiverвисит на root машины.- Считает collision damage через
OnCollisionEnter. - Считает landing damage по каждому
WheelCollider. - Отправляет damage напрямую в
VehicleDamageController.
HUD:
VehicleDamageHudслушаетVehicleDamageController.ModuleStateChanged.- Красит overlay:
- healthy - transparent;
- damaged - red;
- destroyed/detached - black.
Enemy Architecture
flowchart TB
subgraph HarvesterPrefab["HARVESTER PREFAB"]
HarvesterRoot["Root\nRigidbody + BoxCollider"]
Chase["HarvesterChaseController"]
HarvesterWheels["WheelColliders + WheelVisuals"]
Blades["Blades"]
Spinner["HarvesterBladeSpinner"]
BladeDamage["BladeDamageSource"]
end
subgraph Target["TARGET"]
PlayerVehicle["ArcadeBuggyController player"]
PlayerHitboxes["Player DamageHitboxes"]
end
Chase --> HarvesterWheels
Chase -->|"FindFirstObjectByType / target"| PlayerVehicle
Blades --> Spinner
Blades --> BladeDamage
BladeDamage --> PlayerHitboxes
Проблема:
- Harvester сам ищет player.
- Нет
EnemyDirector. - Нет единого
TargetProvider. - При нескольких машинах враг может выбрать не ту.
Camera And HUD Binding
flowchart TB
Spawner["SelectedVehicleSpawner"]
Player["Spawned Player Vehicle"]
subgraph CameraSystem["CAMERA SYSTEM"]
FollowCamera["BuggyFollowCamera"]
RearViewCamera["RearViewMirrorCamera"]
RearViewRT["RearViewMirror RenderTexture"]
RearViewImage["Rear View UI Image"]
end
subgraph HUDSystem["HUD SYSTEM"]
DamageHud["VehicleDamageHud"]
HudBase["DamageHUD_BaseLayers"]
HudOverlays["DamageHUD_DamageOverlays"]
end
Spawner --> Player
Spawner -->|"SetTarget"| FollowCamera
Spawner -->|"SetTarget"| RearViewCamera
RearViewCamera --> RearViewRT
RearViewRT --> RearViewImage
Player -->|"VehicleDamageController"| DamageHud
DamageHud --> HudBase
DamageHud --> HudOverlays
Проблема:
- Камеры и HUD умеют искать player сами.
- Spawner тоже задает им player.
- Нужно выбрать один путь. Лучше: bind только через bootstrap.
Current Dependency Table
| System | Owns | Depends On | Problem |
|---|---|---|---|
SelectionMenuController |
UI выбора, preview, load scene | GameSelectionState, real vehicle prefab |
Preview завязан на gameplay prefab |
GameSelectionState |
выбранная машина/режим/карта | static global state | нет lifecycle, нет reset/session |
SelectedVehicleSpawner |
spawn player, bind camera/HUD | GameSelectionState, cameras, HUD, damage |
делает больше, чем spawner |
ArcadeBuggyController |
input, drive, wheels, audio, VFX | wheel objects, audio, particles | слишком широкий класс |
VehicleDamageController |
health modules | modules array | ок, но нужен profile/config |
VehicleImpactDamageReceiver |
terrain/collision damage | ArcadeBuggyController, VehicleDamageController |
damage зависит от конкретной машины |
VehicleWheelDamageModule |
wheel destruction | ArcadeBuggyController |
damage знает controller |
VehicleDamageHud |
module damage UI | VehicleDamageController, scene UI sprites |
mapping зашит в scene |
HarvesterChaseController |
enemy movement | player search, wheel colliders | нет EnemyDirector |
BladeDamageSource |
blade damage | DamageHitbox |
нормальная связь для source damage |
BuggyFollowCamera |
follow camera | player search or external target | два способа binding |
RearViewMirrorCamera |
rear view | player search or external target | два способа binding |
Architecture Problems To Fix Before Scaling
1. Нужен GameBootstrap
Сейчас gameplay-сцена собирается через SelectedVehicleSpawner, но по факту он уже делает bootstrap.
Нужно:
GameBootstrap
-> read session request
-> create player
-> bind cameras
-> bind HUD
-> start enemies
-> start game mode
2. Нужен GameSession
Сейчас состояние между сценами хранится в static GameSelectionState.
Нужно:
GameSession
selected vehicle
selected map
selected game mode
player instance
active enemies
session state
3. Нужен VehicleDefinition
Сейчас selection хранит GameObject prefab.
Лучше:
VehicleDefinition
display name
gameplay prefab
preview settings
damage profile
HUD damage layout
audio profile
4. Нужен Wheel Rig Adapter
Damage не должен знать ArcadeBuggyController.
Нужно:
public interface IVehicleWheelRig
{
WheelCollider GetWheelCollider(WheelSlot slot);
Transform GetWheelVisual(WheelSlot slot);
void SetWheelEnabled(WheelSlot slot, bool enabled);
bool IsWheelGrounded(WheelSlot slot);
}
Тогда:
VehicleImpactDamageReceiverзависит отIVehicleWheelRig;VehicleWheelDamageModuleзависит отIVehicleWheelRig;- конкретная багги просто предоставляет реализацию.
5. Нужен Input Layer
Сейчас ArcadeBuggyController сам читает keyboard.
Нужно:
IPlayerInputSource
Throttle
Steering
Handbrake
KeyboardInputSource
GamepadInputSource
ArcadeCabinetInputSource
AIInputSource
6. Нужны Game Modes
Сейчас есть enum:
ArenaSurvivalRace
Но нет runtime-режимов.
Нужно:
IGameMode
ArenaMode
SurvivalRaceMode
Game mode должен отвечать за:
- правила старта;
- enemy spawn rules;
- win/lose;
- scoring;
- timer;
- map-specific logic.
Target Architecture
flowchart TB
subgraph Entry["ENTRY"]
Selection["Selection Scene"]
SessionRequest["GameSessionRequest"]
end
subgraph Data["DATA / DEFINITIONS"]
VehicleDefinition["VehicleDefinition"]
MapDefinition["MapDefinition"]
GameModeDefinition["GameModeDefinition"]
DamageProfile["VehicleDamageProfile"]
HudDefinition["VehicleHudDefinition"]
end
subgraph Bootstrap["GAMEPLAY BOOTSTRAP"]
GameBootstrap["GameBootstrap"]
GameSession["GameSession"]
VehicleFactory["VehicleFactory"]
CameraBinder["CameraRigBinder"]
HudBinder["HudBinder"]
EnemyDirector["EnemyDirector"]
GameModeRunner["GameModeRunner"]
end
subgraph Runtime["RUNTIME OBJECTS"]
Player["Player Vehicle"]
Enemies["Enemies"]
World["Map / Terrain / Hazards"]
end
subgraph VehicleSystems["VEHICLE SYSTEMS"]
InputSource["IPlayerInputSource"]
VehicleController["IVehicleController"]
WheelRig["IVehicleWheelRig"]
DamageController["VehicleDamageController"]
ImpactReceiver["VehicleImpactDamageReceiver"]
VehicleAudio["VehicleAudioController"]
VehicleVFX["VehicleEffectsController"]
end
Selection --> SessionRequest
SessionRequest --> GameBootstrap
GameBootstrap --> GameSession
GameBootstrap --> VehicleFactory
GameBootstrap --> CameraBinder
GameBootstrap --> HudBinder
GameBootstrap --> EnemyDirector
GameBootstrap --> GameModeRunner
GameSession --> VehicleDefinition
GameSession --> MapDefinition
GameSession --> GameModeDefinition
VehicleDefinition --> DamageProfile
VehicleDefinition --> HudDefinition
VehicleFactory --> Player
Player --> InputSource
Player --> VehicleController
Player --> WheelRig
Player --> DamageController
Player --> ImpactReceiver
Player --> VehicleAudio
Player --> VehicleVFX
EnemyDirector --> Enemies
GameModeRunner --> Enemies
GameModeRunner --> World
CameraBinder --> Player
HudBinder --> Player
HudBinder --> HudDefinition
Recommended Refactor Plan
Step 1 - Rename / Split Spawner
Current:
SelectedVehicleSpawner
Target:
GameBootstrap
VehicleFactory
CameraRigBinder
HudBinder
Minimum first change:
- оставить spawn в
SelectedVehicleSpawner; - вынести camera/HUD binding в отдельные компоненты;
- или переименовать
SelectedVehicleSpawnerвGameBootstrapи явно признать его ответственность.
Step 2 - Replace Static Selection With Session Request
Current:
GameSelectionState static
Target:
GameSessionRequest
GameSession
Step 3 - Introduce VehicleDefinition
Current:
SelectionMenuController.VehicleOption -> GameObject prefab
Target:
SelectionMenuController.VehicleOption -> VehicleDefinition
VehicleDefinition -> gameplay prefab / preview / damage / HUD / audio
Step 4 - Decouple Damage From ArcadeBuggyController
Current:
VehicleImpactDamageReceiver -> ArcadeBuggyController
VehicleWheelDamageModule -> ArcadeBuggyController
Target:
VehicleImpactDamageReceiver -> IVehicleWheelRig
VehicleWheelDamageModule -> IVehicleWheelRig
ArcadeBuggyController or BuggyWheelRig -> IVehicleWheelRig
Step 5 - Split ArcadeBuggyController
Current:
ArcadeBuggyController
input
physics
wheel visuals
audio
VFX
wheel API for damage
Target:
ArcadeBuggyController / VehicleMotor
VehicleInputSource
VehicleWheelVisuals
VehicleAudioController
VehicleEffectsController
BuggyWheelRig
What Is Safe To Add Now
Можно добавлять без сильного риска:
- tuning параметров машины;
- tuning damage thresholds;
- улучшение HUD;
- настройка harvester movement;
- новые визуальные эффекты;
- простые hazards, если они наносят урон через
DamageHitbox.
What Should Wait
Лучше не добавлять до bootstrap/session/refactor:
- оружие;
- несколько видов enemies;
- power-ups;
- полноценные game modes;
- scoring/results;
- несколько player vehicles;
- сохранение прогресса;
- arcade cabinet input;
- VR-specific gameplay.
Причина: все это сейчас будет цепляться к static state, scene object search и прямым ссылкам.
Summary
Текущая архитектура рабочая для prototype, но главный риск уже виден:
Нет единого runtime-контекста.
Сейчас обязанности размазаны между:
GameSelectionState;SelectedVehicleSpawner;- камерами, которые сами ищут player;
- harvester, который сам ищет player;
- HUD, который сам ищет damage controller.
Нормальная точка сборки должна выглядеть так:
Selection Scene
-> GameSessionRequest
-> Demo Scene
-> GameBootstrap
-> GameSession
-> Player / Cameras / HUD / Enemies / GameMode
После этого новые системы будут подключаться через session/bootstrap, а не случайно искать друг друга по сцене.