added in-game logger

This commit is contained in:
Sebastian Bularca
2026-04-05 12:32:42 +02:00
parent 1ec734d033
commit fa15608f3a
43 changed files with 3019 additions and 8 deletions

View File

@@ -17,7 +17,10 @@
"Bash(grep -l \"bc14507c6a9500d4eac9e684e289d084\" /d/repos/trail-into-darkness/Assets/**/*.meta)",
"mcp__plugin_context7_context7__resolve-library-id",
"mcp__plugin_context7_context7__query-docs",
"Bash(find D:repostrail-into-darknessAssets -type f -name *View.cs)"
"Bash(find D:repostrail-into-darknessAssets -type f -name *View.cs)",
"Bash(grep -c '\\\\\\\\r')",
"Bash(xxd Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs)",
"Bash(find D:/repos/trail-into-darkness/Assets -name *.asmdef)"
]
}
}

View File

@@ -5,6 +5,7 @@ using Nox.Game.UI;
using Nox.Input;
using Nox.Platform;
using Jovian.SaveSystem;
using Jovian.InGameLogging;
using Nox.EditorCode;
using Unity.Profiling;
using UnityEngine;
@@ -24,6 +25,7 @@ namespace Nox.Core {
private ProfilerMarker createApplicationStateMarker = new ProfilerMarker("createApplicationState");
private ISceneTransition sceneTransition;
private ISaveSystem saveSystem;
private IGameLogStore gameLogStore;
private InitializerSettingsFile initializerSettings;
private BootstrapReferences bootstrapReferences;
@@ -98,6 +100,8 @@ namespace Nox.Core {
ISaveSlotManager saveSlotManager = new SaveSlotManager(saveStorage, saveSettings);
saveSystem = new SaveSystem(saveSerializer, saveStorage, saveSlotManager, saveSettings);
gameLogStore = new GameLogStore(500);
var adventureData = new AdventureData();
var characterBaseSettings = Addressables.LoadAssetAsync<StarterCharacterSettings>("CharacterBaseSettings").WaitForCompletion();
@@ -111,8 +115,8 @@ namespace Nox.Core {
applicationStates = new Dictionary<GameState, IGameState> {
[GameState.BootState] = new SplashGameState(bootstrapReferences, gameDataState),
[GameState.MainMenu] = new MainMenuGameState(gameDataState, menuGameStateData, bootstrapReferences, saveSystem, partyCreatorModel, adventureData),
[GameState.GameMode] = new GameModeGameState(gameDataState, bootstrapReferences, platform.PlatformSettings, saveSystem, sceneTransition, adventureData),
[GameState.MainMenu] = new MainMenuGameState(gameDataState, menuGameStateData, bootstrapReferences, saveSystem, partyCreatorModel, adventureData, gameLogStore),
[GameState.GameMode] = new GameModeGameState(gameDataState, bootstrapReferences, platform.PlatformSettings, saveSystem, sceneTransition, adventureData, gameLogStore),
};
createApplicationStateMarker.End();
}

View File

@@ -3,6 +3,7 @@ using Nox.Game;
using Nox.Platform;
using Nox.Game.UI;
using Jovian.SaveSystem;
using Jovian.InGameLogging;
using System.Collections.Generic;
using ZLinq;
using UnityEngine;
@@ -21,6 +22,7 @@ namespace Nox.Core {
private readonly ISaveSystem saveSystem;
private readonly ISceneTransition sceneTransition;
private readonly AdventureData adventuredata;
private readonly IGameLogStore gameLogStore;
private readonly Dictionary<PlayMode, IPlayMode?> playModeCache = new();
@@ -43,12 +45,14 @@ namespace Nox.Core {
PlatformSettings platformSettings,
ISaveSystem saveSystem,
ISceneTransition sceneTransition,
AdventureData adventuredata) {
AdventureData adventuredata,
IGameLogStore gameLogStore) {
this.gameDataState = gameDataState;
this.platformSettings = platformSettings;
this.saveSystem = saveSystem;
this.sceneTransition = sceneTransition;
this.adventuredata = adventuredata;
this.gameLogStore = gameLogStore;
bootstrapSettings = Addressables.LoadAssetAsync<PlayModeSettings>(bootstrapReferences.playModeSettings).WaitForCompletion();
}
@@ -75,7 +79,11 @@ namespace Nox.Core {
private NoxSavedDataSet? CaptureNoxSaveData() {
var adventure = FindAdventurePlayMode();
return adventure?.CaptureNoxSaveData();
var saveData = adventure?.CaptureNoxSaveData();
if(saveData != null) {
saveData.gameLogData = gameLogStore.GetSaveData();
}
return saveData;
}
private AdventurePlayMode? FindAdventurePlayMode() {

View File

@@ -1,5 +1,6 @@
using Nox.Game;
using Jovian.SaveSystem;
using Jovian.InGameLogging;
using Nox.UI;
using System;
using System.Threading.Tasks;
@@ -21,6 +22,7 @@ namespace Nox.Core {
private readonly BootstrapReferences bootstrapReferences;
private readonly ISaveSystem saveSystem;
private readonly PartyCreatorModel partyCreatorModel;
private readonly IGameLogStore gameLogStore;
private AdventureData adventureData;
private Action<PlayMode> onStartGameRequested;
private Action onContinueRequested;
@@ -33,13 +35,15 @@ namespace Nox.Core {
BootstrapReferences bootstrapReferences,
ISaveSystem saveSystem,
PartyCreatorModel partyCreatorModel,
AdventureData adventureData) {
AdventureData adventureData,
IGameLogStore gameLogStore) {
this.gameDataState = gameDataState;
this.menuGameStateData = menuGameStateData;
this.bootstrapReferences = bootstrapReferences;
this.saveSystem = saveSystem;
this.partyCreatorModel = partyCreatorModel;
this.adventureData = adventureData;
this.gameLogStore = gameLogStore;
}
public void EnterGameState() {
@@ -56,7 +60,7 @@ namespace Nox.Core {
gameDataState.ChangePlayMode(mode);
};
onContinueRequested = () => {
var saveData = NoxSaveData.RestoreSavedData(saveSystem, gameDataState, ref adventureData);
var saveData = NoxSaveData.RestoreSavedData(saveSystem, gameDataState, ref adventureData, gameLogStore);
if(saveData == null) {
return;
}

View File

@@ -1,4 +1,5 @@
using Jovian.SaveSystem;
using Jovian.InGameLogging;
using Nox.Core;
using System;
using ZLinq;
@@ -11,7 +12,8 @@ namespace Nox.Game {
public static NoxSavedDataSet RestoreSavedData(
ISaveSystem saveSystem,
GameDataState gameDataState,
ref AdventureData adventureData) {
ref AdventureData adventureData,
IGameLogStore gameLogStore = null) {
var sessions = saveSystem.GetAllSessions().AsValueEnumerable().OrderByDescending(s => s.lastSaveDateUtc).ToList();
if(sessions.Count == 0) {
return null;
@@ -36,6 +38,10 @@ namespace Nox.Game {
gameDataState.ActiveParty = saveData.partyDefinition;
adventureData = saveData.adventureData;
if(gameLogStore != null && saveData.gameLogData != null) {
gameLogStore.RestoreFromSaveData(saveData.gameLogData);
}
return saveData;
}
}
@@ -55,6 +61,9 @@ namespace Nox.Game {
// Party
public PartyDefinition partyDefinition;
public SerializableVector3 partyPosition;
// In-game log
public GameLogSaveData gameLogData;
}
/// <summary>

View File

@@ -107,6 +107,7 @@ RectTransform:
- {fileID: 7330333158031640233}
- {fileID: 9065981712073220368}
- {fileID: 1588673216748120674}
- {fileID: 8217010083354716901}
m_Father: {fileID: 2577996418906990456}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.33224753, y: 0}
@@ -1513,6 +1514,144 @@ RectTransform:
m_CorrespondingSourceObject: {fileID: 4353505352457658128, guid: 4778a14b7457e644ea23e45b40fc3391, type: 3}
m_PrefabInstance: {fileID: 4846874011051483482}
m_PrefabAsset: {fileID: 0}
--- !u!1001 &6018359966710660021
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 8410804869288043777}
m_Modifications:
- target: {fileID: 374280998538979084, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMax.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 374280998538979084, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1525412503934350410, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_Name
value: LogContainer
objectReference: {fileID: 0}
- target: {fileID: 2251566621526024109, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMax.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2251566621526024109, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2251566621526024109, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMin.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_Pivot.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_Pivot.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMax.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMax.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMin.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMin.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_SizeDelta.x
value: 737.42
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_SizeDelta.y
value: 504.25
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_LocalPosition.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_LocalRotation.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_LocalRotation.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_LocalRotation.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchoredPosition.x
value: 1.0002
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchoredPosition.y
value: -252.13
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7479392947244903680, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7479392947244903680, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7479392947244903680, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7479392947244903680, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
--- !u!224 &8217010083354716901 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
m_PrefabInstance: {fileID: 6018359966710660021}
m_PrefabAsset: {fileID: 0}
--- !u!1001 &6727870666199461325
PrefabInstance:
m_ObjectHideFlags: 0

View File

@@ -0,0 +1,893 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1447740470325782801
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6286131802514671469}
- component: {fileID: 7279458507301375298}
- component: {fileID: 7180659085512946289}
- component: {fileID: 1194623820596100263}
m_Layer: 0
m_Name: Viewport
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6286131802514671469
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1447740470325782801}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3059084814696477984}
m_Father: {fileID: 6248177754200331468}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0, y: 1}
--- !u!222 &7279458507301375298
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1447740470325782801}
m_CullTransparentMesh: 1
--- !u!114 &7180659085512946289
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1447740470325782801}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10917, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &1194623820596100263
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1447740470325782801}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Mask
m_ShowMaskGraphic: 0
--- !u!1 &1525412503934350410
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2417638750186197328}
- component: {fileID: 6899372841946195929}
m_Layer: 0
m_Name: LogContainer
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &2417638750186197328
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1525412503934350410}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 6248177754200331468}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 1.0002, y: -252.13}
m_SizeDelta: {x: 737.42, y: 504.25}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &6899372841946195929
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1525412503934350410}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 786fb23f28122964cb30678ea785bd40, type: 3}
m_Name:
m_EditorClassIdentifier: Jovian.InGameLogging::Jovian.InGameLogging.UI.GameLogView
scrollRect: {fileID: 2554617498295089481}
content: {fileID: 3059084814696477984}
entryPrefab: {fileID: 0}
poolSize: 20
--- !u!1 &2405629305058117087
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3059084814696477984}
- component: {fileID: 1744225011043606462}
m_Layer: 0
m_Name: Content
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &3059084814696477984
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2405629305058117087}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7479392947244903680}
m_Father: {fileID: 6286131802514671469}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 487.26}
m_Pivot: {x: 0, y: 1}
--- !u!114 &1744225011043606462
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2405629305058117087}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.VerticalLayoutGroup
m_Padding:
m_Left: 0
m_Right: 0
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 0
m_Spacing: 2
m_ChildForceExpandWidth: 1
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 0
m_ChildControlHeight: 0
m_ChildScaleWidth: 0
m_ChildScaleHeight: 0
m_ReverseArrangement: 0
--- !u!1 &3952660362247579954
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2688140319784364735}
- component: {fileID: 7390804912840202258}
- component: {fileID: 5246482949485489637}
- component: {fileID: 529710424251508929}
m_Layer: 0
m_Name: Scrollbar Horizontal
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &2688140319784364735
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3952660362247579954}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3366448768354673732}
m_Father: {fileID: 6248177754200331468}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 20}
m_Pivot: {x: 0, y: 0}
--- !u!222 &7390804912840202258
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3952660362247579954}
m_CullTransparentMesh: 1
--- !u!114 &5246482949485489637
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3952660362247579954}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &529710424251508929
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3952660362247579954}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 2a4db7a114972834c8e4117be1d82ba3, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Scrollbar
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 1342770482644207694}
m_HandleRect: {fileID: 2251566621526024109}
m_Direction: 0
m_Value: 1
m_Size: 0.99999994
m_NumberOfSteps: 0
m_OnValueChanged:
m_PersistentCalls:
m_Calls: []
--- !u!1 &5969083621566914297
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 374280998538979084}
- component: {fileID: 5436844256530954601}
- component: {fileID: 6879256843609484833}
m_Layer: 0
m_Name: Handle
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &374280998538979084
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5969083621566914297}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8977826534581480953}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5436844256530954601
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5969083621566914297}
m_CullTransparentMesh: 1
--- !u!114 &6879256843609484833
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5969083621566914297}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &6050805973784497029
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3366448768354673732}
m_Layer: 0
m_Name: Sliding Area
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &3366448768354673732
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6050805973784497029}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2251566621526024109}
m_Father: {fileID: 2688140319784364735}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: -8.5, y: 0}
m_SizeDelta: {x: -20, y: -20}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &6074956918412360435
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6248177754200331468}
- component: {fileID: 7678453280902357168}
- component: {fileID: 4027884099002409368}
- component: {fileID: 2554617498295089481}
m_Layer: 0
m_Name: Scroll View
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6248177754200331468
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6074956918412360435}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 6286131802514671469}
- {fileID: 2688140319784364735}
- {fileID: 4070132176653801724}
m_Father: {fileID: 2417638750186197328}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0.0024719}
m_SizeDelta: {x: 737.42, y: 504.26}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7678453280902357168
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6074956918412360435}
m_CullTransparentMesh: 1
--- !u!114 &4027884099002409368
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6074956918412360435}
m_Enabled: 0
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 0.392}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &2554617498295089481
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6074956918412360435}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1aa08ab6e0800fa44ae55d278d1423e3, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ScrollRect
m_Content: {fileID: 3059084814696477984}
m_Horizontal: 0
m_Vertical: 1
m_MovementType: 1
m_Elasticity: 0
m_Inertia: 1
m_DecelerationRate: 0.135
m_ScrollSensitivity: 1
m_Viewport: {fileID: 6286131802514671469}
m_HorizontalScrollbar: {fileID: 529710424251508929}
m_VerticalScrollbar: {fileID: 9136497547051495148}
m_HorizontalScrollbarVisibility: 2
m_VerticalScrollbarVisibility: 2
m_HorizontalScrollbarSpacing: -3
m_VerticalScrollbarSpacing: -3
m_OnValueChanged:
m_PersistentCalls:
m_Calls: []
--- !u!1 &6994548556054523628
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4070132176653801724}
- component: {fileID: 8682590904891029538}
- component: {fileID: 5114829985009070717}
- component: {fileID: 9136497547051495148}
m_Layer: 0
m_Name: Scrollbar Vertical
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4070132176653801724
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6994548556054523628}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8977826534581480953}
m_Father: {fileID: 6248177754200331468}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 1, y: 0}
m_AnchorMax: {x: 1, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 0}
m_Pivot: {x: 1, y: 1}
--- !u!222 &8682590904891029538
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6994548556054523628}
m_CullTransparentMesh: 1
--- !u!114 &5114829985009070717
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6994548556054523628}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &9136497547051495148
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6994548556054523628}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 2a4db7a114972834c8e4117be1d82ba3, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Scrollbar
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 6879256843609484833}
m_HandleRect: {fileID: 374280998538979084}
m_Direction: 2
m_Value: 1
m_Size: 1
m_NumberOfSteps: 0
m_OnValueChanged:
m_PersistentCalls:
m_Calls: []
--- !u!1 &7187724186482890403
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8977826534581480953}
m_Layer: 0
m_Name: Sliding Area
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &8977826534581480953
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7187724186482890403}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 374280998538979084}
m_Father: {fileID: 4070132176653801724}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 8.5}
m_SizeDelta: {x: -20, y: -20}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &7193882241213188023
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2251566621526024109}
- component: {fileID: 1372064744394430487}
- component: {fileID: 1342770482644207694}
m_Layer: 0
m_Name: Handle
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &2251566621526024109
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7193882241213188023}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3366448768354673732}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &1372064744394430487
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7193882241213188023}
m_CullTransparentMesh: 1
--- !u!114 &1342770482644207694
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7193882241213188023}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1001 &7098424197235870041
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 3059084814696477984}
m_Modifications:
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_Pivot.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_Pivot.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_AnchorMax.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_AnchorMin.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_SizeDelta.x
value: 720.42
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_SizeDelta.y
value: 108.565
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_LocalPosition.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_LocalRotation.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_LocalRotation.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_LocalRotation.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6501593483943143564, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
propertyPath: m_Name
value: LogEntry
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
--- !u!224 &7479392947244903680 stripped
RectTransform:
m_CorrespondingSourceObject: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
m_PrefabInstance: {fileID: 7098424197235870041}
m_PrefabAsset: {fileID: 0}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1b41f907ca960b644ae3af6e1942b9fb
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,210 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &6501593483943143564
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 382400732949652569}
- component: {fileID: 4832918257971952213}
- component: {fileID: 1727094465477629914}
m_Layer: 0
m_Name: LogEntry
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &382400732949652569
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6501593483943143564}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8597194705437786868}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 1093, y: 615}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &4832918257971952213
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6501593483943143564}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5526887cdf77a54439357be8b5754ffc, type: 3}
m_Name:
m_EditorClassIdentifier: Jovian.InGameLogging::Jovian.InGameLogging.UI.LogEntryView
messageText: {fileID: 4259574353180979179}
--- !u!114 &1727094465477629914
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6501593483943143564}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredHeight: 100
m_FlexibleWidth: 1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!1 &8033193440249993941
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8597194705437786868}
- component: {fileID: 8331827018725656388}
- component: {fileID: 4259574353180979179}
m_Layer: 0
m_Name: Text (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &8597194705437786868
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8033193440249993941}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 382400732949652569}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: -0.0000000044237822}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8331827018725656388
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8033193440249993941}
m_CullTransparentMesh: 1
--- !u!114 &4259574353180979179
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8033193440249993941}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: New Text
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294967295
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 24
m_fontSizeBase: 36
m_fontWeight: 400
m_enableAutoSizing: 1
m_fontSizeMin: 5
m_fontSizeMax: 24
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 3
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 0
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9d1c7837b0b5a9f45baa84f326fc247c
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1e182a45ed498c445b141e9ec6395805
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,16 @@
{
"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
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 48c945ba5ea83b144b5bbf4eaf33fe29
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,230 @@
# 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.");
```
## 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
### 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
- `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

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8a8d67e42da5eea4a873d8f632bacbbe
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 51325b1e7a05b6740a458bdcae9f8998
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 965eab6edce9dbc49b93d0bda0ad6f6c

View File

@@ -0,0 +1,106 @@
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) {
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);
}
var result = new LogEntry[count];
int start = head;
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;
}
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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1eb9ff03ddd225e46852cb92ba213bc7

View File

@@ -0,0 +1,22 @@
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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b5fac89eac17e874e888928c9618e812

View File

@@ -0,0 +1,23 @@
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>");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 79d11c151d20d2c41a4ad5e288a4f16f

View File

@@ -0,0 +1,19 @@
{
"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
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6053f37e557f955418ef96fdf46f7d6b
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,44 @@
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) {
return string.Equals(id, other.id, StringComparison.Ordinal);
}
public override bool Equals(object obj) {
return obj is LogChannel other && Equals(other);
}
public override int GetHashCode() {
return id != null ? id.GetHashCode() : 0;
}
public override string ToString() {
return id ?? string.Empty;
}
public static bool operator ==(LogChannel left, LogChannel right) {
return left.Equals(right);
}
public static bool operator !=(LogChannel left, LogChannel right) {
return !left.Equals(right);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 08b1132b10325ce4d922fa7b207db4e0

View File

@@ -0,0 +1,15 @@
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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3c3eb6d23e195e345a3546749ca4e96f

View File

@@ -0,0 +1,20 @@
using System;
using Newtonsoft.Json;
namespace Jovian.InGameLogging {
[Serializable]
public readonly struct LogEntry {
[JsonProperty] public readonly string message;
[JsonProperty] [JsonConverter(typeof(LogChannelJsonConverter))]
public readonly LogChannel channel;
[JsonProperty] public readonly float gameTime;
public LogEntry(string message, LogChannel channel, float gameTime) {
this.message = message;
this.channel = channel;
this.gameTime = gameTime;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0f79543f908769543ac8fc14e138df62

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b1125aedc37d43948aeef186bccbeac8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,116 @@
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);
RebuildFromStore();
}
void OnDestroy() {
if(store != null) {
store.OnEntryAdded -= HandleEntryAdded;
store.OnCleared -= HandleCleared;
}
if(scrollRect != null) {
scrollRect.onValueChanged.RemoveListener(HandleScrollChanged);
}
}
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 {
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.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) {
autoScroll = position.y <= 0.01f;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 786fb23f28122964cb30678ea785bd40

View File

@@ -0,0 +1,16 @@
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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5526887cdf77a54439357be8b5754ffc

View File

@@ -0,0 +1,3 @@
# Samples
This folder is reserved for sample scenes and scripts demonstrating the In-Game Logging system.

View File

@@ -0,0 +1,20 @@
{
"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"
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: acdee7574039a3a48980e2cc9c6fe31d
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -18,6 +18,16 @@
"source": "embedded",
"dependencies": {}
},
"com.jovian.ingame-logging": {
"version": "file:com.jovian.ingame-logging",
"depth": 0,
"source": "embedded",
"dependencies": {
"com.unity.textmeshpro": "3.0.6",
"com.unity.nuget.newtonsoft-json": "3.2.1",
"com.jovian.savesystem": "0.1.0"
}
},
"com.jovian.inspector-tools": {
"version": "file:com.jovian.inspector-tools",
"depth": 0,
@@ -354,6 +364,14 @@
},
"url": "https://packages.unity.com"
},
"com.unity.textmeshpro": {
"version": "5.0.0",
"depth": 1,
"source": "builtin",
"dependencies": {
"com.unity.ugui": "2.0.0"
}
},
"com.unity.timeline": {
"version": "1.8.11",
"depth": 0,

View File

@@ -0,0 +1,215 @@
# 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<LogChannel> {
[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("<color=#FFD700>Critical Hit!</color> 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<LogEntry> 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, $"<color={hexColor}>{message}</color>");
}
```
- 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<LogEntry> GetEntries();
// Filtered access for UI
int GetEntries(LogChannel channel, List<LogEntry> results);
event Action<LogEntry> 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<LogEntry> 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<GameLogSaveData>(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 `<color>`, `<b>`, `<size>`, 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 |

View File

@@ -0,0 +1,757 @@
# 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**
```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**
```json
{
"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**
```json
{
"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**
```bash
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**
```csharp
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**
```bash
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**
```csharp
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**
```bash
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**
```csharp
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**
```bash
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**
```csharp
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**
```bash
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**
```csharp
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**
```bash
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**
```csharp
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**
```bash
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**
```csharp
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**
```bash
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**
```csharp
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**
```bash
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**
```csharp
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**
```bash
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**
```bash
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` (add `GameLogSaveData` field to `NoxSavedDataSet`)
**Step 1: Add the field**
Add to `NoxSavedDataSet` class at `Assets/Code/GameState/NoxSaveData.cs:50`:
```csharp
using Jovian.InGameLogging; // add to imports
// Inside NoxSavedDataSet class, add:
public GameLogSaveData gameLogData;
```
**Step 2: Commit**
```bash
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:
```csharp
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**
```bash
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 `NoxSavedDataSet` is populated before save (look for `saveSystem.Save` calls)
- Modify: `Assets/Code/GameState/NoxSaveData.cs` (`RestoreSavedData` method)
**Step 1: Populate log data before save**
Wherever the game creates `NoxSavedDataSet` before saving, add:
```csharp
savedDataSet.gameLogData = gameLogStore.GetSaveData();
```
**Step 2: Restore log data after load**
In `NoxSaveData.RestoreSavedData()`, after loading save data (~line 35), add:
```csharp
// Pass gameLogStore as parameter to RestoreSavedData, then:
if(saveData.gameLogData != null) {
gameLogStore.RestoreFromSaveData(saveData.gameLogData);
}
```
**Step 3: Commit**
```bash
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 |