Unity is a cross-platform game engine and development environment for creating 2D and 3D games for Windows, Linux, macOS, mobile devices, web browsers, and gaming consoles. While Unity games are primarily scripted in C#, the game engine also supports platform-specific code written in C-based languages, which is great for interacting with APIs specific to the targeted platform, reusing third-party libraries written in C or C++, and low-level graphics processing. This post is a guide to including platform-specific code in your game while supporting multiple platforms.
While functionality is generally added to Unity games in the form of “scripts”, which are C# source files which are developed in the Unity environment, code and other resources can also be compiled and packaged for use in several projects and these packages are called “plugins”. Plugin code can be written in C# using .NET APIs (“managed” plugins) or in C-based languages (“unmanaged” or “native” plugins).
Plugins have the same structure as other Unity projects, and creating a new project is the best place to start for a new plugin. Create a new empty project using either the Unity editor or the command-line interface:
/path/to/Unity -batchmode -createProject MyPlugin
Any scripts which should be accessible from the Unity editor should be included inside the plugin directory in Assets/.
Platform-specific resources are easy to manage when placed in separate Assets/Plugins
directories, with a defined name for each platform:
If you use this scheme, Unity will generally automatically detect which platform is needed for a particular file.
There are two ways to add native code to a plugin. If you are using the IL2CPP backend to build your project, you can include C/C++ source files directly in your plugin directories to have the file compiled and included in the project by Unity itself.
If the source is the same on every platform, it can be included in the top-level Plugins directory:
project/
└ Assets/
└ myplugin.c
Otherwise if the source differs for different platforms, include it in the platform-specific Plugins directories:
project/
└ Assets/
└ Plugins/
└ Android/myplugin.cpp
└ iOS/myplugin.cpp
└ OSX/myplugin.cpp
└ Windows/myplugin.cpp
The functions defined in the source file can then be declared as static references in your C# scripts and referenced like regular functions, declaring that the function is imported from __Internal
. These functions are considered “internal” because they will be compiled alongside the C++ code generated by Unity projects rather than into a separate library.
myplugin.c:
// declare functions with C linkage to ensure name-mangling does not break references
extern "C" {
char *interesting_information() {
return "something interesting!";
}
}
MyPlugin.cs:
using System.Runtime.InteropServices;
class MyPlugin {
[DllImport("__Internal")]
static extern string interesting_information();
}
This technique works well for small libraries and personal projects, however the compilation speed may become an issue for larger libraries or libraries with complex build processes. In those cases, it is best to compile the native source file into libraries targeting each platform, and place the library in the platform-specific plugin directory instead. The tradeoff here is that you will need to know how to compile libraries for each platform you are supported, such as getting familiar with building libraries for Android using the NDK.
project/
└ Assets/
└ Plugins/
└ Android/libmyplugin.so
└ iOS/libmyplugin.a
└ OSX/libmyplugin.a
└ Windows/myplugin.dll
MyPlugin.cs:
using System.Runtime.InteropServices;
class MyPlugin {
#if UNITY_IOS || UNITY_TVOS
private const string Import = "__Internal";
#else
private const string Import = "myplugin";
#endif
[DllImport(Import)]
static extern string interesting_information();
}
For precompiled libraries, the import declaration differs per-platform. For iOS and tvOS, use __Internal
. For other platforms, use the name of the plugin, removing the “lib” prefix (if any) and file extension. From there, any C/C++ function declared from C# can be called at runtime.
Once you have organized your library, then it’s time to package it! The Unity command-line interface includes a command to “export” a package file which can then be imported into other Unity projects. Export a project by right-clicking the “Assets” directory in your plugin project and selecting “Export Package” or using the command-line interface:
# projectPath = the path of the project which will be exported
# exportPackage = export the project in projectPath given a relative path to the asset directory
/path/to/Unity -projectPath=/path/to/my/plugin \
-exportPackage Assets \
MyPlugin.unitypackage
Try it out! Open a project where you want to use your plugin, and then import your package file. The assets in the package file will be added to your project and be ready to use. To learn more about building native plugins, I recommend reading the Unity documentation on native plugins as well as the Mono runtime guide to calling native code from C#. It’s also possible to call C# code from native code, though that is a topic for another time.
While writing this post, I wrote a small example plugin which uses a Checkers (or Draughts) implementation written in C to power a C# interface. You can explore the source code on GitHub.
I also came across a few interesting open source native plugins, which may also help you as you build your own:
Thanks for reading!
———
Learn more about Bugsnag’s Unity crash reporting.