using System; using System.Linq; using ImGui; using GlitchyEngine; using GlitchyEditor.EditWindows; using GlitchyEngine.Events; using GlitchyEngine.Math; using GlitchyEngine.Renderer; using GlitchyEngine.World; using GlitchyEngine.Content; using System.Collections; using GlitchyEngine.Renderer.Animation; using GlitchyEngine.Serialization; using System.IO; using GlitchyEngine.Core; using GlitchyEditor.Assets; using GlitchyEngine.Scripting; using GlitchyEditor.Platform.Windows; using GlitchyEditor.ImGui; 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(); 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; /// Returns whether or not the current scene can be saved. public bool CanSaveScene => _sceneState == .Edit; private AssetThumbnailManager _thumbnailManager ~ delete _; #if GE_EDITOR_IMGUI_DEMO private bool _showImguiDemoWindow; #endif [AllowAppend] public this(String[] args, EditorContentManager contentManager) : base("Editor") { Application.Instance.Window.IsVSync = true; ScriptEngine.ApplicationInfo.IsEditor = true; // TODO: Das muss weg _layer = this; _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; _thumbnailManager = new AssetThumbnailManager(_editorIcons); 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})."); } }, _editorIcons.Folder)); Editor.Instance.ContentBrowserWindow.InsertAssetCreatorSeparator(); Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("Scene", "New Scene", ".scene", new (path) => { using (Scene newScene = CreateNewScene()) { SaveScene(newScene, path); } }, _editorIcons.File_Scene)); Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("Shader File", "New Shader", ".hlsl", new (path) => { // TODO! }, _editorIcons.File_Hlsl)); Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("2D Effect", "New Effect", ".fx", new (path) => { // TODO! }, _editorIcons.File_Effect)); // TODO: 3D Effect Lit, 3D Effect unlit, etc. /*Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("3D Effect", "New Effect", ".fx", new (path) => { // TODO! }, _editorIcons.File_Effect));*/ Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("Material", "New Material", ".mat", new (path) => { using (Material newMaterial = new Material()) { _contentManager.SaveAssetToFile(newMaterial, path); } }, _editorIcons.File_Material)); Editor.Instance.ContentBrowserWindow.RegisterAssetCreator(new AssetCreator("Script", "New Entity", ".cs", new (path) => { String templatePath = scope .(); Directory.GetCurrentDirectory(templatePath); Path.Combine(templatePath, "Resources/Templates/ScriptTemplate.cs"); String scriptContent = scope .(); if (File.ReadAllText(templatePath, scriptContent, true) case .Err(let error)) { Log.EngineLogger.Error($"Failed to read template file from \"{templatePath}\". Error: {error}"); return; } scriptContent.Replace("[ProjectName]", CurrentProject.Name); String fileName = scope .(); Path.GetFileName(path, fileName); Log.EngineLogger.AssertDebug(fileName.EndsWith(".cs")); if (fileName.EndsWith(".cs")) fileName.RemoveFromEnd(3); String pascalFileName = scope .(fileName.Length); ToPascalCase(fileName, pascalFileName); String cSharpClassName = scope .(pascalFileName.Length); ToCSharpName(pascalFileName, cSharpClassName); scriptContent.Replace("[EntityName]", cSharpClassName); if (File.WriteAllText(path, scriptContent, false) case .Err) { Log.EngineLogger.Error($"Failed to save script file to \"{path}\"."); return; } }, _editorIcons.File_CSharpScript)); } private void ToCSharpName(String input, String output) { for (char32 c in input.DecodedChars) { if (c.IsLetterOrDigit || c == '_' || c == '@') { output.Append(c); } } let decode = System.Text.UTF8.Decode(output.Ptr, output.Length); // Make sure the name starts with a letter or _ or @ if (!(decode.c.IsLetter || decode.c == '_' || decode.c == '@')) { output.Insert(0, '@'); } } private void ToPascalCase(String input, String output) { bool newWord = true; for (char32 c in input.DecodedChars) { if (c.IsLetter) { if (newWord) { output.Append(c.ToUpper); newWord = false; } else { output.Append(c); } } else { newWord = true; if (c.IsWhiteSpace || c == '_') { continue; } else { output.Append(c); } } } } private void InitGraphics() { _context = Application.Instance.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, ownDebugName: new String("HDR Color")), .(.R32_UInt, ownDebugName: new String("EntityID")) ), DepthTargetDescription = .(.D24_UNorm_S8_UInt) }); _cameraTarget.[Friend]Identifier = "Camera Target"; _editorViewportTarget = new RenderTargetGroup(.() { Width = 100, Height = 100, ColorTargetDescriptions = TargetDescription[]( .(.R8G8B8A8_UNorm)) }); _editorViewportTarget.[Friend]Identifier = "Editor Viewport Target"; _gameViewportTarget = new RenderTargetGroup(.() { Width = 100, Height = 100, ColorTargetDescriptions = TargetDescription[]( .(.R8G8B8A8_UNorm)) }); _gameViewportTarget.[Friend]Identifier = "Game Viewport Target"; _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, _editorScene, _contentManager, _thumbnailManager); _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 = .Scripts | .EditMode; case .Play: updateMode = .Scripts | .Physics; 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.HasFlag(.EditMode)) { 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); // Explicitly set the swap chain as render target, because we end a frame with it bound and the RTV might stop existing if we resized the swapchain between frames. // TODO: maybe we need to be smarter about how we handle resizing in general: should the RenderTarget check during resize whether it is bound and update the GraphicsContext?! RenderCommand.SetRenderTarget(null); RenderCommand.SetBlendState(_alphaBlendState); RenderCommand.SetDepthStencilState(_depthStencilState); if (_editor.GameViewportWindow.Visible) { _gameSceneRenderer.Scene = _activeScene; _activeScene.SetViewportSize((uint32)_editor.GameViewportWindow.RenderedViewportSize.X, (uint32)_editor.GameViewportWindow.RenderedViewportSize.Y); _gameSceneRenderer.SetViewportSize((uint32)_editor.GameViewportWindow.RenderedViewportSize.X, (uint32)_editor.GameViewportWindow.RenderedViewportSize.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.IsEntitySelected(.(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, null, 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.IsEntitySelected(.(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, null, 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() { String title = scope String("Glitchy Engine"); if (_currentProject != null) { title.AppendF($" - {_currentProject.Name}"); } if (_editorScene != null) { title.AppendF($" | {_editorScene.Name}"); } Application.Instance.Window.Title = title; } #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(var 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; } String templatePath = scope .(); Directory.GetCurrentDirectory(templatePath); Path.Combine(templatePath, "Resources/ProjectTemplate"); Result newProjectResult = Project.CreateNewFromTemplate(workspaceDirectory, projectName, templatePath); // If the project couldn't be created or initialization failed, we delete the workspace directory. if (newProjectResult == .Err) { if (Directory.DelTree(workspaceDirectory) case .Err(out error)) { Log.EngineLogger.Error($"Failed to delete workspace ({workspaceDirectory}). ({error})"); } return .Err; } if (OpenProject(newProjectResult.Get()) case .Err) { Log.ClientLogger.Error($"Failed to open newly created project."); return .Err; } return .Ok; } public void OpenCreateNewProjectModal() { PopupService.Instance.OpenPopup("Create new Project", new => ShowCreateNewProjectModal); } private void ShowCreateNewProjectModal(out bool close) { close = false; static char8[128] projectNameBuffer = .(); static char8[1024] projectDirectoryBuffer = .(); // Always center this window when appearing var center = ImGui.GetMainViewport().GetCenter(); ImGui.SetNextWindowPos(center, .Appearing, .(0.5f, 0.5f)); ImGui.TextUnformatted("Project Name:"); ImGui.InputText("##projectName", &projectNameBuffer, projectNameBuffer.Count - 1); StringView projectName = StringView(&projectNameBuffer); if (projectName.IsWhiteSpace) { ImGui.TextColored(.(1, 0, 0, 1), "Project Name required!"); } 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 directory = StringView(&projectDirectoryBuffer); String target = scope String(); Path.Combine(target, directory, projectName); ImGui.NewLine(); ImGui.Text($"The Project will be in:\n{target}"); if (Directory.Exists(target) && !Directory.IsEmpty(target)) { ImGui.TextColored(.(1, 1, 0, 1), "The directory is not empty!"); } ImGui.NewLine(); ImGui.BeginDisabled(directory.IsWhiteSpace || projectName.IsWhiteSpace); if (ImGui.Button("Create")) { Result result = CreateNewProject(target, projectName); if (result case .Ok) close = true; } ImGui.EndDisabled(); ImGui.SameLine(); if (ImGui.Button("Cancel")) { close = true; } } 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; _editor.CurrentProject = _currentProject; String appAssemblyPath = scope String(); _contentManager.SetAssetCacheDirectory(_currentProject.GetScopedPath!(".cache")); _contentManager.SetAssetDirectory(_currentProject.AssetsFolder); _currentProject.PathInProject(appAssemblyPath, scope $".cache/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(); if (Directory.Exists(project.WorkspacePath)) { EditorApp.Instance.Settings.EditorSettings.LastOpenedProject = project.WorkspacePath; EditorApp.Instance.Settings.Save(); } 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); } append ScriptInstanceSerializer _scriptSerializer = .(); /// Starts the play mode for the current scene private void OnScenePlay() { Debug.Profiler.ProfileFunction!(); Log.EngineLogger.AssertDebug(_scriptSerializer.SerializedObjectCount == 0, "Somehow some entities are serialized."); if (EditorApp.Instance.Settings.EditorSettings.ClearLogOnPlay) Editor.Instance.LogWindow.ClearLog(); _scriptSerializer.SerializeScriptInstances(); _editor.SceneViewportWindow.EditorMode = false; _sceneState = .Play; using (Scene runtimeScene = new Scene()) { _editorScene.CopyTo(runtimeScene, true); SetActiveScene(runtimeScene, startRuntime: true, startSimulation: true, newPlayMode: .Play); _scriptSerializer.DeserializeScriptInstances(); } _editor.CurrentScene = _activeScene; if (EditorApp.Instance.Settings.EditorSettings.SwitchToPlayerOnPlay) SwitchToPlayWindow(); } enum PlayMode { Play, Editor, Simulation } /// Activates the given scene. /// @param scene The scene to be activated. /// @param startRuntime If set to true, the script runtime will be initialized for the given scene. /// @param startSimulation If set to true, the physics simulation will be initialized for the given scene. private void SetActiveScene(Scene scene, bool startRuntime, bool startSimulation, PlayMode? newPlayMode = null) { _activeScene?.Stop(); if (newPlayMode != null) { switch (newPlayMode.Value) { case .Play: ScriptEngine.ApplicationInfo.IsInPlayMode = true; case .Editor: ScriptEngine.ApplicationInfo.IsInEditMode = true; case .Simulation: // No scripts in Simulation ScriptEngine.ApplicationInfo.IsInEditMode = false; ScriptEngine.ApplicationInfo.IsInPlayMode = false; } } if (scene != null) { scene.Start(startRuntime, startSimulation); } SetReference!(_activeScene, scene); } /// 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); SetActiveScene(simulationScene, startRuntime: false, startSimulation: true, newPlayMode: .Simulation); } _editor.CurrentScene = _activeScene; if (EditorApp.Instance.Settings.EditorSettings.SwitchToPlayerOnSimulate) SwitchToPlayWindow(); } /// Pauses the scene private void OnScenePause() { _isPaused = true; if (EditorApp.Instance.Settings.EditorSettings.SwitchToEditorOnPause) SwitchToEditorWindow(); } /// Resumes the simulation / game private void OnSceneResume() { _isPaused = false; if (EditorApp.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 (_activeScene == null) return; SetActiveScene(_editorScene, startRuntime: true, startSimulation: false, newPlayMode: .Editor); // TODO: _editorScene.DeserializeScripts(scriptData); _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-ratios will reset once we go back * into "Editor"-mode (because Game-Mode works on a copy of the scene). */ GameViewportSizeChanged(null, _editor.GameViewportWindow.RenderedViewportSize); } if (EditorApp.Instance.Settings.EditorSettings.SwitchToEditorOnStop) SwitchToEditorWindow(); // Reconstruct state before play _scriptSerializer.DeserializeScriptInstances(); _scriptSerializer.Clear(); } /// Stops the scene and cleans up the subsystems to allow loading another scene. private void CloseCurrentScene() { // Clear serialized data, so that we don't waste time deserializing it. _scriptSerializer.Clear(); OnSceneStop(); } /// Creates a new scene and openes it. private void CreateAndOpenNewScene() { if (_currentProject == null) return; 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, ScriptInstanceSerializer serializedObjects = null) { _editorScene?.Stop(); SetReference!(_editorScene, scene); _editor.CurrentScene = _editorScene; _editor.EditorScene = _editorScene; SetReference!(_activeScene, _editorScene); if (_editorScene != null) { _editorScene.Start(startRuntime: true, startSimulation: false); if (serializedObjects != null) { // Reconstruct state serializedObjects.DeserializeScriptInstances(); } } UpdateWindowTitle(); } /// 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 (_currentProject == null) return; if (!CanSaveScene) { Log.ClientLogger.Error("Scene can't be saved while playing the game!"); return; } bool savedScene; if (SceneFilePath.IsWhiteSpace) { savedScene = SaveCurrentSceneAs(); } else { SaveScene(_editorScene, SceneFilePath); // TODO: SaveScene can probably fail too! savedScene = true; } if (savedScene) { // 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 bool SaveCurrentSceneAs() { if (_currentProject == null) return false; if (!CanSaveScene) { Log.ClientLogger.Error("Scene can't be saved while playing the game!"); return false; } SaveFileDialog sfd = scope .(); sfd.InitialDirectory = _currentProject.AssetsFolder; sfd.SetFilter("scene file (*.scene)|*.scene"); if (sfd.ShowDialog() case .Ok(let result) && result == .OK) { SceneFilePath = sfd.FileNames[0]; SaveCurrentScene(); return true; } return false; } /// Opens a open file dialog and load the scene selected by the user specified. private void OpenScene() { if (_currentProject == null) return; OpenFileDialog ofd = scope .(); ofd.InitialDirectory = _currentProject.AssetsFolder; ofd.SetFilter("scene file (*.scene)|*.scene"); 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(); 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, serializer.ScriptSerializer); 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); if (_isDraggingFromOutside not case .None) { // Reset drop effect, so the drop targets have to set it themselves (and don't "leak" it). SetDropEffect(.None); if (ImGui.BeginDragDropSource(.SourceExtern | .SourceAutoExpirePayload | .SourceNoPreviewTooltip)) { void* ptr = Internal.UnsafeCastToPtr(_droppedFiles); ImGui.SetDragDropPayload(.ExternFiles, &ptr, sizeof(int)); ImGui.EndDragDropSource(); } if (_isDraggingFromOutside case .Dropping) { _isDraggingFromOutside = .None; } } else { // TODO: Can Imgui handle this, somehow? DeleteContainerAndItems!(_droppedFiles); _droppedFiles = null; } DrawMainMenuBar(); #if GE_EDITOR_IMGUI_DEMO if (_showImguiDemoWindow) { ImGui.ShowDemoWindow(); } #endif EditorApp.Instance.BackgroundTaskManager.ImGuiRender(); _editor.SceneViewportWindow.RenderTarget = _editorViewportTarget; _editor.GameViewportWindow.RenderTarget = _gameViewportTarget; _editor.Update(); return false; } /// Shows the window with Play/Pause, etc. buttons private void ShowPlayControls() { 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); 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; } 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()) { if (ImGui.BeginTable("time_step_table", 2)) { ImGui.TableNextRow(); ImGui.TableNextColumn(); ImGui.TextUnformatted("Time step:"); ImGui.TableNextColumn(); float itemWidth = ImGui.CalcTextSize("1000.000 FPS").x; ImGui.SetNextItemWidth(itemWidth); ImGui.DragFloat("##timestep", &_fixedTimestep, 0.001f, 0.001f, 1000.0f, "%.4fs"); ImGui.TableNextRow(); ImGui.TableNextColumn(); float fps = 1.0f / _fixedTimestep; ImGui.TextUnformatted("Frame rate:"); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(itemWidth); if (ImGui.DragFloat("##fps", &fps, 1.0f, 0.001f, 1000.0f, "%.3f FPS")) { _fixedTimestep = 1.0f / fps; } ImGui.EndTable(); } 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.PopStyleColor(3); ImGui.PopStyleVar(1); } 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 ShowOpenRecentProjectMenu() { let recentProjects = EditorApp.Instance.Settings.EditorSettings.RecentProjects; if (recentProjects == null) return; int i = 0; for (let workspacePath in recentProjects) { i++; if (!Directory.Exists(workspacePath)) { delete workspacePath; @workspacePath.Remove(); continue; } if (ImGui.MenuItem(scope $"{i}: {workspacePath}")) { LoadAndOpenProject(workspacePath); } } } private void DrawMainMenuBar() { ImGui.BeginMainMenuBar(); if (ImGui.BeginMenu("File", true)) { using (ImGui.DisabledScope(_currentProject == null)) { 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(); using (ImGui.DisabledScope(!CanSaveScene)) { 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")) OpenCreateNewProjectModal(); if (ImGui.MenuItem("Open Project...", "Ctrl+ALT+O")) ShowOpenProjectDialog(); if (ImGui.BeginMenu("Open recent project")) { ShowOpenRecentProjectMenu(); ImGui.EndMenu(); } ImGui.Separator(); if (ImGui.MenuItem("Settings")) Editor.Instance.SettingsWindow.Open = true; ImGui.Separator(); if (ImGui.MenuItem("Exit")) Application.Instance.Close(); ImGui.EndMenu(); } if(ImGui.BeginMenu("View", true)) { if(ImGui.MenuItem(ContentBrowserWindow.s_WindowTitle)) _editor.ContentBrowserWindow.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(InspectorWindow.s_WindowTitle)) _editor.InspectorWindow.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(); #if GE_EDITOR_IMGUI_DEMO ImGui.Checkbox("Show ImGui Demo", &_showImguiDemoWindow); #endif ImGui.EndMenu(); } ShowPlayControls(); 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)); dispatcher.Dispatch(scope (e) => OnDragDrop(e)); dispatcher.Dispatch(scope (e) => OnSettingsApplied(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..." OpenCreateNewProjectModal(); 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; } enum DragDropState { None, Dragging, Dropping } private DragDropState _isDraggingFromOutside; private DropEffect _effectWhenDropping = .None; private static EditorLayer _layer; private List _droppedFiles; public static void SetDropEffect(DropEffect effect) { if (_layer._isDraggingFromOutside == .None) { Log.EngineLogger.Warning("Setting drop effect, even though the user is currently not dragging."); } _layer._effectWhenDropping = effect; } private bool OnDragDrop(DragDropEvent e) { if (all(e.CursorPosition != .(-1,-1))) { ImGui.GetIO().MousePos = .(e.CursorPosition.X, e.CursorPosition.Y); } if (e.FileNames.IsEmpty) { DeleteContainerAndItems!(_droppedFiles); _droppedFiles = null; } else { if (_droppedFiles == null) { _droppedFiles = new List(); } int firstIndexToRemove = 0; for (StringView filePath in e.FileNames) { int index = _droppedFiles.IndexOfAlt(filePath); if (index == -1) { index = _droppedFiles.Count; _droppedFiles.Add(new String(filePath)); } // Make sure, all paths that are in e.FileNames are before firstIndexToRemove Swap!(_droppedFiles[firstIndexToRemove], _droppedFiles[index]); firstIndexToRemove++; } // Remove all paths beyond firstIndexToRemove for (int i = _droppedFiles.Count; i > firstIndexToRemove; i--) { delete _droppedFiles.PopBack(); } } switch (e.DragDropType) { case .Enter: _isDraggingFromOutside = .Dragging; ImGui.GetIO().MouseDown[0] = true; case .Over: case .Leave: _isDraggingFromOutside = .None; ImGui.ClearDragDrop(); case .Drop: _isDraggingFromOutside = .Dropping; ImGui.GetIO().MouseDown[0] = false; } e.OutDropEffect = _effectWhenDropping; return true; } #endregion bool OnSettingsApplied(SettingsAppliedEvent settingsAppliedEvent) { #if DEBUG // In DEBUG we can change whether we use ScriptCore as .dll or .csproj _currentProject?.FixupScriptCorePath(); #endif return false; } } }