WINDAGE ENGINE v1.0.0
Contents

Docs / Start

Your first game

A game is a Swift package that depends on the engine and supplies one object conforming to GameLoop. This page builds the smallest one that opens a window and draws something, using the same set-up the sample game uses.

The package

// swift-tools-version: 6.2
import PackageDescription

let package = Package(
    name: class="p">"MyGame",
    platforms: [.macOS(.v26)],
    dependencies: [
        .package(url: class="p">"https://github.com/Pixla-app/WindageEngine.git", from: class="p">"1.0.0")
    ],
    targets: [
        .executableTarget(
            name: class="p">"MyGame",
            dependencies: [.product(name: class="p">"WindageEngine", package: class="p">"WindageEngine")],
            swiftSettings: [.defaultIsolation(MainActor.self)]
        )
    ]
)

Three things in it are deliberate:

A relative-path dependency works too, for an engine checkout beside the game, but then the game no longer builds from its own checkout alone.

A minimal GameLoop

This game counts simulated seconds and draws the number with the engine's HUDRenderer. It draws no scene, so the frame shows clearColor.

import Metal
import WindageEngine

final class Game: GameLoop {
    private let hud: HUDRenderer
    private var viewportSize = SIMD2<Float>(1, 1)
    private var seconds: Double = 0

    init(engine: FrameRenderer) throws {
        hud = try HUDRenderer(device: engine.device)
        engine.residencySet.addAllocations(hud.allocations)
        engine.commitResidency()
    }

    func beginFrame() {}

    func step(deltaTime: Float) {
        seconds += Double(deltaTime)
    }

    func viewportSizeChanged(width: Int, height: Int) {
        viewportSize = SIMD2(Float(width), Float(height))
    }

    func encode(into encoder: MTL4RenderCommandEncoder, frameSlot: Int, interpolation: Float) {}

    func encodeHUD(into encoder: MTL4RenderCommandEncoder, frameSlot: Int, interpolation: Float) {
        hud.begin(viewportSize: viewportSize)
        hud.text(String(format: class="p">"%.1f s", seconds), atBaseline: SIMD2(40, 80), pointSize: 48, color: .white)
        hud.draw(into: encoder, frameSlot: frameSlot)
    }
}

encodePrePasses is left out because it has a default that does nothing; encodeHUD has one too. Everything the GPU touches has to be in the renderer's residencySet, which is why the HUD's allocations are added and committed. The viewport is in pixels, and since that is what the HUD is begun with, HUD coordinates are pixels from the top left too — the text's position is its baseline.

Getting it on screen

The engine owns no window. A window, a view and a responder chain are your app's decisions, so main.swift makes them, exactly as the sample's does:

import AppKit
import MetalKit
import WindageEngine

let application = NSApplication.shared
application.setActivationPolicy(.regular)

guard let device = MTLCreateSystemDefaultDevice() else { exit(1) }

let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 1280, height: 800),
                      styleMask: [.titled, .closable, .resizable, .miniaturizable],
                      backing: .buffered, defer: false)
window.title = class="p">"My Game"
window.center()

let engine: FrameRenderer
do {
    engine = try FrameRenderer(device: device)
} catch {
    print(error)   // FrameRendererError: no Metal 4, or something a frame needs
    exit(1)
}

let view = MTKView(frame: window.contentLayoutRect, device: device)
view.colorPixelFormat = FrameRenderer.colorPixelFormat
view.depthStencilPixelFormat = FrameRenderer.depthPixelFormat
view.isPaused = true
view.enableSetNeedsDisplay = false
window.contentView = view

guard let game = try? Game(engine: engine) else { exit(1) }
engine.game = game
engine.clearColor = MTLClearColor(red: 0.42, green: 0.55, blue: 0.70, alpha: 1)

guard let metalLayer = view.layer as? CAMetalLayer else { exit(1) }
let displayLink = engine.makeDisplayLink(for: metalLayer)
displayLink.add(to: .main, forMode: .common)

window.makeKeyAndOrderFront(nil)
application.activate(ignoringOtherApps: true)
application.run()

Then:

swift run MyGame

What to watch for

Input

Keys reach the game through the view. The sample subclasses MTKView, overrides keyDown, keyUp, flagsChanged (Shift, Control, Option and Command arrive only there) and the mouse events, and hands them to an InputState, started once with start(). Poll it in beginFrame. See input.

Where to go next