Create a GameObject Condition when a transition needs a game-specific yes-or-no check based on scene objects, Unity components, input, or other familiar gameplay code. This is the recommended custom Condition workflow unless profiling shows that the check needs ECS-specific optimization.

Add the Condition to a transition

  1. Create a class that derives from Opsive.StateDesigner.Runtime.Conditions.Condition.
  2. Implement IsValid() so the Condition reports whether the transition is currently allowed.
  3. After Unity compiles the script, select the transition in the State Designer graph.
  4. Add the new Condition from the Conditions list in the Element Inspector.
  5. Configure its serialized fields for that transition. The HasEnteredTrigger example below exposes an optional Tag filter.

The custom Condition appears as a reusable choice on transitions. It does not perform the gameplay behavior itself; it only reports whether its rule is valid.

How it runs

State Designer evaluates the Condition according to the transition’s Evaluation Mode. When IsValid() returns true, the Condition allows that transition to proceed if its other configured requirements are also satisfied.

In the included HasEnteredTrigger example, OnStart() clears the previous result. A matching trigger sets the result to true, and IsValid() returns that stored result. Leaving the Tag empty accepts any trigger.

Verify the Condition

  1. Add HasEnteredTrigger to a transition and optionally set its Tag.
  2. Enter Play Mode while the transition’s source State is active.
  3. Enter the trigger with a matching GameObject and confirm that the transition can occur.
  4. If a Tag is configured, enter with a nonmatching GameObject and confirm that the Condition remains invalid.

GameObject Condition API

A custom condition can implement the following callbacks:

/// <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 condition starts.
/// </summary>
public virtual void OnStart()

/// <summary>
/// Is the condition valid? If it is valid then the transition will occur.
/// </summary>
/// <returns>True if the condition is valid.</returns>
public abstract bool IsValid()

/// <summary>
/// Callback when the condition 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

Physics callbacks are opt-in for efficiency. If your Condition needs one, 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/Load API

Conditions support runtime 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: HasEnteredTrigger

This Condition returns true after a matching trigger is entered.

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

[Description("Returns true after a matching trigger is entered.")]
public class HasEnteredTrigger : Condition
{
    [Tooltip("Optional tag filter. Leave empty to accept any trigger.")]
    [SerializeField] protected SharedVariable<string> m_Tag;

    private bool m_EnteredTrigger;

    protected override bool ReceiveTriggerEnterCallback => true;

    /// <summary>
    /// Callback when the condition starts.
    /// </summary>
    public override void OnStart()
    {
        m_EnteredTrigger = false;
    }

    /// <summary>
    /// Is the condition valid?
    /// </summary>
    public override bool IsValid()
    {
        return m_EnteredTrigger;
    }

    /// <summary>
    /// Trigger enter callback.
    /// </summary>
    protected override void OnTriggerEnter(Collider other)
    {
        if (!string.IsNullOrEmpty(m_Tag.Value) && !other.gameObject.CompareTag(m_Tag.Value)) {
            return;
        }

        m_EnteredTrigger = true;
    }

    /// <summary>
    /// Save manually.
    /// </summary>
    public override MemberVisibility GetSaveReflectionType(int index)
    {
        return MemberVisibility.None;
    }

    public override object Save(World world, Entity entity)
    {
        return m_EnteredTrigger;
    }

    public override void Load(object saveData, World world, Entity entity)
    {
        m_EnteredTrigger = (bool)saveData;
    }
}