` в DOM.
**Component** — поведение, навешиваемое на GameObject. Mesh, Collider, ваш скрипт. Аналог: набор CSS
+ JS обработчиков, прикреплённых к элементу.
**Prefab** — переиспользуемый шаблон GameObject в виде ассета. Аналог: React-компонент, который
рендерится в много мест.
**Prefab Variant** — наследник prefab с локальными override'ами. Аналог: React-компонент, который
оборачивает другой через композицию.
**Transform** — позиция, поворот, масштаб + ссылка на родителя. RectTransform — расширенный
вариант для UI с anchors/pivot.
## Скрипты
**MonoBehaviour** — базовый класс ваших скриптов. Даёт Unity точки входа (`Awake`, `Start`,
`Update`, ...).
**ScriptableObject** — data-only ассет, не привязанный к GameObject. Аналог: JSON-конфиг,
типизированно встроенный в ассеты.
**Coroutine** — `IEnumerator` с `yield return`, дёргается каждый кадр движком. Не поток, всё на
main thread.
**Awaitable** — современная альтернатива в Unity 6: `async`/`await` без `Task` overhead.
## Жизненный цикл
**Awake / OnEnable / Start** — инициализация (вызывается в этом порядке).
**Update** — каждый кадр (variable timestep).
**FixedUpdate** — каждые `Time.fixedDeltaTime` секунд (фиксированно), для физики.
**LateUpdate** — после всех Update, для следящей камеры.
**OnDisable / OnDestroy** — очистка.
## Физика
**Rigidbody** — компонент, делающий GameObject участником физики. Без него только Static Collider.
**Collider** — форма для столкновений (Box, Sphere, Capsule, Mesh).
**Trigger** — Collider с `isTrigger=true`. Не блокирует, генерирует `OnTrigger*`.
**Layer** — числовой слой (0..31). Управляет, кто с кем сталкивается через Layer Collision Matrix.
**Raycast** — луч из точки в направлении, ищет пересечение с коллайдерами.
## Рендеринг
**Render Pipeline** — стратегия рендеринга. Built-in / URP / HDRP.
**Mesh** — геометрия (вершины, нормали, UV).
**Shader** — программа на GPU. HLSL или Shader Graph.
**Material** — экземпляр шейдера с конкретными параметрами.
**Draw Call** — один вызов отрисовки геометрии. Их количество — важный показатель производительности.
**Batching** — объединение нескольких объектов в один draw call. Static / Dynamic / SRP Batcher / GPU Instancing.
**Lightmap** — текстура с запечённым освещением для статичной геометрии.
**Light Probe** — точка с запечённым освещением для динамических объектов.
**Adaptive Probe Volumes (APV)** — современная замена Light Probes в URP/HDRP 17+. Автоматическая сетка проб с переменной плотностью.
**Volume** — компонент в URP/HDRP, привязывающий Volume Profile для постпроцесса и settings.
**Render Graph** — декларативный API URP 17 (Unity 6) для описания render passes и их зависимостей. Заменил `ScriptableRenderPass.Execute`.
**GPU Resident Drawer** — Unity 6 механизм автоматического батчинга тысяч повторяющихся мешей через BatchRendererGroup.
**SRP Batcher** — батчинг для URP/HDRP объектов с совместимыми шейдерами. Default-механизм.
## Анимация
**Animation Clip** — запись изменений свойств по времени (`.anim`).
**Animator Controller** — state machine с переходами между клипами.
**Blend Tree** — узел внутри состояния Animator, плавно смешивает клипы по параметру.
**Mecanim** — общее название анимационной системы Unity (Animator + Avatar + Retargeting).
**Root Motion** — использование смещения корневой кости как реального движения GameObject.
## Аудио
**AudioListener** — "уши" в сцене. Один на сцену.
**AudioSource** — источник звука, прикреплённый к GameObject.
**AudioMixer** — ассет с группами каналов и эффектами (low-pass, reverb).
**Spatial Blend** — слайдер 2D ↔ 3D на AudioSource.
## UI
**Canvas** — корневой объект для uGUI.
**RectTransform** — Transform для UI, с anchors и pivot.
**EventSystem** — маршрутизатор UI-событий (один на сцену).
**uGUI** — старая UI-система через GameObject.
**UI Toolkit** — новая UI-система через UXML + USS.
**TextMeshPro (TMP)** — SDF-текст. Стандарт для нового UI вместо Legacy UI.Text.
## Сборка
**Mono** — JIT-компиляция C# на устройстве.
**IL2CPP** — AOT-компиляция C# → C++ → нативный бинарь. Обязательна для iOS, WebGL.
**Burst** — компилятор Unity для специальных C# job'ов с большой производительностью.
**Addressables** — современная система загрузки ассетов (вместо `Resources.Load`).
**Build Profile** — конфигурация платформы и сцен для билда.
**CoreCLR** — третий runtime для C# в Unity, в Tech Stream 6.4+. Преемник Mono/IL2CPP для современного .NET.
**WebGPU backend** — публично доступный с Unity 6.1 рендер-таргет для современных браузеров. Преемник WebGL2.
## AI и поведение
**NavMesh** — запечённая упрощённая геометрия для поиска пути агентами.
**NavMeshAgent** — компонент, превращающий GameObject в "пешехода" по NavMesh.
**Unity Behavior** — официальная (с Unity 6.1) graph-based behavior trees система для AI. Аналог Behavior Designer.
**Sentis** — встроенный ML inference в Unity. Заменил Barracuda.
## Мультиплеер
**NGO (Netcode for GameObjects)** — официальный мультиплеер-пакет. Версия 2.x в Unity 6.
**NetworkObject** — сетевая идентичность узла в сцене.
**NetworkVariable** — синхронизированное поле от authority к остальным.
**[Rpc] / SendTo** — унифицированный атрибут для удалённых вызовов в NGO 2.x.
**Distributed Authority** — модель в NGO 2.x: разные клиенты владеют разными узлами (через Unity Cloud Multiplayer Services).
## DOTS
**Entities (ECS)** — data-oriented стек Unity, production-ready с Unity 6.1.
**Burst** — high-performance компилятор для Job-кода.
**Job System** — параллельные C# задачи для CPU-bound работы.
Если хочется глубже — официальный Unity Manual и Scripting Reference (`docs.unity3d.com`)
обычно достаточны. Для версионно-чувствительных вещей убедитесь, что смотрите документацию на
свою версию (в адресе есть `/Manual/6000.0/`, например).
---
# Godot Encyclopedia
## [Godot] Зачем веб-разработчику Godot
URL: https://cadmus.page/godot/01-intro/01-zachem/
Section: Введение
Description: Open-source движок с другой философией — что это даёт разработчику из мира TS/React.
Если Unity ощущается как корпоративная IDE с подпиской (потому что так и есть), то Godot — это нечто
другое. **MIT-лицензия, 0% роялти, ~100 МБ редактор**, который запускается быстрее, чем VS Code.
Своя экосистема, свой язык, и для веб-разработчика — местами куда более интуитивная модель.
Эта энциклопедия параллельна Unity-энциклопедии: те же концепции, тот же подход "через веб-аналоги",
но движок другой. Можно читать обе, можно одну.
## Что Godot делает иначе
Главное архитектурное отличие от Unity: **в Godot одна сцена — это и есть префаб**. Любой объект,
сделанный в редакторе как сцена, можно инстанциировать в другую сцену, наследовать от него,
переопределять отдельные узлы. Никакого разрыва "Scene vs Prefab" нет.
React-компонент — это и страница, и виджет. `` может содержать ``, который
содержит ``. Одна и та же ментальная модель на всех уровнях.
В Unity Scene и Prefab — разные сущности с разной семантикой. В Godot и то и другое — это
PackedScene. Дерево узлов внутри файла `.tscn`, сериализованное в текстовый формат
(читается как JSON — diff'ится через git без боли).
## Что вас может удивить (приятно)
1. **Один скрипт на узел, а не сколько угодно компонентов.** Поведение собирается композицией
**дочерних узлов**, а не нагромождением компонентов. Это ближе к "цепочке HOC'ов" в React, чем
к Unity-стилю.
2. **Signals — встроенный pub-sub в каждом узле.** Объявляете `signal damaged(amount: int)`,
эмитите `damaged.emit(10)`, подписываетесь из любого места. Без UnityEvents и без своего
event-bus.
3. **GDScript — простой и быстрый**, синтаксис похож на Python. Опциональная статическая типизация
ускоряет код в 1.5–2 раза.
4. **`@export var hp: int = 100`** — это эквивалент `[SerializeField]` в Unity, но одной строкой и
с поддержкой подсказок типа в Inspector.
5. **`await signal_name`** — корутины и асинхронность встроены в язык как ключевое слово.
6. **Текстовые форматы сцен (.tscn) и ресурсов (.tres)** — diff в git осмыслен. Слияние веток без
ритуала "кто закоммитил .meta-файлы".
## Что вас может удивить (неприятно)
1. **3D-возможности уступают Unity HDRP / Unreal**: нет нативного аналога Nanite/Lumen, hi-end
рендеринг скромнее. Это видно на крупных проектах с фотореализмом.
2. **Консоли — только через платных партнёров** (W4 Games). Никакой "одной кнопки Build for Switch".
3. **C# доступен, но требует отдельной сборки движка ("Godot .NET edition") и не работает на вебе.**
Для веба — только GDScript.
4. **Меньше платных ассетов и плагинов**, чем в Unity Asset Store. Asset Library / Godot Asset
Store работает, но "тяжёлых" коммерческих решений уровня Cinemachine — мало.
5. **Меньше job market** — индустрия по-прежнему сильно на Unity и Unreal.
## Когда Godot — правильный выбор
- **2D-игры**: первоклассный 2D pipeline, не имитация через 3D-камеру. TileMaps с авто-тайлами,
pixel-perfect, parallax, lighting2D.
- **Малая команда / соло-разработка**: быстрый старт, открытый исходник, MIT.
- **Образование и прототипирование**: ниже порог входа, чем Unity.
- **Веб-игры с GDScript**: Godot экспортируется в HTML5/WASM прямо из коробки.
- **Open-source / free software проекты**: нет vendor lock-in.
- **Если для вас важна возможность форкать движок и фиксить баги самостоятельно**.
## Когда лучше остаться на Unity
- **AAA-уровень 3D-графики** — HDRP/Unreal всё ещё впереди.
- **Console publishing** — нужно через middleware, стоит денег и времени.
- **Большие команды с готовым tooling-стеком** для Unity — миграция дорогая.
- **Готовые SDK и интеграции** (mobile ads/mediation, real-time analytics, asset providers).
## Куда дальше
В следующем разделе — короткий обзор стека Godot: версии, языки (GDScript и C#), рендер-пайплайны
и сопутствующие инструменты. После этого — главная глава про 3D-разработку.
---
## [Godot] Стек Godot и его экосистема
URL: https://cadmus.page/godot/01-intro/02-stack/
Section: Введение
Description: Версии Godot 4.x, GDScript vs C#, рендер-пайплайны и ключевые пакеты.
## Версии Godot
Godot работает в ветке **4.x с 2023 года** (релиз 4.0 — март 2023). На май 2026:
- **Godot 4.6.3** — **текущий stable** (вышел 20 мая 2026). Минорная ветка 4.6 релизнута в январе
2026, патчи 4.6.1 (фев), 4.6.2 (апр), 4.6.3 (май) — стабилизация Jolt physics, фиксы редактора.
Главные новинки 4.6: **Jolt стал default-физикой для 3D**, IK вернулся как нодовая система
(TwoBoneIK3D, FABRIK3D, и ещё 5 солверов), новая editor theme (Modern), unified docking,
переписанный SSR, LibGodot (движок как embedded-библиотека).
- **Godot 4.7** — в beta с апреля 2026, stable ожидается летом 2026. HDR-output на десктопе/iOS,
AreaLight3D, VirtualJoystick для мобильных, ray-tracing improvements, wasm64 для веб-экспорта,
встроенный Asset Store в редакторе.
- **WebGPU backend** — в mainline 4.6 **отсутствует / остаётся experimental на уровне proposal**.
Существует **community-fork** [`godotwebgpu`](https://godotwebgpu.com/) от David Walter (база —
4.6.2, март 2026), включающий собственную WebGPU-имплементацию для Mobile renderer и compute
shaders. Это не Godot Foundation проект. Производственный mainline WebGPU ожидается в 4.7+ или
позже.
- **Godot 5** — будущее. Публичной даты нет; ломающий мажор традиционно "через годы".
- **Godot 3.6** — последний релиз ветки 3.x, в режиме LTS (только багфиксы). Используйте, только
если поддерживаете legacy-проект; для нового — 4.x.
**Shader Baker** (введён в **4.5**, не в 4.6) — отдельная фича для precompile shaders при экспорте,
снижает stutter при первом запуске сцены до 20× на Metal/D3D12. Действует прозрачно — настройка
включения находится в Project Settings.
Godot использует семвер. LTS — только предыдущий major (сейчас 3.x). Внутри 4.x минорные релизы
идут вперёд, "сидеть на 4.4 вечно" не получится — кроме case'ов, когда вы шипите и
замораживаете версию проекта.
## Языки
### GDScript — родной язык
Похож на Python синтаксически: отступы, `def`-style функции (но через `func`), динамическая
типизация. Главные особенности:
- **Опциональная статическая типизация**: `var hp: int = 100`, `func attack(target: Enemy) -> void`.
Type-check на parse-time, ускоряет код на 28–59% за счёт пропуска Variant-диспетчера.
- **Typed arrays / dictionaries**: `Array[Enemy]`, `Dictionary[String, int]` (typed dictionaries
появились в 4.4).
- **Аннотации через `@`**: `@export`, `@onready`, `@tool`, `@export_range`, `@export_enum`,
`@export_file`, `@export_node_path`.
- **Lambda-функции**: `var add = func(a, b): return a + b`.
- **`await` вместо `yield`** для асинхронности.
- **`class_name MyClass extends Node`** — глобальная регистрация класса (виден в редакторе и из
любого скрипта).
```gdscript
class_name Enemy extends Node3D
@export_range(1, 100) var max_hp: int = 50
@onready var sprite: Sprite3D = $Sprite3D
signal died(by: Node)
var hp: int
func _ready() -> void:
hp = max_hp
func take_damage(amount: int, attacker: Node) -> void:
hp -= amount
if hp <= 0:
died.emit(attacker)
queue_free()
```
### C# — для опытных .NET-разработчиков
Поддерживается, но с оговорками:
- Нужна **отдельная сборка редактора** ("Godot .NET edition"). Скачивается с сайта или через
Steam как отдельный SKU.
- **Требуется .NET 8** минимум (с Godot 4.4).
- **На веб-таргет не экспортируется** — давнее ограничение, в работе через NativeAOT/.NET 10.
- Поддерживает NativeAOT-экспорт для нативных платформ.
- Объявлена долгосрочная стратегия "вынести .NET в плагин", но сроков нет — *TODO verify на момент
чтения*.
```csharp
using Godot;
public partial class Enemy : Node3D
{
[Export(PropertyHint.Range, "1,100")]
public int MaxHp { get; set; } = 50;
[Signal]
public delegate void DiedEventHandler(Node by);
private int _hp;
public override void _Ready() {
_hp = MaxHp;
}
public void TakeDamage(int amount, Node attacker) {
_hp -= amount;
if (_hp <= 0) {
EmitSignal(SignalName.Died, attacker);
QueueFree();
}
}
}
```
### GDExtension — нативные плагины
Аналог Unity native plugins, но удобнее:
- **Динамическая библиотека** (`.dll`/`.so`/`.dylib`) + манифест `.gdextension`. Не требует
пересборки движка.
- Официальные C++ биндинги — **godot-cpp**.
- Зрелые сторонние биндинги: **godot-rust** (gdext) для Rust, godot-python — для Python.
- Классы из GDExtension в редакторе неотличимы от core-узлов.
Аналога Unity Bolt в Godot 4.x в core нет. VisualScript присутствовал в 3.x, в 4.0 удалён как
малоиспользуемый (вынесен в отдельный модуль без поддержки). Если нужна визуальная логика —
есть Visual Shader (для шейдеров), а для геймплея — community-плагины.
## Рендер-пайплайны
В отличие от Unity, где выбираете один из трёх SRP при создании проекта, в Godot **три рендерера
переключаются в Project Settings**, можно менять и в процессе (но материалы могут требовать
адаптации).
| Renderer | API | Когда выбирать |
|---|---|---|
| **Forward+** | Vulkan / Metal / D3D12 | Десктоп, консоли, hi-end. Clustered forward+, SDFGI, volumetric fog, SSR, SSAO. |
| **Mobile** | Vulkan / Metal / D3D12 | Мобильные и desktop VR. Single-pass forward, лимит источников света. |
| **Compatibility** | OpenGL ES 3 / WebGL 2 | Слабое железо и **единственный полностью рабочий путь для веба**. |
Compatibility в 4.3 объявлен feature-complete для нужд большинства проектов: lights, shadows,
GI (lightmaps), GPU particles работают (с ограничениями).
Если цель — браузер, выбирайте Compatibility renderer с самого старта. Forward+ и Mobile в
WebGL не работают в полном объёме; mainline WebGPU-бэкенд пока отсутствует (community-fork
godotwebgpu — отдельная история, см. секцию выше).
## Ключевые системы и узлы
Vocabulary Godot, который понадобится:
- **Scene tree** — главное дерево узлов в работающей игре.
- **Node** — атомарный объект, обычно с одним скриптом. У всех узлов одинаковые жизненные
колбэки (`_ready`, `_process`, ...).
- **PackedScene (.tscn)** — сериализованная сцена. Аналог Unity Prefab + Scene в одном файле.
- **Resource (.tres / .res)** — data-only ассет, рефкаунт. Аналог ScriptableObject.
- **Signal** — встроенный pub-sub, объявляется на любом узле.
- **Group** — лейбл для категории узлов. Заменяет Unity-теги в задачах "найти всех врагов".
- **NavigationServer3D**, **PhysicsServer3D**, **RenderingServer**, **AudioServer** —
низкоуровневые синглтоны, на которые опираются высокоуровневые узлы.
## Tooling и экосистема
- **Встроенный script editor** в редакторе Godot — приличный для GDScript: LSP, autocomplete,
multi-cursor, debugger с remote scene tree.
- **Внешние редакторы**:
- **VS Code / Cursor** через расширение `godot-vscode-plugin` (LSP + DAP-дебаг).
- **JetBrains Rider** — first-class для C#-проектов; для GDScript конфигурируется тем же LSP.
- **Asset Library** в редакторе (вкладка AssetLib) — старый, фактически deprecated.
- **Asset Library** (встроена в редактор 4.6 — вкладка AssetLib) — старая система, через которую
ставятся community-плагины (Phantom Camera, Dialogic, Terrain3D и др.). Остаётся актуальной в 4.6.
- **Godot Asset Store** (анонс 2025, развитие 2026) — новый внешний портал с аккаунтами, рейтингами,
планируемой поддержкой платных ассетов. **Интеграция в редактор намечена на 4.7.** На 4.6 это
отдельный сайт, не AssetLib-замена.
- **Version control** через `.gitignore` (Godot сам генерирует при включении в настройках).
Игнорируйте `.godot/`, `export/`, `build/`. Коммитьте `*.tscn`, `*.tres`, `*.gd`, `*.gdshader`,
`*.import`, `project.godot`.
Каждый ассет в Godot получает рядом `.import`-файл с настройками импорта (compression mode для
текстуры, scale для модели и т.д.). **Эти файлы важно коммитить** — иначе при клонировании
репозитория на другой машине настройки потеряются.
## Главные плагины из Asset Store
В дополнение к встроенному:
- **Phantom Camera** — Cinemachine-аналог для 2D и 3D: follow, look-at, host-system,
triggers. Если нужна сложная работа с камерой — это первый stop.
- **Dialogic** — система диалогов и квестов.
- **Terrain3D** — нативный terrain (Godot core не имеет встроенного terrain).
- **Beehave** или **LimboAI** — behavior trees / state machines для AI.
- **GUT** — unit-testing.
- **gd-plug** — менеджер плагинов через `plug.gd`.
В следующей главе — главный материал: всё про 3D-разработку в Godot.
---
## [Godot] Godot Editor, сцена и узлы
URL: https://cadmus.page/godot/02-3d/01-editor-stsena-uzly/
Section: 3D-разработка в Godot
Description: Scene tree, Node3D — фундамент всего в Godot.
## Editor: главные окна
Godot Editor — это IDE для контента. Главные доки (можно перетаскивать как в Unity):
- **3D Viewport** — окно сцены, в котором летаете и расставляете узлы. Переключатели: 2D / 3D /
Script / AssetLib.
- **Scene** (слева сверху) — дерево узлов текущей сцены. Аналог Unity Hierarchy.
- **FileSystem** (слева снизу) — файлы проекта. Аналог Unity Project.
- **Inspector** (справа) — свойства выбранного узла или ресурса.
- **Node** (справа сверху) — вкладка для просмотра/подключения **сигналов** этого узла. Это
отдельная штука относительно Unity.
- **Output / Debugger / Search** — снизу.
Привязки манипуляторов в 3D viewport совпадают с большинством 3D-софта: Q — select, W — move,
E — rotate, R — scale. Snap to grid — клавиша Y (тогглит).
Если вы открываете 4.6 впервые после старых версий, заметите два больших изменения:
- **Modern theme** — новая editor theme (более нейтральная палитра, меньше синего). В Editor Settings можно вернуть классическую.
- **Unified docking** — доки теперь перетаскиваются на любые стороны экрана, включая bottom panels. Можно собрать любую раскладку. Раньше доки были привязаны к фиксированным позициям.
## Scene — это `.tscn` файл
Сцена в Godot — текстовый файл `.tscn` с описанием дерева узлов. Открывается в любом редакторе,
дружит с git. Пример минимальной сцены:
```ini
[gd_scene load_steps=2 format=3 uid="uid://abc123"]
[ext_resource type="Script" path="res://player.gd" id="1"]
[node name="Player" type="CharacterBody3D"]
script = ExtResource("1")
[node name="Mesh" type="MeshInstance3D" parent="."]
[node name="Collision" type="CollisionShape3D" parent="."]
```
Сцены можно **инстанциировать** в другие сцены, **наследовать** их и переопределять отдельные
свойства — это аналог Unity Prefab Variants, но более гибкий.
React-страница `` рендерит ``, ``, ``. Каждое из них — это
компонент в своём файле; их можно переиспользовать.
Сцена `Level_01.tscn` подключает `Player.tscn`, `HUD.tscn`, `Enemy.tscn`. Каждый из них —
отдельный файл, который можно править отдельно. Один скрипт правит — все экземпляры
обновляются.
## Узел (Node) — главный примитив
В Godot нет GameObject. **Есть Node** — базовый класс, к которому каждый узел в сцене относится.
И его специализированные подклассы:
```
Object ← базовый для всего движка
├── RefCounted
│ └── Resource ← data-ассеты (.tres), рефкаунт
└── Node ← всё, что в дереве сцены
├── CanvasItem ← общий предок 2D-объектов и UI
│ ├── Node2D ← 2D-трансформ (position, rotation, scale)
│ └── Control ← UI (anchors, offsets, layout)
├── Node3D ← 3D-трансформ
└── ... десятки специализаций
```
Ключевые 3D-подклассы Node3D:
- **MeshInstance3D** — рисует mesh.
- **Camera3D** — камера.
- **DirectionalLight3D**, **OmniLight3D**, **SpotLight3D** — свет.
- **StaticBody3D**, **RigidBody3D**, **CharacterBody3D**, **AnimatableBody3D**, **Area3D** —
физика.
- **CollisionShape3D** — форма для коллизий.
- **AudioStreamPlayer3D** — звук.
В отличие от Unity, к узлу можно прикрепить **только один скрипт**. Это не ограничение, а
философия: композиция делается через **дочерние узлы**, не через "стек компонентов". Нужен
объект со здоровьем, инвентарём и стрельбой? Один скрипт `Player.gd` + дочерние узлы
`Inventory`, `Weapon`, `Health`, у каждого свой скрипт.
## Иерархия и трансформ
Узел Node3D имеет `transform: Transform3D` — структура из `basis` (3×3 матрица поворота+масштаба)
и `origin: Vector3` (позиция). Удобные ярлыки:
- `position: Vector3` — `transform.origin`.
- `rotation: Vector3` — углы Эйлера (в радианах!).
- `rotation_degrees: Vector3` — то же в градусах для удобства редактора.
- `scale: Vector3`.
- `global_position`, `global_transform` — в мировых координатах.
```gdscript
$Mesh.position = Vector3(0, 1.5, 0) # вверх на 1.5 м
$Mesh.rotation_degrees.y += 90.0 # повернуть на 90° вокруг Y
$Mesh.scale = Vector3.ONE * 2.0 # увеличить в 2 раза
```
`$Name` — это шорткат для `get_node("Name")` — поиск ребёнка по имени. `$Path/To/Child` —
относительный путь, как в файловой системе.
## Координатная система
В Godot — **правосторонняя** Y-up:
- **+X** — вправо
- **+Y** — вверх
- **+Z** — **на наблюдателя** (на нас) — а вперёд от наблюдателя смотрит **−Z**
Unity — left-handed, +Z forward. Godot — right-handed, −Z forward. То есть камера по умолчанию
смотрит "в минус Z". `transform.basis * Vector3.FORWARD` в Godot возвращает направление "вперёд"
(которое внутри `Vector3.FORWARD = Vector3(0, 0, -1)`).
Гравитация по умолчанию: `Vector3(0, -9.8, 0)`. Один Unit = 1 метр (по соглашению).
## Группы
Godot не использует Unity-теги. Вместо них — **группы** (groups):
```gdscript
# Добавить узел в группу (в редакторе или из кода)
enemy.add_to_group("enemies")
# Получить все узлы группы
var all_enemies = get_tree().get_nodes_in_group("enemies")
# Вызвать метод на всех
get_tree().call_group("enemies", "take_damage", 10)
```
Группы можно настраивать в редакторе во вкладке Node → Groups. Подходит для запросов
"найти всех NPC", "все checkpoints", "враги в активной зоне".
## Запуск сцены
Кнопки сверху справа в редакторе:
- **Play** (F5) — запустить главную сцену проекта.
- **Play Scene** (F6) — запустить текущую открытую сцену.
- **Play Custom Scene** (Ctrl+Shift+F5) — выбрать любую.
Главная сцена настраивается в Project → Project Settings → Application → Run → Main Scene.
В следующей главе — GDScript и жизненный цикл узла.
---
## [Godot] GDScript, lifecycle и сигналы
URL: https://cadmus.page/godot/02-3d/02-gdscript-lifecycle/
Section: 3D-разработка в Godot
Description: Как Godot вызывает ваш код — _ready, _process, _physics_process, signals, await.
## Скрипт на узле
GDScript-файл (`.gd`) обычно начинается с `extends`, указывая, от какого узла наследуется:
```gdscript
extends Node3D
# Опционально: глобальное имя класса (виден в редакторе и других скриптах)
class_name Enemy
# Экспорт в Inspector — аналог [SerializeField]
@export var max_hp: int = 100
@export_range(1.0, 10.0) var speed: float = 3.0
@export var target: Node3D # ссылка на другой узел
# @onready — присваивается в _ready, удобно для ссылок на детей
@onready var sprite: MeshInstance3D = $Sprite
@onready var anim: AnimationPlayer = $AnimationPlayer
var hp: int
func _ready() -> void:
hp = max_hp
print("Enemy ready: %s" % name)
func _process(delta: float) -> void:
# каждый кадр
rotation_degrees.y += 30.0 * delta
```
React-компонент: `useState` для полей, `useEffect(() => {}, [])` для инициализации, render-функция
каждый раз. `props` — это `@export` поля.
`[SerializeField] private float speed` ↔ `@export var speed`. `private Animator anim` +
`anim = GetComponent()` в Awake ↔ `@onready var anim: AnimationPlayer = $AnimationPlayer`
однострочный. Лаконичнее.
## Жизненный цикл узла
Главные методы, которые Godot вызывает сам:
1. **`_init()`** — конструктор объекта, ещё до того как узел добавлен в дерево.
2. **`_enter_tree()`** — узел добавлен в scene tree. **Родитель → дети** (top-down).
3. **`_ready()`** — все дети тоже готовы. **Дети → родитель** (bottom-up). Здесь делают
инициализацию, которая зависит от детей.
4. **`_input(event)`** / **`_unhandled_input(event)`** — каждое input-событие.
5. **`_physics_process(delta)`** — фиксированный тик. По умолчанию **60 Hz**
(Project Settings → Physics → Common → Physics Ticks Per Second).
6. **`_process(delta)`** — каждый кадр (variable timestep, как Unity Update).
7. **`_exit_tree()`** — узел удалён из дерева (включая при выходе из сцены).
8. **`_notification(what)`** — низкоуровневый колбэк для NOTIFICATION_* событий.
`_ready` срабатывает один раз, **когда узел впервые попадает в сцену**. Если узел уйдёт из
дерева и вернётся, `_ready` снова не вызовется. `_enter_tree` сработает повторно.
## _process vs _physics_process
Аналогично Unity:
| Метод | Частота | Что туда |
|---|---|---|
| `_physics_process(delta)` | Фиксированно (60 Hz по умолчанию) | Физика: `move_and_slide`, `apply_central_impulse` |
| `_process(delta)` | Каждый кадр | Анимация, UI-обновления, нефизическая логика |
`delta` — время с последнего вызова. В `_process` — переменное, в `_physics_process` —
константное (`1.0 / физических_тиков_в_секунду`).
```gdscript
extends CharacterBody3D
@export var speed: float = 5.0
func _physics_process(delta: float) -> void:
var input := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
velocity.x = input.x * speed
velocity.z = input.y * speed
if not is_on_floor():
velocity.y += get_gravity().y * delta # gravity из настроек проекта
move_and_slide()
```
## Сигналы — встроенный pub-sub
Сигнал объявляется в скрипте узла. Любой другой узел может подключиться.
```gdscript
# Объявление
signal damaged(amount: int, attacker: Node)
signal died
# Эмит (новый синтаксис в 4.x — properties-style)
damaged.emit(10, attacker)
died.emit()
# Подключение из другого скрипта
enemy.damaged.connect(_on_enemy_damaged)
enemy.died.connect(_on_enemy_died, CONNECT_ONE_SHOT) # один раз и отвязаться
func _on_enemy_damaged(amount: int, attacker: Node) -> void:
print("Enemy took ", amount, " from ", attacker.name)
```
В **Inspector → вкладка Node → Signals** можно подключать сигналы визуально, без кода.
Godot сам сгенерирует метод-обработчик.
`EventEmitter` или RxJS Subject. Один объект эмитит, многие подписаны.
Smesь UnityEvent (Inspector-bindings) и C# events. В Godot это одно — signal.
## await — корутины через сигналы
Вместо StartCoroutine в Godot есть `await`:
```gdscript
# Подождать кадр
await get_tree().process_frame
# Подождать секунду (через таймер дерева)
await get_tree().create_timer(1.0).timeout
# Подождать сигнал
await player.died
# Подождать окончания анимации
$AnimationPlayer.play("attack")
await $AnimationPlayer.animation_finished
print("Attack done!")
```
Любой `await` возвращает поток управления движку и продолжается, когда сигнал эмитится. Работает
на main thread, как Unity coroutines — параллельных потоков не создаёт.
## get_node, $-шорткат и unique names
Способы добраться до других узлов:
```gdscript
# Полные пути
var sprite = get_node("Sprite3D")
var sprite_alt = $Sprite3D # шорткат
var deep = $"Body/Head/Sprite3D"
var parent = get_parent()
var first_child = get_child(0)
# Unique names — % префикс
# В сцене узел отмечен "Access as Unique Name" → доступен из любого места сцены через %Name
var label = %ScoreLabel # эквивалент $"path/to/ScoreLabel" но независимо от расположения
```
Если вы переставите узел в дереве, обычный `$Path/To/Child` сломается. Unique names (`%Name`)
ищутся в пределах сцены и устойчивы к переименованию контейнеров. Используйте для часто
ссылочных узлов: главного игрока, HUD-элементов.
## Управление активностью
- `set_process(false)` — выключить `_process` (узел остаётся в сцене).
- `set_physics_process(false)` — выключить `_physics_process`.
- `set_process_input(false)` — выключить input-обработку.
- `visible = false` — скрыть (для CanvasItem/Node3D).
- `queue_free()` — отложенное удаление в конце кадра (безопаснее, чем `free()`).
```gdscript
# Уничтожить через секунду
await get_tree().create_timer(1.0).timeout
queue_free()
```
В следующей главе — ввод через Input Map.
---
## [Godot] Ввод — Input Map и action-based система
URL: https://cadmus.page/godot/02-3d/03-input/
Section: 3D-разработка в Godot
Description: Как читать клавиатуру, мышь, геймпад и тач через единую action-систему.
В Godot ввод — единая система: вы описываете **actions** в Project Settings и обращаетесь к ним
из любого скрипта. Нет двух конкурирующих API как в Unity (legacy Input vs Input System).
## Input Map
Project → Project Settings → Input Map. Создаёте action (например, `jump`), привязываете к нему
любое количество клавиш / кнопок геймпада / мышь / touch:
```
move_forward ← W, ↑, левый-стик-Y+
move_back ← S, ↓, левый-стик-Y−
move_left ← A, ←, левый-стик-X−
move_right ← D, →, левый-стик-X+
jump ← Space, A (gamepad)
fire ← левый клик, RT (gamepad)
crouch ← Ctrl, B (gamepad)
```
Это и есть единственная конфигурация — больше ничего настраивать не нужно.
## Опрос ввода через Input
В скрипте читаете action через глобальный `Input` (singleton):
```gdscript
func _physics_process(delta: float) -> void:
# 1D ось из двух actions: возвращает float в [-1, 1]
var horizontal := Input.get_axis("move_left", "move_right")
# 2D-вектор из четырёх actions: возвращает Vector2
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
# Просто проверка нажатия (повторяется каждый кадр пока зажата)
if Input.is_action_pressed("crouch"):
crouch()
# Только в кадр нажатия
if Input.is_action_just_pressed("jump"):
velocity.y = jump_speed
# Только в кадр отпускания
if Input.is_action_just_released("fire"):
stop_charging()
```
`addEventListener('keydown', ...)`, `addEventListener('keyup', ...)` + ваш state-объект,
обновляющий "какие клавиши держатся".
`Input.GetAxis("Horizontal")` ↔ `Input.get_axis("move_left", "move_right")`.
`Input.GetButtonDown("Jump")` ↔ `Input.is_action_just_pressed("jump")`. Похоже, но имена
конфигурируются полностью.
## _input(event) и _unhandled_input(event)
Альтернативно — event-driven подход. Godot пробрасывает каждое событие через дерево узлов:
```gdscript
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("pause"):
toggle_pause()
get_viewport().set_input_as_handled()
```
Разница между `_input` и `_unhandled_input`:
- **`_input`** — получает все события до того, как UI их обработает.
- **`_unhandled_input`** — получает события, которые **не съел UI**. Это правильный путь для
геймплейного ввода, потому что клик по кнопке меню не запустит стрельбу.
Иерархия проброса:
1. `_input` на всех узлах.
2. GUI обрабатывает событие, если оно касается Control-узла.
3. `_shortcut_input` — для горячих клавиш меню.
4. `_unhandled_key_input` — только клавиатура, после GUI.
5. `_unhandled_input` — всё остальное, после GUI.
## Поворот камеры мышью
Классический FPS-look:
```gdscript
extends CharacterBody3D
@export var sensitivity: float = 0.003
@export var max_pitch: float = 1.4 # ~80°
@onready var head: Node3D = $Head
@onready var camera: Camera3D = $Head/Camera3D
func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED # скрыть курсор и захватить
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
rotate_y(-event.relative.x * sensitivity)
head.rotate_x(-event.relative.y * sensitivity)
head.rotation.x = clamp(head.rotation.x, -max_pitch, max_pitch)
```
`InputEventMouseMotion.relative` — дельта по сравнению с прошлым кадром, что нам и нужно.
Без него мышка уйдёт за пределы окна на повороте. Captured-режим прячет курсор и центрирует
его каждый кадр. ESC — стандартная привычка освобождать курсор:
```gdscript
if event.is_action_pressed("ui_cancel"):
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
```
## Raycast — куда смотрит игрок
Два пути в Godot:
### 1. Узел RayCast3D
Декларативно в сцене. Добавляете дочерний `RayCast3D`, в Inspector ставите target_position
(в локальных координатах, например `(0, 0, -100)` — на 100 м вперёд). Включаете Enabled.
```gdscript
@onready var aim_ray: RayCast3D = $Camera3D/AimRay
func shoot() -> void:
if aim_ray.is_colliding():
var hit = aim_ray.get_collider() # Node3D или Object
var point = aim_ray.get_collision_point()
var normal = aim_ray.get_collision_normal()
if hit.has_method("take_damage"):
hit.take_damage(10)
```
RayCast3D автоматически обновляется в `_physics_process`.
### 2. Программный raycast через PhysicsServer
Если нужен луч "по запросу" с произвольными параметрами:
```gdscript
func shoot_from_camera() -> void:
var space = get_world_3d().direct_space_state
var from = camera.global_position
var to = from - camera.global_transform.basis.z * 100.0 # 100 м вперёд
var query = PhysicsRayQueryParameters3D.create(from, to)
query.collision_mask = 1 # только слой 1
query.exclude = [self] # не попадать в себя
var hit := space.intersect_ray(query)
if not hit.is_empty():
var collider = hit.collider
var point = hit.position
var normal = hit.normal
```
`intersect_ray` возвращает `Dictionary` с полями `collider`, `position`, `normal`, `rid`, `shape`,
или пустой dict при промахе.
## Touch и multi-touch
Touch-события приходят как `InputEventScreenTouch` (нажатие/отпускание) и `InputEventScreenDrag`
(движение). У каждого — `index` идентификатор пальца:
```gdscript
func _input(event: InputEvent) -> void:
if event is InputEventScreenTouch:
if event.pressed:
print("Finger ", event.index, " at ", event.position)
```
Для типовых мобильных контролов (виртуальные джойстики, кнопки) — Control-узлы `TouchScreenButton`
и VirtualJoystick (последний — в 4.7).
В следующей главе — физика.
---
## [Godot] Физика — Bodies, Collision, Jolt
URL: https://cadmus.page/godot/02-3d/04-physics/
Section: 3D-разработка в Godot
Description: StaticBody3D, RigidBody3D, CharacterBody3D, Area3D — и почему с 4.6 физика стала быстрее.
С Godot **4.6 физикой по умолчанию для 3D стал Jolt** — высокопроизводительный движок,
используемый в Death Stranding 2 и других AAA-играх. Старый Godot Physics остался как опция и
продолжает быть default для 2D.
Это значит: предсказуемая, масштабируемая 3D-физика из коробки. Никаких сторонних движков
интегрировать не нужно.
## Четыре класса тел
Узлы-наследники `CollisionObject3D`, образующие физический объект:
| Узел | Когда |
|---|---|
| **StaticBody3D** | Не движется. Стены, пол, статичная архитектура. |
| **AnimatableBody3D** | Двигается через анимацию или скрипт. Толкает RigidBody3D. Аналог Unity Kinematic Rigidbody. |
| **RigidBody3D** | Полная динамика — гравитация, силы, импульсы, столкновения с реакцией. |
| **CharacterBody3D** | Кинематика для персонажа: вы задаёте `velocity`, движок двигает с учётом коллизий. Единственный узел с `move_and_slide()`. |
Также:
- **Area3D** — невидимая область с триггерами `body_entered`/`area_entered`. Аналог Unity Collider
с `isTrigger=true`.
- **PhysicalBone3D** — для рэгдоллов внутри Skeleton3D.
- **SoftBody3D** — мягкие тела (ткань, флаги, простые желейные объекты). Использует mesh-based
soft-body simulation; работает и с Godot Physics, и с Jolt (с некоторыми ограничениями).
## CollisionShape3D — форма для физики
В отличие от Unity, где collider — это компонент на том же GameObject, **в Godot CollisionShape3D
— это отдельный дочерний узел**. У него свойство `shape: Shape3D` — ресурс, описывающий форму:
```
RigidBody3D (Player)
├── MeshInstance3D (визуальный mesh)
└── CollisionShape3D
└── shape: CapsuleShape3D (height=2, radius=0.4)
```
Виды Shape3D:
- **BoxShape3D**, **SphereShape3D**, **CapsuleShape3D**, **CylinderShape3D** — примитивы. Дёшево.
- **ConvexPolygonShape3D** — выпуклый mesh. Для динамических объектов сложной формы.
- **ConcavePolygonShape3D** — произвольный mesh (триангуляция). **Только для статики**.
- **HeightMapShape3D** — рельеф из 2D-карты высот.
Просто добавьте несколько дочерних CollisionShape3D — все станут shape'ами этого тела. Удобно
для составных коллайдеров (например, машина: корпус + бампер).
## CharacterBody3D — главный для платформера/FPS
`CharacterBody3D` хранит **`velocity: Vector3`** и метод **`move_and_slide()`**, который пытается
переместить тело на `velocity * delta` с учётом столкновений: останавливается, скользит вдоль стен,
опускается по уклонам, поднимается на ступеньки.
```gdscript
extends CharacterBody3D
@export var speed: float = 5.0
@export var jump_velocity: float = 6.0
func _physics_process(delta: float) -> void:
# Гравитация — из настроек проекта (Project Settings → Physics → 3D → Default Gravity)
# get_gravity() возвращает Vector3(0, -9.8, 0) по умолчанию
if not is_on_floor():
velocity += get_gravity() * delta
# Прыжок
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_velocity
# Горизонтальное движение
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
if direction:
velocity.x = direction.x * speed
velocity.z = direction.z * speed
else:
velocity.x = move_toward(velocity.x, 0, speed)
velocity.z = move_toward(velocity.z, 0, speed)
move_and_slide()
```
Полезные методы и свойства CharacterBody3D:
- **`is_on_floor()`**, **`is_on_wall()`**, **`is_on_ceiling()`** — простые проверки контакта.
- **`get_floor_normal()`** — нормаль пола, на котором стоим.
- **`floor_max_angle`** — максимальный угол склона, считающегося полом (по умолчанию 45°).
- **`floor_snap_length`** — на какую глубину "примагнитить" к полу при сходе по склону (избегает
отскоков на спусках).
- **`get_slide_collision_count()`** + **`get_slide_collision(i)`** — детали столкновений за этот
тик.
Свой 3D без движка: считаете `next_pos = pos + velocity * dt`, ловите коллизии руками. Сложно
и багги.
`CharacterController.Move(motion)` ↔ `move_and_slide()`. CharacterBody3D мощнее: автоматически
скользит по стенам, поднимается на ступеньки, прижимается к склонам — всё настраивается
флажками.
## RigidBody3D — динамика
Для физических объектов, реагирующих на силы (мяч, ящик, dropped item):
```gdscript
extends RigidBody3D
func _ready() -> void:
mass = 2.0
linear_damp = 0.5
func explode_push(direction: Vector3, force: float) -> void:
apply_central_impulse(direction * force)
```
Методы:
- `apply_force(force: Vector3, position: Vector3 = Vector3.ZERO)` — постоянная сила (в `_physics_process`).
- `apply_central_force(force)` — сила в центр массы.
- `apply_impulse(impulse, position)` — мгновенный импульс.
- `apply_central_impulse(impulse)` — мгновенный импульс в центр массы.
- `apply_torque_impulse(impulse)` — крутящий момент.
Свойства:
- `mass: float` — масса.
- `gravity_scale: float` — множитель гравитации (0 = нет, 1 = нормально).
- `linear_damp` / `angular_damp` — затухание.
- `freeze` / `freeze_mode` — заморозить.
- `lock_rotation: bool` — запретить вращение.
- `continuous_cd: CCDMode` — `DISABLED` / `CAST_RAY` / `CAST_SHAPE` — для быстрых тел против
туннелирования.
## Area3D — триггеры
`Area3D` — не физическое тело, но генерирует сигналы при пересечении с другими CollisionObject3D:
```gdscript
extends Area3D
func _ready() -> void:
body_entered.connect(_on_body_entered)
area_entered.connect(_on_area_entered)
func _on_body_entered(body: Node3D) -> void:
if body.is_in_group("player"):
print("Player picked up coin")
queue_free()
func _on_area_entered(area: Area3D) -> void:
print("Another area overlapped: ", area.name)
```
Сигналы Area3D:
- `body_entered(body)` / `body_exited(body)` — физическое тело вошло/вышло.
- `area_entered(area)` / `area_exited(area)` — другая Area3D.
- `body_shape_entered`, `area_shape_entered` — детализированные с индексами shape'ов.
## Collision Layers и Masks
У каждого CollisionObject3D два битфилда:
- **Collision Layer** — на каких слоях находится этот объект.
- **Collision Mask** — какие слои этот объект **сканирует** (с кем пересекается / реагирует).
Чтобы A замечал B: маска A должна содержать слой(и) B. Чтобы взаимодействие было **двусторонним**,
маска B тоже должна содержать слой A.
Имена слоёв настраиваются в Project Settings → Layer Names → 3D Physics.
В Unity матрица слоёв двунаправленная. В Godot — два независимых битфилда на каждом объекте.
Это даёт больше гибкости (например, "пуля видит врага, но не наоборот"), но требует осторожности.
## Jolt vs Godot Physics
С 4.6 Jolt — default для **новых проектов** 3D. Существующие проекты автоматически не
переключаются — выставьте в Project Settings → Physics → 3D → Physics Engine.
| Параметр | Godot Physics | Jolt |
|---|---|---|
| Производительность с 1000+ телами | Деградирует | Стабильна |
| Stacking (стопки ящиков) | Может проседать | Лучше |
| CCD (continuous) | Есть | Есть, лучше |
| Doubleside thin colliders | Иногда баги | Стабильно |
| Joints | Базовые | Полный набор |
Если у вас уже работающий проект на Godot Physics — оставайтесь. Для нового — Jolt.
В следующей главе — камера и Phantom Camera.
---
## [Godot] Камера и Phantom Camera
URL: https://cadmus.page/godot/02-3d/05-camera/
Section: 3D-разработка в Godot
Description: Camera3D, виды от первого/третьего лица, и плагин-аналог Cinemachine.
## Camera3D — главный узел
Camera3D — узел, который "смотрит" в сцену. Только **одна Camera3D активна в любой момент** (если
не используете отдельные viewports). У сцены может быть несколько камер; активная — та, у которой
`current = true`, или которая стала активной через `make_current()`.
Главные свойства:
- **`projection`** — `PERSPECTIVE` (3D, с перспективой) или `ORTHOGONAL` (без, для изометрии/2.5D).
- **`fov`** — поле зрения в градусах **по вертикали**. Стандарт FPS — 60–90°.
- **`near`**, **`far`** — clipping planes. Большой разброс портит точность Z-buffer (Z-fighting).
- **`cull_mask`** — битмаска: какие visual layers рендерит эта камера.
```gdscript
@onready var camera: Camera3D = $Head/Camera3D
func _ready() -> void:
camera.fov = 75.0
camera.near = 0.05
camera.far = 500.0
```
## Простой follow
Аналог Unity-варианта со SmoothDamp. В Godot нет встроенной `SmoothDamp`, но есть `lerp`:
```gdscript
extends Camera3D
@export var target: Node3D
@export var offset: Vector3 = Vector3(0, 2.0, 4.0) # за спиной, выше
@export var smoothing: float = 5.0
func _process(delta: float) -> void:
if target == null:
return
var desired = target.global_position + target.global_transform.basis * offset
global_position = global_position.lerp(desired, smoothing * delta)
look_at(target.global_position + Vector3.UP * 1.5, Vector3.UP)
```
`vector.lerp(to, weight)` — метод Vector3 для линейной интерполяции. `look_at(target, up_vector)` —
направляет "вперёд" (`−Z`) камеры на цель.
Если двигать камеру в `_physics_process`, на кадрах между физтиками она "застрянет" и будет
выглядеть дёрганой. Камера — это рендер-задача, её место в `_process`. Если игрок — CharacterBody3D
с физикой, его позицию интерполируйте либо включите глобальную physics interpolation в Project
Settings (с 4.3).
## Phantom Camera — Cinemachine-аналог
Godot core не имеет ничего уровня Unity Cinemachine. Но есть **Phantom Camera** —
популярный community-плагин (`ramokz/phantom-camera` в Asset Store / Godot Asset Library).
Дает виртуальные камеры с blendами, follow / look-at modes, framing, dead zones, hosts.
Установка: AssetLib (внутри редактора) → ищите "Phantom Camera" → Install. Активируется в
Project → Project Settings → Plugins.
### Базовая follow-camera в Phantom Camera
```
Player ← CharacterBody3D
└── PhantomCamera3D_Follow ← виртуальная камера (follow_mode = ThirdPerson)
PhantomCameraHost ← один на сцену, висит на главной Camera3D
└── Camera3D ← реальная камера
```
Параметры виртуальной камеры:
- **Follow Mode** — `Simple`, `Group`, `Path`, `Framed`, `ThirdPerson`.
- **Look At Mode** — `None`, `Simple`, `Group`, `Mimic`.
- **Tween Resource** — кривая и длительность блендинга при переключении.
- **Priority** — приоритет; PhantomCameraHost автоматически выбирает камеру с наибольшим.
```gdscript
# Переключение виртуальных камер
$PhantomCamera3D_FirstPerson.priority = 20
$PhantomCamera3D_ThirdPerson.priority = 10
# Host плавно переедет на FirstPerson
```
Если ваша игра требует кат-сцен, переключений вид-от-первого/третьего лица, drone-камеры или
тонкого framing — поставьте плагин с самого старта. Писать всё это вручную долго и хрупко.
## Поворот камеры мышью (FPS)
Уже видели в главе про ввод, но повторим в контексте камеры:
```gdscript
extends CharacterBody3D
@onready var head: Node3D = $Head
@onready var camera: Camera3D = $Head/Camera3D
@export var sensitivity: float = 0.003
const MAX_PITCH: float = 1.4
func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
# yaw — поворот тела игрока (влияет на направление движения)
rotate_y(-event.relative.x * sensitivity)
# pitch — только голова, тело не наклоняется
head.rotate_x(-event.relative.y * sensitivity)
head.rotation.x = clamp(head.rotation.x, -MAX_PITCH, MAX_PITCH)
```
Иерархия:
```
Player (CharacterBody3D) ← rotate_y (yaw)
├── CollisionShape3D
└── Head (Node3D) ← rotate_x (pitch)
└── Camera3D ← position (0, 0, 0)
```
Pitch (вертикаль) применяется к Head, чтобы коллайдер игрока не наклонялся, когда вы смотрите в
небо.
## Несколько viewports — split screen, мини-карты
Для split-screen или картинки-в-картинке используют `SubViewport` + `SubViewportContainer`:
```
SubViewportContainer (растягивается на пол-экрана)
└── SubViewport
└── Camera3D (current = true)
└── (то же 3D-мир, но через World3D-shared)
```
`SubViewport.world_3d = main_viewport.world_3d` — обе камеры рендерят одну сцену.
## Виды проекции
`Camera3D.projection` влияет на ощущение:
- **PERSPECTIVE** — естественное 3D с уменьшением далёких объектов.
- **ORTHOGONAL** — без перспективы. Свойство `size` задаёт ширину видимой области. Подходит для
изометрии, технических визуализаций, 2.5D.
- **FRUSTUM** — продвинутая ассиметричная проекция (off-axis). Редко нужно — VR, спецэффекты.
В следующей главе — рендеринг.
---
## [Godot] Рендеринг и материалы
URL: https://cadmus.page/godot/02-3d/06-rendering-materials/
Section: 3D-разработка в Godot
Description: Forward+/Mobile/Compatibility, BaseMaterial3D, gdshader — как Godot рисует кадр.
## Три рендерера
Godot выбирает рендерер при создании проекта (можно сменить позже, но материалы могут
требовать адаптации):
| Renderer | API | Назначение |
|---|---|---|
| **Forward+** | Vulkan / Metal / D3D12 | Hi-end. Clustered Forward+, SDFGI, volumetric fog, SSR, SSAO/SSIL. |
| **Mobile** | Vulkan / Metal / D3D12 | Мобильные, VR. Single-pass forward, лимит light'ов на меш. |
| **Compatibility** | OpenGL ES 3 / WebGL 2 | Слабое железо, **единственный полноценный путь для веба**. |
Project → Project Settings → Rendering → Renderer → Rendering Method.
### Когда что
- **Forward+ + Vulkan** — десктоп, серьёзные 3D-проекты, hi-end visuals.
- **Mobile + Vulkan/Metal** — телефоны.
- **Compatibility + WebGL 2** — браузер (это единственный путь для веб-таргета, который работает
везде без COOP/COEP-заголовков).
Compatibility объявлен feature-complete с 4.3: lights, shadows, GI (lightmaps), GPU particles — всё
доступно, с некоторыми ограничениями.
## Mesh, Material, Shader
Та же триада, что и в Unity:
- **Mesh** — геометрия (вершины, нормали, UV). Импортируется из glTF/FBX/.obj/.dae.
- **Shader** — программа на GPU (`.gdshader` или встроенный движковый).
- **Material** — экземпляр шейдера с параметрами.
В Godot нет файлов "Material" как у Unity — материал может быть встроен в Mesh-ресурс
(`override material`), либо сохранён отдельно как `.tres`.
## BaseMaterial3D — стандартный PBR
Аналог Unity Standard / URP Lit. Готовый PBR-материал, который покрывает 90% задач без написания
шейдера. Главные свойства:
- **Albedo** — `albedo_color` + `albedo_texture`.
- **Metallic** + **Roughness** — диффузный/металлический workflow.
- **Normal** — карта нормалей.
- **Emission** — самосветящийся цвет/текстура.
- **Ambient Occlusion** — карта AO.
- **Transparency** — `OPAQUE` / `ALPHA` / `ALPHA_SCISSOR` / `ALPHA_DEPTH_PRE_PASS`.
- **Cull Mode** — `BACK` / `FRONT` / `DISABLED`.
- **Texture filtering** — `LINEAR` / `NEAREST`.
```gdscript
var mat = StandardMaterial3D.new() # StandardMaterial3D — наследник BaseMaterial3D
mat.albedo_color = Color.RED
mat.metallic = 0.5
mat.roughness = 0.3
$Mesh.material_override = mat
```
В Inspector видны `StandardMaterial3D` (использует отдельные карты для AO/Roughness/Metallic) и
`ORMMaterial3D` (одна карта с тремя каналами для AO/Roughness/Metallic — ORM). Оба — наследники
абстрактного `BaseMaterial3D`. Для совместимости с стандартом glTF — используйте ORM.
## ShaderMaterial — кастомный шейдер
Когда BaseMaterial3D не хватает, делаете `ShaderMaterial` с собственным `.gdshader`-ресурсом:
```glsl
// dissolve.gdshader
shader_type spatial;
uniform sampler2D albedo : source_color, filter_linear_mipmap;
uniform sampler2D noise;
uniform float threshold : hint_range(0.0, 1.0) = 0.5;
uniform vec3 edge_color : source_color = vec3(1.0, 0.5, 0.0);
void fragment() {
vec4 base = texture(albedo, UV);
float n = texture(noise, UV).r;
if (n < threshold) {
discard; // не рисовать этот пиксель
}
if (n < threshold + 0.05) {
ALBEDO = edge_color; // светящаяся граница
EMISSION = edge_color * 2.0;
} else {
ALBEDO = base.rgb;
}
}
```
Применение:
```gdscript
var shader = preload("res://shaders/dissolve.gdshader")
var mat = ShaderMaterial.new()
mat.shader = shader
mat.set_shader_parameter("threshold", 0.7)
mat.set_shader_parameter("albedo", preload("res://textures/wall.png"))
$Mesh.material_override = mat
```
GLSL ES 3.0 / WebGL — то же самое, только в Godot вокруг шейдера обёртка с `shader_type`,
встроенными переменными (UV, NORMAL, ALBEDO, ALPHA, EMISSION, ROUGHNESS, NORMAL_MAP, ...).
HLSL + ShaderLab в Unity ↔ `.gdshader` (GLSL ES 3.0 dialect) в Godot. Синтаксис другой, идея
та же. Godot Shader Language проще читать новичку: меньше boilerplate, чем Unity URP Lit shader.
## shader_type — категории
В Godot шейдеры явно категоризированы по типу через директиву:
- `shader_type spatial;` — для 3D-мешей.
- `shader_type canvas_item;` — для 2D и Control.
- `shader_type particles;` — для частиц (process material).
- `shader_type sky;` — для skybox.
- `shader_type fog;` — для volumetric fog (Forward+).
Каждый тип имеет свои входы/выходы и встроенные переменные.
## Visual Shader
Godot имеет встроенный визуальный шейдерный редактор — **Visual Shader**, значительно
переработанный в 4.x (последовательные улучшения UX и нод-каталога). Создаётся через FileSystem →
New Resource → VisualShader. Аналог Unity Shader Graph.
Удобен для тех, кто не пишет код шейдеров; собирает граф нод (Texture, Multiply, Mix, ...). Под
капотом генерирует тот же `.gdshader`.
## Draw calls и batching
В отличие от Unity, у Godot нет настроек "Static Batching" / "GPU Instancing" на материалах в
явном виде. Но:
- **MultiMeshInstance3D** — нод, который рисует N экземпляров одного меша одним draw call'ом
(через `MultiMesh`-ресурс). Аналог GPU Instancing. Применение: трава, ассеты декораций, толпы.
- **RenderingServer** автоматически батчит draw call'ы, где может.
```gdscript
var mm = MultiMesh.new()
mm.transform_format = MultiMesh.TRANSFORM_3D
mm.instance_count = 1000
mm.mesh = preload("res://grass.obj")
for i in 1000:
var transform = Transform3D()
transform.origin = Vector3(randf_range(-50, 50), 0, randf_range(-50, 50))
mm.set_instance_transform(i, transform)
$MultiMeshInstance3D.multimesh = mm
```
## Post-processing
В Godot нет Unity-стиля Volume Framework. Post-processing настраивается через **WorldEnvironment**
узел и его ресурс **Environment**:
- **Background** — skybox / colored / camera_feed.
- **Ambient Light**.
- **Glow** (bloom).
- **Tonemap** — Linear / Reinhard / Filmic / ACES / AgX.
- **SSR** (Forward+).
- **SSAO** / **SSIL** (Forward+).
- **SDFGI** (Forward+) — динамическая GI.
- **DOF** (depth of field).
- **Adjustments** — Brightness/Contrast/Saturation/Color Correction LUT.
- **Glow**, **Volumetric Fog**.
Один WorldEnvironment на сцену. Чтобы переключать (например, при входе в пещеру), есть **Camera3D
→ Environment Override** или меняйте через скрипт.
```gdscript
var env = $WorldEnvironment.environment
env.tonemap_mode = Environment.TONE_MAPPER_ACES
env.glow_enabled = true
env.glow_intensity = 0.6
```
SSR, SSAO, SDFGI, volumetric fog — это features Forward+ рендера. На Mobile/Compatibility они
отсутствуют или работают в урезанном виде. Если ваш проект таргетит веб — рассчитывайте на
Compatibility-палитру.
В следующей главе — освещение подробнее.
---
## [Godot] Освещение и тени
URL: https://cadmus.page/godot/02-3d/07-lighting/
Section: 3D-разработка в Godot
Description: DirectionalLight3D, OmniLight3D, SpotLight3D, lightmaps и SDFGI.
## Узлы света
Три встроенных типа света (плюс грядущий AreaLight3D в 4.7):
- **DirectionalLight3D** — солнце. Параллельные лучи, бесконечно далёкие. Один на сцену обычно.
- **OmniLight3D** — точечный (Unity Point Light). От точки во все стороны с радиусом затухания.
- **SpotLight3D** — конус из точки с углом и радиусом.
- **AreaLight3D** — area-light (с 4.7). Прямоугольный/дисковый источник.
```gdscript
$DirectionalLight3D.light_energy = 1.5
$DirectionalLight3D.light_color = Color(1.0, 0.95, 0.85) # тёплое утро
$DirectionalLight3D.shadow_enabled = true
```
Главные свойства каждого:
- **`light_energy`** — яркость.
- **`light_color`** — цвет.
- **`light_indirect_energy`** — множитель для непрямого света от этого источника (в GI).
- **`shadow_enabled`** — рисовать тени.
- **`shadow_bias`**, **`shadow_normal_bias`** — настройки артефактов теней.
- **`light_bake_mode`** — `Light3D.BAKE_DISABLED` / `BAKE_STATIC` (baked) / `BAKE_DYNAMIC` (real-time GI). В Inspector показано как Disabled/Static/Dynamic, но в коде — с префиксом `BAKE_`.
## Realtime, baked, mixed
Как и в Unity, три режима использования:
| Bake Mode | Где вычисляется | Применение |
|---|---|---|
| **DISABLED** | Realtime каждый кадр | Динамические источники (взрыв, ракета) |
| **STATIC** | Запекается один раз | Статичная архитектура |
| **DYNAMIC** | Реалтайм + участвует в GI (SDFGI/VoxelGI) | Главный сценический свет, не двигается |
## Запекание GI
В Godot есть **три системы глобального освещения**, разные по цене:
### 1. LightmapGI — запечённые лайтмапы
Самый эффективный по runtime, но требует запекания. Для статичной геометрии.
1. Пометьте объекты как "static" (через флаг `gi_mode = STATIC` в Inspector у MeshInstance3D).
2. Добавьте узел **LightmapGI** на сцену.
3. Bake Lightmaps кнопкой в редакторе.
Для динамических объектов в зоне lightmap'а — используйте `LightmapProbe` (как Unity Light Probes).
### 2. VoxelGI — voxel-based realtime GI
Делит сцену на 3D-сетку вокселей, рассчитывает GI в realtime. Дорого, но красиво. Узел
**VoxelGI**, кнопка Bake создаёт начальные данные. Подходит для интерьеров.
### 3. SDFGI — Signed Distance Field GI
Самый мощный — динамический GI без запекания. Включается в Environment ресурсе:
```gdscript
env.sdfgi_enabled = true
env.sdfgi_cascades = 6
env.sdfgi_min_cell_size = 0.5
```
Минусы: только Forward+, дорого, заметная "пелена" вблизи граничек каскадов.
Forward+ desktop: главный свет — DirectionalLight3D в DYNAMIC bake mode, sky/world environment
с SDFGI или LightmapGI. Mobile: LightmapGI + DirectionalLight3D в STATIC.
## Тени — каскады для DirectionalLight3D
DirectionalLight (солнце) рисует тени через **shadow cascades** — несколько разрешений теневой
карты с разной дистанцией:
- **`directional_shadow_mode`** — `ORTHOGONAL` / `PARALLEL_2_SPLITS` / `PARALLEL_4_SPLITS`.
- **`directional_shadow_max_distance`** — до какой дистанции рисуем тени.
- **`directional_shadow_split_1/2/3`** — пропорции каскадов.
Чем больше каскадов — тем плавнее переход, но дороже. PARALLEL_4_SPLITS — стандарт.
## Skybox и Environment
WorldEnvironment + Environment ресурс — глобальные настройки сцены. `Environment.background_mode`
принимает значения с префиксом **`BG_`**:
- **`Environment.BG_CLEAR_COLOR`** — один цвет.
- **`Environment.BG_COLOR`** — другой цвет.
- **`Environment.BG_SKY`** — Sky-ресурс (procedural или panorama HDR).
- **`Environment.BG_CANVAS`** — 2D-фон.
- **`Environment.BG_KEEP`** — не очищать (для split-screen).
- **`Environment.BG_CAMERA_FEED`** — AR/камера устройства.
Для HDRI:
```gdscript
var sky_mat = PanoramaSkyMaterial.new()
sky_mat.panorama = preload("res://hdri/sunset_4k.exr")
var sky = Sky.new()
sky.sky_material = sky_mat
env.sky = sky
env.background_mode = Environment.BG_SKY
env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
```
Panorama (equirectangular) HDR работает из коробки — берите с PolyHaven, ставите, освещение
"пропитывается" этим небом.
## Reflection Probes
Узел **ReflectionProbe** — сферическая карта окружения для отражений в материале. Размещайте в
центре комнаты:
- **`box_projection`** — проектировать отражение на box, чтобы отражения не "плыли" в углах.
- **`update_mode`** — `ONCE` (одна запечённая) / `ALWAYS` (каждый кадр, дорого).
Для металлических материалов это критично — без probe отражают только sky.
## Что обычно ломается у новичков
1. **Все объекты dynamic, GI не работает.** Помечайте `gi_mode = STATIC` на не двигающейся
геометрии перед запеканием.
2. **Слишком много realtime теневых источников.** OmniLight3D с тенями в Forward — дорого.
Ограничьте 4–6 видимых одновременно.
3. **Compatibility-рендер и SDFGI / VoxelGI.** Не работают вместе — используйте LightmapGI.
4. **Forgetting WorldEnvironment.** Без него сцена будет рендериться с дефолтным
environment (часто чёрный фон, без ambient'а).
В следующей главе — анимация.
---
## [Godot] Анимация — AnimationPlayer и AnimationTree
URL: https://cadmus.page/godot/02-3d/08-animation/
Section: 3D-разработка в Godot
Description: AnimationLibrary, state machine, blend spaces, root motion.
## AnimationPlayer — главный узел
`AnimationPlayer` хранит **AnimationLibrary** с набором `Animation`-ресурсов и проигрывает их.
Уникальная особенность Godot — **можно анимировать любое свойство любого узла** (или даже
ресурса), не только Transform и Material:
- Transform (position, rotation, scale)
- Любое property (alpha sprite, `visible`, числа, цвета)
- Shader uniforms — через property path `material:shader_parameter/threshold`
- Метод-вызовы (по таймеру в треке вызвать функцию)
- Audio (запустить AudioStreamPlayer)
- Вложенные анимации других AnimationPlayer
```
Player (Node3D)
├── Skeleton3D
│ └── MeshInstance3D
└── AnimationPlayer ← здесь все клипы: Idle, Run, Jump, Attack
```
```gdscript
$AnimationPlayer.play("Run")
$AnimationPlayer.play("Jump", -1, 1.2) # blend_time=-1, speed=1.2
$AnimationPlayer.speed_scale = 0.5 # все клипы замедлены вдвое
$AnimationPlayer.stop()
$AnimationPlayer.seek(0.5, true) # перемотать в 0.5 с
# Сигнал об окончании
$AnimationPlayer.animation_finished.connect(_on_anim_done)
```
GSAP / Framer Motion timeline: вы описываете треки на свойствах элементов с
keyframes, время управляет анимацией.
Unity Animation Window — то же ощущение записи треков по timeline'у. Но Unity Animator
Controller — отдельный state-machine ассет. В Godot его аналог — **AnimationTree**, и он
привязан к одному AnimationPlayer.
## Запись анимации в редакторе
1. Выберите AnimationPlayer в сцене → внизу появится Animation editor.
2. Кнопка "Animation" → New → имя.
3. Включите красную точку (Recording) → измените любое свойство в Inspector → keyframe создастся
автоматически в текущей точке таймлайна.
4. Прокрутите таймлайн → измените снова → второй keyframe.
Также треки добавляются вручную через "Add Track".
## AnimationTree — state machine и blendspace
Для сложной анимации (locomotion, переходы) используется **AnimationTree** — отдельный узел,
который читает анимации из связанного AnimationPlayer и обрабатывает их через граф нод:
```
Player (Node3D)
├── AnimationPlayer
└── AnimationTree ← root: AnimationNodeStateMachine
tree_root указывает на корневой граф
anim_player указывает на AnimationPlayer
```
Корневой граф AnimationTree — это `AnimationNode`. Виды:
- **AnimationNodeStateMachine** — FSM с состояниями и переходами.
- **AnimationNodeBlendTree** — граф для смешивания.
- **AnimationNodeBlendSpace1D** — 1D-параметрический блендинг (например, `speed` 0..6 ↔ Idle ↔ Walk ↔ Run).
- **AnimationNodeBlendSpace2D** — 2D (например, `strafe_x`, `forward_y` для 8-направленного движения).
- **AnimationNodeAdd2/3** — наложение поз.
- **AnimationNodeOneShot** — однократное воспроизведение поверх (например, "выстрел" поверх ходьбы).
- **AnimationNodeTimeScale** / **TimeSeek**.
### Состояния через StateMachine
```gdscript
@onready var anim_tree: AnimationTree = $AnimationTree
@onready var state_machine = anim_tree["parameters/playback"] # AnimationNodeStateMachinePlayback
func _ready() -> void:
anim_tree.active = true
func _process(delta: float) -> void:
var speed = velocity.length()
anim_tree["parameters/Move/blend_position"] = speed # 1D blendspace
if Input.is_action_just_pressed("jump"):
state_machine.travel("Jump") # умный переход через граф (A* find path)
```
`travel("StateName")` — главная фишка Godot StateMachine: ищет короткий путь через переходы.
Из Idle хочется в Land? Если нет прямой стрелки, Godot найдёт Idle → Run → Land и пройдёт.
AnimationTree предоставляет все настраиваемые параметры графа через subscript-нотацию по строке.
`anim_tree["parameters/Locomotion/blend_position"]` — путь в графе. Хеширование путей при
частом доступе можно делать через `var path = "parameters/...":` константу.
## Root Motion
Если в анимации скелет двигается ("шаг вперёд") — по умолчанию AnimationPlayer **не двигает
GameObject**. Только меняет позы. Для перевода смещения скелета в реальное движение узла:
1. На AnimationTree укажите `root_motion_track` — путь к корневой кости (`Skeleton3D:Hips`).
2. В скрипте читайте `anim_tree.get_root_motion_position()` и применяйте к узлу:
```gdscript
func _physics_process(delta: float) -> void:
var motion = anim_tree.get_root_motion_position()
velocity = transform.basis * motion / delta
move_and_slide()
```
Когда не использовать root motion: классические FPS/платформеры, где скорость считается логикой.
Используют для cinematic-движений, грэпплов, добиваний.
## Skeleton3D, Bones, Bone Attachment
`Skeleton3D` — иерархия костей. Применяется к скиннингу `MeshInstance3D` через `Skin`-ресурс.
`BoneAttachment3D` — узел, который следует за кости́ю. Прикрепляете к нему оружие, аксессуары:
```
Player
├── Skeleton3D
│ ├── MeshInstance3D (skinned)
│ └── BoneAttachment3D (bone_name = "RightHand")
│ └── Sword (Node3D + MeshInstance3D)
```
## IK — Inverse Kinematics
В 4.6 IK вернулся в форме нодов-солверов внутри Skeleton3D (узлы-наследники `SkeletonModifier3D`):
- **TwoBoneIK3D** — два звена (рука/нога) с целевой позицией.
- **FABRIK3D** — Forward And Backward Reaching IK (несколько звеньев).
- **CCDIK3D** — Cyclic Coordinate Descent.
- **ChainIK3D**, **SplineIK3D**, **JacobianIK3D**, **IterateIK3D** — дополнительные солверы для
специальных случаев.
Применяется через `SkeletonModifier3D`-узлы, привязанные к Skeleton3D. Пример: рука персонажа
тянется к ручке двери при взаимодействии.
## Импорт анимации
Источники:
- **glTF 2.0** — рекомендованный формат для 3D-моделей с анимацией. Из Blender экспортируется
идеально.
- **FBX** — нативный импорт с 4.3 через ufbx (без внешнего FBX2glTF).
- **Mixamo** анимации — лучше через glTF; FBX тоже работает с retargeting.
При импорте Godot создаёт **сцену** (`.tscn`-аналог) с Skeleton3D + AnimationPlayer + AnimationLibrary.
Настройки импорта (scale factor, animation library mapping) — в Import-доке.
Для retargeting (перенести анимацию с одного скелета на другой) используйте **Skeleton Profile**
(SkeletonProfileHumanoid встроен).
## Animation Events / Method Tracks
Аналог Unity Animation Event в Godot — **Method Track**: трек, который вызывает метод на узле в
заданный момент. В Animation editor: Add Track → Call Method Track → выбрать узел → keyframe
выставляет в payload имя метода и параметры:
```gdscript
# Этот метод можно дёрнуть из Method Track в анимации "Walk"
func on_footstep() -> void:
$StepSound.play()
```
В следующей главе — UI.
---
## [Godot] 3D-аудио и AudioBus
URL: https://cadmus.page/godot/02-3d/09-audio/
Section: 3D-разработка в Godot
Description: AudioStreamPlayer3D, маршрутизация через AudioBus, эффекты.
В Godot аудио — отдельный сервер (`AudioServer`) с маршрутизацией через **AudioBus**. У сцены —
несколько узлов-плееров, у проекта — единый микшер.
## Узлы воспроизведения
Три варианта:
- **AudioStreamPlayer** — без позиционирования. Музыка, UI-звуки, диалоги.
- **AudioStreamPlayer2D** — позиционный для 2D-сцен (громкость по дистанции к слушателю).
- **AudioStreamPlayer3D** — 3D-позиционный с attenuation, Doppler, area reverb.
```gdscript
@onready var step_player: AudioStreamPlayer3D = $StepPlayer
func _ready() -> void:
step_player.stream = preload("res://audio/step.ogg")
func play_step() -> void:
step_player.pitch_scale = randf_range(0.9, 1.1) # вариация для естественности
step_player.play()
```
Главные свойства AudioStreamPlayer3D:
- **`stream`** — `AudioStream`-ресурс (AudioStreamOggVorbis, AudioStreamMP3, AudioStreamWAV).
- **`volume_db`** — громкость в децибелах. 0 — оригинал, -10 — тише, -80 — почти тишина.
- **`pitch_scale`** — множитель скорости/высоты.
- **`unit_size`** — фактор затухания (default `10.0`). Чем больше значение — тем дальше звук слышен.
Конкретная формула зависит от `attenuation_model`. Не "расстояние половинной громкости", а
параметр кривой.
- **`max_distance`** — на каком расстоянии звук затихнет полностью (0 = без ограничения).
- **`attenuation_model`** — `INVERSE_DISTANCE`, `INVERSE_SQUARE_DISTANCE`, `LOGARITHMIC`, `DISABLED`.
- **`doppler_tracking`** — Doppler эффект (`IDLE_STEP` / `PHYSICS_STEP` / `DISABLED`).
- **`area_mask`** — битмаска слоёв Area3D, на которые этот плеер реагирует (Area3D с reverb/bus
override на соответствующих слоях переопределит обработку звука этого плеера, например
"underwater"-эффект в зоне воды).
## AudioBus — микшер
Открывается в нижней панели: вкладка **Audio**. Структура:
```
Master (главный, есть всегда)
├── Music
├── SFX
│ ├── Voice
│ └── Ambience
└── UI
```
Каждый bus:
- **Volume** — слайдер.
- **Solo / Mute / Bypass**.
- **Effects** — стек эффектов: Reverb, EQ, Compressor, Limiter, Chorus, Phaser, Distortion,
Delay, Pitch Shift, Filter, Stereo Enhance, Spectrum Analyzer.
На AudioStreamPlayer задаёте `bus = "SFX/Voice"`, и звук маршрутизируется туда. Удобно глушить
SFX отдельно от музыки или применить low-pass к Master при паузе.
```gdscript
# Настройка громкости из настроек игрока (UI-слайдер)
var music_idx = AudioServer.get_bus_index("Music")
AudioServer.set_bus_volume_db(music_idx, linear_to_db(music_volume_0_to_1))
```
`linear_to_db(linear)` — встроенная функция конвертации линейной громкости (0..1) в децибелы
(`-80..0`).
## 2D / 3D Spatial Blend
В отличие от Unity AudioSource (где slider 2D↔3D), в Godot **сам выбор узла** определяет
позиционность:
- AudioStreamPlayer — никогда не позиционный.
- AudioStreamPlayer2D/3D — всегда позиционный.
Если нужен звук, который в начале 3D, потом 2D (например, диктор-нарратор приближается и
становится UI) — это два разных узла, переключение в коде.
## Listener
В отличие от Unity (где AudioListener — отдельный компонент), в Godot **слушатель — это активная
Camera3D**. По умолчанию. Если нужно сместить слушателя относительно камеры — есть узел
`AudioListener3D`:
```
Camera3D (current)
└── AudioListener3D (current = true) ← теперь слушатель здесь
```
Полезно для third-person: камера далеко, но слушать хочется с уровня головы персонажа.
## AudioStreamInteractive — динамическая музыка
Введён в 4.3. Это специальный AudioStream, в котором описаны **клипы** (части) и **переходы**
между ними с правилами:
- Кросс-фейд через N тактов.
- Match по beat / bar / next-marker.
- Условные переходы (на основе параметра).
Пример: спокойная тема (clip A) переходит в боевую (clip B) через 2 такта при `set_clip("Battle")`.
Это аналог FMOD/Wwise состояний, но встроенно.
## AudioStreamPolyphonic — наслаивающиеся звуки
Если играете много коротких звуков (выстрелы, удары) на одном источнике, обычный AudioStreamPlayer
прерывает предыдущий. `AudioStreamPolyphonic` решает это: один плейер, несколько слоёв.
```gdscript
var poly_stream = AudioStreamPolyphonic.new()
poly_stream.polyphony = 8 # до 8 одновременно
$Player.stream = poly_stream
var playback: AudioStreamPlaybackPolyphonic = $Player.get_stream_playback()
playback.play_stream(preload("res://gunshot.ogg"))
playback.play_stream(preload("res://shell_drop.ogg"))
```
## AudioStreamGenerator — процедурное аудио
Если хочется генерировать звук **программно** (синтезатор, retro-bleeps, реактивная музыка), есть
`AudioStreamGenerator`. Это spezialized AudioStream, в который вы пушите PCM-фреймы из кода.
```gdscript
extends AudioStreamPlayer
@onready var playback: AudioStreamGeneratorPlayback
var phase: float = 0.0
var frequency: float = 440.0 # A4
var sample_rate: float = 44100.0
func _ready() -> void:
var gen := AudioStreamGenerator.new()
gen.mix_rate = sample_rate
gen.buffer_length = 0.1 # 100 мс буфер
stream = gen
play()
playback = get_stream_playback()
func _process(_delta: float) -> void:
var frames_to_fill := playback.get_frames_available()
var increment := frequency * TAU / sample_rate
for i in frames_to_fill:
var sample := sin(phase) * 0.3
playback.push_frame(Vector2(sample, sample)) # stereo
phase += increment
if phase > TAU:
phase -= TAU
```
Это синусоидальный генератор тона. На практике:
- **8-bit chiptune-синтез** — square/triangle/noise через простые алгоритмы.
- **Реактивная музыка** — pitch/timbre, реагирующие на gameplay (низкое здоровье → диссонанс).
- **Voice synthesis** — формантный синтез для процедурных NPC-голосов.
- **Audio-visualizers и spectrum analyzers** — обратная задача через `AudioEffectSpectrumAnalyzer`
на bus'е.
Если `_process` отстаёт от sample rate, в буфере появится "пауза" → щелчок в звуке. Для серьёзной
процедурной аудиогенерации лучше делать заполнение из `_physics_process` или вообще из отдельного
потока через `WorkerThreadPool` (см. главу про Threading).
## Импорт и форматы
Поддерживаются: **OGG Vorbis**, **MP3**, **WAV**. Рекомендации:
- **Музыка** — OGG (Vorbis) с loop-points. MP3 хуже по точности loop-точек.
- **Короткие SFX** — WAV (без потерь) или короткие OGG.
- **Voice** — OGG c bitrate 96–128 kbps.
В Import-доке для каждого аудио настраиваются:
- **Loop** — зацикливать ли.
- **Loop Offset / Length** — точки цикла (для seamless music).
- **BPM / Beat Count / Bar Beats** — для AudioStreamInteractive.
Сделайте копию Master с подключённым LowPassFilter, EQ и slight Pitch Shift. Когда игрок под
водой, поменяйте `default_bus` на этот — все 3D-звуки автоматически "приглушатся". Возврат —
обратно на Master.
В следующей главе — UI и Control-узлы.
---
## [Godot] UI — Control-узлы и контейнеры
URL: https://cadmus.page/godot/02-3d/10-ui-control/
Section: 3D-разработка в Godot
Description: Control, anchors, контейнеры, Theme и StyleBox.
В Godot UI — это **Control-узлы**. Это отдельная иерархия наследников `CanvasItem`, со своим
layout-движком, anchors, событиями. В отличие от Unity uGUI / UI Toolkit — здесь одна UI-система,
встроенная.
## Иерархия Control
```
CanvasItem
├── Node2D (для 2D-геймплея)
└── Control (для UI)
├── Container (HBox/VBox/Grid/...)
├── Button / Label / LineEdit / ...
├── Panel / PanelContainer
└── ColorRect / TextureRect
```
Все Control-узлы имеют:
- **position** + **size** — позиция и размер в пикселях.
- **anchors** + **offsets** — привязки к родителю (как Unity uGUI anchors или CSS absolute).
- **size_flags_horizontal / size_flags_vertical** — поведение внутри контейнера (`FILL`,
`EXPAND`, `SHRINK_CENTER`, ...).
- **theme** + **theme_overrides** — стилизация.
## Anchors — главное
Anchor — точка на родителе, к которой "приклеена" сторона текущего Control. Четыре float'а (top,
left, bottom, right) в диапазоне 0..1. Сочетания дают разные поведения:
- `(0, 0, 0, 0)` — все в верхнем левом углу родителя. Размер фиксированный.
- `(0.5, 0.5, 0.5, 0.5)` — центр. Размер фиксированный.
- `(0, 0, 1, 1)` — растягивается на весь родитель.
- `(0, 0, 1, 0)` — растягивается по ширине, прижат к верху. Высота фиксированная.
В редакторе для удобства есть **Anchor presets** — кнопка в верхней панели Control'а: "Top Left",
"Top Right", "Center", "Full Rect" и т.д.
CSS `position: absolute; top/left/right/bottom` + flexbox. Anchors — это absolute точки
привязки, offsets — это `top/left/...` от них.
Concept тот же, что в Unity uGUI RectTransform. В Godot — встроенный в общий `Control`, нет
отдельного RectTransform.
## Контейнеры — авто-layout
Container-узлы автоматически раскладывают детей:
- **HBoxContainer / VBoxContainer** — горизонтальный / вертикальный flexbox.
- **GridContainer** — N колонок.
- **MarginContainer** — padding.
- **CenterContainer** — центрирует содержимое.
- **AspectRatioContainer** — поддерживает соотношение сторон.
- **PanelContainer** — обёртка с фоном (StyleBox).
- **ScrollContainer** — прокручивает содержимое.
- **TabContainer** / **HSplitContainer** / **VSplitContainer** / **HFlowContainer** / **VFlowContainer** — нишевые.
Поведение детей контейнера определяется их `size_flags_horizontal` / `size_flags_vertical`:
- **`FILL`** — заполняет доступное место.
- **`EXPAND`** — также берёт лишнее.
- **`SHRINK_CENTER / BEGIN / END`** — выравнивание без растягивания.
```
VBoxContainer
├── Label "Score"
├── HBoxContainer
│ ├── Button "Pause" (size_flags = EXPAND_FILL)
│ └── Button "Quit" (size_flags = EXPAND_FILL)
└── PanelContainer (size_flags = EXPAND_FILL)
└── Label "Status..."
```
Не позиционируйте UI вручную через `position`. Используйте Containers — layout считается
автоматически на любом разрешении. Manual positioning оставьте для overlay-эффектов (drag
preview, custom drawing).
## Базовые контролы
- **Label** — текст. `text`, `horizontal_alignment`, `vertical_alignment`, `autowrap_mode`.
- **RichTextLabel** — текст с BBCode (цвета, ссылки, изображения, кастомные эффекты).
- **Button** — кнопка. Сигнал `pressed`.
- **TextureButton** — кнопка-картинка с normal/pressed/hover текстурами.
- **LineEdit** / **TextEdit** — однострочный / многострочный ввод.
- **CheckBox** / **CheckButton** / **OptionButton** (combo box).
- **Slider** (`HSlider` / `VSlider`), **SpinBox**, **ProgressBar**.
- **TextureRect** / **ColorRect** — статичная картинка / цветной прямоугольник.
- **NinePatchRect** — растягиваемая текстура с фиксированными углами (для UI-панелей).
## Сигналы UI
Главные:
- **Button.pressed** — клик.
- **Button.toggled(button_pressed)** — для toggle-кнопок.
- **LineEdit.text_changed(new_text)** / **text_submitted(text)** (Enter).
- **Slider.value_changed(value)**.
- **Control.mouse_entered** / **mouse_exited**.
- **Control.focus_entered** / **focus_exited**.
Подключаются через Inspector → Node → Signals или из кода:
```gdscript
@onready var start_button: Button = $StartButton
func _ready() -> void:
start_button.pressed.connect(_on_start_pressed)
func _on_start_pressed() -> void:
get_tree().change_scene_to_file("res://levels/level_01.tscn")
```
## Theme и StyleBox
`Theme` — ресурс со всеми стилями. Применяется к корневому Control'у (или ко всему проекту через
Project Settings → GUI → Theme → Custom).
В Theme описываются:
- **Colors** — цвета для каждого state каждого типа (Button.font_color, Button.font_color_hover).
- **Fonts** — шрифты для каждого типа.
- **Constants** — числа (отступы, padding).
- **Icons** — `Texture2D` для иконок.
- **StyleBoxes** — фоны и рамки.
**StyleBox** — фон и обводка. Виды:
- **StyleBoxFlat** — заливка с радиусом, обводкой, тенью. Главный для современных интерфейсов.
- **StyleBoxTexture** — растягиваемая текстура (как NinePatchRect, но в Theme).
- **StyleBoxLine** — линия (разделители).
- **StyleBoxEmpty** — невидимый.
```gdscript
var box = StyleBoxFlat.new()
box.bg_color = Color("2a2a2a")
box.border_color = Color("6cb6ff")
box.border_width_left = 2
box.border_width_top = 2
box.border_width_right = 2
box.border_width_bottom = 2
box.corner_radius_top_left = 8
box.corner_radius_top_right = 8
box.corner_radius_bottom_left = 8
box.corner_radius_bottom_right = 8
var theme = Theme.new()
theme.set_stylebox("normal", "Button", box)
```
Можно также переопределять styles прямо на конкретном узле через **theme_override_styles** (в
Inspector — секция "Theme Overrides").
## CanvasLayer — UI поверх 3D
Чтобы UI рисовался поверх 3D и не подчинялся камере, оборачивайте его в **CanvasLayer**:
```
GameScene (Node3D)
├── Player
├── World
└── HUD (CanvasLayer)
└── Control
├── HealthBar
└── ScoreLabel
```
Без CanvasLayer Control-узлы попадут в дефолтный canvas, и могут перекрываться 3D-объектами в
зависимости от Z-order.
`CanvasLayer.layer` — числовой порядок (0 = дефолт, выше = сверху).
## UI в 3D-мире — SubViewport
Для UI "в мире" (надписи над врагами, экраны в комнате) — `SubViewport` рендерит UI в текстуру,
которую вы можете применить к MeshInstance3D:
```
EnemyNameTag (Node3D)
├── SubViewport
│ └── Label "Enemy: Goblin"
└── MeshInstance3D (Quad)
└── material_override.albedo_texture = ViewportTexture(SubViewport)
```
Это аналог Unity Canvas в World Space, через ViewportTexture-механизм.
В следующей главе — PackedScene и Resource.
---
## [Godot] PackedScene и Resource
URL: https://cadmus.page/godot/02-3d/11-packedscene-resource/
Section: 3D-разработка в Godot
Description: Универсальный контейнер сцены/префаба и data-only ассеты.
## PackedScene — сцена и префаб в одном
Главная архитектурная особенность Godot: **сцена и префаб — одна и та же сущность**. Файл
`.tscn` хранит дерево узлов; этот файл можно открыть как корневую сцену проекта или
инстанциировать как ребёнка в другую сцену. **Внутри Engine** оба варианта — `PackedScene`-ресурс.
```
res://
├── scenes/
│ ├── main.tscn ← главная сцена уровня
│ ├── player.tscn ← "префаб" игрока
│ ├── enemy.tscn ← "префаб" врага
│ └── ui/
│ └── hud.tscn ← UI как сцена
```
В Inspector у `Player.tscn` те же узлы и компоненты, что и у `Main.tscn`. Любая сцена — это
шаблон, который можно встраивать.
Это React: один и тот же `` рендерится и как страница `/users/me`, и как виджет
в списке. Никакого разделения "это компонент, а это страница".
В Unity Scene (`.unity`) и Prefab (`.prefab`) — два разных формата. В Godot — один.
Соответственно, "вложенные префабы" — это просто вложенные сцены.
## Инстанцирование
В редакторе: правый клик на узел → Instantiate Child Scene → выбрать `.tscn`. Или drag-and-drop
из FileSystem в Scene.
В коде:
```gdscript
const ENEMY_SCENE := preload("res://scenes/enemy.tscn")
func spawn_enemy(at: Vector3) -> void:
var enemy = ENEMY_SCENE.instantiate() as Node3D
enemy.global_position = at
add_child(enemy)
```
`instantiate()` создаёт корневой узел сцены и все её дети. `preload()` загружает на этапе компиляции
скрипта (статично), `load()` — в runtime.
## Inherited Scenes — мощнее Prefab Variants
В Godot можно создать сцену, **унаследованную** от другой. Это даёт ту же модель, что Prefab Variant
в Unity, но без отдельного механизма.
1. Создайте `Enemy.tscn` — базовый враг.
2. New Inherited Scene → выберите `Enemy.tscn`.
3. Сохраните как `EnemyElite.tscn` — это inherited scene.
4. В Inspector у любого узла переопределяете свойства (выделятся жирным/синим). Можно даже
добавлять новые узлы.
Изменения базового `Enemy.tscn` пропагируются на `EnemyElite.tscn`, кроме override'ов.
Сцена может содержать другую сцену как ребёнка, а та — третью. Глубина не ограничена.
Получается чёткая иерархия "уровень → комната → дверь", где каждая дверь — своя `.tscn`
с собственной логикой. Изменили `door.tscn` — изменились все двери проекта.
## Editable Children
По умолчанию вложенная сцена выглядит как один узел (корень) — дети скрыты, "запечатаны". Чтобы
переопределить дочерний узел вложенной сцены, нажмите Right Click → **Editable Children**. Тогда
дети станут видны и доступны для override.
## Resource — data-only ассеты
`Resource` — базовый класс для всего, что **не узел**. Сериализуется в `.tres` (текст) или `.res`
(бинарный). Подходит для:
- Конфигов (статы оружия, рецепты).
- Базы данных (список предметов).
- Curve / Gradient / Material / Mesh — всё это Resource.
- Custom data объекты в вашей игре.
Resource — **reference-counted**: автоматически удаляется, когда нет ссылок.
## Custom Resource — аналог ScriptableObject
Создаёте GD-скрипт, наследующий `Resource`, с `@export`-полями и `class_name`:
```gdscript
# weapon_data.gd
class_name WeaponData extends Resource
@export var display_name: String = "Pistol"
@export var icon: Texture2D
@export var damage: int = 10
@export var fire_rate: float = 0.4
@export var fire_sound: AudioStream
@export var projectile_scene: PackedScene
```
Теперь в FileSystem → New Resource → WeaponData → сохранить как `pistol.tres`, `shotgun.tres`.
Каждый — независимый ассет с уникальными значениями.
Использование:
```gdscript
extends Node3D
class_name Weapon
@export var data: WeaponData
@onready var muzzle: Marker3D = $Muzzle
var _next_fire_time: float = 0.0
func try_fire() -> void:
var now = Time.get_ticks_msec() / 1000.0
if now < _next_fire_time:
return
_next_fire_time = now + data.fire_rate
var sfx = AudioStreamPlayer3D.new()
sfx.stream = data.fire_sound
add_child(sfx)
sfx.play()
sfx.finished.connect(sfx.queue_free)
var bullet = data.projectile_scene.instantiate()
bullet.global_transform = muzzle.global_transform
get_tree().current_scene.add_child(bullet)
```
В Inspector перетягиваете `pistol.tres` в поле `data` — оружие стреляет как пистолет. Меняете
на `shotgun.tres` — как дробовик. Изменения в коде не нужны.
По умолчанию, если у двух узлов в Inspector один `WeaponData.tres`, и вы поменяете значение
в коде (`weapon.data.damage = 5`), это **изменит ассет на диске** в редакторе. Для рантайма
это норма, в редакторе — нежелательно (испортите ассет случайно).
Если нужен отдельный экземпляр, делайте `data.duplicate()` или включите свойство
`resource_local_to_scene = true`.
## Built-in vs External resources
В `.tscn`-файле сцена может содержать:
- **External resources** — ссылки на отдельные `.tres`-файлы.
- **Built-in resources** — встроенные внутрь сцены (когда вы создаёте ресурс прямо в Inspector
через `[empty]` → New).
Built-in удобно для одноразовых ресурсов конкретной сцены (например, специфический материал
двери). External — для переиспользуемых (одна `WeaponData.tres` на 10 врагов).
## Сравнение с Unity
| Unity | Godot |
|---|---|
| Scene (.unity) | PackedScene (.tscn) — корневая сцена |
| Prefab (.prefab) | PackedScene (.tscn) — инстанцированная |
| Prefab Variant | Inherited Scene |
| Nested Prefab | Просто вложенная сцена |
| Prefab Override | Editable Children + per-property override |
| ScriptableObject | Custom Resource (.tres) |
В следующей главе соберём всё вместе — практический FPS-контроллер на CharacterBody3D.
---
## [Godot] Практика — контроллер от первого лица
URL: https://cadmus.page/godot/02-3d/12-fps-controller/
Section: 3D-разработка в Godot
Description: CharacterBody3D, move_and_slide, RayCast3D — играбельный FPS-каркас.
Собираем играбельного FPS-персонажа: ходит, смотрит мышью, прыгает, стреляет лучом. Используем
**CharacterBody3D** (аналог Unity CharacterController, только с более фичастым `move_and_slide`).
## Иерархия сцены
```
Player (CharacterBody3D) ← скрипт player.gd
├── CollisionShape3D ← shape: CapsuleShape3D (height=2, radius=0.4)
├── MeshInstance3D (optional) ← визуальный mesh (часто скрыт в FPS — не видим себя)
└── Head (Node3D) ← на высоте глаз, например y=1.7
└── Camera3D ← current = true, fov = 75
└── AimRay (RayCast3D) ← target_position = (0, 0, -100)
```
Разделение Head ↔ Player важно: yaw (горизонтальный поворот) применяется к Player'у (двигает всё
тело и направление движения), pitch (вертикаль) — только к Head (чтобы коллайдер не наклонялся).
## Input Map
В Project Settings → Input Map создайте:
- `move_forward` (W, ↑)
- `move_back` (S, ↓)
- `move_left` (A, ←)
- `move_right` (D, →)
- `jump` (Space)
- `fire` (Mouse Left)
- `ui_cancel` (Escape) — обычно уже есть по умолчанию
## player.gd — главный скрипт
```gdscript
extends CharacterBody3D
@export_group("Movement")
@export var speed: float = 5.5
@export var jump_velocity: float = 5.0
@export_group("Look")
@export var sensitivity: float = 0.0025
@export var max_pitch: float = 1.45
@export_group("Combat")
@export var fire_damage: int = 10
@export var impact_scene: PackedScene # эффект попадания (опционально)
@onready var head: Node3D = $Head
@onready var camera: Camera3D = $Head/Camera3D
@onready var aim_ray: RayCast3D = $Head/Camera3D/AimRay
func _ready() -> void:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
Input.mouse_mode = (
Input.MOUSE_MODE_VISIBLE
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED
else Input.MOUSE_MODE_CAPTURED
)
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
rotate_y(-event.relative.x * sensitivity)
head.rotate_x(-event.relative.y * sensitivity)
head.rotation.x = clamp(head.rotation.x, -max_pitch, max_pitch)
if event.is_action_pressed("fire") and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
fire()
func _physics_process(delta: float) -> void:
apply_gravity(delta)
handle_jump()
handle_move()
move_and_slide()
func apply_gravity(delta: float) -> void:
if not is_on_floor():
velocity += get_gravity() * delta
func handle_jump() -> void:
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_velocity
func handle_move() -> void:
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
if direction:
velocity.x = direction.x * speed
velocity.z = direction.z * speed
else:
velocity.x = move_toward(velocity.x, 0, speed)
velocity.z = move_toward(velocity.z, 0, speed)
func fire() -> void:
aim_ray.force_raycast_update() # форсируем обновление в кадре
if not aim_ray.is_colliding():
return
var hit = aim_ray.get_collider()
var hit_point = aim_ray.get_collision_point()
var hit_normal = aim_ray.get_collision_normal()
# Нанести урон, если есть метод
if hit.has_method("take_damage"):
hit.take_damage(fire_damage)
# Эффект попадания (опционально)
if impact_scene:
var impact = impact_scene.instantiate()
impact.global_position = hit_point
impact.look_at(hit_point + hit_normal, Vector3.UP)
get_tree().current_scene.add_child(impact)
```
`get_gravity()` возвращает `Vector3(0, -9.8, 0)` по умолчанию (настраивается в Project Settings →
Physics → 3D). Это удобно: одно место регулирует гравитацию всей игры, плюс при изменении
значение применяется ко всем телам.
## health.gd — получатель урона
Простой компонент здоровья на любом узле:
```gdscript
class_name Health extends Node
signal damaged(amount: int)
signal died
@export var max_hp: int = 100
var hp: int
func _ready() -> void:
hp = max_hp
func take_damage(amount: int) -> void:
if hp <= 0:
return
hp = max(0, hp - amount)
damaged.emit(amount)
if hp == 0:
died.emit()
```
Как использовать: добавьте `Health` как **дочерний узел** к врагу, а в `take_damage`-обработчик
врага делегируйте на него:
```gdscript
# enemy.gd на корне врага
extends StaticBody3D
@onready var health: Health = $Health
func _ready() -> void:
health.died.connect(_on_died)
func take_damage(amount: int) -> void:
health.take_damage(amount)
func _on_died() -> void:
queue_free()
```
Тогда `aim_ray.get_collider().has_method("take_damage")` будет работать — у `Enemy`-узла есть метод.
## Тестовая сцена
Соберите минимальный уровень:
1. **Ground** — `StaticBody3D` + `CollisionShape3D` (BoxShape3D 50×0.1×50) + `MeshInstance3D` (PlaneMesh) для визуала.
2. **Walls** — несколько StaticBody3D-кубов по периметру.
3. **Targets** — 5–10 StaticBody3D с компонентом `enemy.gd` и `Health` ребёнком.
4. **DirectionalLight3D** — основной свет, `light_energy = 1.0`, тени включены.
5. **WorldEnvironment** — с ambient light и sky.
6. **Player.tscn** — инстанциируйте на сцене.
Запускаете (F5) — должен ходить, прыгать, мышью смотреть, ЛКМ стрелять.
## Что улучшить
- **Crouch** — присесть: уменьшить высоту коллайдера и опустить камеру.
- **Sprint** — кнопка Shift + множитель скорости.
- **Headbob** — лёгкое покачивание камеры при ходьбе (`head.position.y = sin(time * walk_freq) * walk_amp`).
- **Footstep audio** — AudioStreamPlayer3D на узле Player, играть при движении на полу.
- **Recoil** — лёгкий импульс камеры вверх при стрельбе через анимацию или код.
- **Reload, Magazine, Ammo HUD** — следующая итерация с UI.
- **Coyote time / jump buffer** — как в Unity TPS-практикуме, чтобы прыжок ощущался "прощающе".
- **Animator** — `AnimationTree` со state machine для анимаций оружия.
Сравните с FPS-controller'ом из Unity-главы: похожая функциональность, но в Godot короче за
счёт `move_and_slide`, встроенного RayCast3D и Input Map. Это типично для Godot — каркас
поднимается быстро.
В следующей главе — Navigation (NavigationServer3D, NavigationAgent3D).
---
## [Godot] Навигация — NavigationServer и AI
URL: https://cadmus.page/godot/02-3d/13-navigation/
Section: 3D-разработка в Godot
Description: NavigationRegion3D, NavigationAgent3D — поиск пути и преследующий враг.
Navigation в Godot 4 — это **NavigationServer3D** (низкоуровневый сервер) и три высокоуровневых
узла: **NavigationRegion3D**, **NavigationAgent3D**, **NavigationLink3D**.
## Идея
NavigationMesh — упрощённая полигональная "карта проходимости". Запекается из коллизий сцены.
Агенты ищут путь по этой карте (A* под капотом).
```
World
├── Ground (StaticBody3D + MeshInstance3D)
├── Walls / Platforms
└── NavigationRegion3D
└── navigation_mesh: NavigationMesh (запекается)
Enemy
├── (модель + коллайдер)
└── NavigationAgent3D (target_position = player_position)
```
## Запекание NavigationMesh
1. Соберите статичную геометрию.
2. Добавьте узел **NavigationRegion3D** на сцену.
3. В Inspector создайте новый ресурс **NavigationMesh** на этом узле.
4. Кнопка **Bake NavigationMesh** в верхней панели → запекание.
В **Geometry → Source Geometry Mode** на NavigationMesh выбирается, из чего собирать меш:
- **`SOURCE_GEOMETRY_ROOT_NODE_CHILDREN`** — обрабатывает детей NavigationRegion3D
(положите внутрь региона статичную геометрию).
- **`SOURCE_GEOMETRY_GROUPS_WITH_CHILDREN`** — узлы из заданной группы + их дети.
- **`SOURCE_GEOMETRY_GROUPS_EXPLICIT`** — только узлы из группы.
Параметры NavigationMesh:
- **`agent_radius`** — радиус агента (отступ от стен).
- **`agent_height`** — высота агента.
- **`agent_max_climb`** — макс высота ступеньки, которую агент перешагнёт.
- **`agent_max_slope`** — макс угол склона в градусах.
- **`cell_size`** / **`cell_height`** — точность мешa.
Если у вас несколько NavigationRegion3D — они автоматически соединяются на границах. Удобно
для модульных уровней: каждая комната — свой регион.
## NavigationAgent3D — движение по сетке
`NavigationAgent3D` — дочерний узел тела, которое должно ходить по мешу. Не двигает само,
рассчитывает следующую точку пути:
```gdscript
extends CharacterBody3D
@export var speed: float = 4.0
@export var target: Node3D
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
func _ready() -> void:
nav_agent.path_desired_distance = 0.5
nav_agent.target_desired_distance = 1.0
func _physics_process(delta: float) -> void:
if target == null:
return
nav_agent.target_position = target.global_position
if nav_agent.is_navigation_finished():
return
var next_point := nav_agent.get_next_path_position()
var direction := (next_point - global_position).normalized()
velocity.x = direction.x * speed
velocity.z = direction.z * speed
if not is_on_floor():
velocity += get_gravity() * delta
move_and_slide()
```
Главные методы:
- **`get_next_path_position()`** — куда идти на этом тике.
- **`is_navigation_finished()`** — путь завершён.
- **`is_target_reachable()`** — есть ли путь до цели.
- **`distance_to_target()`** — оставшееся расстояние.
Сигналы:
- **`target_reached`** — достигли цели.
- **`waypoint_reached(details)`** — достигли промежуточной точки.
- **`navigation_finished`** — путь полностью пройден.
- **`velocity_computed(safe_velocity)`** — если включён avoidance.
## Avoidance — расступание агентов
Если на сцене много агентов, идущих в одну точку, без расступания они сольются "стопкой". Включите
**`avoidance_enabled = true`** на NavigationAgent3D — заработает RVO2 (reciprocal velocity
obstacles).
```gdscript
nav_agent.avoidance_enabled = true
nav_agent.radius = 0.6 # радиус расступания
nav_agent.neighbor_distance = 5.0 # видим соседей в этом радиусе
nav_agent.velocity_computed.connect(_on_safe_velocity)
func _on_safe_velocity(safe_vel: Vector3) -> void:
# Используйте safe_vel вместо raw velocity
velocity.x = safe_vel.x
velocity.z = safe_vel.z
move_and_slide()
func _physics_process(delta: float) -> void:
# ...
nav_agent.set_velocity(desired_velocity) # отдаём желаемую — получаем safe через сигнал
```
## NavigationLink3D — прыжки и телепорты
NavigationMesh не поймёт "тут можно прыгнуть через пропасть". Для этого — **NavigationLink3D**:
прямая связь между двумя точками. Агенты идут по ней как по обычному ребру графа.
В Inspector:
- **`start_position`** + **`end_position`** — концы линка.
- **`bidirectional`** — двусторонний или нет.
При прохождении такого ребра агент пройдёт *геометрически прямо* — это значит, что вам нужно
самостоятельно сделать "прыжок" (анимация + физика) в скрипте, отловив проход через линк.
## NavigationObstacle3D — динамические препятствия
Двери, ящики, поваленные деревья — динамические препятствия. Два режима:
- **Без `affect_navigation_mesh`** — просто отталкивает агентов через avoidance.
- **С `affect_navigation_mesh = true`** — "вырезает" дыру в navmesh (требует rebake региона на лету).
Дорого, но точно.
## Простой враг с state machine
Полный пример из главы про Unity NavMesh, переписанный на Godot:
```gdscript
extends CharacterBody3D
class_name EnemyChaser
enum State { PATROL, CHASE, ATTACK }
@export var patrol_points: Array[Node3D] = []
@export var player: Node3D
@export var speed: float = 3.0
@export var sight_range: float = 12.0
@export var attack_range: float = 2.0
@export var attack_cooldown: float = 1.2
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
var state: State = State.PATROL
var patrol_index: int = 0
var next_attack_time: float = 0.0
func _ready() -> void:
if patrol_points.size() > 0:
nav_agent.target_position = patrol_points[0].global_position
func _physics_process(delta: float) -> void:
var dist_to_player = global_position.distance_to(player.global_position)
match state:
State.PATROL:
if dist_to_player < sight_range and _has_line_of_sight():
state = State.CHASE
elif nav_agent.is_navigation_finished():
patrol_index = (patrol_index + 1) % patrol_points.size()
nav_agent.target_position = patrol_points[patrol_index].global_position
State.CHASE:
nav_agent.target_position = player.global_position
if dist_to_player < attack_range:
state = State.ATTACK
elif dist_to_player > sight_range * 1.5:
state = State.PATROL
State.ATTACK:
nav_agent.target_position = global_position # стоим
var now = Time.get_ticks_msec() / 1000.0
if now >= next_attack_time:
_attack()
next_attack_time = now + attack_cooldown
if dist_to_player > attack_range + 0.5:
state = State.CHASE
_move(delta)
func _move(delta: float) -> void:
if state != State.ATTACK and not nav_agent.is_navigation_finished():
var next_point = nav_agent.get_next_path_position()
var dir = (next_point - global_position).normalized()
velocity.x = dir.x * speed
velocity.z = dir.z * speed
else:
velocity.x = move_toward(velocity.x, 0, speed)
velocity.z = move_toward(velocity.z, 0, speed)
if not is_on_floor():
velocity += get_gravity() * delta
move_and_slide()
func _has_line_of_sight() -> bool:
var space = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(
global_position + Vector3.UP, player.global_position
)
query.exclude = [self]
var hit = space.intersect_ray(query)
return hit.is_empty() or hit.collider == player
func _attack() -> void:
if player.has_method("take_damage"):
player.take_damage(10)
```
Обратите внимание на пороги: вход в CHASE на `sight_range`, выход — на `sight_range * 1.5`.
Это hysteresis, который не даёт врагу "дёргаться" между состояниями на границе. Тот же
приём, что и в Unity-главе.
В следующей главе — частицы.
---
## [Godot] Частицы — GPUParticles3D и CPUParticles3D
URL: https://cadmus.page/godot/02-3d/14-particles/
Section: 3D-разработка в Godot
Description: ParticleProcessMaterial, эмиттеры, sub-emitters, collision.
В Godot два узла для частиц с похожим API:
- **GPUParticles3D** — на GPU, до миллионов частиц.
- **CPUParticles3D** — на CPU, fallback для слабого железа и веба.
| Параметр | GPUParticles3D | CPUParticles3D |
|---|---|---|
| Где вычисляется | GPU (compute) | CPU |
| Максимум частиц | Миллионы | ~10 000 |
| Compatibility-рендер | **Работают с оговорками** (требуется compute) | Работают везде |
| Web-цель | Ограниченно (зависит от WebGL) | Работают |
| Per-particle GDScript-доступ | Нет | Да |
Грубое правило: **GPU где можно, CPU где обязательно**.
## ParticleProcessMaterial
Главное отличие от Unity Particle System: **поведение частиц задаётся не в инспекторе узла, а в
отдельном ресурсе** `ParticleProcessMaterial`. Это удобно — можно сохранить, переиспользовать,
override в инстансах.
```gdscript
@onready var particles: GPUParticles3D = $GPUParticles3D
func _ready() -> void:
var mat: ParticleProcessMaterial = particles.process_material
mat.initial_velocity_min = 5.0
mat.initial_velocity_max = 10.0
mat.gravity = Vector3(0, -3, 0)
```
Главные секции ParticleProcessMaterial:
- **Direction** + **spread** — направление выброса.
- **Initial Velocity** (min/max) — стартовая скорость.
- **Gravity** — на каждую частицу.
- **Linear / Angular / Radial Accel** — постоянные ускорения.
- **Damping** — гашение скорости.
- **Color** + **Color Curve** — цвет и кривая по жизни.
- **Scale** + **Scale Curve** — размер.
- **Hue Variation**, **Anim Speed / Offset** — для текстурных листов.
- **Emission Shape** — `POINT`, `SPHERE`, `BOX`, `POINTS`, `DIRECTED_POINTS`, `RING`.
## GPUParticles3D — узел
Главные свойства на самом узле:
- **`amount`** — максимальное число одновременных частиц.
- **`lifetime`** — время жизни в секундах.
- **`one_shot`** — одноразовый выстрел или непрерывный.
- **`explosiveness`** — 0..1, насколько "залпом" вылетают (0 = равномерно, 1 = все одновременно).
- **`emitting`** — испускать сейчас.
- **`fixed_fps`** — фиксированная частота обновления (0 = с FPS).
- **`local_coords`** — позиции частиц локальные или мировые.
```gdscript
# Запуск одноразового взрыва
func boom(at: Vector3) -> void:
var explosion = explosion_scene.instantiate()
explosion.global_position = at
get_tree().current_scene.add_child(explosion)
explosion.emitting = true # включить эмиссию
# Автоудаление
func _on_finished() -> void:
queue_free()
```
Сигнал **`finished`** срабатывает после `lifetime` (для `one_shot = true`).
## Trails (следы)
Каждая частица может оставлять trail — узкую ленту, идущую за ней:
- **`trail_enabled = true`** на узле GPUParticles3D.
- **`trail_lifetime`** — длина следа в секундах.
- Mesh trail (если хотите кастомный mesh) — через `draw_pass_1.mesh`.
Полезно для пуль-трассеров, спецэффектов магии.
## Sub-emitters
GPUParticles3D могут испускать **другие частицы** при определённых событиях:
```
Explosion (GPUParticles3D — главный)
└── SubEmitter (GPUParticles3D — sub)
```
Виды sub-emitter activation:
- **CONSTANT** — постоянно от каждой родительской частицы.
- **AT_END** — в момент смерти родителя.
- **AT_COLLISION** — при касании коллайдера (требует collision_enabled).
Пример: ракета (родительская частица) — при касании земли испускает 50 искр (sub-emitter).
## Collision с миром
Чтобы частицы реагировали на коллизии:
1. `ParticleProcessMaterial.collision_mode = COLLISION_RIGID` (отскакивают) или `COLLISION_HIDE_ON_CONTACT`.
2. Добавьте на сцену **GPUParticlesCollisionSphere3D**, **GPUParticlesCollisionBox3D**,
**GPUParticlesCollisionHeightField3D** или **GPUParticlesCollisionSDF3D** — это специальные
коллайдеры **только для частиц**, отдельные от физических.
GPU-частицы НЕ сталкиваются с обычными StaticBody3D / Collider'ами. Они используют отдельную
систему `GPUParticlesCollision*` узлов. Это сделано для производительности GPU-симуляции.
## Visibility AABB
GPUParticles3D имеют **visibility_aabb** — bounding box, в пределах которого они считаются
видимыми. Если AABB вне камеры — частицы не симулируются.
По умолчанию AABB маленький (вокруг эмиттера) и **частицы, улетевшие далеко, могут "пропадать"**.
Решение: вручную задать большой AABB или нажать **Generate AABB** в редакторе (Godot подберёт по
симуляции).
```gdscript
particles.visibility_aabb = AABB(Vector3(-20, -20, -20), Vector3(40, 40, 40))
```
## ProcessMaterial vs ShaderMaterial для частиц
`ParticleProcessMaterial` — стандартный, покрывает 90% задач. Если нужно нестандартное поведение
(specific вихрь, attractor по сложной формуле), пишите свой `ShaderMaterial` с
`shader_type particles`:
```glsl
shader_type particles;
render_mode keep_data;
uniform vec3 swirl_center;
uniform float swirl_strength;
void process() {
vec3 to_center = swirl_center - TRANSFORM[3].xyz;
float dist = length(to_center);
vec3 tangent = normalize(cross(to_center, vec3(0, 1, 0)));
VELOCITY += tangent * swirl_strength * (1.0 / max(dist, 0.5));
TRANSFORM[3].xyz += VELOCITY * DELTA;
}
```
Это процессинг-шейдер. Рендерится частица отдельным draw pass материалом (через
`draw_pass_1` на GPUParticles3D).
## CPUParticles3D — когда полезно
- **Web-сборка с Compatibility-рендером** — GPU-симуляция там работает не везде (особенно на
WebGL без compute).
- **Per-particle логика на GDScript** — если хотите менять отдельные частицы в коде.
- **Старое железо**, где compute shader недоступен.
API почти идентично, но настройки — прямо на узле (без отдельного ProcessMaterial).
## Профайлинг
GPU-частицы — одна из самых нагружающих фич. В **Debugger → Profiler → Visual** смотрите время
prepare/process. Если 10000+ частиц на слабом телефоне — режьте `amount` и `lifetime`.
В следующей главе — мультиплеер.
---
## [Godot] Мультиплеер — MultiplayerAPI и RPC
URL: https://cadmus.page/godot/02-3d/15-multiplayer/
Section: 3D-разработка в Godot
Description: ENet/WebSocket/WebRTC peers, RPC, MultiplayerSpawner и Synchronizer.
Godot имеет встроенный high-level мультиплеер: **MultiplayerAPI** + транспорты + узлы Spawner/Synchronizer.
Это аналог Unity Netcode for GameObjects.
## Архитектура
`MultiplayerAPI` работает поверх `MultiplayerPeer`. Транспорты:
- **ENetMultiplayerPeer** — UDP, основной для нативных платформ.
- **WebSocketMultiplayerPeer** — TCP/WebSocket, единственный путь для веб-таргета.
- **WebRTCMultiplayerPeer** — peer-to-peer, NAT traversal через STUN/TURN.
- **OfflineMultiplayerPeer** — заглушка для single-player.
Стандартная модель — **authoritative server**: один peer ID=1 (сервер), остальные — клиенты с
ID >= 2.
## Старт сервера и клиента
```gdscript
extends Node
const PORT := 7777
const MAX_PEERS := 4
func host_game() -> Error:
var peer = ENetMultiplayerPeer.new()
var err = peer.create_server(PORT, MAX_PEERS)
if err != OK:
return err
multiplayer.multiplayer_peer = peer
print("Server up on port ", PORT)
return OK
func join_game(ip: String) -> Error:
var peer = ENetMultiplayerPeer.new()
var err = peer.create_client(ip, PORT)
if err != OK:
return err
multiplayer.multiplayer_peer = peer
print("Connecting to ", ip)
return OK
```
`multiplayer` — встроенный геттер MultiplayerAPI на любом узле.
Сигналы MultiplayerAPI:
- **`peer_connected(id)`** / **`peer_disconnected(id)`** — на сервере для каждого клиента.
- **`connected_to_server`** / **`connection_failed`** / **`server_disconnected`** — на клиенте.
```gdscript
func _ready() -> void:
multiplayer.peer_connected.connect(_on_peer_joined)
multiplayer.peer_disconnected.connect(_on_peer_left)
func _on_peer_joined(id: int) -> void:
print("Peer ", id, " joined")
if multiplayer.is_server():
spawn_player(id)
```
## RPC — удалённые вызовы
GDScript использует annotation `@rpc` на функциях, которые могут вызываться удалённо:
```gdscript
extends Node3D
# from_who: any_peer | authority
# transfer: reliable | unreliable | unreliable_ordered
# call_local: вызывать ли локально при удалённом вызове
@rpc("any_peer", "reliable", "call_local")
func shoot(direction: Vector3) -> void:
spawn_bullet(direction)
# Сервер сообщает всем (call → этот метод сработает на каждом клиенте)
@rpc("authority", "reliable")
func play_explosion_sound(at: Vector3) -> void:
var player := AudioStreamPlayer3D.new()
player.stream = preload("res://audio/explosion.ogg")
player.global_position = at
add_child(player)
player.play()
player.finished.connect(player.queue_free)
```
Вызов:
```gdscript
# Из клиента → серверу (если remote method имеет "authority"):
rpc("shoot", direction)
# Только на конкретный peer:
rpc_id(target_peer_id, "play_explosion_sound", pos)
# Конкретно на сервер:
rpc_id(1, "do_something")
```
Параметры `@rpc`:
- **From who**: `any_peer` (любой клиент может вызвать) или `authority` (только владелец узла).
- **Transfer**: `reliable` (TCP-стиль), `unreliable` (UDP без гарантий), `unreliable_ordered`.
- **Call local**: вызвать ли локально кроме remote.
- **Channel**: номер канала (для параллельной отправки разных типов сообщений).
В authoritative-модели сервер — источник правды. Не доверяйте параметрам RPC от клиента
(любой клиент может прислать `damage = 99999`). Сервер должен валидировать: проверить
расстояние выстрела, кулдаун, права.
## Multiplayer Authority
У каждого узла есть **authority** — peer ID, который "владеет" узлом. По умолчанию — 1 (сервер).
Можно передать клиенту:
```gdscript
# На сервере: дать peer'у управление его игроком
var player = player_scene.instantiate()
player.name = str(peer_id) # имя для уникальности
add_child(player, true)
player.set_multiplayer_authority(peer_id)
```
Внутри скрипта:
```gdscript
func _physics_process(delta: float) -> void:
if not is_multiplayer_authority():
return # только владелец двигает себя
# ... handle input
```
## MultiplayerSpawner
Узел, который **автоматически реплицирует спавн** дочерних узлов от сервера к клиентам. Не нужно
вручную слать RPC "create_enemy_at".
```
World
├── Players (Node) ← дети спавнятся для каждого игрока
│ └── MultiplayerSpawner ← spawn_path = "../Players"
│ spawnable_scenes = [Player.tscn]
```
На сервере вы делаете `players.add_child(player)`. Спавнер ловит это и отправляет всем клиентам,
которые создают такой же узел у себя локально.
## MultiplayerSynchronizer
Декларативная синхронизация properties. Прикрепляете к узлу и в Inspector указываете список
properties (например, `position`, `rotation`, `hp`). MultiplayerAPI автоматически шлёт изменения
от authority к остальным.
```
Player (CharacterBody3D, authority = peer_id)
├── ...
└── MultiplayerSynchronizer
Replication Config:
:position [Always]
:rotation:y [Always]
hp [On Change]
state [On Change]
```
Replication Mode:
- **Always** — каждый тик.
- **On Change** — при изменении значения.
- **Never** — не реплицировать (для visibility-фильтрации).
Это убирает 80% кода ручной синхронизации, который пришлось бы писать на RPC. Аналог Unity
NetworkVariable, но декларативный.
## Сравнение с Unity Netcode
| Концепция | Unity NGO | Godot |
|---|---|---|
| Сетевая сущность | NetworkObject | Узел с authority |
| Спавн | Spawn() через NetworkObject | MultiplayerSpawner |
| Sync property | `NetworkVariable
` | MultiplayerSynchronizer |
| RPC (clent→server) | [ServerRpc] | `@rpc("any_peer")` + `rpc_id(1, ...)` |
| RPC (server→clients) | [ClientRpc] | `@rpc("authority")` |
| Транспорт | UnityTransport | ENetMultiplayerPeer |
| Relay | Unity Relay | Своё (или сторонние, например Nakama) |
| Match | Unity Lobby | Своё |
## Веб
Только через **WebSocketMultiplayerPeer**:
```gdscript
var peer = WebSocketMultiplayerPeer.new()
peer.create_server(8080)
# или peer.create_client("ws://example.com:8080")
multiplayer.multiplayer_peer = peer
```
ENet не работает в браузере. WebRTC требует STUN/TURN-серверов для NAT traversal.
## Подводные камни
1. **Не путайте локальный multiplayer с сетевым.** Split-screen — другая задача, через viewports
и `PlayerInput` источники.
2. **`call_local`** — забывают, в итоге локально метод не срабатывает. Если хотите, чтобы
"и у себя выполнилось, и удалённо" — включайте.
3. **MultiplayerSynchronizer** требует, чтобы узел существовал у всех клиентов. Используйте
совместно с Spawner.
4. **Order matters** при спавне: если сервер `add_child(player)` после `set_multiplayer_authority`,
то authority успеет реплицироваться. Иначе наоборот.
В следующей главе — загрузка ресурсов и Resource Loader.
---
## [Godot] Загрузка ресурсов — preload, load, threaded
URL: https://cadmus.page/godot/02-3d/16-resource-loading/
Section: 3D-разработка в Godot
Description: Как Godot загружает ассеты — синхронно, асинхронно, с прогрессом.
В Godot нет отдельной системы вроде Unity Addressables — встроенный механизм покрывает большинство
сценариев через несколько API.
## Три способа загрузки
### 1. preload — статичная, на этапе парсинга
```gdscript
const ENEMY_SCENE := preload("res://scenes/enemy.tscn")
const FIRE_SOUND := preload("res://audio/fire.ogg")
func spawn() -> void:
var enemy = ENEMY_SCENE.instantiate()
add_child(enemy)
```
`preload` — это **директива компилятора** GDScript. Ресурс загружается, когда сам скрипт
загружается (один раз, кешируется). Самый быстрый способ при инстанцировании.
**Плюс**: ноль пауз в рантайме.
**Минус**: ассет всегда в памяти, пока скрипт жив. Не подходит для тяжёлой геометрии,
которая нужна не всегда.
### 2. load — динамическая, синхронная
```gdscript
func load_level(name: String) -> void:
var path = "res://levels/%s.tscn" % name
var scene = load(path) as PackedScene
if scene == null:
push_error("Failed to load %s" % path)
return
get_tree().change_scene_to_packed(scene)
```
`load` блокирует поток, пока не загрузит. Подходит для лёгких ресурсов в моменты, где пауза не
заметна.
### 3. ResourceLoader.load_threaded_request — асинхронная
Для тяжёлых сцен/моделей:
```gdscript
var loading_path: String
func start_load(scene_path: String) -> void:
loading_path = scene_path
ResourceLoader.load_threaded_request(scene_path)
func _process(_delta: float) -> void:
if loading_path.is_empty():
return
var progress := []
var status = ResourceLoader.load_threaded_get_status(loading_path, progress)
match status:
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
$LoadingBar.value = progress[0] * 100.0 # 0..1
ResourceLoader.THREAD_LOAD_LOADED:
var resource = ResourceLoader.load_threaded_get(loading_path)
_on_loaded(resource)
loading_path = ""
ResourceLoader.THREAD_LOAD_FAILED:
push_error("Load failed: ", loading_path)
loading_path = ""
func _on_loaded(scene: PackedScene) -> void:
get_tree().change_scene_to_packed(scene)
```
`progress` — array, где `progress[0]` это float 0..1 прогресса. Можно строить настоящий
loading screen.
Это аналог Unity Addressables `LoadAssetAsync`: пока игрок проходит уровень, в фоне грузим
следующий. По достижении 100% — мгновенный переход.
## Подсказка типа
`load` и `load_threaded_get` возвращают `Resource`. Чтобы получить типизированный объект,
используйте `as`:
```gdscript
var scene := load(path) as PackedScene
var texture := load(tex_path) as Texture2D
var data := load(cfg_path) as WeaponData
```
Если cast не удался — будет `null`.
## PCK-пакеты
Godot позволяет упаковывать ассеты в **PCK-файлы** (`.pck` — Pack file) и загружать в рантайме.
Используется для DLC, hot updates, mod-поддержки:
```gdscript
# Загрузить PCK в текущий проект
if not ProjectSettings.load_resource_pack("res://dlc/level_pack_2.pck"):
push_error("Failed to load DLC pack")
else:
# Теперь доступны ассеты из pack по их путям
var dlc_scene = load("res://dlc_levels/extra_01.tscn")
```
PCK создаётся через **Project → Export** в режиме "Export PCK/Zip" (без бинарника движка).
Code splitting: `import('./heavy-module')` асинхронно — браузер грузит chunk по сети.
`load_threaded_request` — то же концептуально, но для ассетов.
Unity Addressables ↔ Godot PCK + ResourceLoader. У Godot нет такой удобной remote-CDN
интеграции из коробки, но базовые механизмы есть.
## Кеширование
`load` дважды по одному пути вернёт **тот же** Resource (рефкаунтная природа Resource):
```gdscript
var a = load("res://item.tres")
var b = load("res://item.tres")
print(a == b) # true — это один и тот же объект
```
Если хотите независимую копию (например, чтобы менять без побочных эффектов):
```gdscript
var clone = a.duplicate()
```
Для глубокого клона — `duplicate(true)` (рекурсивно дублирует вложенные ресурсы).
## resource_local_to_scene
Свойство Resource, которое заставляет каждую сцену **клонировать ресурс** при загрузке. Полезно
для материалов, которые должны быть уникальны на каждом инстансе:
```gdscript
var mat = StandardMaterial3D.new()
mat.resource_local_to_scene = true
# Теперь при инстансах сцены каждый получит свою копию
```
## Освобождение памяти
Resource удаляется автоматически по reference count. Если у вас Singleton-ссылки или global
arrays хранят ассеты, явно занулите их:
```gdscript
my_cache.clear() # массив со ссылками
preloaded_scene = null
```
После последней ссылки на следующий кадр Godot почистит память.
## Что делать, когда что-то "не выгружается"
1. Используйте **Resource Monitor** в Debugger → Monitor → Objects: показывает живые объекты в
памяти. Если число только растёт — утечка.
2. **Remote Scene Tree** — позволяет инспектировать запущенную сцену из редактора. Если узел
"должен был быть удалён, но висит" — увидите.
3. `queue_free()` вместо `free()` — освобождение в конце кадра, безопаснее для текущих сигналов.
В следующей главе — сборка и оптимизация.
---
## [Godot] Сборка и оптимизация
URL: https://cadmus.page/godot/02-3d/17-build-optimization/
Section: 3D-разработка в Godot
Description: Export Presets, Profiler, веб-таргет и WASM, чек-лист релиза.
В Godot сборка — это **Export Preset** (конфиг для платформы) + **Export Template** (бинарь
движка без редактора). Платформы first-party: Windows, macOS, Linux, Android, iOS, Web,
visionOS (с 4.5).
## Export Presets
`Project → Export` открывает окно с конфигами. Добавьте preset для нужной платформы:
- **Platform** (Windows Desktop, Android, Web, ...).
- **Bundle Identifier** / **Package Name** (для mobile/Mac/iOS).
- **Icon** и **Splash**.
- **Permissions** (Android — camera, location и т.д.).
- **Encryption** — зашифровать pack-файл.
- **Export Mode** — All resources / Resources from group / By files.
- **Debug / Release** — release делает оптимизированный билд без debugger.
## Export Templates
Это бинарники движка под все целевые платформы (без редактора). Скачайте через
**Editor → Manage Export Templates → Download**. Один раз на версию редактора (~1 GB).
Затем при **Export Project** Godot собирает: ваши ресурсы + template = готовая игра.
## Платформенные особенности
### Windows / macOS / Linux
- **Windows** — `.exe`. Подписывать через `signtool.exe` (опции в Inspector preset'а).
- **macOS** — `.app` (или `.dmg` с автоматической упаковкой). Notarization через Apple Developer
для бесшумного запуска. Требует Mac для подписи; теоретически возможна cross-compile.
- **Linux** — `.x86_64`. PIE/non-PIE опции для совместимости.
### Android
- **Android SDK + JDK** — нужно установить и указать пути в Editor Settings → Export → Android.
- **APK / AAB** — preset выбирает.
- **16 KB page size** — поддержка с 4.5 (требование Google Play от 1 ноября 2025).
- **Architectures** — `arm64-v8a` + `x86_64` для эмуляторов.
### iOS
- Требует **Mac + Xcode**.
- Export создаёт **Xcode project** — открываете в Xcode, собираете и шипите оттуда.
- Подпись и provisioning profile — стандартно через Apple Developer.
### Web (HTML5)
Это один из самых сложных таргетов. Особенности:
- **Compatibility renderer** обязателен. Forward+/Mobile в WebGL2 — экспериментально.
- **Threading** в WASM требует **Cross-Origin Isolation**: заголовки сервера:
```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```
- С Godot 4.2/4.3 есть **single-threaded экспорт** (без SharedArrayBuffer). Работает везде, в
том числе на дефолтных хостингах (itch.io, S3). Включается флажком "Threads" → Off.
- **C# в веб не экспортируется** — open issue, в работе.
- **Размер бандла**: пустой проект ~25 MB (после Brotli), реальные игры — 50–150 MB.
Output веб-экспорта:
```
index.html ← запускающая страница
index.js ← JS-loader
index.wasm ← движок и ваш код
index.pck ← упакованные ассеты
index.icon.png
```
Загружать через любой web-сервер.
Если включите в preset "PWA → Enabled", Godot сгенерирует Service Worker, который инжектит
COOP/COEP заголовки. Тогда threads будут работать даже на хостингах без поддержки этих
заголовков (itch.io, GitHub Pages).
## Профайлер
`Debugger` → внизу редактора → вкладки:
- **Profiler** — CPU-профайлер. Включить → запустить игру → собирает данные. Показывает время на
каждый метод и узел.
- **Visual Profiler** — GPU-профайлер. Время каждого render-pass'а.
- **Network Profiler** — для мультиплеера.
- **Monitors** — графики метрик в реальном времени: FPS, draw calls, objects, мемоори, video memory.
- **Remote Scene Tree** — живое дерево узлов запущенной игры; можно инспектировать значения.
## Бюджет кадра
- 60 FPS = 16.6 мс
- 30 FPS = 33.3 мс
- 120 FPS = 8.3 мс
- VR 90 FPS = 11.1 мс, и без права на просадки
Распределение в типовой 3D-сцене:
- 2–4 мс — rendering на GPU.
- 2–6 мс — physics + ваши скрипты на CPU.
- Остаток — buffer, VSync.
## Главные оптимизации
### Сжатие текстур
В Import-доке для каждой текстуры — **Compress Mode**:
- **VRAM Compressed** — для рантайма (BCn для PC, ASTC для Android/iOS).
- **VRAM Uncompressed** — несжатая.
- **Basis Universal** — кросс-платформенно через супер-сжатие, транскодится на GPU target.
- **Lossless** — PNG.
VRAM Compressed обязателен для большинства проектов; экономит видеопамять и draw-time.
### LOD — Level of Detail
В отличие от Unity LODGroup, Godot применяет **Mesh-level LOD** автоматически, если mesh содержит
несколько уровней детализации (генерируется при импорте при включённой "Generate LODs"). MeshInstance3D
имеет свойство `mesh_lod_threshold` (px на экране, при котором переключаться).
### Occlusion Culling
Godot поддерживает **OccluderInstance3D** — узлы-occluder'ы (статичная геометрия), которые
рассказывают рендереру "за этой стенкой ничего не видно". Запекается через Bake Occluders в
редакторе.
### Static Lighting
Запекание света через LightmapGI — снимает realtime cost для статичной геометрии. См. главу
про освещение.
### Меньше draw calls — MultiMesh
Для повторяющихся объектов (трава, ассеты) используйте **MultiMeshInstance3D** — рендерит N
экземпляров одним draw call'ом. Аналог Unity GPU Instancing.
### Профилирование GDScript
- **Статическая типизация** даёт 28–59% ускорение горячего кода. Используйте `var x: int` вместо
просто `var x`.
- **Каждое обращение `$Name`** — это `get_node`, не бесплатно. Кешируйте в `@onready`.
- **Сигналы дешевле, чем polling**: вместо проверять `if hp != last_hp:` каждый кадр, эмитьте
сигнал при изменении.
## Чек-лист релиза
- [ ] Профайлер показывает стабильные 60 FPS на целевом устройстве.
- [ ] Память не растёт за длительный gameplay (нет утечек, Resource Monitor стабилен).
- [ ] Текстуры сжаты (VRAM Compressed), не сырые PNG.
- [ ] LOD на тяжёлых моделях.
- [ ] Occlusion baked для интерьеров.
- [ ] Lightmaps baked для статичных сцен.
- [ ] Project Settings → Application: версия, иконка, splash настроены.
- [ ] Билд протестирован на target-устройстве (не только на dev).
- [ ] Print statements / отладочный код почищен или обёрнут в `if OS.is_debug_build()`.
- [ ] Settings меню (громкость, графика, кеи) сохраняется через `ConfigFile` или `user://`.
- [ ] Для веба — проверены заголовки COOP/COEP или single-threaded билд.
Файлы в `user://` — это специальное место (Roaming AppData / Library / .local) для save-файлов
и пользовательских настроек. Не пишите в `res://` в рантайме — это read-only после билда.
---
На этом главный раздел про 3D-разработку в Godot завершён. Дальше — глоссарий терминов с
веб-аналогами и параллелями к Unity.
---
## [Godot] Практика — 3rd-person платформер с NavMesh-врагом
URL: https://cadmus.page/godot/02-3d/18-tps-platformer/
Section: 3D-разработка в Godot
Description: PhantomCamera FreeLook, CharacterBody3D, double jump, coyote time, преследующий враг, чекпойнты.
Второй практический капстон Godot — параллель Unity TPS-главы. Собираем платформер от третьего
лица: персонаж бегает, прыгает (с double jump и coyote time), его преследует AI-враг по NavMesh,
при смерти игрок респаунится на чекпойнте.
Используем то, что разбирали раньше: CharacterBody3D с `move_and_slide`, NavigationAgent3D,
**Phantom Camera** плагин для third-person вида, Resource как ScriptableObject-аналог.
## Иерархия сцены
```
World (Node3D)
├── Ground ← StaticBody3D + CollisionShape3D + MeshInstance3D
├── Platforms ← набор StaticBody3D на разной высоте
├── Checkpoints
│ ├── Checkpoint_01 ← Area3D + CollisionShape3D + script
│ └── Checkpoint_02
├── Enemies
│ └── Patroller ← CharacterBody3D + NavigationAgent3D + EnemyAI
├── NavigationRegion3D ← запекаем NavMesh из Ground + Platforms
└── WorldEnvironment
Player (CharacterBody3D)
├── CollisionShape3D ← CapsuleShape3D (height=2, radius=0.4)
├── Body (Node3D) ← визуальная модель + MeshInstance3D
├── GroundCheck (Marker3D)← позиция у ног для is_on_floor
└── CameraTarget (Node3D) ← на уровне головы, цель для PhantomCamera
PhantomCamera3D ← следящая 3rd-person камера
tracking_target = Player/CameraTarget
follow_mode = ThirdPerson
PhantomCameraHost ← дочерний к главной Camera3D
Camera3D ← реальная камера сцены
GameState (Node) ← синглтон через autoload
HUD (CanvasLayer)
```
В Unity-главе мы взяли Rigidbody для платформера ради инерции. В Godot **CharacterBody3D с
`move_and_slide()` достаточно умный** — он сам "скользит" по стенам, обрабатывает наклоны и
ступеньки. Плюс предсказуем как Unity CharacterController. RigidBody3D имеет смысл только
если нужна полноценная физика на игроке (рэгдолл, пинать ящики).
## player_motor.gd — контроллер
```gdscript
extends CharacterBody3D
class_name PlayerMotor
@export_group("Movement")
@export var move_speed: float = 6.0
@export var air_control: float = 0.35
@export var turn_speed: float = 12.0 # рад/с
@export_group("Jump")
@export var jump_velocity: float = 5.5
@export var max_jumps: int = 2 # обычный + double
@export var coyote_time: float = 0.12 # окно прыжка после схода с края
@export var jump_buffer: float = 0.15 # окно "пред-нажатия" перед землёй
@export_group("References")
@export var camera_rig: Node3D # обычно — главная Camera3D
@onready var body: Node3D = $Body
var _jumps_left: int = 0
var _last_grounded_time: float = -1.0
var _last_jump_press_time: float = -1.0
var _was_on_floor: bool = false
func _physics_process(delta: float) -> void:
_update_grounded(delta)
_update_jump(delta)
_update_move(delta)
move_and_slide()
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("jump"):
_last_jump_press_time = _now()
func _update_grounded(_delta: float) -> void:
var on_floor := is_on_floor()
if on_floor:
_last_grounded_time = _now()
if not _was_on_floor:
_jumps_left = max_jumps # приземлились — восстанавливаем
_was_on_floor = on_floor
func _update_jump(delta: float) -> void:
var wants_jump := (_now() - _last_jump_press_time) < jump_buffer
var in_coyote := (_now() - _last_grounded_time) < coyote_time
if not wants_jump:
# Гравитация
if not is_on_floor():
velocity += get_gravity() * delta
return
var can_first_jump := in_coyote and _jumps_left == max_jumps
var can_air_jump := not is_on_floor() and _jumps_left > 0 and _jumps_left < max_jumps
if can_first_jump or can_air_jump:
velocity.y = jump_velocity # фиксированная высота прыжка
_jumps_left -= 1
_last_jump_press_time = -1.0 # съели буфер
else:
if not is_on_floor():
velocity += get_gravity() * delta
func _update_move(delta: float) -> void:
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
if input_dir == Vector2.ZERO:
# Затухание горизонтальной скорости на земле
var damp: float = 0.85 if is_on_floor() else 0.99
velocity.x *= damp
velocity.z *= damp
return
# Направление относительно камеры (проекция на горизонтальную плоскость)
var cam_basis := camera_rig.global_transform.basis
var cam_fwd := -cam_basis.z
cam_fwd.y = 0
cam_fwd = cam_fwd.normalized()
var cam_right := cam_basis.x
cam_right.y = 0
cam_right = cam_right.normalized()
var wish_dir := (cam_right * input_dir.x + cam_fwd * -input_dir.y).normalized()
# Поворот тела персонажа в направлении движения
var target_yaw := atan2(wish_dir.x, wish_dir.z)
body.rotation.y = lerp_angle(body.rotation.y, target_yaw, turn_speed * delta)
# Применение скорости: на земле — мгновенно (lerp с t=1.0 = snap к target),
# в воздухе — частично (t=air_control ≈ 0.35). Это даёт классический FPS-feel.
var t := 1.0 if is_on_floor() else air_control
var target_horizontal := wish_dir * move_speed
velocity.x = lerp(velocity.x, target_horizontal.x, t)
velocity.z = lerp(velocity.z, target_horizontal.z, t)
func _now() -> float:
return Time.get_ticks_msec() / 1000.0
```
Те же 0.12 / 0.15 секунды, что в Unity-главе. Игрок не нажимает прыжок точно в кадр касания
земли — он жмёт чуть раньше или чуть позже. Без этих окон до трети прыжков "не срабатывают".
Celeste, Hollow Knight, Mario Odyssey все делают это.
## Phantom Camera для 3rd-person
Установите плагин Phantom Camera (AssetLib → "Phantom Camera" → Install → активировать в Project
Settings → Plugins).
### Иерархия камеры
```
Player (CharacterBody3D)
└── CameraTarget (Node3D, y=1.6) ← цель слежения
PhantomCamera3D ← виртуальная камера
tracking_target = "../Player/CameraTarget"
follow_mode = THIRD_PERSON
third_person_settings:
follow_distance: 4.5
pitch_min: -45
pitch_max: 70
enable_collision: true ← не пробивать стены
PhantomCameraHost (на главной Camera3D)
Camera3D (current = true)
```
Параметры:
- **follow_distance** — расстояние от target.
- **pitch_min / pitch_max** — диапазон вертикального угла.
- **enable_collision** — камера не пробивает стены (SpringArm3D-стиль).
- **damping** — плавность догоняния.
### Управление мышью
PhantomCamera 3D в third-person режиме автоматически обрабатывает мышь, если включён
`mouse_look_enabled`. Sensitivity настраивается в Inspector.
Если хотите вручную:
```gdscript
# В скрипте на PhantomCamera3D — или используйте input_axis_x/y bindings плагина
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
var orbit := global_rotation
orbit.y -= event.relative.x * 0.003
orbit.x = clamp(orbit.x - event.relative.y * 0.003, deg_to_rad(-45), deg_to_rad(70))
global_rotation = orbit
```
## EnemyChaser — преследование по NavMesh
Из главы про Navigation, но упрощённо для платформера (без атаки в ближнем бою — просто
контактный урон):
```gdscript
extends CharacterBody3D
class_name ChaserEnemy
@export var target: Node3D
@export var speed: float = 3.5
@export var contact_damage: int = 1
@export var hit_cooldown: float = 0.8
@export var catch_distance: float = 1.2
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D
var _next_hit_time: float = 0.0
func _physics_process(delta: float) -> void:
if target == null:
return
nav_agent.target_position = target.global_position
if not nav_agent.is_navigation_finished():
var next_point := nav_agent.get_next_path_position()
var dir := (next_point - global_position).normalized()
velocity.x = dir.x * speed
velocity.z = dir.z * speed
else:
velocity.x = move_toward(velocity.x, 0, speed)
velocity.z = move_toward(velocity.z, 0, speed)
if not is_on_floor():
velocity += get_gravity() * delta
move_and_slide()
# Контактный урон
var dist := global_position.distance_to(target.global_position)
var now := Time.get_ticks_msec() / 1000.0
if dist <= catch_distance and now >= _next_hit_time:
if target.has_method("take_damage"):
target.take_damage(contact_damage)
_next_hit_time = now + hit_cooldown
```
Если ваши платформы разнесены и только игрок может допрыгнуть — враг застрянет внизу.
Решение: либо ставьте врагов в зоне, где они физически могут достичь игрока, либо используйте
`NavigationLink3D` для предзаданных скачков, либо пишите AI без NavMesh.
## PlayerHealth — с i-frames
```gdscript
extends Node
class_name PlayerHealth
signal damaged(hp_left: int)
signal died
@export var max_hearts: int = 3
@export var invincibility_seconds: float = 0.7
var _hp: int
var _invincible_until: float = 0.0
func _ready() -> void:
_hp = max_hearts
func take_damage(amount: int) -> void:
var now := Time.get_ticks_msec() / 1000.0
if now < _invincible_until or _hp <= 0:
return
_hp = max(0, _hp - amount)
_invincible_until = now + invincibility_seconds
damaged.emit(_hp)
if _hp == 0:
died.emit()
func reset() -> void:
_hp = max_hearts
damaged.emit(_hp)
```
## Чекпойнты и респаун через autoload
В Godot **синглтоны делаются через Autoload** (Project → Project Settings → Autoload). Это удобнее,
чем `Instance.set/get` шаблон.
```gdscript
# game_state.gd
extends Node
var active_checkpoint: Transform3D = Transform3D.IDENTITY
func set_active_checkpoint(t: Transform3D) -> void:
active_checkpoint = t
func respawn(player: Node3D, health: PlayerHealth) -> void:
if player is CharacterBody3D:
(player as CharacterBody3D).velocity = Vector3.ZERO
player.global_transform = active_checkpoint
health.reset()
```
Регистрация: Project Settings → Autoload → Path = `res://scripts/game_state.gd`, Name = `GameState`.
После этого `GameState` доступен из любого скрипта как глобал.
`checkpoint.gd` — Area3D-триггер на чекпойнте:
```gdscript
extends Area3D
class_name Checkpoint
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node3D) -> void:
if body.is_in_group("player"):
GameState.set_active_checkpoint(global_transform)
```
Главное: пометьте игрока в группе `player` (Inspector → Node → Groups → add "player").
Связать смерть с респауном:
```gdscript
# Скрипт игрока, в _ready
$Health.died.connect(_on_died)
func _on_died() -> void:
GameState.respawn(self, $Health)
```
## HUD сердечек
`hud.tscn`:
```
CanvasLayer
└── MarginContainer
└── HBoxContainer
├── TextureRect (heart_full.png)
├── TextureRect (heart_full.png)
└── TextureRect (heart_full.png)
```
`hearts_hud.gd`:
```gdscript
extends HBoxContainer
@export var heart_full: Texture2D
@export var heart_empty: Texture2D
@onready var heart_nodes: Array[TextureRect] = []
func _ready() -> void:
for child in get_children():
if child is TextureRect:
heart_nodes.append(child)
func on_health_changed(current: int) -> void:
for i in heart_nodes.size():
heart_nodes[i].texture = heart_full if i < current else heart_empty
```
Подключите сигнал `PlayerHealth.damaged` к этому методу через Inspector → Node → Signals или из кода.
## Что добавить дальше
- **Animator** через AnimationTree: BlendSpace2D для locomotion (forward/strafe), one-shot для прыжка.
- **Footstep audio** через Method Track в AnimationPlayer.
- **Movable платформы** — AnimatableBody3D с `velocity` свойством (применяется к стоящим на ней).
- **Wall slide / wall jump** — отдельный state-машина с проверкой `is_on_wall_only()`.
- **Pause Menu** — отдельная сцена с `get_tree().paused = true`.
- **Save System** — через `FileAccess` или `ConfigFile` в `user://saves.cfg`.
## Сравнение со сложностью Unity TPS
Эквивалентный Unity-проект занимает примерно столько же кода. Главные отличия:
- **`move_and_slide()`** делает больше за нас, чем `CharacterController.Move()` — slope handling
и slide-along-wall встроены.
- **Phantom Camera** даёт Cinemachine-стиль 3rd-person из одного плагина.
- **Autoload** проще, чем Unity Singleton-паттерн.
- **`Time.get_ticks_msec() / 1000.0`** вместо `Time.time` — немного многословнее.
- **Resource** + `class_name` дают такой же ScriptableObject-стиль конфига.
В целом — порядка 150–200 строк кода для играбельного TPS. Те же 1.5–2 часа на сборку, что и
Unity-эквивалент.
---
## [Godot] gdshader подробнее — vertex, fragment, light
URL: https://cadmus.page/godot/02-3d/19-gdshader-deep/
Section: 3D-разработка в Godot
Description: Структура spatial-шейдера, встроенные функции, uniforms, варинги, шаги оптимизации.
В главе про рендеринг видели базовый ShaderMaterial. Здесь — подробнее: устройство 3D-шейдера, что
такое vertex/fragment/light-функции и как писать своё освещение.
## Полная структура spatial-шейдера
```glsl
shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
// Uniforms — параметры материала, доступные из Inspector
uniform sampler2D albedo_tex : source_color, filter_linear_mipmap;
uniform vec4 tint : source_color = vec4(1.0);
uniform float roughness : hint_range(0.0, 1.0) = 0.5;
// Varyings — данные, передаваемые из vertex в fragment
varying vec3 world_pos;
varying float vertical_factor;
void vertex() {
// Здесь модифицируем позиции вершин и передаём данные вниз
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
vertical_factor = clamp(VERTEX.y * 0.1, 0.0, 1.0);
// Простая волна: смещение по синусу от мирового времени
VERTEX.y += sin(TIME + world_pos.x) * 0.1;
}
void fragment() {
// Здесь считаем цвет каждого пикселя
vec4 tex = texture(albedo_tex, UV);
ALBEDO = tex.rgb * tint.rgb;
ALPHA = tex.a * tint.a;
ROUGHNESS = roughness;
METALLIC = 0.0;
EMISSION = vec3(0.0, 0.5 * vertical_factor, 0.0); // зелёный градиент по высоте
}
void light() {
// (опционально) кастомное освещение, заменяет встроенное
// LIGHT — итоговый цвет, добавляется к ALBEDO
DIFFUSE_LIGHT += LIGHT_COLOR * dot(NORMAL, LIGHT) * ATTENUATION;
}
```
Что внутри:
- **`shader_type spatial;`** — это для 3D mesh'а. Для 2D — `canvas_item`, для skybox — `sky`, и т.д.
- **`render_mode`** — флаги: `blend_*` (transparency), `cull_*` (отбраковка), `depth_*` (Z-буфер),
`diffuse_*` (модель диффузного освещения), `specular_*` (зеркальная), `unshaded` (без света).
- **`uniform`** — параметр, видимый в Inspector материала и доступный из кода через
`set_shader_parameter`.
- **`varying`** — переменная, передаваемая из vertex в fragment (интерполируется по треугольнику).
- **`vertex()`** — модифицирует вершины перед растеризацией.
- **`fragment()`** — считает свойства поверхности (ALBEDO, NORMAL, ROUGHNESS, METALLIC, EMISSION, ALPHA).
- **`light()`** — кастомная функция освещения (если хотите свой не-PBR look).
## Встроенные переменные
Главные `in`/`out`:
**В vertex():**
- `VERTEX` — позиция вершины в object-space (in/out).
- `NORMAL` — нормаль (in/out).
- `TANGENT`, `BINORMAL` — касательные.
- `UV`, `UV2` — координаты текстуры.
- `COLOR` — vertex color.
- `MODEL_MATRIX`, `VIEW_MATRIX`, `PROJECTION_MATRIX` — стандартные матрицы.
- `TIME` — игровое время в секундах.
- `INSTANCE_ID`, `VERTEX_ID` — индексы (для MultiMesh).
**В fragment():**
- `ALBEDO` — диффузный цвет (out, vec3).
- `ALPHA` — прозрачность (out, float).
- `METALLIC`, `ROUGHNESS`, `SPECULAR` — PBR-параметры (out).
- `EMISSION` — самосветящееся (out, vec3).
- `NORMAL`, `NORMAL_MAP` — нормали (out).
- `AO`, `AO_LIGHT_AFFECT` — ambient occlusion (out).
- `RIM`, `RIM_TINT` — rim light (out).
- `CLEARCOAT`, `CLEARCOAT_GLOSS`.
- `UV`, `FRAGCOORD`, `VIEW`, `VERTEX` — input.
- `DEPTH` — out (можно писать в depth buffer).
**В light():**
- `LIGHT` — направление к источнику (in, vec3).
- `LIGHT_COLOR` — цвет с интенсивностью (in, vec3).
- `ATTENUATION` — затухание (in, float).
- `DIFFUSE_LIGHT`, `SPECULAR_LIGHT` — итоговые (out, vec3).
- `NORMAL`, `VIEW` — для расчётов.
## Hints для uniforms
```glsl
// Источник: цветовая текстура (применит inverse gamma — sRGB → linear автоматически)
uniform sampler2D albedo : source_color, filter_linear_mipmap, repeat_enable;
// Источник: normal map (распакуется правильно)
uniform sampler2D normal_map : hint_normal;
// Range slider в Inspector
uniform float intensity : hint_range(0.0, 5.0, 0.01) = 1.0;
// Color picker
uniform vec4 emission_color : source_color = vec4(1.0, 0.5, 0.0, 1.0);
// Cubemap для skybox / reflection
uniform samplerCube environment : source_color, filter_linear_mipmap;
```
Главные hints:
- **`source_color`** — для color-текстур (sRGB-преобразование).
- **`hint_normal`** — для normal-map.
- **`hint_roughness_r`** / `_g` / `_b` / `_gray` — channel pack для PBR.
- **`hint_range(min, max, step)`** — slider.
- **`filter_*`** — фильтрация: `nearest` / `linear` / `linear_mipmap` / `linear_mipmap_anisotropic`.
- **`repeat_*`** — wrap: `enable` / `disable`.
## Пример: dissolve (распад)
Классический эффект "объект растворяется в шумовой узор":
```glsl
shader_type spatial;
uniform sampler2D albedo : source_color, filter_linear_mipmap;
uniform sampler2D noise : filter_linear, repeat_enable;
uniform float threshold : hint_range(0.0, 1.0) = 0.0;
uniform vec3 edge_color : source_color = vec3(1.0, 0.5, 0.0);
uniform float edge_width : hint_range(0.0, 0.1) = 0.03;
void fragment() {
vec4 tex = texture(albedo, UV);
float n = texture(noise, UV).r;
if (n < threshold) {
discard; // отбросить этот пиксель
}
ALBEDO = tex.rgb;
if (n < threshold + edge_width) {
EMISSION = edge_color * 3.0;
ALBEDO = edge_color;
}
}
```
Использование:
```gdscript
var mat = $Mesh.material_override as ShaderMaterial
var tween = create_tween()
tween.tween_property(mat, "shader_parameter/threshold", 1.0, 2.0)
tween.tween_callback(queue_free)
```
## Sub-pass: Cull back и transparency
Прозрачные объекты часто требуют ручной настройки:
```glsl
shader_type spatial;
render_mode blend_mix, depth_draw_always, cull_disabled;
uniform vec4 color : source_color = vec4(1.0, 0.5, 0.5, 0.4);
void fragment() {
ALBEDO = color.rgb;
ALPHA = color.a;
}
```
- **`blend_mix`** — стандартный alpha-blending.
- **`depth_draw_always`** — писать в Z-буфер всегда (для правильных пересечений).
- **`cull_disabled`** — рисовать обе стороны (для стекла, листвы).
## Свой light() — toon shading
```glsl
shader_type spatial;
render_mode unshaded; // полностью отключим built-in lighting
uniform sampler2D albedo : source_color, filter_linear_mipmap;
uniform float bands : hint_range(2, 8, 1) = 3;
varying vec3 world_normal;
void vertex() {
world_normal = (MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz;
}
void light() {
float diffuse = dot(normalize(world_normal), normalize(LIGHT));
float quantized = floor(diffuse * bands) / bands;
DIFFUSE_LIGHT += LIGHT_COLOR * max(quantized, 0.0);
}
void fragment() {
ALBEDO = texture(albedo, UV).rgb;
}
```
`render_mode unshaded` отключает встроенное PBR-освещение, мы пишем своё в `light()`.
`if (n < threshold) { discard; }` — это разветвление в fragment-шейдере. Стоит дёшево, но
массивные ветвления убивают производительность. Чаще лучше `smoothstep` или маска через
`step()` — без if'ов.
## Particle shader
Для частиц `shader_type particles;` со своей семантикой:
```glsl
shader_type particles;
render_mode keep_data;
uniform vec3 attract_center;
uniform float attract_strength : hint_range(0.0, 10.0) = 1.0;
void process() {
vec3 to_center = attract_center - TRANSFORM[3].xyz;
float dist = length(to_center);
VELOCITY += normalize(to_center) * attract_strength * DELTA;
TRANSFORM[3].xyz += VELOCITY * DELTA;
}
```
Применяется как `process_material` на GPUParticles3D (заменяет ParticleProcessMaterial).
## Что не делать в шейдерах
1. **Тяжёлые вычисления в fragment().** Каждый пиксель вызывает fragment. Один лишний texture-sampling
на 1080p — это 2 миллиона sample'ов в кадр.
2. **Большие циклы**. GPU плохо переносит расходящиеся ветвления. Цикл `for (int i = 0; i < 100; i++)` —
допустимо, но `for (int i = 0; i < N; i++) if (cond[i]) ...` — нет.
3. **Pixel-perfect логика в fragment**. Если можно сделать в vertex (раз в N меньше вызовов) — делайте
там.
4. **`dynamic` texture sampling** через переменный индекс — GPU не любит. Лучше один atlas, чем
массив текстур.
## Что почитать
- **godotshaders.com** — community-сайт с готовыми шейдерами.
- Документация — `Shading reference` в Godot Docs (`docs.godotengine.org/en/stable/tutorials/shaders/`).
- Исходники встроенных шейдеров — в Godot-репозитории `servers/rendering/renderer_rd/shaders/`.
В следующей главе — глоссарий.
---
## [Godot] Path3D, PathFollow3D и сплайны
URL: https://cadmus.page/godot/02-3d/20-path-splines/
Section: 3D-разработка в Godot
Description: Curve3D, Path3D, PathFollow3D — движение по сплайну, AI-патрули, кат-сцены камеры.
В Godot есть встроенные **Path3D** и **PathFollow3D** для движения по сплайну. Кривая хранится в
ресурсе `Curve3D`. Не нужно ставить плагины — это часть core engine.
## Главные узлы
- **Path3D** — узел-контейнер с ресурсом `Curve3D`. Описывает кривую в пространстве.
- **PathFollow3D** — дочерний узел Path3D. Содержит свойство `progress` или `progress_ratio`, по
которому Godot вычисляет позицию вдоль кривой. Всё, что лежит внутри PathFollow3D, едет вдоль
сплайна.
- **Curve3D** — ресурс с массивом точек, у каждой — позиция + in/out tangents (для Bezier).
SVG-path с командами `M`, `L`, `C`. Или GSAP MotionPath plugin для анимации по кривой.
Path3D — это контейнер кривой, PathFollow3D — "ползунок" по кривой. Дочерние узлы PathFollow3D
автоматически едут вместе с ним.
## Базовая работа
### Создание Path3D в редакторе
1. Добавьте на сцену **Path3D**.
2. В Inspector создайте новый `Curve3D` (если ещё нет).
3. В Scene view выберите Path3D — появится Path Tool на toolbar.
4. Кликами добавляйте точки. Перетаскивайте handles для редактирования касательных (Bezier).
### Структура сцены
```
Level (Node3D)
└── Path3D ← curve: Curve3D с N точками
└── PathFollow3D
└── Cart (CharacterBody3D или Node3D) ← поедет вдоль сплайна
```
### Движение из кода
```gdscript
extends PathFollow3D
@export var speed: float = 4.0
@export var loop_mode: int = 0 # 0 = loop, 1 = stop at end
func _process(delta: float) -> void:
progress += speed * delta
# progress — дистанция в метрах вдоль кривой
# progress_ratio — нормализовано 0..1 (для удобного цикла)
```
Свойства PathFollow3D:
- **`progress: float`** — текущая позиция вдоль кривой в **метрах**.
- **`progress_ratio: float`** — нормализованный параметр `[0, 1]`.
- **`loop: bool`** — зацикливать ли (по умолчанию `true`).
- **`rotation_mode: RotationMode`** — `ROTATION_NONE` / `_Y` / `_XY` / `_XYZ` / `_ORIENTED`.
ORIENTED — поворачивает по tangent кривой (для машинки, движущейся по дороге).
- **`use_model_front: bool`** — корректирует ориентацию (если модель смотрит в -Z).
`progress` — это **расстояние в метрах** вдоль сплайна. Скорость в м/с — `progress += speed * delta`
даст постоянную скорость. `progress_ratio` (0..1) удобнее для UI или анимации длительности
("проехать всю кривую за 5 секунд"), но скорость станет зависеть от длины кривой.
## Программное создание кривой
```gdscript
var curve := Curve3D.new()
curve.add_point(Vector3(0, 0, 0))
curve.add_point(Vector3(5, 0, 0))
curve.add_point(Vector3(5, 2, -5),
Vector3(0, 0, 2), # in-tangent — вход в точку из предыдущей
Vector3(0, 0, -2)) # out-tangent — выход к следующей
curve.add_point(Vector3(0, 0, -10))
var path := Path3D.new()
path.curve = curve
add_child(path)
```
`add_point(position, in_handle, out_handle, index)` — без in/out даёт прямые сегменты,
с — Bezier-кривые.
## Сэмплирование без узла PathFollow3D
`Curve3D` имеет методы для прямого сэмплирования:
```gdscript
var point := curve.sample(0.5) # точка на середине (нормализовано)
var point_baked := curve.sample_baked(distance_m) # точка на distance_m метров вдоль
var transform := curve.sample_baked_with_rotation(distance_m, true, true)
```
`sample_baked` использует **baked length cache** — внутренний массив precomputed точек, который
обновляется при изменении кривой. Сэмплирование по distance работает в O(log N).
## Камера на рельсах (cinemachine-style)
В Godot нет встроенного эквивалента Cinemachine SplineDolly, но самостоятельно — 5 строк:
```gdscript
extends Camera3D
@export var path: Path3D
@export var duration: float = 5.0
@export var look_at: Node3D
var _t: float = 0.0
var _playing: bool = false
func play() -> void:
_t = 0.0
_playing = true
func _process(delta: float) -> void:
if not _playing or path == null:
return
_t += delta / duration
if _t >= 1.0:
_t = 1.0
_playing = false
var distance := path.curve.get_baked_length() * _t
var pos := path.curve.sample_baked(distance)
global_position = path.to_global(pos)
if look_at != null:
look_at(look_at.global_position, Vector3.UP)
```
Или используйте плагин **Phantom Camera** (упомянут в главе про камеру) — у него есть Path follow
mode.
## Расстановка объектов вдоль сплайна
Godot core не имеет встроенного "Instantiate along path" — это нужно делать самостоятельно. Простой
скрипт-инструмент через `@tool`:
```gdscript
@tool
extends Node3D
@export var path: Path3D
@export var prefab: PackedScene
@export var spacing: float = 2.0
@export var update: bool = false:
set(value):
if value: _rebuild()
update = false
func _rebuild() -> void:
for child in get_children():
child.queue_free()
if path == null or prefab == null:
return
var length := path.curve.get_baked_length()
var count := int(length / spacing)
for i in count:
var instance := prefab.instantiate()
var pos := path.curve.sample_baked(i * spacing)
instance.position = pos
add_child(instance)
instance.owner = get_tree().edited_scene_root
```
Поставьте этот узел детям Path3D, перетащите prefab, нажмите `update` в Inspector — расставит
N инстансов с шагом `spacing`.
## Когда использовать сплайны
- **AI-патруль** по предзаданному маршруту — PathFollow3D + ROTATION_Y.
- **Машинки на трассе** — PathFollow3D + ROTATION_ORIENTED.
- **Кат-сцены камеры** — Camera3D на PathFollow3D + look_at цель.
- **Конвейерные ленты, грузовые тележки** — что угодно, движущееся по фиксированной траектории.
- **Trail-эффекты** — позиция позади игрока, сэмплируется из недавнего трейла.
## Когда НЕ использовать
- **Прямые отрезки** — массив `Vector3` дешевле.
- **NavMesh-навигация по неровной местности** — у Path3D нет obstacle avoidance. Используйте
`NavigationAgent3D` (см. главу про Navigation).
- **Динамически создаваемые сотни кривых** в кадре — для тяжёлых случаев лучше WorkerThreadPool
(см. следующую главу) или GDExtension.
---
## [Godot] GridMap — модульные уровни
URL: https://cadmus.page/godot/02-3d/21-gridmap/
Section: 3D-разработка в Godot
Description: MeshLibrary + GridMap для сборки локаций из переиспользуемых блоков.
**GridMap** — встроенный Godot узел для сборки уровней из **модульных блоков на 3D-сетке**.
Думайте: Minecraft, Dungeon Crawler, Cities Skylines — где мир состоит из переиспользуемых
"кирпичиков" размером с тайл.
## Идея
- Сделайте **MeshLibrary**-ресурс — набор mesh'ей с заранее заданными CollisionShape3D.
- Положите его на узел **GridMap**.
- В Scene View рисуете "кистью" по 3D-сетке — каждая клетка получает выбранный из библиотеки mesh.
- Godot рендерит весь GridMap **одним вызовом на тип меша** (внутреннее instancing).
Это и быстрее, и компактнее, чем десять тысяч отдельных `MeshInstance3D`-узлов.
CSS-grid с tile-images. Только в 3D, и с physical colliders.
GridMap — это `MeshInstance3D` × N с auto-instancing и snapping к фиксированной сетке.
`set_cell_item(x, y, z, mesh_id)` рисует, `get_cell_item(x, y, z)` читает.
## Создание MeshLibrary
MeshLibrary — это `Resource` со словарём `id → { mesh, collision, navigation, preview }`. Создать
можно двумя способами:
### Способ 1. Конвертировать сцену с детьми-mesh
1. Создайте сцену `tiles.tscn` с корнем `Node3D`.
2. Дочерние — `MeshInstance3D` с уникальными именами (`wall_corner`, `floor_plain`, `door_wood`).
3. Каждый mesh — `StaticBody3D` ребёнком с `CollisionShape3D`, чтобы collision запеклась.
4. Scene → Convert To → MeshLibrary → сохранить `.tres` или `.res`.
### Способ 2. Программно
```gdscript
var lib := MeshLibrary.new()
var id := 0
lib.create_item(id)
lib.set_item_name(id, "wall_corner")
lib.set_item_mesh(id, preload("res://meshes/wall_corner.obj"))
lib.set_item_shapes(id, [my_box_shape]) # массив Shape3D
ResourceSaver.save(lib, "res://tiles/library.tres")
```
## Использование GridMap
1. Добавьте на сцену **GridMap**.
2. В Inspector укажите **Mesh Library** → ваш `.tres`.
3. Настройте **Cell Size** (например, `(2, 2, 2)` — кубики 2×2×2 метра).
4. В Scene View — GridMap toolbar с палитрой ваших mesh'ей. Кликами рисуете в сетке.
### Управление из кода
```gdscript
extends GridMap
const FLOOR := 0
const WALL := 1
const DOOR := 2
func _ready() -> void:
# Простая комната 5×5 из стен и пола
for x in range(5):
for z in range(5):
set_cell_item(Vector3i(x, 0, z), FLOOR)
if x == 0 or x == 4 or z == 0 or z == 4:
set_cell_item(Vector3i(x, 1, z), WALL)
# Дверь в (2, 1, 0)
set_cell_item(Vector3i(2, 1, 0), DOOR)
func is_cell_empty(pos: Vector3i) -> bool:
return get_cell_item(pos) == GridMap.INVALID_CELL_ITEM
```
`Vector3i` — целочисленные координаты ячейки. `set_cell_item(pos, item_id, orientation)` —
последний параметр — поворот блока (24 ориентации куба, через `GridMap.get_orthogonal_index_from_basis`).
## Procedural dungeon generation
GridMap идеально подходит для процедурной генерации:
```gdscript
extends GridMap
const FLOOR := 0
const WALL := 1
@export var size_x: int = 30
@export var size_z: int = 30
@export var iterations: int = 5
func _ready() -> void:
randomize()
_generate_cellular()
func _generate_cellular() -> void:
# 1. Заполнить случайно
var grid: Array = []
for x in size_x:
grid.append([])
for z in size_z:
grid[x].append(randf() < 0.45)
# 2. Cellular automata smoothing
for _i in iterations:
var next: Array = []
for x in size_x:
next.append([])
for z in size_z:
var n := _count_neighbors(grid, x, z)
# классическое cave-generation правило
next[x].append(n >= 5 if grid[x][z] else n >= 6)
grid = next
# 3. Применить в GridMap
for x in size_x:
for z in size_z:
set_cell_item(Vector3i(x, 0, z), FLOOR)
if grid[x][z]:
set_cell_item(Vector3i(x, 1, z), WALL)
func _count_neighbors(g: Array, x: int, z: int) -> int:
var count := 0
for dx in [-1, 0, 1]:
for dz in [-1, 0, 1]:
if dx == 0 and dz == 0:
continue
var nx := x + dx
var nz := z + dz
if nx < 0 or nx >= size_x or nz < 0 or nz >= size_z:
count += 1 # за границей — стена
elif g[nx][nz]:
count += 1
return count
```
Запускаете — получаете cave-style dungeon. С добавлением decoration-layer (статуи, факелы как
отдельные ID в MeshLibrary) — выглядит уже как готовый уровень.
GridMap может автоматически выдавать navmesh-baking. Установите **NavigationRegion3D** с ребёнком
GridMap, и в Bake Navigation Mesh `Geometry Source` поставьте `Group → дети_с_коллизиями`.
Получаете navmesh без отдельного навиграционного слоя. См. [главу про
Navigation](/godot/02-3d/13-navigation/).
## MeshLibrary vs MultiMeshInstance3D
Когда выбирать одно или другое:
| | GridMap + MeshLibrary | MultiMeshInstance3D |
|---|---|---|
| Структура | Сетка с фиксированным шагом | Произвольные позиции |
| Редактирование | В Scene View "кистью" | Только из кода |
| Кол-во разных mesh'ей | До нескольких сотен | Один на узел |
| Per-instance variants | Через orientations | Через transform-buffer |
| Animation | Нет | Нет (для анимированных — другой подход) |
| Collision | Автоматическое из MeshLibrary | Нужно строить отдельно |
Простое правило: **сетка → GridMap; trava/декорации без сетки → MultiMesh**.
## Когда GridMap — НЕ выбор
- **Open-world террейн** с нерегулярной топологией — лучше `Terrain3D` плагин.
- **Динамическая деструкция** мира (Minecraft-стиль с реальным изменением) — работает, но
оптимизация тонкая; рассмотрите ECS-style approach с GDExtension.
- **Сцены с уникальной геометрией каждого блока** — теряется выгода MeshLibrary, проще класть
отдельные MeshInstance3D в PackedScene.
## Альтернативы
- **TileMap (2D)** — то же самое для 2D-сцен.
- **CSG (Constructive Solid Geometry)** узлы — для прототипирования архитектуры без модульных
мешей. Не для финального уровня.
- **Terrain3D** плагин (через AssetLib) — для terrain'а с heightmap.
---
## [Godot] Threading — WorkerThreadPool и Thread
URL: https://cadmus.page/godot/02-3d/22-threading/
Section: 3D-разработка в Godot
Description: Параллельные задачи в Godot — генерация карт, обработка данных, длинные операции без фриза UI.
В Godot main-thread обрабатывает рендер, физику, скрипты. Если в `_ready` процедурно сгенерировать
карту мира — игра зависнет на секунду-две. **Threading** — способ вынести тяжёлую работу в
фоновые потоки.
## Три уровня инструментов
| Узел/класс | Назначение | Когда |
|---|---|---|
| **`WorkerThreadPool`** | Pool готовых worker-потоков, по числу ядер CPU | 80% случаев — рекомендован |
| **`Thread`** | Один ручной поток | Если нужен полный контроль или long-running |
| **`Mutex` / `Semaphore`** | Синхронизация между потоками | Везде, где shared data |
`WorkerThreadPool` появился в Godot 4 и — главный путь. Он управляет потоками за вас, вы только
submit-ите задачи.
Web Workers + SharedArrayBuffer. Только Godot Thread работает на native-уровне, без
serialization overhead.
`WorkerThreadPool.add_task(callable)` — отправляете callable в пул, получаете id, потом по нему
дождётесь результата. Очень похоже на `setTimeout(work, 0)`, но реально параллельно.
## WorkerThreadPool — базовый пример
Задача: процедурно сгенерировать heightmap 1024×1024.
```gdscript
extends Node
var _task_id: int = -1
var _heightmap: PackedFloat32Array
func _ready() -> void:
# Запускаем тяжёлую задачу в фоне.
_task_id = WorkerThreadPool.add_task(_generate_heightmap, true)
# Второй аргумент — high_priority. Этот таск пойдёт впереди очереди.
func _process(_delta: float) -> void:
if _task_id == -1:
return
# Не блокируем main-thread. Проверяем, готова ли задача.
if WorkerThreadPool.is_task_completed(_task_id):
WorkerThreadPool.wait_for_task_completion(_task_id)
_on_heightmap_ready()
_task_id = -1
func _generate_heightmap() -> void:
# Этот метод выполняется в фоновом потоке.
var data := PackedFloat32Array()
data.resize(1024 * 1024)
for y in 1024:
for x in 1024:
data[y * 1024 + x] = sin(x * 0.05) * cos(y * 0.05)
_heightmap = data
func _on_heightmap_ready() -> void:
print("Heightmap generated! Size: ", _heightmap.size())
# Здесь применяем результат — главный поток
```
Главные методы `WorkerThreadPool`:
- **`add_task(callable, high_priority = false, description = "")`** — submit, возвращает `task_id`.
- **`is_task_completed(task_id)`** — проверить готовность.
- **`wait_for_task_completion(task_id)`** — заблокировать main, пока не закончится (вызывается
после `is_task_completed` для cleanup).
- **`add_group_task(callable, elements, tasks_needed, ...)`** — параллельное "for each".
## Параллельный for-each через group task
Аналог `IJobParallelFor` в Unity — `add_group_task` делит N итераций на worker-ы пула:
```gdscript
extends Node
var _agents: Array[Node3D] = []
func _ready() -> void:
# 1000 агентов, каждый — независимое обновление
var group_id := WorkerThreadPool.add_group_task(
_update_agent,
_agents.size(), # tasks_needed
-1, # tasks_needed (-1 = по числу ядер)
false, # high_priority
"Agent batch update"
)
WorkerThreadPool.wait_for_group_task_completion(group_id)
func _update_agent(index: int) -> void:
# Выполняется параллельно для разных index. Главное правило:
# каждый index трогает ТОЛЬКО свой агент, иначе race condition.
var agent := _agents[index]
var new_pos := agent.global_position + Vector3.RIGHT
# ! НЕЛЬЗЯ менять Node API из потока. Можно только посчитать значения.
# Применить — на main thread.
```
Большинство Godot API (Node, Transform3D, Image, RenderingServer) **не thread-safe**. Изменять
узлы или ассеты из фонового потока — путь к крашу или undefined behavior. В фоне делайте только
**вычисления над данными** (массивы, PackedArray, ваши custom struct'ы), результаты применяйте
на main-thread через сигнал или флаг.
Исключения, явно безопасные для потоков: `ResourceLoader.load_threaded_*`, физический сервер с
явными mutex'ами.
## Mutex — защита shared data
Если несколько worker-ов пишут в общий массив, нужен Mutex:
```gdscript
extends Node
var _results: Array = []
var _mutex := Mutex.new()
func _calc(index: int) -> void:
var value := index * 2.0
_mutex.lock()
_results.append(value)
_mutex.unlock()
```
Без `lock` две параллельные `_calc` могут одновременно `.append`, повредив внутреннее состояние
массива.
**Альтернатива**: дать каждому task свой output-array, потом мерджить на main thread. Это быстрее
mutex'а — нет contention.
## Thread — ручной поток
Когда нужен **долгоживущий** background-thread (long-poll server, audio streaming, custom job
queue), используйте `Thread`:
```gdscript
extends Node
var _thread: Thread
func _ready() -> void:
_thread = Thread.new()
_thread.start(_background_loop)
func _background_loop() -> void:
while not _should_stop:
# тяжёлая работа
OS.delay_msec(100)
var _should_stop: bool = false
func _exit_tree() -> void:
_should_stop = true
_thread.wait_to_finish()
```
В отличие от WorkerThreadPool, `Thread` создаёт **новый поток на каждый старт** — нет пула.
Используйте только если задача live долго.
## Resource loading в потоке
Главный сценарий, для которого threading "обязателен" — это `ResourceLoader.load_threaded_request`:
```gdscript
ResourceLoader.load_threaded_request("res://big_scene.tscn")
func _process(_delta: float) -> void:
var status = ResourceLoader.load_threaded_get_status("res://big_scene.tscn")
match status:
ResourceLoader.THREAD_LOAD_LOADED:
var scene = ResourceLoader.load_threaded_get("res://big_scene.tscn")
get_tree().change_scene_to_packed(scene)
```
Внутри Godot создаёт свой поток для загрузки. Подробнее — в главе [Загрузка
ресурсов](/godot/02-3d/16-resource-loading/).
## C# в Godot — threading
В C#-варианте Godot можно использовать стандартные .NET-thread-инструменты: `Task.Run`, `Parallel.For`,
`async/await`, `lock`. Те же ограничения: **Godot Node API — main-thread only**.
```csharp
await Task.Run(() => {
// тяжёлая работа в фоне
});
// здесь — снова main thread, применяем результат
```
## Когда стоит выносить в поток
- **Процедурная генерация** уровней, текстур, mesh'ей.
- **Pathfinding** для большого количества агентов (если NavigationServer перегружен).
- **Парсинг больших файлов** (JSON, бинарные форматы).
- **Сетевые запросы** с долгим ответом (хотя `HTTPRequest` уже асинхронен).
- **Audio analysis / DSP** в реальном времени.
## Когда НЕ стоит
- **Лёгкие операции** (< 1 мс) — overhead создания/синхронизации съест выигрыш.
- **Постоянный поток данных в Node API** — main thread всё равно станет bottleneck.
- **Прототипы** — добавит сложности без видимой пользы.
---
## [Godot] Decals — наклейки в 3D
URL: https://cadmus.page/godot/02-3d/23-decals/
Section: 3D-разработка в Godot
Description: Decal-узел для bullet holes, граффити, dirt-масок, footprint'ов и других накладок на геометрию.
**Decal** — узел в Godot, который проецирует текстуру на поверхности под собой. Это классический
приём для:
- Дырок от пуль на стене
- Грязи на полу, лужи
- Граффити, постеров
- Следов от шин
- Cracks, кровь, scorch marks от взрывов
Реализовано как **отдельный render pass**, не лежит на геометрии — поэтому можно "клеить" поверх
любой поверхности без модификации mesh'ей.
CSS `mix-blend-mode: multiply` или Canvas `globalCompositeOperation = 'multiply'` поверх
background. В 3D у браузеров аналога нет — это специфика real-time graphics.
Decal — projector, который рисует своё содержимое поверх существующего pixel'я. Glsl-эквивалент:
"renderer берёт final color пикселя пола → blend с decal-текстурой".
## Базовое использование
1. Добавьте на сцену узел **Decal**.
2. В Inspector задайте **`texture_albedo`** — основная текстура.
3. (Опционально) **`texture_normal`** — карта нормалей для рельефа.
4. (Опционально) **`texture_orm`** — Occlusion/Roughness/Metallic mask.
5. Поверните и масштабируйте узел: bounding box decal'а определяет, на какую область он проецируется.
6. `cull_mask` — фильтр по visual layers (только определённые объекты получают decal).
```
World3D
├── Floor (StaticBody3D + MeshInstance3D)
└── BulletHole (Decal)
texture_albedo: bullet_hole.png (RGBA)
size: (0.3, 0.3, 0.3) # 30×30 cm область
rotation: looking at -Y (вниз — на пол)
```
## Программный spawn
```gdscript
extends Node
@export var decal_scene: PackedScene # Decal в виде PackedScene
@export var lifetime: float = 30.0
func spawn_bullet_hole(at: Vector3, normal: Vector3) -> void:
var decal := decal_scene.instantiate() as Decal
add_child(decal)
decal.global_position = at + normal * 0.01 # чуть выше поверхности
# Ориентируем decal так, чтобы он смотрел "вниз" (по -Y) на нормаль
decal.look_at(at - normal, Vector3.UP if abs(normal.y) < 0.9 else Vector3.BACK)
# Маленькая рандомизация поворота для естественности
decal.rotate_object_local(Vector3.UP, randf() * TAU)
# Удаляем через lifetime — типовой подход для "тает" эффекта
var tween := create_tween()
tween.tween_interval(lifetime * 0.7)
tween.tween_property(decal, "albedo_mix", 0.0, lifetime * 0.3)
tween.tween_callback(decal.queue_free)
```
`albedo_mix` — float 0..1, контролирует "силу" decal'а. Анимируете в 0 → decal плавно исчезает.
## Параметры Decal'а
- **`size: Vector3`** — bounding box проекции (X×Y по поверхности, Z — глубина проекции).
- **`texture_albedo`** + **`albedo_mix`** — цветовая текстура и её сила.
- **`texture_normal`** + **`normal_fade`** — карта нормалей и насколько она применяется.
- **`texture_orm`** — комбинированная Occlusion/Roughness/Metallic.
- **`texture_emission`** + **`emission_energy`** — самосветящиеся decal'ы (огни, экраны).
- **`modulate: Color`** — общий цвет умножения (для перекраски без замены текстуры).
- **`upper_fade` / `lower_fade`** — fade в углах bounding box'а.
- **`distance_fade_*`** — fade на расстоянии (для оптимизации; дальние decal'ы исчезают).
- **`cull_mask`** — битмаска visual layers; decal видим только на этих слоях.
В сценах с активной стрельбой накапливаются сотни decal'ов. Без `distance_fade_enabled = true`
+ разумных `begin` / `length` рендерер будет процессить их все. С fade — дальние плавно
становятся прозрачными и пропускаются, оставляя видимыми только ~30-50 ближних.
## Rendering требования
- **Forward+ или Mobile renderer**. На Compatibility (WebGL) decal'ы работают, но в урезанном виде
и через эмуляцию.
- Decal **не пишет в depth buffer** — он лежит поверх пикселя. Это значит: за углом или сквозь
стены он не виден, и сортировка с прозрачными объектами может быть сложной.
## Когда decal — НЕ выбор
- **Большие, точные знаки на полу** (например, разметка для рендера-на-карту) — может быть дешевле
включить как часть текстуры пола или второй UV-слой.
- **Геометрия, которая должна влиять на физику** (выпуклость, статуэтка) — Decal только визуальный,
не меняет collision shape.
- **Анимированные decals** — поддерживаются (sprite sheet через UV-shift в `texture_albedo`), но
через ShaderMaterial; стандартный Decal node не имеет анимации в Inspector.
## Сравнение с Unity
Unity имеет аналог: **Decal Projector** в URP (`com.unity.render-pipelines.universal/Decals`).
Концепция и API почти 1-в-1: тоже projector, texture_albedo, distance fade, cull mask. Различия —
в деталях performance: Unity URP Decal Projector обычно дороже, чем Godot Decal в Forward+.
---
## [Godot] @tool и EditorPlugin — расширение редактора
URL: https://cadmus.page/godot/02-3d/24-tool-editor-plugin/
Section: 3D-разработка в Godot
Description: Tool-скрипты, custom inspectors, EditorPlugin, добавление узлов и docks к редактору Godot.
Godot полностью написан на собственном языке для UI — это значит, что **сам редактор может быть
расширен GDScript-кодом**. Никаких C++-плагинов, никаких пересборок.
Два уровня расширения:
1. **`@tool`-скрипты** — обычные узлы с поведением **в редакторе**. Например, скрипт, который
автоматически расставляет деревья по сплайну.
2. **EditorPlugin** — настоящий плагин: добавляет узлы, кастомные docks, инспекторы, gizmos.
## @tool — скрипт работает в редакторе
Обычный GDScript исполняется только в Play Mode. Добавьте `@tool` в первой строке — и скрипт будет
работать прямо в Scene View.
```gdscript
@tool
extends Node3D
@export var radius: float = 5.0:
set(value):
radius = value
_rebuild_circle() # перерисовываем при изменении
@export var count: int = 8:
set(value):
count = value
_rebuild_circle()
@export var rebuild: bool = false:
set(value):
if value: _rebuild_circle()
rebuild = false
func _rebuild_circle() -> void:
# Чистим старых детей
for child in get_children():
child.queue_free()
# Расставляем сферы по кругу
for i in count:
var angle := TAU * i / count
var sphere := MeshInstance3D.new()
sphere.mesh = SphereMesh.new()
sphere.position = Vector3(cos(angle), 0, sin(angle)) * radius
add_child(sphere)
sphere.owner = get_tree().edited_scene_root # чтобы дети сохранились в .tscn
```
Меняете `radius` в Inspector — круг сразу перерисовывается в Scene View. Сохраняете сцену —
дочерние сферы остаются.
Storybook + Design Tokens: интерактивные превью компонентов с live-параметрами в редакторе.
Tool-скрипты — это "Storybook прямо в финальном продукте".
Tool-скрипт — тот же ваш код, но движок исполняет его в редактор-time. Все API доступны (с
оговорками — нет Input, нет _process в традиционном смысле).
Если поставите `while true:` или тяжёлый цикл в `@tool`-скрипте без условия выхода — **зависнет
редактор**. Перед сохранением проверяйте: ничего бесконечного, нет блокирующих операций. Если
всё сломалось — запускайте Godot из терминала с флагом `--no-window` для удаления скрипта.
## owner = edited_scene_root — обязательно
Если вы создаёте детей в `@tool`-скрипте, **обязательно** проставляйте им `owner`:
```gdscript
var instance := preload("res://prefab.tscn").instantiate()
add_child(instance)
instance.owner = get_tree().edited_scene_root
```
Без `owner` — дети существуют в Scene Tree во время редактирования, но **не сохраняются в .tscn**.
Сохранение и открытие сцены сделает их невидимыми.
## EditorPlugin — настоящий плагин
Полноценный плагин — это `EditorPlugin`-узел, который Godot загружает при старте редактора. Может:
- Добавить **новый тип узла** (`add_custom_type`).
- Добавить **dock** в боковую панель (`add_control_to_dock`).
- Добавить пункт в меню **Project/Scene** (`add_tool_menu_item`).
- Зарегистрировать **gizmo** для узла (`add_node_3d_gizmo_plugin`).
- Перехватить **save/load** сцены.
### Структура плагина
```
addons/
└── my_plugin/
├── plugin.cfg ← манифест
├── plugin.gd ← главный EditorPlugin
├── my_node.gd ← новый тип узла
└── icon.svg
```
`plugin.cfg`:
```ini
[plugin]
name="MyAwesomePlugin"
description="Adds X to the editor"
author="You"
version="1.0"
script="plugin.gd"
```
`plugin.gd`:
```gdscript
@tool
extends EditorPlugin
func _enter_tree() -> void:
# Регистрируем новый тип узла "TerrainPainter"
add_custom_type(
"TerrainPainter",
"Node3D",
preload("res://addons/my_plugin/my_node.gd"),
preload("res://addons/my_plugin/icon.svg")
)
func _exit_tree() -> void:
remove_custom_type("TerrainPainter")
```
После сохранения: Project → Project Settings → Plugins → активируйте свой. В меню Add Node
появится "TerrainPainter".
### Dock-панель
```gdscript
@tool
extends EditorPlugin
var dock: Control
func _enter_tree() -> void:
dock = preload("res://addons/my_plugin/dock.tscn").instantiate()
add_control_to_dock(EditorPlugin.DOCK_SLOT_LEFT_UR, dock)
func _exit_tree() -> void:
remove_control_from_docks(dock)
dock.queue_free()
```
`dock.tscn` — обычная сцена с `Control`-узлом. Появится в редакторе как настоящая dock-панель,
перетаскиваемая.
### Inspector plugin — custom property edits
Для нестандартных property-эдиторов (например, color-palette picker вместо обычного Color):
```gdscript
extends EditorInspectorPlugin
func _can_handle(object: Object) -> bool:
return object is MyCustomResource
func _parse_property(object, type, name, hint, hint_string, usage, wide):
if name == "palette_color":
add_property_editor(name, MyCustomPaletteEditor.new())
return true
return false
```
## Gizmos — 3D-визуализация в редакторе
Хотите видеть в 3D viewport круг радиуса агрессии вашего врага? Зарегистрируйте Node3DGizmoPlugin:
```gdscript
class_name EnemyGizmo extends EditorNode3DGizmoPlugin
func _has_gizmo(node):
return node is Enemy # ваш custom class
func _redraw(gizmo):
var node = gizmo.get_node_3d() as Enemy
gizmo.clear()
# Рисуем sphere wireframe радиуса agro_radius
var lines := PackedVector3Array()
var segments := 32
for i in segments:
var a1 := TAU * i / segments
var a2 := TAU * (i + 1) / segments
lines.append(Vector3(cos(a1), 0, sin(a1)) * node.agro_radius)
lines.append(Vector3(cos(a2), 0, sin(a2)) * node.agro_radius)
gizmo.add_lines(lines, get_material("main", gizmo))
```
В редакторе вокруг каждого `Enemy` будет нарисован круг агро — мгновенная визуальная подсказка.
## Когда писать плагин vs использовать @tool
- **@tool** — для **одной сцены**: процедурная расстановка, генератор, превью.
- **EditorPlugin** — для **переиспользуемой фичи**: кастомные узлы, нестандартные инспекторы, dock'и.
Большинство Asset Library-плагинов (Phantom Camera, Dialogic, Terrain3D) — это EditorPlugin.
Они активируются через Project Settings → Plugins.
## Сравнение с Unity
Unity-аналог:
- **`[ExecuteAlways]`** атрибут MonoBehaviour ↔ Godot **@tool**.
- **`OnDrawGizmos()` / `OnDrawGizmosSelected()`** ↔ Godot **EditorNode3DGizmoPlugin**.
- **Custom Editor (`[CustomEditor(typeof(...))]`)** ↔ Godot **EditorInspectorPlugin**.
- **`EditorWindow`** ↔ Godot **dock с `Control`**.
- **`MenuItem`** ↔ Godot **`add_tool_menu_item`**.
Концепции эквивалентны, имена разные.
---
## [Godot] TileMap (2D) — модульные 2D-уровни
URL: https://cadmus.page/godot/02-3d/25-tilemap-2d/
Section: 3D-разработка в Godot
Description: TileMapLayer, TileSet, terrains, autotile, navigation для 2D-проектов в Godot 4.x.
Эта энциклопедия про 3D, но Godot имеет **первоклассный 2D-pipeline**, и TileMap — главный
инструмент для модульных уровней (платформеры, top-down RPG, dungeon crawlers, city builders).
## Архитектура TileMap в Godot 4.x
С Godot 4.3 произошло важное изменение: классический `TileMap`-узел **deprecated** в пользу
**`TileMapLayer`**-узлов.
| | TileMap (legacy) | TileMapLayer (current) |
|---|---|---|
| Год | до 4.3 | с 4.3, рекомендован |
| Слои | Внутри одного узла как массив | Каждый слой — отдельный узел |
| Производительность | Хуже на сценах с layer modulation | Лучше — каждый layer обрабатывается параллельно |
| Editor UX | Сложные UI для multi-layer | Простой: drag-drop узлы для перепорядочивания |
В новом проекте используйте `TileMapLayer`. Один узел = один слой (например, фон → детали → объекты).
## Главные понятия
- **TileSet** — `Resource` со списком всех доступных тайлов (sprite + collision + nav-data).
- **TileMapLayer** — `Node2D`, который рисует тайлы из TileSet на координатной сетке.
- **Tile** — ячейка TileSet'а: sprite-источник + collision-shape + custom data layers.
- **TileSetAtlasSource** — источник тайлов из одной картинки (atlas-style).
- **TileSetScenesCollectionSource** — источник тайлов из PackedScene (анимированные / сложные).
CSS sprite-sheet с background-position + Pixi.js tilemap libraries.
TileSet — это база, TileMapLayer — рисователь. Один TileSet может использоваться многими
TileMapLayer (например, общий tile-стиль для разных уровней).
## Создание TileSet
1. В FileSystem → New Resource → TileSet.
2. Двойной клик — открывается TileSet editor (bottom panel).
3. Кликаете `+` → New Atlas Source → выбираете texture (sprite-atlas с тайлами).
4. Редактор автоматически режет атлас на ячейки (по умолчанию 16×16). Каждая клетка — отдельный
tile.
5. Для каждого tile настраиваете:
- **Physics layer** — collision shape (выберите Physics Layer 0, нарисуйте полигон).
- **Navigation layer** — для NavigationAgent2D.
- **Custom data layers** — произвольные свойства (например, "damage_per_tick" для лавы).
## TileMapLayer в работе
```
World
├── TileMapLayer_Background
│ tile_set: floor.tres
│ y_sort_enabled: false
├── TileMapLayer_Walls
│ tile_set: floor.tres # тот же TileSet
│ y_sort_enabled: false
└── TileMapLayer_Objects
tile_set: objects.tres
y_sort_enabled: true # для top-down с глубиной
```
В Scene View выберите слой → TileMap Editor (bottom panel) → выбираете tile из палитры → рисуете.
## Программное управление
```gdscript
extends TileMapLayer
func _ready() -> void:
# Координаты в TileMap — Vector2i (целые)
set_cell(Vector2i(0, 0), 0, Vector2i(0, 0)) # source_id, atlas_coords
# source_id — индекс источника в TileSet
# atlas_coords — позиция тайла в атласе
# Стереть тайл
erase_cell(Vector2i(0, 0))
# Получить tile data
var tile_data := get_cell_tile_data(Vector2i(0, 0))
if tile_data:
var dmg = tile_data.get_custom_data("damage_per_tick")
print(dmg)
# Конвертация world ↔ map координат
var map_pos := local_to_map(Vector2(64, 32))
var world_pos := map_to_local(Vector2i(2, 1))
```
## Terrains — авто-соединение тайлов
Главная фича для красивых уровней. Вы рисуете **зону** (трава, вода, дорога), и движок сам
подбирает правильный tile для каждой ячейки на основе соседей.
1. В TileSet editor → выберите Terrains tab.
2. Добавьте Terrain Set → новый Terrain ("Grass").
3. На каждом tile в Atlas выделите, какие его углы относятся к terrain "Grass".
4. В TileMap editor переключите режим на Terrains → выбираете Grass → "красите" большой
областью.
5. Godot для каждой клетки автоматически выбирает tile из набора, у которого углы совпадают с
соседями.
Это **terrain matching по углам и/или сторонам** — классический "auto-tile". Можно нарисовать
кривую границу травы по карте, и стыковка с дорогой / водой будет идеальной.
Godot Terrains использует corner-based matching (4 угла tile'а имеют bitmask). Это позволяет
плавные переходы и diagonal-correct неровные границы. Старая Wang-tile-схема (с side-based
matching) тоже есть в API, но corner — современный путь.
## Navigation на TileMap
Если хотите, чтобы NavigationAgent2D ходил по вашей карте:
1. В TileSet → Physics Layers НЕ хватит — это collision, не nav.
2. В TileSet → Navigation Layers → добавьте слой.
3. На каждом proходимом tile → нарисуйте navigation polygon (обычно совпадает с границей tile'а).
4. На TileMapLayer узле → **Navigation Layers** → отметьте используемый.
5. Godot автоматически собирает navmesh из всех проходимых tile'ов.
## Анимированные тайлы
В TileSet Atlas Source → выберите tile → в Inspector → Animation Frames Count > 1. Указываете
длительность каждого фрейма. Movement / textures animation работает без скриптов — TileMapLayer
обновляет автоматически.
Применение: текущая вода, факелы, мерцающие cristals.
## Подводные камни
1. **Tile size в TileSet vs TileMapLayer** — должны совпадать. Если поменяли в TileSet → перерисуйте
слои.
2. **Physics не работают по умолчанию** — Physics Layer должна быть включена и tile должен иметь
collision polygon. Часто забывают.
3. **Y-sort на одном слое** не дружит с tile-collision — для top-down с правильной сортировкой
персонажа за деревом используйте Object Layer (Y-sort) отдельно от ground layers.
4. **Custom data layers** — мощно, но запоминайте имена. Опечатка `"damage_per_tick"` vs
`"damagePerTick"` — silent fail.
## Когда TileMap — НЕ выбор
- **3D-сцена** — это 2D-узлы; в 3D используйте GridMap (см. главу 21).
- **Hex grid** — TileMap поддерживает hex layout (Inspector → tile_shape = HALF_OFFSET_*), но
редактор удобнее под orthogonal. Для серьёзного hex-проекта смотрите community-плагины.
- **Voxel-style 3D** — это уже совсем другая задача; TileMap не подходит.
---
## [Godot] XR / OpenXR — VR-приложения в Godot
URL: https://cadmus.page/godot/02-3d/26-xr-openxr/
Section: 3D-разработка в Godot
Description: XROrigin3D, XRCamera3D, XRController3D — базовая VR-сцена для Meta Quest и SteamVR.
Godot имеет **встроенную поддержку OpenXR** прямо в core, без отдельных пакетов. На фоне Unity
это особенно приятно: запуск VR-проекта в Godot — буквально несколько узлов.
## OpenXR в Godot — встроено
OpenXR — открытый стандарт Khronos Group для VR/AR. Заменяет SDK от Oculus, SteamVR, Vive Wave.
В Godot 4.x OpenXR — часть engine, активируется в Project Settings → XR.
### Активация
1. **Project Settings → XR → OpenXR → Enabled = true**.
2. Выберите interaction profiles: Oculus Touch, Khronos Simple, Valve Index. По умолчанию
включены основные.
3. Для **Android (Quest)**: Build Profile → Android, в Export preset включите XR-permissions.
## Базовая сцена
```
Main (Node3D)
└── XROrigin3D ← представляет физическую комнату игрока
├── XRCamera3D ← VR-камера, current=true
├── XRController3D (Left) ← controller_tracker = "/user/hand/left"
│ └── MeshInstance3D ← визуальная модель контроллера
└── XRController3D (Right) ← controller_tracker = "/user/hand/right"
└── MeshInstance3D
```
Узлы:
- **`XROrigin3D`** — корень VR-rig. Его позиция = центр игровой комнаты.
- **`XRCamera3D`** — камера, привязанная к headset. Двигается, когда игрок физически ходит.
- **`XRController3D`** — узел, отслеживающий контроллер. Position/rotation обновляются автоматически.
## Минимальный код для запуска VR
```gdscript
extends Node3D
var xr_interface: XRInterface
func _ready() -> void:
xr_interface = XRServer.find_interface("OpenXR")
if xr_interface and xr_interface.is_initialized():
print("OpenXR initialized!")
# В Godot для VR-режима нужно выставить viewport
get_viewport().use_xr = true
else:
push_warning("OpenXR not available. Falling back to desktop.")
```
После этого ваша сцена с XROrigin3D запустится в VR-режиме на подключённом headset.
Без `viewport.use_xr = true` ваша сцена откроется как обычное desktop-окно, даже если headset
подключён. Это удобно для проверки в "невыезжая в VR". Когда готовы — выставляете флаг.
## Получение input от контроллеров
XR-controller имеет sender'ы для кнопок/осей через **input actions**, как обычные Input actions в
Godot:
1. В Project Settings → Input Map создайте action `vr_select`.
2. Привязка: добавьте **OpenXR Action Map → Bind to vr_select** (через OpenXR Action Map editor).
3. В скрипте:
```gdscript
extends XRController3D
func _ready() -> void:
button_pressed.connect(_on_button)
button_released.connect(_on_button_released)
input_float_changed.connect(_on_float_input)
func _on_button(name: String) -> void:
if name == "trigger_click":
_on_trigger_pressed()
func _on_trigger_pressed() -> void:
print("Right trigger pressed")
# Можно выпустить raycast / instantiate projectile
```
XRController3D имеет встроенные сигналы:
- `button_pressed(name)`, `button_released(name)`
- `input_float_changed(name, value)` — для analog axes (trigger, grip)
- `input_vector2_changed(name, value)` — для thumbsticks
## Grab — взять предмет
Простейший grab без community-плагинов:
```gdscript
extends XRController3D
@export var grab_area: Area3D # ребёнок XRController3D со сферой ~5cm
var held_object: RigidBody3D
func _ready() -> void:
button_pressed.connect(_on_button)
func _on_button(name: String) -> void:
if name == "grip_click":
if held_object == null:
_try_grab()
else:
_release()
func _try_grab() -> void:
var bodies := grab_area.get_overlapping_bodies()
for body in bodies:
if body is RigidBody3D and body.has_method("on_grabbed"):
held_object = body
held_object.freeze = true
held_object.reparent(self)
return
func _release() -> void:
if held_object == null:
return
held_object.reparent(get_tree().current_scene)
held_object.freeze = false
# Можно пересчитать velocity из недавнего движения контроллера
held_object = null
```
Для серьёзного VR-проекта используйте плагин **godot-xr-tools** (Asset Library) — он имеет
готовые grabbable, teleporter, climbing, hand poses. Аналог Unity XRI.
## Hand tracking
Godot 4.6 поддерживает hand tracking через OpenXR Hand Tracking extension:
1. Project Settings → OpenXR → Extensions → Hand Tracking = enabled.
2. Узлы **`XRHandModifier3D`** (4.6+) или `OpenXRInterface.hand_tracking_enabled` в коде.
3. Получаете доступ к 26 joint'ам каждой руки через `XRPose` lookup.
```gdscript
var xr := XRServer.find_interface("OpenXR")
var left_hand_pose := XRServer.get_tracker("/user/hand/left").get_pose("default")
```
## Performance в VR
Те же жёсткие требования, что и в Unity:
- 72 FPS на Quest 2/3, 90+ на PCVR.
- **Mobile renderer** обязателен для Quest standalone.
- **MSAA 4×** — стандарт для VR.
- **Multiview** rendering (single-pass) — включается через Project Settings → Rendering →
Performance → Multiview.
- **Foveated rendering** — Project Settings → OpenXR → Foveation Level (Low/Med/High).
## Стандартный пайплайн разработки
1. Локальный тест в editor через **Quest Link** (USB или AirLink).
2. После полировки — **standalone build для Quest** (.apk через Android export).
3. Установка через **adb** или **Meta Quest Developer Hub**.
## Plugin godot-xr-tools
Большинство VR-проектов в Godot опираются на community-плагин **godot-xr-tools** (через Asset
Library). Что он даёт:
- **XRToolsFunctionPointer** — луч из контроллера для дальнего выбора.
- **XRToolsFunctionPickup** — продвинутый grab с throw-velocity, snap-points.
- **XRToolsFunctionTeleport** — teleport-locomotion с правильным fade и validation.
- **XRToolsHandPoseController** — анимация поз руки в зависимости от того, что схвачено.
- **XRToolsMovementProvider** — direct/snap/smooth turn, climbing, gliding.
Если планируете серьёзный VR-проект — поставьте сразу.
## Сравнение Godot vs Unity для VR
| | Godot | Unity |
|---|---|---|
| OpenXR в core | ✅ встроено | ❌ требует OpenXR Plugin package |
| Interaction toolkit | ⚠️ community (godot-xr-tools) | ✅ official (XRI) |
| Hand-tracking | ✅ с 4.6 | ✅ XR Hands package |
| Performance в VR | Хорошо | Хорошо (Single-Pass Instanced) |
| Magic Leap / Vision Pro | Limited | Лучше (полная поддержка через XR Hub) |
| Размер билда (минимум) | ~40 MB | ~80 MB |
Для **hobby-проекта Quest 2** Godot часто проще. Для **commercial AAA-VR** Unity всё ещё впереди
за счёт XRI и broader device support.
---
## [Godot] GDExtension — нативные плагины на C++ и Rust
URL: https://cadmus.page/godot/02-3d/27-gdextension/
Section: 3D-разработка в Godot
Description: godot-cpp, gdext (Rust), когда нужны нативные расширения, как они работают.
GDScript хорош для большинства gameplay-логики, но иногда нужно опуститься ниже:
- Воксельный движок с миллионами блоков
- Custom shading в реальном времени с сложной CPU-математикой
- Интеграция со существующей C/C++ библиотекой (физика, NN inference, audio DSP)
- Performance hot path, где даже static-typed GDScript не справляется
**GDExtension** — современный способ расширять Godot на нативном уровне. Заменил GDNative с
Godot 4.0.
## Главные идеи
- Вы пишете код в **C++** (через `godot-cpp`) или **Rust** (через `gdext`).
- Компилируете в **dynamic library** (`.so` / `.dll` / `.dylib`).
- Создаёте манифест `.gdextension`, который Godot загружает при старте.
- В редакторе ваши классы появляются **как обычные узлы** — добавляются в сцену, имеют свойства
в Inspector, можно подключать сигналы.
**Главное преимущество GDExtension над модулями** (которые в monorepo Godot): не нужно
**пересобирать сам редактор**. Меняете плагин → reload Godot → новая версия работает.
Node native modules (`.node` файлы через node-gyp). Или WASM-модули, скомпилированные из
Rust/C++ для браузера.
GDExtension — это `.dll`/`.so`, загружаемый Godot'ом по манифесту. Регистрирует свои классы
в общую систему ClassDB — редактор видит их 1-в-1 с встроенными.
## Структура GDExtension-плагина
```
my_plugin/
├── my_plugin.gdextension ← манифест
├── src/
│ ├── register_types.cpp ← регистрация классов
│ ├── my_node.cpp
│ └── my_node.h
├── bin/ ← собранные .so/.dll
│ ├── libmyplugin.linux.x86_64.so
│ ├── libmyplugin.windows.x86_64.dll
│ └── libmyplugin.macos.universal.dylib
├── SConstruct ← сборка через SCons
└── godot-cpp/ ← submodule с биндингами
```
`my_plugin.gdextension`:
```ini
[configuration]
entry_symbol = "myplugin_library_init"
compatibility_minimum = "4.4"
[libraries]
linux.x86_64 = "bin/libmyplugin.linux.x86_64.so"
windows.x86_64 = "bin/libmyplugin.windows.x86_64.dll"
macos = "bin/libmyplugin.macos.universal.dylib"
```
## Минимальный пример на C++
`my_node.h`:
```cpp
#ifndef MY_NODE_H
#define MY_NODE_H
#include
namespace godot {
class MyNode : public Node3D {
GDCLASS(MyNode, Node3D)
private:
double speed = 1.0;
protected:
static void _bind_methods();
public:
MyNode();
~MyNode();
void _process(double delta) override;
void set_speed(double p_speed);
double get_speed() const;
};
}
#endif
```
`my_node.cpp`:
```cpp
#include "my_node.h"
#include
using namespace godot;
void MyNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_speed", "p_speed"), &MyNode::set_speed);
ClassDB::bind_method(D_METHOD("get_speed"), &MyNode::get_speed);
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "speed"), "set_speed", "get_speed");
}
MyNode::MyNode() {}
MyNode::~MyNode() {}
void MyNode::_process(double delta) {
rotate_y(speed * delta);
}
void MyNode::set_speed(double p_speed) { speed = p_speed; }
double MyNode::get_speed() const { return speed; }
```
После компиляции (`scons` → бинарь в `bin/`) → перезапуск Godot → "MyNode" появится в Add Node
с полем `speed` в Inspector.
## Rust — gdext
Сообщество разработало **gdext** — биндинги для Rust. Стиль более идиоматичный:
```rust
use godot::prelude::*;
struct MyExtension;
#[gdextension]
unsafe impl ExtensionLibrary for MyExtension {}
#[derive(GodotClass)]
#[class(base=Node3D)]
struct MyNode {
#[var]
speed: f64,
base: Base,
}
#[godot_api]
impl INode3D for MyNode {
fn init(base: Base) -> Self {
Self { speed: 1.0, base }
}
fn process(&mut self, delta: f64) {
let rotation_y = self.base().get_rotation().y + (self.speed * delta) as f32;
let mut rot = self.base().get_rotation();
rot.y = rotation_y;
self.base_mut().set_rotation(rot);
}
}
```
Rust-преимущества:
- **Memory safety без overhead** — нет malloc/free ошибок, нет UB.
- **Cargo** — экосистема пакетов проще, чем CMake/SCons для C++.
- **Performance** на уровне C++.
Минусы:
- Чуть медленнее компиляция первого раза.
- Меньше community-плагинов с примерами (чем C++).
gdext активно развивается и считается **production-ready** на 2026 год.
## Когда стоит писать GDExtension
✅ **Стоит:**
- Voxel-engine, terrain LOD, marching cubes.
- Custom physics constraints или soft-body симуляция.
- Real-time DSP / audio synthesis.
- Интеграция со существующей C++ библиотекой (Open3D, OpenCV, библиотеки ML inference).
- Workflows, где даже Burst/SIMD-уровень GDScript недостаточен.
❌ **НЕ стоит:**
- Стандартная gameplay-логика (используйте GDScript).
- Single-shot операции (один раз сгенерировать карту — пишите в WorkerThreadPool из GDScript).
- Когда не уверены, что узкое место именно в CPU вычислениях. **Профайлите сначала**.
## Производительность
Бенчмарки для простого numerical workload (миллион итераций):
| Язык | Время |
|---|---|
| GDScript (untyped) | 850 мс |
| GDScript (typed) | 400 мс |
| C# | 80 мс |
| C++ (GDExtension) | 12 мс |
| Rust (gdext) | 12 мс |
C++ и Rust — **в десятки раз быстрее** typed GDScript. Но overhead начинается при пересечении
GDExtension boundary (вызов C++ метода из GDScript). Поэтому пишут **большие куски в C++**,
вызывая редко.
GDExtension — это нативные бинарники. Linux x86_64 не работает на Windows. Нужно собирать **под
каждую target-платформу**, что усложняет CI/CD. Для одной платформы — просто; для shipping в
Windows + Linux + macOS + Android + iOS — нужны 5 разных CI-job'ов.
## Расширения, которые часто пишут на GDExtension
- **godot-rapier-3d** — Rapier physics на Rust (альтернатива встроенной физике).
- **Voxelity** — voxel engine для Godot.
- **godot-luau-script** — Luau (Roblox-style) скриптинг.
- **NAVigator** — продвинутая навигация.
В Asset Library есть категория "Native plugins" — там готовые GDExtension-плагины.
## Альтернатива — C# в Godot
Если у вас .NET-команда и не хочется лезть в C++, **C#** через `Godot.NET` даёт ~10× ускорение
над GDScript (untyped). См. следующую главу.
## Сравнение с Unity
Unity-аналог:
- **Native Plugin** (`.dll` / `.dylib` / `.so`) загружается через `[DllImport]`.
- **Менее интегрированно** — нативные плагины не появляются как Node-классы в иерархии.
- Альтернатива в Unity для **тяжёлых вычислений** — Job System + Burst (см. главу про Jobs+Burst),
всё внутри C# и без выхода в нативный мир.
GDExtension в Godot — более глубокая интеграция с движком; Burst в Unity — оптимизация без выхода
из C#. Оба подхода правомерны.
---
## [Godot] C# в Godot — практика и нюансы
URL: https://cadmus.page/godot/02-3d/28-csharp-godot/
Section: 3D-разработка в Godot
Description: Godot .NET edition, отличия от GDScript, миграция, gotcha'и для разработчиков из мира Unity.
GDScript — отличный язык, но если вы пришли из Unity (или серьёзного .NET-проекта), хочется
работать на C#. Godot поддерживает это **через отдельную сборку** — Godot **.NET edition**.
## Чем .NET edition отличается
| Параметр | Godot Standard | Godot .NET edition |
|---|---|---|
| Размер редактора | ~60 MB | ~120 MB |
| Скриптинг | GDScript, GDExtension | + C# (.NET 8) |
| Веб-экспорт | ✅ | ❌ (не работает на 2026 май) |
| C# AOT-экспорт | n/a | NativeAOT (с оговорками) |
| .csproj | n/a | Стандартный |
Скачайте .NET-вариант с [godotengine.org/download](https://godotengine.org/download). В Steam-версии
есть отдельный SKU "Godot Engine (.NET)".
Использование Node.js + TypeScript в Express-проекте VS использование Deno или Bun. Базовая
логика та же, но runtime и tooling отличаются.
.NET edition — это Godot с интегрированным Mono/.NET runtime. Узлы, сцены, ресурсы — всё то
же самое; только скрипты можно писать на C#.
## Первый C#-скрипт
```csharp
using Godot;
public partial class Enemy : Node3D
{
[Export]
public int MaxHp { get; set; } = 100;
[Export(PropertyHint.Range, "1,10,0.1")]
public float Speed { get; set; } = 3.0f;
[Signal]
public delegate void DiedEventHandler(Node killer);
private int _hp;
public override void _Ready() {
_hp = MaxHp;
GD.Print($"Enemy spawned with {MaxHp} HP");
}
public override void _PhysicsProcess(double delta) {
// delta — типа double (не float как в Unity)
Position += Vector3.Forward * Speed * (float)delta;
}
public void TakeDamage(int amount, Node attacker) {
_hp -= amount;
if (_hp <= 0) {
EmitSignal(SignalName.Died, attacker);
QueueFree();
}
}
}
```
Ключевые отличия от Unity:
- **`partial`** — обязательно (генератор кода добавляет код для signals).
- **`[Export]`** вместо `[SerializeField]` — атрибут видимости в Inspector.
- **`PropertyHint.Range`** вместо `[Range(1, 10)]` — другой синтаксис.
- **`[Signal] public delegate`** — сигналы декларируются как делегаты с `EventHandler` суффиксом.
- **`_Ready`, `_PhysicsProcess`** (PascalCase) — а не `_ready`, `_physics_process`.
- **`(double)delta`** — нужно явно кастовать в float если используете с Vector3.
## Сравнение синтаксиса с Unity C#
| Unity | Godot C# |
|---|---|
| `class : MonoBehaviour` | `partial class : Node` (или Node2D/Node3D) |
| `[SerializeField]` | `[Export]` |
| `void Update()` | `public override void _Process(double delta)` |
| `void FixedUpdate()` | `public override void _PhysicsProcess(double delta)` |
| `void Start()` | `public override void _Ready()` |
| `transform.position` | `Position` (без `transform`) |
| `Vector3.forward` | `Vector3.Forward` (PascalCase) |
| `gameObject.SetActive(false)` | `Visible = false` (или `ProcessMode = Disabled`) |
| `GetComponent()` | `GetNode("Name")` |
| `Instantiate(prefab)` | `prefab.Instantiate()` |
| `Destroy(go)` | `QueueFree()` |
| `Time.deltaTime` | параметр `delta` метода |
| `Time.time` | `(float)Time.GetTicksMsec() / 1000.0f` |
| `Input.GetKey(...)` | `Input.IsActionPressed("...")` |
| `Debug.Log(...)` | `GD.Print(...)` |
## Сигналы — два пути
Декларация:
```csharp
[Signal]
public delegate void DamagedEventHandler(int amount);
[Signal]
public delegate void DiedEventHandler();
```
Эмит:
```csharp
EmitSignal(SignalName.Damaged, 10);
EmitSignal(SignalName.Died);
```
Подписка:
```csharp
// Type-safe
enemy.Damaged += OnEnemyDamaged;
void OnEnemyDamaged(int amount) {
GD.Print($"Enemy took {amount} damage");
}
```
`SignalName` — автогенерированный класс с константами. IDE даёт autocomplete.
## Resources и Exports
C# Custom Resource — почти как Unity ScriptableObject:
```csharp
[GlobalClass]
public partial class WeaponData : Resource
{
[Export] public string DisplayName { get; set; } = "Pistol";
[Export] public int Damage { get; set; } = 10;
[Export] public float FireRate { get; set; } = 0.5f;
[Export] public PackedScene ProjectilePrefab { get; set; }
}
```
`[GlobalClass]` атрибут регистрирует класс глобально — он появится в FileSystem → New Resource →
WeaponData (как scriptable object в Unity).
Применение:
```csharp
public partial class Weapon : Node3D
{
[Export] public WeaponData Data { get; set; }
[Export] public Marker3D Muzzle { get; set; }
private double _nextFireTime = 0;
public void TryFire() {
var now = Time.GetTicksMsec() / 1000.0;
if (now < _nextFireTime) return;
_nextFireTime = now + Data.FireRate;
var bullet = Data.ProjectilePrefab.Instantiate();
bullet.GlobalTransform = Muzzle.GlobalTransform;
GetTree().CurrentScene.AddChild(bullet);
}
}
```
## Async / Await
Стандартный .NET `async`/`await` работает:
```csharp
public async Task LoadAndShow() {
await ToSignal(GetTree().CreateTimer(1.0), Timer.SignalName.Timeout);
GD.Print("1 second passed");
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
GD.Print("Next frame");
}
```
`ToSignal(node, "signal_name")` — ключевой helper: возвращает Task, который завершается при
получении сигнала.
## Ограничения C# в Godot
### 1. Веб-экспорт не работает
На май 2026 — это **главное ограничение**. C# не экспортируется в HTML5. Команда работает над
решением через .NET 10 и NativeAOT, но в production пока нет. Если цель — браузер, остаётся
GDScript.
### 2. Hot reload ограниченный
GDScript перекомпилируется на лету; C# нужно **остановить Play Mode → пересборка → запустить**.
Это дольше итерации, чем в GDScript.
### 3. .NET 8 (или 9 в зависимости от Godot)
Старые C# фичи доступны; самые свежие — иногда нет. Проверяйте target framework в `.csproj`.
### 4. PackedScene.Instantiate в C# чуть многословнее
```csharp
// GDScript: var enemy = enemy_scene.instantiate()
// C#:
Node enemy = enemyScene.Instantiate();
// Или с generic:
var enemy3D = enemyScene.Instantiate();
```
### 5. NativeAOT export — экспериментально
NativeAOT даёт быстрый startup и меньший размер билда, но не все C# фичи работают (рефлексия,
JIT-генерация кода). С Godot 4.6 — limited support; следите за релизами.
## Когда выбирать C# vs GDScript
✅ **C# — хороший выбор, если:**
- У вас .NET-команда / .NET-задний план.
- Нужны существующие .NET-библиотеки (Newtonsoft.Json, AutoMapper, и др.).
- Большой проект с сильной типизацией важна для maintenance.
- Веб-таргет не нужен.
✅ **GDScript лучше, если:**
- Маленький-средний проект.
- Веб-экспорт обязателен.
- Команда — gamedev'ы, не senior .NET-разработчики.
- Скорость итерации (hot reload) критична.
- Опираетесь на community-плагины (большинство — GDScript).
## Производительность
C# в Godot **примерно ×5–10 быстрее GDScript untyped**, **×2–3 быстрее typed GDScript**. Это
заметно на CPU-bound workload'ах: процедурная генерация, AI с большим количеством агентов,
сложная математика.
Для **gameplay-логики** (90% игрового кода) разница не критична — оба языка справляются.
## Mixing GDScript and C#
Можно. В одном проекте разные узлы могут иметь разные языки. Вызов между ними работает через
общий ClassDB API.
```csharp
// C# вызывает GDScript-метод
var gdNode = GetNode("GDScriptNode");
gdNode.Call("some_method", arg1, arg2);
```
```gdscript
# GDScript вызывает C#-метод
var cs_node = $CSharpNode
cs_node.Call("SomeMethod", arg1, arg2)
```
Не молниеносно (рефлексия под капотом), но работает.
---
## [Godot] Save / Load — сохранение прогресса
URL: https://cadmus.page/godot/02-3d/29-save-load/
Section: 3D-разработка в Godot
Description: FileAccess, ConfigFile, ResourceSaver, JSON, encryption, user:// пути.
В Godot есть несколько встроенных способов сохранять прогресс. Каждый подходит под свою задачу.
## Пути и user:// schema
В Godot для всех пользовательских данных есть специальный путь — **`user://`**. На разных
платформах он указывает в правильное место:
- **Windows**: `%APPDATA%\Godot\app_userdata\[name]\`
- **macOS**: `~/Library/Application Support/Godot/app_userdata/[name]/`
- **Linux**: `~/.local/share/godot/app_userdata/[name]/`
- **Android**: `/data/data/com.example.app/files/`
- **iOS**: `Documents/`
- **Web**: `IndexedDB` через виртуальную ФС
**Главное правило**: никогда не пишите в `res://` (это read-only после билда). Всё пользовательское
идёт в `user://`.
`localStorage` — для primitive (как Godot ConfigFile). `IndexedDB` — для большого
структурированного (как Godot FileAccess + JSON).
`user://` ↔ конкретный sandbox-folder каждой ОС. В Web это виртуальный IndexedDB, прозрачно
для вашего кода.
## ConfigFile — для настроек
`ConfigFile` сохраняет в INI-формате (как .ini-файлы Windows). Идеально для player-config'а:
громкость, разрешение, бинды клавиш.
```gdscript
extends Node
const SETTINGS_PATH := "user://settings.cfg"
func save_settings(master_volume: float, fullscreen: bool, player_name: String) -> void:
var config := ConfigFile.new()
config.set_value("audio", "master_volume", master_volume)
config.set_value("video", "fullscreen", fullscreen)
config.set_value("player", "name", player_name)
config.save(SETTINGS_PATH)
func load_settings() -> Dictionary:
var config := ConfigFile.new()
var err := config.load(SETTINGS_PATH)
if err != OK:
return {} # файл ещё не существует
return {
"master_volume": config.get_value("audio", "master_volume", 1.0), # default 1.0
"fullscreen": config.get_value("video", "fullscreen", false),
"player_name": config.get_value("player", "name", "Player"),
}
```
На диске получится:
```ini
[audio]
master_volume=0.8
[video]
fullscreen=true
[player]
name="Konstantin"
```
Это **читаемо, версионируемо, легко редактируется руками** — идеально для настроек, где
безопасность не важна.
## FileAccess — для произвольных файлов
Для структурированных сейвов (с массивами, словарями, custom data) — `FileAccess` + JSON:
```gdscript
extends Node
const SAVE_PATH := "user://save.json"
func save_game(state: Dictionary) -> Error:
var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if file == null:
return FileAccess.get_open_error()
file.store_string(JSON.stringify(state, "\t")) # \t — pretty-print indent
return OK
func load_game() -> Dictionary:
if not FileAccess.file_exists(SAVE_PATH):
return {}
var file := FileAccess.open(SAVE_PATH, FileAccess.READ)
if file == null:
push_error("Failed to open save file: %s" % FileAccess.get_open_error())
return {}
var content := file.get_as_text()
var parsed = JSON.parse_string(content)
return parsed if parsed != null else {}
```
Использование:
```gdscript
var state := {
"version": 1,
"player": {
"name": "Konstantin",
"level": 5,
"hp": 80,
"position": [10.5, 2.0, -3.2], # Vector3 не JSON-сериализуем напрямую
},
"inventory": ["sword", "potion", "potion"],
"play_time_seconds": 3600.5,
"active_quests": ["q_main_01", "q_side_03"],
}
save_game(state)
# Позже:
var loaded := load_game()
print(loaded.player.name)
```
`JSON.stringify` **не сериализует Godot-типы** (`Vector3`, `Color`, `Transform3D`). Конвертируйте
в массивы:
```gdscript
var pos := player.global_position
state.player.position = [pos.x, pos.y, pos.z]
# При загрузке:
player.global_position = Vector3(loaded.player.position[0],
loaded.player.position[1],
loaded.player.position[2])
```
## ResourceSaver / ResourceLoader — нативно для Godot Resource
Если ваше состояние представимо как `Resource`-подкласс, можно использовать встроенную сериализацию:
```gdscript
# save_data.gd
class_name SaveData extends Resource
@export var player_name: String = "Player"
@export var level: int = 1
@export var hp: int = 100
@export var position: Vector3 = Vector3.ZERO
@export var inventory: Array[String] = []
```
```gdscript
# save_system.gd
const SAVE_PATH := "user://save.tres"
func save_game(data: SaveData) -> void:
ResourceSaver.save(data, SAVE_PATH)
func load_game() -> SaveData:
if not ResourceLoader.exists(SAVE_PATH):
return SaveData.new()
return load(SAVE_PATH) as SaveData
```
**Плюсы**:
- Vector3, Color, и другие Godot-типы сериализуются автоматически.
- Файл человекочитаемый (`.tres`).
- Можно открывать в Godot Editor для отладки.
**Минусы**:
- Жёсткая привязка к structure — миграция между версиями требует осторожности.
- Безопасность нулевая (файл легко правится).
## Бинарный формат — `.res`
Если хотите компактный и быстрый формат:
```gdscript
ResourceSaver.save(data, "user://save.res", ResourceSaver.FLAG_COMPRESS)
```
`.res` — бинарный (быстрее парсится), плюс `FLAG_COMPRESS` — компрессия zstd. Это **в десятки
раз компактнее JSON** для больших сейвов.
## Шифрование
### Простой XOR
```gdscript
const KEY := 0x5A
func encrypt_xor(text: String) -> PackedByteArray:
var bytes := text.to_utf8_buffer()
for i in bytes.size():
bytes[i] ^= KEY
return bytes
func decrypt_xor(bytes: PackedByteArray) -> String:
for i in bytes.size():
bytes[i] ^= KEY
return bytes.get_string_from_utf8()
```
### AES через FileAccess (встроенный в Godot)
Godot имеет встроенный `FileAccess.open_encrypted_with_pass`:
```gdscript
func save_encrypted(state: Dictionary, password: String) -> void:
var file := FileAccess.open_encrypted_with_pass(
"user://save.dat", FileAccess.WRITE, password)
file.store_string(JSON.stringify(state))
func load_encrypted(password: String) -> Dictionary:
if not FileAccess.file_exists("user://save.dat"):
return {}
var file := FileAccess.open_encrypted_with_pass(
"user://save.dat", FileAccess.READ, password)
if file == null:
push_warning("Wrong password or corrupted file")
return {}
var content := file.get_as_text()
return JSON.parse_string(content)
```
⚠️ Как и в Unity: пароль в коде → reverse-engineering найдёт. Это защита от **казуальных
читеров**, не более.
## Версионирование сейвов
```gdscript
const CURRENT_VERSION := 3
func load_game() -> Dictionary:
var data := _read_save()
if data.is_empty():
return _default_state()
var version: int = data.get("version", 1)
if version < CURRENT_VERSION:
data = _migrate(data, version)
return data
func _migrate(data: Dictionary, from_version: int) -> Dictionary:
if from_version == 1:
# v1 → v2: добавили inventory
data.inventory = []
from_version = 2
if from_version == 2:
# v2 → v3: переименовали поле
data.player_level = data.get("level", 1)
data.erase("level")
from_version = 3
data.version = CURRENT_VERSION
return data
```
Версионируйте с самого начала. Добавить поле `version` после релиза — больно.
## Async-сохранение через WorkerThreadPool
Если сейв большой и стопорит main thread:
```gdscript
extends Node
var _save_task_id: int = -1
func save_async(state: Dictionary) -> void:
var json := JSON.stringify(state)
_save_task_id = WorkerThreadPool.add_task(_write_to_disk.bind(json))
func _write_to_disk(json: String) -> void:
var file := FileAccess.open("user://save.json", FileAccess.WRITE)
file.store_string(json)
func _process(_delta: float) -> void:
if _save_task_id != -1 and WorkerThreadPool.is_task_completed(_save_task_id):
WorkerThreadPool.wait_for_task_completion(_save_task_id)
_save_task_id = -1
print("Save complete")
```
См. [главу про Threading](/godot/02-3d/22-threading/) для деталей.
## Auto-save паттерн
```gdscript
extends Node
@export var interval_seconds: float = 60.0
@onready var _timer := Timer.new()
func _ready() -> void:
_timer.wait_time = interval_seconds
_timer.timeout.connect(_on_autosave)
add_child(_timer)
_timer.start()
func _on_autosave() -> void:
var state := GameState.capture_state()
SaveSystem.save_game(state)
# show toast "💾 Сохранено"
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_CLOSE_REQUEST or what == NOTIFICATION_APPLICATION_PAUSED:
_on_autosave()
get_tree().quit()
```
`NOTIFICATION_APPLICATION_PAUSED` срабатывает на mobile при сворачивании — гарантированно
успеваем сохранить.
## Cloud Saves
Godot core не имеет встроенного Cloud Save. Решения:
- **Steam Cloud** — через GodotSteam модуль (community).
- **Google Play Games Services** — через AssetLib плагин.
- **Custom backend** — пишете API → HTTPRequest узел общается с сервером.
## Подводные камни
1. **`res://` — read-only в билде.** Только `user://`.
2. **`Vector3` и др. Godot-типы — НЕ JSON.** Конвертируйте в массивы или используйте Resource.
3. **JSON.parse_string возвращает null при ошибке.** Всегда проверяйте.
4. **Тестируйте миграцию** — добавили поле → проверьте старый сейв.
5. **WebGL — IndexedDB лимиты.** Браузер может ограничить ваш storage (~50MB+). Большие
сохранения дробите.
---
## [Godot] Глоссарий — Godot термины и параллели
URL: https://cadmus.page/godot/03-glossary/01-terms/
Section: Глоссарий
Description: Короткие определения с веб-аналогами и параллелями к Unity.
Сводный список ключевых терминов Godot, упомянутых в энциклопедии. В скобках — Unity-аналог,
если он есть.
## Сцена и узлы
**Node** — базовый класс всего в дереве сцены. Унаследованные классы: `Node2D`, `Node3D`,
`Control`, `CanvasItem`. (Unity: GameObject, но без stack of components — один скрипт на узел.)
**Node3D** (раньше Spatial) — узел с 3D-трансформом. (Unity: GameObject + Transform.)
**Control** — базовый класс UI-узлов. С anchors, offsets, layout. (Unity: RectTransform / Control в uGUI.)
**Scene Tree** — иерархия живых узлов в работающей игре. Доступ через `get_tree()`.
**PackedScene** — сериализованная сцена-`.tscn`. (Unity: Scene + Prefab в одном.)
**Inherited Scene** — сцена, унаследованная от другой, с возможностью переопределения.
(Unity: Prefab Variant.)
**Group** — лейбл-категория для узлов: `add_to_group("enemies")`. (Unity: Tag.)
## Скриптинг
**GDScript** — родной язык Godot. Python-like, опциональная статическая типизация.
**`@export`** — аннотация: показать поле в Inspector. (Unity: `[SerializeField]`.)
**`@onready`** — присвоить поле в `_ready` (часто `$ChildNode` через шорткат).
**`class_name`** — глобальная регистрация класса в редакторе.
**Signal** — встроенный pub-sub на любом узле. (Unity: UnityEvent / C# event.)
**`@rpc`** — annotation для удалённого вызова в мультиплеере.
## Жизненный цикл
**`_init`** — конструктор объекта (до scene tree).
**`_enter_tree`** — узел добавлен в дерево. (Unity: Awake.)
**`_ready`** — все дети готовы. (Unity: Start.)
**`_input` / `_unhandled_input`** — обработка событий ввода.
**`_process(delta)`** — каждый кадр. (Unity: Update.)
**`_physics_process(delta)`** — фиксированный тик 60 Hz. (Unity: FixedUpdate.)
**`_exit_tree`** — удаление из дерева. (Unity: OnDestroy / OnDisable.)
## Физика
**StaticBody3D** — неподвижное тело. (Unity: Static collider, без Rigidbody.)
**AnimatableBody3D** — двигается через скрипт/анимацию, толкает другие. (Unity: Kinematic Rigidbody.)
**RigidBody3D** — полная динамика. (Unity: Rigidbody.)
**CharacterBody3D** — кинематика для персонажа, метод `move_and_slide()`. (Unity: CharacterController.)
**Area3D** — невидимая область с триггер-сигналами. (Unity: Collider c isTrigger=true.)
**CollisionShape3D** — отдельный дочерний узел со shape-ресурсом. (Unity: Collider component.)
**SoftBody3D** — мягкие тела (ткань, флаги). Mesh-based soft body simulation.
**Jolt** — рекомендованный 3D-физик-движок с 4.6 (default для новых проектов).
**Godot Physics** — встроенный, всё ещё default для 2D, опция для 3D.
## Рендеринг
**Forward+** — главный рендерер для десктопа/консолей. Vulkan/Metal/D3D12.
**Mobile** — для мобильных и VR. Single-pass forward.
**Compatibility** — OpenGL ES 3 / WebGL 2, единственный путь для веба.
**BaseMaterial3D / StandardMaterial3D / ORMMaterial3D** — стандартные PBR-материалы.
**ShaderMaterial** — обёртка для кастомного `.gdshader`.
**gdshader** — язык шейдеров Godot, диалект GLSL ES 3.0.
**Visual Shader** — визуальный редактор шейдеров. (Unity: Shader Graph.)
**MultiMeshInstance3D** — рендерит N экземпляров меша одним draw call. (Unity: GPU Instancing.)
**WorldEnvironment** + **Environment** — постпроцесс и settings сцены. (Unity: Volume Framework.)
## Освещение
**DirectionalLight3D** — солнце. (Unity: Directional Light.)
**OmniLight3D** — точечный. (Unity: Point Light.)
**SpotLight3D** — конус. (Unity: Spot Light.)
**LightmapGI** — запекание лайтмапов для статики. (Unity: Progressive Lightmapper.)
**VoxelGI** — voxel-based realtime GI.
**SDFGI** — signed distance field GI, динамический.
**LightmapProbe** — точка для GI динамических объектов. (Unity: Light Probe.)
**ReflectionProbe** — карта окружения для отражений. (Unity: Reflection Probe.)
## Анимация
**AnimationPlayer** — основной узел проигрывания анимаций.
**Animation** + **AnimationLibrary** — клип и его контейнер.
**AnimationTree** — state machine + blendspaces. (Unity: Animator Controller.)
**AnimationNodeStateMachine** — FSM в AnimationTree.
**AnimationNodeBlendSpace1D / 2D** — параметрический блендинг. (Unity: Blend Tree.)
**Skeleton3D** — иерархия костей. (Unity: Avatar / Skeleton.)
**BoneAttachment3D** — узел, следящий за костью. (Unity: AvatarBone parenting.)
**SkeletonModifier3D** / **IKModifier3D** — базовые классы для модификаторов скелета (в т.ч. IK).
**TwoBoneIK3D / ChainIK3D / SplineIK3D / IterateIK3D / FABRIK3D / CCDIK3D / JacobianIK3D** — IK-солверы (в 4.6+). Унаследованы от `IKModifier3D`.
## Аудио
**AudioStreamPlayer / 2D / 3D** — узлы воспроизведения. (Unity: AudioSource.)
**AudioStreamInteractive** — динамическая музыка с переходами.
**AudioBus** — канал микшера. (Unity: AudioMixer Group.)
**AudioListener3D** — слушатель, по умолчанию активная Camera3D. (Unity: AudioListener.)
## UI
**Control** — базовый UI-узел.
**Container** (HBox, VBox, Grid, Margin, Center, Aspect) — авто-layout.
**Theme** — ресурс со стилями всего UI.
**StyleBox** (Flat, Texture, Line, Empty) — фон/рамка.
**CanvasLayer** — слой выше / ниже сцены.
**SubViewport** — рендеринг в текстуру (для UI в 3D-мире). (Unity: World Space Canvas.)
## Ресурсы
**Resource** — базовый класс data-ассетов. (Unity: ScriptableObject и др.)
**Custom Resource** — пользовательский data-only объект (`class_name X extends Resource`).
**`.tres`** — текстовый формат ресурса.
**`.res`** — бинарный формат.
**preload** — статическая загрузка при парсинге скрипта.
**load** — синхронная загрузка в рантайме.
**ResourceLoader.load_threaded_request** — асинхронная загрузка. (Unity: Addressables.)
**PCK** — упакованный архив ассетов. (Unity: AssetBundle / Asset Pack.)
**`uid://`** — UID-схема для идентификации ресурсов независимо от пути. Введена в **Godot 4.0**,
расширена в **4.4** (поддержка scripts и shader-ресурсов). Путь к файлу может меняться, UID
остаётся. Используется в `.tscn`/`.tres` для устойчивых ссылок.
## Навигация
**NavigationServer3D** — низкоуровневый сервер.
**NavigationRegion3D** — узел с запечённым NavigationMesh. (Unity: NavMeshSurface.)
**NavigationAgent3D** — агент-помощник для перемещения. (Unity: NavMeshAgent.)
**NavigationLink3D** — ручная связь между регионами. (Unity: NavMesh Off-Mesh Link.)
**NavigationObstacle3D** — динамическое препятствие. (Unity: NavMeshObstacle.)
## Частицы
**GPUParticles3D** — на GPU, миллионы частиц. (Unity: VFX Graph.)
**CPUParticles3D** — на CPU, fallback. (Unity: built-in Particle System.)
**ParticleProcessMaterial** — ресурс с поведением частиц.
**GPUParticlesCollision*3D** — отдельные коллайдеры для GPU-частиц.
## Мультиплеер
**MultiplayerAPI** — высокий уровень. Геттер `multiplayer` на любом узле.
**ENetMultiplayerPeer** — UDP-транспорт.
**WebSocketMultiplayerPeer** — для веба.
**MultiplayerSpawner** — авто-репликация спавна.
**MultiplayerSynchronizer** — декларативная синхронизация properties. (Unity: NetworkVariable, NetworkTransform.)
**Multiplayer Authority** — peer ID владельца узла. (Unity: NetworkObject Owner.)
## Сборка
**Export Preset** — конфигурация для одной платформы.
**Export Template** — бинарь движка без редактора.
**GDExtension** — нативный плагин (C++ через `godot-cpp` или Rust через `gdext`). (Unity: native plugin.)
**user://** — sandbox-папка для сохранений и пользовательских настроек.
Официальная документация: `docs.godotengine.org/en/stable`. Убедитесь, что смотрите на свою
версию (в URL `/en/4.6/...` или `/en/stable/...`). API-Reference покрывает все классы, методы,
сигналы — обычно достаточно поиска по имени класса.
---
# Phaser 4 Encyclopedia
## [Phaser] Что такое Phaser 4?
URL: https://cadmus.page/phaser/01-start/what-is-phaser-4/
Section: Старт
Description: Знакомство с Phaser 4 — что это такое, чем он не является и чем отличается от Phaser 3.
Phaser 4 — это фреймворк для создания 2D-игр в вебе. Он отрисовывает графику через WebGL (с откатом на Canvas), работает во всех современных браузерах и поставляется как библиотека, изначально ориентированная на TypeScript.
## Что вы получаете
- Архитектуру на основе сцен для организации состояния игры и отрисовки.
- Список отображения игровых объектов с сохранением состояния (спрайты, текст, фигуры, контейнеры).
- Загрузчик ресурсов с типизированным кэшем.
- Подключаемую физику (Arcade для быстрой обработки AABB, Matter для полноценной физики твёрдых тел).
- Ввод с клавиатуры, указателя (мышь + сенсор) и геймпада.
- Систему тайловых карт, систему частиц, менеджер анимаций, аудиоконвейер и движок твинов для интерполяции свойств.
## Чем Phaser 4 *не* является
- Это не 3D-движок. Для этого используйте Three.js, Babylon или PlayCanvas.
- Это не универсальный фреймворк для приложений. Применяйте его для игр и игроподобных интерактивов.
- Это не редактор. Данные мира и уровней создаются в других инструментах (например, в Tiled) и загружаются во время выполнения.
## Переходите с Phaser 3?
Phaser 4 сохраняет общую форму API Phaser 3, но перестраивает внутреннее устройство вокруг современного TypeScript, более строгих типов и более компактного рендерера. О пути обновления смотрите в разделе Миграция с Phaser 3.
---
## [Phaser] Установка
URL: https://cadmus.page/phaser/01-start/installation/
Section: Старт
Description: Установка Phaser 4 через npm или CDN и проверка установки на однострочной сцене.
## Из npm
```sh
# npm
npm install phaser
# bun
bun add phaser
```
Phaser 4 поставляется с собственными типами TypeScript — пакет `@types/phaser` не нужен.
## Из CDN
Для быстрых экспериментов или однофайловых демо:
```html
```
`@4` отслеживает последний релиз ветки 4.x. В продакшене закрепляйте точную версию (например, `phaser@4.1.0`), чтобы новый релиз не изменил поведение вашего кода. Текущие релизы смотрите в списке изменений.
## Проверка установки
Минимально возможная программа на Phaser 4 — чёрный холст, готовый принимать игровые объекты:
```ts
new Phaser.Game({
type: Phaser.AUTO,
width: 400,
height: 300,
scene: { create() { this.add.text(10, 10, 'Phaser 4 is alive'); } },
});
```
Если вы видите текст на чёрном фоне — всё настроено. Переходите к разделу [Ваша первая сцена](/phaser/01-start/first-scene/).
---
## [Phaser] Ваша первая сцена
URL: https://cadmus.page/phaser/01-start/first-scene/
Section: Старт
Description: Создание и запуск вашей первой сцены Phaser 4 — прыгающего логотипа — в изолированной песочнице.
Программа на Phaser 4 — это как минимум `Game`, настроенный хотя бы с одной `Scene`. У сцены есть три хука жизненного цикла, которые вы будете использовать постоянно:
- `preload()` — загрузка ресурсов в кэш.
- `create()` — построение начального графа сцены.
- `update(time, delta)` — выполнение покадровой логики.
Вот самая маленькая интересная программа: спрайт, отскакивающий в пределах своих границ. Отредактируйте исходный код в собственном проекте — и тот же код будет работать точно так же.
## Что только что произошло
1. **`Phaser.AUTO`** просит Phaser выбрать наилучший рендерер (WebGL там, где он доступен, иначе Canvas).
2. Конфигурация `physics` включает физику Arcade без гравитации, поэтому спрайт движется по прямой, пока что-нибудь его не отклонит.
3. `this.load.image(key, url)` ставит ресурс в очередь; загрузчик разрешает его до запуска `create()`.
4. `this.physics.add.image(...)` создаёт спрайт, участвующий в физике, — именно поэтому у него есть `setVelocity` и `setBounce`.
5. `setCollideWorldBounds(true)` превращает ограничивающую рамку игры в коллайдер, давая нам эффект «отскока».
## Куда двигаться дальше
- Подробно изучите [жизненный цикл сцены](/phaser/02-concepts/scenes/).
- Разберитесь, как организованы [игровые объекты](/phaser/02-concepts/game-objects/).
- Выберите физическую систему в [руководстве по физике](/phaser/03-guides/physics-arcade/).
---
## [Phaser] Настройка проекта
URL: https://cadmus.page/phaser/01-start/project-setup/
Section: Старт
Description: Рекомендуемая структура проекта, инструменты сборки и настройки TypeScript для серьёзных проектов на Phaser 4.
Для наброска или однофайлового демо достаточно фрагмента из раздела [установка](/phaser/01-start/installation/). Для настоящего проекта несколько решений сэкономят вам недели.
## Сборщик
Vite — рекомендуемая отправная точка: Phaser 4 поставляется в формате ESM, а Vite обрабатывает URL ресурсов и HMR без настройки.
```sh
npm create vite@latest my-game -- --template vanilla-ts
cd my-game
npm install phaser
```
## Структура каталогов
Структура, которая масштабируется от джем-игры до выпущенного проекта:
```
src/
main.ts ← создаёт Game и регистрирует сцены
scenes/
BootScene.ts ← предзагружает ресурсы глобальной полосы загрузки
PreloadScene.ts ← предзагружает всё остальное, показывает прогресс
TitleScene.ts
GameScene.ts
objects/ ← переиспользуемые подклассы GameObject
systems/ ← игровая логика, не зависящая от фреймворка
data/ ← типизированные манифесты ресурсов, таблицы конфигурации
public/
assets/ ← изображения, аудио, тайловые карты, атласы
```
По возможности держите `systems/` свободным от импортов Phaser — чистую логику проще тестировать.
## TypeScript
Phaser 4 поставляется со строгими типами. Используйте их:
```jsonc
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true
}
}
```
## Нюансы горячей перезагрузки
HMR в Vite работает для всего, что находится *вне* запущенной сцены. Чтобы подхватить изменения внутри сцены, перезапустите её через `this.scene.restart()` или используйте предназначенную только для разработки привязку клавиши, делающую то же самое.
---
## [Phaser] Основные концепции
URL: https://cadmus.page/phaser/02-concepts/00-overview/
Section: Концепции
Description: Ментальная модель, лежащая в основе Phaser 4 — для чего нужна каждая подсистема и как они сочетаются друг с другом.
Страницы этого раздела объясняют, *почему* Phaser устроен именно так. Это не рецепты — за рецептами обращайтесь к разделу [Руководства](/phaser/03-guides/00-overview/), — но используемая здесь терминология встречается на протяжении всей остальной энциклопедии.
## Основные подсистемы
- **[Игровой цикл](/phaser/02-concepts/game-loop/)** — как планируются `update` и `render` и что на самом деле означает `delta`.
- **[Сцены](/phaser/02-concepts/scenes/)** — единица «экрана» или «режима» в Phaser и то, как несколько сцен сосуществуют.
- **[Игровые объекты](/phaser/02-concepts/game-objects/)** — сохраняемый (retained) список отображения, трансформации и разница между `Image` и `Sprite`.
- **[Камеры](/phaser/02-concepts/cameras/)** — вьюпорты, прокрутка, масштабирование и эффекты.
- **[Ввод](/phaser/02-concepts/input/)** — клавиатура, указатель и геймпад, а также то, как Phaser направляет события объектам.
- **[Загрузчик и ресурсы](/phaser/02-concepts/loader/)** — конвейер загрузки и типизированный кеш, обслуживающий всё остальное.
Если вы прочитаете в этом разделе только одну страницу, прочитайте [Сцены](/phaser/02-concepts/scenes/). Почти всё остальное выстроено вокруг них.
---
## [Phaser] Игровой цикл
URL: https://cadmus.page/phaser/02-concepts/game-loop/
Section: Концепции
Description: Как Phaser 4 планирует update и render, что означает delta и как писать код, независимый от частоты кадров.
Каждый кадр Phaser выполняет примерно следующее:
1. Опустошает очередь ввода (события указателя, клавиатуры, геймпада, полученные с прошлого кадра).
2. Вызывает `update(time, delta)` каждой активной сцены.
3. Выполняет шаг физического мира.
4. Отрисовывает список отображения на канвас.
`time` — это монотонная отметка времени в миллисекундах; `delta` — число миллисекунд, прошедших с предыдущего кадра.
## Независимость от частоты кадров
Самое важное правило:
> Умножайте перемещение и скорости на `delta`, а не на константу.
```ts
// Неправильно — при 144fps ваш спрайт движется в 2,4 раза быстрее.
sprite.x += 5;
// Правильно — скорость в «пикселях в секунду», независимо от частоты кадров.
sprite.x += 200 * (delta / 1000);
```
Это касается физических скоростей, которые вы задаёте вручную, таймеров анимации, темпов эмиссии частиц и всего остального, что выражается «в секунду».
## Фиксированный шаг против переменного шага
По умолчанию Phaser использует переменный шаг времени — `delta` отражает то, что произошло на самом деле. Для детерминированной симуляции (синхронный (lockstep) мультиплеер, записанные повторы) используйте цикл с фиксированным шагом внутри `update`:
```ts
private accumulator = 0;
private readonly STEP = 1000 / 60;
update(_time: number, delta: number) {
this.accumulator += delta;
while (this.accumulator >= this.STEP) {
this.tick(this.STEP);
this.accumulator -= this.STEP;
}
}
```
## Связанные темы
- [Сцены](/phaser/02-concepts/scenes/) — где располагается покадровый `update`.
- Руководства по физике — о том, как выполняется шаг физического мира относительно основного цикла.
---
## [Phaser] Сцены
URL: https://cadmus.page/phaser/02-concepts/scenes/
Section: Концепции
Description: Единица «экрана» или «режима» в Phaser — жизненный цикл, параллельные сцены и передача данных.
**Сцена** — это самодостаточный мир: ей принадлежат список отображения, камера, обработчик ввода и (опционально) физический мир. `Game` запускает одну или несколько сцен; у каждой свой цикл обновления.
## Жизненный цикл
| Хук | Когда выполняется | Типичное применение |
|----------------|-------------------------------------------------------------|--------------------------------------|
| `init(data)` | Один раз, перед `preload`. Получает данные от `scene.start`. | Чтение параметров, сброс полей класса. |
| `preload()` | Один раз, перед `create`. Загрузчик отрабатывает до конца. | Постановка ресурсов в очередь загрузки. |
| `create(data)` | Один раз. Загрузчик завершил работу. | Построение графа сцены. |
| `update(t, d)` | Каждый кадр после `create`. | Покадровая логика. |
| `shutdown()` | Когда сцена останавливается (например, `scene.stop`, `scene.start` другой сцены). | Снятие слушателей, таймеров, звуков, твинов, запущенных в `create`. |
| `destroy()` | Когда сцена окончательно удаляется из игры. | Финальная очистка; редкость в приложениях, которые просто переключаются между сценами. |
Сцена, которая была остановлена и затем запущена снова, увидит повторный вызов `init` / `preload` / `create` — соотносите каждый `shutdown` с тем `create`, после которого он наводит порядок.
## Несколько сцен одновременно
Сцены не являются взаимоисключающими. Распространённые шаблоны:
- Постоянная сцена **HUD** работает параллельно с игровой сценой.
- Сцена **меню паузы** располагается поверх; игровая сцена приостановлена, но не остановлена.
- Долгоживущая сцена **аудио** или **сохранений** существует исключительно ради сквозного состояния.
```ts
// Запустить геймплей, затем наложить HUD сверху.
this.scene.start('Game');
this.scene.launch('HUD');
```
`scene.start(key)` заменяет текущую сцену; `scene.launch(key)` запускает новую сцену рядом с ней.
## Передача данных между сценами
```ts
this.scene.start('Game', { level: 3, seed: 0xdead });
// Внутри Game:
init(data: { level: number; seed: number }) {
this.level = data.level;
}
```
Для сквозного состояния, которое переживает любую отдельную сцену (профиль игрока, настройки, данные сохранений), используйте **реестр** сцены (registry) или обычное хранилище уровня модуля. Энциклопедия рекомендует обычное хранилище, если только вам специально не нужна интеграция с событиями сцен.
## Связанные темы
- [Игровой цикл](/phaser/02-concepts/game-loop/) — откуда вызывается `update`.
- [Игровые объекты](/phaser/02-concepts/game-objects/) — что содержит список отображения сцены.
---
## [Phaser] Игровые объекты
URL: https://cadmus.page/phaser/02-concepts/game-objects/
Section: Концепции
Description: Сохраняемый (retained-mode) список отображения — что такое игровые объекты, как работает иерархия трансформаций и когда какой встроенный тип использовать.
**Игровой объект** — это всё, что живёт в графе сцены и может быть отрисовано, трансформировано или проверено на попадание. Phaser использует рендеринг в *retained-mode (сохраняемом режиме)*: вы создаёте объекты один раз, изменяете их свойства, а рендерер отрисовывает то, что существует в текущий момент. Никакого покадрового вызова «нарисовать спрайт в (x, y)» нет.
## Встроенные типы
| Тип | Используйте, когда |
|---------------|-------------------------------------------------------------|
| `Image` | Статический текстурированный прямоугольник. Без анимации, без покадровой логики. |
| `Sprite` | `Image` с присоединённым менеджером анимации. |
| `Text` | Отрисованный текст. Недорого, если не менять его каждый кадр. |
| `BitmapText` | Текст, отрисованный из атласа шрифта. Недорого, *даже* если изменять его. |
| `Container` | Группа дочерних объектов, разделяющих общую трансформацию. |
| `Graphics` | Фигуры в immediate-режиме, отрисованные в один объект. |
| `TileSprite` | Текстура, замощённая по области; дёшево для фонов. |
Эмпирическое правило: сначала тянитесь к `Image`; переходите к `Sprite` только тогда, когда нужны анимации.
## Иерархия трансформаций
Контейнеры образуют дерево. Мировая трансформация каждого дочернего объекта — это его локальная трансформация, скомпонованная с трансформацией его родителя. Именно так строятся составные объекты:
```ts
const ship = this.add.container(200, 200);
ship.add(this.add.image(0, 0, 'hull'));
ship.add(this.add.image(0, -20, 'turret'));
ship.setAngle(45); // поворачивает корпус и башню вместе
```
## Добавление против создания
`this.add.image(...)` одновременно **создаёт** объект *и* **добавляет** его в список отображения сцены. Чтобы создать без добавления (например, чтобы добавить его только в контейнер), используйте `this.make.image(...)`.
## Связанные темы
- [Сцены](/phaser/02-concepts/scenes/) — владельцы списка отображения.
- [Камеры](/phaser/02-concepts/cameras/) — то, что определяет, какие объекты отрисовываются и где.
---
## [Phaser] Камеры
URL: https://cadmus.page/phaser/02-concepts/cameras/
Section: Концепции
Description: Вьюпорты, прокрутка, масштабирование, следование и эффекты — как Phaser решает, что и где отрисовывать.
**Камера** — это вьюпорт в мировое пространство сцены. У каждой сцены есть как минимум одна (`this.cameras.main`); вы можете добавить ещё для разделённого экрана, миникарт и «картинки в картинке».
## Мировые против экранных координат
- **Мировые координаты** — это место, где живут игровые объекты. Они не меняются при движении камеры.
- **Экранные координаты** — это место, куда пиксели попадают на канвас.
Камера отображает одни в другие через `scroll`, `zoom` и `rotation`. Когда нужен оверлей в экранном пространстве (HUD), поместите его в отдельную сцену, камера которой не прокручивается.
## Следование за целью
```ts
this.cameras.main.startFollow(player, true, 0.1, 0.1);
this.cameras.main.setBounds(0, 0, world.width, world.height);
```
Коэффициенты линейной интерполяции `0.1` сглаживают следование — `1` означает мгновенно, `0` — никогда не двигается.
## Эффекты
Встроенные переходы:
- `shake(duration, intensity)` — тряска экрана.
- `flash(duration, r, g, b)` — полноэкранная цветовая вспышка.
- `fade(duration, r, g, b)` — затухание в цвет.
- `pan(x, y, duration)` — перемещение к точке мира.
- `zoomTo(zoom, duration)` — анимация масштабирования.
Каждый возвращает похожий на твин дескриптор, который можно объединять в цепочку или ожидать через `await` посредством события завершения.
## Несколько камер
```ts
const minimap = this.cameras.add(600, 10, 200, 150)
.setZoom(0.2)
.setBackgroundColor(0x002244);
minimap.ignore([hudLayer]); // не отрисовывать HUD на миникарте
```
Используйте `camera.ignore(...)`, чтобы исключить определённые объекты из определённой камеры.
## Связанные темы
- [Сцены](/phaser/02-concepts/scenes/) — каждой сцене принадлежит свой менеджер камер.
- [Ввод](/phaser/02-concepts/input/) — координаты указателя нужно преобразовать через камеру, чтобы попасть по объектам мира.
---
## [Phaser] Ввод
URL: https://cadmus.page/phaser/02-concepts/input/
Section: Концепции
Description: Клавиатура, указатель (мышь + касания) и геймпад — и то, как Phaser направляет события игровым объектам.
Phaser объединяет клавиатуру, указатель (мышь + касания) и геймпад за единым менеджером ввода, доступным как `this.input`.
## Опрос против событий
Два допустимых стиля:
```ts
// Событийный стиль — реакция на один переход.
this.input.keyboard!.on('keydown-SPACE', () => this.jump());
// Стиль опроса — проверка состояния каждый кадр в update().
const cursors = this.input.keyboard!.createCursorKeys();
update() {
if (cursors.left.isDown) this.player.x -= 4;
}
```
Опрос подходит для непрерывных действий (движение); события подходят для дискретных действий (прыжок, выстрел, выбор в меню).
## Указатель
```ts
this.input.on('pointerdown', (pointer) => {
const world = this.cameras.main.getWorldPoint(pointer.x, pointer.y);
this.spawnAt(world.x, world.y);
});
```
Указатель всегда сообщает экранные координаты. Используйте `camera.getWorldPoint` для преобразования.
## Интерактивность отдельных объектов
Для семантики «кликнуть по этому спрайту» включите объект в обработку:
```ts
button.setInteractive({ useHandCursor: true });
button.on('pointerdown', () => this.scene.start('Game'));
```
Phaser проверяет границы объекта (по умолчанию прямоугольник; попиксельно, если запрошено) и генерирует события только тогда, когда указатель действительно пересекает их.
## Геймпад
Поддержка геймпада по умолчанию выключена — включите её в конфигурации игры:
```ts
input: { gamepad: true }
```
Затем читайте `this.input.gamepad!.pad1?.buttons[0].pressed` и т. п. в `update`.
## Связанные темы
- [Камеры](/phaser/02-concepts/cameras/) — источник `getWorldPoint`.
- [Сцены](/phaser/02-concepts/scenes/) — у каждой сцены свои обработчики ввода; приостановка сцены приостанавливает её ввод.
---
## [Phaser] Загрузчик и ресурсы
URL: https://cadmus.page/phaser/02-concepts/loader/
Section: Концепции
Description: Как работает конвейер ресурсов — постановка в очередь в preload, типизированный кеш и шаблоны для больших игр.
У каждой сцены есть **загрузчик** (`this.load`), который ставит запросы на ресурсы в очередь во время `preload()`. Когда `preload()` возвращает управление, загрузчик отрабатывает до конца, прежде чем будет вызван `create()`.
## Базовая форма
```ts
preload() {
this.load.image('player', 'assets/player.png');
this.load.spritesheet('explosion', 'assets/explosion.png', { frameWidth: 64, frameHeight: 64 });
this.load.audio('hit', ['assets/hit.ogg', 'assets/hit.mp3']);
this.load.json('levels', 'assets/levels.json');
}
```
Каждая запись регистрируется под строковым ключом. Соответствующий кеш предоставляет типизированные методы доступа — `this.textures.get('player')`, `this.cache.json.get('levels')` и т. д.
## Прогресс загрузки
Чтобы показать индикатор прогресса, подпишитесь на события загрузчика во время `preload`:
```ts
this.load.on('progress', (p: number) => bar.setScale(p, 1));
this.load.on('complete', () => bar.destroy());
```
Распространённый шаблон — крошечная сцена **Boot**, которая загружает ресурсы самого индикатора загрузки, а затем переходит к сцене **Preload**, загружающей всё остальное.
## Загрузка после `preload`
Вы *можете* вызвать `this.load.image(...)` после `preload`, но при этом необходимо явно запустить загрузчик:
```ts
this.load.image('boss', 'assets/boss.png');
this.load.once('complete', () => this.spawnBoss());
this.load.start();
```
Полезно для потоковой подгрузки данных уровня, но по возможности предпочитайте предварительную (eager) загрузку — стоимость задержки посреди игры во время выполнения гораздо выше, чем стоимость памяти на хранение текстуры.
## Атласы
Для нетривиальных игр упаковывайте спрайты в текстурный атлас, а не загружайте каждый PNG по отдельности:
```ts
this.load.atlas('ui', 'assets/ui.png', 'assets/ui.json');
this.add.image(0, 0, 'ui', 'button-play');
```
Меньше файлов, меньше вызовов отрисовки, меньше перерасхода памяти GPU.
## Связанные темы
- [Сцены](/phaser/02-concepts/scenes/) — жизненный цикл, которому принадлежит загрузчик.
- [Игровые объекты](/phaser/02-concepts/game-objects/) — потребители загруженных текстур.
---
## [Phaser] Гайды
URL: https://cadmus.page/phaser/03-guides/00-overview/
Section: Гайды
Description: Практические рецепты для систем, которые вы действительно будете использовать — физика, тайлмапы, анимация, аудио, частицы, шейдеры и плагины.
Гайды — это документы в формате *how-to*. Каждый из них от начала до конца разбирает одну задачу с рабочим кодом. Если нужно понять *почему*, см. [Основные концепции](/phaser/02-concepts/00-overview/); если нужно узнать *что*, см. Справочник API.
## Содержание
- **Физика**
- [Arcade-физика](/phaser/03-guides/physics-arcade/) — быстрые AABB-столкновения для платформеров, шмапов и большинства аркадных игр.
- [Matter-физика](/phaser/03-guides/physics-matter/) — полноценная симуляция твёрдых тел с сочленениями и стопками объектов.
- [Тайлмапы](/phaser/03-guides/tilemaps/) — загрузка карт Tiled, слои и столкновения.
- [Анимация](/phaser/03-guides/animation/) — покадровые анимации и менеджер анимаций.
- [Твины](/phaser/03-guides/tweens/) — интерполяция любого числового свойства во времени.
- [Аудио](/phaser/03-guides/audio/) — интеграция с Web Audio, музыка и пространственный звук.
- [Частицы](/phaser/03-guides/particles/) — эмиттер частиц и распространённые эффекты.
- [Шейдеры](/phaser/03-guides/shaders/) — собственные WebGL-пайплайны.
- [Плагины](/phaser/03-guides/plugins/) — упаковка и использование плагинов Phaser 4.
---
## [Phaser] Физика Arcade
URL: https://cadmus.page/phaser/03-guides/physics-arcade/
Section: Гайды
Description: Быстрая AABB-физика для платформеров, шутеров и большинства аркадных игр — тела, столкновения, группы и подводные камни, на которые натыкаются новички.
Arcade — это быстрая система физики Phaser 4. Она моделирует всё как **ограничивающие прямоугольники, выровненные по осям** (AABB) или окружности — без вращения, без стопок твёрдых тел, без соединений. Взамен вы получаете систему, достаточно быструю, чтобы обсчитывать сотни тел за кадр на телефоне.
## Когда использовать Arcade
Используйте Arcade, когда выполняются **все** условия:
- Вам не нужна вращательная физика («наклонённый ящик лежит на склоне»).
- Вам не нужна укладка стопкой («ящики правильно складываются в кучу»).
- Вам не нужны соединения, ограничения или составные тела.
Подходит большинство платформеров, приключений с видом сверху, шутеров, арканоидов и казуальных головоломок. Если вы ловите себя на борьбе с Arcade ради поведения твёрдых тел, переходите на [Matter](/phaser/03-guides/physics-matter/) — не пытайтесь это имитировать.
## Включение Arcade
Задаётся в конфигурации игры:
```ts
new Phaser.Game({
type: Phaser.AUTO,
width: 800,
height: 600,
physics: {
default: 'arcade',
arcade: {
gravity: { y: 300 },
debug: false,
},
},
scene: { /* ... */ },
});
```
Внутри сцены `this.physics` — это мир; `this.physics.add` — фабрика.
## Создание тел
Два вида: **динамические** (двигаются, сталкиваются, реагируют на гравитацию) и **статические** (неподвижные; существуют для того, чтобы *с ними* сталкивались).
```ts
// Динамическое — имеет скорость, ускорение, гравитацию.
const player = this.physics.add.sprite(100, 100, 'player');
// Статическое — закреплено на месте. Дёшево обсчитывать столкновения с ним.
const platform = this.physics.add.staticImage(400, 568, 'ground');
```
Превратить обычный игровой объект в физический постфактум можно через `this.physics.add.existing(obj, isStatic)`.
## Движение
Движением управляют три параметра:
```ts
player.setVelocity(120, 0); // пикселей в секунду
player.setAcceleration(0, 400); // пикселей в секунду^2
player.setDrag(100); // затухание скорости в секунду
player.setMaxVelocity(300, 800); // ограничение
```
Предпочитайте `setVelocity` прямому изменению `x` — прямая запись позиции обходит разрешение столкновений, и вы будете проходить сквозь стены.
## Столкновения против пересечений
Два отношения с разной семантикой:
- **Collider** — Phaser разделяет тела и не даёт им взаимно проникать друг в друга. Используйте для стен, полов, врагов, которые должны физически толкать игрока.
- **Overlap** — Phaser обнаруживает пересечение, но позволяет телам проходить насквозь. Используйте для подбираемых предметов, триггеров, зон поражения.
```ts
this.physics.add.collider(player, platforms);
this.physics.add.overlap(player, coins, (_p, coin) => coin.destroy());
```
Оба принимают необязательный callback обработки (верните `false`, чтобы пропустить разрешение для этой пары) и контекст.
## Группы
**Группа** — это типизированный пул объектов с включённой физикой. Используйте её для любого набора объектов, которые вы будете обрабатывать на столкновения как единое целое:
```ts
const enemies = this.physics.add.group({
defaultKey: 'enemy',
maxSize: 50,
collideWorldBounds: true,
});
enemies.get(400, 0).setVelocityY(80);
this.physics.add.collider(enemies, platforms);
this.physics.add.overlap(player, enemies, this.onHit, undefined, this);
```
`group.get()` повторно использует неактивного участника, если такой есть, или создаёт нового вплоть до `maxSize`. Дёшево.
## Живой пример
Три прыгающих мяча сталкиваются друг с другом и с границами мира.
{
b.setVelocityX(Phaser.Math.Between(-150, 150));
b.setCircle(b.width / 2);
return null;
});
this.physics.add.collider(balls, balls);
}
}
});
`}
/>
Обратите внимание на вызов `setCircle` — по умолчанию Arcade использует прямоугольное тело даже для круглых спрайтов. Вызов `setCircle` после создания переключает тело на окружность заданного радиуса. Для круглых спрайтов это почти всегда то, что нужно.
## Распространённые ошибки
**Туннелирование.** Быстро движущееся тело может за один кадр проскочить сквозь тонкую стену. В физике Arcade нет непрерывного обнаружения столкновений, поэтому меры противодействия геометрические:
- Делайте стены толще максимального перемещения тела за кадр (`speed × delta`).
- Ограничивайте максимальную скорость (`setMaxVelocity`) для объектов, которым не нужно быть сколь угодно быстрыми.
- Для пулеподобных объектов замените тело на overlap с callback обработки, который трассирует лучом путь пули каждый кадр.
**Субпиксельное дрожание.** Смешивание `setVelocity` с ручной записью `x` вызывает мерцание. Выберите один подход и придерживайтесь его на протяжении жизни тела.
**Смещение тела против смещения спрайта.** Ограничивающий прямоугольник тела может не совпадать с видимыми границами спрайта (представьте персонажа с длинным хвостом волос). Используйте `body.setSize(w, h)` и `body.setOffset(x, y)`, чтобы подогнать прямоугольник.
**Итерация по группе во время столкновения.** Изменять группу изнутри callback столкновения (например, порождать нового участника) безопасно; итерировать группу циклом `for`, когда callback того же цикла её изменяет, — нет. Предпочитайте `group.children.iterate(...)`.
## Отладочная отрисовка
Установите `arcade.debug: true` в конфигурации, чтобы рисовать контуры тел и векторы скорости. Незаменимо при настройке столкновений.
## Связанные материалы
- [Физика Matter](/phaser/03-guides/physics-matter/) — когда AABB недостаточно.
- [Игровой цикл](/phaser/02-concepts/game-loop/) — что на самом деле означает «в секунду».
- [Игровые объекты](/phaser/02-concepts/game-objects/) — что оборачивает `physics.add`.
---
## [Phaser] Физика Matter
URL: https://cadmus.page/phaser/03-guides/physics-matter/
Section: Гайды
Description: Полноценная симуляция твёрдых тел для укладки стопкой, соединений, составных тел и всего, что не может смоделировать Arcade.
Matter — это тяжеловесный вариант физики. Он моделирует настоящие твёрдые тела — с вращением, трением, упругостью, соединениями, ограничениями и произвольными полигональными формами — ценой большей нагрузки на процессор на одно тело и более сложного API.
## Когда использовать Matter
Выбирайте Matter, как только вам понадобится что-либо из перечисленного:
- **Вращение.** Наклонённый ящик, который приходит в состояние покоя на склоне под правильным углом.
- **Укладка стопкой.** Куча ящиков, которая реалистично оседает.
- **Соединения / ограничения.** Верёвки, цепи, шарниры, пружины, моторизованные колёса.
- **Составные тела.** Транспортное средство с шасси плюс отдельными колёсами.
- **Полигональные столкновения.** Непрямоугольная зона поражения.
Для всего остального предпочитайте [Arcade](/phaser/03-guides/physics-arcade/) — он на порядок быстрее.
## Включение Matter
```ts
new Phaser.Game({
physics: {
default: 'matter',
matter: {
gravity: { y: 1 }, // масштаб гравитации Matter, а не пикселей/с^2
debug: false,
enableSleeping: true, // прекращать интегрирование осевших тел
},
},
});
```
Внутри сцены `this.matter` — это мир.
## Создание тел
```ts
// Прямоугольное тело — простейший случай.
const box = this.matter.add.image(200, 100, 'crate');
// Окружность.
const ball = this.matter.add.image(300, 100, 'ball', undefined, { shape: 'circle' });
// Полигон из явно заданных вершин.
const tri = this.matter.add.fromVertices(400, 100, '0 0 60 0 30 50', { friction: 0.4 });
// Составное тело — полезно, когда одному объекту нужно несколько отдельных зон поражения.
const ship = this.matter.add.image(500, 100, 'ship', undefined, {
shape: { type: 'fromVerts', verts: shipHullPath, flagInternal: true },
});
```
Типичные свойства, задаваемые для каждого тела:
```ts
box.setStatic(false);
box.setFriction(0.3, 0.001, 0.05); // поверхностное, воздушное, статическое
box.setBounce(0.4);
box.setMass(2);
box.setAngularVelocity(0.05);
```
## Ограничения и соединения
```ts
// Маятник — статический якорь с качающимся грузом.
// Якорям не нужна текстура; достаточно небольшого невидимого прямоугольного тела.
const anchor = this.matter.add.rectangle(400, 50, 4, 4, { isStatic: true });
const weight = this.matter.add.image(400, 200, 'weight');
this.matter.add.constraint(anchor, weight, 150, 0.9);
// Верёвка из N сегментов.
let prev = anchor;
for (let i = 0; i < 10; i++) {
const segment = this.matter.add.image(400 + i * 10, 80, 'link');
this.matter.add.constraint(prev, segment, 12, 0.9);
prev = segment;
}
```
`constraint(a, b, length, stiffness)` — `stiffness: 1` — это жёсткий стержень, `stiffness: 0.001` — мягкая пружина.
## Живой пример
Небольшая стопка ящиков оседает на полу.
## Производительность: засыпание
Matter гораздо дороже Arcade в расчёте на одно тело. Самая значимая мера противодействия — **засыпание**: тела, которые какое-то время не двигались, временно исключаются из цикла интегрирования. Включите его в конфигурации (`enableSleeping: true`); осевшая стопка почти ничего не стоит, пока что-то её не потревожит.
Другие параметры:
- **`positionIterations` и `velocityIterations`** — повышайте для более точных стопок ценой нагрузки на процессор. Значения по умолчанию 6/4 обычно подходят.
- **Количество тел.** Matter масштабируется сублинейно, но не бесплатно. Несколько сотен активных тел комфортны на десктопе, ~50 — на мобильных устройствах среднего уровня.
- **Не опрашивайте мир каждый кадр.** Используйте события столкновений Matter (`'collisionstart'`, `'collisionactive'`, `'collisionend'`) вместо сканирования.
## Интеграция со списком отображения
Игровые объекты Matter — это обычные игровые объекты Phaser: они находятся в списке отображения сцены, тонируются и анимируются как спрайты и могут быть вложены в контейнеры. Их позиция и поворот задаются физическим телом каждый кадр; прямая запись в `.x`/`.y` работает, но рассинхронизирует тело до следующего шага физики.
## Связанные материалы
- [Физика Arcade](/phaser/03-guides/physics-arcade/) — более быстрый, менее выразительный собрат.
- [Игровой цикл](/phaser/02-concepts/game-loop/) — как физический мир обсчитывается каждый кадр.
---
## [Phaser] Тайлмапы
URL: https://cadmus.page/phaser/03-guides/tilemaps/
Section: Гайды
Description: Загрузка карт Tiled в формате JSON, отрисовка слоёв, построение столкновений и изменение тайлов во время выполнения.
Система тайлмапов Phaser 4 читает JSON-экспорты из [Tiled](https://www.mapeditor.org/). Вы создаёте карту во внешнем редакторе, а Phaser берёт на себя отрисовку, столкновения и изменение во время выполнения.
## Загрузка карты Tiled
Задействованы два файла: JSON карты и изображение тайлсета, на которое она ссылается.
```ts
preload() {
this.load.tilemapTiledJSON('level-1', 'assets/maps/level-1.json');
this.load.image('tiles', 'assets/tilesets/world.png');
}
```
Затем собираем карту и её слои:
```ts
create() {
const map = this.make.tilemap({ key: 'level-1' });
// Первый аргумент — имя тайлсета *как оно задано в Tiled*; второй —
// ключ кэша загруженного изображения. Часто они совпадают.
const tileset = map.addTilesetImage('world', 'tiles');
const ground = map.createLayer('Ground', tileset, 0, 0);
const walls = map.createLayer('Walls', tileset, 0, 0);
const decor = map.createLayer('Decor', tileset, 0, 0);
}
```
Третий и четвёртый аргументы — мировые координаты левого верхнего угла слоя, обычно `0, 0`.
## Столкновения
Три идиоматичных способа отметить, какие тайлы участвуют в столкновениях:
```ts
// 1. По индексу тайла — точные ID тайлов из тайлсета.
walls.setCollision([12, 13, 25, 26]);
// 2. По диапазону.
walls.setCollisionBetween(1, 99);
// 3. По пользовательскому свойству Tiled — лучший вариант, если вы управляете источником.
walls.setCollisionByProperty({ collides: true });
```
Затем добавляем коллайдер:
```ts
this.physics.add.collider(player, walls);
```
Для односторонних платформ, склонов и других столкновений с учётом угла см. справочник API `Tilemaps` — методы `TilemapLayer.setCollision*` и специфичный для Arcade `TileCollisionGroup` покрывают большинство случаев.
## Слои объектов
Слои объектов Tiled — это канонический способ размещать игровые маркеры: точку появления игрока, позиции врагов, двери. Они приходят в виде списка обычных объектов:
```ts
const spawns = map.getObjectLayer('Spawns');
spawns?.objects.forEach((obj) => {
switch (obj.name) {
case 'player': this.player = this.physics.add.sprite(obj.x!, obj.y!, 'player'); break;
case 'enemy': this.enemies.create(obj.x!, obj.y!, 'enemy'); break;
}
});
```
Свойство `properties` каждого объекта несёт пользовательские свойства Tiled в виде записей `{name, type, value}` — если вы часто с ними работаете, упростите доступ небольшим хелпером, разворачивающим их в плоский объект.
## Границы камеры
Почти всегда привязывайте карту к камере:
```ts
this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
this.physics.world.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
this.cameras.main.startFollow(this.player);
```
Без этого камера будет спокойно прокручиваться за края карты в пустоту.
## Изменение во время выполнения
Слои предоставляют прямой доступ к тайлам:
```ts
const tile = walls.getTileAtWorldXY(player.x, player.y - 32);
if (tile?.properties.breakable) {
walls.removeTileAt(tile.x, tile.y);
}
// Заменяем каждый экземпляр одного тайла на другой.
ground.swapByIndex(45, 67);
// Прорезаем линию из тайлов.
for (let x = 0; x < 8; x++) walls.putTileAt(12, startX + x, startY);
```
Обратите внимание: координаты тайлов здесь — это *тайловое пространство* (`0, 0` — это левый верхний тайл), а не пиксели. Используйте варианты `WorldXY`, когда у вас пиксельные координаты, и индексные варианты, когда у вас тайловые координаты.
## Производительность
- **Статические слои дёшевы.** Phaser батчирует слой в один или несколько вызовов отрисовки. Карта 200×200 отрисовывается практически бесплатно.
- **Свойства по тайлам стоят времени поиска.** Если вы обнаружили, что перебираете каждый тайл каждый кадр, кэшируйте результат.
- **Отсечение (culling) включено по умолчанию.** Тайлы за пределами камеры пропускаются. Не отключайте отсечение, не проведя сначала замеры.
## Связанное
- [Arcade-физика](/phaser/03-guides/physics-arcade/) — как разрешаются столкновения с тайлами.
- [Камеры](/phaser/02-concepts/cameras/) — установка границ под размер карты.
- [Загрузчик и ассеты](/phaser/02-concepts/loader/) — загрузчики `tilemapTiledJSON` и изображений.
---
## [Phaser] Анимация
URL: https://cadmus.page/phaser/03-guides/animation/
Section: Гайды
Description: Покадровые анимации из спрайт-листов и атласов — определение, воспроизведение, объединение в цепочки и реакция на события анимации.
Система анимации Phaser 4 воспроизводит последовательности кадров текстуры на объекте `Sprite`. Анимации определяются один раз на уровне **сцены** (или глобально), а затем *воспроизводятся* на любом количестве спрайтов — определение и воспроизведение разделены.
## Где хранятся анимации
Два места:
- **`this.anims`** — менеджер анимаций уровня сцены. Анимации хранятся здесь по умолчанию.
- **`this.game.anims`** (редко) — глобальный менеджер, который можно использовать для совместного использования определений между сценами.
Почти всегда используйте менеджер уровня сцены. Совместное использование между сценами склонно к утечкам состояния.
## Определение анимации
Нужен источник — спрайт-лист (равномерные кадры) или атлас (именованные кадры, нерегулярные):
```ts
preload() {
// Равномерная сетка.
this.load.spritesheet('dude', 'assets/dude.png', { frameWidth: 32, frameHeight: 48 });
// Атлас с именованными кадрами.
this.load.atlas('hero', 'assets/hero.png', 'assets/hero.json');
}
create() {
this.anims.create({
key: 'walk',
frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 3 }),
frameRate: 10,
repeat: -1, // -1 означает бесконечный цикл
});
this.anims.create({
key: 'attack',
frames: this.anims.generateFrameNames('hero', { prefix: 'attack-', start: 1, end: 6, zeroPad: 2 }),
frameRate: 18,
repeat: 0, // воспроизвести один раз
});
}
```
`frameRate` — это **кадры в секунду**, а не миллисекунды на кадр. Альтернатива — `duration`, общая длительность анимации в мс; она переопределяет `frameRate`, если заданы оба значения.
## Воспроизведение на спрайте
```ts
const player = this.add.sprite(100, 100, 'dude');
player.play('walk'); // запустить (или перезапустить) немедленно
player.play({ key: 'walk', repeat: 3 }); // переопределить конфигурацию для этого воспроизведения
player.playAfterDelay('walk', 250); // запустить через 250 мс
player.chain('attack'); // поставить в очередь анимацию для воспроизведения по окончании текущей
player.stop(); // остановиться на текущем кадре
player.stopAfterRepeat(); // завершить текущий цикл, затем остановиться
```
## Живой пример
Шагающий персонаж — определён один раз, воспроизводится на одном спрайте.
dude.setFlipX(true),
onRepeat: () => dude.setFlipX(false),
});
}
}
});
`}
/>
## Реакция на воспроизведение
Анимации генерируют события на воспроизводящем их спрайте:
```ts
player.on('animationcomplete', (anim) => {
if (anim.key === 'attack') player.play('idle');
});
player.on('animationrepeat', (anim) => {
if (anim.key === 'walk') this.playFootstep();
});
player.on('animationupdate', (anim, frame) => {
// Срабатывает один раз при каждой смене кадра.
});
```
Для сценария «проиграть X один раз, затем вернуться к Y» API `chain` чище, чем ручная обработка `animationcomplete`:
```ts
player.play('attack').chain('idle');
```
## Распространённые ошибки
**Определение внутри `update`.** `anims.create` идемпотентен (он выдаёт предупреждение при дублировании ключей), но не следует вызывать его каждый кадр. Определяйте анимации в `create`.
**Состояние на спрайт против состояния на ключ.** *Определение* анимации является общим, но у каждого спрайта своя позиция воспроизведения. Воспроизведение одной и той же анимации на двух спрайтах выполняется независимо.
**Порядок загрузки.** `generateFrameNumbers` и `generateFrameNames` требуют, чтобы исходная текстура уже существовала в кэше, поэтому вызывайте их из `create()` (после завершения `preload`), а не раньше.
## Связанные материалы
- [Загрузчик и ресурсы](/phaser/02-concepts/loader/) — как спрайт-листы и атласы попадают в кэш.
- [Игровые объекты](/phaser/02-concepts/game-objects/) — `Sprite` — единственный встроенный тип с прикреплённым менеджером анимаций.
---
## [Phaser] Твины
URL: https://cadmus.page/phaser/03-guides/tweens/
Section: Гайды
Description: Система твинов Phaser 4 — интерполяция любого числового свойства во времени. Цели, функции плавности (easing), цепочки, твины значений и подводные камни.
**Твин** интерполирует значение во времени. Вы направляете твин на один или несколько объектов и указываете, какие свойства менять, как и за какую длительность — Phaser берёт на себя покадровые вычисления.
Твины — самая часто используемая утилита в коде на Phaser: затухания, плавные перемещения камеры, отскок UI, появление диалогов, анимации значений, управляющие пользовательской логикой.
## Форма твина
```ts
this.tweens.add({
targets: sprite,
x: 400,
duration: 1000,
});
```
Читается так: «перемести `sprite.x` к 400 за 1000 мс, используя функцию плавности по умолчанию». Phaser захватывает *текущее* значение `sprite.x` в момент старта твина и интерполирует от него.
## Цели
Один твин может управлять любым количеством объектов одновременно:
```ts
targets: sprite // один объект
targets: [s1, s2, s3] // несколько
targets: group.getChildren() // члены группы
targets: { value: 0 } // произвольный обычный объект — см. «Твины значений» ниже
```
## Свойства
Подходит любое числовое свойство. Phaser интерполирует от текущего значения к целевому.
```ts
this.tweens.add({
targets: sprite,
x: '+=50', // ОТНОСИТЕЛЬНОЕ — прибавляет 50 к текущему значению
alpha: 0, // твин к 0
scale: { from: 2, to: 1 }, // явные начало + конец
angle: { value: 360, ease: 'Cubic.Out' }, // переопределение для отдельного свойства
duration: 600,
});
```
Относительные цели (`'+=50'`, `'-=50'`, `'*=2'`) вычисляются в момент старта — это полезно, когда абсолютная цель зависит от того, какое значение свойство имеет на момент запуска твина.
## Функции плавности (easing)
```ts
ease: 'Linear' // значение по умолчанию при отсутствии тоже близко к Linear
ease: 'Sine.InOut'
ease: 'Cubic.Out'
ease: 'Bounce.Out'
ease: 'Back.InOut'
ease: 'Elastic.Out'
```
Полный набор находится в `Phaser.Math.Easing.*`. У каждого семейства есть варианты `.In`, `.Out` и `.InOut`. Выбирайте `Out` для естественно ощущающихся прибытий (объекты замедляются к своей цели); `In` — для отбытий; `InOut` — для симметричного движения.
## Длительность, задержка, повтор, yoyo
```ts
this.tweens.add({
targets: sprite,
y: '+=50',
duration: 400,
delay: 200, // подождать 200 мс перед стартом
yoyo: true, // проиграть вперёд, затем назад
repeat: -1, // -1 = бесконечно; положительное N = N дополнительных проигрываний
repeatDelay: 100,
});
```
`yoyo: true` удваивает эффективную длительность одного цикла — твин на 400 мс становится 800 мс (вперёд + назад). `hold: N` делает паузу на N мс в точке yoyo, если нужна задержка.
## Колбэки
```ts
this.tweens.add({
targets: sprite,
x: 400,
duration: 1000,
onStart: () => this.sound.play('whoosh'),
onUpdate: (_tween, target) => { /* каждый кадр */ },
onYoyo: () => { /* достигли точки yoyo */ },
onRepeat: () => { /* начался новый цикл */ },
onComplete: () => sprite.destroy(),
});
```
Для сценария «проиграть один раз и убрать» паттерн `onComplete: () => target.destroy()` — самый чистый.
## Живой пример
Пять блоков с разбросом по времени и плавностью `Bounce.Out`, зацикленные через `yoyo + repeat: -1`.
`this.tweens.stagger(step, options)` возвращает функцию, которая вычисляет задержку для каждой цели — `0`, `step`, `2*step`, ... Это самый чистый способ распределить эффект по группе.
## Цепочки
Для сценария «сделай A, затем B, затем C» используйте `chain`:
```ts
this.tweens.chain({
targets: sprite,
tweens: [
{ x: 400, duration: 500, ease: 'Cubic.Out' },
{ y: 200, duration: 300 },
{ angle: 360, duration: 800, ease: 'Sine.InOut' },
],
});
```
`chain` — это более простой преемник `timeline` из v3. Каждая дочерняя запись — это обычный конфиг твина: функция плавности, колбэки, yoyo, всё это.
## Твины значений
Когда нужно интерполировать что-то, до чего Phaser не может добраться через присваивание свойства — юниформ шейдера, CSS-переменную, значение в сторонней библиотеке — делайте твин обычного объекта и считывайте его в `onUpdate`:
```ts
const state = { mix: 0 };
this.tweens.add({
targets: state,
mix: 1,
duration: 2000,
ease: 'Cubic.InOut',
onUpdate: () => myShader.setUniform('uMix', state.mix),
});
```
Этот паттерн работает для чего угодно: плавного изменения громкости звука, анимации смешивания `RenderTexture`, затухания DOM-оверлея.
## Остановка и управление твинами
```ts
const t = this.tweens.add({ targets: sprite, x: 400, duration: 1000 });
t.pause();
t.resume();
t.stop(); // остановить и уничтожить
t.seek(500); // перейти на отметку 500 мс
t.timeScale = 2; // проигрывать на двойной скорости
this.tweens.killTweensOf(sprite); // остановить каждый твин, затрагивающий этот объект
this.tweens.killAll(); // радикальный вариант
```
Для ответа на вопрос «выполняется ли сейчас твин этого объекта?» используйте `this.tweens.isTweening(sprite)`.
## Типичные ошибки
**Не делайте твин позиции напрямую на физических объектах.** Твины пишут `.x`/`.y`, минуя физическое тело. Тело рассинхронизируется до следующего шага физики. Либо делайте твин `body.velocity.*`, либо отключайте тело на время твина.
**Мусорные твины.** Твины остаются зарегистрированными после завершения, если не указать им очиститься. Для одноразовых эффектов, запускаемых в горячем цикле, используйте `onComplete: () => tween.remove()` — или используйте `this.tweens.add({ ..., persist: false })`, чтобы менеджер сбрасывал их автоматически.
**Привязка к сцене.** Твины живут на `this.tweens` — менеджере твинов сцены. Они останавливаются, когда останавливается сцена. Межсценовых твинов не существует — вместо этого моделируйте общее состояние.
**Математика плавности применяется к каждому свойству.** Твин с `x` и `alpha` будет использовать *одну и ту же* функцию плавности для обоих, если не переопределить её для каждого свойства. Если нужны разные кривые, разбейте на два твина или используйте форму с объектом для отдельного свойства.
## Связанное
- [Анимация](/phaser/03-guides/animation/) — покадровая анимация спрайтов; дополняет твины (которые интерполируют свойства).
- [Игровой цикл](/phaser/02-concepts/game-loop/) — твины продвигаются каждый кадр, используя `delta`, поэтому они независимы от частоты кадров без дополнительных усилий.
- [Камеры](/phaser/02-concepts/cameras/) — эффекты камеры (`shake`, `fade`, `pan`, `zoomTo`) под капотом являются твинами.
---
## [Phaser] Аудио
URL: https://cadmus.page/phaser/03-guides/audio/
Section: Гайды
Description: Интеграция Web Audio в Phaser 4 — музыка против звуковых эффектов, разблокировка по жесту пользователя, микширование и пространственный звук.
Phaser 4 оборачивает браузерный Web Audio API за `this.sound`. Аудио загружается через стандартный загрузчик и воспроизводится через типизированный менеджер звука.
## Загрузка
```ts
preload() {
// Несколько форматов — браузер выбирает первый, который может декодировать.
this.load.audio('music', ['assets/audio/theme.ogg', 'assets/audio/theme.mp3']);
this.load.audio('hit', 'assets/audio/hit.ogg');
}
```
Всегда поставляйте как минимум два формата. OGG покрывает Firefox и Chrome; для Safari нужен MP3 или M4A.
## Воспроизведение
```ts
this.sound.play('hit'); // звуковой эффект по принципу «выстрелил и забыл»
this.sound.play('hit', { volume: 0.6, detune: -200 }); // настройка для каждого вызова
// Для всего, на что нужна ссылка:
const music = this.sound.add('music', { loop: true, volume: 0.5 });
music.play();
music.setVolume(0.2);
music.stop();
```
`this.sound.play(key, config)` возвращает звук, но не сохраняет ссылку на него — используйте `add` + `play`, когда нужно управлять звуком после запуска (пауза, затухание, изменение громкости).
## Разблокировка по жесту пользователя
Браузеры не запускают аудио до тех пор, пока страница не получит жест пользователя (клик, нажатие клавиши, касание). Phaser обнаруживает это и разблокирует аудиоконтекст при первом вводе — но **любой звук, который вы попытаетесь воспроизвести до этого момента, будет беззвучным**.
Защитные паттерны:
```ts
// Дождаться разблокировки перед воспроизведением музыки.
if (this.sound.locked) {
this.sound.once(Phaser.Sound.Events.UNLOCKED, () => music.play());
} else {
music.play();
}
```
Для большинства игр это не проблема, потому что первое взаимодействие (клик в меню, «нажмите любую клавишу») естественным образом предшествует любому аудио. Это становится подвохом, когда вы пытаетесь воспроизвести музыку в автоматически запускающейся сцене-заставке.
## Музыка: зацикливание и затухание
```ts
const music = this.sound.add('music', { loop: true, volume: 0 });
music.play();
this.tweens.add({ targets: music, volume: 0.6, duration: 1500 });
// Перекрёстное затухание на другой трек.
this.tweens.add({ targets: music, volume: 0, duration: 1000, onComplete: () => {
music.stop();
const next = this.sound.add('battle', { loop: true, volume: 0 });
next.play();
this.tweens.add({ targets: next, volume: 0.6, duration: 1000 });
}});
```
Для бесшовных циклов исходный файл должен быть точным до сэмпла в точке зацикливания. Vorbis (`.ogg`) наиболее терпим к этому; MP3 исторически добавляет беззвучные отступы.
## Микширование
У менеджера звука есть мастер-громкость:
```ts
this.sound.volume = 0.8; // 0..1, умножается на громкость каждого звука
this.sound.mute = true; // переключатель, не абсолютное значение
this.sound.pauseAll();
this.sound.resumeAll();
```
Для отдельных шин музыки и звуковых эффектов сгруппируйте звуки в собственном помощнике и применяйте множители категорий — встроенной поддержки шин Phaser не предоставляет.
## Пространственный звук
Для 2D-позиционного аудио используйте поддержку панорамирования бэкенда Web Audio:
```ts
const sfx = this.sound.add('engine', { loop: true });
sfx.play();
// Панорамирование от -1 (полностью влево) до 1 (полностью вправо) в зависимости от расстояния до центра камеры.
this.events.on('update', () => {
const dx = (vehicle.x - this.cameras.main.midPoint.x) / (this.cameras.main.width / 2);
sfx.setPan(Phaser.Math.Clamp(dx, -1, 1));
sfx.setVolume(1 / (1 + Math.abs(dx) * 1.5));
});
```
Для более насыщенного 3D-подобного поведения (HRTF, модели расстояния) спуститесь к чистому Web Audio API — Phaser предоставляет лежащий в основе `AudioContext` через `this.sound.context`.
## Распространённые ошибки
**Забывание удалять звуки при переходах между сценами.** Звуки продолжают воспроизводиться между сценами, если вы их не остановите. Останавливайте музыку сцены в `shutdown()` или привязывайте её к жизненному циклу сцены:
```ts
this.events.once('shutdown', () => music.stop());
```
**Конфигурация для отдельного вызова не сохраняется.** `this.sound.play('hit', { volume: 0.3 })` воспроизводит *именно этот* звук с громкостью 30% — кэшированная конфигурация для ключа остаётся без изменений.
**Стоимость декодирования.** Большие аудиофайлы декодируются при первом воспроизведении, что может вызвать заикание. Выполните предварительное декодирование, воспроизведя звук в беззвучном режиме при старте игры, или загружайте файлы меньшего размера.
## Связанные материалы
- [Загрузчик и ресурсы](/phaser/02-concepts/loader/) — как аудио попадает в кэш.
- [Сцены](/phaser/02-concepts/scenes/) — куда помещать хуки очистки.
---
## [Phaser] Частицы
URL: https://cadmus.page/phaser/03-guides/particles/
Section: Гайды
Description: Эмиттер частиц — типичные эффекты, анатомия конфигурации и компромиссы производительности, которые действительно важны.
**Эмиттер частиц** порождает множество короткоживущих спрайтов со случайными свойствами и (опционально) интерполированным временем жизни. В Phaser 4 поставляется единый класс эмиттера, который покрывает всё — от дымового шлейфа до сотрясающего экран взрыва.
## Структура эмиттера
```ts
const emitter = this.add.particles(0, 0, 'spark', {
speed: { min: 80, max: 160 },
angle: { min: 250, max: 290 },
scale: { start: 1, end: 0 },
alpha: { start: 1, end: 0 },
lifespan: 600,
frequency: 30, // мс между испусканиями; -1 означает «режим взрыва»
quantity: 1, // частиц за одно испускание
blendMode: 'ADD',
});
emitter.startFollow(player);
```
Две ключевые идеи:
- **Случайность для каждой частицы через объекты-диапазоны.** `speed: { min, max }` рандомизирует начальное значение; `scale: { start, end }` интерполирует от начального к конечному значению на протяжении времени жизни частицы.
- **Темп для всего эмиттера через `frequency` + `quantity`.** Вместе они задают «частиц в секунду». `frequency: -1` переключает в режим взрыва — испустить `quantity` частиц одновременно при вызове `emitter.explode()`.
## Живой пример
Непрерывный шлейф искр, следующий за невидимой целью, движущейся по окружности.
{
t += delta / 1000;
target.x = center.x + Math.cos(t * 1.6) * 130;
target.y = center.y + Math.sin(t * 1.6) * 70;
});
}
}
});
`}
/>
## Режим взрыва
```ts
const burst = this.add.particles(0, 0, 'spark', {
speed: { min: 120, max: 260 },
lifespan: 500,
scale: { start: 1, end: 0 },
emitting: false, // не испускать автоматически
});
// Позже, по событию:
burst.emitParticleAt(player.x, player.y, 30);
```
`emitting: false` — это ключ — он подавляет непрерывный поток, чтобы можно было запускать всплески вручную.
## Зоны эмиттера
Порождайте или уничтожайте частицы в произвольных формах:
```ts
const emitter = this.add.particles(0, 0, 'spark', {
emitZone: { type: 'edge', source: new Phaser.Geom.Circle(0, 0, 50), quantity: 64 },
// или 'random' для заполнения области порождением
});
```
`emitZone` управляет тем, где частицы *рождаются*; `deathZone` удаляет любые, попадающие в заданную форму — полезно для эффекта «частицы поглощаются предметом».
## Типичные рецепты
**Дым** — медленный, крупный, затухающий, без аддитивного смешивания.
```ts
{ speed: 20, scale: { start: 0.4, end: 1.6 }, alpha: { start: 0.6, end: 0 },
lifespan: 1500, frequency: 80, blendMode: 'NORMAL' }
```
**Взрыв** — быстрый, аддитивный, единичный всплеск.
```ts
{ speed: { min: 200, max: 400 }, scale: { start: 1, end: 0 },
lifespan: 400, blendMode: 'ADD', emitting: false }
// ...затем emitter.explode(50, x, y);
```
**Шлейф** — высокая частота, короткое время жизни, следует за целью.
```ts
{ speed: 10, scale: { start: 0.5, end: 0 }, lifespan: 200,
frequency: 10, follow: target, blendMode: 'ADD' }
```
## Производительность
- **Смешивание `ADD` — самая дорогая операция.** Переключитесь на `NORMAL`, если эффект свечения не нужен.
- **Размер текстуры частицы важнее их количества.** Эмиттер на 1000 частиц с искрами 8×8 дешевле, чем 100 частиц с клубами 256×256.
- **Ограничивайте одновременное число частиц** с помощью `maxParticles` — это предотвращает патологические всплески, когда произведение frequency × lifespan становится большим.
## Связанные материалы
- [Игровые объекты](/phaser/02-concepts/game-objects/) — менеджер частиц сам является игровым объектом и может быть добавлен в контейнеры.
- [Шейдеры](/phaser/03-guides/shaders/) — для эффектов, выходящих за пределы того, что можно выразить режимами смешивания.
---
## [Phaser] Шейдеры
URL: https://cadmus.page/phaser/03-guides/shaders/
Section: Гайды
Description: Собственные WebGL-эффекты в Phaser 4 — встроенная библиотека фильтров (Filter), свой GLSL через игровой объект Shader и место RenderNodes во всём этом.
Система `Pipeline` из Phaser 3 в Phaser 4 отсутствует. Три замены покрывают почти все сценарии:
| Чего хотите | Используйте |
|---------------------------------------------------|----------------------------------------|
| Встроенный эффект на спрайте или камере | **Фильтр** (`obj.filters.internal.addX()`) |
| Собственный фрагментный шейдер GLSL в виде квада | **Игровой объект `Shader`** |
| Целый новый шаг отрисовки внутри рендерера | Собственный **RenderNode** |
Пайплайны, `setPipeline`, `setPostPipeline` и `PostFXPipeline` больше не существуют. Если вы портируете проект с v3, см. Миграция с Phaser 3 для механических замен.
## Фильтры: встроенный каталог
Каждый игровой объект и каждая камера предоставляют свойство `filters` со списками фильтров `internal` и `external`. Внутренние (internal) фильтры воздействуют на сам объект; внешние (external) фильтры воздействуют на контекст отрисовки (как правило, на весь экран).
```ts
const camera = this.cameras.main;
// Встроенные эффекты в одну строку.
camera.filters.internal.addBarrel(0.6);
camera.filters.external.addVignette(0.5);
camera.filters.external.addGlow(0xffaa00);
```
Полный набор поставляется с v4: `Barrel`, `Blend`, `Blocky`, `Blur`, `Bokeh`, `ColorMatrix`, `CombineColorMatrix`, `Displacement`, `Glow`, `GradientMap`, `ImageLight`, `Key`, `Mask`, `NormalTools`, `PanoramaBlur`, `ParallelFilters`, `Pixelate`, `Quantize`, `Sampler`, `Shadow`, `Threshold`, `TiltShift`, `Vignette`, `Wipe`. Каждый вызов `addX()` возвращает контроллер, который можно настроить или удалить позже:
```ts
const glow = camera.filters.internal.addGlow();
glow.outerStrength = 4;
camera.filters.internal.remove(glow);
```
Фильтры компонуются стопкой — порядок добавления совпадает с порядком применения.
:::note[Bloom в v4 не является отдельным фильтром]
В v3 был эффект `Bloom`; в v4 метода `addBloom()` нет. Используйте `Phaser.Actions.AddEffectBloom(target, config?)` — он внутренне настраивает несколько фильтров через `ParallelFilters` и возвращает объект со ссылками на каждый из них, которые можно изменять для анимации bloom.
:::
## Игровой объект `Shader`
Для произвольного GLSL `this.add.shader(config, x, y, w, h)` рисует один квад с вашим фрагментным шейдером. Конструктор принимает `ShaderQuadConfig` — единый объект, а не позиционные аргументы, как в v3.
```ts
const fragSource = `
precision mediump float;
uniform vec2 uResolution;
uniform float uTime;
void main() {
vec2 uv = gl_FragCoord.xy / uResolution.xy;
vec3 col = 0.5 + 0.5 * cos(uTime + uv.xyx + vec3(0.0, 2.0, 4.0));
gl_FragColor = vec4(col, 1.0);
}
`;
create() {
const shader = this.add.shader(
{
name: 'rainbow',
fragmentSource: fragSource,
initialUniforms: { uTime: 0, uResolution: [480, 320] },
setupUniforms: (setUniform) => {
setUniform('uTime', this.time.now / 1000);
setUniform('uResolution', [this.scale.width, this.scale.height]);
},
},
240, 160, 480, 320,
);
}
```
Ключевые поля:
- **`fragmentSource`** (или `fragmentKey` для ключа кэша, загруженного через `this.load.glsl`) — ваш GLSL.
- **`initialUniforms`** — значения для однократной инициализации.
- **`setupUniforms(setUniform, drawingContext)`** — выполняется каждый кадр. Здесь передавайте покадровые значения (время, разрешение, мышь, уровень звука).
Phaser поставляет юниформ `uProjectionMatrix` по умолчанию. Автоматические `iResolution`/`iTime` в стиле Shadertoy из v3 исчезли — объявляйте и передавайте их самостоятельно, если они вам нужны.
## Загрузка шейдеров из файлов
Для нетривиальных шейдеров поставляйте GLSL отдельным файлом:
```ts
preload() {
this.load.glsl('rainbow', 'shaders/rainbow.frag');
}
create() {
this.add.shader({ name: 'rainbow', fragmentKey: 'rainbow' }, 240, 160, 480, 320);
}
```
В v3 загрузчику требовался вид `'fragment' | 'vertex'`. В v4 GLSL загружается как непрозрачный исходный код; вид определяется там, где вы конструируете `Shader`.
## Разобранный пример: пост-эффект CRT
Фильтр CRT на всю камеру, собранный из собственного Shader, наложенного поверх виньетки. Шейдер сэмплирует `RenderTexture` сцены, а не фреймбуфер камеры напрямую — это сохраняет компонуемость эффекта.
```ts
const crtFrag = `
precision mediump float;
uniform sampler2D uMainSampler;
uniform vec2 uResolution;
uniform float uTime;
varying vec2 outTexCoord;
void main() {
vec2 uv = outTexCoord;
// Хроматическая аберрация: сэмплируем R/G/B в слегка смещённых позициях.
float aberr = 0.0015;
vec3 col;
col.r = texture2D(uMainSampler, uv + vec2(aberr, 0.0)).r;
col.g = texture2D(uMainSampler, uv).g;
col.b = texture2D(uMainSampler, uv - vec2(aberr, 0.0)).b;
// Развёртка (scanlines): затемняем каждый второй ряд пикселей.
float scan = 0.92 + 0.08 * sin(uv.y * uResolution.y * 3.14159);
col *= scan;
// Виньетка: затемнение к углам.
vec2 d = uv - 0.5;
col *= 1.0 - dot(d, d) * 0.6;
gl_FragColor = vec4(col, 1.0);
}
`;
create() {
// 1. Отрисовываем сцену в текстуру каждый кадр.
const rt = this.add.renderTexture(0, 0, this.scale.width, this.scale.height).setOrigin(0);
// 2. Рисуем квад Shader, который сэмплирует эту текстуру и применяет CRT.
const crt = this.add.shader(
{
name: 'crt',
fragmentSource: crtFrag,
setupUniforms: (setUniform) => {
setUniform('uResolution', [this.scale.width, this.scale.height]);
setUniform('uTime', this.time.now / 1000);
},
},
this.scale.width / 2, this.scale.height / 2,
this.scale.width, this.scale.height,
[rt.texture],
);
// Поддерживаем rt в актуальном состоянии каждый кадр.
this.events.on('update', () => {
rt.clear();
rt.draw(this.cameras.main);
rt.render();
});
}
```
В Phaser 4 `RenderTexture` буферизует команды — нужно вызвать `render()`, чтобы их сбросить (см. DynamicTexture / RenderTexture в гайде по миграции).
## Практические замечания
- **`mediump`, как правило, достаточно.** `highp` удваивает нагрузку на регистры на некоторых GPU.
- **Текстурные координаты используют соглашения GL.** Y=0 находится внизу. Сжатые текстуры нужно перекодировать с отражённой осью Y; стандартные изображения обрабатываются автоматически.
- **Стоимость масштабируется с размером фреймбуфера.** Уменьшите разрешение вдвое и применяйте шейдер развёртки к нему — дешёвая ретро-эстетика.
- **Отлаживайте упрощением.** Если шейдер выглядит неправильно, сначала замените его тело на `gl_FragColor = vec4(outTexCoord, 0.0, 1.0);`, чтобы проверить UV-маппинг.
- **Шейдеры — это самостоятельные отрисовки.** Каждый игровой объект `Shader` завершает текущий батч и отрисовывается отдельно. Умеренное использование нормально; тысячи таких объектов — нет.
## Когда вместо этого стоит взяться за RenderNode
Квад `Shader` стоит одного вызова отрисовки. Если нужен собственный батчируемый эффект — скажем, тип спрайта со своей вершинной раскладкой — вы пишете собственный `RenderNode` и регистрируете его через `RenderConfig#renderNodes` при загрузке игры. Это самый нижний слой рендерера и существенно больший объём кода; см. `Phaser.Renderer.WebGL.RenderNodes` в справочнике API для оценки объёма работы.
Для 95% эффектов — встроенных фильтров или игрового объекта `Shader` — это не требуется.
## Связанное
- Миграция с Phaser 3 — изменения API рендерера/шейдеров между v3 и v4.
- [Игровые объекты](/phaser/02-concepts/game-objects/) — `Shader` является одним из них.
- [Камеры](/phaser/02-concepts/cameras/) — у каждой камеры есть список фильтров.
---
## [Phaser] Плагины
URL: https://cadmus.page/phaser/03-guides/plugins/
Section: Гайды
Description: Создание и использование плагинов Phaser 4 — три формы плагинов (глобальный, сценический, игрового объекта) и разобранный пример системы сохранения.
**Плагин** — это самодостаточный модуль, встраивающийся в жизненный цикл Phaser. Phaser 4 поддерживает три формы, каждая со своей областью видимости и жизненным циклом:
| Вид | Существует в | Жизненный цикл | Использовать для |
|--------------|----------------|------------------------------------|--------------------------------------|
| Глобальный | `game.plugins` | Создаётся вместе с игрой; сохраняется. | Межсценовые службы (сохранение, аналитика, сеть). |
| Сценический | `scene.plugins`| Создаётся для каждой сцены; привязан к её жизненному циклу. | Помощники уровня сцены (менеджер эффектов, движок диалогов). |
| Игрового объекта | Расширяет `this.add` / `this.make` | Прикрепляет фабрику к сценам, которые её подключают. | Пользовательские типы игровых объектов (`HealthBar`, `Minimap`). |
Выбирайте наименьшую подходящую область видимости. Глобальный плагин, который использует лишь одна сцена, — это просто утечка.
## Глобальный плагин
Класс, расширяющий `Phaser.Plugins.BasePlugin`. Он создаётся один раз при запуске игры и доступен отовсюду через `this.plugins.get('Name')`.
```ts
class SavePlugin extends Phaser.Plugins.BasePlugin {
private readonly KEY = 'save-v1';
save(state: object) {
localStorage.setItem(this.KEY, JSON.stringify(state));
}
load(): T | null {
const raw = localStorage.getItem(this.KEY);
return raw ? (JSON.parse(raw) as T) : null;
}
clear() {
localStorage.removeItem(this.KEY);
}
}
```
Зарегистрируйте его в конфигурации игры:
```ts
new Phaser.Game({
plugins: {
global: [{ key: 'Save', plugin: SavePlugin, start: true, mapping: 'save' }],
},
});
```
`start: true` создаёт экземпляр немедленно; `mapping: 'save'` делает его доступным как `this.save` в каждой сцене. Без `mapping` к нему обращаются через `this.plugins.get('Save')`.
Использование:
```ts
this.save.save({ level: 3, hp: 80 });
const restored = this.save.load<{ level: number; hp: number }>();
```
## Сценический плагин
Класс, расширяющий `Phaser.Plugins.ScenePlugin`. Каждая сцена получает собственный экземпляр, и плагин может встраиваться в события жизненного цикла сцены:
```ts
class DialogPlugin extends Phaser.Plugins.ScenePlugin {
private activeBox?: Phaser.GameObjects.Container;
boot() {
this.systems.events.on('shutdown', this.shutdown, this);
this.systems.events.on('destroy', this.destroy, this);
}
say(text: string) {
this.activeBox?.destroy();
const scene = this.scene!;
this.activeBox = scene.add.container(scene.scale.width / 2, scene.scale.height - 80);
this.activeBox.add(scene.add.rectangle(0, 0, 600, 100, 0x000000, 0.7));
this.activeBox.add(scene.add.text(0, 0, text, { fontSize: '20px' }).setOrigin(0.5));
}
private shutdown() {
this.activeBox?.destroy();
this.activeBox = undefined;
}
}
```
Регистрация для каждой сцены:
```ts
plugins: {
scene: [{ key: 'Dialog', plugin: DialogPlugin, mapping: 'dialog' }],
}
```
Теперь `this.dialog.say('Hello.')` работает в любой сцене, которая получила эту регистрацию.
## Плагин игрового объекта
Третья форма добавляет **новые фабрики** в `this.add` и `this.make`. Каноничный паттерн Phaser 4 — обычная функция, зарегистрированная через `Phaser.GameObjects.GameObjectFactory.register`:
```ts
class HealthBar extends Phaser.GameObjects.Container {
private readonly fg: Phaser.GameObjects.Rectangle;
constructor(scene: Phaser.Scene, x: number, y: number, max: number) {
super(scene, x, y);
const bg = scene.add.rectangle(0, 0, 120, 10, 0x222222);
this.fg = scene.add.rectangle(-60, 0, 120, 10, 0x44dd44).setOrigin(0, 0.5);
this.add([bg, this.fg]);
this.setData('max', max);
this.setData('value', max);
}
set(value: number) {
this.setData('value', value);
this.fg.width = 120 * (value / (this.getData('max') as number));
}
}
Phaser.GameObjects.GameObjectFactory.register('healthBar', function (x, y, max) {
const hb = new HealthBar(this.scene, x, y, max);
this.displayList.add(hb);
this.updateList.add(hb);
return hb;
});
```
Теперь `this.add.healthBar(20, 20, 100)` работает в любой сцене.
Для пользователей TypeScript дополните тип фабрики:
```ts
declare global {
namespace Phaser.GameObjects {
interface GameObjectFactory {
healthBar(x: number, y: number, max: number): HealthBar;
}
}
}
```
## Подводные камни жизненного цикла
- **Не обращайтесь к `this.scene` до `boot`.** Сценические плагины создаются рано; `this.scene` устанавливается в `boot`.
- **Очищайте подписки в `shutdown`.** Сцены могут запускаться и останавливаться многократно; плагины, привязывающиеся к событиям сцены без отписки, будут давать утечки.
- **Синглтоны глобальных плагинов.** Глобальный плагин — это синглтон: храните состояние, специфичное для сцены, внутри сцен, а не в плагине.
## Упаковка
Для совместного использования между проектами поставляйте как ESM-модуль, где класс плагина является экспортом по умолчанию, плюс вызов регистрации (или помощник `register(game)`, если хотите, чтобы потребители делали это явно). Не держите никаких зависимостей от Phaser в рантайм-импортах пакета — Phaser принадлежит потребителю; объявите его как `peerDependency`.
## Связанные материалы
- [Сцены](/phaser/02-concepts/scenes/) — где живут сценические плагины.
- [Игровые объекты](/phaser/02-concepts/game-objects/) — что расширяют плагины игровых объектов.
---
## [Phaser] Переходы между сценами
URL: https://cadmus.page/phaser/03-guides/scene-transitions/
Section: Гайды
Description: API `scene.transition()` — анимированная передача управления между сценами с одновременной работой обеих сцен в момент наложения.
`scene.start(key)` переключает сцены жёстко — `shutdown` уходящей сцены срабатывает раньше, чем `create` входящей. `scene.transition({...})` даёт **окно наложения**, в котором обе сцены работают вместе — идеально для перекрёстных затуханий (crossfade), въездов (slide-in) и эффектов «сдвинуть одну сцену за пределы экрана, пока следующая въезжает».
## Форма вызова
```ts
this.scene.transition({
target: 'Game', // ключ сцены, которую нужно ввести
duration: 600, // мс — в течение этого времени работают обе сцены
sleep: false, // если true, эта сцена засыпает (с возможностью возобновления); иначе shutdown
remove: false, // если true, эта сцена полностью удаляется
moveAbove: true, // отрисовывать целевую сцену поверх этой
allowInput: false, // отключить ввод на этой сцене во время перехода
onUpdate: (progress) => { /* вызывается каждый кадр, 0..1 */ },
});
```
Читается так: «запусти `Game` параллельно со мной; в течение следующих 600 мс вызывай `onUpdate` со значением прогресса; когда переход завершится, сделай со мной *X* (sleep / shutdown / remove)».
`init` и `create` целевой сцены срабатывают немедленно. Её `update` выполняется каждый кадр начиная с нулевого кадра перехода. Уходящая сцена тоже продолжает обновляться (если только вы не остановите её из `onUpdate`).
## Crossfade (перекрёстное затухание)
Самый распространённый паттерн. Твином снижаем alpha уходящей сцены, пока alpha входящей сцены растёт.
```ts
// В уходящей сцене:
this.scene.transition({
target: 'Game',
duration: 500,
moveAbove: true,
allowInput: false,
onUpdate: (progress) => {
this.cameras.main.setAlpha(1 - progress);
},
});
// create() входящей сцены:
create() {
this.cameras.main.setAlpha(0);
this.scene.get('Title').events.once('transitionout', () => {
// Уходящая сцена завершена — при необходимости выполните очистку.
});
// Прогресс перехода доступен в `this.scene.systems.settings.transitionProgress`
// в течение окна наложения; alpha камеры этой сцены может зеркалить его.
}
```
Если обе сцены зеркалят прогресс, получается настоящий crossfade — входящая сцена поднимается от alpha 0, пока уходящая опускается.
## Slide-in (въезд)
```ts
this.scene.transition({
target: 'Pause',
duration: 400,
sleep: true, // приостановить эту сцену; возобновим её после закрытия Pause
moveAbove: true,
onUpdate: (progress) => {
// Вдвигаем сцену паузы справа.
const pause = this.scene.get('Pause');
pause.cameras.main.scrollX = (1 - progress) * this.scale.width;
},
});
```
`sleep: true` — это ключевой приём: когда сцена паузы закрывается (`this.scene.stop()` изнутри неё), Phaser **пробуждает** эту сцену, а не создаёт её заново. Это снимает всю проблему «сохранения состояния при переключении сцен».
## События, генерируемые во время перехода
| Событие | Где | Когда |
|----------------------|---------------------------|------------------------------------------|
| `transitionout` | events уходящей сцены | Когда начинается переход. |
| `transitioninit` | events целевой сцены | После `init` целевой сцены, до `create`. |
| `transitionstart` | events целевой сцены | После `create` целевой сцены. |
| `transitioncomplete` | events целевой сцены | Когда окно перехода завершается. |
```ts
this.events.once('transitioncomplete', () => {
this.cameras.main.setAlpha(1); // гарантируем полную видимость
});
```
## Когда использовать transition, а когда start или launch
| Чего хотите | Используйте |
|------------------------------------------|--------------------------------------|
| Жёсткая склейка, замена текущей сцены | `scene.start(key)` |
| Запустить другую сцену **параллельно**, бессрочно (HUD, фоновая музыка) | `scene.launch(key)` |
| Анимировать переключение с наложением | `scene.transition({...})` |
| Анимировать переключение без наложения (склейка + анимация появления в новой сцене) | `scene.start` + `camera.fadeIn` в новой сцене |
«Переключение» на основе затухания, показанное в [examples/scene-transition](/phaser/04-examples/scene-transition/), — это вариант *без наложения*: затухание, переключение, появление. Он проще и подходит для большинства случаев. Используйте `scene.transition()`, когда нужно, чтобы обе сцены отрисовывались одновременно.
## Типичные ошибки
**Забыть про `allowInput: false`.** Во время наложения обработчики ввода обеих сцен активны. Клик по кнопке в уходящей сцене вызовет её обработчик — обычно это не то, что нужно. Установка `allowInput: false` на уходящей сцене во время перехода предотвращает это.
**Устаревшее состояние при возобновлении.** Если использовать `sleep: true`, то `create` сцены **не** запускается заново при возобновлении. Сбрасывайте временное состояние вместо этого в обработчике события `'wake'`:
```ts
this.events.on('wake', () => {
this.timer = 0;
this.player.setVelocity(0);
});
```
**Очистка при `remove: true`.** Установка `remove: true` немедленно уничтожает display list уходящей сцены. Если во время перехода у вас был твин, пишущий в этот display list, он выдаст ошибку, когда цели исчезнут. Для анимированной передачи используйте семантику `sleep` или `shutdown`.
## Связанное
- [Сцены](/phaser/02-concepts/scenes/) — жизненный цикл, параллельные сцены, передача данных.
- [Камеры](/phaser/02-concepts/cameras/) — `fadeIn` / `fadeOut` для более простых переходов без наложения.
- [examples/scene-transition](/phaser/04-examples/scene-transition/) — запускаемое переключение на основе затухания.
---
## [Phaser] Примеры
URL: https://cadmus.page/phaser/04-examples/00-overview/
Section: Примеры
Description: Запускаемые примеры Phaser 4 — каждый представляет собой изолированную песочницу, которую можно прочитать, форкнуть и изменить.
Каждый пример здесь выполняется в изолированном iframe с зафиксированной сборкой Phaser 4. Исходный код на странице — это в точности тот код, который выполняется; никакой скрытой настройки нет.
## Начальные примеры
- [Базовый спрайт](/phaser/04-examples/basic-sprite/) — загрузить текстуру и отрисовать её.
- [Движение клавиатурой](/phaser/04-examples/keyboard-movement/) — перемещение спрайта на WASD / стрелках, в стиле опроса ввода со скоростью на основе delta-time.
- [Переход между сценами](/phaser/04-examples/scene-transition/) — затухание между двумя сценами через API затухания камеры.
- [Цепочка твинов](/phaser/04-examples/tween-chain/) — выстроить последовательность эффектов-твинов на одном объекте.
- [Реакция на столкновение](/phaser/04-examples/collision-response/) — клик создаёт физические шарики, которые сталкиваются друг с другом и с границами мира.
- [Залп частиц](/phaser/04-examples/particle-burst/) — клик в любом месте запускает однократный взрыв через режим explode эмиттера.
- [Пересечение в физике (подбираемые предметы)](/phaser/04-examples/physics-overlap/) — заходите в звёзды, чтобы собирать их; канонический паттерн «пересечение как триггер».
- [Шейдерный фильтр (Glow)](/phaser/04-examples/shader-filter/) — встроенный фильтр Glow на спрайте, сила которого анимируется твином.
По-прежнему хотелось бы добавить больше — загрузку tilemap, ввод с геймпада, сохранение/загрузку через глобальный плагин, собственный GLSL-объект Shader. Если у вас есть рабочий пример, демонстрирующий то, что существующие руководства лишь описывают, откройте PR.
## Создание примера
Примеры находятся в `src/content/docs/examples/` в виде файлов `.mdx`. Они импортируют компонент `Playground` и передают ему единственную строку `code`. Простейший рабочий шаблон смотрите в [basic-sprite.mdx](https://github.com/rigelirin/phaser4-encyclopedia/blob/master/src/content/docs/examples/basic-sprite.mdx).
---
## [Phaser] Базовый спрайт
URL: https://cadmus.page/phaser/04-examples/basic-sprite/
Section: Примеры
Description: Минимально возможный пример Phaser 4 — загрузить текстуру и однократно её отрисовать.
## Примечания
- `Phaser.AUTO` выбирает WebGL там, где он поддерживается, иначе Canvas.
- `parent: 'game'` монтирует игру в ``, предоставляемый iframe песочницы.
- Изображение загружается по сети — пока оно подгружается, песочница ненадолго покажет пустой canvas.
---
## [Phaser] Движение клавиатурой
URL: https://cadmus.page/phaser/04-examples/keyboard-movement/
Section: Примеры
Description: Перемещение спрайта на WASD или стрелках с опросом ввода каждый кадр и скоростью на основе delta-time.
## Примечания
- **Сначала кликните по песочнице**, чтобы iframe получил фокус клавиатуры.
- `createCursorKeys()` даёт готовые к опросу `cursors.left/right/up/down/space/shift`.
- `addKeys({ up: 'W', ... })` позволяет добавить поддержку WASD одной строкой.
- Движение задаётся как `speed * (delta / 1000)` — пиксели в секунду, независимо от частоты кадров. См. [Игровой цикл](/phaser/02-concepts/game-loop/).
- Стиль опроса подходит для непрерывного движения; события — для дискретных действий (прыжок, выстрел). См. [Ввод](/phaser/02-concepts/input/).
---
## [Phaser] Переход между сценами
URL: https://cadmus.page/phaser/04-examples/scene-transition/
Section: Примеры
Description: Затухание между двумя сценами через API fade-out / fade-in камеры.
{
this.cameras.main.fadeOut(500, 0, 0, 0);
this.cameras.main.once('camerafadeoutcomplete', () => this.scene.start('Game'));
});
}
}
class Game extends Phaser.Scene {
constructor() { super('Game'); }
create() {
this.cameras.main.fadeIn(500, 0, 0, 0);
this.add.rectangle(240, 150, 480, 300, 0x301a1a);
this.add.text(240, 130, 'GAME', { fontSize: '26px' }).setOrigin(0.5);
this.add.text(240, 170, 'click to return', { fontSize: '13px', color: '#c99' }).setOrigin(0.5);
this.input.once('pointerdown', () => {
this.cameras.main.fadeOut(500, 0, 0, 0);
this.cameras.main.once('camerafadeoutcomplete', () => this.scene.start('Title'));
});
}
}
new Phaser.Game({
type: Phaser.AUTO,
width: 480,
height: 300,
backgroundColor: '#000',
parent: 'game',
scene: [Title, Game],
});
`}
/>
## Примечания
- Массив `scene:` регистрирует несколько сцен; по умолчанию запускается **первая**.
- `cameras.main.fadeOut(duration, r, g, b)` затемняет камеру; `fadeIn` делает обратное.
- О завершении сигнализирует событие `camerafadeoutcomplete` — идеальное место для смены сцен.
- `scene.start(key)` **заменяет** текущую сцену (в отличие от `scene.launch`, который запускает её параллельно — полезно для HUD и меню паузы).
- Остальную часть жизненного цикла см. в [Сцены](/phaser/02-concepts/scenes/).
---
## [Phaser] Цепочка твинов
URL: https://cadmus.page/phaser/04-examples/tween-chain/
Section: Примеры
Description: Выстроить последовательность эффектов-твинов на одном объекте — перемещение, вращение, затухание, сброс, цикл.
this.tweens.chain({
targets: box,
tweens: [
{ x: 400, duration: 700, ease: 'Cubic.Out' },
{ y: 220, duration: 500, ease: 'Sine.InOut' },
{ angle: 360, duration: 800, ease: 'Back.Out' },
{ alpha: 0, duration: 300 },
{ alpha: 1, duration: 300 },
{ x: 80, y: 80, angle: 0, duration: 700, ease: 'Cubic.InOut' },
],
onComplete: () => run(),
});
run();
}
}
});
`}
/>
## Примечания
- Каждый элемент в `tweens: [...]` — это полноценная конфигурация твина: собственные `duration`, `ease`, `delay`, колбэки.
- Цепочка выполняет их последовательно, фиксируя текущее значение каждого свойства в момент старта.
- Внешний `onComplete: () => run()` перезапускает цепочку — паттерн «вечного цикла». (Для более простых циклов можно также использовать `repeat: -1` на обычном твине.)
- См. [руководство по твинам](/phaser/03-guides/tweens/) для цепочек, value-твинов и остальной части API.
---
## [Phaser] Реакция на столкновение
URL: https://cadmus.page/phaser/04-examples/collision-response/
Section: Примеры
Description: Клик в любом месте создаёт физический шарик — шарики сталкиваются друг с другом и с границами мира.
{
const b = balls.create(pointer.x, pointer.y, 'ball');
b.setCircle(b.width / 2);
b.setVelocityX(Phaser.Math.Between(-180, 180));
});
}
}
});
`}
/>
## Примечания
- Физическая группа с `collideWorldBounds` удерживает каждого участника внутри canvas.
- `physics.add.collider(group, group)` заставляет участников группы сталкиваться друг с другом — именно второй проход по группе даёт отскок шариков друг от друга.
- `setCircle(radius)` переключает тело с прямоугольного AABB по умолчанию на круг. Arcade в Phaser по умолчанию использует прямоугольники даже для круглых спрайтов.
- События указателя срабатывают на iframe — `pointer.x` / `pointer.y` уже в координатах canvas.
- См. [Arcade Physics](/phaser/03-guides/physics-arcade/) для групп, коллайдеров, пересечений и прочего.
---
## [Phaser] Залп частиц
URL: https://cadmus.page/phaser/04-examples/particle-burst/
Section: Примеры
Description: Клик в любом месте запускает однократный взрыв через режим explode эмиттера частиц.
{
burst.emitParticleAt(pointer.x, pointer.y, 40);
});
}
}
});
`}
/>
## Примечания
- **`emitting: false`** не даёт эмиттеру непрерывно порождать частицы, оставляя возможность запускать залпы по требованию.
- **`emitParticleAt(x, y, count)`** немедленно порождает `count` частиц в заданной мировой позиции.
- **`blendMode: 'ADD'`** даёт яркий эффект «свечения» — переключитесь на `'NORMAL'`, если хотите более чёткие, менее «раскалённые» частицы.
- Сам эмиттер находится в `(0, 0)` — позиции частиц абсолютны, поскольку мы переопределяем их через `emitParticleAt`. Для следа за игроком разместите эмиттер и используйте поведение по умолчанию (испускание из центра).
- См. [руководство по частицам](/phaser/03-guides/particles/) для зон эмиттера, типовых рецептов (дым / взрыв / след) и производительности.
---
## [Phaser] Пересечение в физике (подбираемые предметы)
URL: https://cadmus.page/phaser/04-examples/physics-overlap/
Section: Примеры
Description: Заходите в звёзды, чтобы собирать их — канонический паттерн «пересечение как триггер» с подсчётом очков.
{
coin.destroy();
this.score += 1;
this.scoreText.setText('Score: ' + this.score);
});
this.cursors = this.input.keyboard.createCursorKeys();
},
update(_time, delta) {
const speed = 240 * (delta / 1000);
if (this.cursors.left.isDown) this.player.x -= speed;
if (this.cursors.right.isDown) this.player.x += speed;
if (this.cursors.up.isDown) this.player.y -= speed;
if (this.cursors.down.isDown) this.player.y += speed;
}
}
});
`}
/>
## Примечания
- **Сначала кликните по песочнице**, чтобы передать ей фокус клавиатуры.
- **`overlap` против `collider`**: overlap вызывает колбэк при пересечении ограничивающих рамок *без* их разделения — идеально для подбираемых предметов, триггеров, хитбоксов. `collider` разделяет тела и не даёт им проникать друг в друга — для стен, полов, врагов.
- **Удобство групп**: один вызов `overlap(player, coinsGroup, cb)` охватывает каждую монету в группе. Не нужно настраивать каждую монету по отдельности.
- **`coin.destroy()`** удаляет монету из сцены и из группы; последующие проверки пересечения автоматически её пропускают.
- См. [Arcade Physics](/phaser/03-guides/physics-arcade/) для групп, динамических и статических тел, подводных камней с туннелированием и отладочной отрисовки.
---
## [Phaser] Шейдерный фильтр (Glow)
URL: https://cadmus.page/phaser/04-examples/shader-filter/
Section: Примеры
Description: Применить встроенный фильтр Glow к спрайту и анимировать его силу твином.
## Примечания
- **`filters.external.addGlow(color?, outerStrength?, innerStrength?, ...)`** — один из встроенных фильтров Phaser 4. Полный набор: `Barrel`, `Blend`, `Blocky`, `Blur`, `Bokeh`, `ColorMatrix`, `CombineColorMatrix`, `Displacement`, `Glow`, `GradientMap`, `ImageLight`, `Key`, `Mask`, `NormalTools`, `PanoramaBlur`, `ParallelFilters`, `Pixelate`, `Quantize`, `Sampler`, `Shadow`, `Threshold`, `TiltShift`, `Vignette`, `Wipe`.
- **Списки `internal` против `external`**: внутренние (internal) фильтры влияют только на сам объект; внешние (external) влияют на объект в его контексте отрисовки (обычно на весь экран). Для Glow обычно нужен `external`, поскольку свечение выходит за пределы исходных пикселей.
- **Возвращаемый Controller** — обычный объект: такие свойства, как `outerStrength`, `innerStrength` и `color`, дружелюбны к твинам, что делает анимацию эффекта тривиальной.
- **Примечание: Bloom — это не один фильтр.** В v3 был FX-эффект `Bloom`; в v4 он собирается через `Phaser.Actions.AddEffectBloom(target, config?)`, который внутри настраивает несколько фильтров через `ParallelFilters`. Фильтр Glow выше — это более простое приближение из одного фильтра.
- См. [руководство по шейдерам](/phaser/03-guides/shaders/) для полного каталога фильтров, собственных игровых объектов `Shader` и случаев, когда стоит спуститься до уровня RenderNodes.
---
## [Phaser] Глоссарий — термины Phaser глазами веб-разработчика
URL: https://cadmus.page/phaser/05-glossary/01-terms/
Section: Глоссарий
Description: Короткие определения ключевых понятий Phaser 4 с аналогами из веба и других движков.
Сводный список терминов Phaser 4, которые встречаются в энциклопедии. Phaser живёт прямо в
браузере на JavaScript/TypeScript, поэтому веб-аналогии здесь особенно близки.
## Игра и сцены
**Game** — корневой объект (`new Phaser.Game(config)`): создаёт canvas, заводит игровой цикл,
менеджеры рендера, ввода, звука и сцен. Аналог: корень приложения (`ReactDOM.createRoot`), который
монтируется в DOM один раз.
**Scene** — самостоятельный «экран» или режим: владеет своим списком отображения, камерой,
обработчиком ввода и (опционально) миром физики. Аналог: маршрут (route) в SPA. Сцены не
взаимоисключающие — несколько могут идти параллельно (геймплей + HUD + пауза).
**Lifecycle** — методы сцены, которые движок вызывает по порядку: `init(data)` → `preload()` →
`create(data)` → `update(t, dt)` … → `shutdown()`. Аналог: хуки жизненного цикла компонента
(`constructor` → монтирование → `render` каждый кадр → размонтирование).
**preload / create / update** — `preload` ставит ассеты в очередь загрузки; `create` строит сцену,
когда загрузка завершена; `update` вызывается каждый кадр. Аналог: загрузка данных → первый рендер →
анимационный кадр (`requestAnimationFrame`).
## Объекты на экране
**Game Object** — всё, что можно поместить в сцену: `Sprite`, `Image`, `Text`, `Container`,
`Graphics` и т.д. Аналог: DOM-элемент. Создаются фабрикой `this.add.*` (например `this.add.sprite(...)`).
**Sprite** — игровой объект с текстурой, который умеет проигрывать анимации. Аналог: `
`, который
ещё и анимируется покадрово.
**Image** — как `Sprite`, но без анимаций; легче по накладным расходам. Берите, когда кадр статичен.
**Container** — игровой объект-группировщик с собственным трансформом: двигаешь контейнер — двигаются
дети. Аналог: ``-обёртка, задающая систему координат для вложенных элементов.
**Group** — пул/коллекция игровых объектов для массовых операций и переиспользования (например пули).
В отличие от `Container`, не задаёт общий трансформ — это про управление множеством, а не про вложенность.
**Display List** — упорядоченный список игровых объектов сцены; порядок задаёт порядок отрисовки
(z-order). Аналог: дерево DOM, где порядок элементов влияет на наложение. Это *retained-mode*: вы
описываете объекты один раз, а движок перерисовывает их каждый кадр сам.
## Графика и ассеты
**Texture** — растровые данные изображения в видеопамяти, на которые ссылаются спрайты по ключу.
Аналог: уже декодированная картинка в кэше браузера, готовая к отрисовке.
**Sprite Sheet** — одна картинка с сеткой кадров одинакового размера. Phaser нарезает её на кадры по
индексам. Основа покадровой анимации.
**Texture Atlas** — одна картинка + JSON с произвольными прямоугольниками кадров (имена, координаты).
Гибче спрайт-листа; экономит вызовы отрисовки. Аналог: CSS-спрайты с картой координат.
**Loader** — система загрузки ассетов (`this.load.image(...)`, `this.load.audio(...)`), которая
работает в `preload`. Аналог: пакетный `fetch` с прогрессом и событиями завершения.
**Cache** — типизированное хранилище загруженных ассетов (текстуры, аудио, JSON). После загрузки
ассет доступен по ключу из любой сцены. Аналог: in-memory store загруженных ресурсов.
## Цикл и время
**Game Loop** — главный цикл: каждый кадр движок обновляет логику (`update`) и перерисовывает кадр.
Аналог: бесконечный `requestAnimationFrame`, но с менеджментом сцен, ввода и физики поверх.
**delta (dt)** — сколько миллисекунд прошло с прошлого кадра. Умножайте перемещения на `dt`, чтобы
скорость не зависела от частоты кадров. Аналог: разница `timestamp` между двумя `rAF`.
**Time / Clock** — таймеры и отложенные события сцены (`this.time.addEvent`, `delayedCall`). Аналог:
`setTimeout` / `setInterval`, но привязанные к игровому циклу и паузе сцены.
## Камера и ввод
**Camera** — определяет, какую часть мира и куда рисовать: скролл, зум, поворот, следование за целью,
эффекты (тряска, затухание). У каждой сцены есть `this.cameras.main`. Аналог: вьюпорт + CSS-трансформ
для всего содержимого.
**Viewport** — прямоугольная область экрана, в которую рисует камера. Можно иметь несколько камер
(сплит-скрин, миникарта).
**Input / Pointer** — единый слой ввода: клавиатура, указатель (мышь + тач), геймпад. `Pointer`
абстрагирует мышь и палец в один объект. Аналог: pointer events в DOM.
## Физика
**Arcade Physics** — быстрая физика на основе AABB (прямоугольники, выровненные по осям) и окружностей:
без вращения и стопок тел. Для платформеров, шутеров, большинства аркад. Аналог по «дешевизне»:
простые проверки пересечения прямоугольников.
**Body** — физическое представление игрового объекта (позиция, скорость, ускорение, границы). Меняете
`body`, а движок двигает спрайт. Бывает динамическим или статическим.
**collision / overlap** — `collider` заставляет тела отталкиваться; `overlap` лишь сообщает о
пересечении, ничего не двигая (триггеры, подбор предметов). Аналог: hit-test без физического отклика.
**Matter** — интеграция Matter.js: полноценная физика твёрдых тел с вращением, стопками, соединениями
и произвольными формами. Тяжелее Arcade — берите, когда нужна «настоящая» физика.
## Анимация и эффекты
**Animation** — именованная последовательность кадров текстуры, проигрываемая на спрайте
(`this.anims.create`, `sprite.play('run')`). Аналог: покадровый CSS `@keyframes` для спрайт-листа.
**Tween** — плавная интерполяция свойств объекта во времени (позиция, угол, прозрачность, масштаб):
`this.tweens.add({ targets, x: 100, duration: 300 })`. Аналог: CSS-transition / Web Animations API.
**Easing** — функция плавности твина (`Linear`, `Sine.easeInOut`, `Bounce` …), задающая «характер»
движения. Аналог: `cubic-bezier()` / `ease-in-out`.
**Tween Chain** — последовательность твинов, выполняемых друг за другом. Аналог: цепочка `.then()`
или последовательность keyframes.
**Particle Emitter** — источник частиц (`this.add.particles`) для эффектов: искры, дым, взрывы.
Настраивается скоростью, временем жизни, гравитацией; режим `explode` даёт одиночный залп.
## Тайлмапы
**Tilemap** — сетка из тайлов, описывающая уровень; обычно импортируется из редактора Tiled
(`this.make.tilemap`). Аналог: CSS-grid из переиспользуемых клеток, но для игрового уровня.
**Tileset** — изображение-источник тайлов, на которое ссылается тайлмап.
**Tile / Layer** — `Tile` — одна клетка сетки; `Layer` — слой тайлов (земля, декор, коллизии).
Слои рисуются и проверяются на столкновения независимо.
## Рендеринг и масштаб
**WebGL / Canvas** — два бэкенда отрисовки. `Phaser.AUTO` выбирает WebGL, где он поддерживается, иначе
откатывается на Canvas 2D. Аналог: выбор между ускоренным GPU-рендером и программным.
**Shader / Filter** — программа на GPU для пост-эффектов (свечение, размытие, искажение). В Phaser 4
встроенные эффекты доступны через фильтры (`filters.external.addGlow(...)`); для своего кода пишут
`Shader` game object на GLSL.
**Pipeline / RenderNode** — низкоуровневый слой конвейера рендеринга WebGL. Трогают редко — когда
нужен полный контроль над тем, как объекты попадают на экран.
**Scale Manager** — управляет тем, как canvas вписывается в окно (режимы `FIT`, `RESIZE`,
`ENVELOP`, …). Аналог: адаптивная вёрстка/`object-fit` для игрового холста.
## Расширение
**Plugin** — переиспользуемый модуль, добавляющий возможности в `Game` или сцену (глобальный или
сценовый). Аналог: npm-пакет / middleware, расширяющий приложение без правки ядра.