This commit is contained in:
2025-03-15 12:33:50 +01:00
parent 0d3516774e
commit 569b1d00b3
28 changed files with 446 additions and 147 deletions

View File

@@ -15,8 +15,6 @@ namespace RebootKit.Engine {
public ServiceAsset<GameService> GameService;
[Space]
public ServiceAsset[] Services;
public GameModeAsset StartingGameMode;
}
}

View File

@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace RebootKit.Engine.Foundation {
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Method)]
public class InjectAttribute : Attribute {
}
public class DIContext {
private const BindingFlags k_fieldsBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
private const BindingFlags k_methodsBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
public interface IFieldInjector {
bool Inject(FieldInfo field, object target, DIContext context);
}
private static readonly Logger Logger = new(nameof(DIContext));
private readonly Dictionary<Type, object> _bindingsMaps = new();
private readonly List<IFieldInjector> _fieldInjectors = new();
public DIContext() {
Bind(this);
AddInjector(new InjectAttributeFieldInjector());
}
public void AddInjector(IFieldInjector injector) {
_fieldInjectors.Add(injector);
}
public void Bind(Type type, object obj) {
if (!_bindingsMaps.TryAdd(type, obj)) {
Logger.Error($"Cannot bind to '{type}', slot is already occupied");
}
}
public void Bind<TBind>(TBind obj) {
Bind(typeof(TBind), obj);
}
public object Resolve(Type type) {
if (_bindingsMaps.TryGetValue(type, out object obj)) {
return obj;
}
Logger.Error($"Couldn't resolve `{type}`");
return null;
}
public T Resolve<T>() {
return (T) Resolve(typeof(T));
}
public void Inject<T>(T target) {
Type type = typeof(T);
foreach (FieldInfo field in type.GetFields(k_fieldsBindingFlags)) {
if (!InjectField(field, target)) {
return;
}
}
foreach (MethodInfo method in type.GetMethods(k_methodsBindingFlags)) {
if (!Attribute.IsDefined(method, typeof(InjectAttribute))) {
continue;
}
Type[] paramsTypes = method.GetParameters()
.Select(t => t.ParameterType)
.ToArray();
object[] instances = new object[paramsTypes.Length];
for (int i = 0; i < paramsTypes.Length; ++i) {
instances[i] = Resolve(paramsTypes[i]);
if (instances[i] == null) {
Logger.Error($"Failed to resolve method parameter of type `{paramsTypes[i]}`");
}
}
method.Invoke(target, instances);
}
}
private bool InjectField(FieldInfo field, object target) {
for (int i = _fieldInjectors.Count - 1; i >= 0; i--) {
if (_fieldInjectors[i].Inject(field, target, this)) {
return true;
}
}
return false;
}
private class InjectAttributeFieldInjector : IFieldInjector {
public bool Inject(FieldInfo field, object target, DIContext context) {
if (!Attribute.IsDefined(field, typeof(InjectAttribute))) {
return false;
}
object instance = context.Resolve(field.FieldType);
if (instance == null) {
Logger.Error($"Cannot resolve `{field.FieldType}`");
return false;
}
field.SetValue(target, instance);
return true;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cee4133e4a594126868703cc035663cd
timeCreated: 1742001112

View File

@@ -16,7 +16,6 @@ namespace RebootKit.Engine.Foundation {
}
RR.Shared = null;
ServicesLocator.Shared.UnRegisterAll();
_cancellationTokenSource = new CancellationTokenSource();
SceneManager.LoadScene(0, LoadSceneMode.Single);

View File

@@ -0,0 +1,22 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace RebootKit.Engine.Foundation {
public interface IService : IDisposable {
UniTask OnInit(CancellationToken cancellationToken);
}
public abstract class ServiceMonoBehaviour : MonoBehaviour, IService {
public abstract UniTask OnInit(CancellationToken cancellationToken);
public abstract void Dispose();
}
public abstract class ServiceAsset<T> : ScriptableObject where T : IService {
public abstract T Create();
}
public abstract class ServiceAsset : ServiceAsset<IService> {
}
}

View File

@@ -0,0 +1,22 @@
using UnityEngine;
namespace RebootKit.Engine.Foundation {
public interface IDependencyInstaller {
void Install(DIContext context);
}
public abstract class SceneDependencyInstaller : MonoBehaviour, IDependencyInstaller {
public abstract void Install(DIContext context);
}
public class SceneDI : MonoBehaviour, IDependencyInstaller {
[SerializeField]
private SceneDependencyInstaller[] _sceneInstallers;
public void Install(DIContext context) {
foreach (SceneDependencyInstaller installer in _sceneInstallers) {
installer.Install(context);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8dd28652b58c4d689ab3f2f9354d7589
timeCreated: 1742006992

View File

@@ -1,94 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace RebootKit.Engine.Foundation {
public interface IService : IDisposable {
UniTask OnInit(CancellationToken cancellationToken);
}
public abstract class ServiceMonoBehaviour : MonoBehaviour, IService {
public abstract UniTask OnInit(CancellationToken cancellationToken);
public abstract void Dispose();
}
public abstract class ServiceAsset<T> : ScriptableObject where T : IService {
public abstract T Create();
}
public abstract class ServiceAsset : ServiceAsset<IService> {
}
public class ServicesLocator {
private static readonly Logger _logger = new(nameof(ServicesLocator));
private static ServicesLocator _shared;
public static ServicesLocator Shared {
get {
if (_shared == null) {
_shared = new ServicesLocator();
}
return _shared;
}
}
private readonly Dictionary<Type, IService> _servicesMap = new();
public void Register(IService service, Type type) {
if (!_servicesMap.TryAdd(type, service)) {
_logger.Error($"There is already game service of type `{type}`");
return;
}
service.OnInit(default);
_logger.Info($"Registered service of type {type.Name}");
}
public void Register<T>(T service) where T : IService {
Register(service, service.GetType());
}
public async UniTask RegisterAsync<T>(T service, CancellationToken cancellationToken = default) where T : IService {
if (!_servicesMap.TryAdd(typeof(T), service)) {
_logger.Error($"There is already game service of type `{typeof(T).Name}`");
return;
}
await service.OnInit(cancellationToken);
_logger.Info($"Registered service of type {typeof(T).Name}");
}
public void UnRegister(Type type) {
if (_servicesMap.TryGetValue(type, out IService service)) {
service.Dispose();
_servicesMap.Remove(type);
_logger.Info($"Unregistered service of type {type.Name}");
}
}
public void UnRegisterAll() {
foreach (IService service in _servicesMap.Values) {
service.Dispose();
}
_servicesMap.Clear();
}
public IService Get(Type type) {
if (_servicesMap.TryGetValue(type, out IService service)) {
return service;
}
Debug.LogAssertionFormat($"Couldn't find service of type `{type}`");
return null;
}
public T Get<T>() where T : IService {
return (T) Get(typeof(T));
}
}
}

View File

@@ -0,0 +1,14 @@
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Graphics;
using UnityEngine;
namespace RebootKit.Engine {
public class MainSceneInstaller : SceneDependencyInstaller {
[SerializeField]
private MainCameraService _mainCameraService;
public override void Install(DIContext context) {
context.Bind(_mainCameraService);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4238ea1a17e342e583cdd929103a22c6
timeCreated: 1742007242

View File

@@ -11,15 +11,13 @@ using UnityEngine.SceneManagement;
using Logger = RebootKit.Engine.Foundation.Logger;
namespace RebootKit.Engine {
public abstract class RAsset : ScriptableObject {
}
public class RR : IDisposable {
public static RR Shared { get; set; }
private static readonly Logger _logger = new Logger("RR");
private static readonly Logger Logger = new("RR");
private EngineConfigAsset _engineConfigAsset;
@@ -28,40 +26,46 @@ namespace RebootKit.Engine {
private WorldService _worldService;
private GameService _gameService;
private DIContext _diContext;
public void Dispose() {
}
public async UniTask Init(CancellationToken cancellationToken) {
_logger.Info("Waking up");
Logger.Info("Waking up");
_logger.Info("Loading engine config");
Logger.Info("Loading engine config");
_engineConfigAsset = Resources.Load<EngineConfigAsset>(RConsts.EngineConfigResourcesPath);
if (_engineConfigAsset == null) {
_logger.Error($"Couldn't load engine config from resources: {RConsts.EngineConfigResourcesPath}");
Logger.Error($"Couldn't load engine config from resources: {RConsts.EngineConfigResourcesPath}");
return;
}
_logger.Info("Loading engine found");
Logger.Info("Loading engine found");
_diContext = new DIContext();
_diContext.AddInjector(new CVarFieldInjector());
await CreateCoreServices(cancellationToken);
await RegisterServiceAssetsFromConfig(cancellationToken);
await RegisterServicesFromMainSceneAsync(cancellationToken);
}
public async UniTask Run(CancellationToken cancellationToken) {
_logger.Info($"Starting initial game mode");
Logger.Info($"Starting initial game mode");
await _gameService.Start(_engineConfigAsset.StartingGameMode, cancellationToken);
}
private async UniTask<TService> CreateServiceAsync<TService>(ServiceAsset<TService> asset, CancellationToken cancellationToken = default) where TService : IService {
TService service = asset.Create();
await ServicesLocator.Shared.RegisterAsync(service, cancellationToken);
_diContext.Bind<TService>(service);
_diContext.Inject(service);
await service.OnInit(cancellationToken);
return service;
}
private async UniTask CreateCoreServices(CancellationToken cancellationToken) {
_logger.Debug("Registering core services");
Logger.Debug("Registering core services");
_consoleService = await CreateServiceAsync(_engineConfigAsset.ConsoleService, cancellationToken);
_inputService = await CreateServiceAsync(_engineConfigAsset.InputService, cancellationToken);
@@ -69,38 +73,23 @@ namespace RebootKit.Engine {
_gameService = await CreateServiceAsync(_engineConfigAsset.GameService, cancellationToken);
}
private async UniTask RegisterServiceAssetsFromConfig(CancellationToken cancellationToken = default) {
if (_engineConfigAsset.Services.Length == 0) {
return;
}
_logger.Info($"Registering {_engineConfigAsset.Services.Length} services");
foreach (ServiceAsset service in _engineConfigAsset.Services) {
await ServicesLocator.Shared.RegisterAsync(service.Create(), cancellationToken);
}
}
private async UniTask RegisterServicesFromMainSceneAsync(CancellationToken cancellationToken = default) {
_logger.Info("Looking for services in mainscene");
int foundCount = 0;
GameObject[] gameObjects = SceneManager.GetSceneByBuildIndex(RConsts.MainSceneBuildIndex).GetRootGameObjects();
if (gameObjects.Length == 0) {
_logger.Info("No services found in main scene");
return;
}
foreach (GameObject root in gameObjects) {
ServiceMonoBehaviour[] services = root.GetComponentsInChildren<ServiceMonoBehaviour>();
SceneDI di = root.GetComponent<SceneDI>();
foreach (ServiceMonoBehaviour service in services) {
await ServicesLocator.Shared.RegisterAsync(service, cancellationToken);
foundCount += 1;
if (di == null) {
continue;
}
di.Install(_diContext);
}
_logger.Info($"Found and registered {foundCount} services from active scene");
await UniTask.Yield();
}
//
@@ -108,10 +97,6 @@ namespace RebootKit.Engine {
//
/// Services
public static T Service<T>() where T : IService {
return ServicesLocator.Shared.Get<T>();
}
public static ConsoleService Console() => Shared._consoleService;
public static InputService Input() => Shared._inputService;
public static WorldService World() => Shared._worldService;

View File

@@ -0,0 +1,59 @@
using System;
using System.Reflection;
using RebootKit.Engine.Foundation;
namespace RebootKit.Engine.Services.Console {
[AttributeUsage(AttributeTargets.Field)]
public class CVarAttribute : Attribute {
public string Name { get; }
public CVarValue Value { get; }
public CVarAttribute(string name, float defaultValue) {
Name = name;
Value = new CVarValue(defaultValue);
}
public CVarAttribute(string name, double defaultValue) {
Name = name;
Value = new CVarValue(defaultValue);
}
public CVarAttribute(string name, string defaultValue) {
Name = name;
Value = new CVarValue(defaultValue);
}
public CVarAttribute(string name, int defaultValue) {
Name = name;
Value = new CVarValue(defaultValue);
}
}
public class CVarFieldInjector : DIContext.IFieldInjector {
private static readonly Logger Logger = new(nameof(CVarFieldInjector));
public bool Inject(FieldInfo field, object target, DIContext context) {
if (!Attribute.IsDefined(field, typeof(CVarAttribute))) {
return false;
}
ConsoleService console = context.Resolve<ConsoleService>();
if (console == null) {
Logger.Error($"Cannot inject field because cannot resolve `{nameof(ConsoleService)}`");
return false;
}
CVarAttribute cvarAttribute = field.GetCustomAttribute<CVarAttribute>();
CVar cvar = console.GetCVar(cvarAttribute.Name);
if (cvar == null) {
cvar = new CVar(cvarAttribute.Name, cvarAttribute.Value);
console.Replace(cvar);
}
field.SetValue(target, cvar);
return true;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1ce6213dda8c4f8d9eafe4afd1614a86
timeCreated: 1742005021

View File

@@ -62,16 +62,26 @@ namespace RebootKit.Engine.Services.Console {
_ui.Clear();
_ui.Write("Hello shelf\n");
}
public void Dispose() {
if (_ui != null) {
UnityEngine.Object.Destroy(_ui);
_ui = null;
}
_config.ToggleAction.Action.performed -= OnToggleAction;
}
public bool CVarExists(string name) {
foreach (CVar cvar in _cvars) {
if (cvar.Name.Equals(name)) {
return true;
}
}
return false;
}
public CVar GetCVar(string name) {
foreach (CVar cvar in _cvars) {
if (cvar.Name.Equals(name)) {

View File

@@ -1,11 +1,12 @@
using System.Threading;
using Cysharp.Threading.Tasks;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Services.Console;
namespace RebootKit.Engine.Services.Game {
public class GameService : IService {
private static readonly Logger _logger = new(nameof(GameService));
private static readonly Logger Logger = new(nameof(GameService));
private GameModeAsset _gameModeAsset;
private GameMode _gameMode;
private bool _running;
@@ -28,7 +29,7 @@ namespace RebootKit.Engine.Services.Game {
private async UniTask Run(CancellationToken cancellationToken) {
if (_gameMode == null) {
_logger.Error("Trying to run game without game mode");
Logger.Error("Trying to run game without game mode");
return;
}

View File

@@ -1,5 +1,5 @@
{
"name": "com.szafastudio.szafakit",
"name": "RebootKit.Engine",
"rootNamespace": "RebootKit",
"references": [
"GUID:6055be8ebefd69e48b49212b09b47b2f",