21 KiB
In-Game Logging System Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Build com.jovian.ingame-logging, a low-allocation in-game logging package with pooled UI, channel-based filtering, and save system integration.
Architecture: Injectable IGameLogStore service with ring buffer storage. InGameLogger readonly struct facades scoped to a LogChannel. Pooled GameLogView MonoBehaviour for ScrollRect UI. Save data as GameLogSaveData serializable type, persisted as a field in the game's NoxSavedDataSet.
Tech Stack: Unity 6 / C# 9, TextMeshPro, Newtonsoft.Json, Jovian SaveSystem
Task 1: Package scaffold
Files:
- Create:
Packages/com.jovian.ingame-logging/package.json - Create:
Packages/com.jovian.ingame-logging/Runtime/Jovian.InGameLogging.asmdef - Create:
Packages/com.jovian.ingame-logging/Editor/Jovian.InGameLogging.Editor.asmdef
Step 1: Create package.json
{
"name": "com.jovian.ingame-logging",
"version": "0.1.0",
"displayName": "Jovian In-Game Logging",
"description": "An optimized, low-allocation in-game logging system with pooled UI, channel-based filtering, and save system integration.",
"unity": "2022.3",
"dependencies": {
"com.unity.textmeshpro": "3.0.6",
"com.unity.nuget.newtonsoft-json": "3.2.1",
"com.jovian.savesystem": "0.1.0"
},
"keywords": [
"logging",
"ui",
"ingame"
],
"author": {
"name": "Jovian"
}
}
Step 2: Create Runtime asmdef
{
"name": "Jovian.InGameLogging",
"rootNamespace": "Jovian.InGameLogging",
"references": [
"Unity.TextMeshPro",
"Jovian.SaveSystem"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"Newtonsoft.Json.dll"
],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
Step 3: Create Editor asmdef
{
"name": "Jovian.InGameLogging.Editor",
"rootNamespace": "Jovian.InGameLogging.Editor",
"references": [
"Jovian.InGameLogging"
],
"includePlatforms": ["Editor"],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
Step 4: Commit
git add Packages/com.jovian.ingame-logging/
git commit -m "feat: scaffold com.jovian.ingame-logging package"
Task 2: LogChannel readonly struct
Files:
- Create:
Packages/com.jovian.ingame-logging/Runtime/LogChannel.cs
Step 1: Implement LogChannel
using System;
using UnityEngine;
namespace Jovian.InGameLogging {
[Serializable]
public readonly struct LogChannel : IEquatable<LogChannel> {
[SerializeField] readonly string id;
public string Id => id;
public LogChannel(string id) {
this.id = id;
}
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");
public bool Equals(LogChannel other) => string.Equals(id, other.id, StringComparison.Ordinal);
public override bool Equals(object obj) => obj is LogChannel other && Equals(other);
public override int GetHashCode() => id != null ? id.GetHashCode() : 0;
public override string ToString() => id ?? string.Empty;
public static bool operator ==(LogChannel left, LogChannel right) => left.Equals(right);
public static bool operator !=(LogChannel left, LogChannel right) => !left.Equals(right);
}
}
Step 2: Commit
git add Packages/com.jovian.ingame-logging/Runtime/LogChannel.cs
git commit -m "feat: add LogChannel readonly struct with built-in channels"
Task 3: LogChannelJsonConverter
Files:
- Create:
Packages/com.jovian.ingame-logging/Runtime/LogChannelJsonConverter.cs
Step 1: Implement Newtonsoft converter
using System;
using Newtonsoft.Json;
namespace Jovian.InGameLogging {
public sealed class LogChannelJsonConverter : JsonConverter<LogChannel> {
public override void WriteJson(JsonWriter writer, LogChannel value, JsonSerializer serializer) {
writer.WriteValue(value.Id);
}
public override LogChannel ReadJson(JsonReader reader, Type objectType, LogChannel existingValue, bool hasExistingValue, JsonSerializer serializer) {
var id = reader.Value as string;
return new LogChannel(id);
}
}
}
Step 2: Commit
git add Packages/com.jovian.ingame-logging/Runtime/LogChannelJsonConverter.cs
git commit -m "feat: add Newtonsoft JSON converter for LogChannel"
Task 4: LogEntry readonly struct
Files:
- Create:
Packages/com.jovian.ingame-logging/Runtime/LogEntry.cs
Step 1: Implement LogEntry
using System;
using Newtonsoft.Json;
namespace Jovian.InGameLogging {
[Serializable]
public readonly struct LogEntry {
public readonly string message;
[JsonConverter(typeof(LogChannelJsonConverter))]
public readonly LogChannel channel;
public readonly float gameTime;
public LogEntry(string message, LogChannel channel, float gameTime) {
this.message = message;
this.channel = channel;
this.gameTime = gameTime;
}
}
}
Step 2: Commit
git add Packages/com.jovian.ingame-logging/Runtime/LogEntry.cs
git commit -m "feat: add LogEntry readonly struct"
Task 5: GameLogSaveData
Files:
- Create:
Packages/com.jovian.ingame-logging/Runtime/GameLogSaveData.cs
Step 1: Implement serializable save container
using System;
using System.Collections.Generic;
namespace Jovian.InGameLogging {
[Serializable]
public sealed class GameLogSaveData {
public List<LogEntry> entries;
public GameLogSaveData() {
entries = new List<LogEntry>();
}
public GameLogSaveData(List<LogEntry> entries) {
this.entries = entries;
}
}
}
Step 2: Commit
git add Packages/com.jovian.ingame-logging/Runtime/GameLogSaveData.cs
git commit -m "feat: add GameLogSaveData serializable container"
Task 6: IGameLogStore interface
Files:
- Create:
Packages/com.jovian.ingame-logging/Runtime/IGameLogStore.cs
Step 1: Define the store interface
using System;
using System.Collections.Generic;
namespace Jovian.InGameLogging {
public interface IGameLogStore {
int Count { get; }
int Capacity { get; }
void Add(LogChannel channel, string message);
void Clear();
void Clear(LogChannel channel);
ReadOnlySpan<LogEntry> GetEntries();
int GetEntries(LogChannel channel, List<LogEntry> results);
event Action<LogEntry> OnEntryAdded;
event Action OnCleared;
GameLogSaveData GetSaveData();
void RestoreFromSaveData(GameLogSaveData data);
}
}
Step 2: Commit
git add Packages/com.jovian.ingame-logging/Runtime/IGameLogStore.cs
git commit -m "feat: add IGameLogStore interface"
Task 7: GameLogStore ring buffer implementation
Files:
- Create:
Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs
Step 1: Implement the ring buffer store
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Jovian.InGameLogging {
public sealed class GameLogStore : IGameLogStore {
readonly LogEntry[] buffer;
int head;
int count;
public int Count => count;
public int Capacity => buffer.Length;
public event Action<LogEntry> OnEntryAdded;
public event Action OnCleared;
public GameLogStore(int capacity = 500) {
buffer = new LogEntry[capacity];
head = 0;
count = 0;
}
public void Add(LogChannel channel, string message) {
var entry = new LogEntry(message, channel, Time.time);
buffer[head] = entry;
head = (head + 1) % buffer.Length;
if(count < buffer.Length) {
count++;
}
OnEntryAdded?.Invoke(entry);
}
public void Clear() {
head = 0;
count = 0;
OnCleared?.Invoke();
}
public void Clear(LogChannel channel) {
// Compact: copy non-matching entries into a temp span, then rebuild
var kept = new List<LogEntry>(count);
var entries = GetEntries();
for(int i = 0; i < entries.Length; i++) {
if(entries[i].channel != channel) {
kept.Add(entries[i]);
}
}
head = 0;
count = 0;
for(int i = 0; i < kept.Count; i++) {
buffer[i] = kept[i];
count++;
}
head = count % buffer.Length;
OnCleared?.Invoke();
}
public ReadOnlySpan<LogEntry> GetEntries() {
if(count < buffer.Length) {
return new ReadOnlySpan<LogEntry>(buffer, 0, count);
}
// Buffer is full and wrapping - need to return in chronological order
var result = new LogEntry[count];
int start = head; // head points to the oldest entry when full
for(int i = 0; i < count; i++) {
result[i] = buffer[(start + i) % buffer.Length];
}
return result;
}
public int GetEntries(LogChannel channel, List<LogEntry> results) {
results.Clear();
var entries = GetEntries();
for(int i = 0; i < entries.Length; i++) {
if(entries[i].channel == channel) {
results.Add(entries[i]);
}
}
return results.Count;
}
public GameLogSaveData GetSaveData() {
var entries = GetEntries();
var list = new List<LogEntry>(entries.Length);
for(int i = 0; i < entries.Length; i++) {
list.Add(entries[i]);
}
return new GameLogSaveData(list);
}
public void RestoreFromSaveData(GameLogSaveData data) {
head = 0;
count = 0;
if(data?.entries == null) {
OnCleared?.Invoke();
return;
}
// Only restore up to capacity, taking the most recent entries
int startIndex = Math.Max(0, data.entries.Count - buffer.Length);
for(int i = startIndex; i < data.entries.Count; i++) {
buffer[count] = data.entries[i];
count++;
}
head = count % buffer.Length;
OnCleared?.Invoke(); // Signal UI to rebuild from scratch
}
}
}
Step 2: Commit
git add Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs
git commit -m "feat: add GameLogStore ring buffer implementation"
Task 8: InGameLogger facade struct
Files:
- Create:
Packages/com.jovian.ingame-logging/Runtime/InGameLogger.cs
Step 1: Implement the per-caller facade
using System.Runtime.CompilerServices;
namespace Jovian.InGameLogging {
public readonly struct InGameLogger {
readonly IGameLogStore store;
readonly LogChannel channel;
public InGameLogger(IGameLogStore store, LogChannel channel) {
this.store = store;
this.channel = channel;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(string message) {
store.Add(channel, message);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(string message, string hexColor) {
store.Add(channel, $"<color={hexColor}>{message}</color>");
}
}
}
Step 2: Commit
git add Packages/com.jovian.ingame-logging/Runtime/InGameLogger.cs
git commit -m "feat: add InGameLogger readonly struct facade"
Task 9: LogEntryView MonoBehaviour
Files:
- Create:
Packages/com.jovian.ingame-logging/Runtime/UI/LogEntryView.cs
Step 1: Implement the minimal UI entry component
using TMPro;
using UnityEngine;
namespace Jovian.InGameLogging.UI {
public class LogEntryView : MonoBehaviour {
[SerializeField] TMP_Text messageText;
public void SetEntry(in LogEntry entry) {
messageText.text = entry.message;
}
public void ClearEntry() {
messageText.text = string.Empty;
}
}
}
Step 2: Commit
git add Packages/com.jovian.ingame-logging/Runtime/UI/LogEntryView.cs
git commit -m "feat: add LogEntryView MonoBehaviour for pooled UI entries"
Task 10: GameLogView MonoBehaviour with object pooling
Files:
- Create:
Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs
Step 1: Implement the scrollable log view with object pool
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace Jovian.InGameLogging.UI {
public class GameLogView : MonoBehaviour {
[SerializeField] ScrollRect scrollRect;
[SerializeField] RectTransform content;
[SerializeField] LogEntryView entryPrefab;
[SerializeField] int poolSize = 20;
IGameLogStore store;
LogChannel? channelFilter;
bool autoScroll = true;
readonly List<LogEntryView> activeEntries = new();
readonly Stack<LogEntryView> pool = new();
public void Initialize(IGameLogStore store, LogChannel? channelFilter = null) {
this.store = store;
this.channelFilter = channelFilter;
WarmPool();
store.OnEntryAdded += HandleEntryAdded;
store.OnCleared += HandleCleared;
scrollRect.onValueChanged.AddListener(HandleScrollChanged);
// Populate existing entries
RebuildFromStore();
}
void OnDestroy() {
if(store != null) {
store.OnEntryAdded -= HandleEntryAdded;
store.OnCleared -= HandleCleared;
}
}
void WarmPool() {
for(int i = 0; i < poolSize; i++) {
var entry = Instantiate(entryPrefab, content);
entry.gameObject.SetActive(false);
pool.Push(entry);
}
}
LogEntryView GetFromPool() {
LogEntryView entry;
if(pool.Count > 0) {
entry = pool.Pop();
}
else {
// Recycle oldest active entry
entry = activeEntries[0];
activeEntries.RemoveAt(0);
}
entry.gameObject.SetActive(true);
entry.transform.SetAsLastSibling();
return entry;
}
void ReturnToPool(LogEntryView entry) {
entry.ClearEntry();
entry.gameObject.SetActive(false);
pool.Push(entry);
}
void HandleEntryAdded(LogEntry entry) {
if(channelFilter.HasValue && entry.channel != channelFilter.Value) {
return;
}
var view = GetFromPool();
view.SetEntry(in entry);
activeEntries.Add(view);
if(autoScroll) {
Canvas.ForceUpdateCanvases();
scrollRect.verticalNormalizedPosition = 0f;
}
}
void HandleCleared() {
for(int i = activeEntries.Count - 1; i >= 0; i--) {
ReturnToPool(activeEntries[i]);
}
activeEntries.Clear();
// If store still has entries (channel-specific clear), rebuild
if(store.Count > 0) {
RebuildFromStore();
}
}
void RebuildFromStore() {
var entries = store.GetEntries();
for(int i = 0; i < entries.Length; i++) {
if(channelFilter.HasValue && entries[i].channel != channelFilter.Value) {
continue;
}
var view = GetFromPool();
view.SetEntry(in entries[i]);
activeEntries.Add(view);
}
if(autoScroll) {
Canvas.ForceUpdateCanvases();
scrollRect.verticalNormalizedPosition = 0f;
}
}
void HandleScrollChanged(Vector2 position) {
// Pause auto-scroll when user scrolls up, resume when at bottom
autoScroll = position.y <= 0.01f;
}
}
}
Step 2: Commit
git add Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs
git commit -m "feat: add GameLogView with object pooling and scroll management"
Task 11: Generate Unity .meta files
Unity requires .meta files for every file and folder. These will be auto-generated by Unity when the editor imports the package. However, to make the package work without opening Unity, we need to ensure the directory structure is correct.
Step 1: Create placeholder to ensure Editor directory exists
Create an empty Editor/.gitkeep or let Unity generate metas on next editor open. Since this project builds through Unity Editor, the meta files will be generated automatically on next load.
Step 2: Commit all remaining files
git add Packages/com.jovian.ingame-logging/
git commit -m "feat: complete com.jovian.ingame-logging package"
Task 12: Game integration - Add GameLogSaveData to NoxSavedDataSet
Files:
- Modify:
Assets/Code/GameState/NoxSaveData.cs(addGameLogSaveDatafield toNoxSavedDataSet)
Step 1: Add the field
Add to NoxSavedDataSet class at Assets/Code/GameState/NoxSaveData.cs:50:
using Jovian.InGameLogging; // add to imports
// Inside NoxSavedDataSet class, add:
public GameLogSaveData gameLogData;
Step 2: Commit
git add Assets/Code/GameState/NoxSaveData.cs
git commit -m "feat: add GameLogSaveData field to NoxSavedDataSet"
Task 13: Game integration - Wire GameLogStore in EntryPoint
Files:
- Modify:
Assets/Code/Core/EntryPoint.cs
Step 1: Create GameLogStore and pass it to game states
In EntryPoint.CreateApplicationStates(), after the character systems creation (~line 109), add:
using Jovian.InGameLogging; // add to imports
// After characterSystems creation:
var gameLogStore = new GameLogStore(500);
Then pass gameLogStore as IGameLogStore to the game state constructors that need it. The exact constructors depend on which states need logging - at minimum GameModeGameState for combat/adventure logging.
Step 2: Commit
git add Assets/Code/Core/EntryPoint.cs
git commit -m "feat: wire GameLogStore in EntryPoint"
Task 14: Game integration - Save/Load log data
Files:
- Modify: wherever
NoxSavedDataSetis populated before save (look forsaveSystem.Savecalls) - Modify:
Assets/Code/GameState/NoxSaveData.cs(RestoreSavedDatamethod)
Step 1: Populate log data before save
Wherever the game creates NoxSavedDataSet before saving, add:
savedDataSet.gameLogData = gameLogStore.GetSaveData();
Step 2: Restore log data after load
In NoxSaveData.RestoreSavedData(), after loading save data (~line 35), add:
// Pass gameLogStore as parameter to RestoreSavedData, then:
if(saveData.gameLogData != null) {
gameLogStore.RestoreFromSaveData(saveData.gameLogData);
}
Step 3: Commit
git add Assets/Code/GameState/NoxSaveData.cs
git commit -m "feat: integrate log save/load with game persistence"
File Summary
| File | Type | Purpose |
|---|---|---|
Packages/com.jovian.ingame-logging/package.json |
Create | Package manifest |
Packages/com.jovian.ingame-logging/Runtime/Jovian.InGameLogging.asmdef |
Create | Runtime assembly |
Packages/com.jovian.ingame-logging/Editor/Jovian.InGameLogging.Editor.asmdef |
Create | Editor assembly |
Packages/com.jovian.ingame-logging/Runtime/LogChannel.cs |
Create | Channel type (readonly struct) |
Packages/com.jovian.ingame-logging/Runtime/LogChannelJsonConverter.cs |
Create | Newtonsoft serialization |
Packages/com.jovian.ingame-logging/Runtime/LogEntry.cs |
Create | Entry type (readonly struct) |
Packages/com.jovian.ingame-logging/Runtime/GameLogSaveData.cs |
Create | Serializable save container |
Packages/com.jovian.ingame-logging/Runtime/IGameLogStore.cs |
Create | Store interface |
Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs |
Create | Ring buffer implementation |
Packages/com.jovian.ingame-logging/Runtime/InGameLogger.cs |
Create | Per-caller facade struct |
Packages/com.jovian.ingame-logging/Runtime/UI/LogEntryView.cs |
Create | Entry UI component |
Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs |
Create | Scrollable log view with pool |
Assets/Code/GameState/NoxSaveData.cs |
Modify | Add GameLogSaveData field + restore |
Assets/Code/Core/EntryPoint.cs |
Modify | Create and wire GameLogStore |