Race_fields/Assets/_Project/Docs/Architecture.md
2026-08-12 12:11:13 +03:00

23 KiB
Raw Permalink Blame History

Race Fields Architecture

Этот документ фиксирует текущую архитектуру проекта по фактическим сценам, prefab'ам и скриптам.

Главная цель: видеть не просто граф зависимостей, а слои проекта, ответственность каждого блока и места, где архитектура сейчас хрупкая.

Как Читать Диаграммы

Главная диаграмма читается сверху вниз:

  1. Entry / Scene Layer - откуда стартует игра.
  2. Scene Bootstrap Layer - кто собирает игровую сцену.
  3. Runtime Entity Layer - реальные игровые объекты.
  4. Subsystem Layer - системы, которые обслуживают player/enemies.
  5. 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.prefab
  • Buggy_1.prefab
  • Buggy_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:

  • Arena
  • SurvivalRace

Но нет 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

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, а не случайно искать друг друга по сцене.