GameObject Action

GameObject-based actions are the recommended way to create custom actions in State Designer unless profiling shows you need ECS-level optimization. They are easy to author, have a familiar lifecycle, and run inside an Action State.

/// <summary>
/// Callback when the state machine is initialized.
/// </summary>
public virtual void OnAwake()

/// <summary>
/// Callback when the state machine is started.
/// </summary>
public virtual void OnStateMachineStarted()

/// <summary>
/// Callback when the action is started.
/// </summary>
public virtual void OnStart()

/// <summary>
/// Executes the action logic.
/// </summary>
/// <returns>The status of the action.</returns>
public virtual StateStatus OnUpdate()

/// <summary>
/// Callback when the action stops.
/// </summary>
public virtual void OnEnd()

/// <summary>
/// Callback when the state machine is stopped.
/// </summary>
/// <param name="pause">Is the state machine paused?</param>
public virtual void OnStateMachineStopped(bool pause)

/// <summary>
/// Callback when the state machine is destroyed.
/// </summary>
public virtual void OnDestroy()

A custom GameObject action derives from Opsive.StateDesigner.Runtime.Actions.Action.

Physics Callbacks

For performance, physics callbacks are only registered when needed. Override the corresponding receive property with true:

  • ReceiveCollisionEnterCallback
  • ReceiveCollisionExitCallback
  • ReceiveCollisionEnter2DCallback
  • ReceiveCollisionExit2DCallback
  • ReceiveTriggerEnterCallback
  • ReceiveTriggerExitCallback
  • ReceiveTriggerEnter2DCallback
  • ReceiveTriggerExit2DCallback
  • ReceiveControllerColliderHitCallback

Then implement the callback method (OnTriggerEnterOnCollisionEnter, etc.).

Save/Load API

If your action needs runtime persistence, implement save/load:

/// <summary>
/// Specifies the type of reflection that should be used to save the action.
/// </summary>
/// <param name="index">The index of the sub-action.</param>
public virtual MemberVisibility GetSaveReflectionType(int index)

/// <summary>
/// Returns the current action state.
/// </summary>
/// <param name="world">The DOTS world.</param>
/// <param name="entity">The DOTS entity.</param>
/// <returns>The current action state.</returns>
public virtual object Save(World world, Entity entity)

/// <summary>
/// Loads the previous action state.
/// </summary>
/// <param name="saveData">The previous action state.</param>
/// <param name="world">The DOTS world.</param>
/// <param name="entity">The DOTS entity.</param>
public virtual void Load(object saveData, World world, Entity entity)

MemberVisibility options:

  • MemberVisibility.All: Save public and private fields via reflection.
  • MemberVisibility.Public: Save public and serialized private fields via reflection.
  • MemberVisibility.None: No reflection-based save. Implement Save and Load manually.

Example: Move Towards a Random Point

This action chooses a random destination in a radius around a center, moves toward it, and finishes once it arrives. It also saves/restores the chosen destination.

using Opsive.GraphDesigner.Runtime;
using Opsive.GraphDesigner.Runtime.Variables;
using Opsive.Shared.Utility;
using Opsive.StateDesigner.Runtime.Actions;
using Opsive.StateDesigner.Runtime.States;
using Unity.Entities;
using UnityEngine;

[Category("Movement")]
[NodeDescription("Moves the agent towards a random position within the specified radius.")]
[NodeIcon("Assets/MyIcon.png")]
public class MoveTowardsRandomPoint : Action
{
    [Tooltip("The center point of the random position.")]
    [SerializeField] protected SharedVariable<Vector3> m_Center;
    [Tooltip("The radius that contains the random position.")]
    [SerializeField] protected SharedVariable<float> m_Radius = 10f;
    [Tooltip("The speed that the agent should move towards the destination.")]
    [SerializeField] protected SharedVariable<float> m_MoveSpeed = 5f;
    [Tooltip("Distance threshold used to consider the destination reached.")]
    [SerializeField] protected SharedVariable<float> m_ArriveDistance = 0.5f;

    private Vector3 m_Destination;

    /// <summary>
    /// Callback when the action is started.
    /// </summary>
    public override void OnStart()
    {
        m_Destination = m_Center.Value + Random.insideUnitSphere * Random.Range(0f, m_Radius.Value);
    }

    /// <summary>
    /// Executes the action logic.
    /// </summary>
    /// <returns>The status of the action.</returns>
    public override StateStatus OnUpdate()
    {
        if (Vector3.Distance(transform.position, m_Destination) <= m_ArriveDistance.Value) {
            return StateStatus.Finished;
        }

        transform.position = Vector3.MoveTowards(transform.position, m_Destination, m_MoveSpeed.Value * Time.deltaTime);
        return StateStatus.Running;
    }

    /// <summary>
    /// Specifies the type of reflection that should be used to save the action.
    /// </summary>
    public override MemberVisibility GetSaveReflectionType(int index)
    {
        // Save manually.
        return MemberVisibility.None;
    }

    /// <summary>
    /// Returns the current action state.
    /// </summary>
    public override object Save(World world, Entity entity)
    {
        return m_Destination;
    }

    /// <summary>
    /// Loads the previous action state.
    /// </summary>
    public override void Load(object saveData, World world, Entity entity)
    {
        m_Destination = (Vector3)saveData;
    }
}

That’s all you need for a typical custom GameObject action.