Game resources
Authored data that belongs to the game, not to any one entity — enemy stat blocks, weapon definitions, dialogue tables, level configuration.
GameEntityis a thing in a scene.GameResourceis 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:
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:
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).
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.
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.
// 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
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
.jresourcefiles live - GameEntity — the other half of the pair