Controlling Values in Unity with Drivers
Working with animation assets is frustrating. You need to use the clunky animation UI in Unity, manage an ever-growing collection of animation assets, and go back and forth adjusting multiple animations when you want them to look good together. Transform animations also don’t play well with physics, and if you want varied animations, you actually need to make a new asset for each variation (For my purposes here I’m going to ignore fancier animation tech like blend trees, animation overrides, etc.).
In anticipation of the Ludum Dare 48 gamejam, I had been watching an assortment of programming and gamedev videos for inspiration. One in particular really got me thinking about ways to use numbers in the range [0, 1]. Now, I had some familiarity with the concept of drivers as they exist in Blender, so I thought that implementing a similar system in Unity might be a nice way to avoid working with static animations.
Here is the sort of control that I wanted, ideally in a generalised way that could be applied to moving platforms, flickering lights, and doors, among other things:
In a nutshell, my idea was to create a class which exposes a single value, generally (but not necessarily) within the range [0, 1], which other objects could use as a control value for their own behaviour. Since I was developing the system for a jam, I figured that a Driver system would allow me to implement a larger number of behaviours with more variation than I would be able to with a standard animation-asset approach.
Although I was partially correct, the jam timelines were just too tight for me to get everything going the way I had envisioned. In the frenzied rush of the jam I made a bad design decision for how to structure the Driver system. I chose to create Drivers and Driveables, where Drivers would store a list of all Driveables that they should influence, and propagate their control value to all Driveables every frame. Technically this works, but it’s kind of awkward that Drivers are also managing other objects. In addition, there was no mechanism to allow a Driveable to be influenced by more than one Driver, as each Driver would overwrite the value.
I realised my error the next day, but there wasn’t enough time to refactor, so I had to make it work. With only enough time to add a TimeDriver, which loops back-and-forth between 0 and 1 with a specific time duration (essentially a triangle wave, but an added an easing function can bring it closer to a sin wave). I ended up using it to control moving platforms and swinging obstacles, where they oscillate between a start position and an end position as shown below:
After the jam ended, I refactored the code, scrapping Driveables altogether in favour of the idea that a Driver should not care who is using the drive value and instead just publish events for any other class to subscribe to, and let the subscribers decide how to interpret the data (this also opens the door for them to add their own processing or smoothing to the drive values). Here’s a link to the new Driver class. It’s a little bit lengthy, but the key idea is that it manages all the event publishing and range checking of the drive value, while concrete derived Drivers need only specify how to calculate that drive value in the method CalculateDriverValue().
Hooray! Adding new types of Drivers is actually fairless painless now (I have implemented a few so far such as the very simple RandomDriver and the slightly-more-complex NoiseDriver and TimeDriver). I have also begun working on a CompositeDriver, which takes input from one or more other Drivers and uses that to drive its own value (right now only a weighted average is up and running, but I’d like to also add optional multiplicative influence to each Driver in the composite).
Here are 3 different flickering Driver animations that I was able to set up quite quickly in the editor:
So, there are certainly a lot of improvements that could be made to this system (like improving the inspector interface, possibly implementing as a C# object rather than monobehaviour, etc.), but overall, I think it offers a really flexible way to quickly create interesting, context-sensitive animations/behaviours with minimal additional code. The ProximityDriver for instance has all sorts of applications. The first one that comes to mind is using it to control the camera follow smoothing such that it will move lazily while the player is nearby and gradually accelerate to keep up as he or she moves away. Other uses could include objects which exert a distance-reliant force on the player, or to move obstacles out of the way as another object approaches them.
By the way, you can try the jam version of this game here.