Create a custom GameObject Action when an Action State needs to perform project-specific work that the built-in Actions do not provide. This is the usual workflow for behavior that works with scene objects and Unity components, such as a movement rule, animation command, sound, or interaction.

Start with a GameObject Action unless profiling shows that this behavior needs an ECS implementation for a large number of entities.

Create the Action

  1. Create a C# script for one focused behavior. The example on this page uses MoveTowardsRandomPoint.
  2. Derive the class from Opsive.StateDesigner.Runtime.Actions.Action.
  3. Add serialized fields for the values that should be configurable in the editor. Use Shared Variables when another Action or state should share those values.
  4. Use OnStart for setup that should happen when the Action begins.
  5. Implement OnUpdate and return StateStatus.Running while the work continues or StateStatus.Finished when it is complete.
  6. Let Unity compile the script before opening the Action selector.

The complete movement example appears later on this page, after the editor workflow and optional APIs.

Add and configure the Action in the editor

  1. Add or select an Action State in the graph.
  2. In the Element Inspector, use the Actions list’s add control and search for Move Towards Random Point.
  3. Select the Action from the Movement category. The example’s Category attribute places it there, while its Description and NodeIcon attributes provide editor details.
  4. Configure Center, Radius, Move Speed, and Arrive Distance. Each field can use its local value or a compatible Shared Variable.
  5. Choose the Action State’s Sequence or Parallel execution type according to the other Actions in that state.

After it is added, the custom Action appears in the Action State’s ordered Actions list and can be reused in other Action States with different values.

How it runs

When the Action starts, MoveTowardsRandomPoint chooses a destination within the configured radius around Center. During each update, it moves the GameObject toward that destination and returns Running.

When the GameObject comes within Arrive Distance, the Action returns Finished. In a Sequence, the Action State can then continue to its next Action. In Parallel, the Action State waits for its other enabled Actions to finish as well.

Lifecycle callbacks

A custom GameObject Action derives from Opsive.StateDesigner.Runtime.Actions.Action. Override only the callbacks that the behavior needs:

/// <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()

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 matching callback method, such as OnTriggerEnter or OnCollisionEnter.

Save and load

Add save and load support only when the Action has runtime state that must survive a save. The available API is:

/// <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)

Choose the reflection behavior with MemberVisibility:

  • MemberVisibility.All: Save public and private fields through reflection.
  • MemberVisibility.Public: Save public and serialized private fields through reflection.
  • MemberVisibility.None: Do not save fields through reflection. Implement Save and Load manually.

Complete 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 and 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")]
[Description("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;
    }
}

Verify in Play Mode

Add MoveTowardsRandomPoint to an Action State on a GameObject, assign Center, and use visible Radius, Move Speed, and Arrive Distance values. Enter Play Mode and activate that state.

Confirm that the Action is highlighted while the GameObject moves and that the GameObject stops within Arrive Distance of its chosen destination. When the Action finishes, the highlight should clear; a Sequence can then start its next Action, and a transition configured for state completion can leave the state. If the Action is not available in the selector, first resolve any script compilation errors and confirm that the class derives from the State Designer Action type.