Jovian In-Game Logging
A low-allocation in-game logging system for Unity with channel-based filtering, pooled UI, and save system integration. This is a player-facing game log (combat feed, event history), not a debug logger.
Requirements
- Unity 2022.3 or later
- Dependencies:
com.unity.textmeshpro3.0.6+com.unity.nuget.newtonsoft-json3.2.1+com.jovian.savesystem0.1.0+
Quick Start
1. Create the store
GameLogStore is the central service that holds log entries in a ring buffer. Create it once and pass it via constructor injection.
// Default capacity of 500 entries
var logStore = new GameLogStore();
// Or specify a custom capacity
var logStore = new GameLogStore(capacity: 1000);
2. Create a logger
InGameLogger is a lightweight facade bound to a specific channel. Create one per system or class that needs to write log entries.
var combatLogger = new InGameLogger(logStore, LogChannel.Combat);
var worldLogger = new InGameLogger(logStore, LogChannel.World);
3. Log messages
combatLogger.Log("Kael strikes the skeleton for 12 damage.");
combatLogger.Log("Critical hit!", "#FF4444");
4. Set up the UI
Add a GameLogView component to a GameObject with a ScrollRect. Assign the ScrollRect, a content RectTransform, and a LogEntryView prefab in the Inspector. Then initialize from code:
// Show all channels
gameLogView.Initialize(logStore);
// Or filter to a single channel
gameLogView.Initialize(logStore, LogChannel.Combat);
The view uses object pooling internally. It auto-scrolls to the bottom as new entries arrive and pauses auto-scroll when the player scrolls up manually.
Prefab Setup
The UI requires two prefabs: a LogEntryView prefab (the individual log row) and a GameLogView prefab (the scrollable container).
LogEntryView prefab
- Create a new GameObject, rename it
LogEntry - Add a
LayoutElementcomponent. SetPreferred Heightto your desired row height (e.g. 24). EnableFlexible Widthso it stretches to the scroll content width - Add a child GameObject with a
TextMeshPro - Text (UI)component- Set
OverflowtoEllipsisorTruncate(prevents text from spilling outside the row) - Anchor and stretch it to fill the parent (
Left: 0, Right: 0, Top: 0, Bottom: 0) - Set font, font size, and color to match your game's UI style
Rich Textshould remain enabled (default)
- Set
- Add the
LogEntryViewcomponent to the rootLogEntryGameObject - Drag the child
TMP_Textinto theLogEntryView.messageTextfield - Save as a prefab
GameLogView prefab
- Create a new UI GameObject with a
ScrollRectcomponent- Disable
Horizontalscrolling, keepVerticalenabled - Set
Movement TypetoClampedorElasticas preferred - Set
Scroll Sensitivityto a comfortable value (e.g. 20)
- Disable
- Add a child
ContentGameObject as the scroll content area- Add a
RectTransformanchored top-left, pivot(0, 1), with aVerticalLayoutGroup:Child Alignment: Upper LeftChild Force Expand Width: true,Height: falseSpacing: 2 (gap between log rows)Padding: set as needed
- Add a
ContentSizeFitterwithVertical Fitset toPreferred Size(so it grows as entries are added) - Assign this as the
ScrollRect.Content
- Add a
- Optionally add a
Scrollbarchild for the vertical scrollbar, and assign it toScrollRect.Vertical Scrollbar - Optionally add a
MaskorRectMask2Don the scroll viewport to clip entries outside the visible area - Add the
GameLogViewcomponent to the root GameObject - Assign the fields in the Inspector:
scrollRect: theScrollRectcomponentcontent: theContentchild'sRectTransformentryPrefab: yourLogEntryViewprefabpoolSize: number of pre-instantiated entries (default 20, increase for taller views)
Initialization from code
After instantiating or finding the GameLogView in the scene:
var gameLogView = Object.FindFirstObjectByType<GameLogView>();
gameLogView.Initialize(logStore);
// Or filtered to a single channel:
gameLogView.Initialize(logStore, LogChannel.Combat);
Log Channels
Built-in channels
| Channel | Usage |
|---|---|
LogChannel.Combat |
Combat events, damage, abilities |
LogChannel.CharacterCreation |
Character creation flow |
LogChannel.World |
World events, exploration |
LogChannel.General |
General-purpose messages |
Custom channels
LogChannel is a readonly struct keyed by a string ID. Define custom channels as static fields:
public static class MyLogChannels {
public static readonly LogChannel Trading = new("Trading");
public static readonly LogChannel Dialogue = new("Dialogue");
}
Then use them like any built-in channel:
var tradeLogger = new InGameLogger(logStore, MyLogChannels.Trading);
tradeLogger.Log("Sold Iron Sword for 50 gold.");
Enable / Disable Channels
Each logger can be toggled on or off. When disabled, calls to Log are silently dropped (no allocation, no entry added). All channels are enabled by default.
var combatLog = new InGameLogger(logStore, LogChannel.Combat);
combatLog.Disable();
combatLog.Log("This message is silently dropped.");
combatLog.Enable();
combatLog.Log("Back online.");
if(combatLog.IsEnabled) {
// check state
}
The state lives in the IGameLogStore, so disabling a channel from one InGameLogger instance disables it for all instances using the same store and channel. You can also call the store directly:
logStore.DisableChannel(LogChannel.World);
logStore.EnableChannel(LogChannel.World);
bool enabled = logStore.IsChannelEnabled(LogChannel.World);
Rich Text
Log messages support TextMeshPro rich text tags. The InGameLogger.Log(message, hexColor) overload wraps the message in a <color> tag automatically:
logger.Log("Poisoned!", "#00FF00");
// Stored as: <color=#00FF00>Poisoned!</color>
You can also embed TMP tags directly in the message string:
logger.Log("Gained <b>+5</b> <color=#FFD700>experience</color>.");
Save / Load Integration
GameLogStore supports serializing its contents for use with a save system.
Save
GameLogSaveData saveData = logStore.GetSaveData();
// Serialize saveData into your save file
Load
// Deserialize saveData from your save file
logStore.RestoreFromSaveData(saveData);
RestoreFromSaveData replaces the current buffer contents. If the save data contains more entries than the store's capacity, only the most recent entries are kept. The OnCleared event fires after restoration so the UI can rebuild.
JSON serialization
LogChannel is serialized as a plain string via the included LogChannelJsonConverter (Newtonsoft.Json). The converter is applied via attribute on LogEntry.channel, so no manual converter registration is needed.
API Reference
LogChannel (readonly struct)
LogChannel(string id)-- constructorstring Id-- the channel identifier- Static fields:
Combat,CharacterCreation,World,General - Implements
IEquatable<LogChannel>, ordinal string comparison
LogEntry (readonly struct)
string message-- the log message (may contain TMP rich text)LogChannel channel-- the channel this entry belongs tofloat gameTime--Time.timewhen the entry was added
InGameLogger (readonly struct)
InGameLogger(IGameLogStore store, LogChannel channel)-- constructorvoid Log(string message)-- add an entry to the storevoid Log(string message, string hexColor)-- add a color-wrapped entryvoid Enable()-- enable this logger's channelvoid Disable()-- disable this logger's channel (Log calls are silently dropped)bool IsEnabled-- whether this logger's channel is currently enabled
IGameLogStore / GameLogStore
GameLogStore(int capacity = 500)-- constructor with ring buffer sizeint Count-- current number of entriesint Capacity-- maximum entries before oldest are overwrittenvoid Add(LogChannel channel, string message)-- add an entry (silently dropped if channel is disabled)void EnableChannel(LogChannel channel)-- enable a channelvoid DisableChannel(LogChannel channel)-- disable a channelbool IsChannelEnabled(LogChannel channel)-- check if a channel is enabledvoid Clear()-- remove all entriesvoid Clear(LogChannel channel)-- remove entries for a specific channelReadOnlySpan<LogEntry> GetEntries()-- all entries in chronological orderint GetEntries(LogChannel channel, List<LogEntry> results)-- filtered entries, returns countevent Action<LogEntry> OnEntryAdded-- fires after each new entryevent Action OnCleared-- fires afterClear()orRestoreFromSaveData()GameLogSaveData GetSaveData()-- snapshot for serializationvoid RestoreFromSaveData(GameLogSaveData data)-- replace contents from save data
GameLogSaveData
List<LogEntry> entries-- serializable entry list
GameLogView (MonoBehaviour) -- Jovian.InGameLogging.UI
void Initialize(IGameLogStore store, LogChannel? channelFilter = null)-- bind to a store, optionally filtering to one channel- Requires:
ScrollRect, contentRectTransform, andLogEntryViewprefab assigned in Inspector
LogEntryView (MonoBehaviour) -- Jovian.InGameLogging.UI
void SetEntry(in LogEntry entry)-- display an entryvoid ClearEntry()-- reset the text- Requires:
TMP_Textreference assigned in Inspector
LogChannelJsonConverter
- Newtonsoft.Json
JsonConverter<LogChannel>-- serializesLogChannelas its string ID