diff --git a/Editor/PropertyDrawers/CVarDrawer.cs b/Editor/PropertyDrawers/CVarDrawer.cs index b8b66ee..1473241 100644 --- a/Editor/PropertyDrawers/CVarDrawer.cs +++ b/Editor/PropertyDrawers/CVarDrawer.cs @@ -1,4 +1,5 @@ -using RebootKit.Engine.Services.Console; +using RebootKit.Engine.Foundation; +using RebootKit.Engine.Services.Console; using RebootKitEditor.Utils; using UnityEditor; using UnityEngine; diff --git a/Editor/REditorConsts.cs b/Editor/REditorConsts.cs index d96f695..0781dd2 100644 --- a/Editor/REditorConsts.cs +++ b/Editor/REditorConsts.cs @@ -1,5 +1,7 @@ namespace RebootKitEditor { static class REditorConsts { internal const string k_EditorMenu = "Reboot Reality/"; + + internal const string k_OnGameRunScriptContentKey = "RebootKitEditor.OnGameRunScriptContent"; } } \ No newline at end of file diff --git a/Editor/RebootWindow/GameServicesView.cs b/Editor/RebootWindow/GameServicesView.cs index b67f6f6..27282f7 100644 --- a/Editor/RebootWindow/GameServicesView.cs +++ b/Editor/RebootWindow/GameServicesView.cs @@ -27,21 +27,23 @@ namespace RebootKitEditor.RebootWindow { public VisualElement Build() { m_RootElement = new ScrollView(); - Label servicesAmountLabel = new($"Game services: {m_EngineConfigAsset.services.Length}") { - style = { - color = new Color(0.7f, 0.9f, 0.9f), - unityFontStyleAndWeight = FontStyle.Bold - } - }; - m_RootElement.Add(servicesAmountLabel); - - for (int i = 0; i < m_EngineConfigAsset.services.Length; i++) { - ServiceAsset serviceAsset = m_EngineConfigAsset.services[i]; - - VisualElement serviceView = CreateServicesView(serviceAsset); - serviceView.style.backgroundColor = i % 2 == 0 ? new Color(0.1f, 0.1f, 0.1f) : new Color(0.2f, 0.2f, 0.2f); - m_RootElement.Add(serviceView); - } + // int servicesLength = m_EngineConfigAsset.services?.Length ?? 0; + // + // Label servicesAmountLabel = new($"Game services: {servicesLength}") { + // style = { + // color = new Color(0.7f, 0.9f, 0.9f), + // unityFontStyleAndWeight = FontStyle.Bold + // } + // }; + // m_RootElement.Add(servicesAmountLabel); + // + // for (int i = 0; i < servicesLength; i++) { + // ServiceAsset serviceAsset = m_EngineConfigAsset.services[i]; + // + // VisualElement serviceView = CreateServicesView(serviceAsset); + // serviceView.style.backgroundColor = i % 2 == 0 ? new Color(0.1f, 0.1f, 0.1f) : new Color(0.2f, 0.2f, 0.2f); + // m_RootElement.Add(serviceView); + // } return m_RootElement; } diff --git a/Editor/RebootWindow/HomeView.cs b/Editor/RebootWindow/HomeView.cs index 5138954..c0fa505 100644 --- a/Editor/RebootWindow/HomeView.cs +++ b/Editor/RebootWindow/HomeView.cs @@ -1,4 +1,7 @@ -using RebootKit.Engine.UI; +using RebootKit.Engine.Main; +using RebootKit.Engine.Services.Console; +using RebootKit.Engine.UI; +using UnityEditor; using UnityEngine; using UnityEngine.UIElements; @@ -55,6 +58,67 @@ namespace RebootKitEditor.RebootWindow { persistentPathContainer.Add(openPersistentPathButton); rootContainer.Add(persistentPathContainer); + + Label onGameRunScriptLabel = new("On Game Run Script (User):") { + style = { + fontSize = 12, + color = new Color(0.7f, 0.9f, 0.9f) + } + }; + rootContainer.Add(onGameRunScriptLabel); + + TextField onGameRunScriptTextField = new() { + style = { + fontSize = 12, + }, + multiline = true, + value = EditorPrefs.GetString(REditorConsts.k_OnGameRunScriptContentKey, "") + }; + + onGameRunScriptTextField.RegisterValueChangedCallback(evt => { + EditorPrefs.SetString(REditorConsts.k_OnGameRunScriptContentKey, evt.newValue); + }); + rootContainer.Add(onGameRunScriptTextField); + + Label consoleCommandsLabel = new("Console Commands:") { + style = { + fontSize = 12, + color = new Color(0.7f, 0.9f, 0.9f) + } + }; + rootContainer.Add(consoleCommandsLabel); + + ConsoleService.ConsoleCommand[] consoleCommands = ConsoleService.GenerateCommandsToRegister(); + foreach (ConsoleService.ConsoleCommand consoleCommand in consoleCommands) { + VisualElement commandContainer = new() { + style = { + flexDirection = FlexDirection.Row, + marginTop = 4, + marginBottom = 4, + paddingLeft = 4, + paddingRight = 4, + paddingTop = 4, + paddingBottom = 4, + } + }; + Label commandLabel = new(consoleCommand.name) { + style = { + fontSize = 12, + color = new Color(0.7f, 0.9f, 0.9f) + } + }; + commandContainer.Add(commandLabel); + + Label descriptionLabel = new(consoleCommand.description) { + style = { + fontSize = 12, + color = new Color(0.5f, 0.7f, 0.7f) + } + }; + commandContainer.Add(descriptionLabel); + + rootContainer.Add(commandContainer); + } return rootContainer; } diff --git a/Editor/RebootWindow/WorldsView.cs b/Editor/RebootWindow/WorldsView.cs index c757a4e..c357fc6 100644 --- a/Editor/RebootWindow/WorldsView.cs +++ b/Editor/RebootWindow/WorldsView.cs @@ -52,7 +52,7 @@ namespace RebootKitEditor.RebootWindow { } }; root.Add(label); - + Button openButton = new(() => OpenWorldScenes(worldConfigAsset)) { text = "Open", style = { @@ -76,14 +76,16 @@ namespace RebootKitEditor.RebootWindow { return; } - EditorSceneManager.SaveOpenScenes(); - // Load first scene from build settings - string mainScenePath = SceneManager.GetSceneByBuildIndex(RConsts.k_MainSceneBuildIndex).path; + string mainScenePath = GetScenePathByBuildIndex(RConsts.k_MainSceneBuildIndex); + if (mainScenePath == null) { + return; + } + EditorSceneManager.OpenScene(mainScenePath, OpenSceneMode.Single); // Load world scene - string worldScenePath = AssetDatabase.GetAssetPath(worldConfigAsset.Config.mainScene.editorAsset); + string worldScenePath = AssetDatabase.GUIDToAssetPath(worldConfigAsset.Config.mainScene.AssetGUID); if (string.IsNullOrEmpty(worldScenePath)) { Debug.LogError($"WorldConfigAsset {worldConfigAsset.name} has invalid main scene path"); return; @@ -91,5 +93,14 @@ namespace RebootKitEditor.RebootWindow { SceneManager.SetActiveScene(EditorSceneManager.OpenScene(worldScenePath, OpenSceneMode.Additive)); } + + static string GetScenePathByBuildIndex(int buildIndex) { + if (buildIndex < 0 || buildIndex >= EditorBuildSettings.scenes.Length) { + Debug.LogError($"Build index {buildIndex} out of range. Total scenes in build: {EditorBuildSettings.scenes.Length}"); + return null; + } + + return EditorBuildSettings.scenes[buildIndex].path; + } } } \ No newline at end of file diff --git a/README.md b/README.md index 93b6971..b9c47a1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # RebootKit -# Installation +# Installationa (WIP) ### nuget @@ -22,3 +22,10 @@ Ensure you have the following dependencies in your `Packages/manifest.json` file } } ``` + +# TODO +- [ ] Save system +- [ ] Built in main menu +- [ ] Generic settings ui based on Config Variables +- [ ] Extendable FPP Controller +- [ ] UI System diff --git a/Runtime/Engine/Code/AppConfig.cs b/Runtime/Engine/Code/AppConfig.cs deleted file mode 100644 index c2efe9d..0000000 --- a/Runtime/Engine/Code/AppConfig.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using RebootKit.Engine.Services.Game; -using UnityEngine.AddressableAssets; - -namespace RebootKit.Engine { - [Serializable] - public class AppConfig { - public MainMenuConfig mainMenuConfig; - public GameConfig gameConfig; - } - - [Serializable] - public class MainMenuConfig { - public AssetReference scene; - } - - [Serializable] - public class GameConfig { - public GameModeAsset defaultGameMode; - } -} \ No newline at end of file diff --git a/Runtime/Engine/Code/AppConfig.cs.meta b/Runtime/Engine/Code/AppConfig.cs.meta deleted file mode 100644 index 075ff59..0000000 --- a/Runtime/Engine/Code/AppConfig.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 75a5232723f54287b48c0294a0009869 -timeCreated: 1743174115 \ No newline at end of file diff --git a/Runtime/Engine/Code/Components.meta b/Runtime/Engine/Code/Components.meta new file mode 100644 index 0000000..fd555de --- /dev/null +++ b/Runtime/Engine/Code/Components.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 87d77002476b4d1cbe17b0b969e66c9d +timeCreated: 1746819904 \ No newline at end of file diff --git a/Runtime/Engine/Code/Components/CopyTransform.cs b/Runtime/Engine/Code/Components/CopyTransform.cs new file mode 100644 index 0000000..3ec25d8 --- /dev/null +++ b/Runtime/Engine/Code/Components/CopyTransform.cs @@ -0,0 +1,39 @@ +using System; +using UnityEngine; + +namespace RebootKit.Engine.Components { + [Flags] + public enum TransformComponents { + None = 0, + Position = 1 << 0, + Rotation = 1 << 1, + Scale = 1 << 2, + All = Position | Rotation | Scale + } + + [DefaultExecutionOrder(100)] + public class CopyTransform : MonoBehaviour { + [SerializeField] TransformComponents m_Components = TransformComponents.All; + [SerializeField] Transform m_Source; + + Transform m_Transform; + + void Awake() { + m_Transform = transform; + } + + void LateUpdate() { + if (m_Components.HasFlag(TransformComponents.Position)) { + m_Transform.position = m_Source.position; + } + + if (m_Components.HasFlag(TransformComponents.Rotation)) { + m_Transform.rotation = m_Source.rotation; + } + + if (m_Components.HasFlag(TransformComponents.Scale)) { + m_Transform.localScale = m_Source.localScale; + } + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Components/CopyTransform.cs.meta b/Runtime/Engine/Code/Components/CopyTransform.cs.meta new file mode 100644 index 0000000..87b8a57 --- /dev/null +++ b/Runtime/Engine/Code/Components/CopyTransform.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 43e47286597d44b9bca21389d3909958 +timeCreated: 1746661605 \ No newline at end of file diff --git a/Runtime/Engine/Code/EngineConfigAsset.cs b/Runtime/Engine/Code/EngineConfigAsset.cs index a9e66ed..909b023 100644 --- a/Runtime/Engine/Code/EngineConfigAsset.cs +++ b/Runtime/Engine/Code/EngineConfigAsset.cs @@ -1,4 +1,4 @@ -using RebootKit.Engine.Foundation; +using RebootKit.Engine.Main; using UnityEngine; namespace RebootKit.Engine { @@ -7,8 +7,7 @@ namespace RebootKit.Engine { public bool initializeOnLoad = true; public EngineCoreServicesAsset coreServices; - public ServiceAsset[] services; - - public AppConfig appConfig; + + public GameAsset gameAsset; } } diff --git a/Runtime/Engine/Code/EngineCoreServicesAsset.cs b/Runtime/Engine/Code/EngineCoreServicesAsset.cs index bac6946..1e6ebf5 100644 --- a/Runtime/Engine/Code/EngineCoreServicesAsset.cs +++ b/Runtime/Engine/Code/EngineCoreServicesAsset.cs @@ -1,6 +1,6 @@ using RebootKit.Engine.Foundation; using RebootKit.Engine.Services.Console; -using RebootKit.Engine.Services.Game; +using RebootKit.Engine.Services.GameMode; using RebootKit.Engine.Services.Input; using RebootKit.Engine.Services.Simulation; using UnityEngine; @@ -11,6 +11,6 @@ namespace RebootKit.Engine { public ServiceAsset consoleService; public ServiceAsset inputService; public ServiceAsset worldService; - public ServiceAsset gameService; + public ServiceAsset gameService; } } \ No newline at end of file diff --git a/Runtime/Engine/Code/Extensions/Vector3Ex.cs b/Runtime/Engine/Code/Extensions/Vector3Ex.cs index b07d894..2d7a1a3 100644 --- a/Runtime/Engine/Code/Extensions/Vector3Ex.cs +++ b/Runtime/Engine/Code/Extensions/Vector3Ex.cs @@ -32,5 +32,12 @@ namespace RebootKit.Engine.Extensions { public static bool IsNonZero(this float3 vec, float epsilon = float.Epsilon) { return !vec.IsZero(epsilon); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInRange(this float3 vec, float3 pos, float range) { + float dstSquared = math.distancesq(vec, pos); + float rangeSquared = range * range; + return dstSquared <= rangeSquared; + } } } \ No newline at end of file diff --git a/Runtime/Engine/Code/Foundation/ConfigVar.cs b/Runtime/Engine/Code/Foundation/ConfigVar.cs index 05ccf6a..e957710 100644 --- a/Runtime/Engine/Code/Foundation/ConfigVar.cs +++ b/Runtime/Engine/Code/Foundation/ConfigVar.cs @@ -1,6 +1,6 @@ using System; -namespace RebootKit.Engine.Services.Console { +namespace RebootKit.Engine.Foundation { public enum CVarValueKind { Number, String } diff --git a/Runtime/Engine/Code/Foundation/Logger.cs b/Runtime/Engine/Code/Foundation/Logger.cs index e3daff7..04bda84 100644 --- a/Runtime/Engine/Code/Foundation/Logger.cs +++ b/Runtime/Engine/Code/Foundation/Logger.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using RebootKit.Engine.Main; namespace RebootKit.Engine.Foundation { public enum LogLevel { @@ -19,16 +20,16 @@ namespace RebootKit.Engine.Foundation { public void Log(LogLevel level, string message) { switch (level) { case LogLevel.Info: - UnityEngine.Debug.Log(FormatMessage(level, m_Name, message, true)); + RR.Log(FormatMessage(level, m_Name, message, true)); break; case LogLevel.Debug: - UnityEngine.Debug.Log(FormatMessage(level, m_Name, message, true)); + RR.Log(FormatMessage(level, m_Name, message, true)); break; case LogLevel.Warning: - UnityEngine.Debug.LogWarning(FormatMessage(level, m_Name, message, true)); + RR.LogWarning(FormatMessage(level, m_Name, message, true)); break; case LogLevel.Error: - UnityEngine.Debug.LogError(FormatMessage(level, m_Name, message, true)); + RR.LogError(FormatMessage(level, m_Name, message, true)); break; default: throw new ArgumentOutOfRangeException(nameof(level), level, null); diff --git a/Runtime/Engine/Code/Foundation/SceneContext.cs b/Runtime/Engine/Code/Foundation/SceneContext.cs index 0c794ec..45162bf 100644 --- a/Runtime/Engine/Code/Foundation/SceneContext.cs +++ b/Runtime/Engine/Code/Foundation/SceneContext.cs @@ -1,4 +1,5 @@ -using UnityEngine; +using RebootKit.Engine.Main; +using UnityEngine; using UnityEngine.Assertions; namespace RebootKit.Engine.Foundation { @@ -20,7 +21,7 @@ namespace RebootKit.Engine.Foundation { DIContext context = RR.DIContext; foreach (GameObject root in gameObject.scene.GetRootGameObjects()) { - s_logger.Info("Injecting root game object: " + root.name); + // s_logger.Info("Injecting root game object: " + root.name); context.InjectGameObject(root); } } diff --git a/Runtime/Engine/Code/Main.meta b/Runtime/Engine/Code/Main.meta new file mode 100644 index 0000000..1ce162e --- /dev/null +++ b/Runtime/Engine/Code/Main.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ce874693b1034e70a925c0043b78e7ea +timeCreated: 1746821949 \ No newline at end of file diff --git a/Runtime/Engine/Code/Foundation/EntryPoint.cs b/Runtime/Engine/Code/Main/EntryPoint.cs similarity index 73% rename from Runtime/Engine/Code/Foundation/EntryPoint.cs rename to Runtime/Engine/Code/Main/EntryPoint.cs index 7c5d8e1..011cf15 100644 --- a/Runtime/Engine/Code/Foundation/EntryPoint.cs +++ b/Runtime/Engine/Code/Main/EntryPoint.cs @@ -1,10 +1,11 @@ using System.Threading; using Cysharp.Threading.Tasks; -using UnityEditor; +using RebootKit.Engine.Foundation; using UnityEngine; using UnityEngine.SceneManagement; +using Logger = RebootKit.Engine.Foundation.Logger; -namespace RebootKit.Engine.Foundation { +namespace RebootKit.Engine.Main { public static class EntryPoint { static readonly Logger s_logger = new(nameof(EntryPoint)); @@ -16,32 +17,30 @@ namespace RebootKit.Engine.Foundation { s_cancellationTokenSource.Cancel(); } - RR.s_Shared = null; - - // unload all scenes - s_cancellationTokenSource = new CancellationTokenSource(); RunAsync(s_cancellationTokenSource.Token).Forget(); #if UNITY_EDITOR - static void OnPlayerModeState(PlayModeStateChange state) { - if (state == PlayModeStateChange.ExitingPlayMode) { + static void OnPlayerModeState(UnityEditor.PlayModeStateChange state) { + if (state == UnityEditor.PlayModeStateChange.ExitingPlayMode) { s_cancellationTokenSource.Cancel(); } } - EditorApplication.playModeStateChanged -= OnPlayerModeState; - EditorApplication.playModeStateChanged += OnPlayerModeState; + UnityEditor.EditorApplication.playModeStateChanged -= OnPlayerModeState; + UnityEditor.EditorApplication.playModeStateChanged += OnPlayerModeState; #endif } - + static async UniTask RunAsync(CancellationToken cancellationToken) { + ConfigVarsContainer.Init(); + s_logger.Info("Loading boot scene"); SceneManager.LoadScene(RConsts.k_BootSceneBuildIndex, LoadSceneMode.Single); - + s_logger.Info("Loading engine config"); EngineConfigAsset configAsset = Resources.Load(RConsts.k_EngineConfigResourcesPath); - if (configAsset == null) { + if (configAsset is null) { s_logger.Error($"Couldn't load engine config from resources: {RConsts.k_EngineConfigResourcesPath}"); return; } @@ -50,19 +49,18 @@ namespace RebootKit.Engine.Foundation { return; } - using RR instance = new(); - RR.s_Shared = instance; - s_logger.Info("Initializing RR"); - await instance.Init(configAsset, cancellationToken); + await RR.InitAsync(configAsset, cancellationToken); s_logger.Info("Loading main scene"); await SceneManager.LoadSceneAsync(RConsts.k_MainSceneBuildIndex, LoadSceneMode.Single).ToUniTask(cancellationToken: cancellationToken); s_logger.Info("Starting RR"); - instance.Run(); + RR.Run(); await UniTask.WaitUntilCanceled(Application.exitCancellationToken); + + RR.Shutdown(); } } } \ No newline at end of file diff --git a/Runtime/Engine/Code/Foundation/EntryPoint.cs.meta b/Runtime/Engine/Code/Main/EntryPoint.cs.meta similarity index 100% rename from Runtime/Engine/Code/Foundation/EntryPoint.cs.meta rename to Runtime/Engine/Code/Main/EntryPoint.cs.meta diff --git a/Runtime/Engine/Code/Main/RR.cs b/Runtime/Engine/Code/Main/RR.cs new file mode 100644 index 0000000..3dee0e6 --- /dev/null +++ b/Runtime/Engine/Code/Main/RR.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Cysharp.Threading.Tasks; +using R3; +using RebootKit.Engine.Foundation; +using RebootKit.Engine.Services.Console; +using RebootKit.Engine.Services.GameMode; +using RebootKit.Engine.Services.Input; +using RebootKit.Engine.Services.Simulation; +using UnityEngine; +using UnityEngine.AddressableAssets; +using Assert = UnityEngine.Assertions.Assert; +using Logger = RebootKit.Engine.Foundation.Logger; + +namespace RebootKit.Engine.Main { + public interface IGame : IDisposable { + UniTask InitAsync(CancellationToken cancellationToken); + void Run(); + } + + public abstract class GameAsset : ScriptableObject { + public abstract IGame CreateGame(); + } + + public static class RR { + static readonly Logger s_logger = new("RR"); + + [ConfigVar("con.write_log", 1, "Enables writing game log to console output")] + static ConfigVar s_writeLogToConsole; + + static EngineConfigAsset s_engineConfigAsset; + + static DisposableBag s_disposableBag; + static DisposableBag s_servicesBag; + static DIContext s_diContext; + static ConsoleService s_consoleService; + + static GameModesService s_gameModesService; + static InputService s_inputService; + static WorldService s_worldService; + + public static ConsoleService Console => s_consoleService; + public static InputService Input => s_inputService; + public static WorldService World => s_worldService; + public static GameModesService GameModes => s_gameModesService; + public static DIContext DIContext => s_diContext; + + static IGame s_game; + + public static async UniTask InitAsync(EngineConfigAsset configAsset, CancellationToken cancellationToken) { + Assert.IsNotNull(configAsset, "Config asset is required"); + Assert.IsNotNull(configAsset.gameAsset, "Game asset is required"); + + s_engineConfigAsset = configAsset; + + s_logger.Info("Initializing"); + s_servicesBag = new DisposableBag(); + s_disposableBag = new DisposableBag(); + s_diContext = new DIContext(); + + s_logger.Debug("Registering core services"); + s_consoleService = CreateService(s_engineConfigAsset.coreServices.consoleService); + s_inputService = CreateService(s_engineConfigAsset.coreServices.inputService); + s_worldService = CreateService(s_engineConfigAsset.coreServices.worldService); + s_gameModesService = CreateService(s_engineConfigAsset.coreServices.gameService); + + await InitializeAssetsAsync(cancellationToken); + + s_logger.Debug("Creating game"); + s_game = s_engineConfigAsset.gameAsset.CreateGame(); + + await s_game.InitAsync(cancellationToken); + } + + public static void Shutdown() { + s_logger.Info("Shutting down"); + s_servicesBag.Dispose(); + s_disposableBag.Dispose(); + } + + public static void Run() { + s_game.Run(); + +#if UNITY_EDITOR + string scriptContent = UnityEditor.EditorPrefs.GetString("RebootKitEditor.OnGameRunScriptContent", ""); + s_logger.Info($"Executing script: {scriptContent}"); + + if (!string.IsNullOrEmpty(scriptContent)) { + foreach (string cmd in scriptContent.Split('\n')) { + s_logger.Info($"Executing command: {cmd}"); + Console.Execute(cmd); + } + } +#endif + } + + // Assets API + static readonly List s_gameModesAssets = new(); + static readonly List s_worldConfigsAssets = new(); + + public static IReadOnlyList GameModesAssets => s_gameModesAssets; + public static IReadOnlyList WorldConfigsAssets => s_worldConfigsAssets; + + public static async UniTask InitializeAssetsAsync(CancellationToken cancellationToken) { + s_gameModesAssets.Clear(); + s_worldConfigsAssets.Clear(); + + s_logger.Info("Loading game assets"); + + await Addressables.LoadAssetsAsync("game_mode", asset => { s_gameModesAssets.Add(asset); }).ToUniTask(cancellationToken: cancellationToken); + s_logger.Info($"Loaded {s_gameModesAssets.Count} game modes"); + + await Addressables.LoadAssetsAsync("world", asset => { s_worldConfigsAssets.Add(asset); }).ToUniTask(cancellationToken: cancellationToken); + } + + // Game API + public static void StartGameMode(GameModeAsset gameMode, WorldConfig world) { + if (gameMode is null) { + throw new ArgumentNullException(nameof(gameMode)); + } + + s_logger.Info($"Starting game mode: {gameMode.name} in world: {world.name}"); + s_gameModesService.Start(gameMode, world); + } + + public static TGame Game() where TGame : IGame { + if (s_game is TGame game) { + return game; + } + + throw new InvalidOperationException($"Game is not of type {typeof(TGame)}"); + } + + // Service API + public static TService CreateService(ServiceAsset asset) where TService : class, IService { + TService service = asset.Create(s_diContext); + s_diContext.Bind(service); + s_servicesBag.Add(service); + return service; + } + + public static TService CreateService() where TService : class, IService { + TService service = s_diContext.Create(); + s_diContext.Bind(service); + s_servicesBag.Add(service); + return service; + } + + // General API + public static void Log(string message) { + Debug.Log(message); + + s_consoleService?.WriteToOutput(message); + } + + public static void LogWarning(string message) { + Debug.LogWarning(message); + s_consoleService?.WriteToOutput(message); + } + + public static void LogError(string message) { + Debug.LogError(message); + s_consoleService?.WriteToOutput(message); + } + + // CVar API + public static ConfigVar CVarIndex(string name, int defaultValue = -1) { + ConfigVar cvar = ConfigVarsContainer.Get(name); + if (cvar != null) { + return cvar; + } + + cvar = new ConfigVar(name, defaultValue); + ConfigVarsContainer.Register(cvar); + return cvar; + } + + public static ConfigVar CVarNumber(string name, double defaultValue = 0) { + ConfigVar cvar = ConfigVarsContainer.Get(name); + if (cvar != null) { + return cvar; + } + + cvar = new ConfigVar(name, defaultValue); + ConfigVarsContainer.Register(cvar); + return cvar; + } + + public static ConfigVar CVarString(string name, string defaultValue = "") { + ConfigVar cvar = ConfigVarsContainer.Get(name); + if (cvar != null) { + return cvar; + } + + cvar = new ConfigVar(name, defaultValue); + ConfigVarsContainer.Register(cvar); + return cvar; + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/RR.cs.meta b/Runtime/Engine/Code/Main/RR.cs.meta similarity index 100% rename from Runtime/Engine/Code/RR.cs.meta rename to Runtime/Engine/Code/Main/RR.cs.meta diff --git a/Runtime/Engine/Code/Misc.meta b/Runtime/Engine/Code/Misc.meta new file mode 100644 index 0000000..2e49d4e --- /dev/null +++ b/Runtime/Engine/Code/Misc.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7a57aa60af614860ba14c07fdb43ad4e +timeCreated: 1746661579 \ No newline at end of file diff --git a/Runtime/Engine/Code/RR.API.cs b/Runtime/Engine/Code/RR.API.cs deleted file mode 100644 index 3f9eaba..0000000 --- a/Runtime/Engine/Code/RR.API.cs +++ /dev/null @@ -1,49 +0,0 @@ -using RebootKit.Engine.Foundation; -using RebootKit.Engine.Services.Console; -using RebootKit.Engine.Services.Game; -using RebootKit.Engine.Services.Input; -using RebootKit.Engine.Services.Simulation; -using UnityEngine.Events; - -namespace RebootKit.Engine { - public partial class RR { - public static ConsoleService Console => s_Shared.m_ConsoleService; - public static InputService Input => s_Shared.m_InputService; - public static WorldService World => s_Shared.m_WorldService; - public static GameService Game => s_Shared.m_GameService; - public static DIContext DIContext => s_Shared.m_DIContext; - - public static ConfigVar CVarIndex(string name, int defaultValue = -1) { - ConfigVar cvar = ConfigVarsContainer.Get(name); - if (cvar != null) { - return cvar; - } - - cvar = new ConfigVar(name, defaultValue); - ConfigVarsContainer.Register(cvar); - return cvar; - } - - public static ConfigVar CVarNumber(string name, double defaultValue = 0) { - ConfigVar cvar = ConfigVarsContainer.Get(name); - if (cvar != null) { - return cvar; - } - - cvar = new ConfigVar(name, defaultValue); - ConfigVarsContainer.Register(cvar); - return cvar; - } - - public static ConfigVar CVarString(string name, string defaultValue = "") { - ConfigVar cvar = ConfigVarsContainer.Get(name); - if (cvar != null) { - return cvar; - } - - cvar = new ConfigVar(name, defaultValue); - ConfigVarsContainer.Register(cvar); - return cvar; - } - } -} \ No newline at end of file diff --git a/Runtime/Engine/Code/RR.API.cs.meta b/Runtime/Engine/Code/RR.API.cs.meta deleted file mode 100644 index 25d97c9..0000000 --- a/Runtime/Engine/Code/RR.API.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 90375a4c5db84d538d31d54f302ac022 -timeCreated: 1743212750 \ No newline at end of file diff --git a/Runtime/Engine/Code/RR.Game.cs b/Runtime/Engine/Code/RR.Game.cs deleted file mode 100644 index 39e52d8..0000000 --- a/Runtime/Engine/Code/RR.Game.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using RebootKit.Engine.Services.Game; - -namespace RebootKit.Engine { - public partial class RR { - public static void StartGameMode(GameModeAsset gameMode) { - if (gameMode == null) { - throw new ArgumentNullException(nameof(gameMode)); - } - - s_Shared.m_GameService.Start(gameMode); - } - } -} \ No newline at end of file diff --git a/Runtime/Engine/Code/RR.Game.cs.meta b/Runtime/Engine/Code/RR.Game.cs.meta deleted file mode 100644 index 3b9cab8..0000000 --- a/Runtime/Engine/Code/RR.Game.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: c614fae80bc84ae8b7f3ee034a42f074 -timeCreated: 1743437396 \ No newline at end of file diff --git a/Runtime/Engine/Code/RR.cs b/Runtime/Engine/Code/RR.cs deleted file mode 100644 index 8920b12..0000000 --- a/Runtime/Engine/Code/RR.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Threading; -using Cysharp.Threading.Tasks; -using R3; -using RebootKit.Engine.Foundation; -using RebootKit.Engine.Services.Console; -using RebootKit.Engine.Services.Game; -using RebootKit.Engine.Services.Input; -using RebootKit.Engine.Services.Simulation; -using UnityEngine.Assertions; -using Logger = RebootKit.Engine.Foundation.Logger; - -namespace RebootKit.Engine { - public partial class RR : IDisposable { - internal static RR s_Shared; - - static readonly Logger s_logger = new("RR"); - - DIContext m_DIContext; - - ConsoleService m_ConsoleService; - - EngineConfigAsset m_EngineConfigAsset; - GameService m_GameService; - InputService m_InputService; - WorldService m_WorldService; - - DisposableBag m_DisposableBag; - - public RR() { - m_DisposableBag = new DisposableBag(); - - m_DIContext = new DIContext(); - // m_DIContext.AddInjector(new CVarFieldInjector()); - } - - public void Dispose() { - s_logger.Info("Shutting down"); - m_DisposableBag.Dispose(); - } - - public async UniTask Init(EngineConfigAsset configAsset, CancellationToken cancellationToken) { - Assert.IsNotNull(configAsset, "Config asset is required"); - - s_logger.Info("Initializing"); - m_EngineConfigAsset = configAsset; - - CreateCoreServices(); - CreateServices(); - - await UniTask.Yield(cancellationToken); - } - - public void Run() { - StartGameMode(m_EngineConfigAsset.appConfig.gameConfig.defaultGameMode); - } - - TService CreateService(ServiceAsset asset) where TService : class, IService { - TService service = asset.Create(m_DIContext); - m_DIContext.Bind(service); - m_DisposableBag.Add(service); - return service; - } - - void CreateCoreServices() { - s_logger.Debug("Registering core services"); - - m_ConsoleService = CreateService(m_EngineConfigAsset.coreServices.consoleService); - m_InputService = CreateService(m_EngineConfigAsset.coreServices.inputService); - m_WorldService = CreateService(m_EngineConfigAsset.coreServices.worldService); - m_GameService = CreateService(m_EngineConfigAsset.coreServices.gameService); - } - - void CreateServices() { - foreach (ServiceAsset serviceAsset in m_EngineConfigAsset.services) { - CreateService(serviceAsset); - } - } - } -} \ No newline at end of file diff --git a/Runtime/Engine/Code/Services/Console/ConsoleService.cs b/Runtime/Engine/Code/Services/Console/ConsoleService.cs index 6f5939c..ee8da30 100644 --- a/Runtime/Engine/Code/Services/Console/ConsoleService.cs +++ b/Runtime/Engine/Code/Services/Console/ConsoleService.cs @@ -2,40 +2,35 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using RebootKit.Engine.Foundation; +using RebootKit.Engine.Main; using UnityEngine; using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootKit.Engine.Services.Console { - public interface IConsoleCommand { - string Name { get; } - string Description { get; } - void Execute(string[] args); - } + [AttributeUsage(AttributeTargets.Method)] + public class RCCMD : Attribute { + public string name; + public string description; - public class HelpCommand : IConsoleCommand { - public string Name { get; } = "help"; - public string Description { get; } = "Prints available commands/cvars and their descriptions."; - - public void Execute(string[] args) { - RR.Console.PrintHelp(); - } - } - - public class PrintCVarsCommand : IConsoleCommand { - public string Name { get; } = "print_cvars"; - public string Description { get; } = "Prints all cvars and their values."; - - public void Execute(string[] args) { - RR.Console.PrintCVars(); + public RCCMD(string name, string description) { + this.name = name; + this.description = description; } } public class ConsoleService : IService { static readonly Logger s_logger = new(nameof(ConsoleService)); - readonly List m_ConsoleCommands = new(); + public struct ConsoleCommand { + public string name; + public string description; + public Action action; + } + + readonly List m_ConsoleCommands = new(); FileStream m_LogFileStream; TextWriter m_LogFileWriter; @@ -60,8 +55,7 @@ namespace RebootKit.Engine.Services.Console { Load(); - RegisterCommand(new PrintCVarsCommand()); - RegisterCommand(new HelpCommand()); + RegisterCommands(); } public void Dispose() { @@ -97,37 +91,34 @@ namespace RebootKit.Engine.Services.Console { OnOutputMessage?.Invoke(message); } - string[] ParseCommandInputArguments(string text) { - if (text.Length == 0) { - return Array.Empty(); - } - - return new[] {text}; + string[] ParseCommandInput(string text) { + return text.Split(' '); } + // @NOTE: Input must be in format: "command arg1 arg2 arg3", one command = one call public void Execute(string input) { if (input.Length == 0) { return; } - string commandName = input; - if (input.IndexOf(' ') != -1) { - commandName = input.Substring(0, input.IndexOf(' ')); + string[] arguments = ParseCommandInput(input); + if (arguments.Length == 0) { + return; } - string[] arguments = ParseCommandInputArguments(input.Substring(commandName.Length)); + string commandName = arguments[0]; - foreach (IConsoleCommand command in m_ConsoleCommands) { - if (command.Name.Equals(commandName)) { - command.Execute(arguments); + foreach (ConsoleCommand command in m_ConsoleCommands) { + if (command.name.Equals(commandName)) { + command.action(arguments); return; } } foreach (ConfigVar cvar in ConfigVarsContainer.All()) { if (cvar.name.Equals(commandName)) { - if (arguments.Length == 1) { - cvar.ParseFromString(arguments[0]); + if (arguments.Length == 2) { + cvar.ParseFromString(arguments[1]); } WriteToOutput($"{cvar.name} - {cvar}\n"); @@ -138,25 +129,84 @@ namespace RebootKit.Engine.Services.Console { WriteToOutput($"ERROR: Command/CVar `{commandName}` not found."); } - public void RegisterCommand(IConsoleCommand command) { - if (m_ConsoleCommands.Any(t => t.Name.Equals(command.Name))) { - s_logger.Error($"`{command.Name}` command is already registered"); + public void RegisterCommand(string name, string description, Action action) { + if (IsCommandRegistered(name)) { + s_logger.Error($"`{name}` command is already registered"); return; } - m_ConsoleCommands.Add(command); - s_logger.Info($"Registered command: {command.Name}"); + m_ConsoleCommands.Add(new ConsoleCommand { + name = name, + description = description, + action = action + }); + s_logger.Info($"Registered command: {name}"); } - public void PrintHelp() { + public static ConsoleCommand[] GenerateCommandsToRegister() { + IEnumerable methods = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assembly => assembly.GetTypes()) + .SelectMany(type => type.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)) + .Where(method => method.GetCustomAttributes(typeof(RCCMD), false).Length > 0); + + List commands = new(); + foreach (MethodInfo method in methods) { + RCCMD attribute = (RCCMD)method.GetCustomAttributes(typeof(RCCMD), false)[0]; + + if (!method.IsStatic) { + s_logger.Error($"Command `{attribute.name}` is not static, skipping"); + continue; + } + + if (method.GetParameters().Length != 1) { + s_logger.Error($"Command `{attribute.name}` has invalid number of parameters, skipping"); + continue; + } + + if (method.GetParameters()[0].ParameterType != typeof(string[])) { + s_logger.Error($"Command `{attribute.name}` has invalid parameter type, skipping"); + continue; + } + + Action action = (Action)Delegate.CreateDelegate(typeof(Action), method); + + commands.Add(new ConsoleCommand { + name = attribute.name, + description = attribute.description, + action = action + }); + } + + return commands.ToArray(); + } + + public void RegisterCommands() { + ConsoleCommand[] commands = GenerateCommandsToRegister(); + foreach (ConsoleCommand command in commands) { + RegisterCommand(command.name, command.description, command.action); + } + } + + bool IsCommandRegistered(string name) { + foreach (ConsoleCommand command in m_ConsoleCommands) { + if (command.name.Equals(name)) { + return true; + } + } + + return false; + } + + [RCCMD("help", "Prints help message with all commands and cvars")] + public static void PrintHelpCommand(string[] args) { StringBuilder message = new(); message.AppendLine("Available commands:"); - foreach (IConsoleCommand command in m_ConsoleCommands) { + foreach (ConsoleCommand command in RR.Console.m_ConsoleCommands) { message.Append(" "); - message.Append(command.Name); + message.Append(command.name); message.Append(" - "); - message.Append(command.Description); + message.Append(command.description); message.AppendLine(); } @@ -169,17 +219,18 @@ namespace RebootKit.Engine.Services.Console { message.AppendLine(); } - WriteToOutput(message.ToString()); + RR.Console.WriteToOutput(message.ToString()); } - public void PrintCVars() { + [RCCMD("cvars", "Prints all cvars")] + public static void PrintCVars(string[] args) { StringBuilder message = new(); foreach (ConfigVar cvar in ConfigVarsContainer.All()) { message.AppendLine($"{cvar.name} - {cvar}"); } - WriteToOutput(message.ToString()); + RR.Console.WriteToOutput(message.ToString()); } void Save() { diff --git a/Runtime/Engine/Code/Services/ConsoleUI/ConsoleUIService.cs b/Runtime/Engine/Code/Services/ConsoleUI/ConsoleUIService.cs index 06f59df..3abfda8 100644 --- a/Runtime/Engine/Code/Services/ConsoleUI/ConsoleUIService.cs +++ b/Runtime/Engine/Code/Services/ConsoleUI/ConsoleUIService.cs @@ -1,5 +1,6 @@ using System.Text; using RebootKit.Engine.Foundation; +using RebootKit.Engine.Main; using RebootKit.Engine.Services.Input; using UnityEngine; using UnityEngine.InputSystem; diff --git a/Runtime/Engine/Code/Services/ConsoleUI/ConsoleVC.cs b/Runtime/Engine/Code/Services/ConsoleUI/ConsoleVC.cs index a2d5391..90752ef 100644 --- a/Runtime/Engine/Code/Services/ConsoleUI/ConsoleVC.cs +++ b/Runtime/Engine/Code/Services/ConsoleUI/ConsoleVC.cs @@ -44,6 +44,10 @@ namespace RebootKit.Engine.Services.ConsoleUI { public event Action ClearRequested = () => { }; public void SetMessageContent(string message) { + if (m_LabelMessage == null) { + return; + } + m_LabelMessage.text = message; m_ScrollView.schedule.Execute(() => { m_ScrollView.scrollOffset = new Vector2(0, m_ScrollView.contentContainer.contentRect.height); }).StartingIn(16); } diff --git a/Runtime/Engine/Code/Services/Crosshair.meta b/Runtime/Engine/Code/Services/Crosshair.meta new file mode 100644 index 0000000..7493f37 --- /dev/null +++ b/Runtime/Engine/Code/Services/Crosshair.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3d31f3fe40394defabe1b77f6ddf8861 +timeCreated: 1746665551 \ No newline at end of file diff --git a/Runtime/Engine/Code/Services/Crosshair/CrosshairService.cs b/Runtime/Engine/Code/Services/Crosshair/CrosshairService.cs new file mode 100644 index 0000000..47245ce --- /dev/null +++ b/Runtime/Engine/Code/Services/Crosshair/CrosshairService.cs @@ -0,0 +1,13 @@ +using RebootKit.Engine.Foundation; + +namespace RebootKit.Engine.Services.Crosshair { + public class CrosshairService : IService { + + public CrosshairService() { + + } + + public void Dispose() { + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Services/Crosshair/CrosshairService.cs.meta b/Runtime/Engine/Code/Services/Crosshair/CrosshairService.cs.meta new file mode 100644 index 0000000..a4f576f --- /dev/null +++ b/Runtime/Engine/Code/Services/Crosshair/CrosshairService.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f23a52c23aae46e8bd6b77222b10e5e2 +timeCreated: 1746665562 \ No newline at end of file diff --git a/Runtime/Engine/Code/Services/Development/DebugOverlayView.cs b/Runtime/Engine/Code/Services/Development/DebugOverlayView.cs index 3335c7b..0c278a8 100644 --- a/Runtime/Engine/Code/Services/Development/DebugOverlayView.cs +++ b/Runtime/Engine/Code/Services/Development/DebugOverlayView.cs @@ -1,4 +1,5 @@ using System; +using RebootKit.Engine.Main; using RebootKit.Engine.UI; using UnityEngine; using UnityEngine.UIElements; @@ -10,13 +11,18 @@ namespace RebootKit.Engine.Services.Development { VisualElement m_RootElement; Label m_FPSLabel; + Label m_GameModeLabel; void Update() { if (m_RootElement == null) { return; } - m_FPSLabel.text = $"fps: {Mathf.RoundToInt(1f / Time.deltaTime)} | dt: {Time.deltaTime:F4}ms | runtime: {Time.time:F4}s"; + Resolution resolution = Screen.currentResolution; + m_FPSLabel.text = $"fps: {Mathf.RoundToInt(1f / Time.deltaTime)} | dt: {Time.deltaTime:F4}ms | runtime: {Time.time:F4}s | resolution: {resolution.width}x{resolution.height}@{resolution.refreshRateRatio}Hz"; + m_GameModeLabel.text = RR.GameModes.ActiveGameMode.CurrentValue != null + ? $"Game Mode: {RR.GameModes.ActiveGameMode.CurrentValue.GetType().Name}" + : "Game Mode: none"; } public override VisualElement Build() { @@ -24,7 +30,8 @@ namespace RebootKit.Engine.Services.Development { CreateLabel($"Toggle Overlay [F3] | RebootKit | game: {Application.productName}, version: {Application.version}"); m_FPSLabel = CreateLabel($"FPS: {Application.targetFrameRate}"); - + + m_GameModeLabel = CreateLabel("Game Mode: none"); return m_RootElement; } diff --git a/Runtime/Engine/Code/Services/Game/GameService.cs b/Runtime/Engine/Code/Services/Game/GameService.cs deleted file mode 100644 index 7e70974..0000000 --- a/Runtime/Engine/Code/Services/Game/GameService.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Threading; -using Cysharp.Threading.Tasks; -using R3; -using RebootKit.Engine.Foundation; - -namespace RebootKit.Engine.Services.Game { - public class GameService : IService { - static readonly Logger s_logger = new(nameof(GameService)); - - [Inject] DIContext m_DIContext; - - GameModeAsset m_GameModeAsset; - - CancellationTokenSource m_DestroyCancellationTokenSource = new(); - DisposableBag m_ActiveGameModeDisposableBag; - IGameMode m_ActiveGameMode; - - public void Dispose() { - m_DestroyCancellationTokenSource.Cancel(); - m_DestroyCancellationTokenSource.Dispose(); - m_ActiveGameModeDisposableBag.Dispose(); - } - - public void Start(GameModeAsset asset) { - if (m_ActiveGameMode != null) { - s_logger.Warning("Game is already running"); - return; - } - - Stop(); - - m_ActiveGameModeDisposableBag = new DisposableBag(); - m_ActiveGameMode = asset.Create(m_DIContext); - m_ActiveGameModeDisposableBag.Add(m_ActiveGameMode); - - InitializeGameModeAsync().Forget(); - } - - async UniTask InitializeGameModeAsync() { - await m_ActiveGameMode.OnInit(m_DestroyCancellationTokenSource.Token); - - m_ActiveGameMode.OnStart(); - Observable.EveryUpdate().Subscribe(_ => { m_ActiveGameMode?.OnTick(); }).AddTo(ref m_ActiveGameModeDisposableBag); - } - - public void Stop() { - if (m_ActiveGameMode == null) { - return; - } - - m_ActiveGameMode.OnStop(); - m_ActiveGameMode = null; - - m_ActiveGameModeDisposableBag.Dispose(); - } - } -} \ No newline at end of file diff --git a/Runtime/Engine/Code/Services/Game/GameServiceAsset.cs b/Runtime/Engine/Code/Services/Game/GameServiceAsset.cs deleted file mode 100644 index 7f5ee7e..0000000 --- a/Runtime/Engine/Code/Services/Game/GameServiceAsset.cs +++ /dev/null @@ -1,12 +0,0 @@ -using RebootKit.Engine.Foundation; -using UnityEngine; - -namespace RebootKit.Engine.Services.Game { - [CreateAssetMenu(menuName = RConsts.k_ServiceAssetMenu + "Game")] - public class GameServiceAsset : ServiceAsset { - public override GameService Create(DIContext context) { - GameService service = context.Create(); - return service; - } - } -} \ No newline at end of file diff --git a/Runtime/Engine/Code/Services/Game/Player.cs b/Runtime/Engine/Code/Services/Game/Player.cs deleted file mode 100644 index 4974701..0000000 --- a/Runtime/Engine/Code/Services/Game/Player.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace RebootKit.Engine.Services.Game { - public class Player { - - } -} \ No newline at end of file diff --git a/Runtime/Engine/Code/Services/Game/Player.cs.meta b/Runtime/Engine/Code/Services/Game/Player.cs.meta deleted file mode 100644 index c80dd28..0000000 --- a/Runtime/Engine/Code/Services/Game/Player.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: be94d631484b4bd2a2c9825290b74b36 -timeCreated: 1744413325 \ No newline at end of file diff --git a/Runtime/Engine/Code/Services/Game.meta b/Runtime/Engine/Code/Services/GameMode.meta similarity index 100% rename from Runtime/Engine/Code/Services/Game.meta rename to Runtime/Engine/Code/Services/GameMode.meta diff --git a/Runtime/Engine/Code/Services/GameMode/GameModesService.cs b/Runtime/Engine/Code/Services/GameMode/GameModesService.cs new file mode 100644 index 0000000..733454e --- /dev/null +++ b/Runtime/Engine/Code/Services/GameMode/GameModesService.cs @@ -0,0 +1,62 @@ +using System.Threading; +using Cysharp.Threading.Tasks; +using R3; +using RebootKit.Engine.Foundation; +using RebootKit.Engine.Main; +using RebootKit.Engine.Services.Simulation; + +namespace RebootKit.Engine.Services.GameMode { + public class GameModesService : IService { + static readonly Logger s_logger = new(nameof(GameModesService)); + + [Inject] DIContext m_DIContext; + + GameModeAsset m_GameModeAsset; + + readonly CancellationTokenSource m_DestroyCancellationTokenSource = new(); + DisposableBag m_ActiveGameModeDisposableBag; + readonly ReactiveProperty m_ActiveGameMode = new(null); + + public ReadOnlyReactiveProperty ActiveGameMode => m_ActiveGameMode; + + public void Dispose() { + m_DestroyCancellationTokenSource.Cancel(); + m_DestroyCancellationTokenSource.Dispose(); + m_ActiveGameModeDisposableBag.Dispose(); + } + + public void Start(GameModeAsset asset, WorldConfig worldConfig) { + if (m_ActiveGameMode.Value != null) { + s_logger.Warning("Game is already running"); + return; + } + + Stop(); + + m_ActiveGameModeDisposableBag = new DisposableBag(); + m_ActiveGameMode.Value = asset.Create(m_DIContext); + m_ActiveGameModeDisposableBag.Add(m_ActiveGameMode); + + InitializeGameModeAsync(worldConfig, CancellationToken.None).Forget(); + } + + async UniTask InitializeGameModeAsync(WorldConfig worldConfig, CancellationToken cancellationToken) { + await m_ActiveGameMode.Value.OnInit(m_DestroyCancellationTokenSource.Token); + + await RR.World.LoadAsync(worldConfig, cancellationToken); + m_ActiveGameMode.Value.OnStart(); + Observable.EveryUpdate().Subscribe(_ => { m_ActiveGameMode.Value?.OnTick(); }).AddTo(ref m_ActiveGameModeDisposableBag); + } + + public void Stop() { + if (m_ActiveGameMode.Value == null) { + return; + } + + m_ActiveGameMode.Value.OnStop(); + m_ActiveGameMode.Value = null; + + m_ActiveGameModeDisposableBag.Dispose(); + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Services/Game/GameService.cs.meta b/Runtime/Engine/Code/Services/GameMode/GameModesService.cs.meta similarity index 100% rename from Runtime/Engine/Code/Services/Game/GameService.cs.meta rename to Runtime/Engine/Code/Services/GameMode/GameModesService.cs.meta diff --git a/Runtime/Engine/Code/Services/GameMode/GameServiceAsset.cs b/Runtime/Engine/Code/Services/GameMode/GameServiceAsset.cs new file mode 100644 index 0000000..6def6b7 --- /dev/null +++ b/Runtime/Engine/Code/Services/GameMode/GameServiceAsset.cs @@ -0,0 +1,12 @@ +using RebootKit.Engine.Foundation; +using UnityEngine; + +namespace RebootKit.Engine.Services.GameMode { + [CreateAssetMenu(menuName = RConsts.k_ServiceAssetMenu + "Game")] + public class GameServiceAsset : ServiceAsset { + public override GameModesService Create(DIContext context) { + GameModesService service = context.Create(); + return service; + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Services/Game/GameServiceAsset.cs.meta b/Runtime/Engine/Code/Services/GameMode/GameServiceAsset.cs.meta similarity index 100% rename from Runtime/Engine/Code/Services/Game/GameServiceAsset.cs.meta rename to Runtime/Engine/Code/Services/GameMode/GameServiceAsset.cs.meta diff --git a/Runtime/Engine/Code/Services/Game/IGameMode.cs b/Runtime/Engine/Code/Services/GameMode/IGameMode.cs similarity index 88% rename from Runtime/Engine/Code/Services/Game/IGameMode.cs rename to Runtime/Engine/Code/Services/GameMode/IGameMode.cs index 282fa09..9c8e309 100644 --- a/Runtime/Engine/Code/Services/Game/IGameMode.cs +++ b/Runtime/Engine/Code/Services/GameMode/IGameMode.cs @@ -3,7 +3,7 @@ using System.Threading; using Cysharp.Threading.Tasks; using RebootKit.Engine.Foundation; -namespace RebootKit.Engine.Services.Game { +namespace RebootKit.Engine.Services.GameMode { public interface IGameMode : IDisposable { UniTask OnInit(CancellationToken cancellationToken); diff --git a/Runtime/Engine/Code/Services/Game/IGameMode.cs.meta b/Runtime/Engine/Code/Services/GameMode/IGameMode.cs.meta similarity index 100% rename from Runtime/Engine/Code/Services/Game/IGameMode.cs.meta rename to Runtime/Engine/Code/Services/GameMode/IGameMode.cs.meta diff --git a/Runtime/Engine/Code/Services/Simulation/WorldService.cs b/Runtime/Engine/Code/Services/Simulation/WorldService.cs index 9f40d53..734c549 100644 --- a/Runtime/Engine/Code/Services/Simulation/WorldService.cs +++ b/Runtime/Engine/Code/Services/Simulation/WorldService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Threading; using Cysharp.Threading.Tasks; using RebootKit.Engine.Foundation; @@ -7,30 +8,72 @@ using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.ResourceManagement.ResourceProviders; using UnityEngine.SceneManagement; +using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootKit.Engine.Services.Simulation { public class WorldService : IService { - readonly List m_Actors = new(); - WorldConfig m_Config; - - public void Dispose() { - KillAllActors(); + static readonly Logger s_logger = new(nameof(WorldService)); + + enum WorldState { + Unloaded, + Loading, + Loaded } - public async UniTask Load(WorldConfig worldConfig) { + WorldState m_WorldState = WorldState.Unloaded; + WorldConfig m_Config; + AsyncOperationHandle m_SceneInstance; + + readonly List m_Actors = new(); + + public void Dispose() { + Unload(); + } + + public async UniTask LoadAsync(WorldConfig worldConfig, CancellationToken cancellationToken) { + await UniTask.WaitWhile(() => m_WorldState == WorldState.Loading, cancellationToken: cancellationToken); + + Unload(); + + m_WorldState = WorldState.Loading; m_Config = worldConfig; - AsyncOperationHandle handle = worldConfig.mainScene.LoadSceneAsync(LoadSceneMode.Additive, false); - await handle.ToUniTask(); + m_SceneInstance = worldConfig.mainScene.LoadSceneAsync(LoadSceneMode.Additive, false); + await m_SceneInstance.ToUniTask(cancellationToken: cancellationToken); - await handle.Result.ActivateAsync(); - SceneManager.SetActiveScene(handle.Result.Scene); + await m_SceneInstance.Result.ActivateAsync(); + SceneManager.SetActiveScene(m_SceneInstance.Result.Scene); - foreach (GameObject root in handle.Result.Scene.GetRootGameObjects()) { + foreach (GameObject root in m_SceneInstance.Result.Scene.GetRootGameObjects()) { foreach (Actor actor in root.GetComponentsInChildren()) { RegisterActor(actor); } } + + m_WorldState = WorldState.Loaded; + } + + public async UniTask LoadAsync(WorldConfig worldConfig, CancellationToken cancellationToken) where TRequiredActor : Actor { + await LoadAsync(worldConfig, cancellationToken); + + TRequiredActor actor = m_Actors.FirstOrDefault(t => t is TRequiredActor) as TRequiredActor; + if (actor is null) { + s_logger.Error($"Actor of type {typeof(TRequiredActor)} not found in the scene"); + return null; + } + + return actor; + } + + public void Unload() { + KillAllActors(); + + if (m_SceneInstance.IsValid()) { + m_SceneInstance.Release(); + m_SceneInstance = default; + } + + m_WorldState = WorldState.Unloaded; } public async UniTask SpawnActor(AssetReferenceT asset, CancellationToken cancellationToken) where TActor : Actor { diff --git a/Runtime/Engine/core_assets/ui/fonts/Teko/Teko-VariableFont_wght SDF.asset b/Runtime/Engine/core_assets/ui/fonts/Teko/Teko-VariableFont_wght SDF.asset new file mode 100644 index 0000000..248d0b8 --- /dev/null +++ b/Runtime/Engine/core_assets/ui/fonts/Teko/Teko-VariableFont_wght SDF.asset @@ -0,0 +1,218 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 19001, guid: 0000000000000000e000000000000000, type: 0} + m_Name: Teko-VariableFont_wght SDF + m_EditorClassIdentifier: + m_Version: 1.1.0 + m_Material: {fileID: 1107025805838944769} + m_SourceFontFileGUID: 7c0f83ef535953a479fd66830c0386b1 + m_fontAssetCreationEditorSettings: + sourceFontFileGUID: 7c0f83ef535953a479fd66830c0386b1 + faceIndex: 0 + pointSizeSamplingMode: 0 + pointSize: 90 + padding: 9 + paddingMode: 2 + packingMode: 0 + atlasWidth: 1024 + atlasHeight: 1024 + characterSetSelectionMode: 7 + characterSequence: + referencedFontAssetGUID: + referencedTextAssetGUID: + fontStyle: 0 + fontStyleModifier: 0 + renderMode: 4165 + includeFontFeatures: 0 + m_SourceFontFile: {fileID: 12800000, guid: 7c0f83ef535953a479fd66830c0386b1, type: 3} + m_SourceFontFilePath: + m_AtlasPopulationMode: 1 + InternalDynamicOS: 0 + IsEditorFont: 0 + m_FaceInfo: + m_FaceIndex: 0 + m_FamilyName: Teko + m_StyleName: Light + m_PointSize: 90 + m_Scale: 1 + m_UnitsPerEM: 1000 + m_LineHeight: 128.97 + m_AscentLine: 86.22 + m_CapLine: 56 + m_MeanLine: 44 + m_Baseline: 0 + m_DescentLine: -42.75 + m_SuperscriptOffset: 86.22 + m_SuperscriptSize: 0.5 + m_SubscriptOffset: -42.75 + m_SubscriptSize: 0.5 + m_UnderlineOffset: -11.25 + m_UnderlineThickness: 4.5 + m_StrikethroughOffset: 17.6 + m_StrikethroughThickness: 4.5 + m_TabWidth: 12 + m_GlyphTable: [] + m_CharacterTable: [] + m_AtlasTextures: + - {fileID: 4954975621857233213} + m_AtlasTextureIndex: 0 + m_IsMultiAtlasTexturesEnabled: 0 + m_GetFontFeatures: 1 + m_ClearDynamicDataOnBuild: 0 + m_AtlasWidth: 1024 + m_AtlasHeight: 1024 + m_AtlasPadding: 9 + m_AtlasRenderMode: 4165 + m_UsedGlyphRects: [] + m_FreeGlyphRects: + - m_X: 0 + m_Y: 0 + m_Width: 1023 + m_Height: 1023 + m_FontFeatureTable: + m_MultipleSubstitutionRecords: [] + m_LigatureSubstitutionRecords: [] + m_GlyphPairAdjustmentRecords: [] + m_MarkToBaseAdjustmentRecords: [] + m_MarkToMarkAdjustmentRecords: [] + m_ShouldReimportFontFeatures: 0 + m_FallbackFontAssetTable: [] + m_FontWeightTable: + - regularTypeface: {fileID: 0} + italicTypeface: {fileID: 0} + - regularTypeface: {fileID: 0} + italicTypeface: {fileID: 0} + - regularTypeface: {fileID: 0} + italicTypeface: {fileID: 0} + - regularTypeface: {fileID: 0} + italicTypeface: {fileID: 0} + - regularTypeface: {fileID: 0} + italicTypeface: {fileID: 0} + - regularTypeface: {fileID: 0} + italicTypeface: {fileID: 0} + - regularTypeface: {fileID: 0} + italicTypeface: {fileID: 0} + - regularTypeface: {fileID: 0} + italicTypeface: {fileID: 0} + - regularTypeface: {fileID: 0} + italicTypeface: {fileID: 0} + - regularTypeface: {fileID: 0} + italicTypeface: {fileID: 0} + m_RegularStyleWeight: 0 + m_RegularStyleSpacing: 0 + m_BoldStyleWeight: 0.75 + m_BoldStyleSpacing: 7 + m_ItalicStyleSlant: 35 + m_TabMultiple: 10 +--- !u!21 &1107025805838944769 +Material: + serializedVersion: 8 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Teko-VariableFont_wght Atlas Material + m_Shader: {fileID: 19011, guid: 0000000000000000f000000000000000, type: 0} + m_Parent: {fileID: 0} + m_ModifiedSerializedProperties: 0 + m_ValidKeywords: [] + m_InvalidKeywords: [] + m_LightmapFlags: 4 + m_EnableInstancingVariants: 0 + m_DoubleSidedGI: 0 + m_CustomRenderQueue: -1 + stringTagMap: {} + disabledShaderPasses: [] + m_LockedProperties: + m_SavedProperties: + serializedVersion: 3 + m_TexEnvs: + - _MainTex: + m_Texture: {fileID: 4954975621857233213} + m_Scale: {x: 1, y: 1} + m_Offset: {x: 0, y: 0} + m_Ints: [] + m_Floats: + - _FaceDilate: 0 + - _GradientScale: 10 + - _OutlineSoftness: 0 + - _OutlineWidth: 0 + - _PerspectiveFilter: 0.875 + - _ScaleRatioA: 1 + - _ScaleRatioB: 1 + - _ScaleRatioC: 1 + - _ScaleX: 1 + - _ScaleY: 1 + - _ShaderFlags: 0 + - _Sharpness: 0 + - _TextureHeight: 1024 + - _TextureWidth: 1024 + - _UnderlayDilate: 0 + - _UnderlayOffsetX: 0 + - _UnderlayOffsetY: 0 + - _UnderlaySoftness: 0 + - _VertexOffsetX: 0 + - _VertexOffsetY: 0 + - _WeightBold: 0.75 + - _WeightNormal: 0 + m_Colors: + - _FaceColor: {r: 1, g: 1, b: 1, a: 1} + - _OutlineColor: {r: 0, g: 0, b: 0, a: 1} + - _UnderlayColor: {r: 0, g: 0, b: 0, a: 0.5} + m_BuildTextureStacks: [] + m_AllowLocking: 1 +--- !u!28 &4954975621857233213 +Texture2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Teko-VariableFont_wght Atlas + m_ImageContentsHash: + serializedVersion: 2 + Hash: 00000000000000000000000000000000 + m_IsAlphaChannelOptional: 0 + serializedVersion: 3 + m_Width: 1 + m_Height: 1 + m_CompleteImageSize: 1 + m_MipsStripped: 0 + m_TextureFormat: 1 + m_MipCount: 1 + m_IsReadable: 1 + m_IsPreProcessed: 0 + m_IgnoreMipmapLimit: 1 + m_MipmapLimitGroupName: + m_StreamingMipmaps: 0 + m_StreamingMipmapsPriority: 0 + m_VTOnly: 0 + m_AlphaIsTransparency: 0 + m_ImageCount: 1 + m_TextureDimension: 2 + m_TextureSettings: + serializedVersion: 2 + m_FilterMode: 1 + m_Aniso: 1 + m_MipBias: 0 + m_WrapU: 0 + m_WrapV: 0 + m_WrapW: 0 + m_LightmapFormat: 0 + m_ColorSpace: 1 + m_PlatformBlob: + image data: 1 + _typelessdata: cd + m_StreamData: + serializedVersion: 2 + offset: 0 + size: 0 + path: diff --git a/Runtime/Engine/core_assets/ui/fonts/Teko/Teko-VariableFont_wght SDF.asset.meta b/Runtime/Engine/core_assets/ui/fonts/Teko/Teko-VariableFont_wght SDF.asset.meta new file mode 100644 index 0000000..8c5ffdf --- /dev/null +++ b/Runtime/Engine/core_assets/ui/fonts/Teko/Teko-VariableFont_wght SDF.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0332b18d9c9c6214c809861e68baf097 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/FPPKit/Code/FPPLocomotion.cs b/Runtime/FPPKit/Code/FPPLocomotion.cs index 921c230..75ad1c2 100644 --- a/Runtime/FPPKit/Code/FPPLocomotion.cs +++ b/Runtime/FPPKit/Code/FPPLocomotion.cs @@ -5,12 +5,18 @@ namespace RebootKit.FPPKit { public class FPPLocomotion : MonoBehaviour { [SerializeField] CharacterController m_CharacterController; + public float maxStrafeSpeed = 2.0f; + public float maxMovementSpeedBackward = 2.0f; public float maxMovementSpeed = 4.0f; public float maxSprintSpeed = 15.0f; public float jumpHeight = 1.0f; public float gravity = 10f; public float maxFallSpeed = 20f; public float damping = 20.0f; + + [Range(0.0f, 1.0f)] + public float airControlRatio = 0.5f; + float3 m_CurrentVelocity; bool m_IsFalling; @@ -20,8 +26,12 @@ namespace RebootKit.FPPKit { float3 m_PendingInputValue; public bool IsGrounded => m_CharacterController.isGrounded; + public bool IsMovingBackwards { get; private set; } + public bool IsSprinting => m_IsSprinting; + public bool IsStrafing { get; private set; } - public float3 Velocity => m_CurrentVelocity; + public float3 Velocity => m_CurrentVelocity; + public float3 LocalVelocity => m_CharacterController.transform.InverseTransformDirection(m_CurrentVelocity); void Update() { ConsumePendingInput(); @@ -42,12 +52,13 @@ namespace RebootKit.FPPKit { } void ConsumePendingInput() { - // if (!IsGrounded) { - // m_PendingInputValue = float3.zero; - // return; - // } + if (!IsGrounded) { + m_PendingInputValue *= airControlRatio; + } m_PendingInputValue.y = 0.0f; + + float3 localInputValue = m_CharacterController.transform.InverseTransformDirection(m_PendingInputValue); float pendingInputMagnitude = math.length(m_PendingInputValue); float3 direction = float3.zero; @@ -56,7 +67,21 @@ namespace RebootKit.FPPKit { direction = math.normalize(m_PendingInputValue); } - float movementSpeed = m_IsSprinting ? maxSprintSpeed : maxMovementSpeed; + IsStrafing = false; + IsMovingBackwards = false; + + float movementSpeed; + if (math.dot(localInputValue, Vector3.forward) <= 0.0f) { + movementSpeed = maxMovementSpeedBackward; + IsMovingBackwards = true; + } else if (m_IsSprinting) { + movementSpeed = maxSprintSpeed; + } else if (math.abs(localInputValue.x) > 0.0f && math.abs(localInputValue.z) < 0.1f) { + movementSpeed = maxStrafeSpeed; + } else { + movementSpeed = maxMovementSpeed; + } + float3 movementVelocity = m_CurrentVelocity; movementVelocity.y = 0.0f; movementVelocity += direction * (movementSpeed * pendingInputMagnitude); diff --git a/Runtime/FPPKit/Code/FPPPlayerController.cs b/Runtime/FPPKit/Code/FPPPlayerController.cs index c359cc5..78aef2a 100644 --- a/Runtime/FPPKit/Code/FPPPlayerController.cs +++ b/Runtime/FPPKit/Code/FPPPlayerController.cs @@ -3,6 +3,7 @@ using System.Threading; using Cysharp.Threading.Tasks; using RebootKit.Engine; using RebootKit.Engine.Foundation; +using RebootKit.Engine.Main; using RebootKit.Engine.Services.Console; using RebootKit.Engine.Services.Input; using Unity.Mathematics; @@ -12,7 +13,6 @@ using UnityEngine.InputSystem; using Object = UnityEngine.Object; namespace RebootKit.FPPKit { - public static class FPPConfig { [ConfigVar("fpp.mouse.sens", 0.25f, "Mouse look sensitivity")] public static ConfigVar s_MouseSensitivity; [ConfigVar("fpp.mouse.invert.y", 0, "Invert mouse look")] public static ConfigVar s_MouseInvertY; @@ -21,20 +21,28 @@ namespace RebootKit.FPPKit { public static class SensitivityReader { public static float2 Read(InputAction action) { + if (action == null || action.activeControl == null) { + return float2.zero; + } + float2 sensitivity = action.ReadValue(); - - // @TODO: differentiate between gamepad and mouse - sensitivity *= FPPConfig.s_MouseSensitivity.FloatValue; + bool isGamepad = action.activeControl.device is Gamepad; - if (FPPConfig.s_MouseInvertY.IndexValue == 1) { - sensitivity.y *= -1; + if (isGamepad) { + sensitivity *= FPPConfig.s_GamepadSensitivity.FloatValue; + } else { + sensitivity *= FPPConfig.s_MouseSensitivity.FloatValue; + + if (FPPConfig.s_MouseInvertY.IndexValue == 1) { + sensitivity.y *= -1; + } } return sensitivity; } } - + public class FPPPlayerController : IController { readonly Config m_Config; FPPActor m_FPPActor;