From 7cd881b7d472330a35db0afdd5a475aad9d0187a Mon Sep 17 00:00:00 2001 From: Misaki Date: Sat, 5 Apr 2025 16:07:53 +0900 Subject: [PATCH] Refactor project management and enhance architecture Added a new static class `AssetsPath` for asset management. Added a new icon file (`icon-256.ico`) for UI representation. Added new package references to enhance functionality. Added internals visibility attributes for better testing. Added a new `EngineEditorViewModel` class for MVVM support. Added a new `GameObject` class for component management. Added a new `BitSet` class for efficient bit manipulation. Added various utility classes to support the new entity system. Changed the `ID` property in `ProjectInfo` to internal. Changed the `AddProjectAsync` method to return the created `ProjectInfo`. Changed the connection string retrieval method to use the new `Command` constant. Changed the `DataPath` class to use `readonly` fields for folder paths. Changed the `ActivationHandler` class to use new `DataPath` constants. Changed the `OpenProjectPage` layout and interaction for better UI. Updated the target framework to a newer version for compatibility. Updated the `ProjectService` to use new constants from `DataPath`. Updated the `World` class to improve entity management. Refactored the `ProjectRepository` class to encapsulate SQL commands. Refactored the `Transform` class to use properties for better encapsulation. --- Ghost.Data/DataContext/ProjectRepository.cs | 53 +- Ghost.Data/Models/ProjectInfo.cs | 2 +- Ghost.Data/Resources/AssetsPath.cs | 8 + Ghost.Data/Resources/DataPath.cs | 4 +- Ghost.Data/Services/ProjectService.cs | 30 +- Ghost.Editor/ActivationHandler.cs | 8 +- Ghost.Editor/App.xaml.cs | 27 +- Ghost.Editor/Assets/icon-256.ico | Bin 0 -> 167295 bytes Ghost.Editor/Ghost.Editor.csproj | 9 +- .../Pages => Helpers}/HostHelpers.Page.cs | 6 +- .../View/Pages/Landing/OpenProjectPage.xaml | 5 +- .../Pages/Landing/OpenProjectPage.xaml.cs | 13 +- .../View/Windows/EngineEditorWindow.xaml | 156 ++++- .../View/Windows/EngineEditorWindow.xaml.cs | 41 +- Ghost.Editor/View/Windows/LandingWindow.xaml | 5 +- .../View/Windows/LandingWindow.xaml.cs | 7 +- .../Pages/Landing/CreateProjectViewModel.cs | 8 +- .../Windows/EngineEditorViewModel.cs | 17 + Ghost.Engine/AssemblyInfo.cs | 3 + Ghost.Engine/Component.cs | 24 + Ghost.Engine/Components/Transform.cs | 59 +- Ghost.Engine/EngineCore.cs | 17 +- Ghost.Engine/GameObject.cs | 96 +++ Ghost.Engine/Ghost.Engine.csproj | 5 - Ghost.Engine/Helpers/MatrixHelpers.cs | 20 + Ghost.Engine/Models/Component.cs | 10 - Ghost.Engine/Models/GameEntity.cs | 18 - Ghost.Engine/Models/LaunchArgument.cs | 2 +- Ghost.Engine/Models/Scene.cs | 3 + Ghost.Engine/Resources/EngineData.cs | 8 + Ghost.Engine/Services/GameLoopService.cs | 80 +++ Ghost.Entities/Archetype.cs | 5 + Ghost.Entities/AssemblyInfo.cs | 5 +- Ghost.Entities/Chunk.cs | 213 ++++++ Ghost.Entities/Component.cs | 103 +++ Ghost.Entities/Core/EntityInfo.cs | 5 - Ghost.Entities/Core/World.cs | 141 ---- Ghost.Entities/{Core => }/Entity.cs | 17 +- Ghost.Entities/Helpers/BitSet.cs | 608 ++++++++++++++++++ .../Registries/ComponentRegistry.cs | 22 + Ghost.Entities/Services/EntityChangeQueue.cs | 6 + Ghost.Entities/Signature.cs | 38 ++ Ghost.OOP/AssemblyInfo.cs | 3 + Ghost.OOP/Ghost.Game.csproj | 9 + 44 files changed, 1672 insertions(+), 247 deletions(-) create mode 100644 Ghost.Data/Resources/AssetsPath.cs create mode 100644 Ghost.Editor/Assets/icon-256.ico rename Ghost.Editor/{View/Pages => Helpers}/HostHelpers.Page.cs (76%) create mode 100644 Ghost.Editor/ViewModel/Windows/EngineEditorViewModel.cs create mode 100644 Ghost.Engine/AssemblyInfo.cs create mode 100644 Ghost.Engine/Component.cs create mode 100644 Ghost.Engine/GameObject.cs create mode 100644 Ghost.Engine/Helpers/MatrixHelpers.cs delete mode 100644 Ghost.Engine/Models/Component.cs delete mode 100644 Ghost.Engine/Models/GameEntity.cs create mode 100644 Ghost.Engine/Resources/EngineData.cs create mode 100644 Ghost.Engine/Services/GameLoopService.cs create mode 100644 Ghost.Entities/Archetype.cs create mode 100644 Ghost.Entities/Chunk.cs create mode 100644 Ghost.Entities/Component.cs delete mode 100644 Ghost.Entities/Core/EntityInfo.cs delete mode 100644 Ghost.Entities/Core/World.cs rename Ghost.Entities/{Core => }/Entity.cs (78%) create mode 100644 Ghost.Entities/Helpers/BitSet.cs create mode 100644 Ghost.Entities/Registries/ComponentRegistry.cs create mode 100644 Ghost.Entities/Services/EntityChangeQueue.cs create mode 100644 Ghost.Entities/Signature.cs create mode 100644 Ghost.OOP/AssemblyInfo.cs create mode 100644 Ghost.OOP/Ghost.Game.csproj diff --git a/Ghost.Data/DataContext/ProjectRepository.cs b/Ghost.Data/DataContext/ProjectRepository.cs index e444602..9d4d1da 100644 --- a/Ghost.Data/DataContext/ProjectRepository.cs +++ b/Ghost.Data/DataContext/ProjectRepository.cs @@ -6,17 +6,22 @@ namespace Ghost.Data.DataContext; internal static class ProjectRepository { - private const string _CONNECTION_STRING = "Data Source={0}\\projects.db;Version=3;"; - private const string _CREATE_PROJECT_TABLE_STRING = "CREATE TABLE IF NOT EXISTS Projects (ID INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT, Path TEXT, EngineVersion TEXT, LastOpened DATETIME);"; - private const string _SELECT_PROJECT_STRING = "SELECT * FROM Projects"; - private const string _INSERT_PROJECT_STRING = "INSERT INTO Projects (Name, Path, EngineVersion, LastOpened) VALUES (@Name, @Path, @EngineVersion, @LastOpened);"; + private static class Command + { + public const string CONNECTION_STRING = "Data Source={0}\\projects.db;Version=3;"; + public const string CREATE_PROJECT_TABLE_STRING = "CREATE TABLE IF NOT EXISTS Projects (ID INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT, Path TEXT, EngineVersion TEXT, LastOpened DATETIME);"; + public const string SELECT_PROJECT_STRING = "SELECT * FROM Projects"; + public const string INSERT_PROJECT_STRING = "INSERT INTO Projects (Name, Path, EngineVersion, LastOpened) VALUES (@Name, @Path, @EngineVersion, @LastOpened);"; + public const string REMOVE_PROJECT_STRING = "DELETE FROM Projects WHERE ID = @ID;"; + public const string UPDATE_PROJECT_STRING = "UPDATE Projects SET Name = @Name, Path = @Path, EngineVersion = @EngineVersion, LastOpened = @LastOpened WHERE ID = @ID;"; + } - private static string GetConnectionString() => string.Format(_CONNECTION_STRING, DataPath.ApplicationDataFolder); + private static string GetConnectionString() => string.Format(Command.CONNECTION_STRING, DataPath.APPLICATION_DATA_FOLDER); private static async Task EnsureTableCreatedAsync(SQLiteConnection connection) { using var createCommand = connection.CreateCommand(); - createCommand.CommandText = _CREATE_PROJECT_TABLE_STRING; + createCommand.CommandText = Command.CREATE_PROJECT_TABLE_STRING; await createCommand.ExecuteNonQueryAsync(); } @@ -28,13 +33,14 @@ internal static class ProjectRepository await EnsureTableCreatedAsync(connection); using var command = connection.CreateCommand(); - command.CommandText = _SELECT_PROJECT_STRING; + command.CommandText = Command.SELECT_PROJECT_STRING; using var reader = command.ExecuteReader(); while (await reader.ReadAsync()) { var project = new ProjectInfo { + ID = reader.GetInt32(0), Name = reader.GetString(1), Path = reader.GetString(2), EngineVersion = new Version(reader.GetString(3)), @@ -53,12 +59,43 @@ internal static class ProjectRepository await EnsureTableCreatedAsync(connection); using var command = connection.CreateCommand(); - command.CommandText = _INSERT_PROJECT_STRING; + command.CommandText = Command.INSERT_PROJECT_STRING; command.Parameters.AddWithValue("@Name", project.Name); command.Parameters.AddWithValue("@Path", project.Path); command.Parameters.AddWithValue("@EngineVersion", project.EngineVersion.ToString()); command.Parameters.AddWithValue("@LastOpened", project.LastOpened); + + await command.ExecuteNonQueryAsync(); + } + + public static async Task RemoveProjectAsync(ProjectInfo project) + { + using var connection = new SQLiteConnection(GetConnectionString()); + await connection.OpenAsync(); + + using var command = connection.CreateCommand(); + command.CommandText = Command.REMOVE_PROJECT_STRING; + + command.Parameters.AddWithValue("@ID", project.ID); + + await command.ExecuteNonQueryAsync(); + } + + public static async Task UpdateProjectAsync(ProjectInfo project) + { + using var connection = new SQLiteConnection(GetConnectionString()); + await connection.OpenAsync(); + + using var command = connection.CreateCommand(); + command.CommandText = Command.UPDATE_PROJECT_STRING; + + command.Parameters.AddWithValue("@Name", project.Name); + command.Parameters.AddWithValue("@Path", project.Path); + command.Parameters.AddWithValue("@EngineVersion", project.EngineVersion.ToString()); + command.Parameters.AddWithValue("@LastOpened", project.LastOpened); + command.Parameters.AddWithValue("@ID", project.ID); // Ensure the ID parameter is added + await command.ExecuteNonQueryAsync(); } } \ No newline at end of file diff --git a/Ghost.Data/Models/ProjectInfo.cs b/Ghost.Data/Models/ProjectInfo.cs index 4eb6635..b44ca12 100644 --- a/Ghost.Data/Models/ProjectInfo.cs +++ b/Ghost.Data/Models/ProjectInfo.cs @@ -7,7 +7,7 @@ public class ProjectInfo [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int ID { - get; set; + get; internal set; } public required string Name diff --git a/Ghost.Data/Resources/AssetsPath.cs b/Ghost.Data/Resources/AssetsPath.cs new file mode 100644 index 0000000..e800f20 --- /dev/null +++ b/Ghost.Data/Resources/AssetsPath.cs @@ -0,0 +1,8 @@ +namespace Ghost.Data.Resources; + +public static class AssetsPath +{ + public const string ASSETS_FOLDER = "Assets"; + + public readonly static string AppIconPath = Path.Combine(AppContext.BaseDirectory, $"{ASSETS_FOLDER}/Icon-256.ico"); +} \ No newline at end of file diff --git a/Ghost.Data/Resources/DataPath.cs b/Ghost.Data/Resources/DataPath.cs index 992e6a2..486a2bd 100644 --- a/Ghost.Data/Resources/DataPath.cs +++ b/Ghost.Data/Resources/DataPath.cs @@ -4,6 +4,6 @@ public class DataPath { public const string ENGINE_DATA_FOLDER_NAME = "GhostEngine"; - public static string ApplicationDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ENGINE_DATA_FOLDER_NAME); - public static string ProjectTemplatesFolder = Path.Combine(ApplicationDataFolder, "ProjectTemplates"); + public readonly static string APPLICATION_DATA_FOLDER = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ENGINE_DATA_FOLDER_NAME); + public readonly static string PROJECT_TEMPLATES_FOLDER = Path.Combine(APPLICATION_DATA_FOLDER, "ProjectTemplates"); } \ No newline at end of file diff --git a/Ghost.Data/Services/ProjectService.cs b/Ghost.Data/Services/ProjectService.cs index 2a7c514..42b1633 100644 --- a/Ghost.Data/Services/ProjectService.cs +++ b/Ghost.Data/Services/ProjectService.cs @@ -8,19 +8,18 @@ namespace Ghost.Data.Services; public class ProjectService { - private const string _TEMPLATE_CONTENT_FILE = "content.zip"; - private const string _ASSETS_FOLDER = "Assets"; + private const string _TEMPLATE_CONTENT_FILE = "content.zip"; public async IAsyncEnumerable<(string path, TemplateInfo info)> GetProjectTemplatesAsync() { - var templatesFolder = DataPath.ProjectTemplatesFolder; + var templatesFolder = DataPath.PROJECT_TEMPLATES_FOLDER; if (!Directory.Exists(templatesFolder)) { yield break; } - var templates = Directory.GetFiles(DataPath.ProjectTemplatesFolder, "template.json", SearchOption.AllDirectories); + var templates = Directory.GetFiles(DataPath.PROJECT_TEMPLATES_FOLDER, "template.json", SearchOption.AllDirectories); foreach (var templatePath in templates) { var fileStream = File.OpenRead(templatePath); @@ -52,6 +51,11 @@ public class ProjectService }); } + public IAsyncEnumerable LoadAllProjectAsync() + { + return ProjectRepository.LoadProjectsAsync(); + } + public async Task CreateProjectAsync(string projectName, string projectDirectory, string templatePath) { var projectPath = Path.Combine(projectDirectory, projectName); @@ -70,19 +74,27 @@ public class ProjectService return ProjectRepository.AddProjectAsync(project); } - public Task AddProjectAsync(string name, string path, Version version) + public async Task AddProjectAsync(string name, string path, Version version) { - return ProjectRepository.AddProjectAsync(new ProjectInfo + var project = new ProjectInfo { Name = name, Path = path, EngineVersion = version, LastOpened = DateTime.Now - }); + }; + await ProjectRepository.AddProjectAsync(project); + + return project; } - public IAsyncEnumerable LoadProjectAsync() + public Task RemoveProjectAsync(ProjectInfo project) { - return ProjectRepository.LoadProjectsAsync(); + return ProjectRepository.RemoveProjectAsync(project); + } + + public Task UpdateProjectAsync(ProjectInfo project) + { + return ProjectRepository.UpdateProjectAsync(project); } } \ No newline at end of file diff --git a/Ghost.Editor/ActivationHandler.cs b/Ghost.Editor/ActivationHandler.cs index 13692a3..b920373 100644 --- a/Ghost.Editor/ActivationHandler.cs +++ b/Ghost.Editor/ActivationHandler.cs @@ -8,14 +8,14 @@ internal static class ActivationHandler { private static void FolderInitialization() { - if (!Directory.Exists(DataPath.ApplicationDataFolder)) + if (!Directory.Exists(DataPath.APPLICATION_DATA_FOLDER)) { - Directory.CreateDirectory(DataPath.ApplicationDataFolder); + Directory.CreateDirectory(DataPath.APPLICATION_DATA_FOLDER); } - if (!Directory.Exists(DataPath.ProjectTemplatesFolder)) + if (!Directory.Exists(DataPath.PROJECT_TEMPLATES_FOLDER)) { - Directory.CreateDirectory(DataPath.ProjectTemplatesFolder); + Directory.CreateDirectory(DataPath.PROJECT_TEMPLATES_FOLDER); } } diff --git a/Ghost.Editor/App.xaml.cs b/Ghost.Editor/App.xaml.cs index aaa545b..6c28d67 100644 --- a/Ghost.Editor/App.xaml.cs +++ b/Ghost.Editor/App.xaml.cs @@ -1,5 +1,5 @@ using Ghost.Data.Services; -using Ghost.Editor.View.Pages; +using Ghost.Editor.Helpers; using Ghost.Editor.View.Windows; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -17,13 +17,8 @@ namespace Ghost.Editor public partial class App : Application { private Window? _window; - public Window? CurrentWindow - { - get => _window; - set => _window = value; - } - public IHost Host + internal IHost Host { get; } @@ -32,7 +27,7 @@ namespace Ghost.Editor /// Initializes the singleton application object. This is the first line of authored code /// executed, and as such is the logical equivalent of main() or WinMain(). /// - public App() + internal App() { InitializeComponent(); @@ -41,19 +36,27 @@ namespace Ghost.Editor UseContentRoot(AppContext.BaseDirectory). ConfigureServices((context, services) => { - services.AddTransient(); + services.AddSingleton(); HostHelper.SetupPageService(context, services); }) .Build(); } - public static Window? GetWindow() + internal static Window? GetWindow() { - return (Current as App)?.CurrentWindow; + return (Current as App)?._window; } - public static T GetService() where T : class + internal static void SetWindow(Window window) + { + if (Current is App app) + { + app._window = window; + } + } + + internal static T GetService() where T : class { if ((Current as App)!.Host.Services.GetService(typeof(T)) is not T service) { diff --git a/Ghost.Editor/Assets/icon-256.ico b/Ghost.Editor/Assets/icon-256.ico new file mode 100644 index 0000000000000000000000000000000000000000..40c9344f4617c3af34ed75ee503dd3944f39a4f5 GIT binary patch literal 167295 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 WinExe - net9.0-windows10.0.20348.0 + net9.0-windows10.0.22621.0 10.0.17763.0 Ghost.Editor x86;x64;ARM64 @@ -69,10 +69,13 @@ + + + @@ -95,10 +98,10 @@ + - @@ -132,7 +135,7 @@ True True enable - 10.0.19041.0 + 10.0.20348.0 app.manifest True True diff --git a/Ghost.Editor/View/Pages/HostHelpers.Page.cs b/Ghost.Editor/Helpers/HostHelpers.Page.cs similarity index 76% rename from Ghost.Editor/View/Pages/HostHelpers.Page.cs rename to Ghost.Editor/Helpers/HostHelpers.Page.cs index a9d79d0..423d0c2 100644 --- a/Ghost.Editor/View/Pages/HostHelpers.Page.cs +++ b/Ghost.Editor/Helpers/HostHelpers.Page.cs @@ -1,16 +1,17 @@ using Ghost.Editor.View.Pages.Landing; using Ghost.Editor.View.Windows; using Ghost.Editor.ViewModel.Pages.Landing; +using Ghost.Editor.ViewModel.Windows; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace Ghost.Editor.View.Pages; +namespace Ghost.Editor.Helpers; internal static partial class HostHelper { public static void SetupPageService(HostBuilderContext context, IServiceCollection services) { - services.AddTransient(); + services.AddSingleton(); services.AddTransient(); services.AddTransient(); @@ -18,5 +19,6 @@ internal static partial class HostHelper services.AddTransient(); services.AddSingleton(); + services.AddSingleton(); } } \ No newline at end of file diff --git a/Ghost.Editor/View/Pages/Landing/OpenProjectPage.xaml b/Ghost.Editor/View/Pages/Landing/OpenProjectPage.xaml index 86d41a1..f893957 100644 --- a/Ghost.Editor/View/Pages/Landing/OpenProjectPage.xaml +++ b/Ghost.Editor/View/Pages/Landing/OpenProjectPage.xaml @@ -15,7 +15,7 @@ - + @@ -40,12 +40,13 @@ diff --git a/Ghost.Editor/View/Pages/Landing/OpenProjectPage.xaml.cs b/Ghost.Editor/View/Pages/Landing/OpenProjectPage.xaml.cs index 9fad562..69664fc 100644 --- a/Ghost.Editor/View/Pages/Landing/OpenProjectPage.xaml.cs +++ b/Ghost.Editor/View/Pages/Landing/OpenProjectPage.xaml.cs @@ -1,5 +1,6 @@ using Ghost.Data.Models; using Ghost.Data.Services; +using Ghost.Editor.View.Windows; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Navigation; @@ -25,7 +26,7 @@ internal sealed partial class OpenProjectPage : Page protected override async void OnNavigatedTo(NavigationEventArgs e) { - await foreach (var project in _projectService.LoadProjectAsync()) + await foreach (var project in _projectService.LoadAllProjectAsync()) { projects.Add(project); } @@ -37,13 +38,19 @@ internal sealed partial class OpenProjectPage : Page } } - private void ListView_ItemClick(object sender, ItemClickEventArgs e) + private async void ListView_ItemClick(object sender, ItemClickEventArgs e) { if (e.ClickedItem is not ProjectInfo project) { return; } - //TODO: Load project + if (EngineEditorWindow.TryLoadProject(project)) + { + App.GetService().Close(); + + project.LastOpened = System.DateTime.Now; + await _projectService.UpdateProjectAsync(project); + } } } \ No newline at end of file diff --git a/Ghost.Editor/View/Windows/EngineEditorWindow.xaml b/Ghost.Editor/View/Windows/EngineEditorWindow.xaml index 1c7500b..86e6ab6 100644 --- a/Ghost.Editor/View/Windows/EngineEditorWindow.xaml +++ b/Ghost.Editor/View/Windows/EngineEditorWindow.xaml @@ -3,12 +3,164 @@ x:Class="Ghost.Editor.View.Windows.EngineEditorWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:controls="using:CommunityToolkit.WinUI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:Ghost.Editor.View.Windows" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:winex="using:WinUIEx" - Title="EngineEditorWindow" mc:Ignorable="d"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs b/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs index af43234..90f60f3 100644 --- a/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs +++ b/Ghost.Editor/View/Windows/EngineEditorWindow.xaml.cs @@ -1,4 +1,8 @@ -using WinUIEx; +using Ghost.Data.Models; +using Ghost.Data.Resources; +using Ghost.Editor.ViewModel.Windows; +using Ghost.Engine.Resources; +using WinUIEx; // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. @@ -7,10 +11,43 @@ namespace Ghost.Editor.View.Windows; /// /// An empty window that can be used on its own or navigated to within a Frame. /// -public sealed partial class EngineEditorWindow : WindowEx +internal sealed partial class EngineEditorWindow : WindowEx { + public EngineEditorViewModel ViewModel + { + get; + } + public EngineEditorWindow() { + ViewModel = App.GetService(); + + AppWindow.SetIcon(AssetsPath.AppIconPath); + Title = EngineData.ENGINE_NAME; + ExtendsContentIntoTitleBar = true; + InitializeComponent(); + + this.CenterOnScreen(); + } + + public static bool TryLoadProject(ProjectInfo project) + { + try + { + var window = App.GetService(); + window.ViewModel.CurrentProject = project; + + window.Activate(); + window.Bindings.Update(); + + App.SetWindow(window); + + return true; + } + catch (System.Exception) + { + return false; + } } } \ No newline at end of file diff --git a/Ghost.Editor/View/Windows/LandingWindow.xaml b/Ghost.Editor/View/Windows/LandingWindow.xaml index 72e58b9..46ee4f6 100644 --- a/Ghost.Editor/View/Windows/LandingWindow.xaml +++ b/Ghost.Editor/View/Windows/LandingWindow.xaml @@ -7,7 +7,6 @@ xmlns:local="using:Ghost.Editor.View.Windows" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:winex="using:WinUIEx" - Title="Landing" IsResizable="False" mc:Ignorable="d"> @@ -15,7 +14,7 @@ - + @@ -55,6 +54,8 @@ x:Name="ContentFrame" Grid.Row="1" Padding="8" + CacheMode="BitmapCache" + CacheSize="10" IsNavigationStackEnabled="False" /> diff --git a/Ghost.Editor/View/Windows/LandingWindow.xaml.cs b/Ghost.Editor/View/Windows/LandingWindow.xaml.cs index 9add811..66e4446 100644 --- a/Ghost.Editor/View/Windows/LandingWindow.xaml.cs +++ b/Ghost.Editor/View/Windows/LandingWindow.xaml.cs @@ -1,4 +1,6 @@ -using Ghost.Editor.View.Pages.Landing; +using Ghost.Data.Resources; +using Ghost.Editor.View.Pages.Landing; +using Ghost.Engine.Resources; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media.Animation; using WinUIEx; @@ -11,6 +13,9 @@ internal sealed partial class LandingWindow : WindowEx public LandingWindow() { + AppWindow.SetIcon(AssetsPath.AppIconPath); + Title = EngineData.ENGINE_NAME; + InitializeComponent(); this.SetWindowSize(1000, 750); diff --git a/Ghost.Editor/ViewModel/Pages/Landing/CreateProjectViewModel.cs b/Ghost.Editor/ViewModel/Pages/Landing/CreateProjectViewModel.cs index 92a61d9..b690bf1 100644 --- a/Ghost.Editor/ViewModel/Pages/Landing/CreateProjectViewModel.cs +++ b/Ghost.Editor/ViewModel/Pages/Landing/CreateProjectViewModel.cs @@ -4,6 +4,7 @@ using Ghost.Data.Models; using Ghost.Data.Services; using Ghost.Editor.Contracts; using Ghost.Editor.Helpers; +using Ghost.Editor.View.Windows; using System.Collections.ObjectModel; using System.IO; using System.Linq; @@ -68,6 +69,11 @@ internal partial class CreateProjectViewModel(ProjectService projectService) : O var projectPath = await projectService.CreateProjectAsync(ProjectName, ProjectLocation, SelectedTemplate.directory); var packageVersion = Package.Current.Id.Version; - await projectService.AddProjectAsync(ProjectName, projectPath, new System.Version(packageVersion.Major, packageVersion.Minor, packageVersion.Build)); + var newProject = await projectService.AddProjectAsync(ProjectName, projectPath, new System.Version(packageVersion.Major, packageVersion.Minor, packageVersion.Build)); + + if (EngineEditorWindow.TryLoadProject(newProject)) + { + App.GetService().Close(); + } } } \ No newline at end of file diff --git a/Ghost.Editor/ViewModel/Windows/EngineEditorViewModel.cs b/Ghost.Editor/ViewModel/Windows/EngineEditorViewModel.cs new file mode 100644 index 0000000..6d278c7 --- /dev/null +++ b/Ghost.Editor/ViewModel/Windows/EngineEditorViewModel.cs @@ -0,0 +1,17 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Ghost.Data.Models; +using Ghost.Engine.Resources; + +namespace Ghost.Editor.ViewModel.Windows; + +internal partial class EngineEditorViewModel : ObservableRecipient +{ + public string engineVersionDescriptor = $"{EngineData.ENGINE_NAME} - {EngineData.ENGINE_VERSION}"; + + [ObservableProperty] + public partial ProjectInfo CurrentProject + { + get; + set; + } +} \ No newline at end of file diff --git a/Ghost.Engine/AssemblyInfo.cs b/Ghost.Engine/AssemblyInfo.cs new file mode 100644 index 0000000..9092fb4 --- /dev/null +++ b/Ghost.Engine/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Ghost.Editor")] diff --git a/Ghost.Engine/Component.cs b/Ghost.Engine/Component.cs new file mode 100644 index 0000000..344f180 --- /dev/null +++ b/Ghost.Engine/Component.cs @@ -0,0 +1,24 @@ +namespace Ghost.Engine; + +public abstract class Component +{ + public virtual void Start() + { + } + + public virtual void Update() + { + } + + public virtual void LateUpdate() + { + } + + public virtual void FixedUpdate() + { + } + + public virtual void OnDestroy() + { + } +} \ No newline at end of file diff --git a/Ghost.Engine/Components/Transform.cs b/Ghost.Engine/Components/Transform.cs index c7cf623..07be4af 100644 --- a/Ghost.Engine/Components/Transform.cs +++ b/Ghost.Engine/Components/Transform.cs @@ -1,11 +1,62 @@ -using Ghost.Engine.Models; +using Ghost.Engine.Helpers; using System.Numerics; namespace Ghost.Engine.Components; public class Transform : Component { - public Vector3 position = Vector3.Zero; - public Quaternion rotation = Quaternion.Identity; - public Vector3 scale = Vector3.One; + private Vector3 _position = Vector3.Zero; + public Vector3 position + { + get => _position; + set + { + _position = value; + hasChanged = true; + UpdateMatrices(); + } + } + + private Quaternion _rotation = Quaternion.Identity; + public Quaternion Rotation + { + get => _rotation; + set + { + _rotation = value; + hasChanged = true; + UpdateMatrices(); + } + } + + private Vector3 _scale = Vector3.One; + public Vector3 Scale + { + get => _scale; + set + { + _scale = value; + hasChanged = true; + UpdateMatrices(); + } + } + + public bool hasChanged = true; + + private Matrix4x4 _localToWorldMatrix = Matrix4x4.Identity; + private Matrix4x4 _worldToLocalMatrix = Matrix4x4.Identity; + + public Matrix4x4 LocalToWorldMatrix => _localToWorldMatrix; + public Matrix4x4 WorldToLocalMatrix => _worldToLocalMatrix; + + private void UpdateMatrices() + { + _localToWorldMatrix = MatrixHelpers.CreateTRS(_position, _rotation, _scale); + Matrix4x4.Invert(_localToWorldMatrix, out _worldToLocalMatrix); + } + + public override void Start() + { + UpdateMatrices(); + } } \ No newline at end of file diff --git a/Ghost.Engine/EngineCore.cs b/Ghost.Engine/EngineCore.cs index 6ad7a8d..3970f28 100644 --- a/Ghost.Engine/EngineCore.cs +++ b/Ghost.Engine/EngineCore.cs @@ -4,9 +4,22 @@ namespace Ghost.Engine; internal class EngineCore { - public async Task StartAsync() + public static EngineCore? Current { - ActivationHandler.Handle(new LaunchArgument()); + get; + private set; + } + + public static async Task StartAsync(LaunchArgument args) + { + if (Current != null) + { + return; + } + + Current = new EngineCore(); + + ActivationHandler.Handle(args); await Task.CompletedTask; } diff --git a/Ghost.Engine/GameObject.cs b/Ghost.Engine/GameObject.cs new file mode 100644 index 0000000..33ca292 --- /dev/null +++ b/Ghost.Engine/GameObject.cs @@ -0,0 +1,96 @@ +using Ghost.Engine.Components; +using Ghost.Engine.Services; +using System.Collections.ObjectModel; + +namespace Ghost.Engine; + +public class GameObject +{ + private readonly ObservableCollection _components = new(); + + public string name = string.Empty; + public bool isActive = true; + + public Transform Transform { get; } = new(); + + private GameObject() + { + AddComponent(Transform); + } + + public static GameObject Create(string name = "") + { + var gameObject = new GameObject + { + name = name + }; + + GameLoopService.RegisterGameObject(gameObject); + return gameObject; + } + + public void AddComponent(Component component) + { + _components.Add(component); + } + + public void RemoveComponent(Component component) + { + _components.Remove(component); + } + + public T? GetComponent() where T : Component + { + foreach (var component in _components) + { + if (component is T t) + { + return t; + } + } + + return null; + } + + internal void Start() + { + foreach (var component in _components) + { + component.Start(); + } + } + + internal void Update() + { + foreach (var component in _components) + { + component.Update(); + } + } + + internal void LateUpdate() + { + foreach (var component in _components) + { + component.LateUpdate(); + } + } + + internal void FixedUpdate() + { + foreach (var component in _components) + { + component.FixedUpdate(); + } + } + + public void Destroy() + { + foreach (var component in _components) + { + component.OnDestroy(); + } + + GameLoopService.UnregisterGameObject(this); + } +} \ No newline at end of file diff --git a/Ghost.Engine/Ghost.Engine.csproj b/Ghost.Engine/Ghost.Engine.csproj index 4f84ab1..2820cf2 100644 --- a/Ghost.Engine/Ghost.Engine.csproj +++ b/Ghost.Engine/Ghost.Engine.csproj @@ -14,9 +14,4 @@ True - - - - - diff --git a/Ghost.Engine/Helpers/MatrixHelpers.cs b/Ghost.Engine/Helpers/MatrixHelpers.cs new file mode 100644 index 0000000..49770d0 --- /dev/null +++ b/Ghost.Engine/Helpers/MatrixHelpers.cs @@ -0,0 +1,20 @@ +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Ghost.Engine.Helpers; + +public static class MatrixHelpers +{ + /// + /// Generates a transformation matrix from position, rotation, and scale vectors. + /// + /// Defines the translation component of the transformation matrix. + /// Specifies the orientation of the object in 3D space. + /// Determines the size of the object along each axis. + /// Returns a transformation matrix that combines the specified position, rotation, and scale. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix4x4 CreateTRS(Vector3 position, Quaternion rotation, Vector3 scale) + { + return Matrix4x4.CreateScale(scale) * Matrix4x4.CreateFromQuaternion(rotation) * Matrix4x4.CreateTranslation(position); + } +} \ No newline at end of file diff --git a/Ghost.Engine/Models/Component.cs b/Ghost.Engine/Models/Component.cs deleted file mode 100644 index af4cdd3..0000000 --- a/Ghost.Engine/Models/Component.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Ghost.Engine.Models; - -public abstract class Component -{ - public required GameEntity Owner - { - get; - set; - } -} \ No newline at end of file diff --git a/Ghost.Engine/Models/GameEntity.cs b/Ghost.Engine/Models/GameEntity.cs deleted file mode 100644 index d06587d..0000000 --- a/Ghost.Engine/Models/GameEntity.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.ObjectModel; - -namespace Ghost.Engine.Models; - -public abstract class GameEntity -{ - private ObservableCollection _components = new(); - - public GameEntity() - { - //AddComponent(new Transform()); - } - - public void AddComponent(Component component) - { - _components.Add(component); - } -} \ No newline at end of file diff --git a/Ghost.Engine/Models/LaunchArgument.cs b/Ghost.Engine/Models/LaunchArgument.cs index fe81901..ed92446 100644 --- a/Ghost.Engine/Models/LaunchArgument.cs +++ b/Ghost.Engine/Models/LaunchArgument.cs @@ -2,4 +2,4 @@ internal class LaunchArgument { -} +} \ No newline at end of file diff --git a/Ghost.Engine/Models/Scene.cs b/Ghost.Engine/Models/Scene.cs index 561d371..5bf47d7 100644 --- a/Ghost.Engine/Models/Scene.cs +++ b/Ghost.Engine/Models/Scene.cs @@ -2,4 +2,7 @@ public class Scene { + internal Scene() + { + } } \ No newline at end of file diff --git a/Ghost.Engine/Resources/EngineData.cs b/Ghost.Engine/Resources/EngineData.cs new file mode 100644 index 0000000..2f1bece --- /dev/null +++ b/Ghost.Engine/Resources/EngineData.cs @@ -0,0 +1,8 @@ +namespace Ghost.Engine.Resources; + +internal class EngineData +{ + public const string ENGINE_NAME = "Ghost Engine"; + + public readonly static Version ENGINE_VERSION = new(0, 1, 0); +} \ No newline at end of file diff --git a/Ghost.Engine/Services/GameLoopService.cs b/Ghost.Engine/Services/GameLoopService.cs new file mode 100644 index 0000000..7eb1f8f --- /dev/null +++ b/Ghost.Engine/Services/GameLoopService.cs @@ -0,0 +1,80 @@ + +namespace Ghost.Engine.Services; + +internal static class GameLoopService +{ + private readonly static HashSet _gameObjects = new(); + + private static Timer? _timer; + private static bool _isRunning = false; + + // TODO: Implement the actual time system + public static float fixedDeltaTime = 0.02f; + + public static void RegisterGameObject(GameObject gameObject) + { + _gameObjects.Add(gameObject); + } + + public static void UnregisterGameObject(GameObject gameObject) + { + _gameObjects.Remove(gameObject); + } + + public static void Start() + { + if (_isRunning) + { + return; + } + + foreach (var gameObject in _gameObjects) + { + if (!gameObject.isActive) + { + continue; + } + + gameObject.Start(); + } + + _timer ??= new Timer(FixedUpdate, null, 0, (int)(fixedDeltaTime * 1000)); + + while (_isRunning) + { + Update(); + } + } + + private static void Update() + { + foreach (var gameObject in _gameObjects) + { + if (!gameObject.isActive) + { + continue; + } + + gameObject.Update(); + gameObject.LateUpdate(); + } + } + + private static void FixedUpdate(object? state) + { + foreach (var gameObject in _gameObjects) + { + if (!gameObject.isActive) + { + continue; + } + + gameObject.FixedUpdate(); + } + } + + public static void Stop() + { + _isRunning = false; + } +} \ No newline at end of file diff --git a/Ghost.Entities/Archetype.cs b/Ghost.Entities/Archetype.cs new file mode 100644 index 0000000..9447a39 --- /dev/null +++ b/Ghost.Entities/Archetype.cs @@ -0,0 +1,5 @@ +namespace Ghost.Entities; + +public struct Archetype +{ +} \ No newline at end of file diff --git a/Ghost.Entities/AssemblyInfo.cs b/Ghost.Entities/AssemblyInfo.cs index 0eb0bf2..744263a 100644 --- a/Ghost.Entities/AssemblyInfo.cs +++ b/Ghost.Entities/AssemblyInfo.cs @@ -1,4 +1,7 @@ global using EntityID = System.UInt32; - global using GenerationID = System.UInt16; global using WorldID = System.UInt16; + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Ghost.Engine")] \ No newline at end of file diff --git a/Ghost.Entities/Chunk.cs b/Ghost.Entities/Chunk.cs new file mode 100644 index 0000000..a20d3c7 --- /dev/null +++ b/Ghost.Entities/Chunk.cs @@ -0,0 +1,213 @@ +using Misaki.HighPerformance.Unsafe.Collections; +using Misaki.HighPerformance.Unsafe.Helpers; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Ghost.Entities; + +internal struct Chunks : IDisposable +{ + private UnsafeArray _chunks; + private int _count; + private int _capacity; + + public readonly int Count => _count; + public readonly int Capacity => _capacity; + + public ref Chunk this[int index] => ref _chunks[index]; + + public Chunks(int capacity) + { + _chunks = new(capacity, Allocator.Persistent); + _count = 0; + _capacity = capacity; + } + + public void Add(Chunk chunk) + { + _chunks[_count] = chunk; + _count++; + } + + public void EnsureCapacity(int newCapacity) + { + if (newCapacity <= _capacity) + { + return; + } + + _chunks.Resize(newCapacity); + } + + public void TrimExcess() + { + if (_count == _capacity) + { + return; + } + + _chunks.Resize(_count); + } + + public void Clear() + { + for (var i = 0; i < _count; i++) + { + _chunks[i].Clear(); + } + + _count = 0; + _capacity = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Span AsSpan() + { + return _chunks.AsSpan(); + } + + public void Dispose() + { + for (var i = 0; i < _count; i++) + { + _chunks[i].Dispose(); + } + + _chunks.Dispose(); + _count = 0; + _capacity = 0; + } +} + +internal struct Chunk : IDisposable +{ + public UnsafeArray entities; + public UnsafeArray> components; + + // The component lookup array is used to quickly find the index of a component in the components array. + // Mapping component ID to component index in the components array. + private UnsafeArray _componentLookup; + + private int _count; + private readonly int _capacity; + private bool _isDisposed; + + public readonly int Count => _count; + public readonly int Capacity => _capacity; + + public Chunk(int capacity, Span data) : this(capacity, data, Component.ToLookupArray(data, Allocator.Persistent)) + { + } + + public Chunk(int capacity, Span data, UnsafeArray lookup) + { + _count = 0; + _capacity = capacity; + + entities = new(capacity, Allocator.Persistent); + components = new(data.Length, Allocator.Persistent); + + _componentLookup = lookup; + + for (var i = 0; i < data.Length; i++) + { + var component = data[i]; + components[component.id] = new UnsafeArray(capacity * component.sizeInByte, Allocator.Persistent); + } + } + + public int Add(Entity entity) + { + var index = _count; + entities[index] = entity; + _count++; + + return index; + } + + public unsafe bool Remove(int index) + { + if (index < 0 || index >= _count) + { + return false; + } + + var lastIndex = _count--; + entities[index] = entities[lastIndex]; + + for (var i = 0; i < components.Count; i++) + { + var componentArray = UnsafeUtilities.ReadArrayElementUnsafe>(components.GetUnsafePtr(), i); + var componentSize = componentArray->Count / _capacity; + var removedComponent = UnsafeUtilities.ReadArrayElementUnsafe(componentArray->GetUnsafePtr(), index * componentSize); + var lastComponent = UnsafeUtilities.ReadArrayElementUnsafe(componentArray->GetUnsafePtr(), lastIndex * componentSize); + MemoryUtilities.MemCpy(removedComponent, lastComponent, (nuint)componentSize); + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly int IndexOf() + where T : unmanaged, IComponent + { + var id = Component.data.id; + Debug.Assert(id != -1 && id < _componentLookup.Count, $"Index is out of bounds, component {typeof(T)} with id {id} does not exist in this chunk."); + return _componentLookup[id]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Has() + where T : unmanaged, IComponent + { + var id = Component.data.id; + return id < _componentLookup.Count && _componentLookup[id] != -1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly unsafe UnsafeArray GetArrayOf() + where T : unmanaged, IComponent + { + var index = IndexOf(); + var componentArray = components[index]; + return UnsafeUtilities.CastArray(componentArray); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly unsafe ref T GetComponent(int index) + where T : unmanaged, IComponent + { + var componentArray = GetArrayOf(); + return ref componentArray[index]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Entity GetEntity(int index) + { + return entities[index]; + } + + public void Clear() + { + _count = 0; + } + + public void Dispose() + { + if (_isDisposed) + { + return; + } + + entities.Dispose(); + _componentLookup.Dispose(); + + for (var i = 0; i < components.Count; i++) + { + components[i].Dispose(); + } + components.Dispose(); + + _isDisposed = true; + } +} \ No newline at end of file diff --git a/Ghost.Entities/Component.cs b/Ghost.Entities/Component.cs new file mode 100644 index 0000000..b3e504a --- /dev/null +++ b/Ghost.Entities/Component.cs @@ -0,0 +1,103 @@ +using Ghost.Entities.Helpers; +using Ghost.Entities.Registries; +using Misaki.HighPerformance.Unsafe.Collections; +using Misaki.HighPerformance.Unsafe.Helpers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ghost.Entities; + +public interface IComponent +{ + +} + +[SkipLocalsInit] +internal readonly record struct ComponentData +{ + public readonly int id; + public readonly int sizeInByte; + + public ComponentData(int id, int sizeInByte) + { + this.id = id; + this.sizeInByte = sizeInByte; + } +} + +internal static class Component +{ + public static unsafe UnsafeArray ToLookupArray(Span datas, Allocator allocator) + { + var max = 0; + foreach (var data in datas) + { + var componentId = data.id; + if (componentId >= max) + { + max = componentId; + } + } + + // Create lookup table where the component ID points to the component index. + var array = new UnsafeArray(max + 1, allocator); + array.AsSpan().Fill(-1); + + for (var index = 0; index < datas.Length; index++) + { + ref var type = ref datas[index]; + var componentId = type.id; + array[componentId] = index; + } + + return array; + } + + public static int GetHashCode(Span components) + { + // Search for the highest id to determine how much uints we need for the stack. + var highestId = 0; + foreach (ref var cmp in components) + { + if (cmp.id > highestId) + { + highestId = cmp.id; + } + } + + // Allocate the stack and set bits to replicate a bitset + var length = BitSet.RequiredLength(highestId + 1); + Span stack = stackalloc uint[length]; + var spanBitSet = new SpanBitSet(stack); + + foreach (ref var type in components) + { + var x = type.id; + spanBitSet.SetBit(x); + } + + return GetHashCode(stack); + } + + public static int GetHashCode(Span span) + { + var hashCode = new HashCode(); + hashCode.AddBytes(MemoryMarshal.AsBytes(span)); + + return hashCode.ToHashCode(); + } +} + +internal static class Component + where T : unmanaged, IComponent +{ + public static readonly ComponentData data; + + public static readonly Signature signature; + + static Component() + { + data = ComponentRegistry.GetOrAdd(); + signature = new Signature(data); + } +} \ No newline at end of file diff --git a/Ghost.Entities/Core/EntityInfo.cs b/Ghost.Entities/Core/EntityInfo.cs deleted file mode 100644 index 4874826..0000000 --- a/Ghost.Entities/Core/EntityInfo.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Ghost.Entities.Core; - -public readonly struct EntityInfo -{ -} \ No newline at end of file diff --git a/Ghost.Entities/Core/World.cs b/Ghost.Entities/Core/World.cs deleted file mode 100644 index fa5c57a..0000000 --- a/Ghost.Entities/Core/World.cs +++ /dev/null @@ -1,141 +0,0 @@ -using Ghost.Entities.Helpers; -using Misaki.HighPerformance.Unsafe.Collections; - -namespace Ghost.Entities.Core; - -public partial struct World -{ - public static UnsafeArray Worlds - { - get; - private set; - } = new(4, AllocationType.UnInitialized); - - public static UnsafeQueue FreeIndices - { - get; - private set; - } = new(4, AllocationType.UnInitialized); - - public static ushort Count - { - get; - private set; - } - - public static World Create(int chunkSizeInBytes = 16384, int minimumAmountOfEntitiesPerChunk = 100, int archetypeCapacity = 2, int entityCapacity = 64) - { - lock (ThreadLocker.WorldLock) - { - var recycle = FreeIndices.TryDequeue(out var id); - var recycledId = recycle ? id : Count; - - var world = new World(recycledId, chunkSizeInBytes, minimumAmountOfEntitiesPerChunk, archetypeCapacity, entityCapacity); - - if (recycledId >= Worlds.Size) - { - var newCapacity = Worlds.Size * 2; - Worlds.ReAlloc(newCapacity); - } - - Worlds[recycledId] = world; - Count++; - return world; - } - } -} - -public partial struct World -{ - /// - /// The unique ID. - /// - public int Id - { - get; - } - - /// - /// The amount of s currently stored by this . - /// - public int Size - { - get; internal set; - } - - /// - /// The available capacity of this . - /// - public int Capacity - { - get; internal set; - } - - ///// - ///// All s that exist in this . - ///// - //public Archetypes Archetypes - //{ - // get; - //} - - ///// - ///// Maps an to its for quick lookup. - ///// - //internal EntityInfoStorage EntityInfo - //{ - // get; - //} - - ///// - ///// Stores recycled IDs and their last version. - ///// - //internal PooledQueue RecycledIds - //{ - // get; set; - //} - - ///// - ///// A cache to map to their , to avoid allocs. - ///// - //internal PooledDictionary QueryCache - //{ - // get; set; - //} - - /// - /// The size of each in bytes. - /// For the best cache optimisation use values that are divisible by 16Kb. - /// - public int BaseChunkSize { get; private set; } = 16_384; - - /// - /// The minimum number of 's that should fit into a within all s. - /// On the basis of this, the s chunk size may increase. - /// - public int BaseChunkEntityCount { get; private set; } = 100; - - private World(int id, int baseChunkSize, int baseChunkEntityCount, int archetypeCapacity, int entityCapacity) - { - Id = id; - - // Mapping. - //GroupToArchetype = new PooledDictionary(archetypeCapacity); - - // Entity stuff. - //Archetypes = new Archetypes(archetypeCapacity); - //EntityInfo = new EntityInfoStorage(baseChunkSize, entityCapacity); - //RecycledIds = new PooledQueue(entityCapacity); - - // Query. - //QueryCache = new PooledDictionary(archetypeCapacity); - - // Multithreading/Jobs. - //JobHandles = new PooledList(Environment.ProcessorCount); - //JobsCache = new List(Environment.ProcessorCount); - - // Config - BaseChunkSize = baseChunkSize; - BaseChunkEntityCount = baseChunkEntityCount; - } -} \ No newline at end of file diff --git a/Ghost.Entities/Core/Entity.cs b/Ghost.Entities/Entity.cs similarity index 78% rename from Ghost.Entities/Core/Entity.cs rename to Ghost.Entities/Entity.cs index 8309213..f111698 100644 --- a/Ghost.Entities/Core/Entity.cs +++ b/Ghost.Entities/Entity.cs @@ -1,6 +1,6 @@ using System.Runtime.CompilerServices; -namespace Ghost.Entities.Core; +namespace Ghost.Entities; [SkipLocalsInit] public struct Entity : IEquatable, IComparable @@ -14,7 +14,7 @@ public struct Entity : IEquatable, IComparable private const EntityID _INDEX_MASK = (1u << (int)_INDEX_BITS) - 1; private const EntityID _ID_MASK = EntityID.MaxValue; - private uint _id; + private EntityID _id; public readonly bool IsValid { @@ -31,13 +31,13 @@ public struct Entity : IEquatable, IComparable public readonly GenerationID Generation { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => (GenerationID)((_id >> (int)_INDEX_BITS) & _GENERATION_MASK); + get => (GenerationID)(_id >> (int)_INDEX_BITS & _GENERATION_MASK); } public readonly WorldID WorldIndex { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => (WorldID)((_id >> (int)(_INDEX_BITS + _GENERATION_BITS)) & _WORLD_INDEX_MASK); + get => (WorldID)(_id >> (int)(_INDEX_BITS + _GENERATION_BITS) & _WORLD_INDEX_MASK); } public void IncrementGeneration() @@ -48,12 +48,12 @@ public struct Entity : IEquatable, IComparable throw new InvalidOperationException("Generation overflow"); } - _id = (_id & ~(_GENERATION_MASK << (int)_INDEX_BITS)) | (generation << (int)_INDEX_BITS); + _id = _id & ~(_GENERATION_MASK << (int)_INDEX_BITS) | generation << (int)_INDEX_BITS; } internal Entity(EntityID index, EntityID generation, EntityID worldIndex) { - _id = (worldIndex << (int)(_INDEX_BITS + _GENERATION_BITS)) | (generation << (int)_INDEX_BITS) | index; + _id = worldIndex << (int)(_INDEX_BITS + _GENERATION_BITS) | generation << (int)_INDEX_BITS | index; } public readonly bool Equals(Entity other) @@ -85,4 +85,9 @@ public struct Entity : IEquatable, IComparable { return !(left == right); } + + public override readonly string ToString() + { + return $"Entity {{ Index: {Index}, Generation: {Generation}, WorldIndex: {WorldIndex} }}"; + } } \ No newline at end of file diff --git a/Ghost.Entities/Helpers/BitSet.cs b/Ghost.Entities/Helpers/BitSet.cs new file mode 100644 index 0000000..04a88c5 --- /dev/null +++ b/Ghost.Entities/Helpers/BitSet.cs @@ -0,0 +1,608 @@ +// Code from https://github.com/genaray/Arch/blob/master/src/Arch/Core/Utils/BitSet.cs + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Ghost.Entities.Helpers; + +// NOTE: Can this be replaced with `System.Collections.BitArray`? +// NOTE: If not, can it at least mirror that type's API? +/// +/// The class +/// represents a resizable collection of bits. +/// +public sealed class BitSet +{ + private const int _BIT_SIZE = (sizeof(uint) * 8) - 1; // 31 + private const int _INDEX_SIZE = 5; // log_2(BitSize + 1) + + private static readonly int _padding = Vector.Count; // The padding used for vectorisation, the amount of uints required for being vectorized basically + + /// + /// Determines the required length of an to hold the passed id or bit. + /// + /// The id or bit. + /// A size of required s for the bitset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int RequiredLength(int id) + { + return (id >> 5) + int.Sign(id & _BIT_SIZE); + } + + /// + /// The bits from the bitset. + /// + private uint[] _bits; + + /// TODO: Update on ClearBit, however clearbit is only used in tests so its fine for now. + /// + /// The highest bit set. + /// + private int _highestBit; + + /// TODO: Update on ClearBit, probably remove in favor? + /// + /// The maximum -index current in use. + /// + private int _max; + + /// + /// Initializes a new instance of the class. + /// + public BitSet() + { + _bits = new uint[_padding]; + } + + /// + /// Initializes a new instance of the class. + /// + public BitSet(params uint[] bits) + { + _bits = bits; + } + + /// + /// The highest uint index in use inside the -array. + /// + public int HighestIndex + { + get => _max; + } + + /// + /// The highest bit set. + /// + public int HighestBit + { + get => _highestBit; + } + + /// + /// Returns the length of the bitset, how many ints it consists of. + /// + public int Length + { + get => _bits.Length; + } + + /// + /// Checks whether a bit is set at the index. + /// + /// The index. + /// True if it is, otherwise false + public bool IsSet(int index) + { + var b = index >> _INDEX_SIZE; + if (b >= _bits.Length) + { + return false; + } + + return (_bits[b] & (1 << (index & _BIT_SIZE))) != 0; + } + + /// + /// Sets a bit at the given index. + /// Resizes its internal array if necessary. + /// + /// The index. + public void SetBit(int index) + { + var b = index >> _INDEX_SIZE; + if (b >= _bits.Length) + { + Array.Resize(ref _bits, (b + _padding) / _padding * _padding); // Round up to a multiply of Padding + } + + // Track highest set bit + _highestBit = Math.Max(_highestBit, index); + _max = (_highestBit / (_BIT_SIZE + 1)) + 1; + _bits[b] |= 1u << (index & _BIT_SIZE); + } + + /// + /// Clears the bit at the given index. + /// + /// The index. + public void ClearBit(int index) + { + var b = index >> _INDEX_SIZE; + if (b >= _bits.Length) + { + return; + } + + _bits[b] &= ~(1u << (index & _BIT_SIZE)); + } + + /// + /// Sets all bits. + /// + public void SetAll() + { + var count = _bits.Length; + for (var i = 0; i < count; i++) + { + _bits[i] = 0xffffffff; + } + + _highestBit = (_bits.Length * (_BIT_SIZE + 1)) - 1; + _max = (_highestBit / (_BIT_SIZE + 1)) + 1; + } + + /// + /// Clears all set bits. + /// + public void ClearAll() + { + Array.Clear(_bits, 0, _bits.Length); + } + + /// + /// Checks if all bits from this instance match those of the other instance. + /// + /// The other . + /// True if they match, false if not. + [SkipLocalsInit] + public bool All(BitSet other) + { + var min = Math.Min(Math.Min(Length, other.Length), _max); + if (!Vector.IsHardwareAccelerated || min < _padding) + { + var bits = _bits.AsSpan(); + var otherBits = other._bits.AsSpan(); + + // Bitwise and + for (var i = 0; i < min; i++) + { + var bit = bits[i]; + if ((bit & otherBits[i]) != bit) + { + return false; + } + } + + // Handle extra bits on our side that might just be all zero. + for (var i = min; i < _max; i++) + { + if (bits[i] != 0) + { + return false; + } + } + } + else + { + // Vectorized bitwise and + for (var i = 0; i < min; i += _padding) + { + var vector = new Vector(_bits, i); + var otherVector = new Vector(other._bits, i); + + var resultVector = Vector.BitwiseAnd(vector, otherVector); + if (!Vector.EqualsAll(resultVector, vector)) + { + return false; + } + } + + // Handle extra bits on our side that might just be all zero. + for (var i = min; i < _max; i += _padding) + { + var vector = new Vector(_bits, i); + if (!Vector.EqualsAll(vector, Vector.Zero)) // Vectors are not zero bits[0] != 0 basically + { + return false; + } + } + } + + return true; + } + + /// + /// Checks if any bits from this instance match those of the other instance. + /// + /// The other . + /// True if they match, false if not. + public bool Any(BitSet other) + { + var min = Math.Min(Math.Min(Length, other.Length), _max); + if (!Vector.IsHardwareAccelerated || min < _padding) + { + var bits = _bits.AsSpan(); + var otherBits = other._bits.AsSpan(); + + // Bitwise and, return true since any is met + for (var i = 0; i < min; i++) + { + var bit = bits[i]; + if ((bit & otherBits[i]) > 0) + { + return true; + } + } + + // Handle extra bits on our side that might just be all zero. + for (var i = min; i < _max; i++) + { + if (bits[i] > 0) + { + return false; + } + } + } + else + { + // Vectorized bitwise and, return true since any is met + for (var i = 0; i < min; i += _padding) + { + var vector = new Vector(_bits, i); + var otherVector = new Vector(other._bits, i); + + var resultVector = Vector.BitwiseAnd(vector, otherVector); + if (!Vector.EqualsAll(resultVector, Vector.Zero)) + { + return true; + } + } + + // Handle extra bits on our side that might just be all zero. + for (var i = min; i < _max; i += _padding) + { + var vector = new Vector(_bits, i); + if (!Vector.EqualsAll(vector, Vector.Zero)) // Vectors are not zero bits[0] != 0 basically + { + return false; + } + } + } + + return _highestBit <= 0; + } + + /// + /// Checks if none bits from this instance match those of the other instance. + /// + /// The other . + /// True if none match, false if not. + public bool None(BitSet other) + { + var min = Math.Min(Math.Min(Length, other.Length), _max); + if (!Vector.IsHardwareAccelerated || min < _padding) + { + var bits = _bits.AsSpan(); + var otherBits = other._bits.AsSpan(); + + // Bitwise and, return true since any is met + for (var i = 0; i < min; i++) + { + var bit = bits[i]; + if ((bit & otherBits[i]) != 0) + { + return false; + } + } + } + else + { + // Vectorized bitwise and, return true since any is met + for (var i = 0; i < min; i += _padding) + { + var vector = new Vector(_bits, i); + var otherVector = new Vector(other._bits, i); + + var resultVector = Vector.BitwiseAnd(vector, otherVector); + if (!Vector.EqualsAll(resultVector, Vector.Zero)) + { + return false; + } + } + } + + return true; + } + + /// + /// Checks if exactly all bits from this instance match those of the other instance. + /// + /// The other . + /// True if they match, false if not. + public bool Exclusive(BitSet other) + { + var min = Math.Min(Math.Min(Length, other.Length), _max); + + if (!Vector.IsHardwareAccelerated || min < _padding) + { + var bits = _bits.AsSpan(); + var otherBits = other._bits.AsSpan(); + + // Bitwise xor, if both are not totally equal, return false + for (var i = 0; i < min; i++) + { + var bit = bits[i]; + if ((bit ^ otherBits[i]) != 0) + { + return false; + } + } + + // handle extra bits on our side that might just be all zero + for (var i = min; i < _max; i++) + { + if (bits[i] != 0) + { + return false; + } + } + } + else + { + // Vectorized bitwise xor, return true since any is met + for (var i = 0; i < min; i += _padding) + { + var vector = new Vector(_bits, i); + var otherVector = new Vector(other._bits, i); + + var resultVector = Vector.Xor(vector, otherVector); + if (!Vector.EqualsAll(resultVector, Vector.Zero)) + { + return false; + } + } + + // Handle extra bits on our side that might just be all zero. + for (var i = min; i < _max; i += _padding) + { + var vector = new Vector(_bits, i); + if (!Vector.EqualsAll(vector, Vector.Zero)) // Vectors are not zero bits[0] != 0 basically + { + return false; + } + } + } + + return true; + } + + /// + /// Creates a to access the . + /// + /// The hash. + public Span AsSpan() + { + var max = (_highestBit / (_BIT_SIZE + 1)) + 1; + return _bits.AsSpan()[0..max]; + } + + /// + /// Copies the bits into a and returns a slice containing the copied . + /// + /// The to copy into. + /// If true, it will zero the unused space from the . + /// The . + public Span AsSpan(Span span, bool zero = true) + { + // Copy everything thats possible from one to another + var length = Math.Min(Length, span.Length); + for (var index = 0; index < length; index++) + { + span[index] = _bits[index]; + } + + // Zero the rest space which was not overriden due to the copy. + for (var index = length; zero && index < span.Length; index++) + { + span[index] = 0; + } + + return span[0..length]; + } + + /// + /// Calculates the hash, this is unique for the set bits. Two with the same set bits, result in the same hash. + /// + /// The hash. + public override int GetHashCode() + { + return Component.GetHashCode(AsSpan()); + } + + /// + /// Prints the content of this instance. + /// + /// The string. + public override string ToString() + { + // Convert uint to binary form for pretty printing + var binaryBuilder = new StringBuilder(); + foreach (var bit in _bits) + { + binaryBuilder.Append(Convert.ToString((uint)bit, 2).PadLeft(32, '0')).Append(','); + } + binaryBuilder.Length--; + + return $"{nameof(_bits)}: {binaryBuilder}, {nameof(Length)}: {Length}"; + } +} + +/// +/// The struct +/// represents a non resizable collection of bits. +/// Used to set, check and clear bits on a allocated or on the stack. +/// +public readonly ref struct SpanBitSet +{ + private const int BitSize = (sizeof(uint) * 8) - 1; // 31 + // NOTE: Is a byte not 8 bits? + private const int ByteSize = 5; // log_2(BitSize + 1) + + /// + /// The bits from the bitset. + /// + private readonly Span _bits; + + /// + /// Initializes a new instance of the class. + /// + public SpanBitSet(Span bits) + { + _bits = bits; + } + + /// + /// Checks whether a bit is set at the index. + /// + /// The index. + /// True if it is, otherwise false + + public bool IsSet(int index) + { + var b = index >> ByteSize; + if (b >= _bits.Length) + { + return false; + } + + return (_bits[b] & (1 << (index & BitSize))) != 0; + } + + /// + /// Sets a bit at the given index. + /// Resizes its internal array if necessary. + /// + /// The index. + + public void SetBit(int index) + { + var b = index >> ByteSize; + if (b >= _bits.Length) + { + return; + } + + _bits[b] |= 1u << (index & BitSize); + } + + /// + /// Clears the bit at the given index. + /// + /// The index. + + public void ClearBit(int index) + { + var b = index >> ByteSize; + if (b >= _bits.Length) + { + return; + } + + _bits[b] &= ~(1u << (index & BitSize)); + } + + /// + /// + /// + + public void SetAll() + { + var count = _bits.Length; + for (var i = 0; i < count; i++) + { + _bits[i] = 0xffffffff; + } + } + + /// + /// Clears all set bits. + /// + + public void ClearAll() + { + _bits.Clear(); + } + + /// + /// Creates a to access the . + /// + /// The hash. + + public Span AsSpan() + { + return _bits; + } + + /// + /// Copies the bits into a and returns a slice containing the copied . + /// + /// + /// The hash. + + public Span AsSpan(Span span, bool zero = true) + { + // Prevent exception because target array is to small for copy operation + var length = Math.Min(this._bits.Length, span.Length); + for (var index = 0; index < length; index++) + { + span[index] = _bits[index]; + } + + // Zero the rest space which was not overriden due to the copy. + for (var index = length; zero && index < span.Length; index++) + { + span[index] = 0; + } + + return span[.._bits.Length]; + } + + /// + /// Calculates the hash, this is unique for the set bits. Two with the same set bits, result in the same hash. + /// + /// The hash. + + public override int GetHashCode() + { + return Component.GetHashCode(AsSpan()); + } + + /// + /// Prints the content of this instance. + /// + /// The string. + + public override string ToString() + { + // Convert uint to binary form for pretty printing + var binaryBuilder = new StringBuilder(); + foreach (var bit in _bits) + { + binaryBuilder.Append(Convert.ToString((uint)bit, 2).PadLeft(32, '0')).Append(','); + } + binaryBuilder.Length--; + + return $"{nameof(_bits)}: {string.Join(",", binaryBuilder)}"; + } +} diff --git a/Ghost.Entities/Registries/ComponentRegistry.cs b/Ghost.Entities/Registries/ComponentRegistry.cs new file mode 100644 index 0000000..91ef809 --- /dev/null +++ b/Ghost.Entities/Registries/ComponentRegistry.cs @@ -0,0 +1,22 @@ +namespace Ghost.Entities.Registries; + +internal static class ComponentRegistry +{ + private static readonly Dictionary _hashCodeToComponentMap = new(64); + + public static unsafe ComponentData GetOrAdd() + where T : unmanaged, IComponent + { + var type = typeof(T); + if (_hashCodeToComponentMap.TryGetValue(type, out var data)) + { + return data; + } + + var id = (ushort)_hashCodeToComponentMap.Count; + data = new ComponentData(id, sizeof(T)); + _hashCodeToComponentMap.Add(type, data); + + return data; + } +} \ No newline at end of file diff --git a/Ghost.Entities/Services/EntityChangeQueue.cs b/Ghost.Entities/Services/EntityChangeQueue.cs new file mode 100644 index 0000000..8782f84 --- /dev/null +++ b/Ghost.Entities/Services/EntityChangeQueue.cs @@ -0,0 +1,6 @@ +namespace Ghost.Entities.Services; + +internal class EntityChangeQueue +{ + // TODO: This class is not implemented yet. +} \ No newline at end of file diff --git a/Ghost.Entities/Signature.cs b/Ghost.Entities/Signature.cs new file mode 100644 index 0000000..4d93c90 --- /dev/null +++ b/Ghost.Entities/Signature.cs @@ -0,0 +1,38 @@ +using Misaki.HighPerformance.Unsafe.Collections; +using Misaki.HighPerformance.Unsafe.Helpers; + +namespace Ghost.Entities; + +internal struct Signature : IDisposable +{ + internal UnsafeArray _componentDatas; + private int _hashCode; + + public Signature(params Span components) + { + _componentDatas = new UnsafeArray(components.Length, Allocator.Persistent); + _componentDatas.CopyFrom(components); + + _hashCode = -1; + _hashCode = GetHashCode(); + } + + public override int GetHashCode() + { + if (_hashCode != -1) + { + return _hashCode; + } + + unchecked + { + _hashCode = Component.GetHashCode(_componentDatas.AsSpan()); + return _hashCode; + } + } + + public void Dispose() + { + _componentDatas.Dispose(); + } +} diff --git a/Ghost.OOP/AssemblyInfo.cs b/Ghost.OOP/AssemblyInfo.cs new file mode 100644 index 0000000..5cf5fcf --- /dev/null +++ b/Ghost.OOP/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Ghost.Engine")] diff --git a/Ghost.OOP/Ghost.Game.csproj b/Ghost.OOP/Ghost.Game.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/Ghost.OOP/Ghost.Game.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + +