Building a Unity Plugin in JavaScript

Make your JS Library compatible with Unity.

Building a Unity Plugin in JavaScript
Photo by Andre Frueh / Unsplash
Playroom for Unity is open-source. Check it out here: https://github.com/asadm/playroom-unity
Interested in trying out Playroom for Unity? Check the docs! Join the Discord channel too in case you need help, you'll find me lurking there sometimes. 😁

Creating a game is no easy task, and crafting a multiplayer experience adds an even greater level of difficulty. But thanks to industry experienced developers who've created numerous tools which make the process of creating multiplayer games easier and more productive, developers can now focus on creating fun and immersive experiences for their users, and not worry about the networking side. One growing platform for this purpose is PlayroomKit by Playroom, which mainly focuses on multiplayer web-based party games.

I'm a Unity Developer, so I've had my eyes on similar multiplayer plugins for quite a while. Some of them are:

  1. Normcore: https://normcore.io/
  2. Photon: https://assetstore.unity.com/packages/tools/network/photon-unity-networking-classic-free-1786
  3. PlayFab: https://learn.microsoft.com/en-us/gaming/playfab/features/multiplayer/lobby/lobby-matchmaking-sdks/multiplayer-unity-plugin-quickstart
  4. Hathora: https://hathora.dev/

One of my biggest complaints (and the community's) is how hard it is to get things up and running with these libraries.

All that previous knowledge helped me, when I had to implement a plan to create PlayroomKit for Unity, keeping the objective in mind, making sure I didn’t hurt Developer Experience, and using my knowledge in Unity and hacking around in C# and JS.

The Goal

Combine the ease of PlayroomKit with power of the Unity engine.

The PlayroomKit package is super simple to get started with… in JS, at least. That’s a developer experience we needed to port to Unity. At the same time, we had to make the Unity library at feature parity with the JS library, following the exact same API. (That makes it easier to write docs, at least 😃)

Here our first problem arises which is quite easy to pinpoint: Unity uses C# and PlayroomKit is a JavaScript package. So some interoperability had to occur. To achieve this, we had the following approaches:

  • Convert the JS library to C#, or,
  • Use a JavaScript Interpreter for .NET (JINT), or,
  • Use the Interaction with browser scripting provided by Unity itself.
If you want to go deeper into the thought process of “why” we went with our final approach, I highly recommend reading Playroom’s official blog on this very topic.

The Approach

We went with the third approach, referring the Unity docs. In short, the workflow is like this:

  • We work with 2 files: a JSLIB (JavaScript Library) and C# class.
  • The JSLIB file acts as a bridge between PlayroomKit (or any other JS library) and C# (Unity)
What I call "The Bridge".

The figure above shows basic working of the system.

Problem 1: Passing Data

The Unity documentation shows an example where the primitive datatypes are being used to pass data between C# and JS. This process of converting is known as:

Marshalling. This process involves converting an object's memory representation into a format suitable for storage or transmission, especially across different runtimes.

The documentation gives us a good starting point with examples for the basic datatypes:

Hello: function () {
	window.alert("Hello, world!");
},

HelloString: function (str) {
	window.alert(UTF8ToString(str));
},

PrintFloatArray: function (array, size) {
	for(var i = 0; i < size; i++)
	console.log(HEAPF32[(array >> 2) + i]);
},

AddNumbers: function (x, y) {
	return x + y;
},

StringReturnValueFunction: function () {
	var returnStr = "bla";
	var bufferSize = lengthBytesUTF8(returnStr) + 1;
	var buffer = _malloc(bufferSize);
	stringToUTF8(returnStr, buffer, bufferSize);
	return buffer;
},

And in C# we will define the functions like this:

[DllImport("__Internal")]
private static extern void Hello();

[DllImport("__Internal")]
private static extern void HelloString(string str);

[DllImport("__Internal")]
private static extern void PrintFloatArray(float[] array, int size);

[DllImport("__Internal")]
private static extern int AddNumbers(int x, int y);

[DllImport("__Internal")]
private static extern string StringReturnValueFunction();

To use these functions, we can call them like so:

void Start() 
{
  Hello();
  
  HelloString("This is a string."); // sending a string to JS
  
  float[] myArray = new float[10];
  PrintFloatArray(myArray, myArray.Length);
  
  int result = AddNumbers(5, 7);
  Debug.Log(result);
  
  Debug.Log(StringReturnValueFunction());
}

Now this is all great, but the issue arises when we have to deal with async code or functions with callbacks.

Problem 2: Async Code or Callbacks?!

There are great discussions on the Unity Forums regarding using async functions and for passing callbacks as well. In the case for PlayroomKit, instead of using async / await, we went with providing callbacks, (PlayroomKit already provides callback parameters wherever required). The pattern here is something like so:

Important things here are:

dynCall('v', callback, [])

dynCall is a prefix used for dynamically calling functions exported from WebAssembly modules, commonly seen in environments like Emscripten.

Note: Unity is using the Emscripten version 2.0.19 toolchain.

dynCall is where the callback will be invoked at. the ‘v’ shows that the callback is of type void and has no parameters. Now let's say that our callback takes a string playerID has a parameter, then the code will be like this:

[DllImport("__Internal")]
public static extern void ExampleFunction(Action<string> callback);

Inside the JavaScript we will be invoking the callback like so:

var id = player.id;
var bufferSize = lengthBytesUTF8(id) + 1;
var buffer = _malloc(bufferSize);
stringToUTF8(id, buffer, bufferSize);
dynCall("vi", functionPtr, [buffer]);

The 'vi' points that the callback is with a string parameter. in case of have 2 string parameters we can use:

dynCall('vi', callback, [dataJson, buffer]);

Now you probably have seen a pattern that we are using strings mostly to pass data, that is because it is quite easy to handle JSON from both C# and JS. Unity’s built-in JsonUtility is great for Unity specific types (such as Vectors etc), but it is quite limited especially when it comes to data structures such as Dictionaries. To solve this issue, we went with the simplest open-source serializer called SimpleJSON.

Limitations:

  • The major limitations are that this only works with WebGL, and to be able to use functions with DllImport header, we need to build the game and test it.
  • Unity’s WebGL is currently not supported on mobile devices (it may work on some high-end ones), so currently mobile web games aren’t supported 🥹
  • Since we didn't have direct access to JavaScript's memory space, we had to create helper functions to get player's by their ID from JavaScript. So we kind of had to treat JavaScript-World as sort of a "database" from which we queried our objects. This lead to some additional code, and, potentially, performance issues.

Improvements:

  • In case of PlayroomKit, we made a mock-mode within unity which simulates the working of PlayroomKit within the editor. This was done to tackle the problem of building the game after changes. Now this isn’t the best solution, but it does give an idea on how a function will work in the actual build.
  • Unity announced some major updates for Web games in Unite 2023, including early access for WebGPU, and supporting mobile web browsers. Hopefully these updates will open up new opportunities for developers to create unique experiences for their users.
  • The best part is, we've created a really nice DX for developers, based off of PlayroomKit's JS DX. So down the line, if we even need to rewrite this entirely in C#, we would just need to adjust things under the hood, and developers wouldn't need to make too many changes in their games.

Conclusion

It definitely is possible to bridge a JavaScript library into Unity. This article explored one approach deeply. If you've tried the others, drop a comment! I'd love to know the use case and keep learning.

Software engineering isn't always about creating things from scratch. More times than often, it's:

  1. Putting two (or more) things together in a cohesive and understandable way. (Adapter pattern)
  2. Writing, writing, and more writing. Write the idea, write the approach, document things. (A major difference between programmers and engineers)
  3. Exposing the underlying mechanisms of systems to exploit them to your advantage, while at the same time understanding that some black boxes don't really need to be opened. (Knowing enough to get the job done, by putting systems together efficiently)

And I got to do them all. Pretty fun!


We love trying out new frontiers, to bridge useful tech with useful communities, and contributing to the overall knowledge stream. A lot of our work is R&D-based and on experimental tech. If you're interested in working with or for Grayhat, DM or comment!


This article was authored by Talha Momin, Software Engineer at Grayhat.