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.Properties
    Is always enabled and captures basic properties of the object (Parent, Transform, Name…)

  • GameObjectUndoFlags.Components
    Captures the components list of the object

  • GameObjectUndoFlags.Children
    Captures all children of the object, this can become very expensive for complex scene hierarchies, so use it only if you have to.

  • GameObjectUndoFlags.All
    Shortcut 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 );
}





Created 23 Jan 2025
Updated 15 Jun 2025