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.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ public class ProjectInfo
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public int ID
|
||||
{
|
||||
get; set;
|
||||
get; internal set;
|
||||
}
|
||||
|
||||
public required string Name
|
||||
|
||||
8
Ghost.Data/Resources/AssetsPath.cs
Normal file
8
Ghost.Data/Resources/AssetsPath.cs
Normal file
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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<ProjectInfo> LoadAllProjectAsync()
|
||||
{
|
||||
return ProjectRepository.LoadProjectsAsync();
|
||||
}
|
||||
|
||||
public async Task<string> 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<ProjectInfo> 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<ProjectInfo> LoadProjectAsync()
|
||||
public Task RemoveProjectAsync(ProjectInfo project)
|
||||
{
|
||||
return ProjectRepository.LoadProjectsAsync();
|
||||
return ProjectRepository.RemoveProjectAsync(project);
|
||||
}
|
||||
|
||||
public Task UpdateProjectAsync(ProjectInfo project)
|
||||
{
|
||||
return ProjectRepository.UpdateProjectAsync(project);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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().
|
||||
/// </summary>
|
||||
public App()
|
||||
internal App()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -41,19 +36,27 @@ namespace Ghost.Editor
|
||||
UseContentRoot(AppContext.BaseDirectory).
|
||||
ConfigureServices((context, services) =>
|
||||
{
|
||||
services.AddTransient<ProjectService>();
|
||||
services.AddSingleton<ProjectService>();
|
||||
|
||||
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<T>() where T : class
|
||||
internal static void SetWindow(Window window)
|
||||
{
|
||||
if (Current is App app)
|
||||
{
|
||||
app._window = window;
|
||||
}
|
||||
}
|
||||
|
||||
internal static T GetService<T>() where T : class
|
||||
{
|
||||
if ((Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
|
||||
{
|
||||
|
||||
BIN
Ghost.Editor/Assets/icon-256.ico
Normal file
BIN
Ghost.Editor/Assets/icon-256.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net9.0-windows10.0.20348.0</TargetFramework>
|
||||
<TargetFramework>net9.0-windows10.0.22621.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>Ghost.Editor</RootNamespace>
|
||||
<Platforms>x86;x64;ARM64</Platforms>
|
||||
@@ -69,10 +69,13 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.1.240916" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.250129-preview2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.1742" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250310001" />
|
||||
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
|
||||
<PackageReference Include="WinUIEx" Version="2.5.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@@ -95,10 +98,10 @@
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Controls\Internal\" />
|
||||
<Folder Include="Models\" />
|
||||
<Folder Include="Resources\" />
|
||||
<Folder Include="Services\" />
|
||||
<Folder Include="ViewModel\Windows\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Windows\EngineEditorWindow.xaml">
|
||||
@@ -132,7 +135,7 @@
|
||||
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
|
||||
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
|
||||
<Nullable>enable</Nullable>
|
||||
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion>10.0.20348.0</SupportedOSPlatformVersion>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<PublishAot>True</PublishAot>
|
||||
<PublishTrimmed>True</PublishTrimmed>
|
||||
|
||||
@@ -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<LandingWindow>();
|
||||
services.AddSingleton<LandingWindow>();
|
||||
|
||||
services.AddTransient<CreateProjectPage>();
|
||||
services.AddTransient<CreateProjectViewModel>();
|
||||
@@ -18,5 +19,6 @@ internal static partial class HostHelper
|
||||
services.AddTransient<OpenProjectPage>();
|
||||
|
||||
services.AddSingleton<EngineEditorWindow>();
|
||||
services.AddSingleton<EngineEditorViewModel>();
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0" Margin="16,8">
|
||||
<Grid Grid.Row="0" Margin="16,8,16,16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="200" />
|
||||
@@ -40,12 +40,13 @@
|
||||
<ListView
|
||||
x:Name="ProjectListView"
|
||||
Grid.Row="1"
|
||||
Padding="16"
|
||||
Padding="8"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ListView_ItemClick"
|
||||
ItemsSource="{x:Bind projects}"
|
||||
SelectionMode="None"
|
||||
Visibility="Visible">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="data:ProjectInfo">
|
||||
|
||||
@@ -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<LandingWindow>().Close();
|
||||
|
||||
project.LastOpened = System.DateTime.Now;
|
||||
await _projectService.UpdateProjectAsync(project);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
<Grid />
|
||||
<Window.SystemBackdrop>
|
||||
<MicaBackdrop />
|
||||
</Window.SystemBackdrop>
|
||||
|
||||
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Titlebar -->
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Padding="8"
|
||||
Orientation="Horizontal">
|
||||
<ImageIcon
|
||||
Width="24"
|
||||
Height="24"
|
||||
VerticalAlignment="Center"
|
||||
Source="ms-appx:///Assets/Icon.targetsize-32.png" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.engineVersionDescriptor}" />
|
||||
<TextBlock
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.CurrentProject.Name}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<Grid Grid.Row="1">
|
||||
<controls:TabbedCommandBar>
|
||||
<controls:TabbedCommandBar.MenuItems>
|
||||
<controls:TabbedCommandBarItem Header="Home">
|
||||
<AppBarButton Label="Undo" />
|
||||
<AppBarButton Label="Redo" />
|
||||
<AppBarButton Label="Paste" />
|
||||
</controls:TabbedCommandBarItem>
|
||||
<controls:TabbedCommandBarItem Header="Home">
|
||||
<AppBarButton Label="Undo" />
|
||||
<AppBarButton Label="Redo" />
|
||||
<AppBarButton Label="Paste" />
|
||||
</controls:TabbedCommandBarItem>
|
||||
<controls:TabbedCommandBarItem Header="Home">
|
||||
<AppBarButton Label="Undo" />
|
||||
<AppBarButton Label="Redo" />
|
||||
<AppBarButton Label="Paste" />
|
||||
</controls:TabbedCommandBarItem>
|
||||
</controls:TabbedCommandBar.MenuItems>
|
||||
</controls:TabbedCommandBar>
|
||||
</Grid>
|
||||
|
||||
<!-- Editor -->
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid
|
||||
Grid.Column="0"
|
||||
Width="350"
|
||||
Background="Aquamarine" />
|
||||
<Grid Grid.Column="1" />
|
||||
<Grid
|
||||
Grid.Column="2"
|
||||
Width="350"
|
||||
Background="Bisque" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" Height="350">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid Grid.Column="0" Background="AliceBlue" />
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
Width="500"
|
||||
Background="HotPink" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<Grid
|
||||
Grid.Row="3"
|
||||
Height="25"
|
||||
Background="{ThemeResource SmokeFillColorDefaultBrush}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon
|
||||
Margin="8,0,0,0"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource SystemFillColorSuccessBrush}"
|
||||
Glyph=""
|
||||
Visibility="Visible" />
|
||||
|
||||
<Grid Visibility="Collapsed">
|
||||
<FontIcon
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource SystemFillColorAttentionBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="0" />
|
||||
<FontIcon
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource SystemFillColorCautionBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="0" />
|
||||
<FontIcon
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="0" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</winex:WindowEx>
|
||||
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// An empty window that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class EngineEditorWindow : WindowEx
|
||||
internal sealed partial class EngineEditorWindow : WindowEx
|
||||
{
|
||||
public EngineEditorViewModel ViewModel
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public EngineEditorWindow()
|
||||
{
|
||||
ViewModel = App.GetService<EngineEditorViewModel>();
|
||||
|
||||
AppWindow.SetIcon(AssetsPath.AppIconPath);
|
||||
Title = EngineData.ENGINE_NAME;
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
this.CenterOnScreen();
|
||||
}
|
||||
|
||||
public static bool TryLoadProject(ProjectInfo project)
|
||||
{
|
||||
try
|
||||
{
|
||||
var window = App.GetService<EngineEditorWindow>();
|
||||
window.ViewModel.CurrentProject = project;
|
||||
|
||||
window.Activate();
|
||||
window.Bindings.Update();
|
||||
|
||||
App.SetWindow(window);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (System.Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
<MicaBackdrop />
|
||||
</Window.SystemBackdrop>
|
||||
|
||||
<Grid>
|
||||
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="32" />
|
||||
<RowDefinition Height="*" />
|
||||
@@ -55,6 +54,8 @@
|
||||
x:Name="ContentFrame"
|
||||
Grid.Row="1"
|
||||
Padding="8"
|
||||
CacheMode="BitmapCache"
|
||||
CacheSize="10"
|
||||
IsNavigationStackEnabled="False" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<LandingWindow>().Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Ghost.Editor/ViewModel/Windows/EngineEditorViewModel.cs
Normal file
17
Ghost.Editor/ViewModel/Windows/EngineEditorViewModel.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
3
Ghost.Engine/AssemblyInfo.cs
Normal file
3
Ghost.Engine/AssemblyInfo.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Ghost.Editor")]
|
||||
24
Ghost.Engine/Component.cs
Normal file
24
Ghost.Engine/Component.cs
Normal file
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
96
Ghost.Engine/GameObject.cs
Normal file
96
Ghost.Engine/GameObject.cs
Normal file
@@ -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<Component> _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<T>() 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);
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,4 @@
|
||||
<IsAotCompatible>True</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Resources\" />
|
||||
<Folder Include="Services\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
20
Ghost.Engine/Helpers/MatrixHelpers.cs
Normal file
20
Ghost.Engine/Helpers/MatrixHelpers.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ghost.Engine.Helpers;
|
||||
|
||||
public static class MatrixHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a transformation matrix from position, rotation, and scale vectors.
|
||||
/// </summary>
|
||||
/// <param name="position">Defines the translation component of the transformation matrix.</param>
|
||||
/// <param name="rotation">Specifies the orientation of the object in 3D space.</param>
|
||||
/// <param name="scale">Determines the size of the object along each axis.</param>
|
||||
/// <returns>Returns a transformation matrix that combines the specified position, rotation, and scale.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Matrix4x4 CreateTRS(Vector3 position, Quaternion rotation, Vector3 scale)
|
||||
{
|
||||
return Matrix4x4.CreateScale(scale) * Matrix4x4.CreateFromQuaternion(rotation) * Matrix4x4.CreateTranslation(position);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Ghost.Engine.Models;
|
||||
|
||||
public abstract class Component
|
||||
{
|
||||
public required GameEntity Owner
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Ghost.Engine.Models;
|
||||
|
||||
public abstract class GameEntity
|
||||
{
|
||||
private ObservableCollection<Component> _components = new();
|
||||
|
||||
public GameEntity()
|
||||
{
|
||||
//AddComponent(new Transform());
|
||||
}
|
||||
|
||||
public void AddComponent(Component component)
|
||||
{
|
||||
_components.Add(component);
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,7 @@
|
||||
|
||||
public class Scene
|
||||
{
|
||||
internal Scene()
|
||||
{
|
||||
}
|
||||
}
|
||||
8
Ghost.Engine/Resources/EngineData.cs
Normal file
8
Ghost.Engine/Resources/EngineData.cs
Normal file
@@ -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);
|
||||
}
|
||||
80
Ghost.Engine/Services/GameLoopService.cs
Normal file
80
Ghost.Engine/Services/GameLoopService.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
|
||||
namespace Ghost.Engine.Services;
|
||||
|
||||
internal static class GameLoopService
|
||||
{
|
||||
private readonly static HashSet<GameObject> _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;
|
||||
}
|
||||
}
|
||||
5
Ghost.Entities/Archetype.cs
Normal file
5
Ghost.Entities/Archetype.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace Ghost.Entities;
|
||||
|
||||
public struct Archetype
|
||||
{
|
||||
}
|
||||
@@ -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")]
|
||||
213
Ghost.Entities/Chunk.cs
Normal file
213
Ghost.Entities/Chunk.cs
Normal file
@@ -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<Chunk> _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<Chunk> 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<Entity> entities;
|
||||
public UnsafeArray<UnsafeArray<byte>> 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<int> _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<ComponentData> data) : this(capacity, data, Component.ToLookupArray(data, Allocator.Persistent))
|
||||
{
|
||||
}
|
||||
|
||||
public Chunk(int capacity, Span<ComponentData> data, UnsafeArray<int> 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<byte>(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<UnsafeArray<byte>>(components.GetUnsafePtr(), i);
|
||||
var componentSize = componentArray->Count / _capacity;
|
||||
var removedComponent = UnsafeUtilities.ReadArrayElementUnsafe<byte>(componentArray->GetUnsafePtr(), index * componentSize);
|
||||
var lastComponent = UnsafeUtilities.ReadArrayElementUnsafe<byte>(componentArray->GetUnsafePtr(), lastIndex * componentSize);
|
||||
MemoryUtilities.MemCpy(removedComponent, lastComponent, (nuint)componentSize);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private readonly int IndexOf<T>()
|
||||
where T : unmanaged, IComponent
|
||||
{
|
||||
var id = Component<T>.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<T>()
|
||||
where T : unmanaged, IComponent
|
||||
{
|
||||
var id = Component<T>.data.id;
|
||||
return id < _componentLookup.Count && _componentLookup[id] != -1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly unsafe UnsafeArray<T> GetArrayOf<T>()
|
||||
where T : unmanaged, IComponent
|
||||
{
|
||||
var index = IndexOf<T>();
|
||||
var componentArray = components[index];
|
||||
return UnsafeUtilities.CastArray<byte, T>(componentArray);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly unsafe ref T GetComponent<T>(int index)
|
||||
where T : unmanaged, IComponent
|
||||
{
|
||||
var componentArray = GetArrayOf<T>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
103
Ghost.Entities/Component.cs
Normal file
103
Ghost.Entities/Component.cs
Normal file
@@ -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<int> ToLookupArray(Span<ComponentData> 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<int>(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<ComponentData> 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<uint> 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<uint> span)
|
||||
{
|
||||
var hashCode = new HashCode();
|
||||
hashCode.AddBytes(MemoryMarshal.AsBytes(span));
|
||||
|
||||
return hashCode.ToHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
internal static class Component<T>
|
||||
where T : unmanaged, IComponent
|
||||
{
|
||||
public static readonly ComponentData data;
|
||||
|
||||
public static readonly Signature signature;
|
||||
|
||||
static Component()
|
||||
{
|
||||
data = ComponentRegistry.GetOrAdd<T>();
|
||||
signature = new Signature(data);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace Ghost.Entities.Core;
|
||||
|
||||
public readonly struct EntityInfo
|
||||
{
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
using Ghost.Entities.Helpers;
|
||||
using Misaki.HighPerformance.Unsafe.Collections;
|
||||
|
||||
namespace Ghost.Entities.Core;
|
||||
|
||||
public partial struct World
|
||||
{
|
||||
public static UnsafeArray<World> Worlds
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
} = new(4, AllocationType.UnInitialized);
|
||||
|
||||
public static UnsafeQueue<WorldID> 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
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique <see cref="World"/> ID.
|
||||
/// </summary>
|
||||
public int Id
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The amount of <see cref="Entity"/>s currently stored by this <see cref="World"/>.
|
||||
/// </summary>
|
||||
public int Size
|
||||
{
|
||||
get; internal set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The available <see cref="Entity"/> capacity of this <see cref="World"/>.
|
||||
/// </summary>
|
||||
public int Capacity
|
||||
{
|
||||
get; internal set;
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// All <see cref="Archetype"/>s that exist in this <see cref="World"/>.
|
||||
///// </summary>
|
||||
//public Archetypes Archetypes
|
||||
//{
|
||||
// get;
|
||||
//}
|
||||
|
||||
///// <summary>
|
||||
///// Maps an <see cref="Entity"/> to its <see cref="EntityInfo"/> for quick lookup.
|
||||
///// </summary>
|
||||
//internal EntityInfoStorage EntityInfo
|
||||
//{
|
||||
// get;
|
||||
//}
|
||||
|
||||
///// <summary>
|
||||
///// Stores recycled <see cref="Entity"/> IDs and their last version.
|
||||
///// </summary>
|
||||
//internal PooledQueue<RecycledEntity> RecycledIds
|
||||
//{
|
||||
// get; set;
|
||||
//}
|
||||
|
||||
///// <summary>
|
||||
///// A cache to map <see cref="QueryDescription"/> to their <see cref="Core.Query"/>, to avoid allocs.
|
||||
///// </summary>
|
||||
//internal PooledDictionary<QueryDescription, Query> QueryCache
|
||||
//{
|
||||
// get; set;
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Chunk"/> size of each <see cref="Archetype"/> in bytes.
|
||||
/// <remarks>For the best cache optimisation use values that are divisible by 16Kb.</remarks>
|
||||
/// </summary>
|
||||
public int BaseChunkSize { get; private set; } = 16_384;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum number of <see cref="Arch.Core.Entity"/>'s that should fit into a <see cref="Chunk"/> within all <see cref="Archetype"/>s.
|
||||
/// On the basis of this, the <see cref="Archetypes"/>s chunk size may increase.
|
||||
/// </summary>
|
||||
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<int, Archetype>(archetypeCapacity);
|
||||
|
||||
// Entity stuff.
|
||||
//Archetypes = new Archetypes(archetypeCapacity);
|
||||
//EntityInfo = new EntityInfoStorage(baseChunkSize, entityCapacity);
|
||||
//RecycledIds = new PooledQueue<RecycledEntity>(entityCapacity);
|
||||
|
||||
// Query.
|
||||
//QueryCache = new PooledDictionary<QueryDescription, Query>(archetypeCapacity);
|
||||
|
||||
// Multithreading/Jobs.
|
||||
//JobHandles = new PooledList<JobHandle>(Environment.ProcessorCount);
|
||||
//JobsCache = new List<IJob>(Environment.ProcessorCount);
|
||||
|
||||
// Config
|
||||
BaseChunkSize = baseChunkSize;
|
||||
BaseChunkEntityCount = baseChunkEntityCount;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ghost.Entities.Core;
|
||||
namespace Ghost.Entities;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public struct Entity : IEquatable<Entity>, IComparable<Entity>
|
||||
@@ -14,7 +14,7 @@ public struct Entity : IEquatable<Entity>, IComparable<Entity>
|
||||
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<Entity>, IComparable<Entity>
|
||||
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<Entity>, IComparable<Entity>
|
||||
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<Entity>, IComparable<Entity>
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
public override readonly string ToString()
|
||||
{
|
||||
return $"Entity {{ Index: {Index}, Generation: {Generation}, WorldIndex: {WorldIndex} }}";
|
||||
}
|
||||
}
|
||||
608
Ghost.Entities/Helpers/BitSet.cs
Normal file
608
Ghost.Entities/Helpers/BitSet.cs
Normal file
@@ -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?
|
||||
/// <summary>
|
||||
/// The <see cref="BitSet"/> class
|
||||
/// represents a resizable collection of bits.
|
||||
/// </summary>
|
||||
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<uint>.Count; // The padding used for vectorisation, the amount of uints required for being vectorized basically
|
||||
|
||||
/// <summary>
|
||||
/// Determines the required length of an <see cref="BitSet"/> to hold the passed id or bit.
|
||||
/// </summary>
|
||||
/// <param name="id">The id or bit.</param>
|
||||
/// <returns>A size of required <see cref="uint"/>s for the bitset.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int RequiredLength(int id)
|
||||
{
|
||||
return (id >> 5) + int.Sign(id & _BIT_SIZE);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The bits from the bitset.
|
||||
/// </summary>
|
||||
private uint[] _bits;
|
||||
|
||||
/// TODO: Update on ClearBit, however clearbit is only used in tests so its fine for now.
|
||||
/// <summary>
|
||||
/// The highest bit set.
|
||||
/// </summary>
|
||||
private int _highestBit;
|
||||
|
||||
/// TODO: Update on ClearBit, probably remove <see cref="_highestBit"/> in favor?
|
||||
/// <summary>
|
||||
/// The maximum <see cref="_bits"/>-index current in use.
|
||||
/// </summary>
|
||||
private int _max;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BitSet" /> class.
|
||||
/// </summary>
|
||||
public BitSet()
|
||||
{
|
||||
_bits = new uint[_padding];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BitSet" /> class.
|
||||
/// </summary>
|
||||
public BitSet(params uint[] bits)
|
||||
{
|
||||
_bits = bits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The highest uint index in use inside the <see cref="_bits"/>-array.
|
||||
/// </summary>
|
||||
public int HighestIndex
|
||||
{
|
||||
get => _max;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The highest bit set.
|
||||
/// </summary>
|
||||
public int HighestBit
|
||||
{
|
||||
get => _highestBit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the length of the bitset, how many ints it consists of.
|
||||
/// </summary>
|
||||
public int Length
|
||||
{
|
||||
get => _bits.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a bit is set at the index.
|
||||
/// </summary>
|
||||
/// <param name="index">The index.</param>
|
||||
/// <returns>True if it is, otherwise false</returns>
|
||||
public bool IsSet(int index)
|
||||
{
|
||||
var b = index >> _INDEX_SIZE;
|
||||
if (b >= _bits.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (_bits[b] & (1 << (index & _BIT_SIZE))) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a bit at the given index.
|
||||
/// Resizes its internal array if necessary.
|
||||
/// </summary>
|
||||
/// <param name="index">The index.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the bit at the given index.
|
||||
/// </summary>
|
||||
/// <param name="index">The index.</param>
|
||||
public void ClearBit(int index)
|
||||
{
|
||||
var b = index >> _INDEX_SIZE;
|
||||
if (b >= _bits.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_bits[b] &= ~(1u << (index & _BIT_SIZE));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets all bits.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all set bits.
|
||||
/// </summary>
|
||||
public void ClearAll()
|
||||
{
|
||||
Array.Clear(_bits, 0, _bits.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if all bits from this instance match those of the other instance.
|
||||
/// </summary>
|
||||
/// <param name="other">The other <see cref="BitSet"/>.</param>
|
||||
/// <returns>True if they match, false if not.</returns>
|
||||
[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<uint>(_bits, i);
|
||||
var otherVector = new Vector<uint>(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<uint>(_bits, i);
|
||||
if (!Vector.EqualsAll(vector, Vector<uint>.Zero)) // Vectors are not zero bits[0] != 0 basically
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any bits from this instance match those of the other instance.
|
||||
/// </summary>
|
||||
/// <param name="other">The other <see cref="BitSet"/>.</param>
|
||||
/// <returns>True if they match, false if not.</returns>
|
||||
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<uint>(_bits, i);
|
||||
var otherVector = new Vector<uint>(other._bits, i);
|
||||
|
||||
var resultVector = Vector.BitwiseAnd(vector, otherVector);
|
||||
if (!Vector.EqualsAll(resultVector, Vector<uint>.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<uint>(_bits, i);
|
||||
if (!Vector.EqualsAll(vector, Vector<uint>.Zero)) // Vectors are not zero bits[0] != 0 basically
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _highestBit <= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if none bits from this instance match those of the other instance.
|
||||
/// </summary>
|
||||
/// <param name="other">The other <see cref="BitSet"/>.</param>
|
||||
/// <returns>True if none match, false if not.</returns>
|
||||
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<uint>(_bits, i);
|
||||
var otherVector = new Vector<uint>(other._bits, i);
|
||||
|
||||
var resultVector = Vector.BitwiseAnd(vector, otherVector);
|
||||
if (!Vector.EqualsAll(resultVector, Vector<uint>.Zero))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if exactly all bits from this instance match those of the other instance.
|
||||
/// </summary>
|
||||
/// <param name="other">The other <see cref="BitSet"/>.</param>
|
||||
/// <returns>True if they match, false if not.</returns>
|
||||
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<uint>(_bits, i);
|
||||
var otherVector = new Vector<uint>(other._bits, i);
|
||||
|
||||
var resultVector = Vector.Xor(vector, otherVector);
|
||||
if (!Vector.EqualsAll(resultVector, Vector<uint>.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<uint>(_bits, i);
|
||||
if (!Vector.EqualsAll(vector, Vector<uint>.Zero)) // Vectors are not zero bits[0] != 0 basically
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Span{T}"/> to access the <see cref="_bits"/>.
|
||||
/// </summary>
|
||||
/// <returns>The hash.</returns>
|
||||
public Span<uint> AsSpan()
|
||||
{
|
||||
var max = (_highestBit / (_BIT_SIZE + 1)) + 1;
|
||||
return _bits.AsSpan()[0..max];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the bits into a <see cref="Span{T}"/> and returns a slice containing the copied <see cref="_bits"/>.
|
||||
/// </summary>
|
||||
/// <param name="span">The <see cref="Span{T}"/> to copy into.</param>
|
||||
/// <param name="zero">If true, it will zero the unused space from the <see cref="span"/>.</param>
|
||||
/// <returns>The <see cref="Span{T}"/>.</returns>
|
||||
public Span<uint> AsSpan(Span<uint> 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];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the hash, this is unique for the set bits. Two <see cref="BitSet"/> with the same set bits, result in the same hash.
|
||||
/// </summary>
|
||||
/// <returns>The hash.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Component.GetHashCode(AsSpan());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints the content of this instance.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SpanBitSet"/> struct
|
||||
/// represents a non resizable collection of bits.
|
||||
/// Used to set, check and clear bits on a allocated <see cref="BitSet"/> or on the stack.
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
/// <summary>
|
||||
/// The bits from the bitset.
|
||||
/// </summary>
|
||||
private readonly Span<uint> _bits;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BitSet" /> class.
|
||||
/// </summary>
|
||||
public SpanBitSet(Span<uint> bits)
|
||||
{
|
||||
_bits = bits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether a bit is set at the index.
|
||||
/// </summary>
|
||||
/// <param name="index">The index.</param>
|
||||
/// <returns>True if it is, otherwise false</returns>
|
||||
|
||||
public bool IsSet(int index)
|
||||
{
|
||||
var b = index >> ByteSize;
|
||||
if (b >= _bits.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (_bits[b] & (1 << (index & BitSize))) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a bit at the given index.
|
||||
/// Resizes its internal array if necessary.
|
||||
/// </summary>
|
||||
/// <param name="index">The index.</param>
|
||||
|
||||
public void SetBit(int index)
|
||||
{
|
||||
var b = index >> ByteSize;
|
||||
if (b >= _bits.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_bits[b] |= 1u << (index & BitSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the bit at the given index.
|
||||
/// </summary>
|
||||
/// <param name="index">The index.</param>
|
||||
|
||||
public void ClearBit(int index)
|
||||
{
|
||||
var b = index >> ByteSize;
|
||||
if (b >= _bits.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_bits[b] &= ~(1u << (index & BitSize));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
|
||||
public void SetAll()
|
||||
{
|
||||
var count = _bits.Length;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
_bits[i] = 0xffffffff;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all set bits.
|
||||
/// </summary>
|
||||
|
||||
public void ClearAll()
|
||||
{
|
||||
_bits.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Span{T}"/> to access the <see cref="_bits"/>.
|
||||
/// </summary>
|
||||
/// <returns>The hash.</returns>
|
||||
|
||||
public Span<uint> AsSpan()
|
||||
{
|
||||
return _bits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the bits into a <see cref="Span{T}"/> and returns a slice containing the copied <see cref="_bits"/>.
|
||||
/// </summary>
|
||||
/// <param name=""></param>
|
||||
/// <returns>The hash.</returns>
|
||||
|
||||
public Span<uint> AsSpan(Span<uint> 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];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the hash, this is unique for the set bits. Two <see cref="BitSet"/> with the same set bits, result in the same hash.
|
||||
/// </summary>
|
||||
/// <returns>The hash.</returns>
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Component.GetHashCode(AsSpan());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints the content of this instance.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
|
||||
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)}";
|
||||
}
|
||||
}
|
||||
22
Ghost.Entities/Registries/ComponentRegistry.cs
Normal file
22
Ghost.Entities/Registries/ComponentRegistry.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace Ghost.Entities.Registries;
|
||||
|
||||
internal static class ComponentRegistry
|
||||
{
|
||||
private static readonly Dictionary<Type, ComponentData> _hashCodeToComponentMap = new(64);
|
||||
|
||||
public static unsafe ComponentData GetOrAdd<T>()
|
||||
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;
|
||||
}
|
||||
}
|
||||
6
Ghost.Entities/Services/EntityChangeQueue.cs
Normal file
6
Ghost.Entities/Services/EntityChangeQueue.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Ghost.Entities.Services;
|
||||
|
||||
internal class EntityChangeQueue
|
||||
{
|
||||
// TODO: This class is not implemented yet.
|
||||
}
|
||||
38
Ghost.Entities/Signature.cs
Normal file
38
Ghost.Entities/Signature.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Misaki.HighPerformance.Unsafe.Collections;
|
||||
using Misaki.HighPerformance.Unsafe.Helpers;
|
||||
|
||||
namespace Ghost.Entities;
|
||||
|
||||
internal struct Signature : IDisposable
|
||||
{
|
||||
internal UnsafeArray<ComponentData> _componentDatas;
|
||||
private int _hashCode;
|
||||
|
||||
public Signature(params Span<ComponentData> components)
|
||||
{
|
||||
_componentDatas = new UnsafeArray<ComponentData>(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();
|
||||
}
|
||||
}
|
||||
3
Ghost.OOP/AssemblyInfo.cs
Normal file
3
Ghost.OOP/AssemblyInfo.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Ghost.Engine")]
|
||||
9
Ghost.OOP/Ghost.Game.csproj
Normal file
9
Ghost.OOP/Ghost.Game.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user