Luna Tech Series: 5 Challenges of Writing in C# for a JavaScript Engine

Luna Labs
Luna Labs
6 min readMay 4, 2020

--

Luna Playable allows developers to build their playable experiences in Unity, and convert them to run on the Luna game engine — a lightweight, feature full engine written in HTML5.

Unity game code is written C#, whilst our engine is in JavaScript. So, there begins one of our challenges and the Luna Tech Series…

Here at Luna, the engineering team is constantly working to resolve challenges. And throughout this new blog series, we will be sharing with you the key problems we encounter on a nearly daily basis and how we overcome them, at the start of every month.

In this particular post, Egor Bashinsky, our Engine Developer, will be walking you through five key challenges of writing in C# for a JavaScript engine and the solutions to them. Without further ado, here’s Egor.

In order to transpile this game code to JavaScript, we actually use a modified version of Bridge.NET. This framework is powerful and, for the most part, does a great job at converting the C# code into an acceptable and working Javascript version. Still, some core differences between these two languages provide us with a constant flow of issues. And in the context of building a game engine, there’s no doubt we’re also facing a greater influx of challenges because of the focus and need for high performance.

1. Memory management (Primitive types in JavaScript and structures in C#)

Both languages use Stack and Heap at their foundations for memory management. However, C# provides developers with more freedom in using it.

In C#, all structs (Vector3 for example) are, by definition, value types. This means they are faster to operate with (stored at stack), are passed to methods as a copy, and don’t get modified.

Example 1: C#

Converting this code in its current form to JavaScript will lead to a “corruption” of the direction vector.

Example 1: JavaScript

So, how do we deal with this? Well, thankfully Bridge.NET has safety guards for these types of difference and handles it for us.

Example 1: Updated JavaScript

But as you can imagine, there are a lot more instances similar to this that we may not be able to handle with such elegance!

Note: all maths is based on Vector2/Vector3/Vector4/Quaternion/Matrix3x3/Matrix4x4 Structs.

Which leads us nicely to our next challenge…

2. Heavy allocation & performance

The performance of our game engine is very important to us and, especially, our partners — we need it to be able to create smooth and responsive playable ads. Given that game engines use a lot of mathematic calculations under-the-hood using these base types (as mentioned above), having many clones is a sure way to kill the performance.

So, how do we solve this?

Well, we’ve extended Bridge.NET to be clever enough (with some hints from our team) to account for places that are safe to pass as a reference and not a clone, helping with performance as much as possible.

Example 2: C#

Using attribute [Ref] with parameters declares that you are aware and happy to take responsibility for the Vector3 (or any other struct at this point), and it will be used as a reference without making an extra copy.

The technique becomes extremely useful when dealing with a call intensive API. For example, the function above is called for each and every object that will be rendered on a scene… and that is at least a hundred allocations.

3. Type-safe language (C#) vs untyped (js)

Although both are object-oriented languages, type-safe languages (…C#) provide additional safety, casting support and performance.

This fundamental difference between the languages, can causes issues when converting. Let’s take a look at a rather simple C# example.

Example 3: C#

Since C# allows casting we are always sure that framework code, even with the same naming, is accessible and won’t be overridden.

That’s not the case with JavaScript.

Example 3: JavaScript

This is less innocent than it looks, and in fact, presents a lot of challenges for Luna Playable. By having any fields, methods or properties with the same name as the base class effectively hides it from both user and engine code. And this breaks things. Many things.

We have some approaches that solve this at scale for Luna, but we’ll save that for another day.

4. Generics?

Generics allow you to define a class with placeholders for the type of its fields, methods, parameters, etc. They provide great flexibility when writing in C#.

But, the bad news is that JavaScript has never heard of them.

The light at the end of the tunnel is that Bridge.NET has an additional layer which provides this functionality. Of course, it’s not as simple as plug-and-play, and a lot can (and does) go wrong during the code conversion. This process has to be carefully managed.

Here’s a simple example.

Example 4: C#

If after reading the above gets you thinking about the differences in reference vs value type handling, then you’re on the right track.

In order to convert the code above whilst maintaining a working container (List in this case), you should be aware of the type it operates with, and perform optional cloning where it’s required. Evaluation overhead — sure, but a necessary evil.

5. Data structure differences: plain arrays over collections

C# has a bunch of juicy, easy to use out-of-the-box data containers such as Lists. Not only does their availability simplify coding, but it also provides an additional access layer (to internally used arrays).

Converting these for JavaScript is fine for most cases, but if it’s heavily abused, then problems will arise. For example, if you’re iterating over vertices, they will then introduce a significant overhead whilst forbidding the js-engine to optimise internally.

Let’s check how translation happens, through a simple example of initialising an array and list, and getting some data from them.

Example 5: C#

Let’s take a look at what this is converted to for JavaScript.

Example 5: Array in JavaScript
Example 5: List in JavaScript

The List in JavaScript has additional checks for out-of-range and before finally accessing the required item.

The overhead isn’t huge, but it all adds up. We’re constantly looking for small wins which when propagated outwards, across thousands of lines of codes, can quickly add up.

Those are just a few examples of some of the details involved in the day-to-day work of our team. If you’re interested in reading more about these types of challenges our developers experience at Luna, then follow this tech series.

And if you want to stay up to date on our whereabouts, then sign up to our newsletter or follow us on Twitter, Facebook, Linkedin or Instagram.

--

--