Data Driven Quest System with JSON


Now you, yes, you random internet stranger can try out our game!

The physics you see here has gone through about 4 major iterations, each previous version with their own charming quirks and head-scratching bugs.

---

Data-driven Quest System:

Past gamejams have taught the value of making sure everyone can work on it without (always) stepping on each other's toes. Hence -- the text-file defined quests!

We have a JSON file, easily and natively parsable in Godot. We have a 376-line script called QuestManager.gd to deal with everything between that file and the player. I wrote that, so I feel disproportionately affectionate toward this code, and I'm going to go into detail about it. The code in this post is not complete - I've skipped several of the duller sections to focus on how and what was done.

There are two signals that QuestManager sends out into the rest of the game. They are both for soundeffect control. Sound effects occur when 1) the player arrives on a celestial body (planet, spacestation, or moon) and 2) when the player completes a conversation in a quest.

signal arrive_sfx
signal stage_sfx(accepted_new_b, reward_item_b, more_money_b, less_money_b, quest_end_b)

We have a list of strings for inventory. Every item is unique, so there's no need to track amounts. Inventory determines what conversations are available.

var inventory = []
var player_money : int = 1000

The JSON file is a list of quests, and each quest has some number of conversations within it. Each conversation is a Stage of the quest.

all_stages stores the entire list of all possible conversations in-game.

var all_stages = [] # MASTER plot point tracker
var accepted_requests = [] 
var current_stage : Stage = null
var all_quests_path = "res://data//quest.json"

When the player gets near a planet and successfully orbits without crashing for a while, it's time to figure out what conversations are relevant to display, based on player inventory and which planet and solar system they're in:

func arrive_at_planet(solar_system: int, planet: int):
# react to the player arriving at the planet specified by the arguments
# by updating the ui with requests from that planet
emit_signal("arrive_sfx")
anim.play("showPlanetAsks")
var relevant_stages = get_relevant_stages_for_planet(solar_system, planet, inventory)
for child in dialogue_choice_ui_parent.get_children():
child.queue_free()
var body_flavor_text = get_flavor_text_for_planet(solar_system, planet)
var travel_guide_text = planet_namer.get_planet(solar_system, planet) + " Travel Guide"
print("body_flavor_text=" + body_flavor_text)
if body_flavor_text != "":
var panel = flavor_text_prefab.instance()
panel.get_child(0).get_child(0).text = travel_guide_text
panel.get_child(0).get_child(1).text = body_flavor_text
dialogue_choice_ui_parent.add_child(panel)
# instantiate Ui elements representing each request on the planet
for a in range(relevant_stages.size()):
var ui = dialogue_choice_ui_prefab.instance()
ui.init(relevant_stages[a])
dialogue_choice_ui_parent.add_child(ui)
ui.connect("append_to_accepted_quest_info", self, "on_append_to_ship_log")
ui.connect("yes_chosen", self, "on_yes_chosen")
ui.connect("no_chosen", self, "on_no_chosen")
# other ui updates
update_planet_request_toggle()
update_ship_log()
update_inventory()
var camcontrol = get_tree().get_nodes_in_group("CameraControl")
if camcontrol.size() > 0:
camcontrol[0].move_to_planet(solar_system, planet)

JSON parsing in godot returns a dictionary. But dicts have to be accessed with strings, which are typo-prone and not flexible to changing data structures. So I went through the dictionary and turned it into a instance of a class.

Not only does this standardize what is used in the dictionary, but it helps Godot autosuggest what field name I'm trying to type. Autosuggestions are great.

This is really just repetition of taking a value from the dictionary and saving it in the nice Stage class instance, again and again:

func parse_jsondict_to_structs(json_result) -> void:
# turn the json dictionary into a struct
# the goal is to ensure we don't have to deal with invalid dictionary accesses
# because the struct's default values will be returned or typos in the field
# name will be easier to correct 
# this is the only function that should use hardcoded strings to access data 
# imported from JSON. This limits human error in typing field names.
for quest_line in range(json_result.root.size()):
var dict_stages = json_result.root[quest_line]["stages"]
var quest_name = json_result.root[quest_line]["quest_name"]
var stage_ids_of_this_quest = [] # list of id for all stages in this quest
for key in dict_stages.keys(): # note: unordered
var new_stage : Stage = Stage.new()
var d = dict_stages[key] # the dictionary relevent to this stage
new_stage.quest_name = quest_name
new_stage.stage_name = key
new_stage.speaker_name = d["speaker_name"]
new_stage.dialogue = d["dialogue"]
new_stage.choices = d["choices"]
if d.has("speaker_responses"):
new_stage.speaker_responses = d["speaker_responses"]
new_stage.solar_system = d["solar_system"]
new_stage.planet = d["planet"]
if d.has("required_inventory"):
new_stage.required_inventory = d["required_inventory"]
if d.has("required_money"):
new_stage.required_money = d["required_money"]
if d.has("is_complete"):
new_stage.is_complete = d["is_complete"]
if d.has("dependent_stages"):
new_stage.dependent_stages = d["dependent_stages"]
elif key == "end": # the last stage in a quest will close out all ids in the quest
print("Dependents:" + str(dict_stages.keys()))
new_stage.dependent_stages = dict_stages.keys()
# results of yes and no
if d.has("yes_accepted_quest_info"):
new_stage.yes_accepted_quest_info = d["yes_accepted_quest_info"]
if d.has("yes_money_change"):
new_stage.yes_money_change = d["yes_money_change"]
if d.has("yes_cost_items"):
new_stage.yes_cost_items = d["yes_cost_items"]
if d.has("yes_reward_items"):
new_stage.yes_reward_items = d["yes_reward_items"]
if d.has("yes_is_complete"):
new_stage.yes_is_complete = d["yes_is_complete"]
if d.has("yes_end"):
new_stage.yes_end = d["yes_end"]
if d.has("no_accepted_quest_info"):
new_stage.no_accepted_quest_info = d["no_accepted_quest_info"]
if d.has("no_cost_items"):
new_stage.no_cost_items = d["no_cost_items"]
if d.has("no_reward_items"):
new_stage.no_reward_items = d["no_reward_items"]
if d.has("no_money_change"):
new_stage.no_money_change = d["no_money_change"]
if d.has("no_end"):
new_stage.no_end = d["no_end"]
if d.has("no_is_complete"):
new_stage.no_is_complete = d["no_is_complete"]
new_stage.id = all_stages.size()
stage_ids_of_this_quest.append(new_stage.id)
all_stages.append(new_stage)

Comments

Log in with itch.io to leave a comment.

(1 edit)

This sounds like a great way to facilitate collaboration! Also, how did you insert code blocks to your post?