Refactor project structure and enhance functionality

Added `InternalsVisibleTo` attribute for "Ghost.Editor" in `AssemblyInfo.cs`.
Added a binary file `Empty.zip` to the project.
Added a new `ProjectMetadata` class in `ProjectMetadata.cs`.
Added new states and interfaces for managing application states in `EditorState.cs`, `LandingState.cs`, and `IAppState.cs`.
Added a notification service in `INotificationService.cs` and `StackedNotificationService.cs`.
Added new XAML files for UI components, including `InspectorView.xaml` and `InternalControls.xaml`.

Changed the `ProjectInfo` class in `ProjectInfo.cs` to include a `MetadataPath` property instead of `Path` and `EngineVersion`.
Changed the `TemplateInfo` class in `TemplateInfo.cs` to use a struct instead of a class for `TemplateData`.
Changed the `ProjectService` class to use the new `ProjectRepository` for managing project data.

Removed several using directives and the entire `ProjectRepository` class from `ProjectRepository.cs`, replacing it with a new implementation.
Removed old methods and properties in `EntityManager` and `World` classes to improve entity management and component handling.

Updated the `Ghost.Data.csproj` file to include the new `Empty.zip` file as a content item.
Updated the `ProjectRepository` class to manage project data using SQLite.
Updated various XAML files to include new styles and controls, improving the overall UI design.
Updated the `CreateProjectViewModel` to include a notification service and handle project creation logic.
Updated the test project to include references to the new `Ghost.Graphics` project and modified test cases to align with the new structure.
This commit is contained in:
2025-05-31 01:45:34 +09:00
parent 67b6040b5e
commit 61bbb1bc68
66 changed files with 1923 additions and 733 deletions

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ghost.Editor")]

Binary file not shown.

View File

@@ -1,101 +0,0 @@
using Ghost.Data.Models;
using Ghost.Data.Resources;
using System.Data.SQLite;
namespace Ghost.Data.DataContext;
internal static class ProjectRepository
{
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(Command.CONNECTION_STRING, DataPath.APPLICATION_DATA_FOLDER);
private static async Task EnsureTableCreatedAsync(SQLiteConnection connection)
{
using var createCommand = connection.CreateCommand();
createCommand.CommandText = Command.CREATE_PROJECT_TABLE_STRING;
await createCommand.ExecuteNonQueryAsync();
}
public static async IAsyncEnumerable<ProjectInfo> LoadProjectsAsync()
{
using var connection = new SQLiteConnection(GetConnectionString());
await connection.OpenAsync();
await EnsureTableCreatedAsync(connection);
using var command = connection.CreateCommand();
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)),
LastOpened = reader.GetDateTime(4)
};
yield return project;
}
}
public static async Task AddProjectAsync(ProjectInfo project)
{
using var connection = new SQLiteConnection(GetConnectionString());
await connection.OpenAsync();
await EnsureTableCreatedAsync(connection);
using var command = connection.CreateCommand();
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();
}
}

View File

@@ -20,4 +20,10 @@
<PackageReference Include="System.Drawing.Common" Version="4.7.3" /> <PackageReference Include="System.Drawing.Common" Version="4.7.3" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="Assets\ProjectTemplates\Empty.zip">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project> </Project>

View File

@@ -5,6 +5,7 @@ namespace Ghost.Data;
[JsonSourceGenerationOptions(WriteIndented = true)] [JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(TemplateInfo))] [JsonSerializable(typeof(TemplateInfo))]
[JsonSerializable(typeof(ProjectMetadata))]
internal partial class JsonContext : JsonSerializerContext internal partial class JsonContext : JsonSerializerContext
{ {
} }

View File

@@ -2,7 +2,7 @@
namespace Ghost.Data.Models; namespace Ghost.Data.Models;
public class ProjectInfo internal class ProjectInfo
{ {
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID public int ID
@@ -15,17 +15,7 @@ public class ProjectInfo
get; set; get; set;
} }
public required string Path public required string MetadataPath
{
get; set;
}
public required Version EngineVersion
{
get; set;
}
public required DateTime LastOpened
{ {
get; set; get; set;
} }

View File

@@ -0,0 +1,51 @@
namespace Ghost.Data.Models;
public class ProjectMetadata
{
public const string PROJECT_EXTENSION = "ghostproj";
public Guid ID
{
get; set;
}
public string Name
{
get; set;
}
public Version EngineVersion
{
get; set;
}
public DateTime CreatedAt
{
get; set;
}
public DateTime LastOpened
{
get; set;
}
public ProjectMetadata(string name, Version engineVersion)
{
ID = Guid.NewGuid();
Name = name;
EngineVersion = engineVersion;
CreatedAt = DateTime.UtcNow;
LastOpened = DateTime.UtcNow;
}
// Parameterless constructor for deserialization
public ProjectMetadata()
{
}
}
public readonly struct ProjectMetadataInfo(string path, ProjectMetadata metadata)
{
public readonly string Path => path;
public readonly ProjectMetadata Metadata => metadata;
}

View File

@@ -0,0 +1,53 @@
namespace Ghost.Data.Models;
public readonly struct Result
{
public readonly bool success;
public readonly string? message;
public Result(bool success, string? message = null)
{
this.success = success;
this.message = message;
}
public static Result OK()
{
return new Result(true);
}
public static Result Error(string? message)
{
return new Result(false, message);
}
public override string ToString() => success ? "OK" : $"Error: {message}";
}
public readonly struct Result<T>
{
public readonly bool success;
public readonly T? data;
public readonly string? message;
public Result(bool success, T? data, string? message = null)
{
this.success = success;
this.data = data;
this.message = message;
}
public static Result<T> OK(T data)
{
return new Result<T>(true, data);
}
public static Result<T> Error(string? message)
{
return new Result<T>(false, default, message);
}
public override string ToString() => success ? $"OK: {data}" : $"Error: {message}";
}

View File

@@ -23,21 +23,21 @@ public class TemplateInfo
} }
} }
public class TemplateData(string templatePath, TemplateInfo info) public struct TemplateData(string templatePath, TemplateInfo info)
{ {
private const string _ICON_NAME = "icon.png"; private const string _ICON_NAME = "icon.png";
private const string _PREVIEW_NAME = "preview.png"; private const string _PREVIEW_NAME = "preview.png";
public string directory = Path.GetDirectoryName(templatePath)!; public string directory = Path.GetDirectoryName(templatePath)!;
public TemplateInfo Info => info; public readonly TemplateInfo Info => info;
public Uri GetIconURI() public readonly Uri GetIconURI()
{ {
return new Uri(Path.Combine(directory, _ICON_NAME)); return new Uri(Path.Combine(directory, _ICON_NAME));
} }
public Uri GetPreviewURI() public readonly Uri GetPreviewURI()
{ {
return new Uri(Path.Combine(directory, _PREVIEW_NAME)); return new Uri(Path.Combine(directory, _PREVIEW_NAME));
} }

View File

@@ -0,0 +1,99 @@
using Ghost.Data.Models;
using System.Data;
using System.Data.SQLite;
namespace Ghost.Data.Repository;
internal class ProjectRepository : IDisposable
{
private readonly SQLiteConnection _connection;
public ProjectRepository(string sourceDirectory)
{
_connection = new SQLiteConnection(string.Format(Command.CONNECTION_STRING, sourceDirectory));
_connection.Open();
}
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, MetadataPath TEXT);";
public const string SELECT_PROJECT_STRING = "SELECT * FROM Projects";
public const string INSERT_PROJECT_STRING = "INSERT INTO Projects (Name, MetadataPath) VALUES (@Name, @MetadataPath);";
public const string REMOVE_PROJECT_STRING = "DELETE FROM Projects WHERE ID = @ID;";
public const string UPDATE_PROJECT_STRING = "UPDATE Projects SET Name = @Name, MetadataPath = @MetadataPath WHERE ID = @ID;";
}
private async Task EnsureTableCreatedAsync()
{
using var createCommand = _connection.CreateCommand();
createCommand.CommandText = Command.CREATE_PROJECT_TABLE_STRING;
await createCommand.ExecuteNonQueryAsync();
}
public async IAsyncEnumerable<ProjectInfo> LoadProjectsAsync()
{
await EnsureTableCreatedAsync();
using var command = _connection.CreateCommand();
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),
MetadataPath = reader.GetString(2),
};
yield return project;
}
}
public async Task AddProjectAsync(ProjectInfo project)
{
await EnsureTableCreatedAsync();
using var command = _connection.CreateCommand();
command.CommandText = Command.INSERT_PROJECT_STRING;
command.Parameters.AddWithValue("@Name", project.Name);
command.Parameters.AddWithValue("@MetadataPath", project.MetadataPath);
await command.ExecuteNonQueryAsync();
}
public async Task RemoveProjectAsync(ProjectInfo project)
{
using var command = _connection.CreateCommand();
command.CommandText = Command.REMOVE_PROJECT_STRING;
command.Parameters.AddWithValue("@ID", project.ID);
await command.ExecuteNonQueryAsync();
}
public async Task UpdateProjectAsync(ProjectInfo project)
{
using var command = _connection.CreateCommand();
command.CommandText = Command.UPDATE_PROJECT_STRING;
command.Parameters.AddWithValue("@Name", project.Name);
command.Parameters.AddWithValue("@MetadataPath", project.MetadataPath);
command.Parameters.AddWithValue("@ID", project.ID);
await command.ExecuteNonQueryAsync();
}
public void Dispose()
{
if (_connection.State == ConnectionState.Open)
{
_connection.Close();
}
_connection.Dispose();
}
}

View File

@@ -4,5 +4,5 @@ public static class AssetsPath
{ {
public const string ASSETS_FOLDER = "Assets"; public const string ASSETS_FOLDER = "Assets";
public readonly static string AppIconPath = Path.Combine(AppContext.BaseDirectory, $"{ASSETS_FOLDER}/Icon-256.ico"); public readonly static string s_appIconPath = Path.Combine(AppContext.BaseDirectory, $"{ASSETS_FOLDER}/Icon-256.ico");
} }

View File

@@ -4,6 +4,6 @@ public class DataPath
{ {
public const string ENGINE_DATA_FOLDER_NAME = "GhostEngine"; public const string ENGINE_DATA_FOLDER_NAME = "GhostEngine";
public readonly static string APPLICATION_DATA_FOLDER = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ENGINE_DATA_FOLDER_NAME); public readonly static string s_applicationDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ENGINE_DATA_FOLDER_NAME);
public readonly static string PROJECT_TEMPLATES_FOLDER = Path.Combine(APPLICATION_DATA_FOLDER, "ProjectTemplates"); public readonly static string s_projectTemplateFolder = Path.Combine(s_applicationDataFolder, "ProjectTemplates");
} }

View File

@@ -1,25 +1,38 @@
using Ghost.Data.DataContext; using Ghost.Data.Models;
using Ghost.Data.Models; using Ghost.Data.Repository;
using Ghost.Data.Resources; using Ghost.Data.Resources;
using System.IO.Compression; using System.IO.Compression;
using System.Text.Json; using System.Text.Json;
namespace Ghost.Data.Services; namespace Ghost.Data.Services;
public class ProjectService internal partial class ProjectService
{ {
private const string _ASSETS_FOLDER = "Assets"; private const string _ASSETS_FOLDER = "Assets";
private const string _CONFIG_FOLDER = "ProjectConfig";
private const string _TEMPLATE_CONTENT_FILE = "content.zip"; private const string _TEMPLATE_CONTENT_FILE = "content.zip";
public async IAsyncEnumerable<(string path, TemplateInfo info)> GetProjectTemplatesAsync() public static void EnsureDefaultTemplate()
{ {
var templatesFolder = DataPath.PROJECT_TEMPLATES_FOLDER; var templates = Directory.GetFiles(DataPath.s_projectTemplateFolder, "template.json", SearchOption.AllDirectories);
if (templates.Length > 0)
{
return; // Default template already exists
}
var defaultTemplatePath = Path.Combine(AppContext.BaseDirectory, "Assets/ProjectTemplates/Empty.zip");
ZipFile.ExtractToDirectory(defaultTemplatePath, DataPath.s_projectTemplateFolder, true);
}
public static async IAsyncEnumerable<(string path, TemplateInfo info)> GetProjectTemplatesAsync()
{
var templatesFolder = DataPath.s_projectTemplateFolder;
if (!Directory.Exists(templatesFolder)) if (!Directory.Exists(templatesFolder))
{ {
yield break; yield break;
} }
var templates = Directory.GetFiles(DataPath.PROJECT_TEMPLATES_FOLDER, "template.json", SearchOption.AllDirectories); var templates = Directory.GetFiles(DataPath.s_projectTemplateFolder, "template.json", SearchOption.AllDirectories);
foreach (var templatePath in templates) foreach (var templatePath in templates)
{ {
var fileStream = File.OpenRead(templatePath); var fileStream = File.OpenRead(templatePath);
@@ -33,68 +46,152 @@ public class ProjectService
} }
} }
private Task SetupAssetsFolder(string projectPath, string templatePath) public static async Task CreateMetadataFileAsync(string path, ProjectMetadata metadata)
{ {
return Task.Run(() => await using var fileStream = File.Create(path);
{ await JsonSerializer.SerializeAsync(fileStream, metadata, JsonContext.Default.ProjectMetadata);
var templateContentPath = Path.Combine(templatePath, _TEMPLATE_CONTENT_FILE);
var projectAssetsPath = Path.Combine(projectPath, _ASSETS_FOLDER);
Directory.CreateDirectory(projectAssetsPath);
if (!File.Exists(templateContentPath))
{
return;
}
ZipFile.ExtractToDirectory(templateContentPath, projectAssetsPath);
});
} }
public IAsyncEnumerable<ProjectInfo> LoadAllProjectAsync() public static async Task<ProjectMetadata?> LoadMetadataAsync(string ghostprojPath)
{ {
return ProjectRepository.LoadProjectsAsync(); if (!File.Exists(ghostprojPath))
}
public async Task<string> CreateProjectAsync(string projectName, string projectDirectory, string templatePath)
{
var projectPath = Path.Combine(projectDirectory, projectName);
if (!Directory.Exists(projectPath))
{ {
Directory.CreateDirectory(projectPath); throw new FileNotFoundException("Project metadata file not found.", ghostprojPath);
} }
await SetupAssetsFolder(projectPath, templatePath); await using var fileStream = File.OpenRead(ghostprojPath);
return await JsonSerializer.DeserializeAsync<ProjectMetadata>(fileStream, JsonContext.Default.ProjectMetadata);
return projectPath;
} }
public static async Task<Result<ProjectMetadataInfo>> ValidateProjectDirectoryAsync(string? projectDirectory)
{
if (string.IsNullOrWhiteSpace(projectDirectory) || !Directory.Exists(projectDirectory))
{
return Result<ProjectMetadataInfo>.Error("Project directory is invalid or does not exist.");
}
var projectAssetsPath = Path.Combine(projectDirectory, _ASSETS_FOLDER);
var projectConfigPath = Path.Combine(projectDirectory, _CONFIG_FOLDER);
if (!Directory.Exists(projectAssetsPath) || !Directory.Exists(projectConfigPath))
{
return Result<ProjectMetadataInfo>.Error("Project folder structure is invalid.");
}
var metadataPath = Directory.GetFiles(projectDirectory, $"*.{ProjectMetadata.PROJECT_EXTENSION}", SearchOption.TopDirectoryOnly).FirstOrDefault();
if (string.IsNullOrWhiteSpace(metadataPath) || !File.Exists(metadataPath))
{
return Result<ProjectMetadataInfo>.Error("Project metadata file not found.");
}
var metadata = await LoadMetadataAsync(metadataPath);
if (metadata == null)
{
return Result<ProjectMetadataInfo>.Error("Project metadata file is corrupted or invalid.");
}
return Result<ProjectMetadataInfo>.OK(new(metadataPath, metadata));
}
private static async ValueTask SetupRequestFolderAsync(string projectDirectory, string templateDirectory)
{
var projectAssetsPath = Path.Combine(projectDirectory, _ASSETS_FOLDER);
var projectConfigPath = Path.Combine(projectDirectory, _CONFIG_FOLDER);
var templateContentPath = Path.Combine(templateDirectory, _TEMPLATE_CONTENT_FILE);
Directory.CreateDirectory(projectAssetsPath);
if (File.Exists(templateContentPath))
{
await Task.Run(() =>
{
ZipFile.ExtractToDirectory(templateContentPath, projectAssetsPath);
});
}
Directory.CreateDirectory(projectConfigPath);
}
}
internal partial class ProjectService : IDisposable
{
private readonly ProjectRepository _repository = new(DataPath.s_applicationDataFolder);
public Task AddProjectAsync(ProjectInfo project) public Task AddProjectAsync(ProjectInfo project)
{ {
return ProjectRepository.AddProjectAsync(project); return _repository.AddProjectAsync(project);
} }
public async Task<ProjectInfo> AddProjectAsync(string name, string path, Version version) public async Task<ProjectInfo> AddProjectAsync(string name, string path)
{ {
var project = new ProjectInfo var project = new ProjectInfo
{ {
Name = name, Name = name,
Path = path, MetadataPath = path,
EngineVersion = version,
LastOpened = DateTime.Now
}; };
await ProjectRepository.AddProjectAsync(project); await _repository.AddProjectAsync(project);
return project; return project;
} }
public Task RemoveProjectAsync(ProjectInfo project) public Task RemoveProjectAsync(ProjectInfo project)
{ {
return ProjectRepository.RemoveProjectAsync(project); return _repository.RemoveProjectAsync(project);
} }
public Task UpdateProjectAsync(ProjectInfo project) public Task UpdateProjectAsync(ProjectInfo project)
{ {
return ProjectRepository.UpdateProjectAsync(project); return _repository.UpdateProjectAsync(project);
}
public IAsyncEnumerable<ProjectInfo> LoadAllProjectAsync()
{
return _repository.LoadProjectsAsync();
}
public async Task<Result<ProjectInfo>> CreateProjectAsync(string projectName, string projectDirectory, Version engineVersion, string templatePath)
{
try
{
var projectPath = Path.Combine(projectDirectory, projectName);
if (!Directory.Exists(projectPath))
{
Directory.CreateDirectory(projectPath);
}
else
{
// Check if folder is empty
if (Directory.EnumerateFiles(projectPath, "*", SearchOption.AllDirectories).Any())
{
return new(false, null, "Directory is not empty");
}
}
var metadata = new ProjectMetadata(projectName, engineVersion);
var metadataPath = Path.Combine(projectPath, $"{projectName}.{ProjectMetadata.PROJECT_EXTENSION}");
await CreateMetadataFileAsync(metadataPath, metadata);
await SetupRequestFolderAsync(projectPath, templatePath);
var info = await AddProjectAsync(projectName, metadataPath);
return new(true, info);
}
catch (Exception e)
{
return Result<ProjectInfo>.Error($"Failed to create project: {e.Message}");
}
}
public async Task<Result<ProjectMetadataInfo>> AddProjectFromDirectoryAsync(string projectDirectory)
{
var result = await ValidateProjectDirectoryAsync(projectDirectory);
if (result.success)
{
await AddProjectAsync(result.data.Metadata.Name, result.data.Path);
}
return result;
}
public void Dispose()
{
_repository.Dispose();
} }
} }

View File

@@ -1,4 +1,5 @@
using Ghost.Data.Resources; using Ghost.Data.Resources;
using Ghost.Data.Services;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using System.IO; using System.IO;
@@ -8,19 +9,20 @@ internal static class ActivationHandler
{ {
private static void FolderInitialization() private static void FolderInitialization()
{ {
if (!Directory.Exists(DataPath.APPLICATION_DATA_FOLDER)) if (!Directory.Exists(DataPath.s_applicationDataFolder))
{ {
Directory.CreateDirectory(DataPath.APPLICATION_DATA_FOLDER); Directory.CreateDirectory(DataPath.s_applicationDataFolder);
} }
if (!Directory.Exists(DataPath.PROJECT_TEMPLATES_FOLDER)) if (!Directory.Exists(DataPath.s_projectTemplateFolder))
{ {
Directory.CreateDirectory(DataPath.PROJECT_TEMPLATES_FOLDER); Directory.CreateDirectory(DataPath.s_projectTemplateFolder);
} }
} }
public static void Handle(LaunchActivatedEventArgs args) public static void Handle(LaunchActivatedEventArgs args)
{ {
FolderInitialization(); FolderInitialization();
ProjectService.EnsureDefaultTemplate();
} }
} }

View File

@@ -9,6 +9,8 @@
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" /> <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<XamlControlsResources Source="/Controls/EditorControls.xaml" /> <XamlControlsResources Source="/Controls/EditorControls.xaml" />
<ResourceDictionary Source="/Themes/Dark.xaml" />
<ResourceDictionary Source="/Themes/Light.xaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<!-- Other app resources here --> <!-- Other app resources here -->
</ResourceDictionary> </ResourceDictionary>

View File

@@ -1,6 +1,6 @@
using Ghost.Data.Services; using Ghost.Editor.AppStates;
using Ghost.Editor.Helpers; using Ghost.Editor.Helpers;
using Ghost.Editor.View.Windows; using Ghost.Editor.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
@@ -18,6 +18,18 @@ namespace Ghost.Editor
{ {
private Window? _window; private Window? _window;
internal static Window? Window
{
get => (Current as App)?._window;
set
{
if (Current is App app)
{
app._window = value;
}
}
}
internal IHost Host internal IHost Host
{ {
get; get;
@@ -36,27 +48,29 @@ namespace Ghost.Editor
UseContentRoot(AppContext.BaseDirectory). UseContentRoot(AppContext.BaseDirectory).
ConfigureServices((context, services) => ConfigureServices((context, services) =>
{ {
services.AddSingleton<ProjectService>(); services.AddSingleton(sp =>
{
return new AppStateService(
new LandingState(),
new EditorState());
});
HostHelper.SetupPageService(context, services); HostHelper.AddLandingScope(context, services);
HostHelper.AddEngineScope(context, services);
services.AddSingleton<StackedNotificationService>();
}) })
.Build(); .Build();
UnhandledException += App_UnhandledException;
} }
internal static Window? GetWindow() internal static IServiceScope CreateScope()
{ {
return (Current as App)?._window; return (Current as App)!.Host.Services.CreateScope();
} }
internal static void SetWindow(Window window) public static T GetService<T>() where T : class
{
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) if ((Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
{ {
@@ -70,7 +84,7 @@ namespace Ghost.Editor
/// Invoked when the application is launched. /// Invoked when the application is launched.
/// </summary> /// </summary>
/// <param name="args">Details about the launch request and process.</param> /// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs args) protected override async void OnLaunched(LaunchActivatedEventArgs args)
{ {
base.OnLaunched(args); base.OnLaunched(args);
@@ -78,8 +92,13 @@ namespace Ghost.Editor
Host.Start(); Host.Start();
_window = GetService<LandingWindow>(); await GetService<AppStateService>().TransitionToAsync(StateKey.Landing);
_window.Activate(); }
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
// TODO: Log and handle exceptions as appropriate.
// https://docs.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.application.unhandledexception.
} }
} }
} }

View File

@@ -0,0 +1,54 @@
using Ghost.Data.Models;
using Ghost.Editor.Contracts;
using Ghost.Editor.View.Windows;
using System.Threading.Tasks;
namespace Ghost.Editor.AppStates;
internal class EditorState : IAppState<StateKey>
{
private EngineEditorWindow? _window;
public StateKey StateKy => StateKey.EngineEditor;
public Task OnExitingAsync()
{
if (App.Window == _window)
{
App.Window = null;
}
return Task.CompletedTask;
}
public Task OnEnteringAsync(object? parameter)
{
if (parameter is not ProjectMetadata metadata)
{
throw new System.ArgumentException("Parameter must be of type ProjectMetadata.", nameof(parameter));
}
_window = App.GetService<EngineEditorWindow>();
_window.ViewModel.CurrentProject = metadata;
_window.Activate();
App.Window = _window;
return Task.CompletedTask;
}
public Task OnExitedAsync()
{
if (App.Window == _window)
{
App.Window = null;
}
_window?.Close();
_window = null;
return Task.CompletedTask;
}
public Task OnEnteredAsync(object? parameter)
{
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,47 @@
using Ghost.Editor.Contracts;
using Ghost.Editor.View.Windows;
using System.Threading.Tasks;
namespace Ghost.Editor.AppStates;
internal class LandingState : IAppState<StateKey>
{
private LandingWindow? _window;
public StateKey StateKy => StateKey.Landing;
public Task OnExitingAsync()
{
if (App.Window == _window)
{
App.Window = null;
}
return Task.CompletedTask;
}
public Task OnEnteringAsync(object? parameter)
{
_window = App.GetService<LandingWindow>();
App.Window = _window;
_window.Activate();
return Task.CompletedTask;
}
public Task OnExitedAsync()
{
if (App.Window == _window)
{
App.Window = null;
}
_window?.Close();
_window = null;
return Task.CompletedTask;
}
public Task OnEnteredAsync(object? parameter)
{
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,8 @@
namespace Ghost.Editor.AppStates;
internal enum StateKey
{
None,
Landing,
EngineEditor,
}

View File

@@ -0,0 +1,33 @@
using System.Threading.Tasks;
namespace Ghost.Editor.Contracts;
internal interface IAppState<Key>
{
public Key StateKy
{
get;
}
/// <summary>
/// Called when exiting the state.
/// </summary>
public Task OnExitingAsync();
/// <summary>
/// Called when entering the state, right after OnEnteringAsync.
/// <paramref name="parameter">can be used to pass data into the state, such as a project to load.</summary>
/// </summary>
public Task OnEnteringAsync(object? parameter);
/// <summary>
/// Called when exiting the state, specifically for pose transitions.
/// </summary>
public Task OnExitedAsync();
/// <summary>
/// Called when entered the state, specifically after the state has been fully initialized and is ready for interaction.
/// </summary>
/// <param name="parameter">can be used to pass data into the state, such as a project to load.</param>
public Task OnEnteredAsync(object? parameter);
}

View File

@@ -0,0 +1,14 @@
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Contracts;
internal interface INotificationService
{
public void ShowNotification(string? message, InfoBarSeverity severity, int duration = 5, string? title = null);
}
internal interface INotificationService<T> : INotificationService
{
public void Initialize(T notificationQueue);
public void ClearQueueReference();
}

View File

@@ -2,5 +2,6 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Controls/BasicInput/PropertyField.xaml" /> <ResourceDictionary Source="/Controls/BasicInput/PropertyField.xaml" />
<ResourceDictionary Source="/Controls/Internal/InternalControls.xaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -0,0 +1,24 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Controls.Internal;
internal sealed partial class InspectorView : ContentControl
{
public UIElement? Header
{
get => (UIElement)GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
}
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
nameof(Header),
typeof(UIElement),
typeof(InspectorView),
new PropertyMetadata(null));
public InspectorView()
{
DefaultStyleKey = typeof(InspectorView);
}
}

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Controls.Internal">
<Style TargetType="local:InspectorView">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:InspectorView">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header -->
<Grid Grid.Row="0">
<ContentPresenter
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}" />
</Grid>
<!-- Content -->
<Grid Grid.Row="1">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<ContentPresenter
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}" />
</ScrollViewer>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries />
</ResourceDictionary>

View File

@@ -42,6 +42,12 @@
<None Remove="Assets\Icon.targetsize-48_altform-unplated.png" /> <None Remove="Assets\Icon.targetsize-48_altform-unplated.png" />
<None Remove="Controls\BasicInput\PropertyField.xaml" /> <None Remove="Controls\BasicInput\PropertyField.xaml" />
<None Remove="Controls\EditorControls.xaml" /> <None Remove="Controls\EditorControls.xaml" />
<None Remove="Controls\Internal\InspectorView.xaml" />
<None Remove="Controls\Internal\InternalControls.xaml" />
<None Remove="Themes\Dark.xaml" />
<None Remove="Themes\Light.xaml" />
<None Remove="View\Pages\EngineEditor\ConsolePage.xaml" />
<None Remove="View\Pages\EngineEditor\ProjectPage.xaml" />
<None Remove="View\Pages\Landing\CreateProjectPage.xaml" /> <None Remove="View\Pages\Landing\CreateProjectPage.xaml" />
<None Remove="View\Pages\Landing\OpenProjectPage.xaml" /> <None Remove="View\Pages\Landing\OpenProjectPage.xaml" />
<None Remove="View\Windows\EngineEditorWindow.xaml" /> <None Remove="View\Windows\EngineEditorWindow.xaml" />
@@ -69,12 +75,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.1.240916" /> <PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.250129-preview2" /> <PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" /> <PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.250402" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" /> <PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.1742" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250310001" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
<PackageReference Include="System.Private.Uri" Version="4.3.2" /> <PackageReference Include="System.Private.Uri" Version="4.3.2" />
<PackageReference Include="WinUIEx" Version="2.5.1" /> <PackageReference Include="WinUIEx" Version="2.5.1" />
</ItemGroup> </ItemGroup>
@@ -98,10 +105,43 @@
</Page> </Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Controls\Internal\" /> <Folder Include="Helpers\Converters\" />
<Folder Include="Models\" />
<Folder Include="Resources\" /> <Folder Include="Resources\" />
<Folder Include="Services\" /> </ItemGroup>
<ItemGroup>
<Reference Include="Misaki.HighPerformance.Unsafe">
<HintPath>..\..\Class\Misaki.HighPerformance\Misaki.HighPerformance.Unsafe\bin\Release\net9.0\Misaki.HighPerformance.Unsafe.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Page Update="View\Pages\EngineEditor\ProjectPage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="View\Pages\EngineEditor\ConsolePage.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Themes\Light.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Themes\Dark.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\Internal\InternalControls.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Controls\Internal\InspectorView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="View\Windows\EngineEditorWindow.xaml"> <Page Update="View\Windows\EngineEditorWindow.xaml">

View File

@@ -0,0 +1,30 @@
using Ghost.Entities;
using System;
using System.Linq;
namespace Ghost.Editor.Helpers;
public static class ComponentTypeCache
{
private static readonly Type?[][] _componentTypes;
static ComponentTypeCache()
{
_componentTypes = new Type[World.WorldCount][];
for (var i = 0; i < World.WorldCount; i++)
{
var world = World.GetWorld(i);
var typeHandles = world.ComponentStorage.ComponentPools.Keys;
_componentTypes[i] = typeHandles.Select(handle => Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(handle))).ToArray();
}
}
public static Type?[] GetComponentTypes(int worldIndex)
{
if (worldIndex < 0 || worldIndex >= _componentTypes.Length)
{
throw new ArgumentOutOfRangeException(nameof(worldIndex), "Invalid world index.");
}
return _componentTypes[worldIndex];
}
}

View File

@@ -0,0 +1,17 @@
using Microsoft.UI.Xaml.Data;
using System;
namespace Ghost.Editor.Helpers.Converters;
public partial class GetDirectoryNameConverter : IValueConverter
{
public object? Convert(object value, Type targetType, object parameter, string language)
{
return value is string path ? System.IO.Path.GetDirectoryName(path) : null;
}
public object? ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -1,4 +1,5 @@
using Ghost.Editor.View.Pages.Landing; using Ghost.Data.Services;
using Ghost.Editor.View.Pages.Landing;
using Ghost.Editor.View.Windows; using Ghost.Editor.View.Windows;
using Ghost.Editor.ViewModel.Pages.Landing; using Ghost.Editor.ViewModel.Pages.Landing;
using Ghost.Editor.ViewModel.Windows; using Ghost.Editor.ViewModel.Windows;
@@ -9,7 +10,7 @@ namespace Ghost.Editor.Helpers;
internal static partial class HostHelper internal static partial class HostHelper
{ {
public static void SetupPageService(HostBuilderContext context, IServiceCollection services) public static void AddLandingScope(HostBuilderContext context, IServiceCollection services)
{ {
services.AddSingleton<LandingWindow>(); services.AddSingleton<LandingWindow>();
@@ -18,6 +19,11 @@ internal static partial class HostHelper
services.AddTransient<OpenProjectPage>(); services.AddTransient<OpenProjectPage>();
services.AddTransient<ProjectService>();
}
public static void AddEngineScope(HostBuilderContext context, IServiceCollection services)
{
services.AddSingleton<EngineEditorWindow>(); services.AddSingleton<EngineEditorWindow>();
services.AddSingleton<EngineEditorViewModel>(); services.AddSingleton<EngineEditorViewModel>();
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Windows.Storage; using Windows.Storage;
using Windows.Storage.Pickers; using Windows.Storage.Pickers;
@@ -11,7 +12,7 @@ public static class SystemUtilities
public static async Task<StorageFolder?> OpenFolderPickerAsync(PickerLocationId startLocation = PickerLocationId.DocumentsLibrary, string settingsIdentifier = "") public static async Task<StorageFolder?> OpenFolderPickerAsync(PickerLocationId startLocation = PickerLocationId.DocumentsLibrary, string settingsIdentifier = "")
{ {
var openPicker = new FolderPicker(); var openPicker = new FolderPicker();
var hWnd = WindowNative.GetWindowHandle(App.GetWindow()); var hWnd = WindowNative.GetWindowHandle(App.Window);
InitializeWithWindow.Initialize(openPicker, hWnd); InitializeWithWindow.Initialize(openPicker, hWnd);
openPicker.SuggestedStartLocation = startLocation; openPicker.SuggestedStartLocation = startLocation;
@@ -21,4 +22,21 @@ public static class SystemUtilities
var folder = await openPicker.PickSingleFolderAsync(); var folder = await openPicker.PickSingleFolderAsync();
return folder; return folder;
} }
public static async Task<StorageFile?> OpenFilePickerAsync(PickerLocationId startLocation = PickerLocationId.DocumentsLibrary, string settingsIdentifier = "", params IEnumerable<string> filter)
{
var openPicker = new FileOpenPicker();
var hWnd = WindowNative.GetWindowHandle(App.Window);
InitializeWithWindow.Initialize(openPicker, hWnd);
openPicker.SuggestedStartLocation = startLocation;
openPicker.SettingsIdentifier = settingsIdentifier;
foreach (var fileType in filter)
{
openPicker.FileTypeFilter.Add(fileType);
}
var file = await openPicker.PickSingleFileAsync();
return file;
}
} }

View File

@@ -0,0 +1,269 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Entities;
using Ghost.Entities.Helpers;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Ghost.Editor.Models;
public partial class GameObject : ObservableObject
{
[ObservableProperty]
public partial bool IsActive
{
get;
set;
}
[ObservableProperty]
public partial bool IsActiveHierarchy
{
get;
set;
}
public Entity Entity
{
get;
}
public Scene Scene
{
get;
internal set;
}
public GameObject? Parent
{
get;
internal set;
}
public string Name
{
get;
set;
}
[ObservableProperty]
public partial ObservableCollection<IComponentData>? Components
{
get;
private set;
}
[ObservableProperty]
public partial IEnumerable<ScriptComponent>? ScriptComponents
{
get;
private set;
}
[ObservableProperty]
public partial ObservableCollection<GameObject>? Children
{
get;
private set;
}
public GameObject(Scene scene, string name)
{
Entity = scene.World.EntityManager.CreateEntity();
Scene = scene;
Name = name;
IsActive = true;
}
partial void OnIsActiveChanged(bool value)
{
IsActiveHierarchy = value && (Parent?.IsActiveHierarchy ?? true);
HandleActiveStateChanged();
if (Children != null)
{
foreach (var child in Children)
{
child.IsActiveHierarchy = value && IsActiveHierarchy;
}
}
}
partial void OnIsActiveHierarchyChanged(bool value)
{
HandleActiveStateChanged();
}
private void HandleActiveStateChanged()
{
if (IsActive && IsActiveHierarchy)
{
OnEnable();
}
else
{
OnDisable();
}
}
internal void OnEnable()
{
if (ScriptComponents != null)
{
foreach (var script in ScriptComponents)
{
if (!script.Enable)
{
continue;
}
script.OnEnable();
}
}
}
internal void OnDisable()
{
if (ScriptComponents != null)
{
foreach (var script in ScriptComponents)
{
if (!script.Enable)
{
continue;
}
script.OnDisable();
}
}
}
public void AddChild(GameObject child)
{
if (child.Scene != Scene)
{
throw new InvalidOperationException("Child GameObject must belong to the same Scene.");
}
Children ??= new();
Children.Add(child);
child.Parent = this;
}
public bool RemoveChild(GameObject child)
{
if (Children is null)
{
return false;
}
if (!Children.Remove(child))
{
return false;
}
child.Parent = null;
return true;
}
public void Destroy()
{
if (ScriptComponents != null)
{
foreach (var component in ScriptComponents)
{
if (!component.Enable)
{
continue;
}
component.OnDestroy();
}
}
if (Children != null)
{
foreach (var child in Children)
{
child.Destroy();
}
Children.Clear();
}
Parent?.Children?.Remove(this);
Entity.Destroy();
}
}
public partial class GameObject
{
// TODO: Implement a more efficient synchronization mechanism for components
internal void SyncComponents()
{
foreach (var (typeHandle, mask) in Scene.World.ComponentStorage.ComponentEntityMasks)
{
if (!mask.IsSet(Entity.ID))
{
continue;
}
var pool = Scene.World.ComponentStorage.ComponentPools[typeHandle];
}
}
internal void SyncScripts()
{
var scriptsPool = Scene.World.ComponentStorage.ScriptComponentPool.ScriptComponents;
if (scriptsPool == null)
{
return;
}
scriptsPool.TryGetValue(Entity, out var scripts);
ScriptComponents = scripts;
}
public void AddComponent<T>(T component)
where T : struct, IComponentData
{
Entity.AddComponent<T>(component);
SyncComponents();
}
public bool RemoveComponent<T>()
where T : struct, IComponentData
{
var result = Entity.RemoveComponent<T>();
SyncComponents();
return result;
}
public void AddScript<T>()
where T : ScriptComponent, new()
{
Entity.AddScript<T>();
SyncScripts();
}
public void AddScript(Type type)
{
Entity.AddScript(type);
SyncScripts();
}
public bool RemoveScript<T>()
where T : ScriptComponent
{
var result = Scene.World.EntityManager.RemoveScript<T>(Entity);
SyncScripts();
return result;
}
public bool RemoveScriptAt(int index)
{
var result = Scene.World.EntityManager.RemoveScriptAt(Entity, index);
SyncScripts();
return result;
}
}

View File

@@ -1,10 +1,15 @@
namespace Ghost.Engine.Models; using Ghost.Entities;
using System.Collections.Generic;
namespace Ghost.Editor.Models;
public class Scene public class Scene
{ {
private readonly HashSet<GameObject> _rootObjects = new(); private readonly HashSet<GameObject> _rootObjects = new();
private readonly World _world = World.Create();
public IEnumerable<GameObject> RootObjects => _rootObjects; public IEnumerable<GameObject> RootObjects => _rootObjects;
public World World => _world;
internal Scene() internal Scene()
{ {
@@ -14,7 +19,7 @@ public class Scene
{ {
foreach (var gameObject in _rootObjects) foreach (var gameObject in _rootObjects)
{ {
gameObject.Start(); gameObject.OnEnable();
} }
} }
@@ -22,6 +27,7 @@ public class Scene
{ {
foreach (var gameObject in _rootObjects) foreach (var gameObject in _rootObjects)
{ {
gameObject.OnDisable();
gameObject.Destroy(); gameObject.Destroy();
} }

View File

@@ -0,0 +1,35 @@
using Ghost.Editor.AppStates;
using Ghost.Editor.Contracts;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Ghost.Editor.Services;
internal class AppStateService(params IEnumerable<IAppState<StateKey>> states)
{
private readonly Dictionary<StateKey, IAppState<StateKey>> _states = states.ToDictionary(s => s.StateKy, s => s);
private IAppState<StateKey>? _current;
public async Task TransitionToAsync(StateKey stateKey, object? parameter = null)
{
var previous = _current;
var next = _states[stateKey];
if (previous != null)
{
await previous.OnExitingAsync();
}
await next.OnEnteringAsync(parameter);
if (previous != null)
{
await previous.OnExitedAsync();
}
await next.OnEnteredAsync(parameter);
_current = next;
}
}

View File

@@ -0,0 +1,50 @@
using CommunityToolkit.WinUI.Behaviors;
using Microsoft.UI.Xaml.Controls;
using System;
namespace Ghost.Editor.Services;
public class StackedNotificationService
{
private InfoBar? _infoBar;
private StackedNotificationsBehavior? _notificationQueue;
internal void SetReference(InfoBar infoBar, StackedNotificationsBehavior notificationQueue)
{
_infoBar = infoBar;
_notificationQueue = notificationQueue;
}
internal void ClearReference()
{
if (_infoBar != null)
{
_infoBar.IsOpen = false;
}
_infoBar = null;
_notificationQueue = null;
}
public void ShowNotification(string? message, InfoBarSeverity severity, int duration = 5, string? title = null)
{
if (string.IsNullOrWhiteSpace(message))
{
return;
}
var notification = new Notification
{
Message = message,
Severity = severity,
Duration = TimeSpan.FromSeconds(duration),
Title = title
};
ShowNotification(notification);
}
public void ShowNotification(Notification notification)
{
_notificationQueue?.Show(notification);
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark">
<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="ControlFillColorSecondaryBrush" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="ControlFillColorSecondaryBrush" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Ghost.Editor.View.Pages.EngineEditor.ConsolePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.Editor.View.Pages.EngineEditor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Toolbar -->
<Grid
Grid.Row="0"
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
BorderThickness="0,0,0,1">
<CommandBar Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}" DefaultLabelPosition="Collapsed">
<CommandBar.PrimaryCommands>
<AppBarButton Content="Clear" />
<AppBarSeparator />
<AppBarToggleButton Width="45">
<AppBarToggleButton.Icon>
<FontIcon Glyph="&#xF167;" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton Width="45">
<AppBarToggleButton.Icon>
<FontIcon Glyph="&#xE814;" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton Width="45">
<AppBarToggleButton.Icon>
<FontIcon Glyph="&#xEB90;" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
</CommandBar.PrimaryCommands>
<CommandBar.SecondaryCommands>
<AppBarToggleButton BorderThickness="0" Label="Clear On Play" />
<AppBarToggleButton BorderThickness="0" Label="Show Stack Trace" />
</CommandBar.SecondaryCommands>
</CommandBar>
</Grid>
<!-- Log Content -->
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<ListView Grid.Row="0" />
<Grid
Grid.Row="1"
Padding="4"
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
BorderThickness="0,1,0,0">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<TextBlock
IsTextSelectionEnabled="True"
Style="{StaticResource CaptionTextBlockStyle}"
Text="Test Log"
TextWrapping="Wrap" />
</ScrollViewer>
</Grid>
</Grid>
</Grid>
</Page>

View File

@@ -0,0 +1,11 @@
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.View.Pages.EngineEditor;
public sealed partial class ConsolePage : Page
{
public ConsolePage()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="Ghost.Editor.View.Pages.EngineEditor.ProjectPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Ghost.Editor.View.Pages.EngineEditor"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource LayerFillColorDefaultBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Folder Tree View -->
<Grid
Grid.Column="0"
BorderBrush="{ThemeResource CardStrokeColorDefaultSolid}"
BorderThickness="0,0,1,0">
<TreeView
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto">
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="Padding" Value="4,2" />
<Setter Property="Margin" Value="0,0,0,2" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
</Grid>
<!-- Files -->
<ScrollViewer
Grid.Column="1"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<GridView HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<GridView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultGridViewItemStyle}" TargetType="GridViewItem">
<Setter Property="Margin" Value="5,5,5,5" />
</Style>
</GridView.ItemContainerStyle>
<GridView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="0.2*" />
</Grid.RowDefinitions>
<ImageIcon
Grid.Row="0"
Width="24"
Height="24" />
<TextBlock
Grid.Row="1"
Margin="8,0"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" />
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</ScrollViewer>
</Grid>
</Page>

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace Ghost.Editor.View.Pages.EngineEditor;
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class ProjectPage : Page
{
public ProjectPage()
{
InitializeComponent();
}
}

View File

@@ -8,6 +8,7 @@
xmlns:editor="using:Ghost.Editor.Controls" xmlns:editor="using:Ghost.Editor.Controls"
xmlns:local="using:Ghost.Editor.View.Pages.Landing" xmlns:local="using:Ghost.Editor.View.Pages.Landing"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
NavigationCacheMode="Enabled"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid> <Grid>
@@ -46,7 +47,7 @@
Width="24" Width="24"
Height="24"> Height="24">
<ImageIcon.Source> <ImageIcon.Source>
<BitmapImage UriSource="{x:Bind GetIconURI(), Mode=OneWay}" /> <BitmapImage UriSource="{x:Bind GetIconURI()}" />
</ImageIcon.Source> </ImageIcon.Source>
</ImageIcon> </ImageIcon>
<TextBlock <TextBlock
@@ -76,18 +77,18 @@
<Grid Grid.Row="0" CornerRadius="4"> <Grid Grid.Row="0" CornerRadius="4">
<Image VerticalAlignment="Center" Stretch="UniformToFill"> <Image VerticalAlignment="Center" Stretch="UniformToFill">
<Image.Source> <Image.Source>
<BitmapImage UriSource="{x:Bind ViewModel.SelectedTemplate.GetPreviewURI(), Mode=OneWay}" /> <BitmapImage UriSource="{x:Bind ViewModel.SelectedTemplate.Value.GetPreviewURI(), Mode=OneWay}" />
</Image.Source> </Image.Source>
</Image> </Image>
<Grid <Grid
HorizontalAlignment="Stretch" MaxHeight="100"
VerticalAlignment="Stretch" VerticalAlignment="Bottom"
Background="{ThemeResource CircleElevationBorderBrush}"> Background="{ThemeResource ControlOnImageFillColorDefaultBrush}">
<TextBlock <TextBlock
Margin="16" Margin="16"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Foreground="{ThemeResource TextOnAccentFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorTertiaryBrush}"
Text="{x:Bind ViewModel.SelectedTemplate.Info.Description, Mode=OneWay}" /> Text="{x:Bind ViewModel.SelectedTemplate.Value.Info.Description, Mode=OneWay}" />
</Grid> </Grid>
</Grid> </Grid>
@@ -95,7 +96,7 @@
<TextBlock <TextBlock
Margin="0,16,0,8" Margin="0,16,0,8"
Style="{StaticResource TitleTextBlockStyle}" Style="{StaticResource TitleTextBlockStyle}"
Text="{x:Bind ViewModel.SelectedTemplate.Info.Name, Mode=OneWay}" /> Text="{x:Bind ViewModel.SelectedTemplate.Value.Info.Name, Mode=OneWay}" />
<TextBlock <TextBlock
Margin="0,8,0,16" Margin="0,8,0,16"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}"

View File

@@ -3,106 +3,162 @@
x:Class="Ghost.Editor.View.Pages.Landing.OpenProjectPage" x:Class="Ghost.Editor.View.Pages.Landing.OpenProjectPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:Ghost.Editor.Helpers.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:data="using:Ghost.Data.Models" xmlns:data="using:Ghost.Data.Models"
xmlns:local="using:Ghost.Editor.View.Pages.Landing" xmlns:local="using:Ghost.Editor.View.Pages.Landing"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
NavigationCacheMode="Enabled"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid> <Page.Resources>
<converters:GetDirectoryNameConverter x:Key="DirNameConverter" />
</Page.Resources>
<Grid x:Name="MainContainer">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="16,8,16,16"> <Grid Grid.Row="0" Margin="16,4">
<TextBlock
VerticalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="Projects" />
<AutoSuggestBox
Width="300"
HorizontalAlignment="Right"
PlaceholderText="Search project by name"
QueryIcon="Find" />
</Grid>
<!-- Header for the ListView -->
<Grid Grid.Row="1" Margin="28,16,45,8">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="200" /> <ColumnDefinition Width="200" />
<ColumnDefinition Width="200" /> <ColumnDefinition Width="165" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock <TextBlock
Grid.Column="0" Grid.Column="0"
Margin="12,0,0,0" Style="{StaticResource CaptionTextBlockStyle}"
Style="{StaticResource BodyStrongTextBlockStyle}" Text="NAME" />
Text="Name" />
<TextBlock <TextBlock
Grid.Column="1" Grid.Column="1"
Style="{StaticResource BodyStrongTextBlockStyle}" HorizontalAlignment="Right"
Text="Last Open" /> Style="{StaticResource CaptionTextBlockStyle}"
Text="LAST OPEN" />
<TextBlock <TextBlock
Grid.Column="2" Grid.Column="2"
Style="{StaticResource BodyStrongTextBlockStyle}" HorizontalAlignment="Right"
Text="Engine Version" /> Style="{StaticResource CaptionTextBlockStyle}"
Text="ENGINE VERSION" />
</Grid> </Grid>
<ListView <!-- Project ListView -->
x:Name="ProjectListView" <Grid
Grid.Row="1" Grid.Row="2"
Padding="8" Padding="8"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" AllowDrop="True"
CornerRadius="{StaticResource OverlayCornerRadius}" DragEnter="ProjectContainer_DragEnter"
IsItemClickEnabled="True" DragLeave="ProjectContainer_DragLeave"
ItemClick="ListView_ItemClick" DragOver="ProjectContainer_DragOver"
ItemsSource="{x:Bind projects}" Drop="ProjectContainer_Drop">
SelectionMode="None" <ListView
Visibility="Visible"> Padding="4,8"
<ListView.ItemTemplate> Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
<DataTemplate x:DataType="data:ProjectInfo"> CornerRadius="{StaticResource OverlayCornerRadius}"
<Grid Padding="4,8"> IsItemClickEnabled="True"
<Grid.ColumnDefinitions> ItemClick="ListView_ItemClick"
<ColumnDefinition Width="*" /> ItemsSource="{x:Bind projects}"
<ColumnDefinition Width="200" /> SelectionMode="None">
<ColumnDefinition Width="100" /> <ListView.ItemTemplate>
<ColumnDefinition Width="100" /> <DataTemplate x:DataType="data:ProjectMetadataInfo">
</Grid.ColumnDefinitions> <Grid Height="64" Padding="4,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="200" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="65" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0"> <Grid Grid.Column="0" VerticalAlignment="Center">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
VerticalAlignment="Center"
FontSize="16"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{x:Bind Metadata.Name}" />
<TextBlock
Grid.Row="1"
Margin="0,4,0,0"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Path, Converter={StaticResource DirNameConverter}}" />
</Grid>
<TextBlock <TextBlock
Grid.Row="0" Grid.Column="1"
Style="{StaticResource SubtitleTextBlockStyle}" Margin="16,4"
Text="{x:Bind Name}" /> HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Bind Metadata.LastOpened}" />
<TextBlock <TextBlock
Grid.Row="1" Grid.Column="2"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Margin="16,4"
Style="{StaticResource CaptionTextBlockStyle}" HorizontalAlignment="Right"
Text="{x:Bind Path}" /> VerticalAlignment="Center"
Text="{x:Bind Metadata.EngineVersion}" />
<Button
Grid.Column="3"
HorizontalAlignment="Right"
Background="Transparent"
BorderThickness="0">
<FontIcon Glyph="&#xE712;" />
</Button>
</Grid> </Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<TextBlock <!-- Drag Visual -->
Grid.Column="1" <Grid
Margin="16,4" x:Name="DragVisual"
VerticalAlignment="Center" HorizontalAlignment="Stretch"
Text="{x:Bind LastOpened}" /> VerticalAlignment="Stretch"
<TextBlock Background="{ThemeResource CardStrokeColorDefaultBrush}"
Grid.Column="2" BorderBrush="{ThemeResource ControlStrongStrokeColorDefaultBrush}"
Margin="16,4" BorderThickness="2"
VerticalAlignment="Center" CornerRadius="{StaticResource OverlayCornerRadius}"
Text="{x:Bind EngineVersion}" /> Visibility="Collapsed">
<Button <TextBlock
Grid.Column="3" HorizontalAlignment="Center"
HorizontalAlignment="Right" VerticalAlignment="Center"
Background="Transparent" Foreground="{ThemeResource TextFillColorSecondaryBrush}"
BorderThickness="0"> Style="{StaticResource TitleTextBlockStyle}"
<FontIcon Glyph="&#xE712;" /> Text="Drage Project Folder Here" />
</Button> </Grid>
</Grid> </Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<TextBlock <!-- Empty Place Holder -->
x:Name="PlaceHolderText" <Grid
HorizontalAlignment="Center" x:Name="EmptyPlaceHolder"
VerticalAlignment="Center" Grid.Row="2"
Style="{StaticResource TitleTextBlockStyle}" Visibility="Collapsed">
Text="No projects found" <TextBlock
Visibility="Collapsed" /> HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource TitleTextBlockStyle}"
Text="No projects found" />
</Grid>
</Grid> </Grid>
</Page> </Page>

View File

@@ -1,56 +1,133 @@
using Ghost.Data.Models; using Ghost.Data.Models;
using Ghost.Data.Services; using Ghost.Data.Services;
using Ghost.Editor.View.Windows; using Ghost.Editor.AppStates;
using Ghost.Editor.Services;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation; using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq;
// To learn more about WinUI, the WinUI project structure, using Windows.ApplicationModel.DataTransfer;
// and more about our project templates, see: http://aka.ms/winui-project-info. using Windows.Storage;
namespace Ghost.Editor.View.Pages.Landing; namespace Ghost.Editor.View.Pages.Landing;
internal sealed partial class OpenProjectPage : Page internal sealed partial class OpenProjectPage : Page
{ {
private readonly ProjectService _projectService; private readonly ProjectService _projectService;
private readonly StackedNotificationService _notificationService;
private readonly AppStateService _stateService;
public readonly ObservableCollection<ProjectInfo> projects = new(); public readonly ObservableCollection<ProjectMetadataInfo> projects = new();
public OpenProjectPage() public OpenProjectPage()
{ {
_notificationService = App.GetService<StackedNotificationService>();
_projectService = App.GetService<ProjectService>(); _projectService = App.GetService<ProjectService>();
_stateService = App.GetService<AppStateService>();
InitializeComponent(); InitializeComponent();
} }
private void UpdateEmptyPlaceHolderVisibility()
{
EmptyPlaceHolder.Visibility = projects.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
}
protected override async void OnNavigatedTo(NavigationEventArgs e) protected override async void OnNavigatedTo(NavigationEventArgs e)
{ {
await foreach (var project in _projectService.LoadAllProjectAsync()) base.OnNavigatedTo(e);
projects.Clear();
await foreach (var projectInfo in _projectService.LoadAllProjectAsync())
{ {
projects.Add(project); var metadata = await ProjectService.LoadMetadataAsync(projectInfo.MetadataPath);
if (metadata == null)
{
continue;
}
projects.Add(new(projectInfo.MetadataPath, metadata));
} }
if (projects.Count == 0) UpdateEmptyPlaceHolderVisibility();
}
private void ProjectContainer_DragEnter(object sender, DragEventArgs e)
{
DragVisual.Visibility = Visibility.Visible;
EmptyPlaceHolder.Visibility = Visibility.Collapsed;
}
private void ProjectContainer_DragLeave(object sender, DragEventArgs e)
{
DragVisual.Visibility = Visibility.Collapsed;
UpdateEmptyPlaceHolderVisibility();
}
private void ProjectContainer_DragOver(object sender, DragEventArgs e)
{
if (e.DataView.Contains(StandardDataFormats.StorageItems))
{ {
PlaceHolderText.Visibility = Visibility.Visible; e.AcceptedOperation = DataPackageOperation.Link;
ProjectListView.Visibility = Visibility.Collapsed;
} }
else
{
e.AcceptedOperation = DataPackageOperation.None;
}
}
private async void ProjectContainer_Drop(object sender, DragEventArgs e)
{
var errorMessage = string.Empty;
if (e.DataView.Contains(StandardDataFormats.StorageItems))
{
var items = await e.DataView.GetStorageItemsAsync();
var rootFolder = items.OfType<StorageFolder>().FirstOrDefault();
if (rootFolder != null)
{
var result = await _projectService.AddProjectFromDirectoryAsync(rootFolder.Path);
if (result.success)
{
projects.Add(result.data);
DragVisual.Visibility = Visibility.Collapsed;
goto CloseDropPanel;
}
else
{
errorMessage = result.message;
}
}
}
else
{
errorMessage = "Unsupported data format. Please drop a folder containing a project.";
}
_notificationService.ShowNotification(errorMessage, InfoBarSeverity.Error);
CloseDropPanel:
DragVisual.Visibility = Visibility.Collapsed;
UpdateEmptyPlaceHolderVisibility();
} }
private async void ListView_ItemClick(object sender, ItemClickEventArgs e) private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
{ {
if (e.ClickedItem is not ProjectInfo project) if (e.ClickedItem is not ProjectMetadataInfo project)
{ {
return; return;
} }
if (EngineEditorWindow.TryLoadProject(project)) try
{ {
App.GetService<LandingWindow>().Close(); project.Metadata.LastOpened = DateTime.Now;
await ProjectService.CreateMetadataFileAsync(project.Path, project.Metadata);
project.LastOpened = System.DateTime.Now; await _stateService.TransitionToAsync(StateKey.EngineEditor, project.Metadata);
await _projectService.UpdateProjectAsync(project); }
catch (Exception exp)
{
_notificationService.ShowNotification($"Failed to load project: {exp.Message}", InfoBarSeverity.Error);
} }
} }
} }

View File

@@ -5,16 +5,18 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls" xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ee="using:Ghost.Editor.View.Pages.EngineEditor"
xmlns:local="using:Ghost.Editor.View.Windows" xmlns:local="using:Ghost.Editor.View.Windows"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:winex="using:WinUIEx" xmlns:winex="using:WinUIEx"
Activated="WindowEx_Activated"
mc:Ignorable="d"> mc:Ignorable="d">
<Window.SystemBackdrop> <Window.SystemBackdrop>
<MicaBackdrop /> <MicaBackdrop />
</Window.SystemBackdrop> </Window.SystemBackdrop>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@@ -41,7 +43,7 @@
Margin="8,0,0,0" Margin="8,0,0,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}" Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.CurrentProject.Name}" /> Text="{x:Bind ViewModel.CurrentProject.Name, Mode=OneWay}" />
</StackPanel> </StackPanel>
<!-- Toolbar --> <!-- Toolbar -->
@@ -85,32 +87,41 @@
Grid.Column="0" Grid.Column="0"
Width="350" Width="350"
Background="Aquamarine" /> Background="Aquamarine" />
<Grid Grid.Column="1" /> <Grid Grid.Column="1">
<Image Source="C:\Users\Misaki\OneDrive\Pictures\Screenshots\Screenshot 2024-07-20 021657.png" Stretch="UniformToFill" />
</Grid>
<Grid <Grid
Grid.Column="2" Grid.Column="2"
Width="350" Width="350"
Background="Bisque" /> Background="Bisque" />
</Grid> </Grid>
<Grid Grid.Row="1" Height="350"> <TabView
<Grid.ColumnDefinitions> Grid.Row="1"
<ColumnDefinition Width="*" /> Height="350"
<ColumnDefinition Width="Auto" /> TabWidthMode="Compact">
</Grid.ColumnDefinitions> <TabView.TabItems>
<TabViewItem Header="Project">
<Grid Grid.Column="0" Background="AliceBlue" /> <TabViewItem.IconSource>
<Grid <FontIconSource Glyph="&#xE8B7;" />
Grid.Column="1" </TabViewItem.IconSource>
Width="500" <ee:ProjectPage />
Background="HotPink" /> </TabViewItem>
</Grid> <TabViewItem Header="Console">
<TabViewItem.IconSource>
<FontIconSource Glyph="&#xE756;" />
</TabViewItem.IconSource>
<ee:ConsolePage />
</TabViewItem>
</TabView.TabItems>
</TabView>
</Grid> </Grid>
<!-- Status Bar --> <!-- Status Bar -->
<Grid <Grid
Grid.Row="3" Grid.Row="3"
Height="25" Height="25"
Background="{ThemeResource SmokeFillColorDefaultBrush}"> Background="{ThemeResource SolidBackgroundFillColorBaseAltBrush}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />

View File

@@ -1,5 +1,4 @@
using Ghost.Data.Models; using Ghost.Data.Resources;
using Ghost.Data.Resources;
using Ghost.Editor.ViewModel.Windows; using Ghost.Editor.ViewModel.Windows;
using Ghost.Engine.Resources; using Ghost.Engine.Resources;
using WinUIEx; using WinUIEx;
@@ -22,7 +21,7 @@ internal sealed partial class EngineEditorWindow : WindowEx
{ {
ViewModel = App.GetService<EngineEditorViewModel>(); ViewModel = App.GetService<EngineEditorViewModel>();
AppWindow.SetIcon(AssetsPath.AppIconPath); AppWindow.SetIcon(AssetsPath.s_appIconPath);
Title = EngineData.ENGINE_NAME; Title = EngineData.ENGINE_NAME;
ExtendsContentIntoTitleBar = true; ExtendsContentIntoTitleBar = true;
@@ -31,23 +30,8 @@ internal sealed partial class EngineEditorWindow : WindowEx
this.CenterOnScreen(); this.CenterOnScreen();
} }
public static bool TryLoadProject(ProjectInfo project) private void WindowEx_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args)
{ {
try Bindings.Update();
{
var window = App.GetService<EngineEditorWindow>();
window.ViewModel.CurrentProject = project;
window.Activate();
window.Bindings.Update();
App.SetWindow(window);
return true;
}
catch (System.Exception)
{
return false;
}
} }
} }

View File

@@ -3,10 +3,14 @@
x:Class="Ghost.Editor.View.Windows.LandingWindow" x:Class="Ghost.Editor.View.Windows.LandingWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:local="using:Ghost.Editor.View.Windows" xmlns:local="using:Ghost.Editor.View.Windows"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:winex="using:WinUIEx" xmlns:winex="using:WinUIEx"
Activated="WindowEx_Activated"
Closed="WindowEx_Closed"
IsResizable="False" IsResizable="False"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -55,8 +59,19 @@
Grid.Row="1" Grid.Row="1"
Padding="8" Padding="8"
CacheMode="BitmapCache" CacheMode="BitmapCache"
CacheSize="10" CacheSize="10" />
IsNavigationStackEnabled="False" /> </Grid>
<Grid Grid.Row="1" Padding="16">
<InfoBar
x:Name="InfoBar"
HorizontalAlignment="Right"
VerticalAlignment="Bottom">
<interactivity:Interaction.Behaviors>
<behaviors:StackedNotificationsBehavior x:Name="NotificationQueue" />
</interactivity:Interaction.Behaviors>
</InfoBar>
</Grid> </Grid>
</Grid> </Grid>
</winex:WindowEx> </winex:WindowEx>

View File

@@ -1,6 +1,8 @@
using Ghost.Data.Resources; using Ghost.Data.Resources;
using Ghost.Editor.Services;
using Ghost.Editor.View.Pages.Landing; using Ghost.Editor.View.Pages.Landing;
using Ghost.Engine.Resources; using Ghost.Engine.Resources;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Animation; using Microsoft.UI.Xaml.Media.Animation;
using WinUIEx; using WinUIEx;
@@ -9,11 +11,13 @@ namespace Ghost.Editor.View.Windows;
internal sealed partial class LandingWindow : WindowEx internal sealed partial class LandingWindow : WindowEx
{ {
private IServiceScope? _landingScope;
private int _previousSelectedIndex; private int _previousSelectedIndex;
public LandingWindow() public LandingWindow()
{ {
AppWindow.SetIcon(AssetsPath.AppIconPath); AppWindow.SetIcon(AssetsPath.s_appIconPath);
Title = EngineData.ENGINE_NAME; Title = EngineData.ENGINE_NAME;
InitializeComponent(); InitializeComponent();
@@ -24,6 +28,19 @@ internal sealed partial class LandingWindow : WindowEx
ExtendsContentIntoTitleBar = true; ExtendsContentIntoTitleBar = true;
} }
private void WindowEx_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args)
{
_landingScope?.Dispose();
_landingScope = App.CreateScope();
App.GetService<StackedNotificationService>().SetReference(InfoBar, NotificationQueue);
}
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
{
_landingScope?.Dispose();
App.GetService<StackedNotificationService>().ClearReference();
}
private void SelectorBar_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs e) private void SelectorBar_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs e)
{ {
var selectedItem = sender.SelectedItem; var selectedItem = sender.SelectedItem;
@@ -37,7 +54,7 @@ internal sealed partial class LandingWindow : WindowEx
var slideNavigationTransitionEffect = currentSelectedIndex - _previousSelectedIndex > 0 ? var slideNavigationTransitionEffect = currentSelectedIndex - _previousSelectedIndex > 0 ?
SlideNavigationTransitionEffect.FromRight : SlideNavigationTransitionEffect.FromLeft; SlideNavigationTransitionEffect.FromRight : SlideNavigationTransitionEffect.FromLeft;
ContentFrame.Navigate(pageType, null, new SlideNavigationTransitionInfo() { Effect = slideNavigationTransitionEffect }); ContentFrame.Navigate(pageType, _landingScope, new SlideNavigationTransitionInfo() { Effect = slideNavigationTransitionEffect });
_previousSelectedIndex = currentSelectedIndex; _previousSelectedIndex = currentSelectedIndex;
} }

View File

@@ -0,0 +1,5 @@
namespace Ghost.Editor.ViewModel.Pages.EngineEditor;
internal class ProjectViewModel
{
}

View File

@@ -2,18 +2,20 @@
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Ghost.Data.Models; using Ghost.Data.Models;
using Ghost.Data.Services; using Ghost.Data.Services;
using Ghost.Editor.AppStates;
using Ghost.Editor.Contracts; using Ghost.Editor.Contracts;
using Ghost.Editor.Helpers; using Ghost.Editor.Helpers;
using Ghost.Editor.View.Windows; using Ghost.Editor.Services;
using Ghost.Engine.Resources;
using Microsoft.UI.Xaml.Controls;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Windows.ApplicationModel;
namespace Ghost.Editor.ViewModel.Pages.Landing; namespace Ghost.Editor.ViewModel.Pages.Landing;
internal partial class CreateProjectViewModel(ProjectService projectService) : ObservableRecipient, INavigationAware internal partial class CreateProjectViewModel(StackedNotificationService notificationService, ProjectService projectService, AppStateService stateService) : ObservableRecipient, INavigationAware
{ {
public ObservableCollection<TemplateData> templates = new(); public ObservableCollection<TemplateData> templates = new();
@@ -25,14 +27,14 @@ internal partial class CreateProjectViewModel(ProjectService projectService) : O
} }
[ObservableProperty] [ObservableProperty]
public partial string ProjectName public partial string? ProjectName
{ {
get; get;
set; set;
} }
[ObservableProperty] [ObservableProperty]
public partial string ProjectLocation public partial string? ProjectLocation
{ {
get; get;
set; set;
@@ -40,7 +42,8 @@ internal partial class CreateProjectViewModel(ProjectService projectService) : O
public async void OnNavigatedTo(object? parameter) public async void OnNavigatedTo(object? parameter)
{ {
await foreach (var (path, info) in projectService.GetProjectTemplatesAsync()) templates.Clear();
await foreach (var (path, info) in ProjectService.GetProjectTemplatesAsync())
{ {
templates.Add(new(path, info)); templates.Add(new(path, info));
} }
@@ -55,25 +58,39 @@ internal partial class CreateProjectViewModel(ProjectService projectService) : O
[RelayCommand] [RelayCommand]
private async Task SelectionProjectLocation() private async Task SelectionProjectLocation()
{ {
ProjectLocation = (await SystemUtilities.OpenFolderPickerAsync())?.Path ?? string.Empty; var folder = await SystemUtilities.OpenFolderPickerAsync();
if (folder != null)
{
ProjectLocation = folder.Path;
}
} }
[RelayCommand] [RelayCommand]
private async Task CreateProject() private async Task CreateProject()
{ {
if (string.IsNullOrWhiteSpace(ProjectName) || !Directory.Exists(ProjectLocation) || SelectedTemplate == null) if (string.IsNullOrWhiteSpace(ProjectName)
|| !Directory.Exists(ProjectLocation)
|| !SelectedTemplate.HasValue)
{ {
notificationService.ShowNotification("Incorrect project info", InfoBarSeverity.Error);
return; return;
} }
var projectPath = await projectService.CreateProjectAsync(ProjectName, ProjectLocation, SelectedTemplate.directory); var result = await projectService.CreateProjectAsync(ProjectName, ProjectLocation, EngineData.s_engineVersion, SelectedTemplate.Value.directory);
if (!result.success || result.data == null)
var packageVersion = Package.Current.Id.Version;
var newProject = await projectService.AddProjectAsync(ProjectName, projectPath, new System.Version(packageVersion.Major, packageVersion.Minor, packageVersion.Build));
if (EngineEditorWindow.TryLoadProject(newProject))
{ {
App.GetService<LandingWindow>().Close(); notificationService.ShowNotification(result.message, InfoBarSeverity.Error);
return;
}
var metadata = await ProjectService.LoadMetadataAsync(result.data.MetadataPath); // Metadata should not be null here if create project succeeded
try
{
await stateService.TransitionToAsync(StateKey.EngineEditor, metadata);
}
catch (System.Exception e)
{
notificationService.ShowNotification($"Failed to load project: {e.Message}", InfoBarSeverity.Error);
} }
} }
} }

View File

@@ -6,10 +6,10 @@ namespace Ghost.Editor.ViewModel.Windows;
internal partial class EngineEditorViewModel : ObservableRecipient internal partial class EngineEditorViewModel : ObservableRecipient
{ {
public string engineVersionDescriptor = $"{EngineData.ENGINE_NAME} - {EngineData.ENGINE_VERSION}"; public string engineVersionDescriptor = $"{EngineData.ENGINE_NAME} - {EngineData.s_engineVersion}";
[ObservableProperty] [ObservableProperty]
public partial ProjectInfo CurrentProject public partial ProjectMetadata CurrentProject
{ {
get; get;
set; set;

View File

@@ -1,190 +0,0 @@
using Ghost.Engine.Models;
using Ghost.Entities;
using System.ComponentModel;
namespace Ghost.Engine;
public unsafe class GameObject : INotifyPropertyChanged
{
private readonly Dictionary<Type, ScriptComponent> _components = new();
private readonly List<GameObject> _children = new();
public event PropertyChangedEventHandler? PropertyChanged;
public Entity Entity
{
get;
}
public Scene Scene
{
get;
internal set;
}
public GameObject? Parent
{
get;
internal set;
}
public string Name
{
get;
set;
}
public bool IsActive
{
get;
set;
}
public IEnumerable<ScriptComponent> Components => _components.Values;
public IEnumerable<GameObject> Children => _children;
public GameObject(Scene scene, string name)
{
// TODO: Initialize Entity properly
//Entity =
Scene = scene;
Name = name;
IsActive = true;
}
public void AddComponent<T>(T component)
where T : ScriptComponent
{
_components.Add(typeof(T), component);
component.Owner = Entity;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Components)));
}
public void RemoveComponent<T>()
where T : ScriptComponent
{
var key = typeof(T);
if (_components.Remove(key, out var component))
{
component.OnDestroy();
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Components)));
}
}
public T? GetComponent<T>()
where T : ScriptComponent
{
if (_components.TryGetValue(typeof(T), out var component))
{
return (T)component;
}
return null;
}
public void AddChild(GameObject child)
{
if (child.Scene != Scene)
{
throw new InvalidOperationException("Child GameObject must belong to the same Scene.");
}
_children.Add(child);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Children)));
}
public void RemoveChild(GameObject child)
{
if (_children.Remove(child))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Children)));
}
}
internal void OnEnable()
{
foreach (var component in Components)
{
if (!component.Enable)
{
continue;
}
component.OnEnable();
}
foreach (var child in _children)
{
child.OnEnable();
}
}
internal void Start()
{
foreach (var component in Components)
{
if (!component.Enable)
{
continue;
}
component.Start();
}
}
internal void Update()
{
foreach (var component in Components)
{
if (!component.Enable)
{
continue;
}
component.Update();
}
}
internal void LateUpdate()
{
foreach (var component in Components)
{
if (!component.Enable)
{
continue;
}
component.LateUpdate();
}
}
internal void FixedUpdate()
{
foreach (var component in Components)
{
if (!component.Enable)
{
continue;
}
component.FixedUpdate();
}
}
public void Destroy()
{
foreach (var component in Components)
{
if (!component.Enable)
{
continue;
}
component.OnDestroy();
}
foreach (var child in _children)
{
child.Destroy();
}
_children.Clear();
_components.Clear();
Parent?._children.Remove(this);
}
}

View File

@@ -4,5 +4,5 @@ internal class EngineData
{ {
public const string ENGINE_NAME = "Ghost Engine"; public const string ENGINE_NAME = "Ghost Engine";
public readonly static Version ENGINE_VERSION = new(0, 1, 0); public readonly static Version s_engineVersion = new(0, 1, 0);
} }

View File

@@ -10,43 +10,43 @@ internal static class PlayerLoopService
public static void Start() public static void Start()
{ {
if (_isRunning) //if (_isRunning)
{ //{
return; // return;
} //}
foreach (var gameObject in SceneManager.QueryRootGameObjects()) //foreach (var gameObject in SceneManager.QueryRootGameObjects())
{ //{
gameObject.Start(); // gameObject.Start();
} //}
_timer ??= new Timer(FixedUpdate, null, 0, (int)(fixedDeltaTime * 1000)); //_timer ??= new Timer(FixedUpdate, null, 0, (int)(fixedDeltaTime * 1000));
while (_isRunning) //while (_isRunning)
{ //{
Update(); // Update();
} //}
} }
private static void Update() private static void Update()
{ {
foreach (var gameObject in SceneManager.QueryRootGameObjects()) //foreach (var gameObject in SceneManager.QueryRootGameObjects())
{ //{
gameObject.Update(); // gameObject.Update();
} //}
foreach (var gameObject in SceneManager.QueryRootGameObjects()) //foreach (var gameObject in SceneManager.QueryRootGameObjects())
{ //{
gameObject.LateUpdate(); // gameObject.LateUpdate();
} //}
} }
private static void FixedUpdate(object? state) private static void FixedUpdate(object? state)
{ {
foreach (var gameObject in SceneManager.QueryRootGameObjects()) //foreach (var gameObject in SceneManager.QueryRootGameObjects())
{ //{
gameObject.FixedUpdate(); // gameObject.FixedUpdate();
} //}
} }
public static void Stop() public static void Stop()

View File

@@ -1,6 +1,4 @@
using Ghost.Engine.Models; namespace Ghost.Engine.Services;
namespace Ghost.Engine.Services;
public enum SceneLoadMode public enum SceneLoadMode
{ {
@@ -10,41 +8,41 @@ public enum SceneLoadMode
public static class SceneManager public static class SceneManager
{ {
private readonly static HashSet<Scene> _activeScenes = new(); //private readonly static HashSet<Scene> _activeScenes = new();
internal static IEnumerable<GameObject> QueryRootGameObjects() //internal static IEnumerable<GameObject> QueryRootGameObjects()
{ //{
foreach (var scene in _activeScenes) // foreach (var scene in _activeScenes)
{ // {
foreach (var gameObject in scene.RootObjects) // foreach (var gameObject in scene.RootObjects)
{ // {
if (!gameObject.IsActive) // if (!gameObject.IsActive)
{ // {
continue; // continue;
} // }
yield return gameObject; // yield return gameObject;
} // }
} // }
} //}
public static void LoadScene(Scene scene, SceneLoadMode loadMode) //public static void LoadScene(Scene scene, SceneLoadMode loadMode)
{ //{
if (loadMode == SceneLoadMode.Single) // if (loadMode == SceneLoadMode.Single)
{ // {
foreach (var activeScene in _activeScenes) // foreach (var activeScene in _activeScenes)
{ // {
activeScene.Unload(); // activeScene.Unload();
} // }
_activeScenes.Clear(); // _activeScenes.Clear();
} // }
_activeScenes.Add(scene); // _activeScenes.Add(scene);
scene.Load(); // scene.Load();
} //}
public static Task LoadSceneAsync(Scene scene, SceneLoadMode loadMode) //public static Task LoadSceneAsync(Scene scene, SceneLoadMode loadMode)
{ //{
return Task.Run(() => LoadScene(scene, loadMode)); // return Task.Run(() => LoadScene(scene, loadMode));
} //}
} }

View File

@@ -5,4 +5,5 @@ global using WorldID = System.UInt16;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ghost.Engine")] [assembly: InternalsVisibleTo("Ghost.Engine")]
[assembly: InternalsVisibleTo("Ghost.Editor")]
[assembly: InternalsVisibleTo("Ghost.Test")] [assembly: InternalsVisibleTo("Ghost.Test")]

View File

@@ -23,11 +23,12 @@ internal interface IComponentPool : IDisposable
get; get;
} }
public void Remove(Entity entity); public bool Remove(Entity entity);
public bool Has(Entity entity); public bool Has(Entity entity);
} }
internal interface IComponentPool<T> : IComponentPool internal interface IComponentPool<T> : IComponentPool
where T : IComponentData
{ {
public void Add(Entity entity, T Component); public void Add(Entity entity, T Component);
} }
@@ -115,8 +116,10 @@ internal class ComponentPool<T> : IComponentPool<T>
_nextId++; _nextId++;
} }
public void Remove(Entity entity) public bool Remove(Entity entity)
{ {
// We do not remove anything here, the generation of the entity will be used to determine if the component is valid.
return true;
} }
public ref T GetRef(Entity entity) public ref T GetRef(Entity entity)
@@ -213,13 +216,62 @@ internal class ScriptComponentPool : IComponentPool<ScriptComponent>
component.Owner = entity; component.Owner = entity;
} }
public void Remove(Entity entity) public bool Remove<T>(Entity entity)
where T : ScriptComponent
{ {
if (!Has(entity) if (!Has(entity)
|| !_scriptComponents!.TryGetValue(entity, out var scriptList) || !_scriptComponents!.TryGetValue(entity, out var scriptList)
|| scriptList == null) || scriptList == null)
{ {
return; return false;
}
var scriptToRemove = scriptList.FirstOrDefault(script => script is T);
if (scriptToRemove == null)
{
return false;
}
scriptToRemove.OnDestroy();
scriptList.Remove(scriptToRemove);
if (scriptList.Count == 0)
{
_scriptComponents.Remove(entity);
}
return true;
}
public bool RemoveAt(Entity entity, int index)
{
if (!Has(entity)
|| !_scriptComponents!.TryGetValue(entity, out var scriptList)
|| scriptList == null)
{
return false;
}
if (index < 0 || index > scriptList.Count)
{
return false;
}
scriptList.RemoveAt(index);
if (scriptList.Count == 0)
{
_scriptComponents.Remove(entity);
}
return true;
}
public bool Remove(Entity entity)
{
if (!Has(entity)
|| !_scriptComponents!.TryGetValue(entity, out var scriptList)
|| scriptList == null)
{
return false;
} }
foreach (var script in scriptList) foreach (var script in scriptList)
@@ -228,6 +280,7 @@ internal class ScriptComponentPool : IComponentPool<ScriptComponent>
} }
_scriptComponents.Remove(entity); _scriptComponents.Remove(entity);
return true;
} }
public bool Has(Entity entity) public bool Has(Entity entity)
@@ -287,7 +340,15 @@ internal class ComponentStorage : IDisposable
private readonly Dictionary<nint, BitSet> _componentEntityMasks = new(); private readonly Dictionary<nint, BitSet> _componentEntityMasks = new();
private readonly ScriptComponentPool _scriptComponentPool = new(); private readonly ScriptComponentPool _scriptComponentPool = new();
private readonly World _world;
internal ComponentStorage(World world)
{
_world = world;
}
internal Dictionary<nint, IComponentPool> ComponentPools => _componentPools; internal Dictionary<nint, IComponentPool> ComponentPools => _componentPools;
internal Dictionary<nint, BitSet> ComponentEntityMasks => _componentEntityMasks;
internal ScriptComponentPool ScriptComponentPool => _scriptComponentPool; internal ScriptComponentPool ScriptComponentPool => _scriptComponentPool;
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -112,6 +112,11 @@ public class EntityManager : IDisposable
public int EntityCount => _entities.Count; public int EntityCount => _entities.Count;
public ReadOnlySpan<Entity> Entities => CollectionsMarshal.AsSpan(_entities); public ReadOnlySpan<Entity> Entities => CollectionsMarshal.AsSpan(_entities);
public event Action<Entity, Type>? OnComponentAdded;
public event Action<Entity, Type>? OnComponentRemoved;
public event Action<Entity>? OnEntityCreated;
public event Action<EntityID>? OnEntityRemoved;
internal EntityManager(World world, int initialCapacity) internal EntityManager(World world, int initialCapacity)
{ {
_entities = new(initialCapacity); _entities = new(initialCapacity);
@@ -125,17 +130,20 @@ public class EntityManager : IDisposable
/// <returns>The created <see cref="Entity"/>.</returns> /// <returns>The created <see cref="Entity"/>.</returns>
public Entity CreateEntity() public Entity CreateEntity()
{ {
Entity entity;
if (_freeEntitySlots.TryDequeue(out var id)) if (_freeEntitySlots.TryDequeue(out var id))
{ {
return _entities[id]; entity = _entities[id];
} }
else else
{ {
id = _entities.Count; id = _entities.Count;
var entity = new Entity(id, 0, _world.ID); entity = new Entity(id, 0, _world.ID);
_entities.Add(entity); _entities.Add(entity);
return entity;
} }
OnEntityCreated?.Invoke(entity);
return entity;
} }
/// <summary> /// <summary>
@@ -149,13 +157,14 @@ public class EntityManager : IDisposable
return; return;
} }
_world._componentStorage.Remove(entity); _world.ComponentStorage.Remove(entity);
var slot = _entities[entity.ID]; var slot = _entities[entity.ID];
slot.IncrementGeneration(); slot.IncrementGeneration();
_entities[entity.ID] = slot; _entities[entity.ID] = slot;
_freeEntitySlots.Enqueue(entity.ID); _freeEntitySlots.Enqueue(entity.ID);
OnEntityRemoved?.Invoke(entity.ID);
entity = Entity.Invalid; entity = Entity.Invalid;
} }
@@ -187,8 +196,9 @@ public class EntityManager : IDisposable
public void AddComponent<T>(Entity entity, T component) public void AddComponent<T>(Entity entity, T component)
where T : struct, IComponentData where T : struct, IComponentData
{ {
_world._componentStorage.GetOrCreateComponentPool<T>().Add(entity, component); _world.ComponentStorage.GetOrCreateComponentPool<T>().Add(entity, component);
_world._componentStorage.GetOrCreateMask(TypeHandle<T>.Value).SetBit(entity.ID); _world.ComponentStorage.GetOrCreateMask(TypeHandle<T>.Value).SetBit(entity.ID);
OnComponentAdded?.Invoke(entity, typeof(T));
} }
/// <summary> /// <summary>
@@ -197,14 +207,23 @@ public class EntityManager : IDisposable
/// <typeparam name="T">The type of the component to remove.</typeparam> /// <typeparam name="T">The type of the component to remove.</typeparam>
/// <param name="entity">The entity for which the component is to be remove.</param> /// <param name="entity">The entity for which the component is to be remove.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveComponent<T>(Entity entity) public bool RemoveComponent<T>(Entity entity)
where T : struct, IComponentData where T : struct, IComponentData
{ {
if (_world._componentStorage.TryGetPool<T>(out var pool) && pool.Has(entity)) if (!_world.ComponentStorage.TryGetPool<T>(out var pool) || !pool.Has(entity))
{ {
pool.Remove(entity); return false;
_world._componentStorage.GetOrCreateMask(TypeHandle<T>.Value).ClearBit(entity.ID);
} }
if (!pool.Remove(entity))
{
return false;
}
_world.ComponentStorage.GetOrCreateMask(TypeHandle<T>.Value).ClearBit(entity.ID);
OnComponentRemoved?.Invoke(entity, typeof(T));
return true;
} }
/// <summary> /// <summary>
@@ -217,7 +236,7 @@ public class EntityManager : IDisposable
public void SetComponent<T>(Entity entity, T component) public void SetComponent<T>(Entity entity, T component)
where T : struct, IComponentData where T : struct, IComponentData
{ {
_world._componentStorage.GetOrCreateComponentPool<T>().Set(entity, component); _world.ComponentStorage.GetOrCreateComponentPool<T>().Set(entity, component);
} }
/// <summary> /// <summary>
@@ -229,7 +248,7 @@ public class EntityManager : IDisposable
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool HasComponent(Entity entity, nint typeHandle) public bool HasComponent(Entity entity, nint typeHandle)
{ {
return _world._componentStorage.TryGetMask(typeHandle, out var bitSet) && bitSet.IsSet(entity.ID); return _world.ComponentStorage.TryGetMask(typeHandle, out var bitSet) && bitSet.IsSet(entity.ID);
} }
/// <summary> /// <summary>
@@ -242,7 +261,7 @@ public class EntityManager : IDisposable
public Ref<T> GetComponent<T>(Entity entity) public Ref<T> GetComponent<T>(Entity entity)
where T : struct, IComponentData where T : struct, IComponentData
{ {
if (_world._componentStorage.TryGetPool<T>(out var pool) && pool.Has(entity)) if (_world.ComponentStorage.TryGetPool<T>(out var pool) && pool.Has(entity))
{ {
return new Ref<T>(ref pool.GetRef(entity)); return new Ref<T>(ref pool.GetRef(entity));
} }
@@ -261,7 +280,8 @@ public class EntityManager : IDisposable
public void AddScript<T>(Entity entity) public void AddScript<T>(Entity entity)
where T : ScriptComponent, new() where T : ScriptComponent, new()
{ {
_world._componentStorage.ScriptComponentPool.Add(entity, new T()); _world.ComponentStorage.ScriptComponentPool.Add(entity, new T());
OnComponentAdded?.Invoke(entity, typeof(ScriptComponent));
} }
/// <summary> /// <summary>
@@ -280,7 +300,31 @@ public class EntityManager : IDisposable
} }
var instance = (ScriptComponent?)Activator.CreateInstance(type) ?? throw new InvalidOperationException($"Failed to create instance of {type}."); var instance = (ScriptComponent?)Activator.CreateInstance(type) ?? throw new InvalidOperationException($"Failed to create instance of {type}.");
_world._componentStorage.ScriptComponentPool.Add(entity, instance); _world.ComponentStorage.ScriptComponentPool.Add(entity, instance);
OnComponentAdded?.Invoke(entity, typeof(ScriptComponent));
}
public bool RemoveScript<T>(Entity entity)
where T : ScriptComponent
{
if (!_world.ComponentStorage.ScriptComponentPool.Remove<T>(entity))
{
return false;
}
OnComponentRemoved?.Invoke(entity, typeof(ScriptComponent));
return true;
}
public bool RemoveScriptAt(Entity entity, int index)
{
if (!_world.ComponentStorage.ScriptComponentPool.RemoveAt(entity, index))
{
return false;
}
OnComponentRemoved?.Invoke(entity, typeof(ScriptComponent));
return true;
} }
/// <summary> /// <summary>
@@ -292,7 +336,7 @@ public class EntityManager : IDisposable
public T? GetScript<T>(Entity entity) public T? GetScript<T>(Entity entity)
where T : ScriptComponent where T : ScriptComponent
{ {
return (T?)_world._componentStorage.ScriptComponentPool.Get(entity)? return (T?)_world.ComponentStorage.ScriptComponentPool.Get(entity)?
.FirstOrDefault(script => script is T tScript); .FirstOrDefault(script => script is T tScript);
} }
@@ -305,7 +349,7 @@ public class EntityManager : IDisposable
public IEnumerable<T> GetScripts<T>(Entity entity) public IEnumerable<T> GetScripts<T>(Entity entity)
where T : ScriptComponent where T : ScriptComponent
{ {
return (IEnumerable<T>?)_world._componentStorage.ScriptComponentPool.Get(entity)?.Where(script => script is T tScript) ?? Enumerable.Empty<T>(); return (IEnumerable<T>?)_world.ComponentStorage.ScriptComponentPool.Get(entity)?.Where(script => script is T tScript) ?? Enumerable.Empty<T>();
} }
public void Dispose() public void Dispose()

View File

@@ -33,11 +33,11 @@ public static class EntityHelpers
/// <typeparam name="T">The type of the component to remove.</typeparam> /// <typeparam name="T">The type of the component to remove.</typeparam>
/// <param name="entity">The entity for which the component is to be remove.</param> /// <param name="entity">The entity for which the component is to be remove.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RemoveComponent<T>(this Entity entity) public static bool RemoveComponent<T>(this Entity entity)
where T : struct, IComponentData where T : struct, IComponentData
{ {
var world = entity.GetWorld(); var world = entity.GetWorld();
world.EntityManager.RemoveComponent<T>(entity); return world.EntityManager.RemoveComponent<T>(entity);
} }
/// <summary> /// <summary>

View File

@@ -37,7 +37,7 @@ internal struct QueryFilter()
// Compute All mask (intersection) // Compute All mask (intersection)
foreach (var typeHandle in _all) foreach (var typeHandle in _all)
{ {
var mask = world._componentStorage.GetOrCreateMask(typeHandle); var mask = world.ComponentStorage.GetOrCreateMask(typeHandle);
if (!hasAll) if (!hasAll)
{ {
@@ -52,7 +52,7 @@ internal struct QueryFilter()
// Compute Any mask (union) // Compute Any mask (union)
foreach (var typeHandle in _any) foreach (var typeHandle in _any)
{ {
var mask = world._componentStorage.GetOrCreateMask(typeHandle); var mask = world.ComponentStorage.GetOrCreateMask(typeHandle);
if (!hasAny) if (!hasAny)
{ {
@@ -66,7 +66,7 @@ internal struct QueryFilter()
// Compute Absent mask (union for exclusion) // Compute Absent mask (union for exclusion)
foreach (var typeHandle in _absent) foreach (var typeHandle in _absent)
{ {
var mask = world._componentStorage.GetOrCreateMask(typeHandle); var mask = world.ComponentStorage.GetOrCreateMask(typeHandle);
if (!hasAbsent) if (!hasAbsent)
{ {

View File

@@ -2,14 +2,32 @@
public abstract class ScriptComponent : IComponentData public abstract class ScriptComponent : IComponentData
{ {
private bool _enable = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this script component is enabled. /// Gets or sets a value indicating whether this script component is enabled.
/// </summary> /// </summary>
public bool Enable public bool Enable
{ {
get; get => _enable;
set; set
} = true; {
if (_enable == value)
{
return;
}
_enable = value;
if (_enable)
{
OnEnable();
}
else
{
OnDisable();
}
}
}
/// <summary> /// <summary>
/// Gets the entity that owns this script component. /// Gets the entity that owns this script component.

View File

@@ -1,19 +1,30 @@
namespace Ghost.Entities; using System.Runtime.CompilerServices;
namespace Ghost.Entities;
public abstract class SystemBase public abstract class SystemBase
{ {
/// <summary>
/// Gets the execution order of the current operation or component.
/// </summary>
public virtual int ExecutionOrder => 0; public virtual int ExecutionOrder => 0;
/// <summary>
/// Gets or sets a value indicating whether the feature is enabled.
/// </summary>
public virtual bool Enable public virtual bool Enable
{ {
get; get;
set; set;
} = true; } = true;
/// <summary>
/// The world that this system belongs to.
/// </summary>
public World World public World World
{ {
get; get;
init; internal set;
} = null!; } = null!;
public virtual void OnCreate() public virtual void OnCreate()
@@ -29,38 +40,73 @@ public abstract class SystemBase
} }
} }
internal class SystemStorage : IDisposable public class SystemStorage : IDisposable
{ {
private readonly List<SystemBase> _systems = new(); private readonly List<SystemBase> _systems = new();
private readonly List<SystemBase> _executionList = new(); private readonly List<SystemBase> _executionList = new();
private readonly World _world;
public event Action<SystemBase>? SystemAdded;
public event Action<SystemBase>? SystemRemoved;
internal SystemStorage(World world)
{
_world = world;
}
public void AddSystem<T>(T system) public void AddSystem<T>(T system)
where T : SystemBase where T : SystemBase
{ {
_systems.Add(system); _systems.Add(system);
system.World = _world;
if (system.Enable) if (system.Enable)
{ {
system.OnCreate(); system.OnCreate();
} }
SystemAdded?.Invoke(system);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddSystem<T>()
where T : SystemBase, new()
{
AddSystem(new T());
} }
public void RemoveSystem<T>(T system) public void RemoveSystem<T>(T system)
where T : SystemBase where T : SystemBase
{ {
system.World = null!;
_systems.Remove(system); _systems.Remove(system);
if (system.Enable) if (system.Enable)
{ {
system.OnDestroy(); system.OnDestroy();
} }
SystemRemoved?.Invoke(system);
} }
public void RebuildExecutionList() [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveSystem<T>()
where T : SystemBase, new()
{
var system = _systems.FirstOrDefault(s => s is T);
if (system != null)
{
RemoveSystem(system);
}
}
internal void RebuildExecutionList()
{ {
_executionList.Clear(); _executionList.Clear();
_executionList.AddRange(_systems.OrderBy(s => s.ExecutionOrder)); _executionList.AddRange(_systems.OrderBy(s => s.ExecutionOrder));
} }
public void UpdateSystems() internal void UpdateSystems()
{ {
foreach (var system in _systems) foreach (var system in _systems)
{ {

View File

@@ -1,19 +0,0 @@
namespace Ghost.Entities.Utilities;
internal class Box<T>
where T : struct
{
public T Value
{
get;
set;
}
public Box(T value)
{
Value = value;
}
public static implicit operator T(Box<T> box) => box.Value;
public static implicit operator Box<T>(T value) => new(value);
}

View File

@@ -1,64 +0,0 @@
using System.Numerics;
namespace Ghost.Entities.Utilities;
internal readonly struct ComponentMask
{
private readonly ulong[] _words;
public ComponentMask(int entityCapacity)
{
_words = new ulong[(entityCapacity + 63) / 64];
}
public void Set(int entityIndex)
=> _words[entityIndex >> 6] |= 1UL << (entityIndex & 63);
public void Clear(int entityIndex)
=> _words[entityIndex >> 6] &= ~(1UL << (entityIndex & 63));
public bool Get(int entityIndex)
=> ((_words[entityIndex >> 6] >> (entityIndex & 63)) & 1) != 0;
// Bitwise AND
public ComponentMask And(in ComponentMask other)
{
var result = new ComponentMask(_words.Length * 64);
for (var i = 0; i < _words.Length; i++)
result._words[i] = _words[i] & other._words[i];
return result;
}
// Bitwise OR
public ComponentMask Or(in ComponentMask other)
{
var result = new ComponentMask(_words.Length * 64);
for (var i = 0; i < _words.Length; i++)
result._words[i] = _words[i] | other._words[i];
return result;
}
// Bitwise NOT
public ComponentMask Not()
{
var result = new ComponentMask(_words.Length * 64);
for (var i = 0; i < _words.Length; i++)
result._words[i] = ~_words[i];
return result;
}
// Iterate set bits (fast scan)
public IEnumerable<int> GetEntityIndices()
{
for (var word = 0; word < _words.Length; word++)
{
var bits = _words[word];
while (bits != 0)
{
var lowBit = BitOperations.TrailingZeroCount(bits);
yield return (word << 6) + lowBit;
bits &= bits - 1; // clear lowest set bit
}
}
}
}

View File

@@ -12,6 +12,8 @@ public partial class World
private static int s_maxWorldCount = (int)MathF.Pow(2, Entity.WORLD_INDEX_BITS); private static int s_maxWorldCount = (int)MathF.Pow(2, Entity.WORLD_INDEX_BITS);
public static int WorldCount => s_worlds.Count - s_freeWorldSlots.Count;
public static World Create(int entityCapacity = 16) public static World Create(int entityCapacity = 16)
{ {
lock (s_worlds) lock (s_worlds)
@@ -46,31 +48,21 @@ public partial class World : IDisposable
{ {
private readonly WorldID _id; private readonly WorldID _id;
private readonly EntityManager _entityManager; private readonly EntityManager _entityManager;
private readonly ComponentStorage _componentStorage;
private readonly SystemStorage _systemStorage;
internal readonly ComponentStorage _componentStorage; internal ComponentStorage ComponentStorage => _componentStorage;
internal readonly SystemStorage _systemStorage;
public WorldID ID => _id; public WorldID ID => _id;
public EntityManager EntityManager => _entityManager; public EntityManager EntityManager => _entityManager;
public SystemStorage SystemStorage => _systemStorage;
private World(WorldID id, int entityCapacity) private World(WorldID id, int entityCapacity)
{ {
_id = id; _id = id;
_entityManager = new EntityManager(this, entityCapacity); _entityManager = new EntityManager(this, entityCapacity);
_componentStorage = new ComponentStorage(); _componentStorage = new ComponentStorage(this);
_systemStorage = new SystemStorage(); _systemStorage = new SystemStorage(this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddSystem<T>()
where T : SystemBase, new()
{
var instance = new T
{
World = this
};
_systemStorage.AddSystem(instance);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -8,9 +8,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ghost.Data\Ghost.Data.csproj" />
<ProjectReference Include="..\Ghost.Engine\Ghost.Engine.csproj" /> <ProjectReference Include="..\Ghost.Engine\Ghost.Engine.csproj" />
<ProjectReference Include="..\Ghost.Entities\Ghost.Entities.csproj" /> <ProjectReference Include="..\Ghost.Entities\Ghost.Entities.csproj" />
<ProjectReference Include="..\Ghost.Graphics\Ghost.Graphics.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -44,8 +44,8 @@ public partial class Test
entity4.AddComponent(new Mesh { index = 44 }); entity4.AddComponent(new Mesh { index = 44 });
entity4.AddScript<UserScript>(); entity4.AddScript<UserScript>();
world.AddSystem<TestSystem>(); world.SystemStorage.AddSystem<TestSystem>();
world._systemStorage.UpdateSystems(); world.SystemStorage.UpdateSystems();
//world.SystemStorage.RebuildExecutionList(); //world.SystemStorage.RebuildExecutionList();
//world.ComponentStorage.RebuildExecutionList(); //world.ComponentStorage.RebuildExecutionList();