Files
unity-ingame-logging/README.md
Sebastian Bularca 2872300873 added code from unity
2026-04-06 20:45:22 +02:00

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