Skip to main content

Create Custom Actions and Triggers

Use this guide when the built-in Actions and Triggers do not cover a project-specific mechanic.

The usual flow is:

  1. Build the mechanic as a normal Unity component.
  2. Create an action or trigger wrapper for the Story System.
  3. Register it through a custom JSON helper.
  4. Add the node to your story.
  5. Test query validation and runtime behavior.
info

Prefer built-in actions, triggers, and Building Blocks first. Custom nodes are best for project-specific mechanics that need code.

Choose What You Need

NeedCreate
The story should make something happen.Custom action.
The story should wait for a condition or event.Custom trigger.
The logic is reusable outside the story.A normal Unity mechanic component plus a thin action/trigger wrapper.

1. Build the Mechanic Component First

Start with regular Unity code. Keep the mechanic independent from the story system where possible.

using UnityEngine;
using UnityEngine.Events;

public class WrenchRotator : MonoBehaviour
{
[SerializeField] private float requiredAngle = 90f;

public UnityEvent OnRequiredAngleReached;

public void SetToRequiredAngle()
{
transform.localRotation = Quaternion.Euler(0f, requiredAngle, 0f);
OnRequiredAngleReached?.Invoke();
}
}

This keeps the mechanical behavior easy to test in Unity before you wire it into a story.

2. Create a Custom Action

Create an action when the story needs to run your mechanic.

using UnityEngine;
using VRseBuilder.Core.Framework;

public class SetWrenchAngleAction : PlayableAction
{
private WrenchRotator wrenchRotator;

public override void Deserialize(Node node)
{
base.Deserialize(node);
wrenchRotator = node.TargetGameObject.GetComponent<WrenchRotator>();
}

public override void OnBegin()
{
wrenchRotator.SetToRequiredAngle();
OnEnd();
}

public override bool DoesRequireQuery() => true;

public override bool IsQueryValid(out string validationMessage)
{
if (node.TargetGameObject == null)
{
validationMessage = "SetWrenchAngleAction requires a target object.";
return false;
}

if (!node.TargetGameObject.TryGetComponent<WrenchRotator>(out _))
{
validationMessage = "Target object must have WrenchRotator.";
return false;
}

validationMessage = string.Empty;
return true;
}
}

3. Create a Custom Trigger

Create a trigger when the story needs to wait for your mechanic.

using VRseBuilder.Core.Framework;

public class WrenchAngleReachedTrigger : PlayableTrigger
{
private WrenchRotator wrenchRotator;

public override void Deserialize(Node node)
{
base.Deserialize(node);
wrenchRotator = node.TargetGameObject.GetComponent<WrenchRotator>();
}

public override void OnBegin()
{
wrenchRotator.OnRequiredAngleReached.AddListener(OnCompleteAction);
InvokeOnBeginEvent();
}

private void OnCompleteAction()
{
wrenchRotator.OnRequiredAngleReached.RemoveListener(OnCompleteAction);
OnEnd();
}

public override bool DoesRequireQuery() => true;

public override bool IsQueryValid(out string validationMessage)
{
if (node.TargetGameObject == null)
{
validationMessage = "WrenchAngleReachedTrigger requires a target object.";
return false;
}

if (!node.TargetGameObject.TryGetComponent<WrenchRotator>(out _))
{
validationMessage = "Target object must have WrenchRotator.";
return false;
}

validationMessage = string.Empty;
return true;
}
}

4. Register Custom Nodes

VRseBuilder looks for a class named VRseBuilder.Core.Framework.CustomJsonHelper and calls its public static methods for unknown action or trigger names.

Create a CustomJsonHelper in that namespace:

using VRseBuilder.Core.Framework;

namespace VRseBuilder.Core.Framework
{
public static class CustomJsonHelper
{
public static PlayableAction GetCustomPlayableActionObjectFromObject(Node node, bool deserialize = true)
{
PlayableAction action = node.Name switch
{
"SetWrenchAngleAction" => new SetWrenchAngleAction(),
_ => null
};

if (action != null && deserialize)
{
action.Deserialize(node);
}

return action;
}

public static PlayableTrigger GetCustomPlayableTriggerObjectFromObject(Node node, bool deserialize = true)
{
PlayableTrigger trigger = node.Name switch
{
"WrenchAngleReachedTrigger" => new WrenchAngleReachedTrigger(),
_ => null
};

if (trigger != null && deserialize)
{
trigger.Deserialize(node);
}

return trigger;
}
}
}

5. Use the Node in a Story

Your story node should use the custom class name as Name.

Custom action JSON example
{
"Name": "SetWrenchAngleAction",
"Query": "Wrench",
"Option": "",
"Data": "{}",
"Type": 0
}
Custom trigger JSON example
{
"Name": "WrenchAngleReachedTrigger",
"Query": "Wrench",
"Option": "",
"Data": "{}",
"Type": 1
}

Query and Validation Rules

Every custom node should clearly define what Query means.

MethodWhy it matters
DoesRequireQuery()Tells the story system whether this node needs a target object.
IsQueryValid(out string validationMessage)Gives users a useful error when the target object or component is missing.
Deserialize(Node node)Reads Query, Option, Data, and resolves scene references.

Best Practices

  • Keep mechanic logic in normal Unity components.
  • Keep action/trigger classes thin.
  • Always unsubscribe trigger listeners in completion, skip, reset, or pause paths where needed.
  • Use clear user-facing node names.
  • Validate missing components with helpful messages.
  • Document what Query, each option, and each data field means if the node becomes reusable.