// Copyright (c) 2012-2022 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 Float3(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();
Editor.UI.AddStatusMessage("Saved!");
}
///
/// 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();
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.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)
node.ParentNode = parentNode;
if (parentNode == null)
{
// 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!");
}
}
}
}