C++ Scripting: Part 7 – MonoBehaviour Messages
The series continues this week by addressing a pretty important issue. Previously, we were limited to doing all our work in just two C++ functions: PluginMain
and PluginUpdate
. This isn’t at all the normal way to work in Unity. It’d be a lot more natural to write our code in MonoBehaviour
classes. So today we’ll come up with some tricks to allow us to write our MonoBehaviour
code in C++ so we are truly scripting in C++.
Table of Contents
- Part 1: C#/C++ Communication
- Part 2: Update C++ Without Restarting the Editor
- Part 3: Object-Oriented Bindings
- Part 4: Performance Validation
- Part 5: Bindings Code Generator
- Part 6: Building the C++ Plugin
- Part 7: MonoBehaviour Messages
- Part 8: Platform-Dependent Compilation
- Part 9: Out and Ref Parameters
- Part 10: Full Generics Support
- Part 11: Collaborators, Structs, and Enums
- Part 12: Exceptions
- Part 13: Operator Overloading, Indexers, and Type Conversion
- Part 14: Arrays
- Part 15: Delegates
- Part 16: Events
- Part 17: Boxing and Unboxing
- Part 18: Array Index Operator
- Part 19: Implement C# Interfaces with C++ Classes
- Part 20: Performance Improvements
- Part 21: Implement C# Properties and Indexers in C++
- Part 22: Full Base Type Support
- Part 23: Base Type APIs
- Part 24: Default Parameters
- Part 25: Full Type Hierarchy
- Part 26: Hot Reloading
- Part 27: Foreach Loops
- Part 28: Value Types Overhaul
- Part 29: Factory Functions and New MonoBehaviours
- Part 30: Overloaded Types and Decimal
There are several pieces required in order to be able to write MonoBehaviour
scripts in C++. First of all, we need a way to specify which MonoBehaviour
classes should be generated by the GenerateBindings
code generator. For that, we’ll add a new top-level field to our code generation JSON:
{ "MonoBehaviours": [ { "Name": "TestScript", "Namespace": "MyGame.MonoBehaviours", "Messages": [ "Awake", "OnAnimatorIK", "OnCollisionEnter", "Update" ] } ] }
Next, we’re going to need some basic support for generics in our C#/C++ bindings so that we can call GameObject.AddComponent
and start using one of the generated MonoBehaviour
classes. That’ll let us write C++ code like this:
void PluginMain() { // Add the MonoBehaviour to the GameObject GameObject go; TestScript script = go.AddComponent<TestScript>(); } void TestScript::Update() { Debug::Log(String("Hello from TestScript's Update()!")); }
We don’t need to define the class in either C# or C++, just fill out the “message” functions like Awake
and Update
.
We also need a place in the JSON to specify which types we want to use for generics like the AddComponent
function. For that, we can simply attach it to the “Methods” section:
{ "Name": "AddComponent", "ReturnType": "T", "ParamTypes": [], "GenericTypes": [ { "Name": "T", "Type": "MyGame.MonoBehaviours.TestScript" } ] }
The “GenericTypes” section allows us to specify various types we want to use for the T
type parameter. We simply list our scripts here, run the code generator, and we’re able to add them with AddComponent
.
With those changes in place, we can go ahead and remove the awkward PluginUpdate
function. As in the example above, it’s easily replaced by an Update
function on our own MonoBehaviour
class.
That’s all there is to using the system:
- Add types to the
MonoBehaviour
section in the JSON - Add those same types to the
AddComponent
method in the JSON - Define the messages (e.g.
Update
) added in C++ - Run the code generator
Now let’s discuss a bit about how this works behind the scenes. As we know by implementing countless MonoBehaviour
classes, Unity’s “messages” to our code are a special sort of function. We don’t use the override
keyword to override a function in MonoBehaviour
and we don’t implement any kind of IMesssageHandler
interface full of message-handling functions. Instead, we simply derive from MonoBehaviour
and implement the functions we want to handle. Unity takes care of figuring out which functions it can call on our classes.
So that’s exactly what we’ll implement when the code generator runs. We’ll make a class with the user’s desired namespace and name and make it derive from MonoBehaviour
. Then we’ll fill in the message functions the user wants, but instead of handling the messages right there we’ll make a call into C++ code. We’ve seen that sort of thing before with MonoBehaviourUpdate
:
#if UNITY_EDITOR // In the editor we want to load from a DLL // This allows to to reload the plugin without restarting the editor public delegate void MonoBehaviourUpdateDelegate(); public static MonoBehaviourUpdateDelegate MonoBehaviourUpdate; #else // In the released game we want to use the plugin directly [DllImport(NativeScriptConstants.PluginName)] public static extern void MonoBehaviourUpdate(); #endif // Open the plugin static void Open() { #if UNITY_EDITOR // In the editor we take care of the loading and unloading MonoBehaviourUpdate = GetDelegate<MonoBehaviourUpdateDelegate>( libraryHandle, "MonoBehaviourUpdate"); #endif }
All we need to do is generate that same sort of code for each message function in each MonoBehaviour
class. Then when we generate the MonoBehaviour
classes, we just need to call the C++ message function:
public class TestScript : UnityEngine.MonoBehaviour { // Object handle to "this" private int thisHandle; public TestScript() { // Save an object handle to "this" thisHandle = NativeScript.ObjectStore.Store(this); } public void Awake() { // Forward to the C++ function // Pass the object handle so C++ knows which instance to use NativeScript.Bindings.TestScriptAwake(thisHandle); } }
That’s all for the C# side. On the C++ side, of course we need to implement the message function there:
DLLEXPORT void TestScriptAwake(int32_t thisHandle) { // Convert the object handle into a class instance // This allows users to deal with objects, just like in C# TestScript thiz(thisHandle); // Call the user's function implementing this message thiz.Awake(); }
One key thing to keep in mind with this arrangement is the split in C++ between a function’s declaration and its definition. The code generator is responsible for declaring the message function, like so:
struct TestScript : UnityEngine::MonoBehaviour { // Declare the function, but don't define it void Awake(); };
The function is callable, but it doesn’t have a body yet. The user is responsible for doing that, as we saw above:
void TestScript::Update() { // Implement the function Debug::Log(String("Hello from TestScript's Update()!")); }
If the user doesn’t define the function, they’ll get a linker error when they build the C++ plugin. It should be obvious from the error message what they need to define in order to make the plugin build again.
Finally, let’s talk about supporting generics so that we can call GameObject.AddComponent<TestScript>
. The compilers for C# and C++ both do the same thing when using generics in C# or templates in C++. They make a copy of the generic/templated thing for each unique set of types we use them with. For example:
struct KeyValuePair<TKey, TValue> { public TKey Key; public TValue Value; } void Foo( KeyValuePair<int, int> kvpIntInt1, KeyValuePair<int, int> kvpIntInt2, KeyValuePair<int, float> kvpIntFloat, KeyValuePair<float, int> kvpFloatInt, KeyValuePair<float, float> kvpFloatFloat) { }
KeyValuePair
is used in four unique combinations of int
and float
, so the compiler will generate this behind the scenes:
struct KeyValuePairIntInt { public int Key; public int Value; } struct KeyValuePairIntFloat { public int Key; public float Value; } struct KeyValuePairFloatInt { public float Key; public int Value; } struct KeyValuePairFloatFloat { public float Key; public float Value; } void Foo( KeyValuePairIntInt kvpIntInt1, KeyValuePairIntInt kvpIntInt2, KeyValuePairIntFloat kvpIntFloat, KeyValuePairFloatInt kvpFloatInt, KeyValuePairFloatFloat kvpFloatFloat) { }
Because generics and templates are just syntax sugar, our code generator must do the same thing when communicating between C# and C++. We need to generate code for each unique combination of types used with C# generics. In the case of GameObject.AddComponent<T>
, we just have one type parameter T
that’s the return value. So we need to give C++ a function pointer for each T
that the user wants to use with AddComponent
. They have to be explicitly listed in the JSON because we aren’t tied into the compilation process for the C++ code. Here’s how it looks:
///// // C# ///// // C++ calls this for AddComponent<TestScript> [MonoPInvokeCallback(typeof(GameObjectMethodAddComponentTestScriptDelegate))] static int GameObjectMethodAddComponentTestScript(int thisHandle) { // Get the object for the given object handle var thiz = (UnityEngine.GameObject)ObjectStore.Get(thisHandle); // Call AddComponent with a fixed type // The type is not a parameter to this function var obj = thiz.AddComponent<TestScript>(); // Get an object handle for the return value int handle = ObjectStore.Store(obj); // Return the object handle return handle; } // ... more versions of the above for other types of T
On the C++ side, we call this C# function. We also make the interface appear as though there are generics, even though there is not generic support for any kind of T
. This is really just for cosmetic purposes so the API looks more like the C# API. We could have renamed the function from AddComponent<MyScript>
to AddComponent_MyScript
to avoid generics/templates entirely.
////// // C++ ////// struct GameObject : UnityEngine::Object { // We make this function "templated"/generic // We say that it takes one type parameter // In actuality, only specific types are supported template <typename T0> T0 AddComponent(); }; // "Specialization" of the template // We explicitly say that T0 is TestScript template<> TestScript GameObject::AddComponent<TestScript>() { // Call the C# function with our object handle // Convert the returned object handle into a TestScript object return TestScript( Plugin::GameObjectMethodAddComponentTestScript( Handle)); }
All of these steps essentially add the same syntax sugar that the C# and C++ compilers add. Behind the scenes we make many copies of the templated/generic function with unique names and proceed to use them just like any other function.
That’s all for this week’s installment of the series. We’ve taken another step to making C++ scripting easier and more like normal C# programming for Unity. Instead of a weird PluginUpdate
function, now we have regular MonoBehaviour
classes that we can call AddComponent
on. We can receive 62 of the 63 different message functions in C++. Only OnAudioFilterRead
doesn’t work because the code generator doesn’t support arrays yet. We’re getting closer and closer to having a viable alternative programming language for Unity.
Note that the GitHub project has been updated with everything in this article. Feel free to fork or submit ideas for changes, new features, bug fixes, etc.