o3de / o3de

Open 3D Engine (O3DE) is an Apache 2.0-licensed multi-platform 3D engine that enables developers and content creators to build AAA games, cinema-quality 3D worlds, and high-fidelity simulations without any fees or commercial obligations.

Home Page:https://o3de.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Transform component changes done with Python do not persist after the level is saved

galibzon opened this issue · comments

Describe the bug
In the Editor, when I try to modify the location of entities with python everything appears to be working fine.
After saving and reloading the level I notice that their expected Transform data never got saved.

Assets required
Create a new level or open an existing one.

editorEntityTransformUpdate.zip
Attach is a python script named editorEntityTransformUpdate.py. You can execute it as:

pyRunFile editorEntityTransformUpdate.py Ninja -z 1.0

The command above will add a new entity named Ninja if it doesn't exist already.
The entity will be relocated to world position 0,0,1

Steps to reproduce
0. Please use a project like AutomatedTesting, because the Python utils import multiplayer

  1. In the Editor create a new level or open an existing level
  2. Execute the attached python script as suggested above
  3. You'll see the entity is create and it location is 0,0,1 (As expected).
  4. Save the level
  5. Close the Editor
  6. Reopen the Level
  7. You'll see that the entity named 'Ninja' is NOT located at 0,0,1

Expected behavior
In step 7, the entity named 'Ninja' should be located at 0,0,1

Actual behavior
In step 7, the entity named 'Ninja' should is located at some random location

Screenshots/Video
N/A

Found in Branch
o3de development branch:
commit 9292148 (origin/development, development)
Date: Thu May 2 22:51:58 2024 -0400

Desktop/Device (please complete the following information):

  • Device: PC
  • OS: Windows
  • Version 11
  • CPU AMD Ryzen Threadripper 3970X 32-Core Processor 3.70 GHz
  • GPU NVidia RTX 4090 (24GB VRAM). Studio Driver Version 546.01
  • Memory 128GB
  • Storage: WD_BLACK 2TB SN850 NVMe M2 SDD PCIe 4.0

Additional context
Another example of executing the script to locate the 'Ninja' entity at 1,2,1

pyRunFile editorEntityTransformUpdate.py Ninja -x 1.0 -y 2.0 -z 1.0

The interesting thing is that when you manually view the value of the Transform component in the Inspector it has the value you set in Python. You save the level and the changes don't make it.

I think this needs better documentation but whats probably happening here is you are affecting the visual representation of the entities int he world, but not the actual saved data

To be more specific, to solve this, you probably need to use the undo system.

My guess is that its editing the transform component, but not setting the component to dirty so its not propagated to the prefab file
the components and entities you see in the viewport are a representation or visualization of the actual source of truth data, which is a prefab heirarchy (JSON) in memory.

so you can jiggle those around as much as you want but you aren't actually affecting the data that's saved to disk, just editing the visual layer, not the data layer.

marking an entity dirty, or using the actual Transform Undo/Redo system is what applies it to the actual data in memory. Ideally you would Begin an undo group before you made your changes, then mark the stuff you poked at dirty, then end undo
as in

editor.ToolsApplicationRequestBus(bus.Broadcast, 'BeginUndoBatch', "Modify entities")
#... modify them here.
editor.ToolsApplicationRequestBus(bus.Broadcast, 'AddDirtyEntity', entity_modified_id)
#... more modifications can occur, as many as you wish.
editor.ToolsApplicationRequestBus(bus.Broadcast, 'EndUndoBatch')

When an undo batch is ended, any dirty entities changed properties are stored in an undo so you can undo the operation(s) your script did, but are also propogated to the actual data layer that is saved.

It could be the case that an undo batch is automatically created for running a script but I'm not so sure.

It feels like the corect thing to do longer term here is to begin and end an undo batch automatically for executing a script, that is, the current python system component does this

bool PythonSystemComponent::ExecuteByFilenameWithArgs(AZStd::string_view filename, const AZStd::vector<AZStd::string_view>& args)
    {
        AzToolsFramework::EditorPythonScriptNotificationsBus::Broadcast(
            &AzToolsFramework::EditorPythonScriptNotificationsBus::Events::OnStartExecuteByFilenameWithArgs, filename, args);
        const Result result = EvaluateFile(filename, args);
        return result == Result::Okay;
    }

but nothing is listening to OnStartExecuteByFilenameWithArgs.

I recommend

  1. add a OnEndExecuteByFilenameWithArgs to the bus
  2. invoke it after invoking the script in the above code and anywhere else appropriate
  3. in the EDITOR ONLY, begin and end an undo batch automatically as part of the OnStartExecute/OnEndExecute.

Note that I say EDITOR ONLY because the above system component lives in its own gem whihc is active in other contexts too, such as asset builders, which we don't want to try to mess with the undo stack there.

Awesome, will give it a shot. What confused me is that my procedural approach used to work in the past and now it stopped working. Granted I had not done anything procedural with O3DE in months. So it seems the APIs improved but now they require a few extra steps like the BeginUndoBatch, etc.

Even if I add the implicit BeginUndoBatch and EndUndoBatch, the developer would need to know that there's another API ebus call they should make:

editor.ToolsApplicationRequestBus(bus.Broadcast, 'AddDirtyEntity', entity_modified_id)

It's all working now. I will post a PR to add automatic calls to BeginUndoBatch/EndUndoBatch + a warning to call editor.ToolsApplicationRequestBus(bus.Broadcast, 'AddDirtyEntity', entity_modified_id).