# 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. ```csharp // 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. ```csharp var combatLogger = new InGameLogger(logStore, LogChannel.Combat); var worldLogger = new InGameLogger(logStore, LogChannel.World); ``` ### 3. Log messages ```csharp 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: ```csharp // 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: ```csharp var gameLogView = Object.FindFirstObjectByType(); 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: ```csharp 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: ```csharp 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. ```csharp 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: ```csharp 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 `` tag automatically: ```csharp logger.Log("Poisoned!", "#00FF00"); // Stored as: Poisoned! ``` You can also embed TMP tags directly in the message string: ```csharp logger.Log("Gained +5 experience."); ``` ## Save / Load Integration `GameLogStore` supports serializing its contents for use with a save system. ### Save ```csharp GameLogSaveData saveData = logStore.GetSaveData(); // Serialize saveData into your save file ``` ### Load ```csharp // 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`, 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 GetEntries()` -- all entries in chronological order - `int GetEntries(LogChannel channel, List results)` -- filtered entries, returns count - `event Action 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 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` -- serializes `LogChannel` as its string ID