C++ Scripting: Part 26 – Hot Reloading
Imagine being able to modify C++ game code and have it take effect without even restarting the game. That’s the motivating idea behind today’s article. Read on to see how this works and how to use it to really speed up iteration times.
Table of Contents
- Part 1: C#/C++ Communication
- Part 2: Update C++ Without Restarting the Editor
- Part 3: Object-Oriented Bindings
- Part 4: Performance Validation
- Part 5: Bindings Code Generator
- Part 6: Building the C++ Plugin
- Part 7: MonoBehaviour Messages
- Part 8: Platform-Dependent Compilation
- Part 9: Out and Ref Parameters
- Part 10: Full Generics Support
- Part 11: Collaborators, Structs, and Enums
- Part 12: Exceptions
- Part 13: Operator Overloading, Indexers, and Type Conversion
- Part 14: Arrays
- Part 15: Delegates
- Part 16: Events
- Part 17: Boxing and Unboxing
- Part 18: Array Index Operator
- Part 19: Implement C# Interfaces with C++ Classes
- Part 20: Performance Improvements
- Part 21: Implement C# Properties and Indexers in C++
- Part 22: Full Base Type Support
- Part 23: Base Type APIs
- Part 24: Default Parameters
- Part 25: Full Type Hierarchy
- Part 26: Hot Reloading
- Part 27: Foreach Loops
- Part 28: Value Types Overhaul
- Part 29: Factory Functions and New MonoBehaviours
- Part 30: Overloaded Types and Decimal
Way back in part two, we added in the ability to make C++ changes without restarting the editor. We did this by using OS functions to manually load a DLL instead of relying on [DllImport]
. By loading on startup and unloading on shutdown, we gained the ability to change the C++ plugin DLL between runs of the game.
This same ability to load and unload the C++ plugin is at the core of “hot reloading.” The “hot” part means we’re loading while the game is running—”hot”—as opposed to stopped: “cold”. So the first step is to add a function to Bindings
to reload the plugin. All this Reload
function needs to do is unload the current plugin and load the new plugin. However, this triggers a couple of issues that we’ll need to work around.
The first issue is easy to solve. When the plugin gets loaded, PluginMain
gets called in C++ as the “entry point” for the plugin. Since it can now be called either at the first boot of the plugin or for a reload of the plugin, we need to pass a parameter to PluginMain
indicating which scenario it’s being called for. This allows it to skip any one-time setup work.
The second issue is a bit tougher. The C++ plugin has its own memory and that will go away when it’s unloaded. This means a reloaded plugin will start fresh and not be able to continue where the previous plugin left off. What we need is a way to keep the C++ plugin’s memory around between reloads of the plugin. Fortunately, the plugin’s host—the C# side—is easily capable of this. We just need the C# side to call Marshal.AllocHGlobal
to allocate unmanaged memory and pass this to the plugin on both the first boot and on reloads. As long as the C++ code only uses this memory and never allocates its own memory (e.g. with malloc
) then this will work out just fine. Here are some examples:
// Define a struct for whatever memory the game needs to hold struct GameState { int32_t Score; int32_t Level; // ... }; // Keep a pointer to the game state. // It doesn't need to be global like this. GameState* gameState; void PluginMain(void* memory, int32_t memorySize, bool isFirstBoot) { // Treat the memory as a game state gameState = (GameState*)memory; // Only do startup tasks on first boot if (isFirstBoot) { GameObject go; go.AddComponent<MyGame::TargetScript>(); } } void MyGame::TargetScript::OnCollisionEnter(Collision& param0) { // Score a point in the game state when the target is hit gameState->Score++; // It's OK to have constants outside of the game state const int32_t levelThreshold = 100; // Scoring enough points goes to the next level if (gameState->Score > levelThreshold) { gameState->Score -= levelThreshold; gameState->Level++; } }
Calling memory allocation functions like malloc
or using static
local variables isn’t allowed with this approach, but we can still use a special form of the new
operator called placement new. This form skips the memory allocation part of new
and just calls the constructor:
// Normal, non-placement 'new' operator // Allocates memory then calls constructor void PluginMain(void* memory, int32_t memorySize, bool isFirstBoot) { GameState* gs = new GameState(); } // Placement 'new' // Just calls constructor #include <new> void PluginMain(void* memory, int32_t memorySize, bool isFirstBoot) { GameState* gs = new (memory) GameState(); }
To use dynamic memory allocation with this approach, we’ll have to use an allocator that allocates only out of the memory passed to PluginMain
. The global new
operator and functions like malloc
don’t have support for this and they’re used by the C++ standard library (a.k.a. STL), so we’ll need to avoid this if we want to reload our plugin. One alternative is Electronic Arts’ EASTL replacement which allows us to control memory allocation while still using container types like vector
and list
.
Because the C++ plugin is going to need plenty of memory to store all of its data, we need a way to specify how much memory for C# to allocate and give it. So we’ll change Bindings.Open
to specify a memory size. The bindings layer in C++ will use the first part of this to keep track of handles and then pass the rest of it to PluginMain
. BootScript
now allows us to specify the memory size we want to allocate for the C++ plugin.
So now that we can hot reload the C++ plugin, how do we use it? There are two options: manual and automatic. There’s now a NativeScript > Reload
menu item in the editor to reload the plugin on-demand. There’s also an “Auto Reload” checkbox on BootScript
alongside an “Auto Reload Poll Time” field. Checking this box will start a coroutine on the BootScript
that polls the C++ plugin file (e.g. DLL) for it’s last write time. The write time will change when the C++ plugin is compiled. This will cause the plugin to be reloaded at that point.
Now we have the ability to modify our C++ game code and have it take effect right away in the game. For example, I’ve been testing this out by changing the bouncing ball in the example code on GitHub. I can tweak the speed or even change the code that makes it move around and it takes effect immediately in the editor window. If we can live with the memory allocation limitations, we can really cut down on the time we spend reloading the game and then playing through until we get to just the part we’re working on.
Hot reloading support is now available on the GitHub project. Feel free to let me know what you think about it or any other aspects of the project in the comments.
#1 by aidan on May 11th, 2018 ·
Is it can hot reload Dll in the mobile platform?
#2 by hsl9999 on May 12th, 2018 ·
No,it isn’t .