Description
Hello Godotneers! Every game revolves around data. Your game may have items, unit types, crafting recipes, dialogue, quests and a lot more. Handling all this…
My Notes
00:00 Introduction
- This video focuses on how to make data models in Godot more composable and reusable.
01:18 A database of items
- 05:04This is a data model. It just hold data.
- It is not concerned with how the data is shown or used.
- 05:21: Using a Dictionary as a database of items.
class_name Items
const Database = {
"pickaxe": {
"name": "Pickaxe"
}
}
extends Node3D
func _on_area_3d_body_entered(body):
if body.has_method("on_item_picked_up"):
body.on_item_picked_up("pickaxe") # problem: this pickaxe ID is hard coded, so this code only works for pickaxes.
queue_free() # make the pickaxe disappear on the world because the character picked it up
func on_item_picked_up(item_id: String):
print("I got a ", Items.Database[item_id].name)
extends Node3D
@export var item_id: String
func _on_area_3d_body_entered(body):
if body.has_method("on_item_picked_up"):
body.on_item_picked_up(item_id)
queue_free()
09:44 How do we tell our pickup object which 3D model to display in the world?
class_name Items
const Database = {
"pickaxe": {
"name": "Pickaxe",
"scene": "res://elements/pickaxe/pickaxe.glb" # tip: you can drag in your 3D asset and Godot will input the string for the path
}
}
extends Node3D
@export var item_id: String
func _ready():
var scene = load(Items.Database[item_id].scene)
var instance = scene.instantiate()
# now the scene is loaded at runtime in code
# so we don't need it in the Scene tree in the Godot editor
add_child(instance)
func _on_area_3d_body_entered(body):
if body.has_method("on_item_picked_up"):
body.on_item_picked_up(item_id)
queue_free()
class_name Items
const Database = {
"pickaxe": {
"name": "Pickaxe",
"scene": "res://elements/pickaxe/pickaxe.glb" # tip: you can drag in your 3D asset and Godot will input the string for the path
},
"sword": {
"name": "Sword",
"scene": "res://elements/sword/sword.glb"
}
}
- 13:15: cons of this approach:
- typos. These are all strings
- Hardcoded scene paths.
- Hard to maintain this dictionary as it gets larger.
14:09 Using resources for game data
- Godot has a built in feature for modeling static data sources called Resource.
- You can also create custom Resources.
class_name Item extends Resource
@export var name: String
@export var scene: PackedScene # this is better than using a String
15:43: In the FileSystem you can add a Resource by right-clicking a folder and choosing Create New Resource.
extends Node3D
@export var item: Item
func _ready():
var instance = item.scene.instantiate()
add_child(instance)
func _on_area_3d_body_entered(body):
if body.has_method("on_item_picked_up"):
body.on_item_picked_up(item)
queue_free()
func on_item_picked_up(item: Item):
print("I got a ", Items.Database[item].name)
20:16 Building an inventory dialog
- 20:43: What should we extend?
- Could be a Nodes in Godot|Node but we don’t need any Node functionality.
- We just need a simple object, which is
RefCounted
. But this is the default anyway, so we can leave it out.
class_name Inventory
var _content: Array[Item] = []
func add_item(item: Item):
_content.append(item)
func remove_item(item: Item):
_content.erase(item)
func get_items() -> Array[Item]:
return _content
class_name Player extends CharacterBody3D
# ...
var inventory := Inventory.new()
func on_item_picked_up(item: Item):
inventory.add_item(item)
23:00: inventory_dialog.tscn
# inventory_dialog.gd
class_name InventoryDialog extends PanelContainer
@export var slot_scene: PackedScene
@onready var grid_container: GridContainer = %GridContainer
func open(inventory: Inventory):
show()
for item in inventory.get_items():
var slot = slot_scene.instantiate()
grid_container.add_child(slot)
func _on_close_button_pressed():
self.hide()
class_name Item extends Resource
@export var name: String
@export var scene: PackedScene
@export var icon: Texture2D # NEW
31:29: InventoryDialog should show the item slots, but it should be unaware of item slot’s child components, so lets create a ItemSlot
class to handle this
31:26:
class_name ItemSlot extends PanelContainer
@onready var texture_rect: TextureRect = %TextureRect
func display(item: Item):
texture_rect.texture = item.icon
func open(inventory: Inventory):
show()
for item in inventory.get_items():
var slot = slot_scene.instantiate()
grid_container.add_child(slot)
# IMPORTANT: Add the node as a child first and THEN call display()
slot.display(item)
33:21: Adding the UI to the scene:
34:34: Code to show the inventory dialog:
# ui_root.gd
extends CanvasLayer
@onready var player: Player = %Player
@onready var inventory_dialog: InventoryDialog = %InventoryDialog
func _unhandled_input(event):
if event.is_action_released("inventory"):
# "inventory" is from the Input Map
inventory_dialog.open(player.inventory)
35:42: Setting up the Input Map.
37:02: inventory_dialog.gd
func open(inventory: Inventory):
show()
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
# PROBLEM: Items are only added, never removed.
for item in inventory.get_items():
var slot = slot_scene.instantiate()
grid_container.add_child(slot)
# IMPORTANT: Add the node as a child first and THEN call display()
slot.display(item)
func _on_close_button_pressed():
hide()
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func open(inventory: Inventory):
show()
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
for child in grid_container.get_children():
child.queue_free()
for item in inventory.get_items():
var slot = slot_scene.instantiate()
grid_container.add_child(slot)
# IMPORTANT: Add the node as a child first and THEN call display()
slot.display(item)
func _on_close_button_pressed():
hide()
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
39:28: Review
40:41 Creating a crafting system
42:14: recipe.gd
class_name Recipe extends Resource
@export var name: String
@export var ingredients: Array[Item] = []
@export var results: Array[Item] = []
44:57: crafting_dialong.tscn: 50:56: crafting_dialong.gd
class_name CraftingDialong extends PanelContainer
@export var slot_scene: PackedScene
@onready var recipe_list: ItemList = %RecipeList
@onready var ingredients_container: GridContainer = %IngredientsContainer
@onready var results_container: GridContainer = %ResultsContainer
func open(recipes: Array[Recipe], inventory: Inventory):
show()
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
recipe_list.clear()
for recipe in recipes:
recipe_list.add_item(recipe.name)
func _on_close_button_pressed():
hide()
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
53:50: Connect to item_selected(index: int) signal from GridContainer
func _on_recipe_list_item_selected(index: int):
# we need to first get the recipe itself
class_name CraftingDialong extends PanelContainer
# ...
func open(recipes: Array[Recipe], inventory: Inventory):
show()
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
recipe_list.clear()
for recipe in recipes:
var index: int = recipe_list.add_item(recipe.name)
recipe_list.set_item_metadata(index, recipe) # 👈🏼
func _on_recipe_list_item_selected(index: int):
# we need to first get the recipe itself
var recipe = recipe_list.get_item_metadata(index)
func _on_close_button_pressed():
hide()
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
56:17: ItemGrid.gd
class_name ItemGrid extends GridContainer
@export var slot_scene: PackedScene
func display(items: Array[Item]):
for child in get_children():
child.queue_free()
for item in items:
var slot = slot_scene.instantiate()
add_child(slot)
slot.display(item)
# inventory_dialog.gd
class_name InventoryDialog extends PanelContainer
@export var slot_scene: PackedScene
@onready var grid_container: GridContainer = %GridContainer
func open(inventory: Inventory):
show()
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
grid_container.display(inventory.get_items())
func _on_close_button_pressed():
self.hide()
Input.mouse_mode = Input.MOUSE_MODE_CAPUTRED
1:12:57 Loading resources dynamically
1:13:34: Load all the resources in a folder
var _all_recipes: Array[Recipe] = []
func _ready():
# Get all the files in this folder
for file in DirAccess.get_files_at("res://data/recipes")
var resource_file = "res://data/recipes/" + file
var recipe: Recipe = load(resource_file) as Recipe
_all_recipes.append(recipe)
func _unhandled_input(event):
if event.is_action_released("inventory"):
inventory_dialog.open(player.inventory)
if event.is_action_released("crafting"):
crafting_dialog.open(_all_recipes, player.inventory)
1:15:26: Exporting the game to windows
Godot Resource Group plugin
Why Use a Resource Database
PROBLEM: The files don’t load when exported, because when Godot exports the files, they are organized and so all the file paths break. Solution: Godot Resource Groups plugin
# ui_root.gd
# ...
@export var all_recipes: ResourceGroup
@onready var _all_recipes: Array[Recipe] = []
func _ready():
all_recipes.load_all_into(_all_recipes)
1:22:15 Maintaining a resource database
1:24:13: Edit Resources as Table 2 plugin
This plugin allows you to edit many resources at the same time in a table view, like a spreadsheet.