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

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:

  1. Add types to the MonoBehaviour section in the JSON
  2. Add those same types to the AddComponent method in the JSON
  3. Define the messages (e.g. Update) added in C++
  4. 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.