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:
- Build the mechanic as a normal Unity component.
- Create an action or trigger wrapper for the Story System.
- Register it through a custom JSON helper.
- Add the node to your story.
- Test query validation and runtime behavior.
Prefer built-in actions, triggers, and Building Blocks first. Custom nodes are best for project-specific mechanics that need code.
Choose What You Need
| Need | Create |
|---|---|
| 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.
| Method | Why 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.