Files
aego/engine/res/docs/platformer.ru.md
sanspie ab6ea7ef58
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / linux amd64 native (push) Skipped
ci / checks (push) Successful in 6m33s
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 12m26s
ci / release (push) Skipped
github-native / darwin amd64 native (push) Canceled after 0s
github-native / windows amd64 native (push) Canceled after 0s
github-native / darwin arm64 native (push) Canceled after 0s
editor: improve scene tools and prepare native CI
2026-11-06 16:49:11 +03:00

653 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: platformer
title: Платформер с нуля
---
# Платформер с нуля
Это руководство собирает небольшой платформер в пустом 2D-проекте: персонажа, который бегает и прыгает, камеру, которая следует за ним, яму с контрольной точкой, монеты и врага. Каждый шаг заканчивается файлом сцены в том виде, какой он имеет после шага, и каждую сцену и каждый скрипт на этой странице запускают проверки документации.
Если вы ещё не открывали редактор, начните с раздела [Первый проект](ref:quickstart).
Готовый пример — отдельный, более подробный проект `examples/platformer`. Откройте его командой `aego edit examples/platformer` и загрузите основную сцену, чтобы изучить игру из демо на сайте. A/D или стрелки двигают персонажа, пробел — прыжок, J — атака, Esc открывает настройки. F6 включает и выключает освещение, F7 переключает его режимы отладки.
Руководство ниже начинается с простых фигур, а не повторяет все ассеты готового проекта. Его освещение, карты поверхностей, частицы и постобработка описаны отдельно: [Свет и эффекты платформера](ref:platformer-effects).
## 1. Пол и игрок
Создайте проект из шаблона **2D**. В «Иерархии» создайте пустой узел и назовите его **Floor**. Добавьте ему в «Инспекторе» такие компоненты:
- **«Отрисовка спрайта»**, «Размер» `30, 1`, серый «Оттенок»;
- **«Тело 2D»**, «Тип» `static`;
- **«Коллайдер-прямоугольник 2D»**, «Размер» `30, 1`.
Переместите его в `0, 3`. Создайте второй узел, `Player`, с компонентами:
- **«Отрисовка спрайта»**, «Размер» `0.6, 1`, оранжевый «Оттенок»;
- **«Тело 2D»**, «Тип» `kinematic`, «Без вращения» включено;
- **«Коллайдер-прямоугольник 2D»**, «Размер» `0.6, 1`, «Трение» `0`;
- **«Движение 2D»**, «Режим» `platformer`.
«Движение 2D» двигает кинематическое тело: оно скользит вдоль стен, стоит на полу и забирается на невысокие ступеньки. Нажмите «Играть»: игрок падает и приземляется на пол.
```aescene check
scene version=1 {
node "f1000000000000000000000000000001" name="Floor" enabled=true layers=1 {
transform {
position=[0, 3]
}
component "engine.SpriteRenderer" version=1 {
tint=#4b5563ff
size=[30, 1]
}
component "physics.Body2D" version=1 {
type=static
}
component "physics.BoxCollider2D" version=1 {
size=[30, 1]
}
}
node "f1000000000000000000000000000002" name="Player" enabled=true layers=1 {
component "engine.SpriteRenderer" version=1 {
tint=#f59e0bff
size=[0.6, 1]
}
component "physics.Body2D" version=1 {
type=kinematic
fixedRotation=true
}
component "physics.BoxCollider2D" version=1 {
friction=0
size=[0.6, 1]
}
component "gameplay.Motion2D" version=1 {
mode=platformer
}
}
node "f1000000000000000000000000000003" name="Camera" enabled=true layers=1 {
component "engine.Camera2D" version=1 {
zoom=36
}
}
}
```
## 2. Бег и прыжок
Создайте скрипт `Player` в `scripts/game` и добавьте его игроку:
```aes check
package game
import "aego/input"
script Player {
func Update(dt float) {
m := node.Get<Motion2D>()
m.Move(input.Axis("Move"))
if input.Pressed("Jump") {
m.Jump()
}
if input.Released("Jump") {
m.JumpReleased()
}
}
}
```
`Move` и `Jump` — действия ввода шаблона 2D: стрелки или A и D и пробел, либо геймпад. `Move` принимает значение от -1 до 1, и «Движение 2D» разгоняется до этой доли своей «Макс. скорости». `JumpReleased` обрывает прыжок, если кнопку отпустили рано, поэтому короткое нажатие даёт маленький подскок. «Высота прыжка», «Макс. скорость» и «Время койота» — поля компонента [«Движение 2D»](ref:gameplay.Motion2D).
```aescene check
scene version=1 {
node "f1000000000000000000000000000001" name="Floor" enabled=true layers=1 {
transform {
position=[0, 3]
}
component "engine.SpriteRenderer" version=1 {
tint=#4b5563ff
size=[30, 1]
}
component "physics.Body2D" version=1 {
type=static
}
component "physics.BoxCollider2D" version=1 {
size=[30, 1]
}
}
node "f1000000000000000000000000000002" name="Player" enabled=true layers=1 {
component "engine.SpriteRenderer" version=1 {
tint=#f59e0bff
size=[0.6, 1]
}
component "physics.Body2D" version=1 {
type=kinematic
fixedRotation=true
}
component "physics.BoxCollider2D" version=1 {
friction=0
size=[0.6, 1]
}
component "gameplay.Motion2D" version=1 {
mode=platformer
}
component "engine.Script" version=1 {
script "game.game.Player" enabled=true {
}
}
}
node "f1000000000000000000000000000003" name="Camera" enabled=true layers=1 {
component "engine.Camera2D" version=1 {
zoom=36
}
}
}
```
Сцена называет скрипт по модулю, пакету и имени. Здесь модуль — `game`; в вашем проекте это модуль из `project.aego`.
## 3. Камера, которая следует за игроком
Выберите камеру и добавьте **«Виртуальная камера 2D»** и **«Слежение камеры 2D»**. Перетащите игрока из «Иерархии» в поле «Цель» компонента «Слежение камеры 2D». Теперь камера держит игрока в кадре и плавно догоняет его; «Мёртвая зона» и «Демпфирование» задают, насколько свободно. Зоны, тряска и переходы описаны в разделе [Камеры](ref:camera).
```aescene check
scene version=1 {
node "f1000000000000000000000000000001" name="Floor" enabled=true layers=1 {
transform {
position=[0, 3]
}
component "engine.SpriteRenderer" version=1 {
tint=#4b5563ff
size=[30, 1]
}
component "physics.Body2D" version=1 {
type=static
}
component "physics.BoxCollider2D" version=1 {
size=[30, 1]
}
}
node "f1000000000000000000000000000002" name="Player" enabled=true layers=1 {
component "engine.SpriteRenderer" version=1 {
tint=#f59e0bff
size=[0.6, 1]
}
component "physics.Body2D" version=1 {
type=kinematic
fixedRotation=true
}
component "physics.BoxCollider2D" version=1 {
friction=0
size=[0.6, 1]
}
component "gameplay.Motion2D" version=1 {
mode=platformer
}
component "engine.Script" version=1 {
script "game.game.Player" enabled=true {
}
}
}
node "f1000000000000000000000000000003" name="Camera" enabled=true layers=1 {
component "engine.Camera2D" version=1 {
zoom=36
}
component "camera.VirtualCamera2D" version=1 {
}
component "camera.CameraFollow2D" version=1 {
target=node("f1000000000000000000000000000002")
}
}
}
```
## 4. Яма и контрольная точка
Разделите пол на две части с промежутком между ними. Под промежутком добавьте узел `Pit` с компонентом **«Смертельная зона 2D»**, «Область» которой накрывает низ уровня. Добавьте игроку **«Здоровье»** и **«Возрождение»**. За ямой добавьте узел `Checkpoint` с компонентом **«Контрольная точка 2D»**.
Падение в яму убивает игрока. Через «Задержку» компонента «Возрождение» он возвращается к последней контрольной точке, которой коснулся, или туда, откуда начал, с полным здоровьем.
```aescene check
scene version=1 {
node "f1000000000000000000000000000001" name="Floor" enabled=true layers=1 {
transform {
position=[-6, 3]
}
component "engine.SpriteRenderer" version=1 {
tint=#4b5563ff
size=[16, 1]
}
component "physics.Body2D" version=1 {
type=static
}
component "physics.BoxCollider2D" version=1 {
size=[16, 1]
}
}
node "f1000000000000000000000000000004" name="Floor 2" enabled=true layers=1 {
transform {
position=[13, 3]
}
component "engine.SpriteRenderer" version=1 {
tint=#4b5563ff
size=[16, 1]
}
component "physics.Body2D" version=1 {
type=static
}
component "physics.BoxCollider2D" version=1 {
size=[16, 1]
}
}
node "f1000000000000000000000000000005" name="Pit" enabled=true layers=1 {
transform {
position=[3.5, 8]
}
component "gameplay.KillZone2D" version=1 {
area=[-30, -1, 60, 2]
}
}
node "f1000000000000000000000000000006" name="Checkpoint" enabled=true layers=1 {
transform {
position=[8, 1.5]
}
component "engine.SpriteRenderer" version=1 {
tint=#22c55eff
size=[0.15, 2]
}
component "gameplay.Checkpoint2D" version=1 {
}
}
node "f1000000000000000000000000000002" name="Player" enabled=true layers=1 {
component "engine.SpriteRenderer" version=1 {
tint=#f59e0bff
size=[0.6, 1]
}
component "physics.Body2D" version=1 {
type=kinematic
fixedRotation=true
}
component "physics.BoxCollider2D" version=1 {
friction=0
size=[0.6, 1]
}
component "gameplay.Motion2D" version=1 {
mode=platformer
}
component "gameplay.Health" version=1 {
max=3
}
component "gameplay.Respawn" version=1 {
}
component "engine.Script" version=1 {
script "game.game.Player" enabled=true {
}
}
}
node "f1000000000000000000000000000003" name="Camera" enabled=true layers=1 {
component "engine.Camera2D" version=1 {
zoom=36
}
component "camera.VirtualCamera2D" version=1 {
}
component "camera.CameraFollow2D" version=1 {
target=node("f1000000000000000000000000000002")
}
}
}
```
## 5. Монеты
Монета — это узел с компонентом **«Подбираемый предмет 2D»**. Её подбирают, когда узел с «Движением 2D» входит в её «Область», и после этого она удаляется. Чтобы считать монеты, добавьте игроку счётчик:
```aes check
package game
import (
"aego/input"
"aego/log"
)
script Player {
var coins int
func Update(dt float) {
m := node.Get<Motion2D>()
m.Move(input.Axis("Move"))
if input.Pressed("Jump") {
m.Jump()
}
if input.Released("Jump") {
m.JumpReleased()
}
}
func AddCoin() {
coins++
log.Info("coins: " + string(coins))
}
}
```
и дайте каждой монете скрипт, который сообщает об этом игроку:
```aes check
package game
script Coin {
func Ready() {
node.Get<Pickup2D>().Collected.Connect(OnCollected)
}
func OnCollected(by NodeRef) {
p := by.GetScript<Player>()
if p != nil {
p.AddCoin()
}
}
}
```
Сделайте одну монету, затем превратите её в префаб (**Создать префаб** в меню «Иерархии») и расставьте её копии; изменение префаба доходит до каждой копии. См. [Префабы](ref:prefabs).
```aescene check
scene version=1 {
node "f1000000000000000000000000000001" name="Floor" enabled=true layers=1 {
transform {
position=[-6, 3]
}
component "engine.SpriteRenderer" version=1 {
tint=#4b5563ff
size=[16, 1]
}
component "physics.Body2D" version=1 {
type=static
}
component "physics.BoxCollider2D" version=1 {
size=[16, 1]
}
}
node "f1000000000000000000000000000004" name="Floor 2" enabled=true layers=1 {
transform {
position=[13, 3]
}
component "engine.SpriteRenderer" version=1 {
tint=#4b5563ff
size=[16, 1]
}
component "physics.Body2D" version=1 {
type=static
}
component "physics.BoxCollider2D" version=1 {
size=[16, 1]
}
}
node "f1000000000000000000000000000005" name="Pit" enabled=true layers=1 {
transform {
position=[3.5, 8]
}
component "gameplay.KillZone2D" version=1 {
area=[-30, -1, 60, 2]
}
}
node "f1000000000000000000000000000006" name="Checkpoint" enabled=true layers=1 {
transform {
position=[8, 1.5]
}
component "engine.SpriteRenderer" version=1 {
tint=#22c55eff
size=[0.15, 2]
}
component "gameplay.Checkpoint2D" version=1 {
}
}
node "f1000000000000000000000000000007" name="Coin" enabled=true layers=1 {
transform {
position=[2, 2]
}
component "engine.SpriteRenderer" version=1 {
tint=#facc15ff
size=[0.4, 0.4]
}
component "gameplay.Pickup2D" version=1 {
}
component "engine.Script" version=1 {
script "game.game.Coin" enabled=true {
}
}
}
node "f1000000000000000000000000000002" name="Player" enabled=true layers=1 {
component "engine.SpriteRenderer" version=1 {
tint=#f59e0bff
size=[0.6, 1]
}
component "physics.Body2D" version=1 {
type=kinematic
fixedRotation=true
}
component "physics.BoxCollider2D" version=1 {
friction=0
size=[0.6, 1]
}
component "gameplay.Motion2D" version=1 {
mode=platformer
}
component "gameplay.Health" version=1 {
max=3
}
component "gameplay.Respawn" version=1 {
}
component "engine.Script" version=1 {
script "game.game.Player" enabled=true {
}
}
}
node "f1000000000000000000000000000003" name="Camera" enabled=true layers=1 {
component "engine.Camera2D" version=1 {
zoom=36
}
component "camera.VirtualCamera2D" version=1 {
}
component "camera.CameraFollow2D" version=1 {
target=node("f1000000000000000000000000000002")
}
}
}
```
## 6. Враг
Враг ходит туда-обратно и ранит игрока при касании. Дайте ему такое же тело, как у игрока, **«Движение 2D»** и **«Хитбокс 2D»** с «Командой» `1`, чтобы он никогда не ранил других врагов из команды 1. Дайте игроку **«Хёртбокс 2D»**: хитбокс наносит урон «Здоровью» узла, хёртбокса которого касается. После этого игрок мигает в течение «Времени неуязвимости», и его нельзя ударить снова, пока оно не закончится.
Враг разворачивается у стен:
```aes check
package game
script Patrol {
@export
var Speed float = 0.5
var dir float = 1
func Ready() {
node.Get<Motion2D>().HitWall.Connect(OnWall)
}
func OnWall(normal Vec2) {
if normal.X != 0 {
dir = normal.X
}
}
func FixedUpdate(dt float) {
node.Get<Motion2D>().Move(dir * Speed)
}
}
```
`HitWall` даёт нормаль стены, направленную от стены, поэтому её X — новое направление.
```aescene check
scene version=1 {
node "f1000000000000000000000000000001" name="Floor" enabled=true layers=1 {
transform {
position=[-6, 3]
}
component "engine.SpriteRenderer" version=1 {
tint=#4b5563ff
size=[16, 1]
}
component "physics.Body2D" version=1 {
type=static
}
component "physics.BoxCollider2D" version=1 {
size=[16, 1]
}
}
node "f1000000000000000000000000000004" name="Floor 2" enabled=true layers=1 {
transform {
position=[13, 3]
}
component "engine.SpriteRenderer" version=1 {
tint=#4b5563ff
size=[16, 1]
}
component "physics.Body2D" version=1 {
type=static
}
component "physics.BoxCollider2D" version=1 {
size=[16, 1]
}
}
node "f1000000000000000000000000000009" name="Wall" enabled=true layers=1 {
transform {
position=[20.5, 1.5]
}
component "engine.SpriteRenderer" version=1 {
tint=#4b5563ff
size=[1, 2]
}
component "physics.Body2D" version=1 {
type=static
}
component "physics.BoxCollider2D" version=1 {
size=[1, 2]
}
}
node "f1000000000000000000000000000005" name="Pit" enabled=true layers=1 {
transform {
position=[3.5, 8]
}
component "gameplay.KillZone2D" version=1 {
area=[-30, -1, 60, 2]
}
}
node "f1000000000000000000000000000006" name="Checkpoint" enabled=true layers=1 {
transform {
position=[8, 1.5]
}
component "engine.SpriteRenderer" version=1 {
tint=#22c55eff
size=[0.15, 2]
}
component "gameplay.Checkpoint2D" version=1 {
}
}
node "f1000000000000000000000000000007" name="Coin" enabled=true layers=1 {
transform {
position=[2, 2]
}
component "engine.SpriteRenderer" version=1 {
tint=#facc15ff
size=[0.4, 0.4]
}
component "gameplay.Pickup2D" version=1 {
}
component "engine.Script" version=1 {
script "game.game.Coin" enabled=true {
}
}
}
node "f1000000000000000000000000000008" name="Enemy" enabled=true layers=1 {
transform {
position=[14, 2]
}
component "engine.SpriteRenderer" version=1 {
tint=#ef4444ff
size=[0.8, 0.8]
}
component "physics.Body2D" version=1 {
type=kinematic
fixedRotation=true
}
component "physics.BoxCollider2D" version=1 {
friction=0
size=[0.8, 0.8]
}
component "gameplay.Motion2D" version=1 {
mode=platformer
}
component "gameplay.Hitbox2D" version=1 {
area=[-0.4, -0.4, 0.8, 0.8]
team=1
active=true
}
component "engine.Script" version=1 {
script "game.game.Patrol" enabled=true {
}
}
}
node "f1000000000000000000000000000002" name="Player" enabled=true layers=1 {
component "engine.SpriteRenderer" version=1 {
tint=#f59e0bff
size=[0.6, 1]
}
component "physics.Body2D" version=1 {
type=kinematic
fixedRotation=true
}
component "physics.BoxCollider2D" version=1 {
friction=0
size=[0.6, 1]
}
component "gameplay.Motion2D" version=1 {
mode=platformer
}
component "gameplay.Health" version=1 {
max=3
}
component "gameplay.Hurtbox2D" version=1 {
area=[-0.3, -0.5, 0.6, 1]
}
component "gameplay.Respawn" version=1 {
}
component "engine.Script" version=1 {
script "game.game.Player" enabled=true {
}
}
}
node "f1000000000000000000000000000003" name="Camera" enabled=true layers=1 {
component "engine.Camera2D" version=1 {
zoom=36
}
component "camera.VirtualCamera2D" version=1 {
}
component "camera.CameraFollow2D" version=1 {
target=node("f1000000000000000000000000000002")
}
}
}
```
## 7. Играйте где угодно
**Файл → Экспорт…** собирает игру для Linux, Windows, macOS или веба, как в разделе [Первый проект](ref:quickstart). На телефоне веб-сборка показывает сенсорный геймпад: стик двигает, а A прыгает, потому что действия привязаны и к геймпаду.
## Что дальше
- [Рецепты](ref:recipes): двойной прыжок, односторонние платформы, движущиеся платформы, рывок и многое другое.
- [Набор геймплея](ref:gameplay): все компоненты геймплея в одном месте.
- [Анимация](ref:animation): анимация спрайтов и машины состояний для игрока.
- [Свет и эффекты платформера](ref:platformer-effects): карты поверхностей, источники света, частицы и постобработка готового примера.
- `examples/platformer` в исходниках движка — эта же игра, развитая дальше: анимация, звук, прыжки на врагов, сохранения, настройки и 2D-освещение.