From e7595bdc8981f757e9c6396ecc44c4f8aa45d6a7 Mon Sep 17 00:00:00 2001 From: Sebastian Bularca Date: Mon, 20 Apr 2026 09:51:08 +0200 Subject: [PATCH] Optimizations --- Runtime/DialogLineLibrary.cs | 5 +- Runtime/EncounterLink.cs | 13 ++--- Runtime/EncounterRegistry.cs | 34 +++++++++--- Runtime/EncounterResolver.cs | 9 +++- Runtime/EncounterTable.cs | 100 ++++++++++++++++++++++++++++------- Runtime/QuestLog.cs | 10 ++-- Runtime/QuestProgress.cs | 29 ++++++++-- 7 files changed, 152 insertions(+), 48 deletions(-) diff --git a/Runtime/DialogLineLibrary.cs b/Runtime/DialogLineLibrary.cs index 8f266e6..467cc04 100644 --- a/Runtime/DialogLineLibrary.cs +++ b/Runtime/DialogLineLibrary.cs @@ -34,12 +34,13 @@ namespace Jovian.EncounterSystem { return; } - cache = new Dictionary(); + cache = new Dictionary(lines?.Count ?? 0); if(lines == null) { return; } - foreach(var line in lines) { + for(int i = 0; i < lines.Count; i++) { + var line = lines[i]; if(line != null && !string.IsNullOrEmpty(line.id)) { cache[line.id] = line.text; } diff --git a/Runtime/EncounterLink.cs b/Runtime/EncounterLink.cs index 5e33563..b3c4cba 100644 --- a/Runtime/EncounterLink.cs +++ b/Runtime/EncounterLink.cs @@ -7,18 +7,13 @@ namespace Jovian.EncounterSystem { public EncounterTable table; public string internalId; + /// O(1) lookup via . Returns null if the + /// table is missing or the id can't be found. public IEncounter Resolve() { - if(table == null || table.encounters == null || string.IsNullOrEmpty(internalId)) { + if(table == null || string.IsNullOrEmpty(internalId)) { return null; } - - foreach(var encounter in table.encounters) { - if(encounter?.EncounterDefinition?.internalId == internalId) { - return encounter; - } - } - - return null; + return table.Resolve(internalId); } } } diff --git a/Runtime/EncounterRegistry.cs b/Runtime/EncounterRegistry.cs index 4b45c16..2513130 100644 --- a/Runtime/EncounterRegistry.cs +++ b/Runtime/EncounterRegistry.cs @@ -14,11 +14,19 @@ namespace Jovian.EncounterSystem { public Dictionary GetEncounters() => encounters; public void RegisterEncounter(IEncounter encounter) { - encounters?.TryAdd(encounter?.EncounterDefinition?.internalId, encounter); + var id = encounter?.EncounterDefinition?.internalId; + if(string.IsNullOrEmpty(id)) { + return; + } + encounters.TryAdd(id, encounter); } public void UnregisterEncounter(IEncounter encounter) { - encounters.Remove(encounter.EncounterDefinition.internalId); + var id = encounter?.EncounterDefinition?.internalId; + if(string.IsNullOrEmpty(id)) { + return; + } + encounters.Remove(id); } public void ClearEncounters() { @@ -26,10 +34,24 @@ namespace Jovian.EncounterSystem { } public void PopulateEncounters() { - foreach(var collection in encounterCollections) { - foreach(var encounter in collection.encounterTables) { - foreach(var encounterInstance in encounter.encounters) { - RegisterEncounter(encounterInstance); + if(encounterCollections == null) { + return; + } + + for(int c = 0; c < encounterCollections.Length; c++) { + var collection = encounterCollections[c]; + if(collection?.encounterTables == null) { + continue; + } + + for(int t = 0; t < collection.encounterTables.Length; t++) { + var table = collection.encounterTables[t]; + if(table?.encounters == null) { + continue; + } + + for(int i = 0; i < table.encounters.Count; i++) { + RegisterEncounter(table.encounters[i]); } } } diff --git a/Runtime/EncounterResolver.cs b/Runtime/EncounterResolver.cs index 6ee5365..c523ca4 100644 --- a/Runtime/EncounterResolver.cs +++ b/Runtime/EncounterResolver.cs @@ -16,12 +16,17 @@ namespace Jovian.EncounterSystem { handlers.Remove(typeof(T)); } - public void Resolve(IEnumerable events, EncounterContext context) { + /// Indexed iteration over — avoids the boxed enumerator + /// that an IEnumerable<T> parameter would force. + /// is a List, which implements IReadOnlyList, so call sites just pass it directly. + public void Resolve(IReadOnlyList events, EncounterContext context) { if(events == null) { return; } - foreach(var evt in events) { + var count = events.Count; + for(int i = 0; i < count; i++) { + var evt = events[i]; if(evt == null) { continue; } diff --git a/Runtime/EncounterTable.cs b/Runtime/EncounterTable.cs index 8076a8b..35245ab 100644 --- a/Runtime/EncounterTable.cs +++ b/Runtime/EncounterTable.cs @@ -8,6 +8,24 @@ namespace Jovian.EncounterSystem { public string id; public List encounters; + private Dictionary idCache; + + /// O(1) lookup by . Used by . + /// Call if you mutate the list at runtime. + public IEncounter Resolve(string internalId) { + if(string.IsNullOrEmpty(internalId) || encounters == null || encounters.Count == 0) { + return null; + } + + EnsureCache(); + return idCache.TryGetValue(internalId, out var encounter) ? encounter : null; + } + + /// Force the id lookup cache to rebuild on next use. + public void InvalidateCache() { + idCache = null; + } + public IEncounter GetRandomEncounter() { if(encounters == null || encounters.Count == 0) { return null; @@ -16,50 +34,92 @@ namespace Jovian.EncounterSystem { return encounters[UnityEngine.Random.Range(0, encounters.Count)]; } + /// Random pick restricted to encounters whose runtime type matches . + /// Zero-alloc — uses reservoir sampling instead of building an intermediate filtered list. public IEncounter GetRandomEncounter(Type type) { + if(encounters == null || encounters.Count == 0 || type == null) { + return null; + } + + IEncounter selected = null; + var seen = 0; + for(int i = 0; i < encounters.Count; i++) { + var encounter = encounters[i]; + if(encounter == null || encounter.GetType() != type) { + continue; + } + seen++; + if(UnityEngine.Random.Range(0, seen) == 0) { + selected = encounter; + } + } + return selected; + } + + /// Random pick restricted by . Used with + /// to exclude gated encounters. Zero-alloc via reservoir sampling. + public IEncounter GetRandomEncounter(Predicate filter) { if(encounters == null || encounters.Count == 0) { return null; } - - var encountersOfType = encounters.FindAll(encounter => encounter.GetType() == type); - if(encountersOfType.Count > 0) { - return encountersOfType[UnityEngine.Random.Range(0, encountersOfType.Count)]; - } - - return null; - } - - /// Random pick limited by a predicate. Used with to exclude gated encounters. - public IEncounter GetRandomEncounter(Predicate filter) { - if(encounters == null || encounters.Count == 0 || filter == null) { + if(filter == null) { return GetRandomEncounter(); } - var pool = encounters.FindAll(filter); - if(pool.Count == 0) { - return null; + IEncounter selected = null; + var seen = 0; + for(int i = 0; i < encounters.Count; i++) { + var encounter = encounters[i]; + if(encounter == null || !filter(encounter)) { + continue; + } + seen++; + if(UnityEngine.Random.Range(0, seen) == 0) { + selected = encounter; + } + } + return selected; + } + + private void EnsureCache() { + if(idCache != null) { + return; } - return pool[UnityEngine.Random.Range(0, pool.Count)]; + idCache = new Dictionary(encounters?.Count ?? 0); + if(encounters == null) { + return; + } + + for(int i = 0; i < encounters.Count; i++) { + var encounter = encounters[i]; + var internalId = encounter?.EncounterDefinition?.internalId; + if(!string.IsNullOrEmpty(internalId)) { + idCache[internalId] = encounter; + } + } } #if UNITY_EDITOR // Unity's inspector "+" duplicates the previous list element, including nested internalId // GUIDs. Regenerate any duplicates so every encounter carries a unique internalId. private void OnValidate() { + InvalidateCache(); + if(encounters == null) { return; } - var seen = new HashSet(); + var seen = new HashSet(encounters.Count); var changed = false; - foreach(var encounter in encounters) { + for(int i = 0; i < encounters.Count; i++) { + var encounter = encounters[i]; if(encounter?.EncounterDefinition == null) { continue; } - var id = encounter.EncounterDefinition.internalId; - if(string.IsNullOrEmpty(id) || !seen.Add(id)) { + var entryId = encounter.EncounterDefinition.internalId; + if(string.IsNullOrEmpty(entryId) || !seen.Add(entryId)) { encounter.EncounterDefinition.internalId = Guid.NewGuid().ToString(); seen.Add(encounter.EncounterDefinition.internalId); changed = true; diff --git a/Runtime/QuestLog.cs b/Runtime/QuestLog.cs index fbbbe7b..a3a93f4 100644 --- a/Runtime/QuestLog.cs +++ b/Runtime/QuestLog.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; namespace Jovian.EncounterSystem { public enum QuestLogEventType { @@ -46,9 +45,12 @@ namespace Jovian.EncounterSystem { } public IEnumerable ResolvedEncounterIds() { - return entries - .Select(entry => entry.encounterInternalId) - .Where(id => !string.IsNullOrEmpty(id)); + for(int i = 0; i < entries.Count; i++) { + var id = entries[i].encounterInternalId; + if(!string.IsNullOrEmpty(id)) { + yield return id; + } + } } private void Record(QuestLogEventType type, IEncounter from, IEncounter to) { diff --git a/Runtime/QuestProgress.cs b/Runtime/QuestProgress.cs index ec28872..2e6551a 100644 --- a/Runtime/QuestProgress.cs +++ b/Runtime/QuestProgress.cs @@ -8,8 +8,8 @@ namespace Jovian.EncounterSystem { /// construction; rolling and advancement are O(1). /// public class QuestProgress { - private readonly HashSet resolvedIds = new(); - private readonly Dictionary predecessorOf = new(); + private readonly HashSet resolvedIds; + private readonly Dictionary predecessorOf; public event Action QuestStarted; public event Action QuestAdvanced; @@ -18,9 +18,26 @@ namespace Jovian.EncounterSystem { public IReadOnlyCollection ResolvedIds => resolvedIds; public QuestProgress(EncountersCollection encountersCollection) { + var capacity = EstimateEncounterCount(encountersCollection); + resolvedIds = new HashSet(capacity); + predecessorOf = new Dictionary(capacity); IndexQuests(encountersCollection); } + private static int EstimateEncounterCount(EncountersCollection collection) { + if(collection?.encounterTables == null) { + return 0; + } + var total = 0; + for(int i = 0; i < collection.encounterTables.Length; i++) { + var table = collection.encounterTables[i]; + if(table?.encounters != null) { + total += table.encounters.Count; + } + } + return total; + } + public bool IsGated(IEncounter encounter) { var id = encounter?.EncounterDefinition?.internalId; if(string.IsNullOrEmpty(id)) { @@ -77,13 +94,15 @@ namespace Jovian.EncounterSystem { return; } - foreach(var table in collection.encounterTables) { + for(int t = 0; t < collection.encounterTables.Length; t++) { + var table = collection.encounterTables[t]; if(table?.encounters == null) { continue; } - foreach(var encounter in table.encounters) { - if(encounter?.EncounterDefinition.Kind is not QuestKind questKind) { + for(int i = 0; i < table.encounters.Count; i++) { + var encounter = table.encounters[i]; + if(encounter?.EncounterDefinition?.Kind is not QuestKind questKind) { continue; }