Skip to content

Game resources

Authored data that belongs to the game, not to any one entity — enemy stat blocks, weapon definitions, dialogue tables, level configuration.

GameEntity is a thing in a scene. GameResource is data that isn't.

A resource has no transform, no scene, and no lifecycle tied to an entity. It can be loaded by a script that holds no entity at all, and it lives in its own asset file so a designer can edit it without opening a scene.

Declaring one

Subclass GameResource and give it public fields:

csharp
using JoystickEngine;

[CreateResourceMenu(MenuName = "Gameplay/Enemy Stats", FileName = "EnemyStats")]
public class EnemyStats : GameResource
{
    public float Speed = 3.0f;
    public int MaxHealth = 100;
    public int Damage = 10;
    public string DeathEffect = "Effects/Puff.jparticle";
}

With [CreateResourceMenu], Assets → Create → Gameplay → Enemy Stats creates a .jresource file you can edit in the Properties panel. Without it, the class still works — it just has no menu item, which is how you keep abstract base classes out of the menu. See Attributes.

Using one

Reference it from a script field, and assign the asset in the Properties panel:

csharp
public class Goblin : GameEntity
{
    [SerializeField] private EnemyStats? m_Stats;

    protected override void OnCreate()
    {
        m_Health = m_Stats?.MaxHealth ?? 100;
    }
}

Or load it by name.

GameResource.Load<T>(string name)

Resolves a resource by its asset file stem — "GoblinStats" for Assets/Data/GoblinStats.jresource. Returns the same instance on every call, or null if nothing has that stem, or if the asset turns out to be a different class than T (which logs a warning rather than throwing).

csharp
EnemyStats? stats = GameResource.Load<EnemyStats>("GoblinStats");

Loading by name resolves through the same lookup Scenes.Load(name) uses, so renaming the file in the Content Browser doesn't break a script that loads it by name.

Instance members

Name

The asset's file stem — "GoblinStats".

ResourceId

The asset's id. Always set by the time any game code can reach the instance.

OnLoad()

Called once, after every authored field has been applied — the resource-side counterpart to GameEntity.OnCreate. Override it to derive values from the authored ones.

csharp
public class WeaponStats : GameResource
{
    public float DamagePerShot = 10f;
    public float FireRate = 4f;

    public float DamagePerSecond { get; private set; }

    protected override void OnLoad()
    {
        DamagePerSecond = DamagePerShot * FireRate;
    }
}

Never called again, however many scripts reference the asset — there is only ever one instance.

The shared-instance rule

Every script referencing the same asset shares the same object

Mutating a field at runtime is visible to every referencer. That's the entire point — one place to tune a value — and it's also the thing people coming from Unity get wrong first.

csharp
// This changes the speed of EVERY goblin, permanently, for this session.
m_Stats!.Speed = 10f;

If you need per-instance state, put it on the entity's script, not on the resource. A resource is authored data to read from, not a place to keep runtime state.

Play-mode edits are not saved back

Unity persists changes you make to a ScriptableObject during play. Joystick deliberately does not: stopping Play drops every cached instance, and the next Play re-reads the authored values from disk. So "Play → Stop reverts everything" holds here, unlike Unity — a difference worth knowing before you tune something in Play mode and expect it to stick.

ResourceRegistry

The runtime cache — one live object per unique asset.

ResourceRegistry.Load(ulong assetId)

Resolves a resource by asset id. GameResource.Load<T> is the friendlier form and what game code should use; this exists because the engine calls it.

Returns null — logged, never thrown — if the id is 0, doesn't resolve to a resource asset, or names a class that can't be found or constructed. An authored-data mismatch is never fatal in this engine.

Example: a data-driven enemy

csharp
using JoystickEngine;

[CreateResourceMenu(MenuName = "Gameplay/Enemy Stats")]
public class EnemyStats : GameResource
{
    public float Speed = 3.0f;
    public int MaxHealth = 100;
    public string DeathEffect = "Effects/Puff.jparticle";
}

public class Enemy : GameEntity
{
    [SerializeField] private EnemyStats? m_Stats;
    private int m_Health;

    protected override void OnCreate()
    {
        m_Health = m_Stats?.MaxHealth ?? 100;   // per-entity state lives here
    }

    public void TakeDamage(int amount)
    {
        m_Health -= amount;
        if (m_Health > 0)
            return;

        if (m_Stats != null)
            Particles.Spawn(m_Stats.DeathEffect, Translation);
    }
}

Three enemy types are now three .jresource files and one script, tunable without a rebuild.

See also

  • Attributes[CreateResourceMenu] and [SerializeField]
  • Assets — where .jresource files live
  • GameEntity — the other half of the pair