using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Superlazy; using UnityEngine; public class SLGame { public static SLEntity Session { get; private set; } public static IEnumerable SessionCacheKeys => sessionCache.Keys; public static Camera Camera { get; private set; } private class SessionCache { public bool validState = false; public string id = null; public SessionCache parent = null; public SLEntity sessionCache; public List sessionHandlers; public float duration; public bool isChanged; } private static bool currValidState = false; private static readonly Dictionary sessionCache = new Dictionary(); private static readonly List sessionCacheIndex = new List(); private static int handlerIndex = 0; private static readonly Dictionary sessionHandlers = new Dictionary(); private static readonly Dictionary sessionCounters = new Dictionary(); private static readonly SortedList runHandlers = new SortedList(); private static Dictionary gameComponents; private static List activeComponents; private static List readyActiveComponents; private static Dictionary commands; private static List<(string command, SLEntity entity)> commandQueue; private static bool canCommand = false; // 커맨드 즉시 실행 or queue private static bool updateSessionCache = false; // 세션 캐시 업데이트 이후에는 필요시 핸들러를 즉시 실행 public static SLEntity SessionGet(string path) { if (sessionCache.TryGetValue(path, out var cache)) { cache.duration = 2f; var parent = cache.parent; while (parent != null) { parent.duration = 2f; parent = parent.parent; } if (cache.validState == currValidState) { return cache.sessionCache; } else { SLLog.Error($"{path} is expired."); return Session.Get(path); } } else { string parentPath = null; var subLength = path.LastIndexOf('.'); if (subLength > 0) { parentPath = path.Substring(0, subLength); SessionGet(parentPath); } var ret = Session.Get(path); sessionCache[path] = new SessionCache { id = path.Substring(subLength + 1, path.Length - subLength - 1), parent = parentPath != null ? sessionCache[parentPath] : sessionCache[""], duration = 2f, validState = currValidState, sessionCache = ret }; sessionCacheIndex.Add(path); return ret; } } private readonly List removeKeyIndex = new List(); private void SessionCacheUpdate() { currValidState = !currValidState; var cacheCount = sessionCacheIndex.Count; for (var i = 0; i < cacheCount; i++) { var c = sessionCacheIndex[i]; var cache = sessionCache[c]; var newValue = cache.sessionCache; // 현재값 var update = false; if (cache.parent != null) { if (cache.parent.sessionCache.HasChild(cache.id)) { newValue = cache.parent.sessionCache[cache.id]; } else if (cache.sessionCache) // 자식이 없고 캐시는 있는경우 삭제를 위해 // 아이디가 null이라면 삭제된거라 다시 댕글 건다 // 근데 아이디는 상관없음 { newValue = false; } update |= cache.parent.isChanged; update |= cache.parent.sessionCache.IsModified(cache.id); } else { if (string.IsNullOrEmpty(c)) { newValue = Session; } else { if (Session.HasChild(c)) { newValue = Session[c]; } else if (cache.sessionCache) // 자식이 없고 캐시는 있는경우 삭제를 위해 { newValue = false; } } } cache.validState = currValidState; // 상위가 변경되거나/업데이트틱이 지났다면 캐시 변경 // 삭제/변경되었다면 캐시가 바뀜 // 추가/변경 되었다면 뉴가 바뀜 update |= SLEntity.Changed(cache.sessionCache, newValue); if (update) { cache.isChanged = true; cache.sessionCache = newValue; if (cache.sessionHandlers != null) { foreach (var handler in cache.sessionHandlers) { var handle = sessionHandlers[handler]; if (runHandlers.ContainsKey(handle) == false) runHandlers[handle] = handler; } } } else if (cache.sessionCache.IsModified(null)) // 추가 삭제인경우 아래로 전파하진 않는다 { cache.sessionCache = newValue; if (cache.sessionHandlers != null) { foreach (var handler in cache.sessionHandlers) { var handle = sessionHandlers[handler]; if (runHandlers.ContainsKey(handle) == false) runHandlers[handle] = handler; } } } if (cache.sessionHandlers == null || cache.sessionHandlers.Count == 0) { if (cache.duration < 0) { removeKeyIndex.Add(i); } else { cache.duration -= Time.unscaledDeltaTime; } } else { // 부모의 유지시간 갱신 var parent = cache.parent; while (parent != null) { parent.duration = parent.duration > 2f ? parent.duration : 2f; parent = parent.parent; } } } for (var i = 0; i < cacheCount; i++) { var c = sessionCacheIndex[i]; var cache = sessionCache[c]; cache.sessionCache.EndModified(); cache.isChanged = false; } for (var i = removeKeyIndex.Count - 1; i >= 0; i--) { var key = sessionCacheIndex[removeKeyIndex[i]]; var duration = sessionCache[key].duration; if (duration <= 0) { sessionCache.Remove(key); sessionCacheIndex.RemoveAt(removeKeyIndex[i]); } } removeKeyIndex.Clear(); while (runHandlers.Count != 0) { var handler = runHandlers.Values[0]; runHandlers.RemoveAt(0); handler.Invoke(); } } public static void AddNotify(string sessionPath, Action onChange) { SessionGet(sessionPath); if (sessionCache[sessionPath].sessionHandlers == null) { sessionCache[sessionPath].sessionHandlers = new List(); } sessionCache[sessionPath].sessionHandlers.Add(onChange); if (sessionHandlers.ContainsKey(onChange) == false) { sessionHandlers[onChange] = handlerIndex; handlerIndex += 1; sessionCounters[onChange] = 0; } sessionCounters[onChange] += 1; var handlerId = sessionHandlers[onChange]; if (runHandlers.ContainsKey(handlerId) == false) { runHandlers[handlerId] = onChange; } if (updateSessionCache == false) // 코루틴 등에서 첫프레임 실행 보장 { while (runHandlers.Count != 0) { var handler = runHandlers.Values[0]; runHandlers.RemoveAt(0); handler.Invoke(); } } } public static void RemoveNotify(string sessionPath, Action onChange) { sessionCounters[onChange] -= 1; if (sessionCounters[onChange] == 0) { sessionHandlers.Remove(onChange); sessionCounters.Remove(onChange); } if (sessionCache.TryGetValue(sessionPath, out var cache)) { cache.sessionHandlers.Remove(onChange); } else { SLLog.Error($"Cannot RemoveNotify : {sessionPath}", onChange.Target); } } public SLGame(string[] managers, string main) { var sessionRoot = SLEntity.Empty; sessionRoot["Session"]["Data"] = SLSystem.Data; Session = sessionRoot["Session"]; Camera = Camera.main; sessionCache[""] = new SessionCache { id = "", parent = null, duration = float.PositiveInfinity, validState = currValidState, sessionCache = Session }; sessionCacheIndex.Add(""); gameComponents = new Dictionary(); commandQueue = new List<(string command, SLEntity entity)>(); activeComponents = new List(); readyActiveComponents = new List(); gameComponents.CreateInstanceDictionary(managers); commands = new Dictionary(); foreach (var component in gameComponents) { commands.CreateCommandInfoToBaseType(component.Value, typeof(SLEntity)); } if (main == string.Empty) { main = managers[0]; } ActiveComponent(main, true); } internal void Update() { Session["Time"] += 1; foreach (var component in readyActiveComponents) { activeComponents.Add(component); } readyActiveComponents.Clear(); canCommand = true; updateSessionCache = true; foreach (var command in commandQueue) { #if UNITY_EDITOR try { #endif Command(command.command, command.entity); #if UNITY_EDITOR } catch (Exception e) { SLLog.Error($"Can't Run [{command.command}]. {e.Message} {e.StackTrace}"); } #endif } foreach (var component in activeComponents) { #if UNITY_EDITOR try { #endif component.Update(); #if UNITY_EDITOR } catch (Exception e) { SLLog.Error($"Can't Update [{component.GetType().Name}]. {e.Message} {e.StackTrace}"); } #endif } commandQueue.Clear(); activeComponents.RemoveAll(c => { if (c.Active == false) { #if UNITY_EDITOR try { #endif c.End(); #if UNITY_EDITOR } catch (Exception e) { SLLog.Error($"Can't Update [{c.GetType().Name}]. {e.Message} {e.StackTrace}"); } #endif return true; } return false; }); canCommand = false; SessionCacheUpdate(); updateSessionCache = false; } public static void ActiveComponent(string component, bool active) { // TODO: 중복 추가등 체크필요가 있을까? var instance = gameComponents[component]; if (active) { if (instance.Active == false) { if (activeComponents.Contains(instance)) // 이번 프레임에 삭제된 경우 { instance.End(); } else { readyActiveComponents.Add(instance); } instance.Active = true; instance.Begin(); } } else { instance.Active = false; } } public static void ActiveComponent(bool active) where T : SLGameComponent { ActiveComponent(typeof(T).Name, active); } public static void Command(string commandName, SLEntity entity) { #if UNITY_EDITOR try { #endif if (commands.ContainsKey(commandName) == false) { SLLog.Error($"Can't Run [{commandName}]"); return; } if (canCommand == false) { commandQueue.Add((commandName, entity)); return; } var (component, method) = commands[commandName]; if (component.Active == false) { SLLog.Error($"Can't Run [{commandName}]. Component is not active"); return; } method.Invoke(component, new[] { entity }); #if UNITY_EDITOR } catch (Exception e) { var message = $"Can't Run [{commandName}]. \n{e.Message} \n{e.StackTrace}"; var inner = e.InnerException; while (inner != null) { message += $"\n{inner.Message} \n{inner.StackTrace}"; inner = inner.InnerException; } SLLog.Error(message); } #endif } public static void Event(string eventName, SLEntity context) { if (context == false) { context = SLEntity.Empty; context["EventName"] = eventName; } var id = Session["Events"].Get(eventName).Count(); context = context.Override(); Session["Events"].Set($"{eventName}.{id}", context); } public static void ClearEvent() { Session["Events"] = false; } }