


AGDDebugHUD is a BepInEx plugin that displays a real-time debug overlay in the top-left corner of the screen. It comes with built-in player stats and a public API so your own plugins/mods can register custom debug lines.
Compatible with BepInEx plugins and Official API mods.

Once installed, the HUD appears automatically when the GameSingleton awakens. Press F3 (configurable) to toggle the overlay on/off.
| Line | Content |
|---|---|
| FPS | Frame time in ms + FPS |
| HUD Time | HUD processing time in ms + FPS impact |
| Line | Content |
|---|---|
| Ping | Current network ping in ms |
| Line | Content |
|---|---|
| Velocity | Current movement speed in m/s + X/Y/Z components |
| Position | World-space coordinates |
| Line | Content |
|---|---|
| Ragdoll State | Whether the player is currently ragdolled |
| Line | Content |
|---|---|
| State | Grounded, Aiming, Alive, NoClip booleans |
| Status Effects | Count and names of active status effects (e.g. Invincible, RecentlyHardHit) |
All default values are sourced from SessionManager.Instance.LocalPlayer. Each section and individual line can be disabled via the BepInEx config file.
All settings live in BepInEx/config/com.unii.debugHud.cfg.
| Setting | Default | Description |
|---|---|---|
Enabled |
true |
Master toggle for the entire HUD |
ToggleKey |
F3 |
Key to show/hide the overlay |
Section and line toggles (all default true):
EnableGameSection / EnableFPS / EnableHUDFPSEnableNetworkSection / EnablePingEnableMovementSection / EnableVelocity / EnablePositionEnablePhysicsSection / EnableRagdollStateEnableStatusSection / EnableState / EnableStatusEffectsDisabling a section hides all its lines regardless of their individual toggles.
This plugin is made to be as easy to implement as possible.
It is natively compatible with BepInEx plugins AND mods using the Official modding API.
The public API lives in the static class AGDDebugHUD.DebugHUDAPI (assembly AGDDebugHUD).
RegisterHeader(string label)Creates a section header — a bold, gold-colored label displayed above grouped lines. Returns a Header object to pass into RegisterCustomLine.
label - display text for the header (e.g. "--- MyPlugin ---").RegisterCustomLine(string label, Func<string> getter)RegisterCustomLine(string label, Func<string> getter, Header header)Registers a new line in the overlay. The label serves as both the display name and a unique key.
label - unique identifier for the line. Also used as the GameObject name.getter - a Func<string> called every frame (during Update) to fetch the current value. Return null to hide the line entirely.header (optional) - the Header returned by RegisterHeader. Lines sharing the same header appear grouped together under it.Best practice is to register (
RegisterCustomLine) during your own plugin'sAwake.
EnableLine(string label)Re-enables a line that was previously disabled.
DisableLine(string label)Hides the line (GameObject set inactive). The getter is not called while disabled.
FreezeLine(string label)Keeps the last visible text on screen but stops calling the getter.
Your plugin does not need a hard reference to AGDDebugHUD.dll.
Use reflection to call the API, this avoids compile-time dependency and keeps your plugin perfectly functional even if the HUD isn't installed.
BepInEx:
private void RegisterDebugHUD()
{
var apiType = Type.GetType("AGDDebugHUD.DebugHUDAPI, AGDDebugHUD");
if (apiType == null) return; // HUD not installed
var registerMethod = apiType.GetMethod("RegisterCustomLine");
if (registerMethod == null) return;
Func<string> myGetter = () => { /* your logic */ return "MyLabel: someValue"; };
registerMethod.Invoke(null, new object[] { "MyLabel", myGetter });
}
private void RegisterDebugHUD()
{
var apiType = Type.GetType("AGDDebugHUD.DebugHUDAPI, AGDDebugHUD");
if (apiType == null) return;
var registerHeaderMethod = apiType.GetMethod("RegisterHeader");
var registerLineMethod = apiType.GetMethod("RegisterCustomLine");
if (registerHeaderMethod == null || registerLineMethod == null) return;
var header = registerHeaderMethod.Invoke(null, new object[] { "--- MyPlugin ---" });
Func<string> myGetter1 = () => { /* your logic */ return "MyLabel: someValue"; };
registerLineMethod.Invoke(null, new object[] { "MyPlugin_1", myGetter1, header });
Func<string> myGetter2 = () => { /* your logic */ return "MyLabel: someValue"; };
registerLineMethod.Invoke(null, new object[] { "MyPlugin_2", myGetter2, header });
}
private void RegisterDebugHUD()
{
Type.GetType("AGDDebugHUD.DebugHUDAPI, AGDDebugHUD")?
.GetMethod("RegisterCustomLine")?
.Invoke(null, new object[] { "Label/Name", (Func<string>)(() => { /* your logic */ return "MyLabel: someValue"; }) });
}
private void RegisterDebugHUD()
{
var apiType = Type.GetType("AGDDebugHUD.DebugHUDAPI, AGDDebugHUD");
if (apiType == null) return;
var registerHeaderMethod = apiType.GetMethod("RegisterHeader");
var registerLineMethod = apiType.GetMethod("RegisterCustomLine");
if (registerHeaderMethod == null || registerLineMethod == null) return;
var rb = GetComponent<Rigidbody>();
var header = registerHeaderMethod.Invoke(null, new object[] { "--- MyMod - Entity ---" });
// Position
Func<string> posGetter = () =>
{
var p = transform.position;
return string.Format("Pos: ({0:F2}, {1:F2}, {2:F2})", p.x, p.y, p.z);
};
registerLineMethod.Invoke(null, new object[] { "MyMod_Position", posGetter, header });
// Velocity
Func<string> velGetter = () =>
{
if (rb == null) return null;
var v = rb.linearVelocity;
return string.Format("Vel: {0:F3} m/s (X:{1:F2} Y:{2:F2} Z:{3:F2})", v.magnitude, v.x, v.y, v.z);
};
registerLineMethod.Invoke(null, new object[] { "MyMod_Velocity", velGetter, header });
// Custom state
Func<string> stateGetter = () =>
{
return string.Format("Last Used By: {0}", lastPlayerUse);
};
registerLineMethod.Invoke(null, new object[] { "MyMod_LastUse", stateGetter, header });
}
Official API mods (implementing AGD.ModAPI.IModPlugin) can use the same reflection pattern from OnLoad:
using AGD.ModAPI;
public class MyModPlugin : IModPlugin
{
public void OnLoad(IModContext context)
{
var apiType = Type.GetType("AGDDebugHUD.DebugHUDAPI, AGDDebugHUD");
if (apiType == null) return;
var registerHeaderMethod = apiType.GetMethod("RegisterHeader");
var registerLineMethod = apiType.GetMethod("RegisterCustomLine");
if (registerHeaderMethod == null || registerLineMethod == null) return;
var header = registerHeaderMethod.Invoke(null, new object[] { "--- MyMod ---" });
Func<string> myGetter = () => string.Format("My Value: {0}", someValue);
registerLineMethod.Invoke(null, new object[] { "MyMod_Value", myGetter, header });
}
public void OnUnload() { }
}
GameSingleton.Awake -> InjectDebugHUD creates the HUD GameObject (AGD_DebugHUD).DebugHUDBehaviour.Awake builds the Canvas/TextMeshPro UI, then calls DebugHUDAPI.Initialize, which creates TMP GameObjects for any lines already registered.DebugHUDBehaviour.Update refreshes built-in lines, then calls DebugHUDAPI.RefreshAll() which invokes all registered getters.RegisterCustomLine is called after Initialize, the line gets created immediately."MyPlugin_PlayerGold"). Labels become GameObject names and serve as the API key for EnableLine/DisableLine/FreezeLine.RegisterHeader to group related lines together. Headers render as bold gold text and make the overlay easier to scan.FindObjectOfType or heavy LINQ inside getters - cache references in your own Awake/Start.null from the getter (line disappears until next call) vs. DisableLine (line disappears until EnableLine is called).Type.GetType(...) == null) means your plugin continues to work normally - the debug lines are simply absent.AGDDebugHUD.dll into BepInEx/plugins/.[[AGDDebugHUD]] Debug HUD injected successfully. in the BepInEx console and the overlay in the top-left corner.You can reach me through the Official AGD Discord server @unii_le_chat or through the GitHub page.