Here’s how I was able to get ImGui working inside the Unity game engine from a .NET mod loaded with BepInEx .
ImGui Demo window rendering inside Town of Salem
ImGuiRenderingPlugin performs the heavy lifting of hooking the relevant graphics interface and acts as an ImGui backend. ImGui.NET is used as a wrapper around the native front-end provided by cimgui .
Dependency Setup
With BepInEx already installed, I built the example project and set up a post-build task to copy it to <Game>/BepInEx/plugins/ImGuiMod/Mod.dll
. Inside that directory, I needed to provide some dependent Mono libraries which were not already included in the <Game>/<Game>_Data/Managed/
directory: System.Runtime.dll
and System.Runtime.CompilerServices.Unsafe.dll
. The versions of these libraries provided with the repository are unsuitable (System.Runtime.dll
is a facade with no actual implementation, System.Runtime.CompilerServices.Unsafe.dll
is a .NET build which is unsuitable to link against the Mono libraries provided by Unity game installs) so I replaced them with compatible counterparts. I installed Mono 6.12.0 and searched C:\Program Files\Mono\lib\mono
to locate them, but a different release version might be required depending on the age of the game.
ImGuiRenderingPlugin
needs to be able to perform hooking via Unity’s native plugin API, so I had to use the third-party tool bundled in the repo to patch <Game>_Data/globalgamemanagers
to load both <Game>_Data/Plugins/cimgui.dll
and <Game>_Data/Plugins/ImGuiRenderingPlugin.dll
during initialization.
Debugging and Refactoring
At this point, the example plugin was successfully rendering ImGui inside the process, but improper state management caused rendering to break between scene changes. Let me explain how the existing code worked:
TrainerLoader
creates a GameObject withTrainerMenu
attachedTrainerMenu
uses Unity’s GUI to create a button that does the following steps when clicked- If the
ImGuiPluginHook
GameObject + component pair was not initialized yet, create it. (This bridges our code andImGuiRenderingPlugin.dll
)- Also, add the
ImGuiDemoWindow
component to the active camera, and subscribe to itsLayout
event to callTrainerMenu.OnLayout
- Also, add the
- If the
ImGuiPluginHook
checks if theImGuiDemoWindow
component exists on the active camera and adds it where absentImGuiDemoWindow
implementsUpdate()
and simply invokes itsLayout
event
This was really frustrating to debug on account of how confusing it was to include all these moving parts for no apparent benefit. I avoided listing anything but the key elements for the sake of brevity. Ultimately, the problem stemmed from ImGuiDemoWindow
’s event not being subscribed by TrainerMenu
when ImGuiPluginHook
re-applied a new ImGuiDemoWindow
component to the new scene camera. ImGuiPluginHook
is only initialized once (due to DontDestroyOnLoad
preserving it between scene changes) and thus TrainerMenu
is never able to subscribe to the new event.
Here’s what I did to fix that:
- Delete
TrainerLoader
- Move
ImGuiPluginHook
GameObject + component initialization toBepInLoader.cs
during the mod initialization - Delete the
TrainerMenu
component - Rename
ImGuiDemoWindow
toImGuiActiveWindow
and delete everything inside - Implement
ImGuiActiveWindow.Update()
and use it to execute my ImGui code
The modding framework executes the mod through BepInLoader
which creates ImGuiPluginHook
. During the next frame, ImGuiPluginHook
creates and attaches the ImGuiActiveWindow
component to the active scene camera. ImGuiActiveWindow
calls a static method to render the GUI in its Update
function each frame. When the scene changes and deletes the camera, ImGuiPluginHook
automatically kicks in and we’re back to rendering two frames later.
Input Fall-through
The final problem: ImGui inputs were “falling-through” to Unity UI elements without being consumed. I’m not very familiar with Unity, but I was able to accomplish a simple hack that fixes this well.
// ImGuiInput.cs
public void UpdateMouse()
{
// ...
// disable input events when ImGui is focused
var inputModule = EventSystem.current?.currentInputModule;
if (inputModule != null)
{
if (inputModule.isActiveAndEnabled && WantCaptureMouse)
{
inputModule.DeactivateModule();
}
else if (!inputModule.isActiveAndEnabled && !WantCaptureMouse)
{
inputModule.ActivateModule();
}
}
}
This prevents key inputs from being processed by the game as long as ImGui is trying to trap the mouse. However, the game will lose keyboard focus when the mouse is moved over an ImGui element. It works well enough in practice that I haven’t been bothered by it.