Contents

johnsusek/swiftgodotpatterns

### SwiftGodotPatterns

Quick Start

import SwiftGodot
import SwiftGodotPatterns

@Godot
final class Game: Node2D {
  override func _ready() {
    addChild(node: GameView().toNode())
  }
}

struct GameView: GView {
  var body: some GView {
    Node2D$ {
      Label$().text("Hello World")
    }
  }
}

Builder Syntax

// $ syntax - shorthand for GNode<T>
Sprite2D$()
CharacterBody2D$()
Label$()

// With children
Node2D$ {
  Sprite2D$()
  CollisionShape2D$()
}

// Named nodes
CharacterBody2D$("Player") {
  Sprite2D$()
}

// Custom initializer
GNode<CustomNode>("Name", make: { CustomNode(config: config) }) {
  // children
}

Properties & Configuration

// Dynamic member lookup - set any property
Sprite2D$()
  .position(Vector2(100, 200))
  .scale(Vector2(2, 2))
  .rotation(45)
  .modulate(.red)
  .zIndex(10)

// Configure closure for complex setup
Sprite2D$().configure { sprite in
  sprite.texture = myTexture
  sprite.centered = true
}

Resource Loading

// Load into property
Sprite2D$()
  .res(\.texture, "player.png")
  .res(\.material, "shader_material.tres")

// Custom resource loading
Sprite2D$()
  .withResource("shader.gdshader", as: Shader.self) { node, shader in
    let material = ShaderMaterial()
    material.shader = shader
    node.material = material
  }

State Management

struct PlayerView: GView {
  @State var health: Int = 100
  @State var position: Vector2 = .zero
  @State var playerNode: CharacterBody2D?

  var body: some GView {
    CharacterBody2D$ {
      Sprite2D$()
      ProgressBar$()
        .value($health)  // One-way binding
    }
    .position($position)  // Bind to property
    .ref($playerNode)     // Capture node reference
    .onProcess { node, delta in
      health -= 1  // Modify state
    }
  }
}

State Binding Patterns

// One-way bind to property
ProgressBar$().value($health)

// Bind with formatter
Label$().bind(\.text, to: $score) { "Score: \($0)" }

// Bind to sub-property
Sprite2D$().bind(\.x, to: $position, \.x)

// Multi-state binding
Label$().bind(\.text, to: $health, $maxHealth) { "\($0)/\($1)" }

// Watch state changes
Node2D$().watch($health) { node, health in
  node.modulate = health < 20 ? .red : .white
}

// Two-way bindings (form controls)
LineEdit$().text($username)
Slider$().value($volume)
CheckBox$().pressed($isEnabled)
OptionButton$().selected($selectedIndex)

Signal Connections

// No arguments
Button$()
  .onSignal(\.pressed) { node in
    print("Pressed!")
  }

// With arguments
Area2D$()
  .onSignal(\.bodyEntered) { node, body in
    print("Body entered: \(body)")
  }

// Multiple arguments
Area2D$()
  .onSignal(\.bodyShapeEntered) { node, bodyRid, body, bodyShapeIndex, localShapeIndex in
    // Handle collision
  }

Process Hooks

Node2D$()
  .onReady { node in
    print("Node ready!")
  }
  .onProcess { node, delta in
    node.position.x += 100 * Float(delta)
  }
  .onPhysicsProcess { node, delta in
    // Physics updates
  }

Dynamic Views

// ForEach - dynamic lists
struct InventoryView: GView {
  @State var items: [Item] = []

  var body: some GView {
    VBoxContainer$ {
      ForEach($items) { item in
        HBoxContainer$ {
          Label$().text(item.wrappedValue.name)
          Button$().text("X").onSignal(\.pressed) { _ in
            items.removeAll { $0.id == item.wrappedValue.id }
          }
        }
      }
    }
  }
}

// If - conditional rendering
struct MenuView: GView {
  @State var showSettings = false

  var body: some GView {
    VBoxContainer$ {
      If($showSettings) {
        SettingsPanel()
      }
      .Else {
        MainMenu()
      }
    }
  }
}

// If modes
If($condition) { /* ... */ }                 // .hide (default) - toggle visible
If($condition) { /* ... */ }.mode(.remove)   // addChild/removeChild
If($condition) { /* ... */ }.mode(.destroy)  // queueFree/rebuild

// Switch/Case - multi-way branching
enum Page { case mainMenu, levelSelect, settings }

struct GameView: GView {
  @State var currentPage: Page = .mainMenu

  var body: some GView {
    VBoxContainer$ {
      Switch($currentPage) {
        Case(.mainMenu) {
          Label$().text("Main Menu")
          Button$().text("Start").onSignal(\.pressed) { _ in
            currentPage = .levelSelect
          }
        }
        Case(.levelSelect) {
          Label$().text("Level Select")
          Button$().text("Back").onSignal(\.pressed) { _ in
            currentPage = .mainMenu
          }
        }
        Case(.settings) {
          Label$().text("Settings")
        }
      }
      .default {
        Label$().text("Unknown page")
      }
    }
  }
}

// Computed state - derive new reactive states
@State var score = 0
let scoreText = $score.computed { "Score: \($0)" }
let isHighScore = $score.computed { $0 > 1000 }

Label$().text(scoreText)

If(isHighScore) {
  Label$().text("New High Score!").modulate(.yellow)
}

// Combine multiple states
@State var currentPage = 1
@State var totalPages = 10
let pageText = $currentPage.computed(with: $totalPages) { current, total in
  "Page \(current) of \(total)"
}

@State var health = 80
@State var maxHealth = 100
@State var playerName = "Hero"
let statusText = $health.computed(with: $maxHealth, $playerName) { hp, maxHp, name in
  "\(name): \(hp)/\(maxHp) HP"
}

Label$().text(statusText)

Groups & Scene Instancing

Node2D$()
  .group("enemies")
  .group("damageable", persistent: true)
  .groups(["enemies", "damageable"])

Node2D$()
  .fromScene("enemy.tscn") { child in
    // Configure instanced scene
  }

Control Layout

// Anchor/offset presets (non-container parents)
Control$()
  .anchors(.center)
  .offsets(.topRight)
  .anchorsAndOffsets(.fullRect, margin: 10)
  .anchor(top: 0, right: 1, bottom: 1, left: 0)
  .offset(top: 12, right: -12, bottom: -12, left: 12)

// Container size flags (for VBox/HBox parents)
Button$()
  .sizeH(.expandFill)
  .sizeV(.shrinkCenter)
  .size(.expandFill, .shrinkCenter)
  .size(.expandFill)  // Both axes

Collision (2D)

CharacterBody2D$()
  .collisionLayer(.alpha)
  .collisionMask([.beta, .gamma])

// Available layers: .alpha, .beta, .gamma, .delta, .epsilon, .zeta, .eta, .theta,
// .iota, .kappa, .lambda, .mu, .nu, .xi, .omicron, .pi, .rho, .sigma, .tau,
// .upsilon, .phi, .chi, .psi, .omega

// Custom layers
CharacterBody2D$()
  .collisionMask(wallLayer | enemyLayer)

Shape Helpers

CollisionShape2D$().shape(RectangleShape2D(w: 50, h: 100))
CollisionShape2D$().shape(CircleShape2D(radius: 25))
CollisionShape2D$().shape(CapsuleShape2D(radius: 10, height: 50))
CollisionShape2D$().shape(SegmentShape2D(a: [0, 0], b: [100, 100]))
CollisionShape2D$().shape(SeparationRayShape2D(length: 100))
CollisionShape2D$().shape(WorldBoundaryShape2D(normal: [0, -1], distance: 0))

EventBus

enum GameEvent {
  case playerDied
  case scoreChanged(Int)
  case itemCollected(String)
}

// Subscribe via modifier
Node2D$()
  .onEvent(GameEvent.self) { node, event in
    switch event {
    case .playerDied: print("Game Over")
    case .scoreChanged(let score): print("Score: \(score)")
    case .itemCollected(let item): print("Got: \(item)")
    }
  }

// Subscribe with filter
Node2D$()
  .onEvent(GameEvent.self, match: { event in
    if case .scoreChanged = event { return true }
    return false
  }) { node, event in
    // Handle only score changes
  }

// Publish via ServiceLocator
let bus = ServiceLocator.resolve(GameEvent.self)
bus.publish(.scoreChanged(100))

// Or use EmittableEvent protocol
enum GameEvent: EmittableEvent {
  case playerDied
}
GameEvent.playerDied.emit()

Store (Uni-directional State)

struct GameState {
  var health: Int = 100
  var score: Int = 0
}

enum GameEvent {
  case takeDamage(Int)
  case addScore(Int)
}

func gameReducer(state: inout GameState, event: GameEvent) {
  switch event {
  case .takeDamage(let amount):
    state.health = max(0, state.health - amount)
  case .addScore(let points):
    state.score += points
  }
}

let store = Store(initialState: GameState(), reducer: gameReducer)

// Use in views
ProgressBar$().value(store.bind(\.health))
Label$().text(store.bind(\.score)) { "Score: \($0)" }

// Send events
store.commit(.takeDamage(10))
store.commit(.addScore(100))

Input Actions

// Define actions
Actions {
  Action("jump") {
    Key(.space)
    JoyButton(.a, device: 0)
  }

  Action("shoot") {
    MouseButton(1)
    Key(.leftCtrl)
  }

  // Analog axes
  ActionRecipes.axisUD(
    namePrefix: "move",
    device: 0,
    axis: .leftY,
    dz: 0.2,
    keyDown: .s,
    keyUp: .w
  )

  ActionRecipes.axisLR(
    namePrefix: "move",
    device: 0,
    axis: .leftX,
    dz: 0.2,
    keyLeft: .a,
    keyRight: .d
  )
}
.install(clearExisting: true)

// Runtime polling
if Action("jump").isJustPressed {
  player.jump()
}

if Action("shoot").isPressed {
  player.shoot(Action("shoot").strength)
}

let horizontal = RuntimeAction.axis(negative: "move_left", positive: "move_right")
let movement = RuntimeAction.vector(
  negativeX: "move_left",
  positiveX: "move_right",
  negativeY: "move_up",
  positiveY: "move_down"
)

Property Wrappers

Call bindProps() in _ready() to activate all property wrappers.

@Godot
final class Player: CharacterBody2D {
  @Child("Sprite") var sprite: Sprite2D?
  @Child("Health", deep: true) var healthBar: ProgressBar?
  @Children var buttons: [Button]
  @Ancestor var level: Level?
  @Sibling("AudioPlayer") var audio: AudioStreamPlayer?
  @Autoload("GameManager") var gameManager: GameManager?
  @Group("enemies") var enemies: [Enemy]
  @Service var events: EventBus<GameEvent>?
  @Prefs("musicVolume", default: 0.5) var volume: Double

  @OnSignal("StartButton", \Button.pressed)
  func onStartPressed(_ sender: Button) {
    print("Started!")
  }

  override func _ready() {
    bindProps()

    sprite?.visible = true
    enemies.forEach { print($0) }

    // Refresh group query
    let currentEnemies = $enemies()
  }
}

Theme Building

let theme = Theme([
  "Button": [
    "colors": ["fontColor": Color.white],
    "constants": ["outlineSize": 2],
    "fontSizes": ["fontSize": 16]
  ],
  "Label": [
    "colors": ["fontColor": Color.white],
    "fontSizes": ["fontSize": 14]
  ]
])

Control$().theme(theme)

Vector2 Extensions

let pos = Vector2(100, 200)
let pos: Vector2 = [100, 200]  // Array literal
let doubled = pos * 2
let scaled = pos * 1.5

Node Extensions

// Typed queries
let sprites: [Sprite2D] = node.getChildren()
let firstSprite: Sprite2D? = node.getChild()
let enemySprite: Sprite2D? = node.getNode("Enemy")

// Group queries
let enemies: [Enemy] = node.getNodes(inGroup: "enemies")

// Parent chain
let parents: [Node2D] = node.getParents()

// Metadata queries (recursive)
let spawns: [Node2D] = root.queryMeta(key: "type", value: "spawn")
let valuable: [Node2D] = root.queryMeta(key: "value", value: 100)
let markers: [Node2D] = root.queryMetaKey("marker")

// Get typed metadata
let coinValue: Int? = node.getMetaValue("coin_value")

Engine Extensions

if let tree = Engine.getSceneTree() {
  // ...
}

Engine.onNextFrame {
  print("Next frame!")
}

Engine.onNextPhysicsFrame {
  print("Next physics frame!")
}

LDtk Integration

Complete workflow for loading LDtk levels.

// Define type-safe enums (auto-generates LDExported.json on build)
enum Item: String, LDExported {
  case knife = "Knife"
  case boots = "Boots"
  case potion = "Potion"
}

enum EnemyType: String, LDExported {
  case goblin = "Goblin"
  case skeleton = "Skeleton"
}

struct GameView: GView {
  let project: LDProject
  @State var inventory: [Item] = []
  @State var health: Int = 100

  var body: some GView {
    Node2D$ {
      LDLevelView(project, level: "Level_0")
        .onSpawn("Player") { entity, level, project in
          let wallLayer = project.collisionLayer(for: "walls", in: level)
          let startItems: [Item] = entity.field("starting_items")?.asEnumArray() ?? []
          inventory.append(contentsOf: startItems)

          CharacterBody2D$ {
            Sprite2D$()
              .res(\.texture, "player.png")
              .anchor([16, 22], within: entity.size, pivot: entity.pivotVector)
            CollisionShape2D$()
              .shape(RectangleShape2D(w: 16, h: 22))
          }
          .position(entity.position)
          .collisionMask(wallLayer)
        }

        .onSpawn("Enemy") { entity, level, project in
          let enemyType: EnemyType? = entity.field("type")?.asEnum()
          let patrolPath: [Vector2] = entity.field("patrol")?.asVector2Array() ?? []
          let enemyHealth: Int = entity.field("health")?.asInt() ?? 10

          Area2D$ {
            Sprite2D$()
              .res(\.texture, "enemy_\(enemyType?.rawValue ?? "default").png")
              .anchor([12, 16], within: entity.size)
            CollisionShape2D$()
              .shape(RectangleShape2D(w: 12, h: 16))
          }
          .position(entity.position)
        }

        .onSpawn("Chest") { entity, level, project in
          let loot: [Item] = entity.field("loot")?.asEnumArray() ?? []
          let locked: Bool = entity.field("locked")?.asBool() ?? false

          Area2D$ {
            Sprite2D$().res(\.texture, locked ? "chest_locked.png" : "chest.png")
          }
          .position(entity.position)
          .onSignal(\.bodyEntered) { _, body in
            inventory.append(contentsOf: loot)
          }
        }

        .onSpawn("Door") { entity, level, project in
          let destination: String? = entity.field("destination")?.asString()
          let keyRequired: Item? = entity.field("key_required")?.asEnum()

          Area2D$()
            .position(entity.position)
        }

        .onSpawned { node, entity in
          // Post-process all entities
          if let debugMode = entity.field("debug")?.asBool(), debugMode {
            node.addChild(node: Label$().text(entity.identifier).toNode())
          }
        }
        .zIndexOffset(100)
        .createEntityMarkers()

      // HUD
      CanvasLayer$ {
        VBoxContainer$ {
          Label$()
            .bind(\.text, to: $health) { "Health: \($0)" }
          Label$()
            .bind(\.text, to: $inventory) { items in
              "Items: \(items.map(\.rawValue).joined(separator: ", "))"
            }
        }
        .offset(top: 10, left: 10)
      }
    }
  }
}

// Usage
let project = LDProject.load("res://game.ldtk")!
addChild(node: GameView(project: project).toNode())

LDtk Field Accessors

All LDtk field types are supported:

// Single values
entity.field("health")?.asInt() -> Int?
entity.field("speed")?.asFloat() -> Double?
entity.field("locked")?.asBool() -> Bool?
entity.field("name")?.asString() -> String?
entity.field("tint")?.asColor() -> Color?
entity.field("destination")?.asPoint() -> LDPoint?
entity.field("spawn_pos")?.asVector2(gridSize: 16) -> Vector2?
entity.field("target")?.asEntityRef() -> LDEntityRef?
entity.field("item_type")?.asEnum<Item>() -> Item?

// Arrays
entity.field("scores")?.asIntArray() -> [Int]?
entity.field("distances")?.asFloatArray() -> [Double]?
entity.field("flags")?.asBoolArray() -> [Bool]?
entity.field("tags")?.asStringArray() -> [String]?
entity.field("waypoints")?.asPointArray() -> [LDPoint]?
entity.field("patrol")?.asVector2Array(gridSize: 16) -> [Vector2]?
entity.field("palette")?.asColorArray() -> [Color]?
entity.field("targets")?.asEntityRefArray() -> [LDEntityRef]?
entity.field("loot")?.asEnumArray<Item>() -> [Item]?
entity.field("values")?.asArray() -> [LDFieldValue]?  // Raw array

LDtk Collision Helper

// Get physics layer bit for IntGrid group name
let wallLayer = project.collisionLayer(for: "walls", in: level)
let platformLayer = project.collisionLayer(for: "platforms", in: level)

CharacterBody2D$()
  .collisionMask(wallLayer | platformLayer)

AseSprite

// Load Aseprite animations
let sprite = AseSprite(
  "character.json",
  layer: "Body",
  options: .init(
    timing: .delaysGCD,
    trimming: .applyPivotOrCenter
  ),
  autoplay: "Idle"
)

// Builder pattern
AseSprite$(path: "player", layer: "Main")
  .configure { sprite in
    sprite.play(anim: "Walk")
  }

Complete Game Example

import SwiftGodot
import SwiftGodotPatterns

@Godot
final class Game: Node2D {
  override func _ready() {
    setupInput()
    let project = LDProject.load("res://game.ldtk")!
    addChild(node: GameView(project: project).toNode())
  }

  func setupInput() {
    Actions {
      Action("move_left") { Key(.a); Key(.left) }
      Action("move_right") { Key(.d); Key(.right) }
      Action("jump") { Key(.space); Key(.w) }
      Action("shoot") { MouseButton(1) }
    }.install()
  }
}

enum Item: String, LDExported {
  case coin = "Coin"
  case key = "Key"
  case potion = "Potion"
}

enum GameEvent: EmittableEvent {
  case itemCollected(Item)
  case enemyKilled
  case playerDied
}

struct GameView: GView {
  let project: LDProject
  @State var inventory: [Item] = []
  @State var health: Int = 100
  @State var score: Int = 0

  var body: some GView {
    Node2D$ {
      LDLevelView(project, level: "Main")
        .onSpawn("Player") { entity, level, project in
          PlayerView(
            startPos: entity.position,
            wallLayer: project.collisionLayer(for: "walls", in: level)
          )
        }
        .onSpawn("Enemy") { entity, level, project in
          EnemyView(
            startPos: entity.position,
            enemyType: entity.field("type")?.asEnum() ?? .goblin
          )
        }
        .onSpawn("Collectible") { entity, level, project in
          let item: Item? = entity.field("item")?.asEnum()

          Area2D$ {
            Sprite2D$().res(\.texture, "item_\(item?.rawValue ?? "unknown").png")
          }
          .position(entity.position)
          .onSignal(\.bodyEntered) { node, _ in
            if let item = item {
              GameEvent.itemCollected(item).emit()
              node.queueFree()
            }
          }
        }

      HUDView(inventory: $inventory, health: $health, score: $score)
    }
    .onEvent(GameEvent.self) { _, event in
      switch event {
      case .itemCollected(let item):
        inventory.append(item)
        score += 10
      case .enemyKilled:
        score += 100
      case .playerDied:
        health = 0
      }
    }
  }
}

struct PlayerView: GView {
  let startPos: Vector2
  let wallLayer: UInt32
  @State var position: Vector2
  @State var velocity: Vector2 = .zero
  @State var player: CharacterBody2D?

  let gravity: Float = 980
  let speed: Float = 200
  let jumpSpeed: Float = 300

  init(startPos: Vector2, wallLayer: UInt32) {
    self.startPos = startPos
    self.wallLayer = wallLayer
    self._position = State(initialValue: startPos)
  }

  var body: some GView {
    CharacterBody2D$ {
      Sprite2D$().res(\.texture, "player.png")
      CollisionShape2D$().shape(RectangleShape2D(w: 16, h: 22))
    }
    .position($position)
    .velocity($velocity)
    .collisionMask(wallLayer)
    .ref($player)
    .onProcess { _, delta in
      updatePlayer(delta)
    }
  }

  func updatePlayer(_ delta: Double) {
    guard let player = player else { return }

    var vel = velocity
    vel.y += gravity * Float(delta)

    var inputX: Float = 0
    if Action("move_left").isPressed { inputX -= 1 }
    if Action("move_right").isPressed { inputX += 1 }
    vel.x = inputX * speed

    if Action("jump").isJustPressed && player.isOnFloor() {
      vel.y = -jumpSpeed
    }

    player.velocity = vel
    player.moveAndSlide()

    velocity = player.velocity
    position = player.position
  }
}

struct EnemyView: GView {
  let startPos: Vector2
  let enemyType: EnemyType
  @State var position: Vector2
  @State var health: Int = 10

  init(startPos: Vector2, enemyType: EnemyType) {
    self.startPos = startPos
    self.enemyType = enemyType
    self._position = State(initialValue: startPos)
  }

  var body: some GView {
    Area2D$ {
      Sprite2D$().res(\.texture, "enemy_\(enemyType.rawValue).png")
      CollisionShape2D$().shape(CircleShape2D(radius: 8))
    }
    .position($position)
    .onSignal(\.bodyEntered) { node, _ in
      health -= 10
      if health <= 0 {
        GameEvent.enemyKilled.emit()
        node.queueFree()
      }
    }
  }
}

enum EnemyType: String, LDExported {
  case goblin = "Goblin"
  case skeleton = "Skeleton"
}

struct HUDView: GView {
  let inventory: State<[Item]>
  let health: State<Int>
  let score: State<Int>

  var body: some GView {
    CanvasLayer$ {
      VBoxContainer$ {
        Label$()
          .bind(\.text, to: health) { "Health: \(String(repeating: "♥", count: max(0, $0)))" }
        Label$()
          .bind(\.text, to: score) { "Score: \($0)" }
        Label$()
          .bind(\.text, to: inventory) { items in
            "Inventory: \(items.map(\.rawValue).joined(separator: ", "))"
          }
      }
      .offset(top: 10, left: 10)
    }
  }
}

Common Patterns

Character Controller

struct PlayerController: GView {
  @State var position: Vector2 = .zero
  @State var velocity: Vector2 = .zero
  @State var player: CharacterBody2D?

  let gravity: Float = 980
  let speed: Float = 200
  let jumpSpeed: Float = 300

  var body: some GView {
    CharacterBody2D$ {
      Sprite2D$().res(\.texture, "player.png")
      CollisionShape2D$().shape(RectangleShape2D(w: 16, h: 22))
    }
    .position($position)
    .velocity($velocity)
    .ref($player)
    .onProcess { _, delta in
      guard let player = player else { return }

      var vel = velocity
      vel.y += gravity * Float(delta)

      let input = RuntimeAction.axis(negative: "move_left", positive: "move_right")
      vel.x = input * speed

      if Action("jump").isJustPressed && player.isOnFloor() {
        vel.y = -jumpSpeed
      }

      player.velocity = vel
      player.moveAndSlide()

      velocity = player.velocity
      position = player.position
    }
  }
}

Interactive Object

struct Chest: GView {
  let position: Vector2
  let loot: [Item]
  @State var isOpen = false

  var body: some GView {
    Area2D$ {
      If($isOpen) {
        Sprite2D$().res(\.texture, "chest_open.png")
      }
      .Else {
        Sprite2D$().res(\.texture, "chest_closed.png")
      }
      CollisionShape2D$().shape(RectangleShape2D(w: 16, h: 16))
    }
    .position(position)
    .onSignal(\.bodyEntered) { _, body in
      guard !isOpen else { return }
      isOpen = true
      GameEvent.lootCollected(loot).emit()
    }
  }
}

Health Bar

struct HealthBar: GView {
  let health: State<Int>
  let maxHealth: Int

  var body: some GView {
    ProgressBar$()
      .maxValue(Double(maxHealth))
      // Formatter used for type conversion
      .bind(\.value, to: health) { Double($0) }
      .size(.expandFill)
  }
}

Menu System

enum MenuPage {
  case mainMenu
  case levelSelect
  case settings
}

struct MainMenu: GView {
  @State var currentPage: MenuPage = .mainMenu

  var body: some GView {
    CanvasLayer$ {
      VBoxContainer$ {
        Label$().text("My Game")

        Switch($currentPage) {
          Case(.mainMenu) {
            Button$().text("Start").onSignal(\.pressed) { _ in
              currentPage = .levelSelect
            }
            Button$().text("Settings").onSignal(\.pressed) { _ in
              currentPage = .settings
            }
            Button$().text("Quit").onSignal(\.pressed) { _ in
              Engine.getSceneTree()?.quit()
            }
          }

          Case(.levelSelect) {
            Label$().text("Level Select")
            Button$().text("Back").onSignal(\.pressed) { _ in
              currentPage = .mainMenu
            }
          }

          Case(.settings) {
            Label$().text("Settings")
            Button$().text("Back").onSignal(\.pressed) { _ in
              currentPage = .mainMenu
            }
          }
        }
      }
      .anchorsAndOffsets(.center)
    }
  }
}

Inventory System

struct InventoryView: GView {
  @State var items: [Item] = []

  var body: some GView {
    VBoxContainer$ {
      Label$().text("Inventory")

      ForEach($items, id: \.rawValue) { item in
        HBoxContainer$ {
          TextureRect$().res(\.texture, "icon_\(item.wrappedValue.rawValue).png")
          Label$().text(item.wrappedValue.rawValue)
          Button$().text("Drop").onSignal(\.pressed) { _ in
            items.removeAll { $0 == item.wrappedValue }
          }
        }
      }
    }
  }
}

Timer/Countdown

struct Countdown: GView {
  @State var timeLeft: Double = 60.0
  @State var isRunning: Bool = true

  var body: some GView {
    Label$()
      .bind(\.text, to: $timeLeft) { String(format: "%.1f", $0) }
      .onProcess { _, delta in
        if isRunning && timeLeft > 0 {
          timeLeft -= delta
        }
      }
  }
}

Package Metadata

Repository: johnsusek/swiftgodotpatterns

Default branch: main

README: README.md