using System; using ImGui; using GlitchyEngine; using GlitchyEditor.EditWindows; using GlitchyEngine.Events; using GlitchyEngine.ImGui; using GlitchyEngine.Math; using GlitchyEngine.Renderer; using GlitchyEngine.World; using GlitchyEngine.Content; using System.Collections; using GlitchyEngine.Renderer.Animation; using System.IO; using GlitchyEngine.Core; using GlitchyEditor.Assets; using GlitchyEngine.Scripting; namespace GlitchyEditor { class EditorLayer : Layer { enum SceneState { Edit, Play, Simulate } RasterizerState _rasterizerState ~ _?.ReleaseRef(); RasterizerState _rasterizerStateClockWise ~ _?.ReleaseRef(); // TODO: we shouldn't hold a reference to the context GraphicsContext _context ~ _.ReleaseRef(); BlendState _alphaBlendState ~ _.ReleaseRef(); BlendState _opaqueBlendState ~ _.ReleaseRef(); DepthStencilState _depthStencilState ~ _.ReleaseRef(); /// Reference to the scene that is currently being played and worked on. Scene _activeScene ~ _?.ReleaseRef(); /** * Referece to the editor scene. * We hold a reference to the editor scene because we need it in order * to restore the original state once we stop the game/simulation. * Before starting the simulation the editor scene will be copied and * the reference in _activeScene will be replaced with the new scene. */ Scene _editorScene ~ _?.ReleaseRef(); SceneRenderer _gameSceneRenderer ~ delete _; SceneRenderer _editorSceneRenderer ~ delete _; /// Path of the current scene. append String _sceneFilePath = .(); Editor _editor ~ delete _; RenderTargetGroup _cameraTarget ~ _.ReleaseRef(); RenderTargetGroup _editorViewportTarget ~ _.ReleaseRef(); RenderTargetGroup _gameViewportTarget ~ _.ReleaseRef(); SettingsWindow _settingsWindow = new .() ~ delete _; ProjectUserSettings _projectUserSettings; EditorCamera _camera ~ _.Dispose(); EditorIcons _editorIcons ~ _.ReleaseRef(); EditorContentManager _contentManager; SceneState _sceneState = .Edit; bool _isPaused = false; bool _singleStep = false; private float _fixedTimestep = 1.0f / 60.0f; append List<(String Name, String Path)> _recentScenePaths = .() ~ { for (var item in _) { delete item.Name; delete item.Path; } }; private GameTime _simulationGameTime = new .() ~ delete _; /// Gets or sets the path of the current scene. public StringView SceneFilePath { get => _sceneFilePath; set { _sceneFilePath.Clear(); if (!value.IsWhiteSpace) _sceneFilePath.Append(value); } } private Project _currentProject ~ delete _; public Project CurrentProject => _currentProject; public bool IsProjectLoaded => _currentProject != null; [AllowAppend] public this(String[] args, EditorContentManager contentManager) : base("Editor") { Application.Get().Window.IsVSync = true; _contentManager = contentManager; InitGraphics(); _gameSceneRenderer = new SceneRenderer(); _editorSceneRenderer = new SceneRenderer(); _camera = EditorCamera(float3(-3f, 3f, -3f), Quaternion.FromEulerAngles(MathHelper.ToRadians(40), MathHelper.ToRadians(25), 0), MathHelper.ToRadians(75), 0.1f, 1); _camera.RenderTarget = _cameraTarget; InitEditor(); RegisterAssetCreators(); if (args.Count >= 1) { Result result = LoadAndOpenProject(args[0]); if (result == .Err) Log.EngineLogger.Error($"Failed to open project \"{args[0]}\"."); } // Create a new scene if no scene is loaded if (_activeScene == null) CreateAndOpenNewScene(); } public ~this() { CloseCurrentProject(); } private void RegisterAssetCreators() { Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("Folder", "New Folder", new (path) => { if (Directory.CreateDirectory(path) case .Err(let error)) { Log.ClientLogger.Error($"Directory \"{path}\" could not be created ({error})."); } })); Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("Scene", "New Scene.scene", new (path) => { using (Scene newScene = CreateNewScene()) { SaveScene(newScene, path); } })); Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("Material", "New Material.mat", new (path) => { using (Material newMaterial = new Material()) { _contentManager.SaveAssetToFile(newMaterial, path); } })); } private void InitGraphics() { _context = Application.Get().Window.Context..AddRef(); RasterizerStateDescription rsDesc = .(.Solid, .Back, true); _rasterizerState = new RasterizerState(rsDesc); rsDesc.FrontCounterClockwise = false; _rasterizerStateClockWise = new RasterizerState(rsDesc); BlendStateDescription blendDesc = .(); blendDesc.RenderTarget[0] = .(true, .SourceAlpha, .InvertedSourceAlpha, .Add, .SourceAlpha, .InvertedSourceAlpha, .Add, .All); _alphaBlendState = new BlendState(blendDesc); _opaqueBlendState = new BlendState(.Default); DepthStencilStateDescription dsDesc = .(); _depthStencilState = new DepthStencilState(dsDesc); _cameraTarget = new RenderTargetGroup(.(){ Width = 100, Height = 100, ColorTargetDescriptions = TargetDescription[]( .(.R16G16B16A16_Float), .(.R32_UInt) ), DepthTargetDescription = .(.D24_UNorm_S8_UInt) }); _editorViewportTarget = new RenderTargetGroup(.() { Width = 100, Height = 100, ColorTargetDescriptions = TargetDescription[]( .(.R8G8B8A8_UNorm)) }); _gameViewportTarget = new RenderTargetGroup(.() { Width = 100, Height = 100, ColorTargetDescriptions = TargetDescription[]( .(.R8G8B8A8_UNorm)) }); _editorIcons = new EditorIcons("Resources/Textures/EditorIcons.dds", .(64, 64)); _editorIcons.SamplerState = SamplerStateManager.AnisotropicClamp; ContentBrowserWindow.s_FolderTexture = _editorIcons.Folder; ContentBrowserWindow.s_FileTexture = _editorIcons.File; LogWindow.s_ErrorIcon = _editorIcons.Error; LogWindow.s_WarningIcon = _editorIcons.Warning; LogWindow.s_InfoIcon = _editorIcons.Info; LogWindow.s_TraceIcon = _editorIcons.Trace; } private void InitEditor() { _editor = new Editor(_editorScene, _contentManager); _editor.SceneViewportWindow.ViewportSizeChanged.Add(new (s, e) => EditorViewportSizeChanged(s, e)); _editor.GameViewportWindow.ViewportSizeChanged.Add(new (s, e) => GameViewportSizeChanged(s, e)); _editor.CurrentCamera = &_camera; _editor.GameSceneRenderer = _gameSceneRenderer; _editor.EditorSceneRenderer = _editorSceneRenderer; _editor.RequestOpenScene.Add(new (s, fileName) => { LoadSceneFile(fileName); }); } public override void Update(GameTime gameTime) { Debug.Profiler.ProfileFunction!(); if (IsProjectLoaded) UpdateScene(gameTime); } private void UpdateScene(GameTime gameTime) { _editor.CurrentScene = _activeScene; Scene.UpdateMode updateMode; switch (_sceneState) { case .Edit: updateMode = .Editor; case .Play: updateMode = .Runtime; case .Simulate: updateMode = .Physics; } // Don't step if we are in in the simulation/game but paused and don't single step if (_sceneState != .Edit && _isPaused && !_singleStep) updateMode = .None; /* * If we are in edit mode we use the "official" gametime. * However in play or simulation mode we use a simulation game time * which is decoupled from the actual time. This allows us to manipulate * the simulation speed without potentially messing with the rest of the editor. */ GameTime gt = null; if (updateMode == .Editor) { gt = gameTime; } else { gt = _simulationGameTime; if (_singleStep) { _singleStep = false; _simulationGameTime.ManualStepFrame(_fixedTimestep); } else { _simulationGameTime.ManualStepFrame(gameTime.DeltaTime); } } _activeScene.Update(gt, updateMode); // Clear the swapchain-buffer RenderCommand.Clear(null, .Color | .Depth, .(0.2f, 0.2f, 0.2f), 1.0f, 0); RenderCommand.SetBlendState(_alphaBlendState); RenderCommand.SetDepthStencilState(_depthStencilState); if (_editor.GameViewportWindow.Visible) { _gameSceneRenderer.Scene = _activeScene; _activeScene.SetViewportSize((uint32)_editor.GameViewportWindow.ViewportSize.X, (uint32)_editor.GameViewportWindow.ViewportSize.Y); _gameSceneRenderer.SetViewportSize((uint32)_editor.GameViewportWindow.ViewportSize.X, (uint32)_editor.GameViewportWindow.ViewportSize.Y); RenderCommand.Clear(_gameViewportTarget, .Color | .Depth, .(0.2f, 0.2f, 0.2f), 1.0f, 0); _gameSceneRenderer.RenderRuntime(gameTime, _gameViewportTarget); } RenderCommand.SetBlendState(_alphaBlendState); RenderCommand.SetDepthStencilState(_depthStencilState); if (_editor.SceneViewportWindow.Visible) { _camera.Update(gameTime); _editorSceneRenderer.Scene = _activeScene; // Only use aspect ratio of editor-window, if game is not being rendered if (!_editor.GameViewportWindow.Visible) _activeScene.SetViewportSize((uint32)_editor.SceneViewportWindow.ViewportSize.X, (uint32)_editor.SceneViewportWindow.ViewportSize.Y); _editorSceneRenderer.SetViewportSize((uint32)_editor.SceneViewportWindow.ViewportSize.X, (uint32)_editor.SceneViewportWindow.ViewportSize.Y); RenderCommand.Clear(_editorViewportTarget, .Color | .Depth, .(0.2f, 0.2f, 0.2f), 1.0f, 0); _editorSceneRenderer.RenderEditor(gameTime, _camera, _editorViewportTarget, scope => DebugDraw3D, scope => DebugDraw2D); } RenderCommand.UnbindRenderTargets(); RenderCommand.SetRenderTarget(null, 0, true); RenderCommand.BindRenderTargets(); RenderCommand.SetViewport(_context.SwapChain.BackbufferViewport); } private void DebugDraw3D() { /*for (var (entity, transform, camera) in _scene.[Friend]_ecsWorld.Enumerate()) { if (_editor.EntityHierarchyWindow.SelectedEntities.Contains(.(entity, _scene))) { DebugRenderer.DrawViewFrustum(transform.WorldTransform, camera.Camera.Projection); } }*/ } private void DebugDraw2D() { RenderCommand.SetBlendState(_alphaBlendState); Matrix billboard = _camera.View.Invert(); billboard.Translation = .Zero; Matrix Billboard(Matrix transform) { float3 worldPos = transform.Translation; return Matrix.Translation(worldPos) * billboard; } float CalculateAlpha(float3 pos) { return Math.Clamp(1.5f - distance(_editor.CurrentCamera.Position, pos) / 50, 0, 1); } for (var (entity, transform, camera) in _activeScene.GetEntities()) { if (_editor.EntityHierarchyWindow.SelectedEntities.Contains(.(entity, _activeScene))) { DebugRenderer.DrawViewFrustum(transform.WorldTransform, camera.Camera.Projection, .White); } Matrix world = Billboard(transform.WorldTransform); float alpha = CalculateAlpha(transform.WorldTransform.Translation); Renderer2D.DrawQuad(world, _editorIcons.Camera, ColorRGBA(alpha, alpha, alpha, alpha), .(0, 0, 1, 1), entity.Index); //Renderer2D.DrawQuad(world, _iconCamera, .White, .(0, 0, 1, 1), entity.Index); } for (var (entity, transform, light) in _activeScene.GetEntities()) { if (_editor.EntityHierarchyWindow.SelectedEntities.Contains(.(entity, _activeScene))) { Renderer.DrawRay(.Zero, .(0, 0, 20), ColorRGBA(light.SceneLight.Color, 1.0f), transform.WorldTransform); for (float angle = 0; angle < MathHelper.TwoPi; angle += MathHelper.TwoPi / 5.0f) { float2 pos = MathHelper.CirclePoint(angle, 0.5f); Renderer.DrawRay(.(pos, 0), .(pos, 20), .White, transform.WorldTransform); } } Matrix world = Billboard(transform.WorldTransform); float alpha = CalculateAlpha(transform.WorldTransform.Translation); Renderer2D.DrawQuad(world, _editorIcons.DirectionalLight, ColorRGBA(light.SceneLight.Color.R * alpha, light.SceneLight.Color.G * alpha, light.SceneLight.Color.B * alpha, alpha), .(0, 0, 1, 1), entity.Index); } for (var (entity, transform, collider) in _activeScene.GetEntities()) { Renderer2D.DrawRect(transform.WorldTransform * Matrix.Translation(collider.Offset.X, collider.Offset.Y, 0) * Matrix.Scaling(collider.Size.X * 2, collider.Size.Y * 2, 0), .Green, entity.Index); } for (var (entity, transform, collider) in _activeScene.GetEntities()) { Renderer2D.DrawCircle(transform.WorldTransform * Matrix.Translation(collider.Offset.X, collider.Offset.Y, 0) * Matrix.Scaling(collider.Radius * 2), (Texture2D)null, ColorRGBA(0f, 1f, 0f), 0.01f, .(0, 0, 1, 1), entity.Index); } for (var (entity, transform, collider) in _activeScene.GetEntities()) { Matrix colliderTransform = transform.WorldTransform * Matrix.Translation(collider.Offset.X, collider.Offset.Y, 0); float3 firstPosition = (colliderTransform * float4(collider.Vertices[0], 0, 1)).XYZ; float3 lastPosition = firstPosition; for (int i = 1; i < collider.VertexCount; i++) { float3 position = (colliderTransform * float4(collider.Vertices[i], 0, 1)).XYZ; Renderer2D.DrawLine(lastPosition, position, .Green, entity.Index); lastPosition = position; } Renderer2D.DrawLine(lastPosition, firstPosition, .Green, entity.Index); } } /// Updates the title of the window. private void UpdateWindowTitle() { Application.Instance.Window.Title = scope $"Glitchy Engine - {_currentProject.Name}"; } #region Project Management /// Creates a new Project and opens it. /// @param workspaceDirectory The workspace directory of the new project (The directory that all files will be in). /// @param projectName Name of the new project. /// @remarks If the creation fails at any point the entire directory will be deleted. private Result CreateNewProject(StringView workspaceDirectory, StringView projectName) { if (Directory.CreateDirectory(workspaceDirectory) case .Err(let error)) { Log.ClientLogger.Error($"Failed to create directory \"{workspaceDirectory}\". ({error})"); return .Err; } if (!Directory.IsEmpty(workspaceDirectory)) { Log.ClientLogger.Error($"Target directory is not empty."); return .Err; } Project newProject = Project.CreateNew(workspaceDirectory, projectName); // If the project couldn't be created or initialization failed, we delete the workspace directory. if (newProject == null || InitProject(newProject) == .Err) { if (Directory.DelTree(workspaceDirectory) case .Err(let error)) { Log.EngineLogger.Error($"Failed to delete workspace ({workspaceDirectory})."); } delete newProject; return .Err; } if (OpenProject(newProject) case .Err) { Log.ClientLogger.Error($"Failed to open newly created project."); return .Err; } return .Ok; } /// Initializes the given project with the template. private static Result InitProject(Project project) { Try!(CopyTemplate(project)); Try!(MoveFile(project, "Assets/Scripts/TemplateProject.sln", scope $"Assets/Scripts/{project.Name}.sln")); Try!(MoveFile(project, "Assets/Scripts/TemplateProject.csproj", scope $"Assets/Scripts/{project.Name}.csproj")); Try!(FixupTemplateFile(project, scope $"Assets/Scripts/{project.Name}.sln")); Try!(FixupTemplateFile(project, scope $"Assets/Scripts/{project.Name}.csproj")); Try!(FixupTemplateFile(project, scope $"Assets/Scripts/Properties/AssemblyInfo.cs")); return .Ok; } /// Copies the template files into the given project. private static Result CopyTemplate(Project project) { String templatePath = scope .(); Directory.GetCurrentDirectory(templatePath); Path.Combine(templatePath, "Resources/ProjectTemplate"); if (Directory.Copy(templatePath, project.WorkspacePath) case .Err) { Log.EngineLogger.Error($"CopyTemplate: Failed to copy template {templatePath} to {project.WorkspacePath}."); return .Err; } return .Ok; } /// Renames a file within the project. private static Result MoveFile(Project project, StringView fromName, StringView toName) { String fromPath = scope String(); project.PathInProject(fromPath, fromName); String toPath = scope String(); project.PathInProject(toPath, toName); if (File.Move(fromPath, toPath) case .Err(let error)) { Log.EngineLogger.Error($"Failed to move file from {fromPath} to {toPath}. ({error})"); return .Err; } return .Ok; } /// Fixups a template file by replacing placeholders with actual values. private static Result FixupTemplateFile(Project project, StringView fileName) { String fileTargetPath = scope String(); project.PathInProject(fileTargetPath, fileName); String fileContent = scope String(); if (File.ReadAllText(fileTargetPath, fileContent, true) case .Err) { Log.EngineLogger.Error($"FixupFile: Failed to read file {fileTargetPath}."); return .Err; } fileContent.Replace("[ProjectName]", project.Name); String scriptCorePath = scope .(); Directory.GetCurrentDirectory(scriptCorePath); scriptCorePath.Append("/Resources/Scripts/ScriptCore.dll"); fileContent.Replace("[CoreLibPath]", scriptCorePath); if (File.WriteAllText(fileTargetPath, fileContent, false) case .Err) { Log.EngineLogger.Error($"FixupFile: Failed to write file {fileTargetPath}."); return .Err; } return .Ok; } /// If set to true, the popup for creating a new project will be shown. private bool _openCreateProjectModal; private void ShowCreateNewProjectModal() { static char8[128] projectNameBuffer = .(); static char8[256] projectDirectoryBuffer = .(); if (_openCreateProjectModal) { ImGui.OpenPopup("Create new Project"); _openCreateProjectModal = false; projectNameBuffer = .(); } // Always center this window when appearing var center = ImGui.GetMainViewport().GetCenter(); ImGui.SetNextWindowPos(center, .Appearing, .(0.5f, 0.5f)); if (ImGui.BeginPopupModal("Create new Project", null, .AlwaysAutoResize)) { ImGui.TextUnformatted("Project Name:"); ImGui.InputText("##projectName", &projectNameBuffer, projectNameBuffer.Count - 1); ImGui.NewLine(); ImGui.TextUnformatted("Directory:"); ImGui.InputText("##directory", &projectDirectoryBuffer, projectDirectoryBuffer.Count - 1); ImGui.SameLine(); if (ImGui.Button("...")) { FolderBrowserDialog folderDialog = scope FolderBrowserDialog(); Result result = folderDialog.ShowDialog(); if (result case .Ok(let dialogResult) && dialogResult case .OK) { folderDialog.SelectedPath.CopyTo(projectDirectoryBuffer); } } StringView projectName = StringView(&projectNameBuffer); StringView directory = StringView(&projectDirectoryBuffer); String target = scope String(); Path.Combine(target, directory, projectName); ImGui.NewLine(); ImGui.Text($"The Project will be in:\n{target}"); ImGui.NewLine(); ImGui.BeginDisabled(directory.IsWhiteSpace || projectName.IsWhiteSpace); if (ImGui.Button("Create")) { Result result = CreateNewProject(target, projectName); if (result case .Ok) ImGui.CloseCurrentPopup(); } ImGui.EndDisabled(); ImGui.SameLine(); if (ImGui.Button("Cancel")) { ImGui.CloseCurrentPopup(); } ImGui.EndPopup(); } } private void ShowOpenProjectDialog() { FolderBrowserDialog folderDialog = scope FolderBrowserDialog(); Result result = folderDialog.ShowDialog(); if (result case .Ok(let dialogResult) && dialogResult case .OK) { LoadAndOpenProject(folderDialog.SelectedPath); } } private void CloseCurrentProject() { CloseCurrentScene(); SetEditorScene(null); _currentProject?.SaveUserSettings(); // TODO: Do actual work here! delete _currentProject; _currentProject = null; } private Result LoadAndOpenProject(StringView workspacePath) { Project openedProject = Project.Load(workspacePath); if (OpenProject(openedProject) case .Err) { Log.ClientLogger.Error($"Failed to open project {workspacePath}."); return .Err; } return .Ok; } /// Opens the given project and makes it the current project. private Result OpenProject(Project project) { if (project == null) return .Err; CloseCurrentProject(); _currentProject = project; String appAssemblyPath = scope String(); _contentManager.SetAssetDirectory(_currentProject.AssetsFolder); Path.Combine(appAssemblyPath, _currentProject.AssetsFolder, scope $"Scripts/bin/{_currentProject.Name}.dll"); ScriptEngine.SetAppAssemblyPath(appAssemblyPath); if (!_currentProject.UserSettings.LastOpenedScene.IsWhiteSpace) { String lastSceneFile = _currentProject.GetScopedPath!(_currentProject.UserSettings.LastOpenedScene); if (File.Exists(lastSceneFile)) LoadSceneFile(lastSceneFile); else CreateAndOpenNewScene(); } else CreateAndOpenNewScene(); UpdateWindowTitle(); return .Ok; } #endregion Project Management #region Scene Management /// Focuses the Play window, so that its tab will be shown. private void SwitchToPlayWindow() { let window = ImGui.FindWindowByName(GameViewportWindow.s_WindowTitle); ImGui.FocusWindow(window); } /// Focuses the Editor window, so that its tab will be shown. private void SwitchToEditorWindow() { let window = ImGui.FindWindowByName(EditorViewportWindow.s_WindowTitle); ImGui.FocusWindow(window); } /// Starts the play mode for the current scene private void OnScenePlay() { _editor.SceneViewportWindow.EditorMode = false; _sceneState = .Play; using (Scene runtimeScene = new Scene()) { _editorScene.CopyTo(runtimeScene, true); runtimeScene.OnRuntimeStart(); SetReference!(_activeScene, runtimeScene); } _editor.CurrentScene = _activeScene; if (Application.Instance.Settings.EditorSettings.SwitchToPlayerOnPlay) SwitchToPlayWindow(); } /// Starts the physics simulation mode for the current scene private void OnSceneSimulate() { _editor.SceneViewportWindow.EditorMode = false; _sceneState = .Simulate; using (Scene simulationScene = new Scene()) { _editorScene.CopyTo(simulationScene, false); simulationScene.OnSimulationStart(); SetReference!(_activeScene, simulationScene); } _editor.CurrentScene = _activeScene; if (Application.Instance.Settings.EditorSettings.SwitchToPlayerOnSimulate) SwitchToPlayWindow(); } /// Pauses the scene private void OnScenePause() { _isPaused = true; if (Application.Instance.Settings.EditorSettings.SwitchToEditorOnPause) SwitchToEditorWindow(); } /// Resumes the simulation / game private void OnSceneResume() { _isPaused = false; if (Application.Instance.Settings.EditorSettings.SwitchToPlayerOnResume) SwitchToPlayWindow(); } /// Requests execution of a single simulation / game tick private void DoSingleStep() { _singleStep = true; } /// Stops the simulation or game and returns to edit mode. private void OnSceneStop() { if (_sceneState == .Play) _activeScene.OnRuntimeStop(); else if (_sceneState == .Simulate) _activeScene.OnSimulationStop(); SetReference!(_activeScene, _editorScene); _editor.SceneViewportWindow.EditorMode = true; _sceneState = .Edit; _editor.CurrentScene = _activeScene; _isPaused = false; if (_activeScene != null) { /* * Update the viewport size because if the game windows size changed in * "Game"-mode the updated aspect-rations will reset once we go back * into "Editor"-mode (because Game-Mode works on a copy of the scene). */ GameViewportSizeChanged(null, _editor.GameViewportWindow.ViewportSize); } if (Application.Instance.Settings.EditorSettings.SwitchToEditorOnStop) SwitchToEditorWindow(); } /// Stops the scene and cleans up the subsystems to allow loading another scene. private void CloseCurrentScene() { OnSceneStop(); ScriptEngine.ClearEntityScriptFields(); } /// Creates a new scene and openes it. private void CreateAndOpenNewScene() { CloseCurrentScene(); SceneFilePath = null; using (Scene newScene = CreateNewScene()) { _camera.Position = .(-1.5f, 1.5f, -2.5f); _camera.RotationEuler = .(MathHelper.ToRadians(25), MathHelper.ToRadians(35), 0); SetEditorScene(newScene); } } /// Sets the current editor scene private void SetEditorScene(Scene scene) { SetReference!(_editorScene, scene); _editor.CurrentScene = _editorScene; SetReference!(_activeScene, _editorScene); } /// Creates a new default scene. private static Scene CreateNewScene() { Scene newScene = new Scene(); // Create a default camera { let cameraEntity = newScene.CreateEntity("Camera"); let transform = cameraEntity.Transform; transform.Position = float3(0, 2, -5); transform.RotationEuler = float3(0, MathHelper.ToRadians(25), 0); let camera = cameraEntity.AddComponent(); camera.Primary = true; camera.Camera.ProjectionType = .InfinitePerspective; camera.Camera.PerspectiveFovY = MathHelper.ToRadians(75); camera.Camera.PerspectiveNearPlane = 0.1f; } // Create a default light source { let lightEntity = newScene.CreateEntity("Light"); let transform = lightEntity.Transform; transform.Position = .(-3, 4, -1.5f); transform.RotationEuler = .(MathHelper.ToRadians(20), MathHelper.ToRadians(75), MathHelper.ToRadians(20)); let light = lightEntity.AddComponent(); light.SceneLight.Illuminance = 10.0f; light.SceneLight.Color = .(1.0f, 0.95f, 0.8f); } return newScene; } /// Saves the scene in the file that is was loaded from or saved to last. If there is no such path (i.e. it is a new scene) the save file dialog will open. private void SaveCurrentScene() { if (SceneFilePath.IsWhiteSpace) { SaveCurrentSceneAs(); } else { SaveScene(_editorScene, SceneFilePath); } // Note: We only update the last opened scene if the CURRENT scene is saved, NOT when ANY scene is saved. String relativePath = scope .(); Path.GetRelativePath(SceneFilePath, _currentProject.WorkspacePath, relativePath); _currentProject.UserSettings.LastOpenedScene = relativePath; } /// Saves the given scene with the specified file name. private static void SaveScene(Scene scene, StringView fileName) { SceneSerializer serializer = scope .(scene); serializer.Serialize(fileName); } /// Opens a save file dialog and saves the scene at the user specified location. private void SaveCurrentSceneAs() { SaveFileDialog sfd = scope .(); sfd.InitialDirectory = _currentProject.AssetsFolder; if (sfd.ShowDialog() case .Ok(let result) && result == .OK) { SceneFilePath = sfd.FileNames[0]; SaveCurrentScene(); } } /// Opens a open file dialog and load the scene selected by the user specified. private void OpenScene() { OpenFileDialog ofd = scope .(); ofd.InitialDirectory = _currentProject.AssetsFolder; if (ofd.ShowDialog() case .Ok(let result) && result == .OK) { LoadSceneFile(ofd.FileNames[0]); } } /// Loads the given scene file and opens it as the current scene. private void LoadSceneFile(StringView filename) { CloseCurrentScene(); ScriptEngine.ClearEntityScriptFields(); SceneFilePath = scope String(filename); using (Scene newScene = new Scene()) { SceneSerializer serializer = scope .(newScene); let result = serializer.Deserialize(SceneFilePath); // Make sure we actually loaded something! if (result case .Ok) { SetEditorScene(newScene); String relativePath = scope .(); Path.GetRelativePath(filename, _currentProject.WorkspacePath, relativePath); _currentProject.UserSettings.LastOpenedScene = relativePath; } else { Log.ClientLogger.Error("Failed to load scene file."); } } } #endregion Scene Management #region ImGui private bool OnImGuiRender(ImGuiRenderEvent event) { ImGui.Viewport* viewport = ImGui.GetMainViewport(); ImGui.DockSpaceOverViewport(viewport); DrawMainMenuBar(); _editor.SceneViewportWindow.RenderTarget = _editorViewportTarget; _editor.GameViewportWindow.RenderTarget = _gameViewportTarget; _editor.Update(); _settingsWindow.Show(); ShowCreateNewProjectModal(); ShowPlayControls(); return false; } /// Shows the window with Play/Pause, etc. buttons private void ShowPlayControls() { ImGui.PushStyleVar(.WindowPadding, ImGui.Vec2(0, 2)); ImGui.PushStyleVar(.ItemInnerSpacing, ImGui.Vec2(0, 0)); ImGui.PushStyleColor(.Button, ImGui.Vec4(0, 0, 0, 0)); let colors = ImGui.GetStyle().Colors; ImGui.Vec4 hoveredColor = colors[(int)ImGui.Col.ButtonHovered]; hoveredColor.w = 0.5f; ImGui.Vec4 activeColor = colors[(int)ImGui.Col.ButtonActive]; activeColor.w = 0.5f; ImGui.PushStyleColor(.ButtonHovered, hoveredColor); ImGui.PushStyleColor(.ButtonActive, activeColor); ImGui.Begin("##toolbar", null, .NoDecoration | .NoScrollbar | .NoScrollWithMouse); float padding = 2.0f; float size = ImGui.GetWindowHeight() - 2 * padding; float centerX = ImGui.GetContentRegionMax().x / 2; if (_sceneState == .Edit) EditorButtons: { // Display the buttons for edit state float totalWidth = size * 3 + padding * 4; ImGui.SameLine(); ImGui.SetCursorPosX(centerX - totalWidth / 2); ImGui.PushID(0); if (ImGui.ImageButton("", _editorIcons.Play, .(size, size), .Zero, .Ones)) OnScenePlay(); ImGui.PopID(); ImGui.AttachTooltip("Play the game."); ImGui.SameLine(); ImGui.PushID(1); if (ImGui.ImageButton("", _editorIcons.Simulate, .(size, size), .Zero, .Ones)) OnSceneSimulate(); ImGui.PopID(); ImGui.AttachTooltip("Enter simulation mode.\nThis only runs the physics engine."); if (_isPaused) { ImGui.PushStyleColor(.Button, *ImGui.GetStyleColorVec4(.ButtonActive)); defer:EditorButtons { ImGui.PopStyleColor(); } } ImGui.PushID(2); //ImGui.SameLine(penX += size + 2 * padding); ImGui.SameLine(); if (ImGui.ImageButton("", _editorIcons.Pause, .(size, size), .Zero, .Ones)) _isPaused = !_isPaused; ImGui.PopID(); ImGui.AttachTooltip("If enabled the game or simulation will be started in paused state."); } else { float totalWidth = size * 2 + padding * 2; if (_isPaused) { totalWidth = size * 3 + padding * 4; } // Display the buttons for play/simulation state ImGui.SameLine(); ImGui.SetCursorPosX(centerX - totalWidth / 2.0f); SubTexture2D pauseButtonIcon = _isPaused ? _editorIcons.Play : _editorIcons.Pause; if (ImGui.ImageButton("PlayPause", pauseButtonIcon, .(size, size), .Zero, .Ones)) { if (_isPaused) OnSceneResume(); else OnScenePause(); } ImGui.AttachTooltip(_isPaused ? "Resume" : "Pause"); if (_isPaused) { ImGui.PushID(2); ImGui.SameLine(); SubTexture2D singleStepButtonIcon = _editorIcons.SingleStep; if (ImGui.ImageButton("Step", singleStepButtonIcon, .(size, size), .Zero, .Ones)) { DoSingleStep(); } if (ImGui.BeginPopupContextItem()) { ImGui.TextUnformatted("Time step:"); ImGui.SameLine(); ImGui.DragFloat("##timestep", &_fixedTimestep, 0.001f, 0.001f, 1000.0f, "%.4fs"); float fps = 1.0f / _fixedTimestep; ImGui.TextUnformatted("Frame rate:"); ImGui.SameLine(); if (ImGui.DragFloat("##fps", &fps, 1.0f, 0.001f, 1000.0f, "%.3f FPS")) { _fixedTimestep = 1.0f / fps; } ImGui.EndPopup(); } ImGui.AttachTooltip("Step one Frame (Right-click to change time step)"); ImGui.PopID(); } ImGui.SameLine(); ImGui.PushID(1); if (ImGui.ImageButton("Stop", _editorIcons.Stop, .(size, size), .Zero, .Ones)) OnSceneStop(); ImGui.PopID(); ImGui.AttachTooltip("Stop"); } ImGui.End(); ImGui.PopStyleColor(3); ImGui.PopStyleVar(2); } private void ShowOpenRecentSceneMenu() { if (_currentProject == null || _currentProject.UserSettings.RecentScenes == null) return; int i = 0; for (let scenePath in _currentProject.UserSettings.RecentScenes) { i++; String fullScenePath = _currentProject.GetScopedPath!(scenePath); if (!File.Exists(fullScenePath)) { delete scenePath; @scenePath.Remove(); } if (ImGui.MenuItem(scope $"{i}: {scenePath}")) { LoadSceneFile(fullScenePath); } } } private void DrawMainMenuBar() { ImGui.BeginMainMenuBar(); if(ImGui.BeginMenu("File", true)) { if (ImGui.MenuItem("New Scene", "Ctrl+N")) CreateAndOpenNewScene(); ImGui.AttachTooltip("Creates a new (almost) empty scene."); if (ImGui.MenuItem("Open Scene...", "Ctrl+O")) OpenScene(); ImGui.AttachTooltip("Opens an existing Scene."); if (ImGui.BeginMenu("Open recent Scene")) { ShowOpenRecentSceneMenu(); ImGui.EndMenu(); } ImGui.Separator(); if (ImGui.MenuItem("Save Scene", "Ctrl+S")) SaveCurrentScene(); ImGui.AttachTooltip("Saves the current scene."); if (ImGui.MenuItem("Save Scene as...", "Ctrl+Shift+S")) SaveCurrentSceneAs(); ImGui.AttachTooltip("Saves the scene under the given file name."); ImGui.Separator(); if (ImGui.MenuItem("Create new Project...", "Ctrl+ALT+N")) _openCreateProjectModal = true; if (ImGui.MenuItem("Open Project...", "Ctrl+ALT+O")) ShowOpenProjectDialog(); if (ImGui.BeginMenu("Open recent project")) { ImGui.EndMenu(); } ImGui.Separator(); if (ImGui.MenuItem("Settings")) _settingsWindow.Open = true; ImGui.Separator(); if (ImGui.MenuItem("Exit")) Application.Get().Close(); ImGui.EndMenu(); } if(ImGui.BeginMenu("View", true)) { if(ImGui.MenuItem(ComponentEditWindow.s_WindowTitle)) _editor.ComponentEditWindow.Open = true; if(ImGui.MenuItem(ContentBrowserWindow.s_WindowTitle)) _editor.ComponentEditWindow.Open = true; if(ImGui.MenuItem(EditorViewportWindow.s_WindowTitle)) _editor.SceneViewportWindow.Open = true; if(ImGui.MenuItem(EntityHierarchyWindow.s_WindowTitle)) _editor.EntityHierarchyWindow.Open = true; if(ImGui.MenuItem(GameViewportWindow.s_WindowTitle)) _editor.GameViewportWindow.Open = true; if(ImGui.MenuItem(PropertiesWindow.s_WindowTitle)) _editor.PropertiesWindow.Open = true; if(ImGui.MenuItem(AssetViewer.s_WindowTitle)) _editor.AssetViewer.Open = true; if(ImGui.MenuItem(LogWindow.s_WindowTitle)) _editor.LogWindow.Open = true; ImGui.EndMenu(); } if(ImGui.BeginMenu("Tools", true)) { if(ImGui.MenuItem("Reload Scripts")) ScriptEngine.ReloadAssemblies(); ImGui.EndMenu(); } ImGui.EndMainMenuBar(); } #endregion ImGui #region Events /// Receive and dispatch global events public override void OnEvent(Event event) { EventDispatcher dispatcher = EventDispatcher(event); dispatcher.Dispatch(scope (e) => OnImGuiRender(e)); dispatcher.Dispatch(scope (e) => OnWindowResize(e)); dispatcher.Dispatch(scope (e) => OnKeyPressed(e)); dispatcher.Dispatch(scope (e) => OnMouseScrolled(e)); } private bool OnWindowResize(WindowResizeEvent e) { return false; } private void EditorViewportSizeChanged(Object sender, float2 viewportSize) { if(viewportSize.X <= 0 || viewportSize.Y <= 0) return; uint32 sizeX = (uint32)viewportSize.X; uint32 sizeY = (uint32)viewportSize.Y; _editorViewportTarget.Resize(sizeX, sizeY); _cameraTarget.Resize(sizeX, sizeY); _camera.OnViewportResize(sizeX, sizeY); _activeScene?.SetViewportSize(sizeX, sizeY); } private void GameViewportSizeChanged(Object sender, float2 viewportSize) { if(viewportSize.X <= 0 || viewportSize.Y <= 0) return; uint32 sizeX = (uint32)viewportSize.X; uint32 sizeY = (uint32)viewportSize.Y; _gameViewportTarget.Resize(sizeX, sizeY); _activeScene?.SetViewportSize(sizeX, sizeY); } private bool OnKeyPressed(KeyPressedEvent e) { // TODO: We need some kind of input system for stuff like this... bool control = Input.IsKeyPressed(Key.Control); bool alt = Input.IsKeyPressed(Key.Alt); bool shift = Input.IsKeyPressed(Key.Shift); if (!_camera.[Friend]BindMouse && control) { switch (e.KeyCode) { case .N: if (alt) // "Create new Project..." _openCreateProjectModal = true; else // "New Scene" CreateAndOpenNewScene(); return true; case .O: if (alt && control) // "Open Project..." ShowOpenProjectDialog(); else if (!alt && !control) // "Open Scene..." OpenScene(); return true; case .S: if (shift) // "Save Scene as..." SaveCurrentSceneAs(); else // "Save Scene" SaveCurrentScene(); return true; default: } } return false; } private bool OnMouseScrolled(MouseScrolledEvent e) { if (_camera.OnMouseScrolled(e)) return true; return false; } #endregion } }