Skip to content
cccskills
FreePublished on GitHub

data-driven-gameplay-design

Use when adding or changing tuning values, items, enemies, abilities or levels - put them in the engine's data assets (ScriptableObject, Resource, DataAsset/DataTable, JSON/RON) with validation and stable IDs, keep runtime state out of shared data, inject services instead of singletons, and test data invariants.

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md6.3 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Data-Driven Gameplay Design

Overview

Designers balance games by changing numbers hundreds of times. If a number lives in code, each change is a programmer task, a rebuild and a review. If it lives in a data asset, it is an edit a designer can make and a test can check. The code defines what a sword is; the data says what this sword does.

Core principle: Behaviour in code, tuning in data. Data is typed, validated at load, referenced by a stable ID, and never mutated at runtime.

The data asset per engine

// Unity: ScriptableObject definition
[CreateAssetMenu(menuName = "Game/Weapon")]
public sealed class WeaponDefinition : ScriptableObject {
    [SerializeField] string id;
    [SerializeField, Min(0)] int damage = 10;
    [SerializeField, Min(0.05f)] float cooldownSeconds = 0.5f;
    [SerializeField] AnimationCurve falloff = AnimationCurve.Linear(0, 1, 1, 0);
    public string Id => id; public int Damage => damage; public float CooldownSeconds => cooldownSeconds;
    void OnValidate() { if (string.IsNullOrWhiteSpace(id)) Debug.LogError($"{name}: missing id", this); }
}
# Godot: a Resource saved as .tres
class_name WeaponDefinition extends Resource
@export var id: StringName
@export_range(0, 1000) var damage: int = 10
@export_range(0.05, 10.0, 0.05) var cooldown_seconds: float = 0.5
// Unreal: a primary data asset (or an FTableRowBase row in a DataTable imported from CSV)
UCLASS(BlueprintType)
class UWeaponDefinition : public UPrimaryDataAsset {
    GENERATED_BODY()
public:
    UPROPERTY(EditDefaultsOnly, meta = (ClampMin = "0")) int32 Damage = 10;
    UPROPERTY(EditDefaultsOnly, meta = (ClampMin = "0.05")) float CooldownSeconds = 0.5f;
    virtual FPrimaryAssetId GetPrimaryAssetId() const override { return FPrimaryAssetId("Weapon", GetFName()); }
};
// Web: JSON validated once at load (zod)
export const Weapon = z.object({
  id: z.string().min(1),
  damage: z.number().int().nonnegative(),
  cooldownSeconds: z.number().min(0.05),
});
export const weapons = z.array(Weapon).parse(await (await fetch("data/weapons.json")).json());
// Bevy: a serde struct loaded from RON/JSON via an asset loader (or bevy_common_assets)
#[derive(Asset, TypePath, Deserialize)]
pub struct WeaponDefinition { pub id: String, pub damage: u32, pub cooldown_seconds: f32 }

Rules

  • No tuning literals in gameplay code. damage = 25 in a script is a data bug. Physics constants and engine settings belong in project settings or a config asset too.
  • Validate at load or in the editor, not on first use mid-game: OnValidate / IsDataValid(FDataValidationContext&) / @export_range / a schema parse.
  • Stable IDs, not indices or display names. Saves, network messages and drop tables reference "sword_iron", never "the third weapon" or "Iron Sword".
  • Data is read-only at runtime. A ScriptableObject changed in Play mode stays changed in the editor; a shared Godot .tres mutated by one enemy changes every enemy. Copy into a runtime state object, or duplicate() the Resource (or set resource_local_to_scene).
  • No logic in data assets beyond trivial derived getters — rules live in systems that read the data.
  • Curves and tables for progressions: AnimationCurve, Godot Curve, UCurveFloat/UCurveTable, or a table in JSON. A level-1-to-50 XP formula hard-coded in a switch is not tunable.

Services, not singletons

// ❌ hidden global state; tests share it; order of initialisation decides behaviour
GameManager.Instance.Gold += reward;

// ✅ the dependency is explicit and replaceable in a test
public sealed class LootSystem {
    readonly IWallet _wallet; readonly IRandom _rng;
    public LootSystem(IWallet wallet, IRandom rng) { _wallet = wallet; _rng = rng; }
}

Each engine has a sanctioned place for services: Unity — a composition root or the repository's DI container (VContainer, Zenject); Godot — an autoload for a service (save, audio, scene loading), never for a level's state; Unreal — subsystems (UGameInstanceSubsystem, UWorldSubsystem); Bevy — Resources inserted by a plugin. Use the one the repository already uses.

Testing data

Two kinds of tests:

  1. Systems with hand-built data: a test creates its own small definition (ScriptableObject.CreateInstance<WeaponDefinition>(), WeaponDefinition.new(), a literal object) so the test does not change when a designer rebalances.
  2. Invariants over all shipped data: load every asset of the type and assert what must always hold.
describe("shipped data", () => {
  it("every drop table has positive total weight and only known item ids", () => {
    for (const t of dropTables) {
      expect(t.entries.reduce((s, e) => s + e.weight, 0)).toBeGreaterThan(0);
      for (const e of t.entries) expect(itemIds.has(e.itemId)).toBe(true);
    }
  });
});

In Unity this is an EditMode test using AssetDatabase.FindAssets("t:WeaponDefinition"); in Godot a test iterating DirAccess over the data folder; in Unreal an Automation test using the Asset Registry or the editor's Data Validation.

Balancing changes

  • A balance task changes data, not code — plus the invariant test if the change exposed a new rule.
  • Never let a unit test assert a shipped tuning value (Assert.AreEqual(25, sword.Damage)) — that is a change detector that breaks on every rebalance.

Common Mistakes

  • Magic numbers in Update "just for now".
  • Mutating a shared ScriptableObject or .tres at runtime.
  • Referencing data by array index in save files.
  • A static Instance holding gameplay state across scenes and tests.

Red Flags

  • A diff that changes a damage or speed number inside a .cs, .gd, .cpp, .ts or .rs file.
  • A data type with no validation.
  • Tests that fail when a designer edits a data asset.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Use when writing acceptance criteria for a task - express each as an observable Given/When/Then that QA can execute, including negative cases

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 18, 2026

Use when the diff adds or changes an endpoint, resolver, RPC, job or query that takes an object id, a role check, a request binding or a tenant filter - BOLA/IDOR, function-level authorization, mass assignment and tenant scoping

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 18, 2026

Use on every UI change - semantic HTML, labels for controls, keyboard-navigable dialogs/menus, visible focus, and never color as the only signal

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 18, 2026

Use when a task changes any screen, form, dialog, menu or control - Lighthouse/axe scan of the changed screens, a keyboard walk, and the thresholds that fail a task

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 18, 2026

How to work a task returned with review, QA or UAT findings. Use when a task is in need_revision or PR review comments are in your context.

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 18, 2026

Use when the repository keeps its alerting in code, or a person asks for alerts outside TaskTrooper - PrometheusRule YAML, a Cloud Monitoring alert policy in Terraform, a Datadog monitor, each routed to TaskTrooper's alert ingest URL so it opens an incident here

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 18, 2026

See all skills by makifbaysal

Report a problem with this skill