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

22 KiB
Raw Permalink Blame History

id, title
id title
platformer Платформер с нуля

Платформер с нуля

Это руководство собирает небольшой платформер в пустом 2D-проекте: персонажа, который бегает и прыгает, камеру, которая следует за ним, яму с контрольной точкой, монеты и врага. Каждый шаг заканчивается файлом сцены в том виде, какой он имеет после шага, и каждую сцену и каждый скрипт на этой странице запускают проверки документации.

Если вы ещё не открывали редактор, начните с раздела Первый проект.

Готовый пример — отдельный, более подробный проект examples/platformer. Откройте его командой aego edit examples/platformer и загрузите основную сцену, чтобы изучить игру из демо на сайте. A/D или стрелки двигают персонажа, пробел — прыжок, J — атака, Esc открывает настройки. F6 включает и выключает освещение, F7 переключает его режимы отладки.

Руководство ниже начинается с простых фигур, а не повторяет все ассеты готового проекта. Его освещение, карты поверхностей, частицы и постобработка описаны отдельно: Свет и эффекты платформера.

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» двигает кинематическое тело: оно скользит вдоль стен, стоит на полу и забирается на невысокие ступеньки. Нажмите «Играть»: игрок падает и приземляется на пол.

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 и добавьте его игроку:

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».

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». Теперь камера держит игрока в кадре и плавно догоняет его; «Мёртвая зона» и «Демпфирование» задают, насколько свободно. Зоны, тряска и переходы описаны в разделе Камеры.

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».

Падение в яму убивает игрока. Через «Задержку» компонента «Возрождение» он возвращается к последней контрольной точке, которой коснулся, или туда, откуда начал, с полным здоровьем.

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» входит в её «Область», и после этого она удаляется. Чтобы считать монеты, добавьте игроку счётчик:

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))
	}
}

и дайте каждой монете скрипт, который сообщает об этом игроку:

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()
		}
	}
}

Сделайте одну монету, затем превратите её в префаб (Создать префаб в меню «Иерархии») и расставьте её копии; изменение префаба доходит до каждой копии. См. Префабы.

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»: хитбокс наносит урон «Здоровью» узла, хёртбокса которого касается. После этого игрок мигает в течение «Времени неуязвимости», и его нельзя ударить снова, пока оно не закончится.

Враг разворачивается у стен:

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 — новое направление.

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 или веба, как в разделе Первый проект. На телефоне веб-сборка показывает сенсорный геймпад: стик двигает, а A прыгает, потому что действия привязаны и к геймпаду.

Что дальше

  • Рецепты: двойной прыжок, односторонние платформы, движущиеся платформы, рывок и многое другое.
  • Набор геймплея: все компоненты геймплея в одном месте.
  • Анимация: анимация спрайтов и машины состояний для игрока.
  • Свет и эффекты платформера: карты поверхностей, источники света, частицы и постобработка готового примера.
  • examples/platformer в исходниках движка — эта же игра, развитая дальше: анимация, звук, прыжки на врагов, сохранения, настройки и 2D-освещение.