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"
	}
}

06:46

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

07:58

func on_item_picked_up(item_id: String):
	print("I got a ", Items.Database[item_id].name)

09:38

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
	}
}

10:44:

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() 

11:49

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.

14:47

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.

16:55:

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() 

18:03:

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

22:25:

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

26:20:

# 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()
 

30:48:

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

32:50:

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

38:36:

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
	

54:48:

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

1:20:30:

# 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.

1:27:08 Conclusion