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( "Your Action Name");
// In Editor Code
var undoScope = SceneEditorSession.Active.UndoScope( "Your Action Name" );
// Push() will turn the scope into a disposable
using ( SceneEditorSession.Active.UndoScope( "Your Action Name" ).Push() )
{
// Actions that modify the scene
}
var undoScope = SceneEditorSession.Active.UndoScope( "Your Action Name" );
using ( undoScope.Push() )
{
// Actions that modify the scene
}
{
using var undoScope = SceneEditorSession.Active.UndoScope( "Your 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.
To capture GameObject changes use undoScope.WithGameObjectChanges().
You also have to specify what part of the GameObject(s) you would like to capture.
GameObjectUndoFlags.PropertiesGameObjectUndoFlags.ComponentsGameObjectUndoFlags.ChildrenGameObjectUndoFlags.Allusing var undoScope = SceneEditorSession.Active.UndoScope( "Your Action Name" )
.WithGameObjectChanges( gameObject, GameObjectUndoFlags.Properties | GameObjectUndoFlags.Components)
.Push();
To capture GameObject creation you can use WithGameObjectCreations().
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 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 );
}
using ( SceneEditorSession.Active.UndoScope( "Add Component(s)" ).WithComponentCreations().Push() )
{
var component = go.Components.Create( componentType );
createdComponents.Add( component );
}
using ( SceneEditorSession.Active.UndoScope( $"Cut Component" ).WithComponentDestructions( component ).Push() )
{
component.CopyToClipboard();
component.Destroy();
}
Selections are always captured and restored on undo/redo
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() )
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 the scope if it does not exist yet
_componentUndoScope = SceneEditorSession.Active.UndoScope( "Resize Box Collider" )
.WithComponentChanges( boxCollider )
.Push();
}
boxCollider.Center = newBox.Center;
boxCollider.Scale = newBox.Size;
}
// Dispose the scope when the mouse is released
if ( Gizmo.WasLeftMouseReleased )
{
_componentUndoScope?.Dispose();
_componentUndoScope = null;
}
}
}
}
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 );
}