A declarative toolkit for building SwiftGodot games, companion to SwiftGodotPatterns.
Build Godot node trees with a SwiftUI-like syntax. Chain configuration calls, resource loaders, signal handlers and more in a familiar syntax.
Simple games should be simple to make.
๐ API Documentation
Instead of:
let label = Label()
label.text = "Hello, World"
let canvas = CanvasLayer()
canvas.addChild(node: label)You can write:
CanvasLayer$ {
Label$().text("Hello, World")
}The SwiftGodotBuilderExample repository contains a project with various samples.
Includes Pong, Breakout, Space Invaders, and many more.
Use the SwiftGodotHelper extension for environment checks, build tasks, and more.
Views describe your nodes, like a
.tscnfile, but using code.
All subclasses of Node can be suffixed with $ to build views.
let view = Node2D$ {
Sprite2D$()
.res(\.texture, "ball.png")
.position(x: 100, y: 200)
Button$()
.text("Start")
.onSignal(\.pressed) { GD.print("Game Start!") }
}
// Create the Godot node we described
let node = view.toNode()All settable properties of nodes can be used as chainable modifiers.
Node2D$()
.position(Vector2(x: 20, y: 20))
.scale(Vector2(x: 0.5, y: 0.5))
.rotation(0.25)All Godot signals can be listened for with .on
Button$()
.text("Toggle Sound")
.onSignal(\.toggled) { node, isOn in
GD.print("Sound is now", isOn ? "ON" : "OFF")
}All resource types can be loaded with .res
Sprite2D$().res(\.texture, "art/player.png")
AudioStreamPlayer2D$().res(\.stream, "audio/laser.ogg")
// Conditionally assign when a path might be nil
Sprite2D$().resIf(\.texture, maybeTexturePath)
// Load any Resource, then mutate the node
Node2D$().withResource("shaders/tint.tres", as: Shader.self) { node, shader in
let mat = ShaderMaterial()
mat.shader = shader
(node as? Sprite2D)?.material = mat
}Aseprite support is included. Just add an exported sprite sheet + JSON to your project.
AseSprite$(path: "player.json")
// Shorthand: omit .json
AseSprite$("MyDino", path: "DinoSprites", layer: "MORT", autoplay: "move")AseSpriteis a subclass ofAnimatedSprite2D- Enable the "Split Layers" option when exporting a file with multiple layers.
Node2D$().onReady { node in }
Node2D$().onProcess { node, delta in }
Node2D$().onPhysicsProcess { node, delta in }// Named layer enums + node helper
let wall = GNode<StaticBody2D>()
.collisionLayer(.alpha) // sets collisionLayer bits
.collisionMask([.beta,.gamma]) // sets collisionMask bits// Time-based and/or offscreen despawn
Node2D$("Bullet") {
Sprite2D$().res(\.texture, "bullet.png")
}
.autoDespawn(seconds: 4, whenOffscreen: true, offscreenDelay: 0.1)
// Pool-friendly variant
let pool = ObjectPool<Node2D>(factory: { Node2D() })
Node2D$("Enemy").autoDespawnToPool(pool, whenOffscreen: true)Any custom subclass of Node can be used as a view.
@Godot
class Paddle: Area2D { }
GNode<Paddle> { }- A
Paddle()will be created whentoNode()is called.
@Godot
class Paddle: Area2D {
var side = "left"
convenience init(side: Side) {
self.init()
self.side = side
}
override func _process(delta: Double) {
if side == "left" { /* ... */ }
}
}Pass a make: { } trailing closure to customize instance:
GNode<Paddle> {
// ...
} make: {
Paddle(side: "right")
}- A
Paddle(side: "right")will be created whentoNode()is called.
Reference Nodes in Views.
let label = Ref<Label>()
VBoxContainer$ {
Label$()
.text("Lives: 0")
.ref(label)
Button$()
.text("โค๏ธ")
.onSignal(\.pressed) { _ in
guard let l = label.node else { return }
l.text = "Lives: 1"
}
}Reference instantiated Views in Nodes.
class Player: Node2D {
let sprite = Slot<Sprite2D>()
override func _ready() {
// sprite.node is a Sprite2D
}
}
let player = GNode<Player>("Player") {
Sprite2D$()
.res(\.texture, "player.png")
.ref(\Player.sprite) // binds to Player.sprite
}-
Use instead of
getChild(NodePath)to keep your nodes & gameplay classes loosely coupled. -
See also: DinoFighter
Instance a PackedScene.
Node2D$()
.instanceScene("scenes/Enemy.tscn") { spawned in
spawned.position = Vector2(x: 128, y: 64)
}Easily add to one or many groups.
Node2D$().group("enemies")
Node2D$().groups(["ui", "interactive"])All standard result-builder patterns work:
Node2D$ {
if debug {
Label$().text("DEBUG")
}
for i in 0..<rows {
HBoxContainer$ {
for j in 0..<cols { Sprite2D$().position(x: j*16, y: i*16) }
}
}
}- This logic is only evaluated when
toNode()is called.
Chain modifiers to set anchors, offsets, sizing, and alignment:
.sizeH(), .sizeV(), .size()
VBoxContainer$ {
Button$().text("Play").sizeH(.expandFill)
Button$().text("Options").size(.shrinkCenter)
Button$().text("Quit").sizeH(.expandFill)
}.anchors(), .offsets(), .anchorsAndOffsets(), .anchor(top:right:bottom:left), .offset(top:right:bottom:left)
CanvasLayer$ {
Label$().text("42 โค๏ธ")
.anchors(.bottomLeft)
.offsets(.bottomLeft, margin: 10)
}alignment()
HBoxContainer$ {
["๐ก๏ธ", "๐ก๏ธ", "๐ฃ", "๐งช", "๐ช"]
.map { Button$().text($0) }
}
.anchors(.topWide)
.offset(top: 10, right: -10)
.alignment(.end)- See also: HUDView
Use declarative code to succinctly describe your input scheme.
let inputs = Actions {
Action("jump") {
Key(.space)
JoyButton(.a, device: 0)
}
// Axis helpers (paired actions)
ActionRecipes.axisLR(
namePrefix: "aim",
device: 0,
axis: .leftX,
dz: 0.25,
btnLeft: .dpadLeft,
btnRight: .dpadRight
)
}
inputs.install()Q: Is this "SwiftUI for Godot"?
No. There's no @State/@Binding. There is no runtime behavior at all, beyond normal Godot events.
Q: Does my whole game need to be in SwiftGodotBuilder?
No, you can add it to an existing SwiftGodot project. Anywhere you would write addChild(node), you can instead write addChild(view.toNode).
Q: Does this hurt runtime performance?
No. toNode() is just syntax sugar around addChild. You control when are where to call this, just like a traditional SwiftGodot game.
Q: So views aren't nodes?
No, views are representations of nodes, like a .tscn file. They aren't actually nodes until you call .toNode(). And they aren't part of the scene until you insert them with e.g. addChild().
Q: Why do I have to write
GNode<MyClass> {}instead ofMyClass$ {}?
Node2D$ is just a typealias for GNode<Node2D>, so you can do typealias MyClass$ = GNode<MyClass> to use that syntax.
- error: cannot convert value of type 'KeyPath<YourClass, Ref<NodeType>>' to expected argument type 'Ref<NodeType>'
Chain your .ref() call after setting properties:
// Bad: causes error
Control$ {}
.ref(\Overlay.panel)
.anchors(.topLeft)
// Good: ref after properties
Control$ {}
.anchors(.topLeft)
.ref(\Overlay.panel)- Add library linking helper to VSCode plugin
- Export to .escn feature
- Additional samples of increasing complexity
