The VTI World Kit is designed to be easily extensible by both world designers and programmers alike. To this end, custom Drivers are highly recommended and are built to be fairly simple to create. This article serves as a tutorial on how to create a custom Driver. It will assume basic knowledge of UdonSharp, and will not cover Udon Graph at all (as I think quite poorly of the interface and believe it takes several orders of magnitude more effort and time to do something you can do in just a few lines of C#).
In this tutorial, we will be replicating the VTIObjectToggle Driver as it demonstrates all the core tenets of how VTI Drivers function.
Create the Script
Create a new U# script and open it in your IDE of choice. Change the inherited class from UdonSharpBehaviour to VTIDriverBase. This will let it work with the VTI World Kit and includes a bunch of necessary methods and members to simplify our Driver setup.
public class CustomDriver : VTIDriverBase
Next, we need our control variables so the script can function. As we are turning a GameObject on for a period of time, and then off after it, add a public property for a GameObject a float for the time it will take to reset. For sanity's sake, also add a call in the Start() method to turn the Target off, to make sure it won't fire when the level loads.
public class CustomDriver : VTIDriverBase
{
public GameObject Target;
public float ResetTime = 5.0f;
void Start()
{
Target.SetActive(false);
}
}
Add Core Functionality
The simplest possible Driver only needs to implement the VTIEventPlay()
method. This is what is called when an Event is received, all checks have passed, and VTI is attempting to Fire this Driver's Target.
Add a public override void method called VTIEventPlay(). Clear out the base.VTIEventPlay();
line if your IDE added it automatically.
public override void VTIEventPlay()
{
}
Next, we need to make the Driver actually do the thing we want it to do, which is to turn on a GameObject, wait, and then turn it off. The first thing we'll need is to call Target.SetActive(true)
in our new method. Then, create another method that will do the exact opposite and call Target.SetActive(false)
.
public override void VTIEventPlay()
{
Target.SetActive(true);
}
public void ResetTarget()
{
Target.SetActive(false);
}
For the core of our functionality, we only need to do one more thing: make VTIEventPlay()
call ResetTarget()
after a delay. We can do this simply by utilizing the built-in SendCustomEventDelayedSeconds()
method. Add that into VTIEventPlay()
and have it wait for ResetTime
seconds.
SendCustomEventDelayedSeconds(nameof(ResetTarget), ResetTime);
As an aside, note the usage of nameof
instead of just putting "ResetTarget"
in the first argument. This allows for a measure of safety when maintaining projects later on, as if you need to rename ResetTarget()
for whatever reason, you can utilize something like Visual Studio's Rename function and not have to worry about manually typing the name change into all the Custom Event calls.
At this end of this section, this is how our script looks:
using UnityEngine;
public class CustomDriver : VTIDriverBase
{
public GameObject Target;
public float ResetTime = 5.0f;
void Start()
{
Target.SetActive(false);
}
public override void VTIEventPlay()
{
Target.SetActive(true);
SendCustomEventDelayedSeconds(nameof(ResetTarget), ResetTime);
}
public void ResetTarget()
{
Target.SetActive(false);
}
}
Dealing With Readiness
For the most basic of Drivers, the Cooldown timer is automatically handled by VTITarget. However, in this case, we're turning an object on and telling VRChat to turn it back off after an arbitrary delay. In some circumstances, you may want your Drivers to be able to overlap themselves when triggered multiple times. But in this case, we have a Custom Event running on a timer, which can cause some level of havoc if we don't tell VTI that we're not ready to Fire again.
There are two ways to do this: The IsReady
variable, and adding in a custom VTICheckReady()
method. The former method is exceptionally simple, and all we really need for this tutorial, so we'll cover that first.
The IsReady Boolean
The IsReady
boolean serves as a bit of a "Green Light" to signal the VTI World Kit that this Driver is ready to go or not. Right now, we need to turn that light "red" while we wait for the Custom Event timer to elapse, and then turn it back to "green" when we're done. To do this, simply set IsReady = false;
in VTIEventPlay()
, and set it back to true
in ResetTarget()
.
public override void VTIEventPlay()
{
IsReady = false;
Target.SetActive(true);
SendCustomEventDelayedSeconds(nameof(ResetTarget), ResetTime);
}
public void ResetTarget()
{
IsReady = true;
Target.SetActive(false);
}
And that's it! By default, VTICheckReady()
will simply return the value of IsReady
unless overridden, so for the purposes of this script, we don't even need to do anything more than this!
VTICheckReady()
However, what if we need to be more complex in our checks to see if we can Fire again or not? In this case, a simple boolean may not be enough. For the purposes of this tutorial, this step is optional, and will only serve as an example of how this can be done another way.
There's not really much we need to check in a Driver as simple as this, so let's just be super paranoid about the state of the Target GameObject and ensure that it got turned off before allowing VTI to Fire this again. To do this, first add in an override for VTICheckReady()
.
Next, replace the auto-generated "base" call (if your IDE made one) with a simple expression to return true
if both IsReady
is true
and Target is not currently active. This can look like the below:
public override bool VTICheckReady()
{
return IsReady && !Target.activeSelf;
}
Basic Driver Complete
And that's basically it! This Driver can now be attached to a VTITarget like any of the base Drivers that came with the VTI World Kit. Our simple Driver code should now look like this:
using UnityEngine;
public class CustomDriver : VTIDriverBase
{
public GameObject Target;
public float ResetTime = 5.0f;
void Start()
{
Target.SetActive(false);
}
public override void VTIEventPlay()
{
IsReady = false;
Target.SetActive(true);
SendCustomEventDelayedSeconds(nameof(ResetTarget), ResetTime);
}
public void ResetTarget()
{
IsReady = true;
Target.SetActive(false);
}
}
Next Steps
Now while we have a completed Driver that does what it needs to do, there are still more things that can be done to improve it. Read on to learn how to handle User Messaging and Dynamic Rebinding.
User Messaging
With the VTIDriverBase base class, User Messaging is already available for any custom Driver, it needs only to be tapped into. The base class includes the following four members that are used to handle Event data:
public string UserName
public string Message
public string TriggerCause
public int TriggerAmount
The latter two variables are for dynamically handling different Triggers and will be covered in the next section. This section concerns the first two strings, UserName and Message. These will automatically be populated by the VTI World Kit when VTIEventPlay()
is called, and can be used at that point to populate any effect you wish your Driver to handle.
For the purposes of this tutorial, we are simply going to put the User Messaging contents into a couple TextMeshPro elements and call it a day. Because the Message can be a lot longer than the UserName, we're going to use two TMP elements here instead of one, so we can make the two elements different sizes.
Add in two public properties for TextMeshPro elements:
public TextMeshPro UserNameText;
public TextMeshPro MessageText;
Next, we will need to expand our VTIEventPlay()
a little bit to actually print out the User Messaging. We can do that simply by setting the TextMeshPro.text
property, but on top of that, we generally want Drivers to be flexible, so we'll wrap them in validity checks as well just in case this is used on a Target that doesn't utilize User Messaging:
public override void VTIEventPlay()
{
IsReady = false;
if (Utilities.IsValid(UserNameText))
{
UserNameText.text = UserName;
}
if (Utilities.IsValid(MessageText))
{
MessageText.text = Message;
}
Target.SetActive(true);
SendCustomEventDelayedSeconds(nameof(ResetTarget), ResetTime);
}
And once again, that's as simple as it needs to be!
Dynamic Rebinding
For the final section of this tutorial, we'll be handling the most complicated (and optional) part of making a custom Driver: dealing with a Target that has been rebound by the user when Allow Event Type Rebinding is enabled. Now, I say complicated, but purely because this part requires just a modicum of thought put into how to handle the different modes.
For our purposes, we're simply going to change up the UserName line to say what the user did to trigger the Event. That is, if the Streamer changed the binding from CHANNEL_POINT_REDEEM to CHEER, we want to put "[username] cheered [x] bits" in the UserNameText element. The way you would detect the type of binding used is with the TriggerCause
string from VTIDriverBase.
From there, we can use the TriggerAmount
value to show how "much" the user did whatever they did to trigger the Event, i.e. how many bits, what tier they subscribed at, how many gifted subs, etc.
An Important Note About TriggerCause
Now, I must make a note about how TriggerCause
works: due to an attempt at future proofing, the TriggerCause
includes the origin service name at the front of the string, as I would like to support other Streaming platforms like YouTube or TikTok at some point in the future, and these services all have different methods of interacting with the stream. For instance, if the Target is bound to the FOLLOW event type, the TriggerCause
will be set to "TWITCH_FOLLOW"
.
The way I handled it in VTIObjectToggle was to simply have a method with a Switch-Case statement that returns a suffix string for each supported event type. This is not the only way to do this, but it worked for my purposes and so we'll do it here. Add in the following method:
private string GetEventVerb(int amount = 0)
{
switch (TriggerCause)
{
case "TWITCH_FOLLOW":
return " followed";
case "TWITCH_SUBSCRIBE":
return " subscribed (Tier " + amount + ")";
case "TWITCH_SUBSCRIBE_GIFT":
return " gifted " + amount + " subs";
case "TWITCH_CHEER":
return " cheered " + amount + " bits";
default:
return "";
}
}
This method will take in TriggerAmount
as an argument, look at TriggerCause
, and branch to what suffix it needs to append to match the bound event type. With the default
case, we're simply returning an empty string since we don't care to add anything for something like CHAT_COMMAND or CHANNEL_POINT_REDEEM.
Finally, we just need to adjust our VTIEventPlay()
method to apply this new method to the string, like so:
if (Utilities.IsValid(UserNameText))
{
UserNameText.text = UserName + GetEventVerb(TriggerAmount);
}
Closing
And that's about all there is to making a custom Driver. This may have been a bit of a long-winded article, but I wanted to explain all the steps along the way to making a custom Driver, rather than just throwing the final code at the screen and making you look at it. This tutorial Driver should now be able to be used near-identically to VTIObjectToggle, and the steps we took to get here should be nearly the same for any new applications you wish to add on top of the VTI World Kit.
Here is what our Driver looks like at the end of the tutorial:
using TMPro;
using UnityEngine;
using VRC.SDKBase;
public class CustomDriver : VTIDriverBase
{
public GameObject Target;
public float ResetTime = 5.0f;
public TextMeshPro UserNameText;
public TextMeshPro MessageText;
void Start()
{
Target.SetActive(false);
}
public override void VTIEventPlay()
{
IsReady = false;
if (Utilities.IsValid(UserNameText))
{
UserNameText.text = UserName + GetEventVerb(TriggerAmount);
}
if (Utilities.IsValid(MessageText))
{
MessageText.text = Message;
}
Target.SetActive(true);
SendCustomEventDelayedSeconds(nameof(ResetTarget), ResetTime);
}
public void ResetTarget()
{
IsReady = true;
Target.SetActive(false);
}
private string GetEventVerb(int amount = 0)
{
switch (TriggerCause)
{
case "TWITCH_FOLLOW":
return " followed";
case "TWITCH_SUBSCRIBE":
return " subscribed (Tier " + amount + ")";
case "TWITCH_SUBSCRIBE_GIFT":
return " gifted " + amount + " subs";
case "TWITCH_CHEER":
return " cheered " + amount + " bits";
default:
return "";
}
}
}