Unity6 Game Development · Lesson 11

ScriptableObject configuration and per-instance ownership

Share authored configuration without sharing every enemy’s mutable health.

Unity 6 Editor walkthrough

The Unity script needs the Editor and the setup listed below. The separate C# companion practice runs in SharpRune without UnityEngine.

Share authored configuration without sharing all actor state

A ScriptableObject asset can centralize authored configuration referenced by several actors. That sharing is useful when every enemy should begin with the same rule, but it is not equivalent to giving each actor independent mutable health. EnemyProfile keeps startingHealth on the asset and constructs a new RuntimeHealth for each request. The distinction is an ownership decision, not a property the engine infers from the words enemy or profile. Decide which values describe reusable configuration and which values belong to one actor’s changing runtime state.

Create an asset and preview two independent states

Save EnemyProfile.cs and use its CreateAssetMenu entry to create an actual Enemy Profile asset. With startingHealth ten, Preview Independent States creates two runtime objects, damages only the first by three and logs First seven, Second ten, Config ten. The preview is an Inspector context action on the asset; no custom second MonoBehaviour is required to run that ownership experiment. The mutable states are temporary ordinary nested objects, while the serialized configuration remains on the selected ScriptableObject asset.

The portable profile demonstrates the same ownership relationship

The C# companion creates a plain Profile with starting health ten, then asks it for two Health instances. Damaging the first by three leaves it at seven while the second and configuration remain ten, producing three corresponding output lines. It does not create a Unity asset or verify Inspector serialization. The result establishes independent runtime objects under the supplied positive-damage input. A broader health model still needs explicit policies for invalid damage, overkill, restoration and pooling; those rules should not be inferred from this ownership-only baseline.

An asset reference is not a per-instance clone

If actors directly reduce a health field stored on one shared ScriptableObject, they can unintentionally damage the shared configuration used by every actor. Conversely, repeatedly cloning configuration assets may be unnecessary when a small separate runtime model suffices. Editor asset edits also should not be assumed to revert like temporary scene-instance changes or to provide a complete save-game system. Persistence, asset authoring and one actor’s current state have different lifetimes, and a cache or event can keep references alive beyond the caller that originally created them.

Practice: change configuration and state in separate steps

Run the context preview at startingHealth ten, then set the asset to twenty and run it again. Predict seventeen, twenty and twenty for the fresh states. Compare that with keeping an already-created runtime state while changing the configuration in a separate gameplay extension: the earlier object’s health should not magically become the new starting value. This isolates creation-time copying from a live shared reference and makes the intended behavior testable before a scene contains many actors using the same profile.

  • Use a disposable course asset and do not rely on Play exit to undo every asset-authoring change.
  • If a pooled actor is reused, state explicitly whether it gets a new runtime object or a validated reset.

Practice: make a configuration change immutable for old states

For an independent portable exercise, add a second profile with a different starting health and create one state from each. Damage only one, then compare all current and configured values. Next decide whether Profile itself should be immutable and whether existing states should ever observe later configuration edits. Add a precise invalid-damage rule in a separate model exercise instead of relying on the demonstration’s positive input. This separates three contracts: shared configuration ownership, per-instance mutation and validation of operations applied to that instance.

  • Write a table with one column for each runtime state and one for each profile.
  • A field copied from configuration is different from a reference that continues reading live shared data.

Choose the smallest state boundary that matches gameplay

The real asset and portable model both teach deliberate ownership rather than a universal ScriptableObject architecture. Assets can remain excellent shared configuration while runtime state stays private to one actor. A production design may add versioned saves, events or pooling, but each adds its own lifetime and reset obligations. Validate those integrations in the actual Editor and Player rather than assuming the context preview exercised them. The goal is that damaging one actor changes exactly the state the game’s contract names, without accidentally rewriting every actor’s starting configuration.

  • Baseline values are seven, ten and ten in both ownership demonstrations.
  • The asset stores configuration; each CreateState request produces separate mutable runtime state.
  • Asset authoring, gameplay mutation and save persistence are different responsibilities.
Runs in Unity 6 Editor

Unity Editor example

Set up in Unity 6

  1. Create EnemyProfile.cs and wait for Unity to import the asset type.
  2. Use Assets > Create > SharpRune > Enemy Profile and set startingHealth to 10.
  3. Select the asset and invoke Preview Independent States from its Inspector context menu.
  4. Inspect the one log and verify that the asset’s configured startingHealth remains 10.
  5. In a separate gameplay extension, give two actors the same profile reference but store the returned RuntimeHealth separately for each.
using UnityEngine;
[CreateAssetMenu(menuName = "SharpRune/Enemy Profile")]
public sealed class EnemyProfile : ScriptableObject
{
    [SerializeField, Min(1)] private int startingHealth = 10;
    public RuntimeHealth CreateState() { return new RuntimeHealth(Mathf.Max(1, startingHealth)); }
    [ContextMenu("Preview Independent States")]
    private void Preview()
    {
        RuntimeHealth first = CreateState();
        RuntimeHealth second = CreateState();
        first.Damage(3);
        Debug.Log("First=" + first.Current + ", Second=" + second.Current + ", Config=" + startingHealth, this);
    }
    public sealed class RuntimeHealth
    {
        public int Current { get; private set; }
        public RuntimeHealth(int health) { Current = health; }
        public void Damage(int amount) { Current = Mathf.Max(0, Current - Mathf.Max(0, amount)); }
    }
}

What to observe in Unity

The context action logs First=7, Second=10, Config=10. The two runtime states are independent even though they originate from one shared configuration asset.

The console practice below isolates the C# logic. It does not run a Unity scene or emulate Unity engine callbacks.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var profile = new Profile(10); var first = profile.Create(); var second = profile.Create();
first.Damage(3); Console.WriteLine(first.Current); Console.WriteLine(second.Current); Console.WriteLine(profile.StartingHealth);
sealed class Profile { public int StartingHealth { get; } public Profile(int health) { StartingHealth = health; } public Health Create() => new Health(StartingHealth); }
sealed class Health { public int Current { get; private set; } public Health(int health) { Current = health; } public void Damage(int amount) { Current = Math.Max(0, Current - amount); } }
Open C# practice

C# practice output

7
10
10
Try this
Shared configuration can produce separate runtime objects; an asset reference alone does not clone state.

Further reading