# In-Game Logging System Design **Package:** `com.jovian.ingame-logging` **Date:** 2026-04-05 ## Purpose An optimized, low-allocation in-game logging system for displaying gameplay messages (roll values, damage, events) to the player. Injectable via constructor DI, not static. Saves/loads with the Jovian save system. ## Core Data Types ### LogChannel (readonly struct) Zero-allocation value type with string-based identity for serialization. ```csharp [Serializable] public readonly struct LogChannel : IEquatable { [SerializeField] readonly string id; public string Id => id; public LogChannel(string id) => this.id = id; // Built-in channels public static readonly LogChannel Combat = new("Combat"); public static readonly LogChannel CharacterCreation = new("CharacterCreation"); public static readonly LogChannel World = new("World"); public static readonly LogChannel General = new("General"); // IEquatable, ==, !=, GetHashCode via id } ``` Game code can define additional channels: `new LogChannel("Crafting")`. Serialized via Newtonsoft `JsonConverter` that reads/writes the `id` string. ### LogEntry (readonly struct) ```csharp [Serializable] public readonly struct LogEntry { public readonly string message; // supports TMP rich text tags public readonly LogChannel channel; public readonly float gameTime; // Time.time when logged } ``` ## Architecture ``` Game Code (CharacterCreationView, CombatSystem, etc.) │ var log = new InGameLogger(store, LogChannel.Combat); │ log.Log("Rolled 18 for Might"); │ log.Log("Critical Hit! 24 damage"); ▼ InGameLogger (readonly struct, per-caller facade) │ Holds: IGameLogStore reference + LogChannel │ log.Log(msg) → store.Add(channel, msg) │ log.Log(msg, color) → store.Add(channel, colored msg) ▼ IGameLogStore (singleton service, created in EntryPoint) │ Ring buffer with configurable max capacity (default 500) │ Add(channel, message), Clear(), Clear(channel) │ GetEntries(), GetEntries(channel) │ event Action OnEntryAdded │ event Action OnCleared │ SaveLog(ISaveSystem, sessionId), LoadLog(ISaveSystem, sessionId) ▼ GameLogView (MonoBehaviour on UI prefab) ScrollRect + VerticalLayoutGroup Object pool of LogEntryView instances Subscribes to IGameLogStore.OnEntryAdded / OnCleared Optional channel filter (show one or all channels) Auto-scroll to bottom, pause on manual scroll-up ``` ## InGameLogger (Facade Struct) ```csharp public readonly struct InGameLogger { readonly IGameLogStore store; readonly LogChannel channel; public InGameLogger(IGameLogStore store, LogChannel channel) { this.store = store; this.channel = channel; } public void Log(string message) => store.Add(channel, message); public void Log(string message, string hexColor) => store.Add(channel, $"{message}"); } ``` - No allocations beyond the message string itself - Each caller (view, system) creates its own instance scoped to a channel - Injected via `IGameLogStore` passed through constructor DI ## IGameLogStore (Service Interface) ```csharp public interface IGameLogStore { int Count { get; } int Capacity { get; } void Add(LogChannel channel, string message); void Clear(); void Clear(LogChannel channel); ReadOnlySpan GetEntries(); // Filtered access for UI int GetEntries(LogChannel channel, List results); event Action OnEntryAdded; event Action OnCleared; // Save integration GameLogSaveData GetSaveData(); void RestoreFromSaveData(GameLogSaveData data); } ``` ## GameLogStore (Implementation) - **Ring buffer** backed by `LogEntry[]` array - Configurable capacity (default 500), oldest entries evicted when full - `Add()` writes to next slot, increments head pointer, fires `OnEntryAdded` - `Clear()` resets head/count, fires `OnCleared` - `Clear(LogChannel)` removes entries matching channel, compacts buffer ## Save Integration **Dependency direction:** `com.jovian.ingame-logging` depends on `com.jovian.savesystem`. ```csharp [Serializable] public sealed class GameLogSaveData { public List entries; // all channels, in order } ``` - `GetSaveData()` snapshots the ring buffer into a `GameLogSaveData` - `RestoreFromSaveData()` rebuilds the ring buffer and notifies UI - All channels saved together in one payload, each entry tagged with its `LogChannel` - Game code calls save/load alongside its own persistence: - Save: `saveSystem.Save(logStore.GetSaveData())` in the same session - Load: `logStore.RestoreFromSaveData(saveSystem.Load(slot))` ## UI Design ### GameLogView (MonoBehaviour) - Attached to a prefab with `ScrollRect` + `Content` (VerticalLayoutGroup) - Holds reference to a `LogEntryView` prefab (for pooling) - Subscribes to `IGameLogStore.OnEntryAdded` and `OnCleared` - Object pool: pre-warms ~20 `LogEntryView` instances - When pool is exhausted, recycles oldest visible entries - Optional `LogChannel` filter field (null = show all) - Auto-scrolls to bottom; pauses auto-scroll when user scrolls up manually ### LogEntryView (MonoBehaviour) - Minimal: single `TMP_Text` serialized field reference - `SetEntry(LogEntry entry)` sets text content - TMP `richText = true` by default (supports ``, ``, ``, etc.) ## Package Structure ``` Packages/com.jovian.ingame-logging/ ├── package.json # com.jovian.ingame-logging, deps: savesystem, TMP ├── Runtime/ │ ├── Jovian.InGameLogging.asmdef │ ├── LogChannel.cs │ ├── LogEntry.cs │ ├── InGameLogger.cs │ ├── IGameLogStore.cs │ ├── GameLogStore.cs │ ├── GameLogSaveData.cs │ ├── LogChannelJsonConverter.cs │ └── UI/ │ ├── GameLogView.cs │ └── LogEntryView.cs └── Editor/ └── Jovian.InGameLogging.Editor.asmdef ``` ### Dependencies - `com.jovian.savesystem` (for ISaveSystem) - `com.unity.textmeshpro` (for TMP_Text) - `com.unity.nuget.newtonsoft-json` (for LogChannel serialization) ### Assembly References - `Jovian.InGameLogging` references: `Jovian.SaveSystem`, `Unity.TextMeshPro`, `Newtonsoft.Json` - `Jovian.InGameLogging.Editor` references: `Jovian.InGameLogging` ## Integration Points (Game Project) 1. **EntryPoint.cs**: Create `GameLogStore` instance, pass to game states that need logging 2. **Game state constructors**: Receive `IGameLogStore`, create `InGameLogger` structs per channel 3. **UI prefab**: Instantiate `GameLogView` prefab, call `Initialize(IGameLogStore)` 4. **Save/Load**: Call `GetSaveData()` / `RestoreFromSaveData()` alongside existing save flow ## Design Decisions | Decision | Rationale | |----------|-----------| | Readonly struct for LogChannel | Zero-alloc, value semantics, serializable via string id | | Readonly struct for InGameLogger | Per-caller facade with no heap allocation | | Ring buffer over List | Fixed capacity, no resizing, O(1) add, oldest eviction built-in | | Object pool for UI | Eliminates instantiation after warmup, O(visible) rendering cost | | Separate save file via save system | Logging depends on save system, not the other way around | | TMP rich text passthrough | No parsing overhead; callers compose rich text directly | | Simple Clear() API | Package stays decoupled from game-specific events |