263 lines
9.6 KiB
Markdown
263 lines
9.6 KiB
Markdown
# 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>();
|
|
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 `<color>` tag automatically:
|
|
|
|
```csharp
|
|
logger.Log("Poisoned!", "#00FF00");
|
|
// Stored as: <color=#00FF00>Poisoned!</color>
|
|
```
|
|
|
|
You can also embed TMP tags directly in the message string:
|
|
|
|
```csharp
|
|
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
|
|
|
|
```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<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
|