Creating custom behaviors

Updated on

August 19, 2022

In this tutorial, you will learn how to create custom behaviors for VR Builder. It is intended to be read by Unity / C# developers. If you are new to coding, you can still follow the guide but many concepts might not be clear for you directly. We also included some links for further readings that might help you.

In addition to following this step-by-step guide, you can download the final behavior for reference.

Introduction

A behavior is something that just happens during the run of the VR application at a given step. This can be used for instance to guide a user in VR (i.e. visual or acoustic hints), or to prepare the scene for the next task (animating, changing, en/disabling objects, etc.). For example, if a VR user has to interact with an object, a behavior could move that object closer, highlight it, or play an audio file with instructions.

Technical foundational concepts

In VR Builder, all entities form a tree-like structure. For instance, steps are children of chapters, behaviors and transitions are children of steps, and conditions belong to transitions. Parent entities have full control over their children, notably their life cycles.

When a step starts activating, it activates its behaviors. After all behaviors are active, it activates transitions. Transitions, in turn, activate their conditions. The step checks until all conditions of any transitions complete. Then it starts deactivating, and deactivates the behaviors.

This means that if you want to do something at the beginning of a step, you have to implement its Activating stage process. If you want to do something at the end of the step after conditions are met, implement the Deactivating stage process.

Use behaviors to prepare the environment for conditions, or to clean it up afterwards. We do not support behaviors with Active stage processes. We advise against implementing them, as we have never tested that case.

Our example use case - creating a "Scale Object" behavior

As the example for this tutorial, we will create a "Scale Object" behavior.

It can be used, for instance, to spawn a new object by increasing the scale from 0 to 1. Since it does not instantly appear but increases over a short time, this will feel more natural to the user VR.

We will also make the behavior versatile by making the target scale and duration of the animation parameters. This way, the same behavior can be used to let objects disappear, become smaller, or become bigger as well.

Let's get started!

Behavior Data

We need to define the behavior's data. It would contain a target scale, an animation's duration, and a reference to a target object. Assuming that we want to scale an object at the beginning of a step, we need to implement the stage process for the Activating stage, which would change the scale of the object every frame. When it has to fast-forward, then it must set the scale to the target value immediately.

Data

We need a C# class file which will contain the data class.

The Unity Editor only recognizes files inside the project's Assets folder. Create it there. Name it ScalingBehaviorData or any other way you prefer, and make sure to change the file's extension to .cs.

Open this file in your favorite IDE or text editor. Make sure that the file is empty. Insert the following:

----------------------------------------
using UnityEngine;
using System.Runtime.Serialization;
using VRBuilder.Core;
using VRBuilder.Core.Attributes;
using VRBuilder.Core.Behaviors;
using VRBuilder.Core.SceneObjects;
----------------------------------------

This way we declare which namespaces we use in this file. Leave this list untouched throughout this subsection.

Now, we have to declare it, as well as the properties that we will need. A data class of any behavior has to implement the IBehaviorData interface.

----------------------------------------
// When you implement a data class for a behavior, you have to implement the IBehaviorData interface.
public class ScalingBehaviorData : IBehaviorData
{
    // The object to scale.
    public SceneObjectReference Target { get; set; }

    // Target scale.
    public Vector3 TargetScale { get; set; }

    // Duration of the animation in seconds.
    public float Duration { get; set; }

    // Interface members:

    // Any IData has to implement the Metadata property.
    // We use it internally in the Step Inspector.
    public Metadata Metadata { get; set; }

    // Any behavior has a name that can be set in the Step Inspector.
    public string Name { get; set; }
}
----------------------------------------

Serialization

Any data object has to be serializable. This is needed so that VR Builder can persist the information and the compiled application can run the process by deserializing our data. VR Builder uses the same mechanism to display on and store changes made in the Workflow Editor and Step Inspector.

This is why the Target property is a SceneObjectReference and not an ISceneObject: We cannot serialize scene objects, but we can serialize references to them. To learn more about this, please check out this article about properties.

To mark classes and properties as serializable, we use [DataContract] and [DataMember] attributes from the System.Runtime.Serialization namespace.

----------------------------------------
// Declare the class to be serializable (so we could save and load it).
// The parameter is mandatory.
[DataContract(IsReference = true)]
public class ScalingBehaviorData : IBehaviorData
{
    [DataMember]
    public SceneObjectReference Target { get; set; }

    [DataMember]
    [DisplayName("Target Scale")]
    public Vector3 TargetScale { get; set; }

    [DataMember]
    [DisplayName("Animation Duration")]
    public float Duration { get; set; }

    public Metadata Metadata { get; set; }

    public string Name { get; set; }
}
----------------------------------------

Displaying names

We could use the data class already, but the Step Inspector would label the TargetScale property without spacing between the two words. To fix it, use the [DisplayName] attribute from the VRBuilder.Core.Attributes namespace:

----------------------------------------
// The step inspector draws data objects, not entities.
// This is why we have to attribute the data class.
[DisplayName("Scale Object")]
[DataContract(IsReference = true)]
public class ScalingBehaviorData : IBehaviorData
{
    [DataMember]
    public SceneObjectReference Target { get; set; }

    [DataMember]
    [DisplayName("Target Scale")]
    public Vector3 TargetScale { get; set; }

    [DataMember]
    [DisplayName("Animation Duration")]
    public float Duration { get; set; }

    public Metadata Metadata { get; set; }

    public string Name { get; set; }
}
----------------------------------------

The StageProcess

Now we need to define the process of the scaling behavior. It will read and modify the data as it will take the target and apply a new scale to it over a given duration in seconds.

Assuming that we want to scale an object at the beginning of a step, we need to implement the stage process of the Activating stage. If we wanted to scale an object at the end, we would implement a Deactivating stage process. If you want the VR designer to choose, check the source code of the PlayAudioBehavior.cs.

IStageProcess is a generic interface. To make a stage process compatible with a data class, you have to pass the data class as a generic parameter of the interface when implementing it.

As in the previous section, prepare a new C# class file. Name it ScalingBehaviorActivatingProcess.cs. Make sure it is empty, and paste the following:

----------------------------------------
// Declare namespaces to use, as in the previous subsection.
using VRBuilder.Core;
using System.Collections;
using UnityEngine;

// We have to declare the type of data which this process can modify.
// We do it by implementing the generic IStageProcess<TData> interface.

public class ScalingBehaviorActivatingProcess : StageProcess<ScalingBehaviorData>
{
    // Always runs when we enter this stage.
    public override void Start()
    {
    }

    // Starting from the next frame,
    // the Innoactive Creator will call it every frame until it will complete it.
    public override IEnumerator Update()
    {
    }

    // Always runs whenever we have finished the Update or executed the FastForward.
    public override void End()
    {
    }

    // We call it when we had no time to complete Update,
    // so we have to imitate it.
    public override void FastForward()
    {
    }

    public ScalingBehaviorActivatingProcess(ScalingBehaviorData data) : base(data)
    {
    }
}
----------------------------------------

We just declared all four methods that belong to the IStageProcess interface. Now we have to write the code that will scale the target scene object.

At the start of the stage process, we will record the current time and initial scale. The target, as any other Unity scene object, has a transform that stores its position, rotation, and scale. We will retrieve the reference to it, too.

In Update(), we will linearly interpolate between the initial and target scale depending on the time passed: every frame, the object will shrink or grow towards the target scale. Afterwards, we will set the scale to the precise value in the End() method. If the stage process has to fast-forward, the Update() method will not iterate completely. We will handle it in the FastForward() method by assigning the target scale to the object.

----------------------------------------
public class ScalingBehaviorActivatingProcess : StageProcess<ScalingBehaviorData>
{
    private float startedAt;
    private Transform scaledTransform;
    private Vector3 initialScale;

    public override void Start()
    {
        scaledTransform = Data.Target.Value.GameObject.transform;
        startedAt = Time.time;
        initialScale = scaledTransform.localScale;
    }

    public override IEnumerator Update()
    {
        while (Time.time - startedAt < Data.Duration)
        {
            float progress = (Time.time - startedAt) / Data.Duration;

            scaledTransform.localScale = Vector3.Lerp(initialScale, Data.TargetScale, progress);

            yield return null;
        }
    }

    public override void End()
    {
        scaledTransform.localScale = Data.TargetScale;
    }

    public override void FastForward()
    {
    }

    public ScalingBehaviorActivatingProcess(ScalingBehaviorData data) : base(data)
    {
    }
}
----------------------------------------

Assembling the Behavior

Prepare a new C# class file for the behavior class with the following:

----------------------------------------
using System.Runtime.Serialization;
using UnityEngine;
using VRBuilder.Core;
using VRBuilder.Core.Attributes;
using VRBuilder.Core.Behaviors;
using VRBuilder.Core.SceneObjects;
----------------------------------------

To create a behavior, we have to inherit from the Behavior abstract class and define the data and the process.

The base class has the basic Data property already. Create an instance of your data class in the behavior's constructor and assign default values to its fields.

To define a process, you have to override a method that corresponds to the target stage: GetActivatingProcess(), GetActiveProcess(), or GetDeactivatingProcess(). An entity could contain multiple processes, up to one per stage. By default, these methods return EmptyStageProcess processes, which do nothing. An entity always expects a process instance from these calls: they should never return null.

----------------------------------------
// We have to declare the behavior as a data contract, too,
// so that the serializer can reach the data.
[DataContract(IsReference = true)]
// Inherit from the abstract Behavior<TData> class and define which type of data it uses.
public class ScalingBehavior : Behavior<ScalingBehaviorData>
{
    // Any serializable class must include a public parameterless constructor.
    // (Classes with no declared constructors have one by default).
    // Setup the Data property here.
    public ScalingBehavior()
    {
        // The default base constructor has created the Data object already.
        // Now we need to set up its values.
        Data.Duration = 0f;
        Data.TargetScale = Vector3.one;
        // Make sure to always initialize scene object references.
        Data.Target = new SceneObjectReference("");
    }

    // Each entity has three virtual methods where you can declare the stage process
    // that that entity should use.
    // By default, these methods return empty processes that do nothing.
    public override IProcess GetActivatingProcess()
    {
        // Always return a new instance of a stage process.
        return new ScalingBehaviorActivatingProcess(Data);
    }
}
----------------------------------------

This behavior is not displayed in the Step Inspector yet. We will explain how to do it in the section "Creating menu items".

Keeping the code clean

Since you need at least three classes per behavior, we recommend to declare data and processes as private classes within the corresponding behavior class:

----------------------------------------
[DataContract(IsReference = true)]
public class ScalingBehavior : Behavior<ScalingBehavior.EntityData>
{
    [DisplayName("Scale Object")]
    [DataContract(IsReference = true)]
    public class EntityData : IBehaviorData
    {
        // Implementation omitted.
    }

    private class ActivatingProcess : Process<EntityData>
    {
        // Implementation omitted.
    }

    // Implementation omitted.
}
----------------------------------------

Creating menu items

If you click on an "Add Behavior" or "Add Condition" button in the Step Inspector, it will display a list of available options. If you select one of them, it will add a new behavior or condition to the step.

We have created a fully functional behavior in the previous chapter, but we still miss it in the list. "Add Behavior" and "Add Condition" buttons do not display behaviors or conditions directly. Instead, they display menu items. A menu item defines the label to display and the way how it creates a new entity for a step. VR designers would be able to use our behavior only after we create a menu item for it.

A menu item is an instance of a class that inherits from MenuItem within the VRBuilder.Editor.UI.StepInspector.Menu namespace. A menu item class is an Editor script, so you must create a file for it under the Editor subfolder of the Assets folder.

The MenuItem class is generic. Use IBehavior as the generic parameter. Once the class compiles, the Step Inspector will find it on its own.

The base class declares one property and one method that you have to implement.

The Step Inspector uses the DisplayedName property as a label. This property returns a string. If you use forward slashes ("/") in it, the Step Inspector will split it into submenus.

When a user selects one of the items, the Step Inspector calls the GetNewItem() method and adds the result to the list of entities.

Call the new file ScalingBehaviorMenuItem.cs and copy the following:

----------------------------------------
using VRBuilder.Core.Behaviors;
using VRBuilder.Editor.UI.StepInspector.Menu;

public class ScalingBehaviorMenuItem : MenuItem<IBehavior>
{
    /// <inheritdoc />
    public override string DisplayedName { get; } = "Tutorial/Scale Behavior";

    /// <inheritdoc />
    public override IBehavior GetNewItem()
    {
        return new ScalingBehavior();
    }
}
----------------------------------------

If you have named your behavior in a different way or placed it under a namespace, adjust the code accordingly.

Open the Unity Editor, let the changes compile, and then click the "Add Behavior" button in the Step Inspector. You will see the menu item in the list.

You can create multiple menu items for a single behavior. If you modify the behavior's data in the GetNewItem() method, you will effectively create different presets of it.

Adding a help button

Each behavior or condition of our base template has a help button in their header. The button is linked to a webpage. If you would like to add your own link, add a [HelpLink] attribute above the behavior class.

----------------------------------------
[DataContract(IsReference = true)]
[HelpLink("https://www.mindport.co/vr-builder-tutorials/creating-custom-behaviors")]
public class ScalingBehavior: Behavior<ScalingBehavior.EntityData>
{
    //Your Behavior code as shown above
}
----------------------------------------

Next Steps

What custom behaviors and conditions are you planning to implement? Join our community to discuss with other VR Builder developers on how to do it and share your extensions! Maybe someone built something similar already that you can pick up on.

Helpful links

Download Vr Builder