Krotox Engine › Docs › Scripting API › Core & Lifecycle

Writing scripts & the lifecycle

How a Krotox C# script is written and attached, every callback and exactly when it runs, the three-pass startup with ExecutionOrder, the per-frame order, what happens when a script throws, and hot reload.

Gameplay in Krotox is written in C#. A script is a public class that derives from KrotoxEngine.Entity. The engine creates one instance per entity that uses it and calls its methods at fixed points in the frame.

Your first script

using KrotoxEngine;

namespace Sandbox
{
    public class PlayerController : Entity
    {
        public float MoveSpeed = 10.0f;     // public fields appear in the inspector
        public float JumpImpulse = 15.0f;

        private Rigidbody2DComponent m_Body;

        // Startup pass 2: set yourself up. Cache components here.
        public override void OnAwake()
        {
            m_Body = GetComponent<Rigidbody2DComponent>();
        }

        // Startup pass 3: every OnAwake has run, so other entities are safe to use.
        void OnCreate()
        {
            Log.Info(quot;Player {Name} ready");
        }

        // Every frame. ts is the frame time in seconds, already scaled by Time.TimeScale.
        void OnUpdate(float ts)
        {
            Vector2 velocity = m_Body.LinearVelocity;
            velocity.X = Input.IsKeyDown(KeyCode.A) ? -MoveSpeed
                       : Input.IsKeyDown(KeyCode.D) ?  MoveSpeed : 0.0f;
            m_Body.LinearVelocity = velocity;

            if (Input.IsKeyPressed(KeyCode.Space))
                m_Body.ApplyLinearImpulse(new Vector2(0.0f, JumpImpulse));
        }
    }
}

Attach it: drag the .cs file from the Content Browser onto the entity, or add a Script component and enter the full class name including the namespace (Sandbox.PlayerController). Press F7 to compile (or let the editor compile on save), then Play.

Create one from a template: right-click in the Content Browser ▸ New C# Script. The template already has OnAwake, OnCreate, OnUpdate and OnDestroy with comments on when to use each.

OnCreate and OnUpdate are not declared on Entity. Write them as plain methods: void OnCreate() and void OnUpdate(float ts). Writing public override void OnCreate() does not compile. The engine finds them by name. The other callbacks (OnAwake, OnFixedUpdate, OnDestroy, the collision callbacks) are virtual and use override.

Callback reference

CallbackDeclare asWhen it runs
OnAwake()public override void OnAwake()Startup pass 2, on every script before any OnEnable or OnCreate. Cache components, register singletons.
OnEnable()public override void OnEnable()Startup pass 2.5, after all OnAwake. Subscribe to events.
OnCreate()void OnCreate()Startup pass 3, after every OnAwake and OnEnable. Safe to talk to other entities.
OnUpdate(float ts)void OnUpdate(float ts)Once per frame. ts is scaled by Time.TimeScale.
OnLateUpdate(float ts)public override void OnLateUpdate(float ts)Once per frame, after OnUpdate, physics and animation. Good for cameras that follow.
OnFixedUpdate()public override void OnFixedUpdate()Before each fixed physics step. Can run 0, 1 or several times in a frame. Read Time.FixedDeltaTime.
OnDisable()public override void OnDisable()Just before the entity is destroyed during Play, right before OnDestroy. Unsubscribe from events.
OnDestroy()public override void OnDestroy()When the entity is destroyed during Play: Destroy(), a destroyed parent, or a hazard zone.
OnCollisionEnter2D(Collision2D c) / OnCollisionStay2D / OnCollisionExit2Dpublic overrideA solid 2D contact began, continues, or ended. With contact details.
OnCollisionEnter(Entity other) / OnCollisionStay / OnCollisionExitpublic overrideThe same contacts, with just the other entity.
OnTriggerEnter(Entity other) / OnTriggerStay / OnTriggerExitpublic overrideA sensor (trigger) overlap began, continues, or ended.
OnJointBreak2D(JointBreak2D info)public overrideA 2D joint on this entity broke.
OnApplicationPause(bool paused)void OnApplicationPause(bool paused)The app went to the background (true) or came back (false). Save here on mobile. Android and iOS.
OnApplicationQuit()void OnApplicationQuit()The app is shutting down, while the scene still exists. Not called when you stop Play in the editor.
Gesture handlers (OnTap, OnSwipe, OnPinch, ...)plain methodsSee Input.
OnDisable and OnDestroy are not called when Play stops or when a scene unloads. Anything you registered outside the scene must be cleaned up by whatever owns it.

UI buttons, sliders, toggles, input fields, input bindings and animation events can also call parameterless public methods on a script by name, set in the inspector.

Startup order: OnAwake, OnEnable, OnCreate

When Play starts, the engine starts every script in separate passes. Each pass finishes for every script before the next begins:

Collect every entity with a Script component
Sort by [ExecutionOrder] (stable: ties keep scene order)
Pass 1    create every C# instance and apply inspector values   (no callbacks)
Pass 2    OnAwake()   on every instance
Pass 2.5  OnEnable()  on every instance
Pass 3    OnCreate()  on every instance

So the rule of thumb is:

[ExecutionOrder(int)] on a class makes it start earlier (lower) or later (higher). Scripts without it use 0. Unity's [DefaultExecutionOrder] is accepted too. The inspector shows the value as an Order badge.

[ExecutionOrder(-100)]
public class GameManager : Entity
{
    public static GameManager Instance { get; private set; }
    public int TargetScore = 100;

    public override void OnAwake() { Instance = this; }
}

public class ScoreLabel : Entity
{
    void OnCreate() { Log.Info(quot;Target: {GameManager.Instance.TargetScore}"); }   // safe
}

Prefabs spawned during Play go through the same passes, for their own subtree, inside the Instantiate call. When Instantiate returns, the new entity's scripts have already run OnAwake and OnCreate.

Per-frame execution order

1. Destroys requested since the last frame are applied
2. Streamed prefabs resolve; transforms update
3. InputManager bindings fire
4. OnUpdate(ts) on every script                       -> pending destroys applied
5. While the physics accumulator has a step:
     OnFixedUpdate() on every script
     Box2D steps; collision and trigger callbacks fire
     pending destroys applied
6. Animation and audio update
7. OnLateUpdate(ts) on every script                   -> pending destroys applied
8. Coroutines advance; Input records this frame's keys -> pending destroys applied

Consequences worth knowing:

When a script throws

An exception in a callback does not stop the game. The engine logs one line naming the script, the entity and the callback, with the full stack trace:

[Script] PlayerController on entity 'Hero' threw in OnUpdate: System.NullReferenceException: ...

If the same script throws five times in a row, its callbacks are paused and retried every 2 seconds; one success resumes it. Everything else keeps running.

Hot reload

Where scripts live

Scripts go under Assets/Scripts/ (any subfolder is fine). They compile into Binaries/<ProjectName>.dll, which references KrotoxEngine-ScriptCore.dll. Add using KrotoxEngine; at the top of every script.

Next