< Back
developer frontend

Easy creation of native Unity plugins - case study

Jakub Biskup ,  

A brief summary of the post

In this article I would like to answer the questions:

  • why is the ability to (easily) create native plugins important?
  • when the need to use this skill may arise?
  • what is Crimson Native?
  • what was the need to create Crimson Native? - our story
  • what topics will be covered in the next articles about Crimson Native?


A brief intro about why this is important

In the life of all Unity Developers, sooner or later there will be a moment when they will be forced to use functionality that is only available from the native platform the game is developed for. It's not a big deal if there are already ready solutions (whether created by the Unity team or other developers) and they address our needs. However, the difficulty arises when the functionality we need is not yet created and shared by anyone, or the solution proposed by someone is not sufficiently matched to our requirements. At such a moment, if you don't have experience in this area, you may end up with a serious headache and stress about whether you will manage to deliver the given functionality on time.

https://media.giphy.com/media/Ofo2Z3DnjvAHa4WJOf/giphy.gif

Examples of functionalities that we may need to use:

  • implementation of player rankings and achievements (e.g. for Google Play and App Store) - this is already implemented by the Unity team (see: Unity Social API Documentation), but also by independent developers, for example by the Union Assets team in the Native Pro asset series (see: iOS Native Pro Game Kit, Android Native Pro Google Play API)
  • detection of active system theme (dark/light mode)
  • implementation of media native sharing (e.g. images)


Continuing with the system themes example - in September 2019, there was an iOS update to version 13 where one of the new features was the addition of dark mode to the entire system. Due to the freshness of this functionality, no solutions were available, so if any company wanted to have an app that supported themes, they were forced to implement the functionality themselves. The ability to quickly implement native plugins for a company developing games and mobile applications using the Unity engine is crucial from the business point of view e.g. implementing the aforementioned system themes could help the application to be distinguished by the Apple team, which really translates to its reach and, of course, earnings.

https://media.giphy.com/media/BMe8heX6IvcypfyDHV/giphy.gif

What’s Crimson Native and where did the idea for it and such post come from?

Some may wonder why am I even writing about this topic? Writing plugins for native platforms isn't rocket science, plus you can find plenty of ready-made solutions that you just pay a few dollars for and integrate into your application and you're done. So here I will introduce you to our reason why we created Crimson Native - but first I will describe its very concept. Crimson Native is a pattern we adopted for creating functionalities that integrate two levels - one of them is Unity level (C#), the second is native platform level (it can be Android with Java or iOS with Objective-C). At its core are several assumptions, i.e.:

  • maximizing implemented logic at the Unity level (and, of course, minimizing logic at the native page level)
  • object management (including memory) happens at the Unity level
  • centralization of communication between the two levels
  • the modularity of the solution
  • implementation consistency
  • ease of implementation and possible changes
  • ease of testing at each stage of implementation


When creating a new plugin using this approach, we can distinguish 4 main stages:

  1. Implementation of native side logic (e.g. for Android platform in Java) - during this step we prepare our code so that it is possible to test it in a native application - this allows us to shorten build times and catch potential bugs faster.
  2. implementation of generic (matching to both platforms) logic - the result of this step is a working Unity application that calls some dummy logic (facade and adapters)
  3. integration of the generic layer with the native side - creating a bridge able to communicate with the library and then connecting it with the adapter - the result is a working end-2-end logic (i.e. Unity ←→ native side)
  4. Application-specific logic implementation - by step 3, the prepared code is modular enough that we are able to put it into any other application - while this step goal is to appropriate use of generic logic for the needs of a specific application


High-level class diagram - App Theme case

Below you can see a class diagram showing the implementation of one of the plugins we implemented, specifically the logic responsible for detecting light/dark mode. A diagram with a better resolution is available here.

Crimson%20Native%20-%20App%20Theme%20Providing%20-%20blog.drawio

The diagram is divided into 3 main layers:

  • Generic plugin layer is written in C#, which consists of classes such as. NativeCallbackReceiver (including IOSNativeObject and AndroidNativeObject) and NativeCallbacksDispatcher. It contains logic that is common to all plugins.
  • A specific plugin layer is written in C# with the logic of a specific plugin. In the example shown here, there is an application theme plugin that consists of classes such as IOSThemeProvider and AndroidThemeProvider (which implement the INativeThemeProvider interface), and classes responsible for direct cross-layer (C#/Unity and native side) communication - IOSThemePluginBridge and AndroidThemePluginBridge.
  • The native layers are written in the appropriate language for the native platform (Java for Android and Objective-C for iOS). These layers are further divided into several smaller parts:
    • common to all plugins (containing the logic responsible for communicating with the C#/Unity layer, see f.e. CallbacksBridge)
    • plugin-specific (see f.e. AppThemeController)


Where did the need to design Crimson Native come from?

Over the years we have been forced to use plugins created by other developers many times. The plugins we integrated were those available for a fee, e.g. on the Asset Store, as well as free ones, such as implementations of advertising networks like Google Mobile Ads (short: AdMob), Facebook Audience Network (short: FAN), or IronSource. Those who worked with third-party code know that it has its pros, but also its cons - let's list some of them.

https://media0.giphy.com/media/3o7bucqI23uxTyptbG/giphy.gif?cid=790b76116c286e55beff409fbb7c47a92727319d29d2bc9f&amp;rid=giphy.gif

Pros:

  • available immediately (in other words - it is ready to integrate with our application)
  • cheaper in the short term (after all, someone already implemented it, and writing such a plugin from scratch is almost always more expensive than buying a license)
  • updates can be really cheap (can is the keyword here, this topic will be discussed later in this article)
  • usually doesn't require a lot of technical skills - it can be integrated by most developers, regardless of experience
  • can have a community of people who contribute to its faster development (detect and report bugs, complete documentation, describe their implementation difficulties and their solutions, etc.)


https://media3.giphy.com/media/l0IygSmXGylPVXyAE/giphy.gif?cid=ecf05e47ehy4g632pbo5tfmwbio2vl200l8qiq9wshn58fvl&amp;rid=giphy.gif

Cons:

  • lack of possibility to change the source code - it is especially important when you detect a bug or when functionality needs to be changed/extended in some way
  • usually contains much more implemented functionality than we actually need, and since there is more code there are also more potential problems (e.g. causing crashes or ANRs)
  • in the long term, it can be more expensive to maintain (paradoxically to what I wrote above) because updates (especially to major versions) can be time-consuming and risky because of the excess of implemented functionalities
  • the latest plugin version may yet not support the latest versions of a given functionality for native platforms (e.g. a plugin that supports iOS Dark Mode version 1.0, while there's a 1.1 version already available, which contains important fixes and new functionalities)
  • lack of knowledge of the implementation, or rather the lack of possibility to learn it thoroughly, which can lead to mistakes in the integration with our application (especially in plugins with poor or even non-existing documentation)


Looking at the above pros and cons you can come to a conclusion that the positive aspects behind the use of ready-made solutions are more significant - so why have we decided to use our own solution?

A tipping point for change

At the turn of 2019 and 2020, we decided to implement our own ads mediation system, which was supposed to work analogously to commercially available mediators such as AdMob or IronSource. One of the ad providers we used was FAN, although after some time we noticed that the latest available FAN Unity SDK was almost a year behind the native version, and moreover, it didn't have all the functionality offered by the version dedicated to a specific platform. Taking all of this into account - we decided to prepare our own implementation of FAN for Unity - for both Android and iOS platforms.

https://media3.giphy.com/media/R03zWv5p1oNSQd91EP/giphy.gif?cid=ecf05e470pqg95ae7qh2c9825uakosczby6drnm7ao4u9zur&amp;rid=giphy.gif

Time to start implementation!

When we started the implementation we already had some experience in creating simple plugins, but our knowledge was not sufficient for our needs. The first and main problem was creating and managing objects from the native side, not just on static dependencies as before. In the case of Android - it was a simple matter because of the ready-made solution to facilitate the management of Java objects from within Unity (see: AndroidJavaObject). All we had to do was to add some logic responsible for managing the objects on the native side, communicating with Unity, and finally making sure that the Java objects are not being cleaned up by the garbage collector until we release them. And voila - Android was quite quick. It took us much longer to implement a reasonable solution for the iOS platform. There were several reasons:


The biggest problems we encountered were those related to object lifecycle management on the native side. Objective-C, or more specifically its compiler, has a mechanism called ARC - Automatic Reference Counting - whose default behavior conflicted with what we wanted to achieve (more specifically, manual memory release). Another same non-trivial aspect was the issue of mapping objects created on the native side with those available on the Unity/C# level. In the end, we managed to achieve our goal - we finished implementing our own version of the FAN plugin for Unity, and an additional positive side effect was that we developed a new convention for creating native plugins. A more detailed description of the whole implementation and its progress will be included in the following articles.

https://media4.giphy.com/media/tn8zWeNYA73G0/giphy.gif?cid=ecf05e47mol2vb90iv1ybgcnqc171a9qln7xzyair07syi5v&amp;rid=giphy.gif&amp;ct=g

What does it look like now?

It's been a while since we finished the first version of Crimson Native and we managed to migrate all our older plugins to the new Crimson Native system. It took some time, but looking at how much easier it is now to maintain our entire native infrastructure - it was worth it. At this point, in addition to the above-mentioned FAN SDK, we have implemented features such as:

  • fetching IDFA / AID
  • application stability monitoring for ANRs/crashes
  • dark mode support
  • local notifications (with rich custom layouts)
  • file sharing (e.g. on social media)
  • support for deep/dynamic links
  • and many, many other, minor functionalities


Looking into the future I can say that the plans are ambitious - for sure we will implement even more functionalities available from the native level using Crimson Native - at this point their creation is much more enjoyable than a few years ago.

Teaser of the next articles in this series

In this article, I wanted to give you an overview of the genesis of Crimson Native and why we want to share this knowledge at all. In future articles, I'd like to focus on technical implementation details for both platforms, i.e. Android and iOS - probably a separate article for each platform, as I'd like to showcase larger pieces of code. However, before getting into the world of native implementations, I will also show the Unity/C# side, which is where most of the implementation is. I will show you also real-world examples of how this has all been used and integrated into our applications. At the very end of the series, we'll get off the strictly technical tone again by going into the post-mortem - the lessons we've learned along the way, ideas for improving the whole process, and other lessons we've learned.