32 KiB
id, title
| id | title |
|---|---|
| gameplay | Набор для геймплея |
Набор для геймплея
Модуль геймплея добавляет то, что большинство 2D-игр собирают вручную: контроллер персонажа, очки здоровья, хитбоксы и хёртбоксы, смертельные зоны, контрольные точки с возрождением, подбираемые предметы и удаление по таймеру. Это компоненты: их настраивают в Инспекторе, сохраняют вместе со сценой и управляют ими из скриптов несколькими вызовами.
Быстрый старт: Создать → Геймплей → Персонаж платформера даёт узел с кинематическим «Телом 2D», коллайдером, «Движением 2D», «Здоровьем», «Хёртбоксом 2D» и «Возрождением». Дайте ему скрипт, который вызывает Move и Jump, добавьте пол и нажмите Играть.
Набор используют две готовые игры. aego new mygame --example platformer — враги, которых давят хитбоксом на ногах, яма со смертельной зоной, контрольная точка, сохраняемая точка возрождения и «Аниматор» для облика игрока с ударом на кадре попадания. aego new mygame --example topdown — меч, которым машут в сторону стика, мыши или направления ходьбы, враги, чья «Машина состояний» переключается между Patrol и Chase, а «Аниматор» это показывает, выпад через root motion, монеты и лава. Лаунчер предлагает обе игры в разделе Примеры.
| Компонент | Назначение |
|---|---|
| Движение 2D | ходьба, прыжки и скольжение кинематического тела: склоны, ступеньки, движущиеся платформы, движение с видом сверху |
| Здоровье | очки здоровья, команда, неуязвимость после удара, смерть и оживление |
| Хёртбокс 2D | область, где хитбоксы ранят «Здоровье» |
| Хитбокс 2D | область, которая наносит урон хёртбоксам, с отбрасыванием |
| Время жизни | убирает узел через несколько секунд |
| Смертельная зона 2D | убивает или возрождает то, что в неё падает |
| Контрольная точка 2D | переносит точку возрождения |
| Возрождение | возвращает узел в его точку возрождения |
| Подбираемый предмет 2D | посылает Collected, когда персонаж его касается |
| Машина состояний | именованные состояния с методами Enter, Exit, Fixed и Update в скриптах узла |
Всё работает во время игры и в aego run, на фиксированном шаге, и ничего не выделяет на каждом шаге. Хёртбоксы, хитбоксы и зоны — области в пространстве своего узла, а не физические коллайдеры: они ничего не толкают, и слои столкновений им не нужны.
id: gameplay/motion title: Движение персонажа
Движение персонажа
«Движение 2D» двигает персонажа с кинематическим «Телом 2D». На каждом фиксированном шаге оно проверяет коллайдеры тела вдоль пути, скользит вдоль того, во что упирается, и переносит тело в итоговую точку. Поэтому персонаж никогда не проваливается в стены и всё равно толкает ящики.
В интерактивном примере ходите клавишами A/D или стрелками и прыгайте клавишей Space, W или Up; подъёмник, склон и стены работают через настоящую физику.
m := node.Get<Motion2D>()
m.Move(input.Axis("Move"))
if input.Pressed("Jump") {
m.Jump()
}
if input.Released("Jump") {
m.JumpReleased()
}
Move действует до следующего вызова. Jump — один запрос, который контроллер хранит «Буфер прыжка» секунд, поэтому прыжок, нажатый незадолго до приземления, всё равно происходит.
| Режим | Поведение |
|---|---|
| platformer | ходит по полу под действием гравитации, прыгает, поднимается на ступеньки и склоны |
| topDown | свободно движется в плоскости через MoveDir, скользит вдоль стен; «Поворот по движению» поворачивает узел |
Пол, склоны и ступеньки
Пол, стены и потолок различаются по углу поверхности к «Направлению вверх»: поверхность круче «Макс. уклона» — стена. На полу персонаж следует его наклону и не съезжает вниз, а «Прилипание к земле» держит его на полу при спуске по склону или с небольшой ступеньки. На ступеньки до «Высоты ступеньки» персонаж поднимается без прыжка. «Безопасный зазор» — расстояние до каждой поверхности, которое не даёт следующей проверке начаться внутри неё.
В воздухе стена останавливает движение в неё и никогда не поднимает персонажа: капсула или круг, задевшие угол уступа, соскальзывают с него, а не повисают на нём.
Уступы
Если Подъём на уступ больше 0, персонаж в воздухе, который движется в стену, чей верх не выше «Подъёма на уступ» над его ногами, забирается на неё. За Время подъёма секунд он поднимается до верха, шагает на уступ и приземляется там. Более высокие стены он не трогает. Подъём идёт на фиксированных шагах, как и всё движение: во время него ввод, гравитация и root motion не учитываются, а скорость остаётся нулевой. Отбрасывание (AddVelocity), Stop, Dash и Teleport сразу прерывают его.
Climbing истинно во время подъёма, а Climbed срабатывает в его начале. Чтобы анимировать подъём, передавайте флаг «Аниматору» в FixedUpdate, как это делает игрок в примере платформера:
a := node.Get<Animator>()
a.SetBool("Climbing", m.Climbing)
и добавьте в контроллер состояние Climb, в которое переход идёт из any, пока Climbing истинно.
Прыжки
Задайте Высоту прыжка, и контроллер вычислит скорость прыжка из неё и гравитации; Скорость прыжка вместо этого задаёт скорость напрямую. Если отпустить кнопку во время подъёма, прыжок укоротится на Множитель короткого прыжка, поэтому короткое нажатие даёт небольшой подскок.
Два окна прощают неточные прыжки. Время койота позволяет прыгнуть ещё мгновение после схода с края. Буфер прыжка помнит прыжок, нажатый в воздухе, до приземления. Управление в воздухе решает, какая часть ускорения на земле доступна в воздухе.
Все поля перечислены на странице «Движение 2D».
Платформы и тела
С «Движущимися платформами» персонаж едет на теле, на котором стоит. «Платформа при уходе» решает, что станет со скоростью платформы после прыжка: keep добавляет её, drop теряет, add сохраняет только горизонтальную часть. С «Толкать тела» динамические тела не останавливают персонажа — он их толкает.
Остальное покрывают Dash(velocity, seconds), AddVelocity для отбрасывания, AddDisplacement(offset) для root motion и скриптовых толчков, Stop и Teleport. Смещение двигает персонажа сквозь стены и склоны так же, как движение, на следующем шаге и не меняет скорость. Поля «Во время игры» показывают скорость, стоит ли персонаж на земле, касается ли стены или потолка, нормаль и угол пола и тело, на котором он стоит.
id: gameplay/health title: Здоровье и урон
Здоровье и урон
«Здоровье» хранит очки здоровья. Хитбоксы, смертельные зоны и скрипты наносят урон через Damage(amount, source), Kill(source) и Heal(amount). При нуле узел умирает один раз и остаётся мёртвым до Revive(hp).
hp := node.Get<Health>()
if hp.Damage(2, other) {
audio.PlayEvent(HitSound)
}
После удара узел игнорирует урон «Время неуязвимости» секунд игрового времени. Отсчёт начинается после шага, поэтому засчитываются все удары одного шага: два одновременно попавших меча ранят оба, а Died всё равно срабатывает один раз. SetInvulnerable(seconds) намеренно делает узел неуязвимым.
При смерти узел с «Возрождением» возвращается. Иначе «Уничтожить при смерти» удаляет его, а «Убрать при смерти» возвращает в его пул. «Команда» защищает узел от хитбоксов той же команды, если в них не включён урон по своим.
В сцене нажмите J рядом с манекеном, чтобы ударить его. Он показывает свои очки здоровья и возвращается через секунду после смерти.
id: gameplay/hurtbox title: Хёртбоксы
Хёртбоксы
«Хёртбокс 2D» — область в пространстве своего узла, где хитбоксы ранят «Здоровье»: указанное в его поле «Здоровье» или ближайшее на этом узле или выше. Несколько хёртбоксов могут делить одно «Здоровье», поэтому у персонажа могут быть голова, тело и щит на дочерних узлах.
У каждого хёртбокса есть область в пространстве его узла: прямоугольник или вписанный в него круг. Множитель масштабирует полученный там урон: 2 для уязвимого места, 0,5 для брони. Выключите Активен, чтобы игнорировать удары, — для уклонений и кадров неуязвимости в анимации.
У манекена есть хёртбокс головы с «Множителем» 2: подпрыгните и ударьте, чтобы попасть в него.
id: gameplay/hitbox title: Хитбоксы
Хитбоксы
«Хитбокс 2D» после шага физики наносит урон хёртбоксам внутри своей области. «Режим ударов» решает, как часто бьётся одна цель:
| Режим | Удары |
|---|---|
| onEnter | при каждом входе цели |
| oncePerActivation | один раз на цель до Activate |
| periodic | каждый «Период» секунд, пока области пересекаются |
«Команда» и «Урон по своим» не дают атакам задевать свою сторону. «Команда» −1 берёт команду ближайшего «Здоровья» выше, поэтому меч на дочернем узле сражается за владельца и никогда его не ранит. «Отбрасывание» — скорость для цели с «Движением 2D» и импульс для динамического тела. Оно поворачивается вместе с узлом хитбокса, поэтому отражённый персонаж толкает в другую сторону. «Отбрасывание от центра» вместо этого отталкивает от хитбокса.
Взмах мечом — один вызов:
if input.Pressed("Attack") {
Sword.Get<Hitbox2D>().Pulse()
}
Pulse() наносит один раунд урона на следующем шаге, активен хитбокс или нет. Activate() и Deactivate() включают хитбокс на время атаки. Hit(target, damage) срабатывает для каждой цели, получившей урон.
Пройдите по трём шипам в сцене, чтобы сравнить режимы.
id: gameplay/lifetime title: Время жизни
Время жизни
«Время жизни» убирает свой узел через «Секунды» игрового времени или реального с «Реальным временем». С «Убрать» созданный узел возвращается в свой пул — это подходит пулям, искрам и обломкам; иначе узел уничтожается. «Осталось» показывает оставшиеся секунды.
Шары в сцене создаются из префаба через scene.Spawn, через две секунды возвращаются в пул и снова создаются из него; полоса слева уничтожается через двадцать.
id: gameplay/killzone title: Смертельные зоны
Смертельные зоны
«Смертельная зона 2D» убивает «Здоровье», позиция узла которого входит в область, или запускает «Возрождение» у узла без «Здоровья». «Тег» ограничивает её узлами с тегом; без тега учитывается любой узел со «Здоровьем» или «Возрождением». Ставьте такие зоны под ямами и одну широкую под уровнем.
Зайдите в яму в сцене: персонаж умрёт и вернётся на старт.
id: gameplay/checkpoint title: Контрольные точки
Контрольные точки
«Контрольная точка 2D» переносит точку возрождения вошедшего в неё узла с «Возрождением» в свою позицию или к узлу из «Точки появления». Контрольная точка никогда не заменяет точку с более высоким «Приоритетом», поэтому возврат назад не теряет прогресс. «Один раз» включает её только для первого узла. Reached(by) срабатывает, когда узел берёт контрольную точку.
id: gameplay/respawn title: Возрождение
Возрождение
«Возрождение» возвращает свой узел после смерти, после смертельной зоны или когда скрипт вызывает Trigger(). Первая точка возрождения — место, где узел начинает; контрольные точки и SetPoint(position) её переносят. Через «Задержку» секунд узел телепортируется туда, «Восстановить здоровье» оживляет его с полными очками здоровья, и срабатывает Respawned.
С «Сохранением» точка возрождения хранится в сохранениях, поэтому загруженная игра начинается с последней контрольной точки; «Здоровье» тоже хранит свои очки в сохранениях.
Нажмите R в сцене, чтобы возродиться намеренно.
id: gameplay/pickup title: Подбираемые предметы
Подбираемые предметы
«Подбираемый предмет 2D» посылает Collected(by), когда в него входит узел: узел с тегом или любой узел с «Движением 2D». С «Один раз» предмет после этого исчезает, а с «Убрать» возвращается в свой пул.
func Ready() {
conn = Coin.Get<Pickup2D>().Collected.Connect(OnCoin)
}
func OnCoin(by NodeRef) {
coins++
}
Счётчик в углу сцены подключён к каждой монете.
id: gameplay/statemachine title: Машины состояний
Машины состояний
«Машина состояний» перечисляет имена состояний, а поведение хранят скрипты её узла. Для состояния Run она вызывает, если они есть:
| Метод | Когда |
|---|---|
EnterRun() |
машина входит в Run |
ExitRun() |
машина выходит из Run |
FixedRun(dt float) |
на каждом фиксированном шаге в Run, до того как двигается «Движение 2D» |
UpdateRun(dt float) |
каждый кадр в Run, после Update скриптов |
func UpdateIdle(dt float) {
if node.Get<Motion2D>().Velocity.X != 0 {
node.Get<StateMachine>().Go("Run")
}
}
func EnterRun() {
node.Get<SpriteRenderer>().Tint = RunColor
}
Go(state) только ставит смену в очередь. Когда выполняемый метод вернётся, выполнятся Exit старого состояния и Enter нового, а затем сработает Changed(from, to) с индексами состояний. Go внутри Enter или Exit выполняется после этой смены, поэтому EnterRun может сразу отправить машину дальше в Air; подряд происходит не больше восьми смен. Is(state) и TimeIn() читают состояние; «Текущее», «Предыдущее» и «Время» показывают его в Инспекторе.
Машина входит в «Начальное» в первом кадре, после Ready скриптов. Методы ищутся по имени и находятся заново после горячей перезагрузки, поэтому переименование метода действует сразу. Методы жизненного цикла, обработчики событий и сопрограммы никогда не считаются методами состояний. «Проблемы» предупреждают о машине без состояний, о состоянии, указанном дважды, и о «Начальном», которого нет среди состояний (GMP005).
Ходите, останавливайтесь и прыгайте в сцене: цвет и надпись следуют за состоянием.
id: gameplay/time title: Время и повторы
Время и повторы
Движение, удары, зоны и возрождения работают на фиксированном шаге: gameplay.fsm_fixed, а затем gameplay.motion — перед физикой, после script.fixed; gameplay.post — после физики, до того как скрипты получат события столкновений. gameplay.fsm_update выполняет методы Update машин состояний после script.update. Поэтому сигналы одного шага доходят до скриптов в том же шаге, что и вызвавшие их столкновения.
Урон, нанесённый скриптом в Update, применяется сразу. Его Damaged, Healed и Died приходят в конце того же кадра из gameplay.frame, который также отсчитывает «Время жизни». Неуязвимость, задержки возрождения и периодические удары считают игровое время и останавливаются на паузе; «Время жизни» с «Реальным временем» считает реальное время.
Всё состояние компонентов, включая скрытое состояние контроллера, хитбоксов, зон и машин состояний, хранится в сцене. Перемотка, повторы и пулы восстанавливают его точно, и хеш состояния, который использует aego test --determinism, его включает.
id: gameplay/scripting title: Скрипты
Скрипты
Компоненты доступны через node.Get<T>(); импортировать модуль не нужно.
| Компонент | Методы | Сигналы |
|---|---|---|
| Motion2D | Move(x), MoveDir(dir), Jump(), JumpReleased(), Dash(velocity, seconds), AddVelocity(v), AddDisplacement(offset), Stop(), Teleport(position) |
Landed(speed), LeftGround(), Jumped(), HitWall(normal), HitCeiling() |
| Health | Damage(amount, source) bool, Heal(amount), Kill(source), Revive(hp), SetInvulnerable(seconds) |
Damaged(amount, source), Healed(amount), Died(source), Revived() |
| Hitbox2D | Activate(), Deactivate(), Pulse() |
Hit(target, damage) |
| Checkpoint2D | Reached(by) |
|
| Respawn | Trigger(), SetPoint(position) |
Respawned() |
| Pickup2D | Collected(by) |
|
| StateMachine | Go(state) bool, Is(state) bool, TimeIn() float |
Changed(from, to) |
Тряска при приземлении и экран смерти:
func Ready() {
landed = node.Get<Motion2D>().Landed.Connect(OnLanded)
died = node.Get<Health>().Died.Connect(OnDied)
}
func OnLanded(speed float) {
if speed > 8 {
camera.Shake(0.3)
}
}
func OnDied(source NodeRef) {
GameOver.SetEnabled(true)
}
Короткое имя вроде Health сначала находит собственный компонент проекта: проект, где уже есть свой Health, продолжает получать его, а node.Get<gameplay.Health>() обращается к этому компоненту по полному имени.
id: gameplay/editor title: Инструменты редактора
Инструменты редактора
Редактор собирает готовые узлы геймплея, рисует области ударов и во время игры показывает состояние контроллеров, здоровья и машин состояний.
Готовые узлы
Создать → Геймплей собирает готовые узлы:
- «Персонаж платформера»: кинематическое тело, коллайдер, «Движение 2D», «Здоровье», «Хёртбокс 2D» и «Возрождение»;
- «Персонаж с видом сверху»;
- «Враг»: динамическое тело, «Здоровье» в команде 1 и периодический «Хитбокс 2D» с отбрасыванием;
- «Движущаяся платформа», «Контрольная точка», «Смертельная зона» и «Подбираемый предмет».
Обязательные компоненты
Добавить компонент добавляет то, что требует компонент. «Движение 2D» на пустом узле приносит с собой «Тело 2D», и одна отмена убирает оба. Список показывает, что добавится вместе с ним. aego scene add-comp, вставка и пресеты добавляют их так же. Если добавить «Движение 2D» на узел с телом, тело станет кинематическим.
Области
- Выберите узел с хитбоксом, хёртбоксом, смертельной зоной, контрольной точкой или подбираемым предметом, чтобы перетаскивать углы и края его области. Привязка работает, и каждое перетаскивание — один шаг отмены. Хитбокс ещё показывает своё отбрасывание стрелкой.
- Геймплей → Области геймплея показывает все области приглушённо на панели «Сцена». Во время игры панель «Игра» рисует активные хёртбоксы и хитбоксы, зоны и полоску здоровья над каждым узлом со «Здоровьем».
Инспектор во время игры
Состояние контроллера во время игры показывает его скорость, стоит ли он на земле, касание стены и потолка, пол и тело под ним. «Движение 2D» добавляет сводку контактов, «Здоровье» — полоску с очками здоровья, а «Машина состояний» — текущее состояние и сколько оно длится. «Время жизни» показывает оставшиеся секунды.
Профилирование и проблемы
- Профилировщик → Геймплей показывает контроллеры и проверки за фиксированный шаг, а также удары, смерти, возрождения и подборы на текущий момент.
- «Монитор событий» перечисляет смены состояний, урон (с величиной до 9), смерти, возрождения и подборы строками
gameplay. - «Проблемы» показывают «Движение 2D», тело которого не кинематическое (GMP001, блокирует игру), платформер с «Масштабом гравитации» 0 без «Скорости прыжка» (GMP002), машину состояний с пустыми, повторяющимися или неизвестными состояниями (GMP005) и «Хёртбокс 2D» без «Здоровья» на своём узле или выше (GMP006; удары по нему игнорируются).
id: gameplay/go title: Go
Go
if hp, ok := scene.Get[gameplay.Health](s, node); ok {
hp.Damage(3, attacker)
}
if m, ok := scene.Get[gameplay.Motion2D](s, player); ok {
m.AddVelocity(mathx.Vec2{X: 6, Y: -4})
}
st := gameplay.Of(world).Stats()
Типы компонентов и их методы те же, что в скриптах. gameplay.Of(world) возвращает сервис мира игры; его метод Stats считает удары, смерти, возрождения и подборы с начала игры.
Машины состояний вызывают методы скриптов через контракт script.call (contracts/scriptcall): Method(node, name, params) находит обычный метод скрипта на узле, Call выполняет его, а новый Generation после горячей перезагрузки делает старые ссылки устаревшими. Раздел сохранения gameplay.respawn хранит точки возрождения с отметкой «Сохранение».
«Движение 2D» читает и двигает тела только через контракт physics2d.queries: CastBody из сдвинутой позы, Penetration, BodyPose, BodyVelocity, MoveKinematic, Teleport и Gravity. С любым физическим модулем, который предоставляет этот контракт, контроллер работает без изменений. Root motion доходит до контроллера через контракт motion2d.receiver (contracts/motion2d): AddDisplacement(node, offset) возвращает false для узла без «Движения 2D».


