Exploring Game Development with SwiftUI: Part 2 - Gameplay Management using JSON


In Part 1 of this series, I started to describe my attempt to explore the potential of SwiftUI in building a narrative-style game. My focus was on using SwiftUI's UI style to create a novel gameplay experience.
As I continue in Part 2, my goal is to delve deeper into gameplay management using JSON, further enhancing the modularity and text-based control of our game's progression.
For the last two weeks, I have been working on developing a gameplay system meant to manage the flow of my SwiftUI ‘idle tapping’ game. As discussed in my previous post, one of the goals of this game is to explore ways of abstracting the structure of gameplay programming towards a more text based modular set up similar to how https://www.inklestudios.com/ink/ Inlkle operates.
I have broken the architecture of the game into the following…
Import of Game Data via JSON file structure. This file structure allows for me to have a simpler control of how gameplay and progression conditions within the game flow instead of needing to hardcode the game’s progress within the program’s Swift code.
Game Manager
The game manager houses all of the individual managers used in the game application. It is a global variable (@StateObject in Swift) that is passed into other objects/datea that are created.
The Sequence Manager contains the a sequence of events that are triggered one after the other. For example, the starting scene of the game is the presentation of a Sheet (SwiftUI pop-up) that presents some dialog to the player. The Sequence Manager contains a unique SceneID which in turn contains the dialog, view type, and trigger/completion conditions within the.
All of these managers communicate and provide control via the UI Manager which is responsible for controlling how the data is presented to the user and how input from the user is collected within the SwiftUI application.
UI Manager → SwiftUI View Manager via Protocols
The role of my UI Manager is to act as a bridge between the SwiftUI View classes and the gameplay coding. Any one of my gameplay managers can connect to the one of the functions within the UI Manager using this protocol access point. These Protocols are very similar to simply referencing the UI Manager within the various Gameplay managers, but without being so tightly coupled.
Exploring Optionals in Swift
One thing that is becoming apparent to me while using Swift is the heavy use of ‘optionals’. I think that these have something to do with the ‘safety’ that I keep hearing surrounding Swift.
Data structures for my gameplay JSON imports are set up in the following way…
enum SceneDefinition:
{
case sceneID: String
case dialogID: String?
case audioID: String?
case presentationTimer: Float32?
case etc....
}
The question mark notation ? indicates that there doesn’t need to be any value that is actually a part of this data structure when it is populated. In the case of this enum, I may have several different scene types where some will have a dialogID, audioID, or any other value, but is not going to be for all scene types.
How to load JSON into a Local Variable
This is a pretty straightforward way of doing this. I have some struct/data type that I want to populate from a JSON formatted element. Load the JSON, create the struct, and populate an array of them so they can be accessed later. Here is that code. The loadSequenceDefinitions of the JSON may be of some help to someone out there trying to read a JSON into Swift so here it is:
typealias SequenceCollection = [String: SequenceDefinition]
// this is used to look up the collection of
// different sequencies using the ID (string)
private var sequenceDefinitions: SequenceCollection = [:]
// Here, we are showing where the sequences are being stored.
struct SequenceStep: Codable { // this is the structure of the scene
// it has a type definition
enum StepType: String, Codable {
case startScene
case assignQuest
}
// a type (notice the non-optional because all sequences must have a type)
let stepType: StepType
let sceneId: String? // an id for the scene it is going to be (if it is)
let questId: String? // an id for the quest it is going to be (if it is)
}
struct SequenceDefinition: Codable, Identifiable {
var id: String? = ""
let steps: [SequenceStep]
}
// so a squences is a series of steps with some name to it.
// here is the verobse way that i am calling the JSON file and storing it as the
// sequence collection. Fun stuff... this is the kind of thing i think ChatGPT (or
// whatever llm) is especially useful for.
// I also think this highlights why i prefer C/C++ over most languages
// I could make this in C++ in a very clear, almost pseudocode-esque structure
// But Look at this mess below, wth is a Bundle.main? I didn't make that, okay...
// You want a withExtension: nil ....? sure why the hell not.
// try? of course i want you to try....why not just say 'if'. Try what dude?!
// C is just so clear and direct it's like reading a book.
// Swift sometimes feels like reading a wiring diagram for the spaceshuttle.
private func loadSequenceDefinitions(from fileName: String) -> SequenceCollection {
guard let url = Bundle.main.url(forResource: fileName, withExtension: nil) else {
print("❌ SequenceManager Error: not finding the file \(fileName). empty.")
return [:]
}
guard let data = try? Data(contentsOf: url) else {
print("❌ SequenceManager Error: can't load data for \(fileName) from url: \(url). empty")
return [:]
}
let decoder = JSONDecoder()
do { let loadedSequences = try decoder.decode(SequenceCollection.self, from: data)
print("✅ SequenceManager: Successfully loaded \(loadedSequences.count) sequence definitions from \(fileName).")
return loadedSequences
} catch {
print("❌ SequenceManager Error: Could not decode \(fileName) into SequenceCollection data structure.")
if let decodingError = error as? DecodingError {
print("Decoding Error: \(decodingError)")
} else {
print("Error: \(error)")
}
return [:]
}
}
// Anyway it works so whatever!
Controlling Gameplay via Quests & Progression Managers
Here is a bit of my code that shows how I am structuring the ‘sequence/progression’ manager.
// SEQUENCES.json
{
"intro_sequence": {
"steps": [
{ "id": "1", "stepType": "startScene", "sceneId": "scene_show_main_sheet" },
{ "id": "2", "stepType": "startScene", "sceneId": "scene_showMyDialog1" },
{ "id": "3", "stepType": "startScene", "sceneId": "scene_showMyDialog2" },
{ "id": "4", "stepType": "startScene", "sceneId": "scene_dismiss_main_sheet" },
{ "id": "6", "stepType": "setGameState", "stateId": "init_game_state"},
{ "id": "5", "stepType": "assignQuest", "questID": "init_quest"}
]
}
}
As you can see, the sequences manager controls various elements within the game’s presentation and flow structure. startScene controls some scene elements of the game (dialog presentation, swiftUI view controls, etc.) but also can control other game elements such as gameState triggers/flags/conditions etc. The goal here is creating a less ‘code heavy’ modular setup for controlling gameplay flow.
Each of the sequence steps contains the id value of the related scene, quest, gamestate which is then controlled by the related manager. The “scene_show_main_sheet” from the sequence is shown below in more detail with what exactly is being controlled.
// SCENES.json
{
"scene_show_main_sheet": {
"onStartActions": [
{ "id": "action1", "actionType": "showSheet", "targetID": "main_sheet", "userDismissable": false }
],
"completionCondition": {
"conditionType": "duration",
"value": 0.01
}
},
"scene_showMyDialog1": {
"onStartActions": [
{ "id": "dialog1", "actionType": "showDialog", "targetID": "intro_player_q1" }
],
"completionCondition": {
"conditionType": "dialogEnd"
}
},
"scene_dismiss_main_sheet": {
"onStartActions": [
{ "id": "action2", "actionType": "hideSheet", "targetID": "main_sheet" }
],
"completionCondition": {
"conditionType": "duration",
"value": 0.01
}
}
}
I’m not crazy about this flow but it works for now. One thing I want to change is that a scene (or whatever manager type) can contain multiple actions which it then iterates through, rather than the Sequencer triggering individual elements one by one. But, this works so I’ll stick with it for now.
Next Steps
The next thing I am going to tackle is creating actual gameplay through the GameState and Quest Management structure described earlier. While I have some initial setup game code, I want to begin the process of making actually narrative and gameplay structure for this SwiftUI game. I’m thinking some post apocalyptic survival/city-builder game.
Stay tuned!
Subscribe to my newsletter
Read articles from Adam Socki directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
