Skip to content

Unity API Reference

MorphynEntity

Attach to any GameObject to give it its own .morph script. The entity is registered into the shared runtime context automatically when the scene starts. Fields and events are visible and editable in the Inspector.

Settings

Setting Description
Script .morph file to load (TextAsset)
Custom Entity Name Override the entity name (defaults to file name)
Auto Save On Destroy Write entity state to disk when the GameObject is destroyed

Inspector

In Edit mode, the Inspector shows all has fields with their current values. Editing a field directly patches the .morph file on disk. In Play mode, fields reflect live runtime values and can be edited in real time. All events are listed with an Emit button to fire them manually — useful for testing without writing C#.

API Methods

MorphynEntity entity = GetComponent<MorphynEntity>();

// Get field — generic with default
float hp   = entity.Get<float>("hp", 100f);
bool alive = entity.Get<bool>("alive", true);

// Get raw MorphynValue
MorphynValue raw = entity.GetField("hp");

// Set field — typed overloads
entity.SetField("hp", 50f);
entity.SetField("alive", true);
entity.SetField("name", "Hero");
entity.SetField("hp", MorphynValue.FromDouble(50.0)); // explicit MorphynValue

// Trigger event
entity.Emit("damage", 25);
entity.Emit("die");

// Watch field for changes — typed
entity.Watch<float>("hp", (old, now) => {
    hpBar.fillAmount = now / maxHp;
});

// Watch field for changes — raw MorphynValue
entity.Watch("hp", (oldVal, newVal) => {
    Debug.Log($"hp changed: {oldVal.NumVal} -> {newVal.NumVal}");
});

// Listen to another entity's events
entity.ListenTo("Boss", "phase_change", args => {
    int phase = System.Convert.ToInt32(args[0].ToObject());
    UpdateBossUI(phase);
});

// Entity name (resolved after registration)
string name = entity.EntityName;

MorphynController

The main runtime component. ONE instance per scene manages the shared context. Required even when using MorphynEntity — it hosts the tick loop, hot reload, and save/load.

Settings

Setting Description
Morphyn Scripts .morph files to load globally
Run On Start Auto-load on Start()
Enable Tick Send tick(dt) every frame
Enable Hot Reload Edit files during Play mode
Auto Save Save on quit
Save Folder Where to save state

API Methods

MorphynController morphyn = MorphynController.Instance;

// Get field — typed helpers
float hp        = morphyn.GetFloat("Player", "hp");
bool alive      = morphyn.GetBool("Player", "alive");
double speed    = morphyn.GetDouble("Player", "speed");
string nickname = morphyn.GetString("Player", "nickname");

// Get field — generic (auto-converts to T)
float hp   = morphyn.Get<float>("Player", "hp");
bool alive = morphyn.Get<bool>("Player", "alive");

// Get field — dynamic (returns double, bool, string, MorphynPool or null)
object val = morphyn.Get("Player", "hp");

// Get raw MorphynValue
MorphynValue raw = morphyn.GetField("Player", "hp");

// Set field — accepts bool, float, double, string or MorphynValue directly
morphyn.SetField("Player", "hp", 50.0);
morphyn.SetField("Player", "alive", true);
morphyn.SetField("Player", "name", "Hero");

// Get all fields
Dictionary<string, MorphynValue> fields = morphyn.GetAllFields("Player");

// Trigger event
morphyn.Emit("Player", "damage", 25);

// Trigger event and get return value
MorphynValue result = morphyn.EmitSync("Player", "calculate", 25.0, 10.0);

// Subscribe Morphyn entity to another entity's event
morphyn.Subscribe("Logger", "Player", "death", "onPlayerDeath");

// Unsubscribe
morphyn.Unsubscribe("Logger", "Player", "death", "onPlayerDeath");

// Subscribe C# method to a Morphyn event
morphyn.When("Player", "death", args => Debug.Log("Player died!"));

// Unsubscribe C# method
morphyn.Unwhen("Player", "death", myHandler);

// Watch a field for changes — raw MorphynValue
morphyn.Watch("Player", "hp", (oldVal, newVal) => {
    Debug.Log($"hp changed: {oldVal.NumVal} -> {newVal.NumVal}");
});

// Watch a field for changes — typed (auto-converts to T)
morphyn.Watch<float>("Player", "hp", (old, now) => {
    hpBar.fillAmount = now / maxHp;
});

// Unwatch
morphyn.Unwatch("Player", "hp", myCallback);

// Save / Load
morphyn.SaveState();
morphyn.LoadState("Player");
morphyn.LoadAllStates();

C# Listeners

Subscribe any C# method directly to a Morphyn entity event using When and Unwhen.

When

MorphynController.Instance.When(entityName, eventName, handler);

The handler receives the same arguments the event was fired with as object?[].

Example:

void Start()
{
    MorphynController.Instance.When("Player", "death", OnPlayerDeath);
    MorphynController.Instance.When("Player", "levelUp", args => {
        double level = Convert.ToDouble(args[0]);
        levelUpScreen.Show((int)level);
    });
}

void OnPlayerDeath(object?[] args)
{
    deathScreen.SetActive(true);
    respawnButton.interactable = true;
}

void OnDestroy()
{
    // Always unsubscribe to avoid memory leaks
    MorphynController.Instance.Unwhen("Player", "death", OnPlayerDeath);
}

Unwhen

MorphynController.Instance.Unwhen(entityName, eventName, handler);

Note

Always call Unwhen in OnDestroy to avoid memory leaks and calls on destroyed objects.

Difference from Subscribe

When / Unwhen Subscribe / Unsubscribe
Subscriber C# Action<object?[]> Morphyn entity event
Use case Unity UI, audio, effects Morphyn-to-Morphyn logic
Handler Any C# lambda or method Morphyn event name
// C# reacts to Morphyn event
morphyn.When("Enemy", "death", args => {
    Instantiate(explosionPrefab, transform.position, Quaternion.identity);
});

// Morphyn entity reacts to Morphyn event
morphyn.Subscribe("Logger", "Enemy", "death", "onEnemyDeath");

Field Watchers

Subscribe a C# callback to changes of a specific field on a Morphyn entity. The callback fires immediately after the field is written, before the next tick. It only fires when the value actually changes — assigning the same value is a no-op.

Watch

// Raw MorphynValue
morphyn.Watch(entityName, fieldName, (oldVal, newVal) => { });

// Typed — auto-converts to T (supports float, double, int, bool, string)
morphyn.Watch<T>(entityName, fieldName, (old, now) => { });

// MorphynEntity shorthand — no entity name needed
entity.Watch<T>(fieldName, (old, now) => { });

Example:

void Start()
{
    MorphynController.Instance.Watch<float>("Player", "hp", OnHpChanged);
    MorphynController.Instance.Watch("Enemy", "alive", (old, now) => {
        if (!System.Convert.ToBoolean(now.ToObject()))
            ShowDeathEffect();
    });
}

void OnHpChanged(float old, float now)
{
    hpBar.fillAmount = now / maxHp;
    if (now <= 0) deathScreen.SetActive(true);
}

void OnDestroy()
{
    MorphynController.Instance.Unwatch("Player", "hp", OnHpChanged);
}

Unwatch

morphyn.Unwatch(entityName, fieldName, callback);

Pass the same delegate instance used in Watch. Always call Unwatch in OnDestroy.

Watch vs When

Watch / Unwatch When / Unwhen
Trigger Field value changes Entity fires an event
Arguments (oldValue, newValue) Event arguments
Use case UI sync, visual feedback Reactions to game events

Event Subscriptions from C

You can manage Morphyn entity subscriptions directly from C# using Subscribe and Unsubscribe.

Subscribe

// subscriberEntity will receive handlerEvent when targetEntity fires targetEvent
morphyn.Subscribe(subscriberEntityName, targetEntityName, targetEvent, handlerEvent);

Example — Logger reacts to Player death:

MorphynController.Instance.Subscribe("Logger", "Player", "death", "onPlayerDeath");

This is equivalent to writing inside a .morph file:

when Player.death : onPlayerDeath

Unsubscribe

morphyn.Unsubscribe(subscriberEntityName, targetEntityName, targetEvent, handlerEvent);

Full Example

logger.morph:

entity Logger {
  has count: 0

  event onPlayerDeath {
    count + 1 -> count
    emit unity("Log", "Player died! Total deaths:", count)
  }

  event onPlayerLevelUp(level) {
    emit unity("Log", "Player reached level:", level)
  }
}

GameManager.cs:

using UnityEngine;

public class GameManager : MonoBehaviour
{
    void Start()
    {
        var morphyn = MorphynController.Instance;
        morphyn.Subscribe("Logger", "Player", "death", "onPlayerDeath");
        morphyn.Subscribe("Logger", "Player", "levelUp", "onPlayerLevelUp");
    }

    public void OnBossFightEnd()
    {
        MorphynController.Instance.Unsubscribe("Logger", "Player", "death", "onPlayerDeath");
    }
}

Notes

  • Subscriptions set from C# behave exactly like when/unwhen inside .morph files
  • An entity cannot subscribe to its own instance — Subscribe("Player", "Player", ...) will log a warning and do nothing
  • Duplicate subscriptions are ignored
  • Dead entities are cleaned up automatically during garbage collection

Unity Bridge

Call Unity functions from .morph files.

Setup

using UnityEngine;
using Morphyn.Unity;

public class GameManager : MonoBehaviour
{
    void Start()
    {
        // Register callbacks BEFORE MorphynController.Start()
        UnityBridge.Instance.RegisterCallback("PlaySound", args => {
            string soundName = args[0]?.ToString() ?? "";
            AudioSource.PlayOneShot(Resources.Load<AudioClip>(soundName));
        });

        UnityBridge.Instance.RegisterCallback("ShowMessage", args => {
            Debug.Log($"Message: {string.Join(" ", args)}");
        });
    }
}

Calling from Morphyn

entity Enemy {
  has hp: 50

  event damage(amount) {
    hp - amount -> hp
    emit unity("PlaySound", "hit")
    check hp <= 0: {
      emit unity("PlaySound", "explosion")
      emit unity("ShowMessage", "Enemy destroyed!")
    }
  }
}

Built-in Callbacks

MorphynController registers these automatically:

emit unity("Log", "Hello", "World")   # Debug.Log
emit unity("Move", 1, 0, 0)           # moves MorphynController's transform
emit unity("Rotate", 45)              # rotates MorphynController's transform

Hot Reload

Edit .morph files during Play mode — changes apply instantly.

Enable: Check Enable Hot Reload on MorphynController.

  1. Start Play mode
  2. Open player.morph
  3. Change has damage: 10 to has damage: 999
  4. Save
  5. Damage updates immediately — no restart needed

Save / Load

Morphyn saves entity state as readable .morph files.

// Save all entities
MorphynController.Instance.SaveState();
// Saved to: Application.persistentDataPath/MorphynData/

// Load single entity
MorphynController.Instance.LoadState("Player");

// Auto-save on quit — set in Inspector

Per-file save policy is set on each entry in the MorphynController Morphyn Scripts array:

Mode Behavior
None Never save or load
Auto Load on startup, save on quit
ManualOnly Only when you call SaveState() / LoadState() from code

MorphynEntity has its own Auto Save On Destroy toggle. When enabled, the entity's fields are written to disk when the GameObject is destroyed.

Saved file example:

entity Player {
  has hp: 75
  has level: 5
  has gold: 230
}

Best Practices

1. Prefer MorphynEntity over dumping everything into MorphynController

PlayerObject  → MorphynEntity → player.morph
EnemyPrefab   → MorphynEntity → enemy.morph
ShopManager   → MorphynEntity → shop.morph

One MorphynController still needs to exist in the scene for the runtime, but it doesn't need to hold every script.

2. One MorphynController per scene

MorphynController.Instance // singleton — only one instance exists

3. Register Unity callbacks early

[DefaultExecutionOrder(-100)]
public class Setup : MonoBehaviour
{
    void Awake() // Use Awake, not Start
    {
        UnityBridge.Instance.RegisterCallback("MyCallback", args => {});
    }
}

4. Always unsubscribe in OnDestroy

void OnDestroy()
{
    MorphynController.Instance.Unwhen("Player", "death", myHandler);
    MorphynController.Instance.Unwatch("Player", "hp", myWatcher);
}

5. Use for configs and logic, not heavy simulation

// GOOD
entity ShopPrices { has swordCost: 100 }

// BAD
entity ComplexAI { /* keep heavy simulation in C# */ }

Troubleshooting

"MorphynController not found"

if (MorphynController.Instance == null)
    Debug.LogError("Add MorphynController to scene!");

"Callback not found"

  • Register callbacks in Awake(), not Start()
  • Register before MorphynController loads scripts

Inspector shows "Failed to parse"

  • Check that the entity name in the .morph file matches the file name (or set Custom Entity Name)
  • Check the Console for parser errors
  • Make sure the .morph file has valid syntax

Hot reload not working

  • Editor only — not available in builds
  • File must be inside the Assets folder
  • Check Enable Hot Reload is ticked

Values not updating

morphyn.SetField("Player", "hp", 50.0);
float hp = morphyn.GetFloat("Player", "hp"); // 50

Watch not firing

  • Check the field name matches exactly (case-sensitive)
  • Watch fires only on value change — setting the same value does nothing
  • Make sure Watch is called after LoadAndRun() completes

C# listener called after object destroyed

void OnDestroy()
{
    MorphynController.Instance.Unwhen("Player", "death", myHandler);
    MorphynController.Instance.Unwatch("Player", "hp", myWatcher);
}