Files

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.textmeshpro 3.0.6+
    • com.unity.nuget.newtonsoft-json 3.2.1+
    • com.jovian.savesystem 0.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

  1. Create a new GameObject, rename it LogEntry
  2. Add a LayoutElement component. Set Preferred Height to your desired row height (e.g. 24). Enable Flexible Width so it stretches to the scroll content width
  3. Add a child GameObject with a TextMeshPro - Text (UI) component
    • Set Overflow to Ellipsis or Truncate (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 Text should remain enabled (default)
  4. Add the LogEntryView component to the root LogEntry GameObject
  5. Drag the child TMP_Text into the LogEntryView.messageText field
  6. Save as a prefab

GameLogView prefab

  1. Create a new UI GameObject with a ScrollRect component
    • Disable Horizontal scrolling, keep Vertical enabled
    • Set Movement Type to Clamped or Elastic as preferred
    • Set Scroll Sensitivity to a comfortable value (e.g. 20)
  2. Add a child Content GameObject as the scroll content area
    • Add a RectTransform anchored top-left, pivot (0, 1), with a VerticalLayoutGroup:
      • Child Alignment: Upper Left
      • Child Force Expand Width: true, Height: false
      • Spacing: 2 (gap between log rows)
      • Padding: set as needed
    • Add a ContentSizeFitter with Vertical Fit set to Preferred Size (so it grows as entries are added)
    • Assign this as the ScrollRect.Content
  3. Optionally add a Scrollbar child for the vertical scrollbar, and assign it to ScrollRect.Vertical Scrollbar
  4. Optionally add a Mask or RectMask2D on the scroll viewport to clip entries outside the visible area
  5. Add the GameLogView component to the root GameObject
  6. Assign the fields in the Inspector:
    • scrollRect: the ScrollRect component
    • content: the Content child's RectTransform
    • entryPrefab: your LogEntryView prefab
    • poolSize: 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) -- constructor
  • string 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 to
  • float gameTime -- Time.time when the entry was added

InGameLogger (readonly struct)

  • InGameLogger(IGameLogStore store, LogChannel channel) -- constructor
  • void Log(string message) -- add an entry to the store
  • void Log(string message, string hexColor) -- add a color-wrapped entry
  • void Enable() -- enable this logger's channel
  • void 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 size
  • int Count -- current number of entries
  • int Capacity -- maximum entries before oldest are overwritten
  • void Add(LogChannel channel, string message) -- add an entry (silently dropped if channel is disabled)
  • void EnableChannel(LogChannel channel) -- enable a channel
  • void DisableChannel(LogChannel channel) -- disable a channel
  • bool IsChannelEnabled(LogChannel channel) -- check if a channel is enabled
  • void Clear() -- remove all entries
  • void Clear(LogChannel channel) -- remove entries for a specific channel
  • ReadOnlySpan<LogEntry> GetEntries() -- all entries in chronological order
  • int GetEntries(LogChannel channel, List<LogEntry> results) -- filtered entries, returns count
  • event Action<LogEntry> OnEntryAdded -- fires after each new entry
  • event Action OnCleared -- fires after Clear() or RestoreFromSaveData()
  • GameLogSaveData GetSaveData() -- snapshot for serialization
  • void 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, content RectTransform, and LogEntryView prefab assigned in Inspector

LogEntryView (MonoBehaviour) -- Jovian.InGameLogging.UI

  • void SetEntry(in LogEntry entry) -- display an entry
  • void ClearEntry() -- reset the text
  • Requires: TMP_Text reference assigned in Inspector

LogChannelJsonConverter

  • Newtonsoft.Json JsonConverter<LogChannel> -- serializes LogChannel as its string ID