Undo System
The scope based system works by creating a snapshot of a change set when the scope is entered and another one when the scope is disposed of. The system will automatically take care of restoring the state on undo/redo.
Scope Based Undo
The scope based system works by creating a snapshot of a change set when the scope is entered and another one when the scope is disposed of. The system will automatically take care of restoring the state on undo/redo.
A basic blank scope can be created as follows:
// In Game & Editor Code
var undoScope = Scene.Editor?.UndoScope( "You Action Name");
// In Editor Code
var undoScope = SceneEditorSession.Active.UndoScope( "Your Action Name" );
// Push() will turn the scope into an disposable
using ( SceneEditorSession.Active.UndoScope( "You Action Name" ).Push() )
{
// Actions that modify the scene
}
var undoScope = SceneEditorSession.Active.UndoScope( "You Action Name" );
using ( undoScope.Push() )
{
// Actions that modify the scene
}
{
using var undoScope = SceneEditorSession.Active.UndoScope( "You Action Name" ).Push();
// Actions that modify the scene
}
However, these scopes will not capture anything yet.
You will have to tell the scope what objects you are about to modify. To ensure the best performance you should keep the set of captured objects as small as possible.
GameObjects
To capture GameObject changes use undoScope.WithGameObjectChanges().
You also have to specify what part of the GameObject(s) you you would like to capture.
-
GameObjectUndoFlags.PropertiesIs always enabled and captures basic properties of the object (Parent, Transform, Nameโฆ) -
GameObjectUndoFlags.ComponentsCaptures the components list of the object -
GameObjectUndoFlags.ChildrenCaptures all children of the object, this can become very expensive for complex scene hierarchies, so use it only if you have to. -
GameObjectUndoFlags.AllShortcut to capture everything\
using var undoScope = SceneEditorSession.Active.UndoScope( "You Action Name" )
.WithGameObjectChanges( gameObject, GameObjectUndoFlags.Properties | GameObjectUndoFlags.Components)
.Push();
To capture GameObject creation you can use GameObjectCreations().
using ( SceneEditorSession.Active.UndoScope( "Create Empty" ).WithGameObjectCreations().Push() )
{
var go = new GameObject( true, "Object" );
}
Similarly you can capture objects that are about to be destroyed.
using ( SceneEditorSession.Active.UndoScope( "Delete Object(s)" ).WithGameObjectDestructions( selectedGos ).Push() )
{
foreach ( var go in selectedGos )
{
if ( !go.IsDeletable() )
return;
go.Destroy();
}
}
Components
Components offer similar functionality. Components are always captured as a whole so there are no flags that need to be specified.
using ( SceneEditorSession.Active.UndoScope( "Drop Material" ).WithComponentChanges( c as Component ).Push() )
{
c.SetMaterial( material, trace.Triangle );
}
Capture Creation
using ( SceneEditorSession.Active.UndoScope( "Add Component(s)" ).WithComponentCreations().Push() )
{
var component = go.Components.Create( componentType );
createdComponents.Add( component );
}
Capture Destruction
using ( SceneEditorSession.Active.UndoScope( $"Cut Component" ).WithComponentDestructions( component ).Push() )
{
component.CopyToClipboard();
component.Destroy();
}
Selections
Selections are always captured and restored on undo/redo
Chaining
As you may have already noticed you can chain the different functions together to capture a variety of changes and events.
var undoScope = SceneEditorSession.Active.UndoScope( "Extract Faces" )
.WithComponentChanges( components )
.WithGameObjectDestructions( gameObjects )
.WithGameObjectCreations();
using ( undoScope.Push() )
More Examples
Actions that span multiple frames
If you have an action that spans multiple frames (e.g. dragging something around) you can use the following pattern to create an undo.
public class BoxColliderTool : EditorTool<BoxCollider>
{
private IDisposable _componentUndoScope;
public override void OnUpdate()
{
var boxCollider = GetSelectedComponent<BoxCollider>();
if ( boxCollider == null )
return;
var currentBox = BBox.FromPositionAndSize( boxCollider.Center, boxCollider.Scale );
using ( Gizmo.Scope( "Box Collider Editor", boxCollider.WorldTransform ) )
{
if ( Gizmo.Control.BoundingBox( "Bounds", currentBox, out var newBox ) )
{
if ( _componentUndoScope == null )
{
// Create scope if it not exists
_componentUndoScope = SceneEditorSession.Active.UndoScope( "Resize Box Collider" )
.WithComponentChanges( boxCollider )
.Push();
}
boxCollider.Center = newBox.Center;
boxCollider.Scale = newBox.Size;
}
// Dispose scope when mouse is release
if ( Gizmo.WasLeftMouseReleased )
{
_componentUndoScope?.Dispose();
_componentUndoScope = null;
}
}
}
}
Group GameObjects Action
var undoScope = SceneEditorSession.Active.UndoScope( "Group Objects" )
.WithGameObjectChanges( selection, GameObjectUndoFlags.Properties )
.WithGameObjectCreations();
using ( undoScope.Push() )
{
var go = new GameObject();
go.WorldTransform = first.WorldTransform;
go.MakeNameUnique();
first.AddSibling( go, false );
for ( var i = 0; i < selection.Length; i++ )
{
selection[i].SetParent( go, true );
}
EditorScene.Selection.Clear();
EditorScene.Selection.Add( go );
}
Referenced API
Canonical API pages mentioned in this guide.
An Axis Aligned Bounding Box.
No summary available.
Creates an AABB at given position `center` and given `size` which acts as a diameter of a sphere contained within the AABB.
Creates an AABB at given position `center` and given `size` a.k.a. "extents".
No summary available.
Allows access to the scene's editor session from the game. This will be null if there is no editor session active on this scene.
No summary available.
Current selection set for the active map