// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.IO; using FlaxEditor.SceneGraph; using FlaxEditor.SceneGraph.Actors; using FlaxEngine; using Object = FlaxEngine.Object; namespace FlaxEditor.Modules { /// /// Scenes and actors management module. /// /// public sealed class SceneModule : EditorModule { /// /// The root node for the scene graph created for the loaded scenes and actors hierarchy. /// /// public class ScenesRootNode : RootNode { private readonly Editor _editor; /// public ScenesRootNode() { _editor = Editor.Instance; } /// public override void Spawn(Actor actor, Actor parent) { _editor.SceneEditing.Spawn(actor, parent); } /// public override Undo Undo => Editor.Instance.Undo; /// public override List Selection => _editor.SceneEditing.Selection; } /// /// The root tree node for the whole scene graph. /// public ScenesRootNode Root; /// /// Occurs when actor gets removed. Editor and all submodules should remove references to that actor. /// public event Action ActorRemoved; internal SceneModule(Editor editor) : base(editor) { // After editor cache but before the windows InitOrder = -900; } /// /// Marks the scene as modified. /// /// The scene. public void MarkSceneEdited(Scene scene) { MarkSceneEdited(GetActorNode(scene) as SceneNode); } /// /// Marks the scene as modified. /// /// The scene. public void MarkSceneEdited(SceneNode scene) { if (scene != null) scene.IsEdited = true; } /// /// Marks the scenes as modified. /// /// The scenes. public void MarkSceneEdited(IEnumerable scenes) { foreach (var scene in scenes) MarkSceneEdited(scene); } /// /// Marks all the scenes as modified. /// public void MarkAllScenesEdited() { MarkSceneEdited(Level.Scenes); } /// /// Determines whether the specified scene is edited. /// /// The scene. /// /// true if the specified scene is edited; otherwise, false. /// public bool IsEdited(Scene scene) { var node = GetActorNode(scene) as SceneNode; return node?.IsEdited ?? false; } /// /// Determines whether any scene is edited. /// /// /// true if any scene is edited; otherwise, false. /// public bool IsEdited() { foreach (var scene in Root.ChildNodes) { if (scene is SceneNode node && node.IsEdited) return true; } return false; } /// /// Determines whether every scene is edited. /// /// /// true if every scene is edited; otherwise, false. /// public bool IsEverySceneEdited() { foreach (var scene in Root.ChildNodes) { if (scene is SceneNode node && !node.IsEdited) return false; } return true; } /// /// Creates the new scene file. The default scene contains set of simple actors. /// /// The path. public void CreateSceneFile(string path) { // Create a sample scene var scene = new Scene { StaticFlags = StaticFlags.FullyStatic }; // var sun = scene.AddChild(); sun.Name = "Sun"; sun.LocalPosition = new Vector3(40, 160, 0); sun.LocalEulerAngles = new Vector3(45, 0, 0); sun.StaticFlags = StaticFlags.FullyStatic; // var sky = scene.AddChild(); sky.Name = "Sky"; sky.LocalPosition = new Vector3(40, 150, 0); sky.SunLight = sun; sky.StaticFlags = StaticFlags.FullyStatic; // var skyLight = scene.AddChild(); skyLight.Mode = SkyLight.Modes.CustomTexture; skyLight.Brightness = 2.5f; skyLight.CustomTexture = FlaxEngine.Content.LoadAsyncInternal(EditorAssets.DefaultSkyCubeTexture); skyLight.StaticFlags = StaticFlags.FullyStatic; // var floor = scene.AddChild(); floor.Name = "Floor"; floor.Scale = new Vector3(4, 0.5f, 4); floor.Model = FlaxEngine.Content.LoadAsync(StringUtils.CombinePaths(Globals.EngineContentFolder, "Editor/Primitives/Cube.flax")); if (floor.Model) { floor.Model.WaitForLoaded(); floor.SetMaterial(0, FlaxEngine.Content.LoadAsync(StringUtils.CombinePaths(Globals.EngineContentFolder, "Engine/WhiteMaterial.flax"))); } floor.StaticFlags = StaticFlags.FullyStatic; // var cam = scene.AddChild(); cam.Name = "Camera"; cam.Position = new Vector3(0, 150, -300); // Serialize var bytes = Level.SaveSceneToBytes(scene); // Cleanup Object.Destroy(ref scene); if (bytes == null || bytes.Length == 0) throw new Exception("Failed to serialize scene."); // Write to file using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) fileStream.Write(bytes, 0, bytes.Length); } /// /// Saves scene (async). /// /// Scene to save. public void SaveScene(Scene scene) { SaveScene(GetActorNode(scene) as SceneNode); } /// /// Saves scene (async). /// /// Scene to save. public void SaveScene(SceneNode scene) { if (!scene.IsEdited) return; scene.IsEdited = false; Level.SaveSceneAsync(scene.Scene); } /// /// Saves all open scenes (async). /// public void SaveScenes() { if (!IsEdited()) return; foreach (var scene in Root.ChildNodes) { if (scene is SceneNode node) node.IsEdited = false; } Level.SaveAllScenesAsync(); } /// /// Opens scene (async). /// /// Scene ID /// True if don't close opened scenes and just add new scene to them, otherwise will release current scenes and load single one. public void OpenScene(Guid sceneId, bool additive = false) { // Check if cannot change scene now if (!Editor.StateMachine.CurrentState.CanChangeScene) return; // In play-mode Editor mocks the level streaming script if (Editor.IsPlayMode) { if (!additive) Level.UnloadAllScenesAsync(); Level.LoadSceneAsync(sceneId); return; } if (!additive) { // Ensure to save all pending changes if (CheckSaveBeforeClose()) return; } // Load scene Editor.StateMachine.ChangingScenesState.LoadScene(sceneId, additive); } /// /// Closes scene (async). /// /// The scene. public void CloseScene(Scene scene) { // Check if cannot change scene now if (!Editor.StateMachine.CurrentState.CanChangeScene) return; // In play-mode Editor mocks the level streaming script if (Editor.IsPlayMode) { Level.UnloadSceneAsync(scene); return; } // Ensure to save all pending changes if (CheckSaveBeforeClose()) return; // Unload scene Editor.StateMachine.ChangingScenesState.UnloadScene(scene); } /// /// Closes all opened scene (async). /// public void CloseAllScenes() { // Check if cannot change scene now if (!Editor.StateMachine.CurrentState.CanChangeScene) return; // In play-mode Editor mocks the level streaming script if (Editor.IsPlayMode) { Level.UnloadAllScenesAsync(); return; } // Ensure to save all pending changes if (CheckSaveBeforeClose()) return; // Unload scenes Editor.StateMachine.ChangingScenesState.UnloadScene(Level.Scenes); } /// /// Show save before scene load/unload action. /// /// The scene that will be closed. /// True if action has been canceled, otherwise false public bool CheckSaveBeforeClose(SceneNode scene) { // Check if scene was edited after last saving if (scene.IsEdited) { // Ask user for further action var result = MessageBox.Show( string.Format("Scene \'{0}\' has been edited. Save before closing?", scene.Name), "Close without saving?", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question ); if (result == DialogResult.OK || result == DialogResult.Yes) { // Save and close SaveScene(scene); } else if (result == DialogResult.Cancel || result == DialogResult.Abort) { // Cancel closing return true; } } ClearRefsToSceneObjects(); return false; } /// /// Show save before scene load/unload action. /// /// True if action has been canceled, otherwise false public bool CheckSaveBeforeClose() { // Check if scene was edited after last saving if (IsEdited()) { // Ask user for further action var scenes = Level.Scenes; var result = MessageBox.Show( scenes.Length == 1 ? string.Format("Scene \'{0}\' has been edited. Save before closing?", scenes[0].Name) : string.Format("{0} scenes have been edited. Save before closing?", scenes.Length), "Close without saving?", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question ); if (result == DialogResult.OK || result == DialogResult.Yes) { // Save and close SaveScenes(); } else if (result == DialogResult.Cancel || result == DialogResult.Abort) { // Cancel closing return true; } } ClearRefsToSceneObjects(); return false; } /// /// Clears references to the scene objects by the editor. Deselects objects. /// /// True if cleanup all data (including serialized and cached data). Otherwise will just clear living references to the scene objects. public void ClearRefsToSceneObjects(bool fullCleanup = false) { Profiler.BeginEvent("SceneModule.ClearRefsToSceneObjects"); Editor.SceneEditing.Deselect(); if (fullCleanup) { Undo.Clear(); } Profiler.EndEvent(); } private void OnSceneLoaded(Scene scene, Guid sceneId) { var startTime = DateTime.UtcNow; // Build scene tree var sceneNode = SceneGraphFactory.BuildSceneTree(scene); var treeNode = sceneNode.TreeNode; treeNode.IsLayoutLocked = true; treeNode.Expand(true); // Add to the tree var rootNode = Root.TreeNode; rootNode.IsLayoutLocked = true; // sceneNode.ParentNode = Root; rootNode.SortChildren(); // treeNode.UnlockChildrenRecursive(); rootNode.IsLayoutLocked = false; rootNode.Parent.PerformLayout(); var endTime = DateTime.UtcNow; var milliseconds = (int)(endTime - startTime).TotalMilliseconds; Editor.Log($"Created graph for scene \'{scene.Name}\' in {milliseconds} ms"); } private void OnSceneUnloading(Scene scene, Guid sceneId) { // Find scene tree node var node = Root.FindChildActor(scene); if (node != null) { Editor.Log($"Cleanup graph for scene \'{scene.Name}\'"); // Cleanup var selection = Editor.SceneEditing.Selection; var hasSceneSelection = false; for (int i = 0; i < selection.Count; i++) { if (selection[i].ParentScene == node) { hasSceneSelection = true; break; } } if (hasSceneSelection) { var newSelection = new List(); for (int i = 0; i < selection.Count; i++) { if (selection[i].ParentScene != node) newSelection.Add(selection[i]); } Editor.SceneEditing.Select(newSelection); } node.Dispose(); } } private void OnActorSpawned(Actor actor) { // Skip for not loaded scenes (spawning actors during scene loading in script Start function) var sceneNode = GetActorNode(actor.Scene); if (sceneNode == null) return; // Skip for missing parent var parent = actor.Parent; if (parent == null) return; var parentNode = GetActorNode(parent); if (parentNode == null) { // Missing parent node when adding child actor to not spawned or unlinked actor return; } var node = SceneGraphFactory.BuildActorNode(actor); if (node != null) { node.TreeNode.UnlockChildrenRecursive(); node.ParentNode = parentNode; } } private void OnActorDeleted(Actor actor) { var node = GetActorNode(actor); if (node != null) { OnActorDeleted(node); } } private void OnActorDeleted(ActorNode node) { for (int i = 0; i < node.ChildNodes.Count; i++) { if (node.ChildNodes[i] is ActorNode child) { i--; OnActorDeleted(child); } } ActorRemoved?.Invoke(node); // Cleanup part of the graph node.Dispose(); } private void OnActorParentChanged(Actor actor, Actor prevParent) { ActorNode node = null; var parentNode = GetActorNode(actor.Parent); // Try use previous parent actor to find actor node var prevParentNode = GetActorNode(prevParent); if (prevParentNode != null) { // If should be one of the children node = prevParentNode.FindChildActor(actor); // Search whole tree if node was not found if (node == null) { node = GetActorNode(actor); } } else if (parentNode != null) { // Create new node for that actor (user may unlink it from the scene before and now link it) node = SceneGraphFactory.BuildActorNode(actor); } if (node == null) return; // Get the new parent node (may be missing) if (parentNode != null) { // Change parent node.TreeNode.UnlockChildrenRecursive(); node.ParentNode = parentNode; } else { // Check if actor is selected in editor if (Editor.SceneEditing.Selection.Contains(node)) Editor.SceneEditing.Deselect(); // Remove node (user may unlink actor from the scene but not destroy the actor) node.Dispose(); } } private void OnActorOrderInParentChanged(Actor actor) { ActorNode node = GetActorNode(actor); node?.TreeNode.OnOrderInParentChanged(); } private void OnActorNameChanged(Actor actor) { ActorNode node = GetActorNode(actor); node?.TreeNode.OnNameChanged(); } private void OnActorActiveChanged(Actor actor) { //ActorNode node = GetActorNode(actor); //node?.TreeNode.OnActiveChanged(); } /// /// Gets the actor node. /// /// The actor. /// Found actor node or null if missing. Actor may not be linked to the scene tree so node won't be found in that case. public ActorNode GetActorNode(Actor actor) { if (actor == null) return null; // ActorNode has the same ID as actor does return SceneGraphFactory.FindNode(actor.ID) as ActorNode; } /// /// Gets the actor node. /// /// The actor id. /// Found actor node or null if missing. Actor may not be linked to the scene tree so node won't be found in that case. public ActorNode GetActorNode(Guid actorId) { // ActorNode has the same ID as actor does return SceneGraphFactory.FindNode(actorId) as ActorNode; } /// /// Executes the custom action on the graph nodes. /// /// The callback. public void ExecuteOnGraph(SceneGraphTools.GraphExecuteCallbackDelegate callback) { Root.ExecuteOnGraph(callback); } /// public override void OnInit() { Root = new ScenesRootNode(); // Bind events Level.SceneLoaded += OnSceneLoaded; Level.SceneUnloading += OnSceneUnloading; Level.ActorSpawned += OnActorSpawned; Level.ActorDeleted += OnActorDeleted; Level.ActorParentChanged += OnActorParentChanged; Level.ActorOrderInParentChanged += OnActorOrderInParentChanged; Level.ActorNameChanged += OnActorNameChanged; Level.ActorActiveChanged += OnActorActiveChanged; } /// public override void OnExit() { // Unbind events Level.SceneLoaded -= OnSceneLoaded; Level.SceneUnloading -= OnSceneUnloading; Level.ActorSpawned -= OnActorSpawned; Level.ActorDeleted -= OnActorDeleted; Level.ActorParentChanged -= OnActorParentChanged; Level.ActorOrderInParentChanged -= OnActorOrderInParentChanged; Level.ActorNameChanged -= OnActorNameChanged; Level.ActorActiveChanged -= OnActorActiveChanged; // Cleanup graph Root.Dispose(); if (SceneGraphFactory.Nodes.Count > 0) { Editor.LogWarning("Not all scene graph nodes has been disposed!"); } } } }