
После долгого пути я рад представить AdaEngine 0.1.0: бесплатный игровой движок и фреймворк для приложений с открытым исходным кодом, написанный на Swift.
AdaEngine строится вокруг простой идеи: Swift должен быть отличным языком для создания игр, интерактивных приложений, инструментов и творческого софта — не только приложений для платформ Apple. Swift выразителен, безопасен, быстр и удобен в написании. AdaEngine пытается перенести эти сильные стороны в разработку игр через модульный движок, data-driven архитектуру и API, которые ощущаются естественно для Swift-разработчиков.
AdaEngine доступен на GitHub под лицензией MIT. Этот первый релиз все еще ранний, но для проекта это уже настоящий рубеж: движок умеет открывать окна, запускать игровой цикл на ECS, рендерить спрайты и UI, загружать ассеты и сцены, воспроизводить звук, обрабатывать ввод, запускать физику и собирать примеры из разных модулей движка.
⚠️ Ранний релиз AdaEngine 0.1.0 — ранний релиз. API будут меняться, часть возможностей пока не завершена, документация еще растет, а шероховатости ожидаемы. Я пока не рекомендую использовать его для серьезных production-проектов, если только вы не готовы к нестабильности и не хотите помочь сформировать движок.
Если это звучит интересно, можно сразу перейти к туториалам или посмотреть репозиторий на GitHub.
📖 В этой статье я по возможности даю ссылки на документацию AdaEngine и исходный код. Документация генерируется из кодовой базы, поэтому она будет развиваться вместе с движком.
Что такое AdaEngine?
AdaEngine — data-driven игровой движок и фреймворк для приложений на Swift. Его основные цели:
-
Простота: легко начать новичкам, но при этом достаточно гибко для опытных пользователей.
-
Модульность: большинство возможностей движка поставляется в виде плагинов, так что можно выбрать только то, что нужно вашему приложению.
-
Data-driven подход: в основе AdaEngine лежит Entity Component System.
-
Быстрая итерация: движок проектируется под быстрые сборки и быструю обратную связь.
-
Практичность: первый фокус — полноценный 2D workflow, при этом поддержка 3D уже есть и будет развиваться.
-
Кроссплатформенность по дизайну: сейчас AdaEngine ориентирован на платформы Apple и активно движется к более широкой поддержке, включая Windows, Linux, Android и WebAssembly/WebGPU.
Текущий набор возможностей включает:
-
Спрайты: рендер множества текстур с batching; отдельные текстуры, sprite sheets и анимированные текстуры.
-
Сцены: сохранение и загрузка ECS-миров из человекочитаемых файлов сцен.
-
Tilemaps: создание уровней через LDtk или интеграция другого редактора через предоставленные API.
-
2D-физика: встроенная поддержка на базе Box2D v3.
-
Ассеты: загрузка и сохранение игровых ассетов, async-загрузка и handles для ассетов.
-
Hot asset reloading: перезагрузка измененных ассетов во время выполнения, чтобы оставаться в потоке.
-
Аудио: загрузка и воспроизведение звуковых ресурсов, включая пространственное воспроизведение, привязанное к сущностям.
-
Плагины: рендеринг, аудио, ввод, UI, события, физика, сцены, спрайты и другие системы собираются через плагины.
-
События и observation: коммуникация внутри игры через глобальные события или ECS-style frame events.
-
Parent/child-связи: иерархии сущностей и распространение transform через них.
-
Несколько render backends: Metal на платформах Apple и WebGPU/Dawn там, где это включено.
-
Render graphs: управление тем, как планируется и компонуется работа рендеринга.
-
AdaUI: UI для игр и приложений с API, вдохновленным SwiftUI.
-
Геймпады: доступ к подключенным геймпадам на поддерживаемых платформах.
-
Примеры: растущий набор демо для спрайтов, UI, ввода, событий, сцен, tilemaps и 3D.
Swift-native точка входа приложения
Приложения AdaEngine стартуют с API, который должен быть знаком тем, кто пользовался SwiftUI:
import AdaEngine
@main
struct AdaApp: App {
var body: some AppScene {
DefaultAppWindow()
.windowMode(.windowed)
.windowTitle("Ada App")
}
}
Этого достаточно, чтобы создать окно и установить стандартные плагины движка.
Ключевая философия — кастомизация через плагины. Рендеринг, аудио, ввод, события, UI, физика, сцены, спрайты и другие возможности добавляются в приложение через композицию плагинов. Можно начать с разумных defaults или собрать более легкий runtime, выбрав только нужные части.
Для большего контроля используйте EmptyWindow и добавляйте плагины вручную:
import AdaEngine
@main
struct AdaApp: App {
var body: some AppScene {
EmptyWindow()
.addPlugins(DefaultPlugins())
.windowMode(.windowed)
.windowTitle("Ada App")
}
}
DefaultPlugins — набор, с которого стоит начинать большинству пользователей. Когда нужен более легкий runtime, части этого набора можно отключать через disable(_:).
Entity Component System
Сердце AdaEngine — ECS-фреймворк. Он вдохновлен движками и фреймворками вроде Bevy и RealityKit, но спроектирован так, чтобы ощущаться естественно в Swift.
В Entity Component System:
-
Entities — уникальные идентификаторы.
-
Components — данные, прикрепленные к сущностям.
-
Systems — логика, которая читает и изменяет компоненты.
-
Resources — уникальные значения уровня мира.
Такой подход отделяет игровые данные от игровой логики. Еще он помогает масштабировать игру от нескольких объектов до множества систем и сущностей.
AdaECS использует обычные Swift-типы и добавляет макросы, чтобы уменьшить boilerplate:
import AdaEngine
@Component
struct Position {
var value: Float
}
@Component
struct Velocity {
var value: Float
}
@System
func Movement(
_ query: Query<
Ref<Position>, // read-write access
Velocity // read-only access
>
) {
query.forEach { position, velocity in
position.value += velocity.value
}
}
struct ExamplePlugin: Plugin {
func setup(in app: AppWorlds) {
app.spawn {
Position(value: 0)
Velocity(value: 1)
}
app.spawn {
Position(value: 1)
Velocity(value: 2)
}
app.addSystem(MovementSystem.self, on: .update)
}
}
@main
struct AdaApp: App {
var body: some AppScene {
DefaultAppWindow()
.addPlugins(ExamplePlugin())
}
}
Макрос @System генерирует конкретный тип системы за вас. Вы пишете логику как Swift-функцию, а AdaEngine превращает ее в зарегистрированную ECS-систему.
Queries
Queries извлекают компоненты из мира:
@System
func Movement(_ query: Query<Entity, Transform>) {
query.forEach { entity, transform in
// Iterate over every entity with a Transform.
}
}
Filter queries
Фильтры ограничивают набор подходящих сущностей:
@System
func PlayerMovement(
_ query: FilterQuery<Entity, Transform, With<Player>>
) {
query.forEach { entity, transform in
// Iterate only over entities that also have Player.
}
}
Change detection
Change detection позволяет системе реагировать только тогда, когда меняются релевантные данные:
@System
func EnemyHealthBar(
_ query: FilterQuery<Enemy, Changed<Health>>
) {
query.forEach { enemy in
// Run when Health has been added or changed.
}
}
Resources
Resources хранят уникальные данные уровня мира:
struct GameScore: Resource {
var score: Int
var bulletFireCount: Int
}
world.insertResource(GameScore(score: 0, bulletFireCount: 0))
@System
func UpdateScore(score: ResMut<GameScore>) {
score.score += 1
}
Delta time тоже доступен как resource:
@System
func Movement(
time: Res<DeltaTime>,
query: Query<Ref<Position>>
) {
query.forEach {
$0.value += 20 * time.deltaTime
}
}
Commands
Когда системе нужно создать или удалить сущности либо вставить компоненты, она может использовать Commands. Команды собираются и применяются после завершения системы, что сохраняет выполнение систем безопасным.
@System
func GameStartup(_ commands: Commands) {
commands.spawn("Player") {
Player()
Transform()
}
}
Local values
Системы могут хранить локальное состояние через Local:
@System
func UpdateData(isUpdated: Local<Bool> = false) {
if !isUpdated.wrappedValue {
// Perform one-time work.
isUpdated.wrappedValue = true
}
}
Struct systems
Для большего контроля AdaECS также поддерживает struct-based системы через @PlainSystem:
@PlainSystem(dependencies: [
.after(EnemyMovement.self),
.before(PhysicsSystem.self)
])
struct MovementSystem {
@Query<Player, Transform>
private var playerQuery
init(world: World) {}
func update(context: UpdateContext) {
playerQuery.forEach {
// Update player movement here.
}
}
}
Schedulers
Системы выполняются в schedulers. В AdaEngine есть типовые стадии: startup, pre-update, update, fixed update и другие:
world
.addSystem(StartupSystem.self, on: .startup)
.addSystem(MovementSystem.self, on: .fixedUpdate)
.addSystem(UpdateEnemySystem.self, on: .preUpdate)
.addSystem(UpdateScoreSystem.self, on: .update)
.startup выполняется один раз при запуске приложения. Можно создавать и собственные schedulers, если игре нужна своя модель выполнения.
⚠️ Ранний релиз Будьте осторожны с зависимостями систем. Если система зависит от другой системы, которая не зарегистрирована в том же scheduler, приложение может упасть во время выполнения.
Bundles
Bundles объединяют несколько компонентов в одну переиспользуемую единицу. Макрос @Bundle генерирует код, нужный для распаковки bundle в компоненты:
@Bundle
struct EnemyBundle {
let enemy = Enemy()
let transform: Transform
let health: Health
}
world.spawn(
"Enemy",
bundle: EnemyBundle(
transform: Transform(),
health: Health(30)
)
)
Scriptable objects
Если для части gameplay-кода вам ближе Unity-подобный workflow, AdaEngine предоставляет ScriptableObject и ScriptableComponents:
final class Player: ScriptableObject {
func update(_ deltaTime: TimeInterval) {
if input.isKeyPressed(.w) {
// Move player.
}
}
}
world.spawn("Player") {
ScriptableComponents(
components: [
Player()
]
)
}
Это дает знакомый object-style escape hatch, при этом движок остается ECS-first.
AdaUI
AdaEngine включает UI-фреймворк AdaUI. Он вдохновлен SwiftUI и рассчитан как на игры, так и на инструменты в духе редакторов.
SwiftUI показал, насколько продуктивным может быть декларативный UI. AdaUI приносит похожий стиль внутрь движка: UI-код можно писать прямо на Swift и рендерить внутри сцены AdaEngine.
Views
View реализует протокол View:
struct GameOverView: View {
var body: some View {
Text("Game Over")
}
}
Layout
В AdaUI есть привычные stack layout primitives:
struct GameOverView: View {
var body: some View {
VStack(spacing: 20) {
Text("Game Over")
Text("Try again")
}
}
}
Interactive elements
Кнопки и другие интерактивные controls можно компоновать в том же стиле:
struct MenuView: View {
var body: some View {
Button("Start Game") {
// Start game.
}
Button(action: {
// Open settings.
}, label: {
Text("Settings")
.foregroundColor(.red)
})
}
}
Modifiers
Modifiers применяют стиль и поведение:
struct GameOverView: View {
var body: some View {
VStack(spacing: 20) {
Text("Game Over")
.font(.system(size: 50))
.foregroundColor(.red)
}
}
}
State and bindings
Views могут хранить состояние и обновляться при его изменении:
struct GameOverView: View {
@State private var isDead = false
var body: some View {
VStack(spacing: 20) {
if isDead {
Text("Game Over")
.font(.system(size: 50))
.foregroundColor(.red)
}
}
.onEvent(YourGameEvent.UserDied) {
self.isDead = true
}
}
}
Bindings передают состояние между views:
struct ParentView: View {
@State private var isDead = false
var body: some View {
SubView(isDead: $isDead)
}
}
struct SubView: View {
@Binding var isDead: Bool
var body: some View {
if isDead {
Text("Game Over")
}
}
}
Attaching UI to an entity
Чтобы показать view в мире, прикрепите его через UIComponent:
let gameOverView = GameOverView()
world.spawn("GameOverView") {
UIComponent(view: gameOverView)
}
Environment access
AdaUI views могут читать значения из environment. Например, view, прикрепленный к сущности, может получить доступ к ECS-миру:
struct DebugView: View {
@Environment(.world)
private var world
var body: some View {
Button("Spawn Enemy") {
world.spawn("Enemy", bundle: EnemyBundle())
}
}
}
Images
Images можно использовать прямо в UI:
struct UserAvatarView: View {
var body: some View {
Image("@res://avatar.png")
}
}
AdaUI особенно важен для будущего AdaEngine, потому что редактор планируется строить поверх той же UI-системы, которую смогут использовать игры.
2D-возможности
AdaEngine 0.1.0 сфокусирован на создании крепкой 2D-основы.
Sprites
Спрайты — базовый строительный блок для многих 2D-игр. AdaEngine умеет рендерить спрайты из Texture2D и других texture resources:
let texture = try await AssetsManager.load(Texture2D.self, at: "@res://sprite.png")
world.spawn {
Sprite(texture: texture)
Transform()
}
Texture atlases and sprite sheets
Texture atlases можно использовать для анимации, tile sets и оптимизированного рендеринга:
let image = try await AssetsManager.load(Image.self, at: "@res://characters.png")
let textureAtlas = TextureAtlas(from: image, size: Vector2(16, 16))
world.spawn {
Sprite(
texture: textureAtlas[0, 1],
size: Size(width: 16, height: 16)
)
Transform()
}
Если размер спрайта не указан, AdaEngine может вывести его из текстуры.
Tilemaps
В AdaEngine есть отдельный модуль AdaTilemap. Встроенные демо включают и собственные примеры tilemap, и загрузку tilemap на базе LDtk. Это позволяет визуально собирать уровни, а затем загружать их в ECS-мир.
Цель — поддержать практичные 2D workflows: рисовать уровни в редакторе, загружать их как данные, прикреплять физику и быстро итерироваться.
2D physics
AdaEngine включает AdaPhysics на базе Box2D. Можно прикреплять collision components к сущностям и получать collision events через систему событий.
Физика интегрирована в ECS-мир, поэтому gameplay-код может объединять transforms, sprites, collision components и systems в одной data-driven модели.
Scenes
Сцена — это набор сущностей, компонентов и ресурсов, который можно сохранить, загрузить и заспавнить в мир.
О сцене можно думать как о prefab или файле уровня: она описывает часть игры, которую можно загрузить при необходимости.
Scene files
Сцены сохраняются как человекочитаемый YAML. Файл сцены может включать сущности, данные компонентов, transforms, sprites, physics components и resources:
version: 1.0.0
scene: Scene
world:
entities:
- name: Ground
id: 122210699653662020
components:
AdaSprite.Sprite:
tintColor:
red: 1.0
green: 1.0
blue: 1.0
alpha: 1.0
flipX: false
flipY: false
AdaTransform.Transform:
rotation:
x: 0.0
y: 0.0
z: 0.0
w: 1.0
scale:
x: 3.0
y: 0.19
z: 0.19
position:
x: 0.0
y: -1.0
z: 0.0
AdaPhysics.Collision2DComponent:
shapes:
- fixture:
box:
_0:
halfWidth: 0.5
halfHeight: 0.5
offset:
x: 0.0
y: 0.0
mode:
default: {}
resources: {}
Loading scenes
Сцены являются ассетами, поэтому их можно загружать через asset system:
let scene = try await AssetsManager.load(Scene.self, at: "@res://game_scene.ascn")
world.spawn("Spawned scene") {
DynamicScene(scene: scene)
}
Заспавненная сцена может прикрепить свои сущности и ресурсы под parent entity.
Hot reloading scenes
Hot reloading сцен — одна из самых важных возможностей для итерации. Когда файл сцены меняется, AdaEngine может применить эти изменения к уже запущенной сцене без перезапуска и полной пересборки. Это заметно ускоряет редактирование уровней и настройку gameplay.
📖 Hot reload — ранняя возможность, но направление понятное: редактировать данные, сразу видеть результат и оставаться сфокусированным на игре, а не на цикле сборки.
Events
Играм и приложениям постоянно нужно общаться внутри себя: начинаются столкновения, нажимаются кнопки, открывается UI, появляются враги, подключаются игроки, а системы должны на это реагировать.
AdaEngine поддерживает и глобальный event-style messaging, и ECS frame events.
EventManager
Можно подписаться на событие и сохранить cancellable token:
let cancellable = world.subscribe(
on: CollisionEvents.Began.self
) { payload in
// Handle collision.
}
world.eventManager.sendEvent(SomeEvent())
// Or send globally:
EventManager.default.sendEvent(SomeEvent())
ECS events
Для ECS-native workflows AdaEngine предоставляет Events и EventSender:
@System
func HostConnection(_ events: Events<OnConnect>) {
for event in events {
print("User connected", event.userId)
}
}
@System
func ConnectionUpdate(_ sender: EventSender<OnConnect>) {
sender(OnConnect(userId: "player#123"))
}
📖 ECS events — это frame events: они хранятся только в течение текущего кадра.
Assets
Asset system позволяет загружать и сохранять игровые данные. Ассеты ссылаются через handles, что делает возможным hot reloading.
Например, загрузка текстуры выглядит так:
let texture: AssetHandle<Texture2D> = try await AssetsManager.load(
Texture2D.self,
at: "@res://my_texture.png"
)
Префикс @res:// указывает на директорию ресурсов приложения. По умолчанию AdaEngine ищет папку Assets или Resources в вашем target. Также директорию ресурсов можно задать вручную.
Чтобы загрузить из конкретного bundle:
let texture: AssetHandle<Texture2D> = try await AssetsManager.load(
Texture2D.self,
at: "my_texture.png",
from: Foundation.Bundle(path: "")
)
Чтобы включить hot reloading для ассета, передайте handleChanges: true:
let texture: AssetHandle<Texture2D> = try await AssetsManager.load(
Texture2D.self,
at: "@res://my_texture.png",
handleChanges: true
)
Adding a new asset type
Можно добавить поддержку собственных ассетов, реализовав протокол Asset:
struct MyAsset: Asset {
init(asset decoder: AssetDecoder) async throws {
// Decode asset contents.
}
func encodeContents(with encoder: AssetEncoder) async throws {
// Encode asset contents.
}
static func extensions() -> [String] {
["txt"]
}
}
После этого ассет становится доступен через тот же loading pipeline, что и встроенные текстуры, звуки, сцены и другие ресурсы.
Audio
AdaEngine включает модуль AdaAudio на базе miniaudio. Можно загрузить audio resource и воспроизвести его от сущности:
let backgroundSound = try await AssetsManager.load(
AudioResource.self,
at: "@res://background.wav"
)
let player = world.spawn {
Player()
}
player.prepareAudio(backgroundSound)
.setLoop(true)
.play()
Аудио можно привязывать к сущностям, что открывает путь к spatial sound и воспроизведению, управляемому gameplay.
Rendering
Рендеринг в AdaEngine разделен на модули и плагины. В текущей кодовой базе есть:
-
AdaRenderдля render abstractions, камер, материалов, meshes, текстур, render pipelines и render graphs. -
AdaSpriteдля 2D sprite rendering. -
AdaCorePipelinesдля встроенных rendering pipelines и shaders. -
Поддержка Metal на платформах Apple.
-
Поддержка WebGPU через Dawn/Swan там, где она включена.
-
Инфраструктура компиляции и транспиляции shaders вокруг SPIR-V tooling.
В этом релизе уже есть основа и для 2D-, и для 3D-рендеринга. Сейчас 2D-путь наиболее зрелый. 3D существует — включая meshes, камеры, материалы и демо с кубом, — но ему нужно больше работы, прежде чем он будет ощущаться завершенным.
Render graphs — важная часть будущего направления. Они делают работу рендеринга явной и компонуемой, что должно помочь движку вырасти от простых sprite scenes до более продвинутых pipelines.
Platforms and tooling
AdaEngine — это Swift Package на Swift 6.2. Сейчас package объявляет targets для платформ Apple, таких как macOS 15, iOS 18, tvOS 18 и visionOS 2. Также в нем есть conditional compilation и platform backends для Linux, Windows, Android, WASI/WebAssembly, Metal, WebGPU, X11 и browser runtimes.
Не все платформы пока одинаково зрелые. Платформы Apple сегодня готовы лучше всего, а Windows, Linux, Android и Web входят в активное кроссплатформенное направление.
В репозитории также есть SwiftPM-плагины и инструменты, включая:
-
Ada web export plugin,
-
build tooling, связанный с WebGPU/Tint,
-
инструмент и плагины для сборки texture atlas,
-
tooling для транспиляции shaders,
-
поддержку генерации документации через DocC.
Examples
В репозитории есть примеры в Demos, включая:
-
рендеринг спрайтов,
-
many sprites / stress examples,
-
custom materials,
-
2D lighting,
-
transparency,
-
text rendering,
-
gamepad input,
-
загрузку сцен,
-
LDtk tilemaps,
-
scriptable components,
-
collision events,
-
UI-примеры: buttons, text fields, scene views, animated text и Kanban board,
-
простой 3D-пример с кубом,
-
небольшие игровые демо вроде Snowman Attacks.
Примеры важны, потому что показывают, что движок уже умеет, а еще служат практическими тестами workflows движка.
Зачем я построил AdaEngine
Создавать игры было моей детской мечтой. Я начал учить Java, потому что хотел делать моды для Minecraft. Позже я стал iOS-инженером, но мечта делать игры никуда не исчезла.
Я много свободного времени изучал Godot, погружался в сообщество game development и пытался понять, как движки устроены изнутри. Я начал с небольшого Metal-проекта, продолжал экспериментировать и после нескольких лет работы дошел до этого рубежа: первого релиза AdaEngine.
Я люблю open source. Я люблю Swift. Я сделал много open source Swift-проектов, и мне было интересно посмотреть, что получится, если использовать Swift не только для приложений, но и для полноценного игрового движка.
Swift может предложить многое: value types, protocol-oriented design, macros, structured concurrency, memory safety, сильный tooling и приятный для написания синтаксис. Самая большая проблема не в языке, а в представлении, что Swift принадлежит только разработке под macOS и iOS.
Я не думаю, что это правда. Swift может быть чем-то большим. AdaEngine — моя попытка помочь это доказать.
Что дальше?
AdaEngine 0.1.0 — это начало, а не финишная черта. Следующая фаза — расширять движок, полировать опыт использования и растить сообщество.
Больше платформ
Долгосрочная цель — поддерживать как можно больше платформ. Swift — безопасный и мощный язык, и я верю, что он может отлично подойти для кроссплатформенной разработки игр.
Следующая важная работа по платформам включает WebAssembly/WebGPU, Linux, Android и дальнейшую поддержку Windows.
Редактор
Разработчики игр хотят быстрее прототипировать и писать меньше boilerplate. AdaUI дает основу для редактора на той же UI-системе, которую смогут использовать игры.
Собрать AdaEditor на AdaUI — важная цель: это улучшит UI-фреймворк, проверит tooling движка и сделает AdaEngine доступнее для пользователей, которым ближе визуальные workflows.
3D rendering and polish
2D feature set — главный фокус этого релиза, но поддержка 3D уже есть и продолжит улучшаться. Работы много: лучшие материалы, более полные возможности рендеринга, MSAA, более богатый scene tooling, workflows для моделей и многое другое.
Движку также нужна полировка во многих системах: asset workflows, hot reloading, интеграция с редактором, diagnostics, examples и дизайн API.
Документация и туториалы
API все еще нестабилен, а документация местами sparse. В ближайшем будущем AdaEngine нужны новые туториалы, более качественные guides и больше примеров, которые показывают полные workflows: от настройки проекта до готовых игровых механик.
Хорошая документация — не опция. Это часть движка.
Присоединяйтесь к AdaEngine
Если вам это интересно, загляните в AdaEngine на GitHub, прочитайте серию туториалов, изучите примеры и присоединяйтесь к обсуждению.
AdaEngine сейчас создается волонтерами. Если вы хотите помочь строить игровой движок на Swift — кодом, документацией, примерами, тестированием, feedback по дизайну или идеями, — вам очень рады.
Это всего лишь версия 0.1.0, но это начало того, что я давно хотел построить.
Давайте делать игры на Swift.
Автор: VladislavPrusakov
