Creating and Using Custom Resources - Data-Driven Design in Godot

Created: 2025-12-08Last updated: 2026-07-09

How to build data-driven design in Godot with custom resources (classes extending Resource). Covers separating logic from data, editing in the Inspector and saving to .tres, deriving subtypes through inheritance, staying safe at runtime with duplicate(), and choosing between JSON, CSV, and SQLite, with diagrams throughout.

When enemy stats are written directly into scripts, every new enemy type means copy-pasting code, and every balance pass means opening code to change numbers. Worse, designers and planners can't change a single value without touching code.

Custom resources solve this. You extend Godot's Resource system to create your own data types, separating game logic (code) from data (values). This is Godot's equivalent of Unity's ScriptableObject and the cornerstone of data-driven design.

Data-driven illustration: several data cards (weapon, armor, and potion stats) feeding into their respective characters and item slots

What You'll Learn

  • What data-driven design means (separating logic from data)
  • How to define a custom resource by extending Resource, and edit it in the Inspector
  • How to derive resources through inheritance (ItemDataWeaponData)
  • How duplicate() makes runtime modification safe
  • When to reach for JSON / CSV / SQLite instead

Sponsored

What Data-Driven Design Means

Data-driven design is the idea of decoupling "processing" (logic) from "values" (data). You feed swappable data into one piece of general-purpose code to produce many variations.

One piece of logic (code) loading several swappable data sets (warrior data, mage data, rogue data) to become different characters

For example, you write one script that makes enemies work. Hand it "slime data," "goblin data," or "dragon data" and the same code becomes a different enemy each time. By not hardcoding data, you get:

  • Adding an enemy means adding a data file, not code
  • Balance tuning becomes tweaking numbers in the Inspector instead of opening code
  • Planners and designers can edit data without knowing how to code

That makes development noticeably smoother, and custom resources are what carry that "data."

Defining and Using a Custom Resource

With a custom resource, you define the type in a script that extends Resource, edit the values in the Inspector, and save them to a .tres file.

The custom resource workflow: define extends Resource with @export in code, edit character_name/max_health/attack_power in the Inspector, and save as .tres

First, extend Resource and list your properties with @export. Adding class_name makes it usable as a global type.

# character_data.gd
class_name CharacterData
extends Resource

@export var character_name: String = "New Character"
@export var max_health: int = 100
@export var attack_power: float = 15.0

enum Job { WARRIOR, MAGE, ROGUE }
@export var job: Job = Job.WARRIOR         # enums become dropdowns in the Inspector

# You can also attach logic tied to the data as methods
func get_description() -> String:
    return "%s / HP:%d / ATK:%.1f / %s" % [
        character_name, max_health, attack_power, Job.find_key(job)]

Once saved, right-click in the FileSystem dock, choose Create New → Resource, pick CharacterData, and save it under a name like player_data.tres. From there, a node can take it directly as a type.

extends CharacterBody2D

@export var data: CharacterData      # drag the .tres onto it in the Inspector

func _ready() -> void:
    if data:
        print(data.get_description())  # behavior driven by the data

Because you can declare the type explicitly with @export var data: CharacterData, the Inspector only accepts CharacterData .tres files, which keeps things type-safe.

Sponsored

Hands-On: Building an Item Database with Inheritance

Custom resources shine brightest in an item or enemy database. RPG equipment, roguelike consumables, cards in a card game: inheritance expresses "a shared foundation plus per-type differences" cleanly.

WeaponData, PotionData, and ArmorData inheriting from a base ItemData (item_name/icon/stackable) and adding their own properties

Start with ItemData, the base shared by every item.

# item_data.gd
class_name ItemData
extends Resource

@export var item_id: int = 0
@export var item_name: String = "Unknown Item"
@export_multiline var description: String = ""
@export var icon: Texture2D
@export var stackable: bool = true

Weapons and potions need their own data. Inherit from ItemData and add only what's missing.

# weapon_data.gd
class_name WeaponData
extends ItemData                 # inherits every property of ItemData

@export var attack_bonus: float = 5.0   # weapon-only extra property
@export var weapon_type: String = "Sword"

Now WeaponData keeps the shared fields like item_name while gaining weapon-specific ones. When loading, you can tell them apart by type.

func load_item(path: String) -> ItemData:
    var res := ResourceLoader.load(path)
    if res is WeaponData:
        print("Weapon: ATK +%.1f" % res.attack_bonus)
    return res if res is ItemData else null

For the idea of inheritance itself, OOP Design Patterns is a useful companion read. Keep shared things in the base and specifics in the derived types, and your catalog stays manageable no matter how many item types you add.

Making Runtime Changes Safe with duplicate()

There's one thing to watch out for: resources are shared. Assign the same .tres to ten enemies and all ten are looking at the same data instance. Change one enemy's HP in place and every enemy's HP changes with it.

Contrast diagram: modifying the original data affects everyone sharing it, while duplicating with duplicate() and modifying the copy leaves the original intact

When you want per-instance values at runtime, make a copy with duplicate() and modify the copy.

func _ready() -> void:
    # Hold a private copy rather than the shared original
    data = data.duplicate()
    # From here on, any change to data leaves other instances and the .tres untouched
    data.max_health += 20

For per-instance changes like "vary each enemy's HP a little" or "reduce the charges on a potion the player picked up," always duplicate() first. Conversely, settings you want shared across all instances (master data) should stay shared and read-only.

Sponsored

Choosing Between the Alternatives

Custom resources aren't the only way to hold game data. Pick based on scale and purpose.

The four options compared: custom resources (.tres, editor integration), JSON (external tool interop), CSV (large volumes of simple data), and SQLite (querying thousands of records)
ApproachStrengthsGood for
Custom resourcesEditor integration, type safety, reuseStructured data: characters, items, skills
JSONHuman-readable, engine-agnosticConfig files, external tool interop
CSVEditable in a spreadsheetLarge volumes of simple records (dialogue lists, etc.)
SQLiteFast queries over large data setsItem or user data beyond a few thousand records

For structured data that stays inside Godot, custom resources are the first choice. Use JSON when exchanging data with external tools, CSV for translations and bulk dialogue, and SQLite when you need queries over thousands of records.

Bonus: Good to Know for Later

  • .tres vs .res: .tres is text, plays well with Git, and suits development. .res is binary, loads faster and is lighter, and suits release builds. Developing in .tres and shipping .res is the standard approach.
  • Load in bulk asynchronously: Reading every resource at startup is heavy, so use ResourceLoader.load_threaded_request() for asynchronous loading and keep the game responsive.
  • Notify changes with signals: If you want to tell other systems that a value inside a resource changed, define a signal inside the resource.
  • They work for save data too: You can pack play state into a custom resource and save it. We cover that in Implementing a Save/Load System.

Summary

  • Custom resources are your own data types extending Resource, separating logic from data
  • Define the type with class_name + @export, edit values in the Inspector, save to .tres
  • Use inheritance to keep shared fields in the base and specifics in derived types, so catalogs scale
  • duplicate() before modifying lets you create per-instance variation without breaking shared data
  • Choose between JSON / CSV / SQLite based on scale and purpose

Start by moving your character's base stats into a CharacterData and tweaking the numbers in the Inspector. Once you feel what it's like to change the game without touching code, you've taken the first step into data-driven design.

Further Reading