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: