Creating Custom VTI Drivers

From WaffleSlapper's Project Wiki
Revision as of 01:06, 14 February 2024 by Steeveeo (talk | contribs) (Added "Next Steps" section and some minor cleanup and code highlighting to previous commits.)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)


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 "";
		}
	}
}