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 0000000..40c9344 Binary files /dev/null and b/Ghost.Editor/Assets/icon-256.ico differ diff --git a/Ghost.Editor/Ghost.Editor.csproj b/Ghost.Editor/Ghost.Editor.csproj index ac99679..b669583 100644 --- a/Ghost.Editor/Ghost.Editor.csproj +++ b/Ghost.Editor/Ghost.Editor.csproj @@ -1,7 +1,7 @@ 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 + + +