Plumbing and Construction

Domain/3D plumbing, and making 3D models

In no small feat of effort, I've connected the domain layer (Swift) and the active game state (Godot/GDScript) layers to one another.

As I've mentioned before, the code that I've spent the majority of this project working on so far uses my fork of the Swift-Godot extension. Well, as part of this bit of work, I created a an object in my Swift code which is a subclass of Godot Node. By importing this into the scene hierarchy in Godot, it functions as my Swift code's entrance point into Godot When its _ready lifecycle method is called, it traverses the scene tree and dynamically inspects the elements, and uses that data to serialize all of the data models for the domain layer:

    public override func _ready() {
        let boardPointer = getNodeOrNull(path: NodePath(from: NodePathID.boardPointer))
        let hexesNode = getNodeOrNull(path: NodePath(from: NodePathID.authoredHexes))
        let spawnMarkersNode = getNodeOrNull(path: NodePath(from: NodePathID.spawnMarkers))
        guard let boardPointer, let hexesNode, let spawnMarkersNode else {
            GD.pushError("""
            Battle3DSceneController scene incompletely loaded.
            Board: \(boardPointer == nil ? "nil" : "non-nil")
            Hexes: \(hexesNode == nil ? "nil" : "non-nil")
            Spawn Markers: \(spawnMarkersNode == nil ? "nil" : "non-nil")
            """)
            return
        }

        SignalWithArguments<Vector2i>(
            target: boardPointer,
            signalName: "hex_clicked"
        ).connect { [weak self] coordinate in
            self?.handleHexClick(coordinate)
        }

        do {
            let terrainTiles = try GodotBattleScenarioImporter.terrainTiles(
                fromHexesNode: hexesNode
            )
            let spawnMarkers = try GodotBattleScenarioImporter.spawnDefinitions(
                fromSpawnMarkersNode: spawnMarkersNode
            )
            let battleScenario = BattleScenarioDefinition(
                id: .verticalSliceBattle,
                topology: .hexAxial,
                terrain: terrainTiles,
                spawns: spawnMarkers
            )
            let battleState = try engine.battleState(from: battleScenario)
            battle = battleState

            GD.print("""
            Battle state constructed:
                ID: \(battleState.id.rawValue)
                Map: \(battleState.map.width)x\(battleState.map.height), \(battleState.map.topology.rawValue)
                Units: \(battleState.units.map(\.id.rawValue).joined(separator: ", "))
                Turn Order: \(battleState.turnOrder.map(\.rawValue).joined(separator: ", "))
                Phase: \(battleState.phase)
                Deck Count: \(battleState.decks.count)
            """)
        } catch {
            GD.pushError("Unable to initialize game state: \(error)")
        }
    }

Swift-Godot gives me a handful of kind of sorta clunky tools for deserializing data from the scene (the Variant API and its getNamed(key:) functions you see below), which I wrap in layers of helper functions like these:

private extension Node {

    func integerProperty(named name: StringName) -> Int? {
        switch Variant(self).getNamed(key: name) {
        case .success(let variant):
            guard let intVal = Int(variant) else {
                return nil
            }
            return intVal
        case .failure:
            return nil
        }
    }

    func stringProperty(named name: StringName) -> String? {
        switch Variant(self).getNamed(key: name) {
        case .success(let variant):
            guard let stringVal = String(variant) else {
                return nil
            }
            return stringVal
        case .failure:
            return nil
        }
    }

}

Then I can use the simpler functions like that to build out domain model initializers like this:

private extension TileCoordinate {

    init?(from node: Node) {
        guard let qVal = node.integerProperty(named: "q"),
              let rVal = node.integerProperty(named: "r") else {
            return nil
        }
        self.init(q: qVal, r: rVal)
    }

}

and constructors like this:

/// Converts the authored hex nodes under `Hexes` in scene tree into an array of terrain tiles.
static func terrainTiles(fromHexesNode hexesNode: Node) throws -> [BattleMap.TerrainTile] {
    try hexesNode.getChildren().map { node in
        guard let node else {
            throw NodeError.nilNode
        }
        guard let terrainTile = BattleMap.TerrainTile(from: node) else {
            throw NodeError.missingOrInvalidProperty
        }
        return terrainTile
    }
}

So my domain layer can now build out a "mental model" of the game state and then be able to reason about things like game rules. For example, when that hex_clicked signal is sent from Godot and receieved by this object, it calls this method:

private extension Battle3DSceneController {

    func handleHexClick(_ coordinate: Vector2i) {
        guard let battle else {
            GD.pushError("Cannot resolve hex click without a battle state.")
            return
        }
        let tile = TileCoordinate(
            q: Int(coordinate.x),
            r: Int(coordinate.y)
        )
        do {
            let legalDestinations = try engine.legalMoveDestinations(
                for: .player,
                in: battle
            )
            let result = legalDestinations.contains(tile) ? "reachable" : "unreachable"
            GD.print("Swift resolved \(tile) as \(result) for player movement.")
        } catch {
            GD.pushError("Unable to resolve movement legality: \(error)")
        }
    }

}

Which can now call into a fully initialized BattleEngine state machine in the Swift code to determine if the clicked hex can be reached by the player unit. Here it is in action:

3D Models in Blender

Next up, I'm having gippity tutor me in how to use Blender. It's slow-going, but I'm learning a lot. Check out this lil guy I'm making here:

← Back to writing