ProjectDDD/Packages/SLUnity/SLGame.cs
2025-07-08 19:46:31 +09:00

483 lines
14 KiB
C#

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<string> 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<Action> sessionHandlers;
public float duration;
public bool isChanged;
}
private static bool currValidState = false;
private static readonly Dictionary<string, SessionCache> sessionCache = new Dictionary<string, SessionCache>();
private static readonly List<string> sessionCacheIndex = new List<string>();
private static int handlerIndex = 0;
private static readonly Dictionary<Action, int> sessionHandlers = new Dictionary<Action, int>();
private static readonly Dictionary<Action, int> sessionCounters = new Dictionary<Action, int>();
private static readonly SortedList<int, Action> runHandlers = new SortedList<int, Action>();
private static Dictionary<string, SLGameComponent> gameComponents;
private static List<SLGameComponent> activeComponents;
private static List<SLGameComponent> readyActiveComponents;
private static Dictionary<string, (SLGameComponent manager, MethodInfo method)> 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<int> removeKeyIndex = new List<int>();
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<Action>();
}
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<string, SLGameComponent>();
commandQueue = new List<(string command, SLEntity entity)>();
activeComponents = new List<SLGameComponent>();
readyActiveComponents = new List<SLGameComponent>();
gameComponents.CreateInstanceDictionary(managers);
commands = new Dictionary<string, (SLGameComponent manager, MethodInfo method)>();
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<T>(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;
}
}