Merge branch 'development' into Atom/jromnoa/fix-asset-path-calls-to-use-new-asset-test-class

monroegm-disable-blank-issue-2
jromnoa 4 years ago
commit 99f1783138

@ -29,6 +29,10 @@ class TestAutomation(EditorTestSuite):
class AtomEditorComponents_DepthOfFieldAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_DepthOfFieldAdded as test_module
@pytest.mark.test_case_id("C36525659")
class AtomEditorComponents_DiffuseProbeGridAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_DiffuseProbeGridAdded as test_module
@pytest.mark.test_case_id("C32078120")
class AtomEditorComponents_DirectionalLightAdded(EditorSharedTest):
from Atom.tests import hydra_AtomEditorComponents_DirectionalLightAdded as test_module

@ -116,7 +116,7 @@ class AtomComponentProperties:
return properties[property]
@staticmethod
def diffuse_probe(property: str = 'name') -> str:
def diffuse_probe_grid(property: str = 'name') -> str:
"""
Diffuse Probe Grid component properties. Requires one of 'shapes'.
- 'shapes' a list of supported shapes as component names.

@ -0,0 +1,187 @@
"""
Copyright (c) Contributors to the Open 3D Engine Project.
For complete copyright and license terms please see the LICENSE at the root of this distribution.
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
class Tests:
creation_undo = (
"UNDO Entity creation success",
"UNDO Entity creation failed")
creation_redo = (
"REDO Entity creation success",
"REDO Entity creation failed")
diffuse_probe_grid_creation = (
"Diffuse Probe Grid Entity successfully created",
"Diffuse Probe Grid Entity failed to be created")
diffuse_probe_grid_component = (
"Entity has a Diffuse Probe Grid component",
"Entity failed to find Diffuse Probe Grid component")
diffuse_probe_grid_disabled = (
"Diffuse Probe Grid component disabled",
"Diffuse Probe Grid component was not disabled")
diffuse_probe_grid_enabled = (
"Diffuse Probe Grid component enabled",
"Diffuse Probe Grid component was not enabled")
enter_game_mode = (
"Entered game mode",
"Failed to enter game mode")
exit_game_mode = (
"Exited game mode",
"Couldn't exit game mode")
is_visible = (
"Entity is visible",
"Entity was not visible")
is_hidden = (
"Entity is hidden",
"Entity was not hidden")
entity_deleted = (
"Entity deleted",
"Entity was not deleted")
deletion_undo = (
"UNDO deletion success",
"UNDO deletion failed")
deletion_redo = (
"REDO deletion success",
"REDO deletion failed")
def AtomEditorComponents_DiffuseProbeGrid_AddedToEntity():
"""
Summary:
Tests the Diffuse Probe Grid component can be added to an entity and has the expected functionality.
Test setup:
- Wait for Editor idle loop.
- Open the "Base" level.
Expected Behavior:
The component can be added, used in game mode, hidden/shown, deleted, and has accurate required components.
Creation and deletion undo/redo should also work.
Test Steps:
1) Create a Diffuse Probe Grid entity with no components.
2) Add a Diffuse Probe Grid component to Diffuse Probe Grid entity.
3) UNDO the entity creation and component addition.
4) REDO the entity creation and component addition.
5) Verify Diffuse Probe Grid component not enabled.
6) Add Shape component since it is required by the Diffuse Probe Grid component.
7) Verify Diffuse Probe Grid component is enabled.
8) Enter/Exit game mode.
9) Test IsHidden.
10) Test IsVisible.
11) Delete Diffuse Probe Grid entity.
12) UNDO deletion.
13) REDO deletion.
14) Look for errors.
:return: None
"""
import azlmbr.legacy.general as general
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper
from Atom.atom_utils.atom_constants import AtomComponentProperties
with Tracer() as error_tracer:
# Test setup begins.
# Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
TestHelper.init_idle()
TestHelper.open_level("", "Base")
# Test steps begin.
# 1. Create a Diffuse Probe Grid entity with no components.
diffuse_probe_grid_entity = EditorEntity.create_editor_entity(AtomComponentProperties.diffuse_probe_grid())
Report.critical_result(Tests.diffuse_probe_grid_creation, diffuse_probe_grid_entity.exists())
# 2. Add a Diffuse Probe Grid component to Diffuse Probe Grid entity.
diffuse_probe_grid_component = diffuse_probe_grid_entity.add_component(
AtomComponentProperties.diffuse_probe_grid())
Report.critical_result(
Tests.diffuse_probe_grid_component,
diffuse_probe_grid_entity.has_component(AtomComponentProperties.diffuse_probe_grid()))
# 3. UNDO the entity creation and component addition.
# -> UNDO component addition.
general.undo()
# -> UNDO naming entity.
general.undo()
# -> UNDO selecting entity.
general.undo()
# -> UNDO entity creation.
general.undo()
general.idle_wait_frames(1)
Report.result(Tests.creation_undo, not diffuse_probe_grid_entity.exists())
# 4. REDO the entity creation and component addition.
# -> REDO entity creation.
general.redo()
# -> REDO selecting entity.
general.redo()
# -> REDO naming entity.
general.redo()
# -> REDO component addition.
general.redo()
general.idle_wait_frames(1)
Report.result(Tests.creation_redo, diffuse_probe_grid_entity.exists())
# 5. Verify Diffuse Probe Grid component not enabled.
Report.result(Tests.diffuse_probe_grid_disabled, not diffuse_probe_grid_component.is_enabled())
# 6. Add Shape component since it is required by the Diffuse Probe Grid component.
for shape in AtomComponentProperties.diffuse_probe_grid('shapes'):
diffuse_probe_grid_entity.add_component(shape)
test_shape = (
f"Entity has a {shape} component",
f"Entity did not have a {shape} component")
Report.result(test_shape, diffuse_probe_grid_entity.has_component(shape))
# 7. Check if required shape allows Diffuse Probe Grid to be enabled
Report.result(Tests.diffuse_probe_grid_enabled, diffuse_probe_grid_component.is_enabled())
# Undo to remove each added shape except the last one and verify Diffuse Probe Grid is not enabled.
if not (shape == AtomComponentProperties.diffuse_probe_grid('shapes')[-1]):
general.undo()
TestHelper.wait_for_condition(lambda: not diffuse_probe_grid_entity.has_component(shape), 1.0)
Report.result(Tests.diffuse_probe_grid_disabled, not diffuse_probe_grid_component.is_enabled())
# 8. Enter/Exit game mode.
TestHelper.enter_game_mode(Tests.enter_game_mode)
general.idle_wait_frames(1)
TestHelper.exit_game_mode(Tests.exit_game_mode)
# 9. Test IsHidden.
diffuse_probe_grid_entity.set_visibility_state(False)
Report.result(Tests.is_hidden, diffuse_probe_grid_entity.is_hidden() is True)
# 10. Test IsVisible.
diffuse_probe_grid_entity.set_visibility_state(True)
general.idle_wait_frames(1)
Report.result(Tests.is_visible, diffuse_probe_grid_entity.is_visible() is True)
# 11. Delete Diffuse Probe Grid entity.
diffuse_probe_grid_entity.delete()
Report.result(Tests.entity_deleted, not diffuse_probe_grid_entity.exists())
# 12. UNDO deletion.
general.undo()
Report.result(Tests.deletion_undo, diffuse_probe_grid_entity.exists())
# 13. REDO deletion.
general.redo()
Report.result(Tests.deletion_redo, not diffuse_probe_grid_entity.exists())
# 14. Look for errors or asserts.
TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in error_tracer.asserts:
Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}")
if __name__ == "__main__":
from editor_python_test_tools.utils import Report
Report.start_test(AtomEditorComponents_DiffuseProbeGrid_AddedToEntity)

@ -50,6 +50,7 @@ def NvCloth_AddClothSimulationToActor():
# Constants
FRAMES_IN_GAME_MODE = 200
CLOTH_GEM_ERROR_WARNING_LIST = ["Cloth", "NvCloth", "ClothComponentMesh", "ActorClothSkinning", "ActorClothSkinning", "TangentSpaceHelper", "MeshAssetHelper", "ActorAssetHelper", "ClothDebugDisplay"]
helper.init_idle()
# 1) Load the level
@ -64,14 +65,16 @@ def NvCloth_AddClothSimulationToActor():
general.idle_wait_frames(FRAMES_IN_GAME_MODE)
# 5) Verify there are no errors and warnings in the logs
success_condition = not (section_tracer.has_errors or section_tracer.has_warnings)
Report.result(Tests.no_errors_and_warnings_found, success_condition)
if not success_condition:
if section_tracer.has_warnings:
Report.info(f"Warnings found: {section_tracer.warnings}")
if section_tracer.has_errors:
Report.info(f"Errors found: {section_tracer.errors}")
Report.failure(Tests.no_errors_and_warnings_found)
has_errors_or_warnings = False
for error_msg in section_tracer.errors:
if error_msg.window in CLOTH_GEM_ERROR_WARNING_LIST:
has_errors_or_warnings = True
Report.info(f"Cloth error found: {error_msg}")
for warning_msg in section_tracer.warnings:
if warning_msg.window in CLOTH_GEM_ERROR_WARNING_LIST:
has_errors_or_warnings = True
Report.info(f"Cloth warning found: {warning_msg}")
Report.result(Tests.no_errors_and_warnings_found, not has_errors_or_warnings)
# 6) Exit game mode
helper.exit_game_mode(Tests.exit_game_mode)

@ -50,6 +50,7 @@ def NvCloth_AddClothSimulationToMesh():
# Constants
FRAMES_IN_GAME_MODE = 200
CLOTH_GEM_ERROR_WARNING_LIST = ["Cloth", "NvCloth", "ClothComponentMesh", "ActorClothSkinning", "ActorClothSkinning", "TangentSpaceHelper", "MeshAssetHelper", "ActorAssetHelper", "ClothDebugDisplay"]
helper.init_idle()
# 1) Load the level
@ -64,14 +65,16 @@ def NvCloth_AddClothSimulationToMesh():
general.idle_wait_frames(FRAMES_IN_GAME_MODE)
# 5) Verify there are no errors and warnings in the logs
success_condition = not (section_tracer.has_errors or section_tracer.has_warnings)
Report.result(Tests.no_errors_and_warnings_found, success_condition)
if not success_condition:
if section_tracer.has_warnings:
Report.info(f"Warnings found: {section_tracer.warnings}")
if section_tracer.has_errors:
Report.info(f"Errors found: {section_tracer.errors}")
Report.failure(Tests.no_errors_and_warnings_found)
has_errors_or_warnings = False
for error_msg in section_tracer.errors:
if error_msg.window in CLOTH_GEM_ERROR_WARNING_LIST:
has_errors_or_warnings = True
Report.info(f"Cloth error found: {error_msg}")
for warning_msg in section_tracer.warnings:
if warning_msg.window in CLOTH_GEM_ERROR_WARNING_LIST:
has_errors_or_warnings = True
Report.info(f"Cloth warning found: {warning_msg}")
Report.result(Tests.no_errors_and_warnings_found, not has_errors_or_warnings)
# 6) Exit game mode
helper.exit_game_mode(Tests.exit_game_mode)

@ -158,7 +158,7 @@ def bundler_batch_setup_fixture(request, workspace, asset_processor, timeout) ->
else:
cmd.append(f"--{key}")
if append_defaults:
cmd.append(f"--project-path={workspace.project}")
cmd.append(f"--project-path={os.path.join(workspace.paths.engine_root(), workspace.project)}")
return cmd
# ******

@ -14,6 +14,7 @@ include(cmake/Version.cmake)
include(cmake/OutputDirectory.cmake)
if(NOT PROJECT_NAME)
include(cmake/CompilerSettings.cmake)
project(O3DE
LANGUAGES C CXX
VERSION ${LY_VERSION_STRING}

@ -249,38 +249,9 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
RUNTIME_DEPENDENCIES
Gem::LmbrCentral
)
ly_add_googletest(
NAME Legacy::EditorLib.Tests
)
ly_add_target(
NAME EditorLib.Camera.Tests ${PAL_TRAIT_TEST_TARGET_TYPE}
NAMESPACE Legacy
FILES_CMAKE
Lib/Tests/Camera/editor_lib_camera_test_files.cmake
INCLUDE_DIRECTORIES
PRIVATE
.
BUILD_DEPENDENCIES
PRIVATE
AZ::AzCore
AZ::AzTest
AZ::AzToolsFramework
AZ::AzTestShared
Legacy::EditorLib
Gem::Camera.Editor
Gem::AtomToolsFramework.Static
RUNTIME_DEPENDENCIES
Legacy::EditorLib
)
ly_add_source_properties(
SOURCES Lib/Tests/Camera/test_EditorCamera.cpp
PROPERTY COMPILE_DEFINITIONS
VALUES CAMERA_EDITOR_MODULE="$<TARGET_FILE_BASE_NAME:Camera.Editor>"
)
ly_add_googletest(
NAME Legacy::EditorLib.Camera.Tests
)
endif()

@ -3827,7 +3827,8 @@ void CCryEditApp::OnOpenQuickAccessBar()
}
QRect geo = m_pQuickAccessBar->geometry();
geo.moveCenter(MainWindow::instance()->geometry().center());
auto mainWindow = MainWindow::instance();
geo.moveCenter(mainWindow->mapToGlobal(mainWindow->geometry().center()));
m_pQuickAccessBar->setGeometry(geo);
m_pQuickAccessBar->setVisible(true);
m_pQuickAccessBar->setFocus();

@ -17,7 +17,7 @@ void SetEditorEnvironment(SSystemGlobalEnvironment* pEnv)
void AttachEditorAZEnvironment(AZ::EnvironmentInstance azEnv)
{
AZ::Environment::Attach(azEnv, true);
AZ::Environment::Attach(azEnv);
}
void DetachEditorAZEnvironment()

@ -13,9 +13,32 @@
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzFramework/Render/IntersectorInterface.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
#include <AzToolsFramework/ViewportSelection/EditorSelectionUtil.h>
#include <AzToolsFramework/ViewportSelection/EditorTransformComponentSelectionRequestBus.h>
#include <EditorViewportSettings.h>
AZ_CVAR(
bool,
ed_cameraPinDefaultOrbit,
true,
nullptr,
AZ::ConsoleFunctorFlags::Null,
"Sets whether the default orbit point moves with the camera or not");
AZ_CVAR(
bool,
ed_cameraDefaultOrbitAxesOrtho,
true,
nullptr,
AZ::ConsoleFunctorFlags::Null,
"Sets whether to draw the default orbit point as orthographic or not");
AZ_CVAR(
float,
ed_cameraDefaultOrbitFadeDuration,
0.5f,
nullptr,
AZ::ConsoleFunctorFlags::Null,
"Sets how long the default orbit point should take to appear and disappear");
namespace SandboxEditor
{
static AzFramework::TranslateCameraInputChannelIds BuildTranslateCameraInputChannelIds()
@ -174,7 +197,7 @@ namespace SandboxEditor
return SandboxEditor::CameraScrollSpeed();
};
const auto pivotFn = []
const auto pivotFn = []() -> AZStd::optional<AZ::Vector3>
{
// use the manipulator transform as the pivot point
AZStd::optional<AZ::Transform> entityPivot;
@ -187,8 +210,7 @@ namespace SandboxEditor
return entityPivot->GetTranslation();
}
// otherwise just use the identity
return AZ::Vector3::CreateZero();
return AZStd::nullopt;
};
m_firstPersonFocusCamera =
@ -199,9 +221,26 @@ namespace SandboxEditor
m_orbitCamera = AZStd::make_shared<AzFramework::OrbitCameraInput>(SandboxEditor::CameraOrbitChannelId());
m_orbitCamera->SetPivotFn(
[pivotFn]([[maybe_unused]] const AZ::Vector3& position, [[maybe_unused]] const AZ::Vector3& direction)
[this, pivotFn](const AZ::Vector3& position, const AZ::Vector3& direction)
{
return pivotFn();
// return the pivot
if (auto pivot = pivotFn())
{
return pivot.value();
}
// start ticking and drawing (for the default pivot)
AZ::TickBus::Handler::BusConnect();
AzFramework::ViewportDebugDisplayEventBus::Handler::BusConnect(AzToolsFramework::GetEntityContextId());
m_defaultOrbiting = true;
// calculate the default orbit point
if (!ed_cameraPinDefaultOrbit || m_orbitCamera->Beginning())
{
m_defaultOrbitPoint = position + direction * SandboxEditor::CameraDefaultOrbitDistance();
}
return m_defaultOrbitPoint;
});
m_orbitRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(SandboxEditor::CameraOrbitLookChannelId());
@ -306,4 +345,67 @@ namespace SandboxEditor
m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::ClearReferenceFrame);
}
}
void EditorModularViewportCameraComposer::OnTick(const float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
{
const float delta = [duration = &ed_cameraDefaultOrbitFadeDuration, deltaTime] {
if (*duration == 0.0f) {
return 1.0f;
}
return deltaTime / *duration;
}();
if (m_defaultOrbiting)
{
m_defaultOrbitOpacity = AZStd::min(m_defaultOrbitOpacity + delta, 1.0f);
}
else
{
m_defaultOrbitOpacity = AZStd::max(m_defaultOrbitOpacity - delta, 0.0f);
if (m_defaultOrbitOpacity == 0.0f)
{
AZ::TickBus::Handler::BusDisconnect();
AzFramework::ViewportDebugDisplayEventBus::Handler::BusDisconnect();
}
}
m_defaultOrbiting = false;
}
static void DrawTransformAxis(
AzFramework::DebugDisplayRequests& display,
const AzFramework::CameraState& cameraState,
const AZ::Vector3& pivot,
const float axisLength,
const float alpha)
{
const int prevState = display.GetState();
display.DepthWriteOff();
display.DepthTestOff();
display.CullOff();
const float orthoScale =
ed_cameraDefaultOrbitAxesOrtho ? AzToolsFramework::CalculateScreenToWorldMultiplier(pivot, cameraState) : 1.0f;
display.SetColor(AZ::Color::CreateFromVector3AndFloat(AZ::Colors::Red.GetAsVector3(), alpha));
display.DrawLine(pivot, pivot + AZ::Vector3::CreateAxisX() * axisLength * orthoScale);
display.SetColor(AZ::Color::CreateFromVector3AndFloat(AZ::Colors::LawnGreen.GetAsVector3(), alpha));
display.DrawLine(pivot, pivot + AZ::Vector3::CreateAxisY() * axisLength * orthoScale);
display.SetColor(AZ::Color::CreateFromVector3AndFloat(AZ::Colors::Blue.GetAsVector3(), alpha));
display.DrawLine(pivot, pivot + AZ::Vector3::CreateAxisZ() * axisLength * orthoScale);
display.DepthWriteOn();
display.DepthTestOn();
display.CullOn();
display.SetState(prevState);
}
void EditorModularViewportCameraComposer::DisplayViewport(
[[maybe_unused]] const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay)
{
DrawTransformAxis(
debugDisplay, AzToolsFramework::GetCameraState(viewportInfo.m_viewportId), m_defaultOrbitPoint, 1.0f, m_defaultOrbitOpacity);
}
} // namespace SandboxEditor

@ -9,6 +9,8 @@
#pragma once
#include <AtomToolsFramework/Viewport/ModularViewportCameraController.h>
#include <AzCore/Component/TickBus.h>
#include <AzFramework/Entity/EntityDebugDisplayBus.h>
#include <AzFramework/Viewport/CameraInput.h>
#include <AzToolsFramework/API/EditorCameraBus.h>
#include <EditorModularViewportCameraComposerBus.h>
@ -20,6 +22,8 @@ namespace SandboxEditor
class EditorModularViewportCameraComposer
: private EditorModularViewportCameraComposerNotificationBus::Handler
, private Camera::EditorCameraNotificationBus::Handler
, private AzFramework::ViewportDebugDisplayEventBus::Handler
, private AZ::TickBus::Handler
{
public:
SANDBOX_API explicit EditorModularViewportCameraComposer(AzFramework::ViewportId viewportId);
@ -29,6 +33,12 @@ namespace SandboxEditor
SANDBOX_API AZStd::shared_ptr<AtomToolsFramework::ModularViewportCameraController> CreateModularViewportCameraController();
private:
// AzFramework::ViewportDebugDisplayEventBus overrides ...
void DisplayViewport(const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay) override;
// AZ::TickBus overrides ...
void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
//! Setup all internal camera inputs.
void SetupCameras();
@ -52,5 +62,9 @@ namespace SandboxEditor
AZStd::shared_ptr<AzFramework::FocusCameraInput> m_orbitFocusCamera;
AzFramework::ViewportId m_viewportId;
float m_defaultOrbitOpacity = 0.0f; //!< The default orbit axes opacity (to fade in and out).
AZ::Vector3 m_defaultOrbitPoint = AZ::Vector3::CreateZero(); //!< The orbit point to use when no entity is selected.
bool m_defaultOrbiting = false; //!< Is the camera default orbiting (orbiting when there's no selected entity).
};
} // namespace SandboxEditor

@ -61,7 +61,7 @@ static AZStd::vector<AZStd::string> GetEditorInputNames()
void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serialize)
{
serialize.Class<CameraMovementSettings>()
->Version(3)
->Version(4)
->Field("TranslateSpeed", &CameraMovementSettings::m_translateSpeed)
->Field("RotateSpeed", &CameraMovementSettings::m_rotateSpeed)
->Field("BoostMultiplier", &CameraMovementSettings::m_boostMultiplier)
@ -76,9 +76,8 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
->Field("OrbitYawRotationInverted", &CameraMovementSettings::m_orbitYawRotationInverted)
->Field("PanInvertedX", &CameraMovementSettings::m_panInvertedX)
->Field("PanInvertedY", &CameraMovementSettings::m_panInvertedY)
->Field("DefaultPositionX", &CameraMovementSettings::m_defaultCameraPositionX)
->Field("DefaultPositionY", &CameraMovementSettings::m_defaultCameraPositionY)
->Field("DefaultPositionZ", &CameraMovementSettings::m_defaultCameraPositionZ);
->Field("DefaultPosition", &CameraMovementSettings::m_defaultPosition)
->Field("DefaultOrbitDistance", &CameraMovementSettings::m_defaultOrbitDistance);
serialize.Class<CameraInputSettings>()
->Version(2)
@ -159,14 +158,12 @@ void CEditorPreferencesPage_ViewportCamera::Reflect(AZ::SerializeContext& serial
AZ::Edit::UIHandlers::CheckBox, &CameraMovementSettings::m_captureCursorLook, "Camera Capture Look Cursor",
"Should the cursor be captured (hidden) while performing free look")
->DataElement(
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionX, "Default X Camera Position",
"Default X Camera Position when a level is opened")
AZ::Edit::UIHandlers::Vector3, &CameraMovementSettings::m_defaultPosition, "Default Camera Position",
"Default Camera Position when a level is first opened")
->DataElement(
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionY, "Default Y Camera Position",
"Default Y Camera Position when a level is opened")
->DataElement(
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultCameraPositionZ, "Default Z Camera Position",
"Default Z Camera Position when a level is opened");
AZ::Edit::UIHandlers::SpinBox, &CameraMovementSettings::m_defaultOrbitDistance, "Default Orbit Distance",
"The default distance to orbit about when there is no entity selected")
->Attribute(AZ::Edit::Attributes::Min, minValue);
editContext->Class<CameraInputSettings>("Camera Input Settings", "")
->DataElement(
@ -283,12 +280,8 @@ void CEditorPreferencesPage_ViewportCamera::OnApply()
SandboxEditor::SetCameraOrbitYawRotationInverted(m_cameraMovementSettings.m_orbitYawRotationInverted);
SandboxEditor::SetCameraPanInvertedX(m_cameraMovementSettings.m_panInvertedX);
SandboxEditor::SetCameraPanInvertedY(m_cameraMovementSettings.m_panInvertedY);
SandboxEditor::SetDefaultCameraEditorPosition(
AZ::Vector3(
m_cameraMovementSettings.m_defaultCameraPositionX,
m_cameraMovementSettings.m_defaultCameraPositionY,
m_cameraMovementSettings.m_defaultCameraPositionZ
));
SandboxEditor::SetCameraDefaultEditorPosition(m_cameraMovementSettings.m_defaultPosition);
SandboxEditor::SetCameraDefaultOrbitDistance(m_cameraMovementSettings.m_defaultOrbitDistance);
SandboxEditor::SetCameraTranslateForwardChannelId(m_cameraInputSettings.m_translateForwardChannelId);
SandboxEditor::SetCameraTranslateBackwardChannelId(m_cameraInputSettings.m_translateBackwardChannelId);
@ -325,11 +318,8 @@ void CEditorPreferencesPage_ViewportCamera::InitializeSettings()
m_cameraMovementSettings.m_orbitYawRotationInverted = SandboxEditor::CameraOrbitYawRotationInverted();
m_cameraMovementSettings.m_panInvertedX = SandboxEditor::CameraPanInvertedX();
m_cameraMovementSettings.m_panInvertedY = SandboxEditor::CameraPanInvertedY();
AZ::Vector3 defaultCameraPosition = SandboxEditor::DefaultEditorCameraPosition();
m_cameraMovementSettings.m_defaultCameraPositionX = defaultCameraPosition.GetX();
m_cameraMovementSettings.m_defaultCameraPositionY = defaultCameraPosition.GetY();
m_cameraMovementSettings.m_defaultCameraPositionZ = defaultCameraPosition.GetZ();
m_cameraMovementSettings.m_defaultPosition = SandboxEditor::CameraDefaultEditorPosition();
m_cameraMovementSettings.m_defaultOrbitDistance = SandboxEditor::CameraDefaultOrbitDistance();
m_cameraInputSettings.m_translateForwardChannelId = SandboxEditor::CameraTranslateForwardChannelId().GetName();
m_cameraInputSettings.m_translateBackwardChannelId = SandboxEditor::CameraTranslateBackwardChannelId().GetName();

@ -9,9 +9,12 @@
#pragma once
#include "Include/IPreferencesPage.h"
#include <AzCore/Math/Vector3.h>
#include <AzCore/RTTI/RTTI.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <QIcon>
inline AZ::Crc32 EditorPropertyVisibility(const bool enabled)
@ -43,6 +46,7 @@ private:
{
AZ_TYPE_INFO(CameraMovementSettings, "{60B8C07E-5F48-4171-A50B-F45558B5CCA1}")
AZ::Vector3 m_defaultPosition;
float m_translateSpeed;
float m_rotateSpeed;
float m_scrollSpeed;
@ -50,16 +54,14 @@ private:
float m_panSpeed;
float m_boostMultiplier;
float m_rotateSmoothness;
bool m_rotateSmoothing;
float m_translateSmoothness;
bool m_translateSmoothing;
float m_defaultOrbitDistance;
bool m_captureCursorLook;
bool m_orbitYawRotationInverted;
bool m_panInvertedX;
bool m_panInvertedY;
float m_defaultCameraPositionX;
float m_defaultCameraPositionY;
float m_defaultCameraPositionZ;
bool m_rotateSmoothing;
bool m_translateSmoothing;
AZ::Crc32 RotateSmoothingVisibility() const
{

@ -38,6 +38,7 @@ namespace SandboxEditor
constexpr AZStd::string_view CameraTranslateSmoothingSetting = "/Amazon/Preferences/Editor/Camera/TranslateSmoothing";
constexpr AZStd::string_view CameraRotateSmoothingSetting = "/Amazon/Preferences/Editor/Camera/RotateSmoothing";
constexpr AZStd::string_view CameraCaptureCursorLookSetting = "/Amazon/Preferences/Editor/Camera/CaptureCursorLook";
constexpr AZStd::string_view CameraDefaultOrbitDistanceSetting = "/Amazon/Preferences/Editor/Camera/DefaultOrbitDistance";
constexpr AZStd::string_view CameraTranslateForwardIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateForwardId";
constexpr AZStd::string_view CameraTranslateBackwardIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateBackwardId";
constexpr AZStd::string_view CameraTranslateLeftIdSetting = "/Amazon/Preferences/Editor/Camera/CameraTranslateLeftId";
@ -114,15 +115,15 @@ namespace SandboxEditor
return AZStd::make_unique<EditorViewportSettingsCallbacksImpl>();
}
AZ::Vector3 DefaultEditorCameraPosition()
AZ::Vector3 CameraDefaultEditorPosition()
{
float xPosition = aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionX, 0.0));
float yPosition = aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionY, -10.0));
float zPosition = aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionZ, 4.0));
return AZ::Vector3(xPosition, yPosition, zPosition);
return AZ::Vector3(
aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionX, 0.0)),
aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionY, -10.0)),
aznumeric_cast<float>(GetRegistry(CameraDefaultStartingPositionZ, 4.0)));
}
void SetDefaultCameraEditorPosition(const AZ::Vector3 defaultCameraPosition)
void SetCameraDefaultEditorPosition(const AZ::Vector3& defaultCameraPosition)
{
SetRegistry(CameraDefaultStartingPositionX, defaultCameraPosition.GetX());
SetRegistry(CameraDefaultStartingPositionY, defaultCameraPosition.GetY());
@ -359,6 +360,16 @@ namespace SandboxEditor
SetRegistry(CameraCaptureCursorLookSetting, capture);
}
float CameraDefaultOrbitDistance()
{
return aznumeric_cast<float>(GetRegistry(CameraDefaultOrbitDistanceSetting, 20.0));
}
void SetCameraDefaultOrbitDistance(const float distance)
{
SetRegistry(CameraDefaultOrbitDistanceSetting, distance);
}
AzFramework::InputChannelId CameraTranslateForwardChannelId()
{
return AzFramework::InputChannelId(

@ -33,9 +33,6 @@ namespace SandboxEditor
//! event will fire when a value in the settings registry (editorpreferences.setreg) is modified.
SANDBOX_API AZStd::unique_ptr<EditorViewportSettingsCallbacks> CreateEditorViewportSettingsCallbacks();
SANDBOX_API AZ::Vector3 DefaultEditorCameraPosition();
SANDBOX_API void SetDefaultCameraEditorPosition(AZ::Vector3 defaultCameraPosition);
SANDBOX_API AZ::u64 MaxItemsShownInAssetBrowserSearch();
SANDBOX_API void SetMaxItemsShownInAssetBrowserSearch(AZ::u64 numberOfItemsShown);
@ -105,6 +102,12 @@ namespace SandboxEditor
SANDBOX_API bool CameraCaptureCursorForLook();
SANDBOX_API void SetCameraCaptureCursorForLook(bool capture);
SANDBOX_API AZ::Vector3 CameraDefaultEditorPosition();
SANDBOX_API void SetCameraDefaultEditorPosition(const AZ::Vector3& position);
SANDBOX_API float CameraDefaultOrbitDistance();
SANDBOX_API void SetCameraDefaultOrbitDistance(float distance);
SANDBOX_API AzFramework::InputChannelId CameraTranslateForwardChannelId();
SANDBOX_API void SetCameraTranslateForwardChannelId(AZStd::string_view cameraTranslateForwardId);

@ -620,9 +620,9 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event)
break;
case eNotify_OnEndNewScene:
PopDisableRendering();
{
PopDisableRendering();
Matrix34 viewTM;
viewTM.SetIdentity();
@ -638,9 +638,9 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event)
break;
case eNotify_OnEndTerrainCreate:
PopDisableRendering();
{
PopDisableRendering();
Matrix34 viewTM;
viewTM.SetIdentity();
@ -2021,10 +2021,6 @@ void EditorViewportWidget::SetDefaultCamera()
GetViewManager()->SetCameraObjectId(GUID_NULL);
SetName(m_defaultViewName);
// Set the default Editor Camera position.
m_defaultViewTM.SetTranslation(Vec3(m_editorViewportSettings.DefaultEditorCameraPosition()));
SetViewTM(m_defaultViewTM);
// Synchronize the configured editor viewport FOV to the default camera
if (m_viewPane)
{
@ -2041,6 +2037,10 @@ void EditorViewportWidget::SetDefaultCamera()
atomViewportRequests->PushView(contextName, m_defaultView);
}
// Set the default Editor Camera position.
m_defaultViewTM.SetTranslation(Vec3(m_editorViewportSettings.DefaultEditorCameraPosition()));
SetViewTM(m_defaultViewTM);
PostCameraSet();
}
@ -2527,7 +2527,7 @@ bool EditorViewportSettings::StickySelectEnabled() const
AZ::Vector3 EditorViewportSettings::DefaultEditorCameraPosition() const
{
return SandboxEditor::DefaultEditorCameraPosition();
return SandboxEditor::CameraDefaultEditorPosition();
}
AZ_CVAR_EXTERNED(bool, ed_previewGameInFullscreen_once);

@ -49,9 +49,6 @@
#include "Include/IObjectManager.h"
#include "ActionManager.h"
// Including this too early will result in a linker error
#include <CryCommon/CryLibrary.h>
// Implementation of System Callback structure.
struct SSystemUserCallback
: public ISystemUserCallback
@ -242,8 +239,7 @@ private:
AZ_PUSH_DISABLE_WARNING(4273, "-Wunknown-warning-option")
CGameEngine::CGameEngine()
: m_gameDll(nullptr)
, m_bIgnoreUpdates(false)
: m_bIgnoreUpdates(false)
, m_ePendingGameMode(ePGM_NotPending)
, m_modalWindowDismisser(nullptr)
AZ_POP_DISABLE_WARNING
@ -253,7 +249,7 @@ AZ_POP_DISABLE_WARNING
m_bInGameMode = false;
m_bSimulationMode = false;
m_bSyncPlayerPosition = true;
m_hSystemHandle = nullptr;
m_hSystemHandle.reset(nullptr);
m_bJustCreated = false;
m_levelName = "Untitled";
m_levelExtension = EditorUtils::LevelFile::GetDefaultFileExtension();
@ -268,18 +264,10 @@ AZ_POP_DISABLE_WARNING
GetIEditor()->UnregisterNotifyListener(this);
m_pISystem->GetIMovieSystem()->SetCallback(nullptr);
if (m_gameDll)
{
CryFreeLibrary(m_gameDll);
}
delete m_pISystem;
m_pISystem = nullptr;
if (m_hSystemHandle)
{
CryFreeLibrary(m_hSystemHandle);
}
m_hSystemHandle.reset(nullptr);
delete m_pSystemUserCallback;
}
@ -347,18 +335,19 @@ AZ::Outcome<void, AZStd::string> CGameEngine::Init(
HWND hwndForInputSystem)
{
m_pSystemUserCallback = new SSystemUserCallback(logo);
m_hSystemHandle = CryLoadLibraryDefName("CrySystem");
if (!m_hSystemHandle)
constexpr const char* crySystemLibraryName = AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX "CrySystem" AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION;
m_hSystemHandle = AZ::DynamicModuleHandle::Create(crySystemLibraryName);
if (!m_hSystemHandle->Load(true))
{
auto errorMessage = AZStd::string::format("%s Loading Failed", CryLibraryDefName("CrySystem"));
auto errorMessage = AZStd::string::format("%s Loading Failed", crySystemLibraryName);
Error(errorMessage.c_str());
return AZ::Failure(errorMessage);
}
PFNCREATESYSTEMINTERFACE pfnCreateSystemInterface =
(PFNCREATESYSTEMINTERFACE)CryGetProcAddress(m_hSystemHandle, "CreateSystemInterface");
m_hSystemHandle->GetFunction<PFNCREATESYSTEMINTERFACE>("CreateSystemInterface");
SSystemInitParams sip;

@ -28,6 +28,8 @@ struct IInitializeUIInfo;
#include <AzCore/Interface/Interface.h>
#include <AzCore/Math/Vector3.h>
#include <AzCore/Module/DynamicModuleHandle.h>
class ThreadedOnErrorHandler : public QObject
{
Q_OBJECT
@ -124,11 +126,6 @@ public:
return s_pakModifyMutex;
}
inline HMODULE GetGameModule() const
{
return m_gameDll;
}
private:
void SetGameMode(bool inGame);
void SwitchToInGame();
@ -150,8 +147,7 @@ private:
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
Matrix34 m_playerViewTM;
struct SSystemUserCallback* m_pSystemUserCallback;
HMODULE m_hSystemHandle;
HMODULE m_gameDll;
AZStd::unique_ptr<AZ::DynamicModuleHandle> m_hSystemHandle;
enum EPendingGameMode
{
ePGM_NotPending,

@ -1,11 +0,0 @@
#
# Copyright (c) Contributors to the Open 3D Engine Project.
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
set(FILES
test_EditorCamera.cpp
)

@ -17,43 +17,33 @@
namespace UnitTest
{
class EditorCameraTestEnvironment : public AZ::Test::GemTestEnvironment
{
// AZ::Test::GemTestEnvironment overrides ...
void AddGemsAndComponents() override;
};
void EditorCameraTestEnvironment::AddGemsAndComponents()
{
AddDynamicModulePaths({ CAMERA_EDITOR_MODULE });
AddComponentDescriptors({ AzToolsFramework::Components::TransformComponent::CreateDescriptor() });
}
class EditorCameraFixture : public ::testing::Test
{
public:
AZ::ComponentApplication* m_application = nullptr;
AtomToolsFramework::ModularCameraViewportContext* m_cameraViewportContextView = nullptr;
AZStd::unique_ptr<SandboxEditor::EditorModularViewportCameraComposer> m_editorModularViewportCameraComposer;
AZStd::unique_ptr<AZ::DynamicModuleHandle> m_editorLibHandle;
AzFramework::ViewportControllerListPtr m_controllerList;
AZStd::unique_ptr<AZ::Entity> m_entity;
AZ::Entity* m_entity = nullptr;
AZ::ComponentDescriptor* m_transformComponent = nullptr;
static const AzFramework::ViewportId TestViewportId;
void SetUp() override
{
m_editorLibHandle = AZ::DynamicModuleHandle::Create("EditorLib");
[[maybe_unused]] const bool loaded = m_editorLibHandle->Load(true);
AZ_Assert(loaded, "EditorLib could not be loaded");
m_controllerList = AZStd::make_shared<AzFramework::ViewportControllerList>();
m_controllerList->RegisterViewportContext(TestViewportId);
m_application = aznew AZ::ComponentApplication;
AZ::ComponentApplication::Descriptor appDesc;
m_entity = m_application->Create(appDesc);
m_transformComponent = AzToolsFramework::Components::TransformComponent::CreateDescriptor();
m_application->RegisterComponentDescriptor(m_transformComponent);
m_entity = AZStd::make_unique<AZ::Entity>();
m_entity->Init();
m_entity->CreateComponent<AzToolsFramework::Components::TransformComponent>();
m_entity->Activate();
m_controllerList = AZStd::make_shared<AzFramework::ViewportControllerList>();
m_controllerList->RegisterViewportContext(TestViewportId);
m_editorModularViewportCameraComposer = AZStd::make_unique<SandboxEditor::EditorModularViewportCameraComposer>(TestViewportId);
auto controller = m_editorModularViewportCameraComposer->CreateModularViewportCameraController();
@ -72,8 +62,17 @@ namespace UnitTest
{
m_editorModularViewportCameraComposer.reset();
m_cameraViewportContextView = nullptr;
m_entity.reset();
m_editorLibHandle = {};
if (m_application)
{
m_application->UnregisterComponentDescriptor(m_transformComponent);
delete m_transformComponent;
m_transformComponent = nullptr;
m_application->Destroy();
delete m_application;
m_application = nullptr;
}
}
};
@ -211,15 +210,3 @@ namespace UnitTest
EXPECT_THAT(currentReferenceFrame, IsClose(AZ::Transform::CreateIdentity()));
}
} // namespace UnitTest
// required to support running integration tests with the Camera Gem
AZTEST_EXPORT int AZ_UNIT_TEST_HOOK_NAME(int argc, char** argv)
{
::testing::InitGoogleMock(&argc, argv);
AZ::Test::printUnusedParametersWarning(argc, argv);
AZ::Test::addTestEnvironments({ new UnitTest::EditorCameraTestEnvironment() });
int result = RUN_ALL_TESTS();
return result;
}
IMPLEMENT_TEST_EXECUTABLE_MAIN();

@ -9,12 +9,13 @@
#include "EditorDefs.h"
#include <AzTest/AzTest.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/UnitTest/UnitTest.h>
#include <EditorEnvironment.h>
#include <QApplication>
class EditorLibTestEnvironment
: public AZ::Test::ITestEnvironment
: public ::UnitTest::TraceBusHook
{
public:
~EditorLibTestEnvironment() override = default;
@ -22,16 +23,20 @@ public:
protected:
void SetupEnvironment() override
{
::UnitTest::TraceBusHook::SetupEnvironment();
AZ::Environment::Create(nullptr);
AttachEditorAZEnvironment(AZ::Environment::GetInstance());
AZ::AllocatorInstance<AZ::SystemAllocator>::Create();
AttachEditorAZEnvironment(AZ::Environment::GetInstance());
}
void TeardownEnvironment() override
{
AZ::AllocatorInstance<AZ::SystemAllocator>::Destroy();
DetachEditorAZEnvironment();
AZ::AllocatorInstance<AZ::SystemAllocator>::Destroy();
AZ::Environment::Destroy();
::UnitTest::TraceBusHook::TeardownEnvironment();
}
};

@ -44,7 +44,7 @@ namespace LyViewPane
static const char* const SubstanceEditor = "Substance Editor";
static const char* const VegetationEditor = "Vegetation Editor";
static const char* const LandscapeCanvas = "Landscape Canvas";
static const char* const AnimationEditor = "EMotion FX Animation Editor (PREVIEW)";
static const char* const AnimationEditor = "EMotion FX Animation Editor";
static const char* const PhysXConfigurationEditor = "PhysX Configuration (PREVIEW)";
static const char* const SliceRelationships = "Slice Relationship View (PREVIEW)";

@ -27,7 +27,6 @@
// Editor
#include "MainStatusBarItems.h"
#include "CryLibrary.h"
#include "ProcessInfo.h"

@ -1061,7 +1061,7 @@ void MainWindow::InitActions()
if (emfxEnabled.value)
{
QAction* action = am->AddAction(ID_OPEN_EMOTIONFX_EDITOR, tr("Animation Editor"))
.SetToolTip(tr("Open Animation Editor (PREVIEW)"))
.SetToolTip(tr("Open Animation Editor"))
.SetIcon(QIcon(":/EMotionFX/EMFX_icon_32x32.png"))
.SetApplyHoverEffect();
QObject::connect(action, &QAction::triggered, this, []() {

@ -22,6 +22,7 @@ set(FILES
Lib/Tests/test_DisplaySettingsPythonBindings.cpp
Lib/Tests/test_ViewportManipulatorController.cpp
Lib/Tests/test_ModularViewportCameraController.cpp
Lib/Tests/Camera/test_EditorCamera.cpp
DisplaySettingsPythonFuncs.cpp
DisplaySettingsPythonFuncs.h
)

@ -70,6 +70,9 @@ namespace AZ::Data
const char* GetFilename() const override { return m_filePath.c_str(); }
AZStd::chrono::milliseconds GetStreamingDeadline() const { return m_curDeadline; }
AZ::IO::IStreamerTypes::Priority GetStreamingPriority() const { return m_curPriority; }
// AssetDataStream specific APIs
//! Whether or not all data has been loaded.

@ -3408,7 +3408,14 @@ LUA_API const Node* lua_getDummyNode()
const BehaviorParameter* arg = method->GetArgument(iArg);
BehaviorClass* argClass = nullptr;
LuaLoadFromStack fromStack = FromLuaStack(context, arg, argClass);
AZ_Assert(fromStack, "Argument %s for Method %s doesn't have support to be converted to Lua!", arg->m_name, method->m_name.c_str());
AZ_Assert(fromStack,
"The argument type: %s for method: %s is not serialized and/or reflected for scripting.\n"
"Make sure %s is added to the SerializeContext and reflected to the BehaviorContext\n"
"For example, verify these two exist and are being called in a Reflect function:\n"
"serializeContext->Class<%s>();\n"
"behaviorContext->Class<%s>();\n"
"%s will not be available for scripting unless these requirements are met."
, arg->m_name, method->m_name.c_str(), arg->m_name, arg->m_name, arg->m_name, method->m_name.c_str());
m_fromLua.push_back(AZStd::make_pair(fromStack, argClass));
}

@ -27,28 +27,4 @@ namespace AzFramework
;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
const char* InputChannelId::GetName() const
{
return m_name.c_str();
}
////////////////////////////////////////////////////////////////////////////////////////////////
const AZ::Crc32& InputChannelId::GetNameCrc32() const
{
return m_crc32;
}
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputChannelId::operator==(const InputChannelId& other) const
{
return (m_crc32 == other.m_crc32);
}
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputChannelId::operator!=(const InputChannelId& other) const
{
return !(*this == other);
}
} // namespace AzFramework

@ -39,53 +39,58 @@ namespace AzFramework
////////////////////////////////////////////////////////////////////////////////////////////
//! Constructor
//! \param[in] name Name of the input channel (will be ignored if exceeds MAX_NAME_LENGTH)
//! \param[in] name Name of the input channel (will be truncated if exceeds MAX_NAME_LENGTH)
explicit constexpr InputChannelId(AZStd::string_view name = "")
: m_name(name)
, m_crc32(name)
: m_name(name.substr(0, MAX_NAME_LENGTH))
, m_crc32(name.substr(0, MAX_NAME_LENGTH))
{
}
constexpr InputChannelId(const InputChannelId& other) = default;
constexpr InputChannelId(InputChannelId&& other) = default;
constexpr InputChannelId& operator=(const InputChannelId& other)
{
m_name = other.m_name;
m_crc32 = other.m_crc32;
return *this;
}
constexpr InputChannelId& operator=(InputChannelId&& other)
{
m_name = AZStd::move(other.m_name);
m_crc32 = AZStd::move(other.m_crc32);
other.m_crc32 = 0;
return *this;
}
////////////////////////////////////////////////////////////////////////////////////////////
// Default copying and moving
AZ_DEFAULT_COPY_MOVE(InputChannelId);
////////////////////////////////////////////////////////////////////////////////////////////
//! Default destructor
~InputChannelId() = default;
////////////////////////////////////////////////////////////////////////////////////////////
//! Access to the input channel's name
//! \return Name of the input channel
const char* GetName() const;
constexpr const char* GetName() const
{
return m_name.c_str();
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Access to the crc32 of the input channel's name
//! \return crc32 of the input channel name
const AZ::Crc32& GetNameCrc32() const;
constexpr const AZ::Crc32& GetNameCrc32() const
{
return m_crc32;
}
////////////////////////////////////////////////////////////////////////////////////////////
///@{
//! Equality comparison operator
//! \param[in] other Another instance of the class to compare for equality
bool operator==(const InputChannelId& other) const;
bool operator!=(const InputChannelId& other) const;
///@}
constexpr bool operator==(const InputChannelId& other) const
{
return m_crc32 == other.m_crc32;
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Inequality comparison operator
//! \param[in] other Another instance of the class to compare for inequality
constexpr bool operator!=(const InputChannelId& other) const
{
return !(*this == other);
}
private:
////////////////////////////////////////////////////////////////////////////////////////////
// Variables
AZStd::fixed_string<MAX_NAME_LENGTH> m_name; //!< Name of the input channel
AZ::Crc32 m_crc32; //!< Crc32 of the input channel
AZ::Crc32 m_crc32; //!< Crc32 of the input channel name
};
} // namespace AzFramework

@ -14,14 +14,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const char* InputDeviceGamepad::Name("gamepad");
const InputDeviceId InputDeviceGamepad::IdForIndex0(Name, 0);
const InputDeviceId InputDeviceGamepad::IdForIndex1(Name, 1);
const InputDeviceId InputDeviceGamepad::IdForIndex2(Name, 2);
const InputDeviceId InputDeviceGamepad::IdForIndex3(Name, 3);
const InputDeviceId InputDeviceGamepad::IdForIndexN(AZ::u32 n) { return InputDeviceId(Name, n); }
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceGamepad::IsGamepadDevice(const InputDeviceId& inputDeviceId)
{

@ -32,16 +32,16 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
//! The name used to identify any game-pad input device
static const char* Name;
static constexpr inline const char* Name{"gamepad"};
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify a game-pad input device with a specific index
///@{
static const InputDeviceId IdForIndex0;
static const InputDeviceId IdForIndex1;
static const InputDeviceId IdForIndex2;
static const InputDeviceId IdForIndex3;
static const InputDeviceId IdForIndexN(AZ::u32 n);
static constexpr inline InputDeviceId IdForIndex0{Name, 0};
static constexpr inline InputDeviceId IdForIndex1{Name, 1};
static constexpr inline InputDeviceId IdForIndex2{Name, 2};
static constexpr inline InputDeviceId IdForIndex3{Name, 3};
static constexpr inline InputDeviceId IdForIndexN(AZ::u32 n) { return InputDeviceId(Name, n); }
///@}
////////////////////////////////////////////////////////////////////////////////////////////

@ -29,71 +29,4 @@ namespace AzFramework
;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
InputDeviceId::InputDeviceId(const char* name, AZ::u32 index)
: m_crc32(name)
, m_index(index)
{
memset(m_name, 0, AZ_ARRAY_SIZE(m_name));
azstrncpy(m_name, NAME_BUFFER_SIZE, name, MAX_NAME_LENGTH);
}
////////////////////////////////////////////////////////////////////////////////////////////////
InputDeviceId::InputDeviceId(const InputDeviceId& other)
: m_crc32(other.m_crc32)
, m_index(other.m_index)
{
memset(m_name, 0, AZ_ARRAY_SIZE(m_name));
azstrcpy(m_name, NAME_BUFFER_SIZE, other.m_name);
}
////////////////////////////////////////////////////////////////////////////////////////////////
InputDeviceId& InputDeviceId::operator=(const InputDeviceId& other)
{
azstrcpy(m_name, NAME_BUFFER_SIZE, other.m_name);
m_crc32 = other.m_crc32;
m_index = other.m_index;
return *this;
}
////////////////////////////////////////////////////////////////////////////////////////////////
const char* InputDeviceId::GetName() const
{
return m_name;
}
////////////////////////////////////////////////////////////////////////////////////////////////
const AZ::Crc32& InputDeviceId::GetNameCrc32() const
{
return m_crc32;
}
////////////////////////////////////////////////////////////////////////////////////////////////
AZ::u32 InputDeviceId::GetIndex() const
{
return m_index;
}
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceId::operator==(const InputDeviceId& other) const
{
return (m_crc32 == other.m_crc32) && (m_index == other.m_index);
}
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceId::operator!=(const InputDeviceId& other) const
{
return !(*this == other);
}
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceId::operator<(const InputDeviceId& other) const
{
if (m_index == other.m_index)
{
return m_crc32 < other.m_crc32;
}
return m_index < other.m_index;
}
} // namespace AzFramework

@ -11,6 +11,7 @@
#include <AzCore/Math/Crc.h>
#include <AzCore/RTTI/ReflectContext.h>
#include <AzCore/std/hash.h>
#include <AzCore/std/string/fixed_string.h>
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
@ -22,8 +23,7 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
// Constants
static const int NAME_BUFFER_SIZE = 64;
static const int MAX_NAME_LENGTH = NAME_BUFFER_SIZE - 1;
static constexpr int MAX_NAME_LENGTH = 64;
////////////////////////////////////////////////////////////////////////////////////////////
// Allocator
@ -41,17 +41,16 @@ namespace AzFramework
//! Constructor
//! \param[in] name Name of the input device (will be truncated if exceeds MAX_NAME_LENGTH)
//! \param[in] index Index of the input device (optional)
explicit InputDeviceId(const char* name, AZ::u32 index = 0);
////////////////////////////////////////////////////////////////////////////////////////////
//! Copy constructor
//! \param[in] other Another instance of the class to copy from
InputDeviceId(const InputDeviceId& other);
explicit constexpr InputDeviceId(AZStd::string_view name, AZ::u32 index = 0)
: m_name(name.substr(0, MAX_NAME_LENGTH))
, m_crc32(name.substr(0, MAX_NAME_LENGTH))
, m_index(index)
{
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Copy assignment operator
//! \param[in] other Another instance of the class to copy from
InputDeviceId& operator=(const InputDeviceId& other);
// Default copying and moving
AZ_DEFAULT_COPY_MOVE(InputDeviceId);
////////////////////////////////////////////////////////////////////////////////////////////
//! Default destructor
@ -60,12 +59,18 @@ namespace AzFramework
////////////////////////////////////////////////////////////////////////////////////////////
//! Access to the input device's name
//! \return Name of the input device
const char* GetName() const;
constexpr const char* GetName() const
{
return m_name.c_str();
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Access to the crc32 of the input device's name
//! \return crc32 of the input device name
const AZ::Crc32& GetNameCrc32() const;
constexpr const AZ::Crc32& GetNameCrc32() const
{
return m_crc32;
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Access to the input device's index. Used for differentiating between multiple instances
@ -75,27 +80,45 @@ namespace AzFramework
//! at startup using indicies 0->3. As gamepads connect/disconnect at runtime we assign the
//! appropriate (system dependent) local user id (see InputDevice::GetAssignedLocalUserId).
//! \return Index of the input device
AZ::u32 GetIndex() const;
constexpr AZ::u32 GetIndex() const
{
return m_index;
}
////////////////////////////////////////////////////////////////////////////////////////////
///@{
//! Equality comparison operator
//! \param[in] other Another instance of the class to compare for equality
bool operator==(const InputDeviceId& other) const;
bool operator!=(const InputDeviceId& other) const;
///@}
constexpr bool operator==(const InputDeviceId& other) const
{
return (m_crc32 == other.m_crc32) && (m_index == other.m_index);
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Inequality comparison operator
//! \param[in] other Another instance of the class to compare for inequality
constexpr bool operator!=(const InputDeviceId& other) const
{
return !(*this == other);
}
////////////////////////////////////////////////////////////////////////////////////////////
//! Less than comparison operator
//! \param[in] other Another instance of the class to compare
bool operator<(const InputDeviceId& other) const;
constexpr bool operator<(const InputDeviceId& other) const
{
if (m_index == other.m_index)
{
return m_crc32 < other.m_crc32;
}
return m_index < other.m_index;
}
private:
////////////////////////////////////////////////////////////////////////////////////////////
// Variables
char m_name[NAME_BUFFER_SIZE]; //!< Name of the input device
AZ::Crc32 m_crc32; //!< Crc32 of the input device
AZ::u32 m_index; //!< Index of the input device
AZStd::fixed_string<MAX_NAME_LENGTH> m_name; //!< Name of the input device
AZ::Crc32 m_crc32; //!< Crc32 of the input device name
AZ::u32 m_index; //!< Index of the input device
};
} // namespace AzFramework

@ -15,9 +15,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const InputDeviceId InputDeviceKeyboard::Id("keyboard");
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceKeyboard::IsKeyboardDevice(const InputDeviceId& inputDeviceId)
{

@ -33,7 +33,7 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify the primary physical keyboard input device
static const InputDeviceId Id;
static constexpr inline InputDeviceId Id{"keyboard"};
////////////////////////////////////////////////////////////////////////////////////////////
//! Check whether an input device id identifies a physical keyboard (regardless of index)

@ -14,9 +14,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const InputDeviceId InputDeviceMotion::Id("motion");
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceMotion::IsMotionDevice(const InputDeviceId& inputDeviceId)
{

@ -28,7 +28,7 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify the primary motion input device
static const InputDeviceId Id;
static constexpr inline InputDeviceId Id{"motion"};
////////////////////////////////////////////////////////////////////////////////////////////
//! Check whether an input device id identifies a motion device (regardless of index)

@ -15,18 +15,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const AZ::u32 InputDeviceMouse::MovementSampleRateDefault = 60;
////////////////////////////////////////////////////////////////////////////////////////////////
const AZ::u32 InputDeviceMouse::MovementSampleRateQueueAll = std::numeric_limits<AZ::u32>::max();
////////////////////////////////////////////////////////////////////////////////////////////////
const AZ::u32 InputDeviceMouse::MovementSampleRateAccumulateAll = 0;
////////////////////////////////////////////////////////////////////////////////////////////////
const InputDeviceId InputDeviceMouse::Id("mouse");
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceMouse::IsMouseDevice(const InputDeviceId& inputDeviceId)
{

@ -31,23 +31,23 @@ namespace AzFramework
////////////////////////////////////////////////////////////////////////////////////////////
//! Default sample rate for raw mouse movement events that aims to strike a balance between
//! responsiveness and performance.
static const AZ::u32 MovementSampleRateDefault;
static constexpr inline AZ::u32 MovementSampleRateDefault{60};
////////////////////////////////////////////////////////////////////////////////////////////
//! Sample rate for raw mouse movement that will cause all events received in the same frame
//! to be queued and dispatched as individual events. This results in maximum responsiveness
//! but may potentially impact performance depending how many events happen over each frame.
static const AZ::u32 MovementSampleRateQueueAll;
static constexpr inline AZ::u32 MovementSampleRateQueueAll{std::numeric_limits<AZ::u32>::max()};
////////////////////////////////////////////////////////////////////////////////////////////
//! Sample rate for raw mouse movement that will cause all events received in the same frame
//! to be accumulated and dispatched as a single event. Optimal for performance, but results
//! in sluggish/unresponsive mouse movement, especially when running at low frame rates.
static const AZ::u32 MovementSampleRateAccumulateAll;
static constexpr inline AZ::u32 MovementSampleRateAccumulateAll{0};
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify the primary mouse input device
static const InputDeviceId Id;
static constexpr inline InputDeviceId Id{"mouse"};
////////////////////////////////////////////////////////////////////////////////////////////
//! Check whether an input device id identifies a mouse (regardless of index)

@ -15,9 +15,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const InputDeviceId InputDeviceTouch::Id("touch");
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceTouch::IsTouchDevice(const InputDeviceId& inputDeviceId)
{

@ -25,7 +25,7 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify the primary touch input device
static const InputDeviceId Id;
static constexpr inline InputDeviceId Id{"touch"};
////////////////////////////////////////////////////////////////////////////////////////////
//! Check whether an input device id identifies a touch device (regardless of index)

@ -14,9 +14,6 @@
////////////////////////////////////////////////////////////////////////////////////////////////////
namespace AzFramework
{
////////////////////////////////////////////////////////////////////////////////////////////////
const InputDeviceId InputDeviceVirtualKeyboard::Id("virtual_keyboard");
////////////////////////////////////////////////////////////////////////////////////////////////
bool InputDeviceVirtualKeyboard::IsVirtualKeyboardDevice(const InputDeviceId& inputDeviceId)
{

@ -25,7 +25,7 @@ namespace AzFramework
public:
////////////////////////////////////////////////////////////////////////////////////////////
//! The id used to identify the primary virtual keyboard input device
static const InputDeviceId Id;
static constexpr inline InputDeviceId Id{"virtual_keyboard"};
////////////////////////////////////////////////////////////////////////////////////////////
//! Check whether an input device id identifies a virtual keyboard (regardless of index)

@ -30,12 +30,22 @@ namespace AzFramework
//! Called when the root spawnable has been assigned a new value. This may be called several times without a call to release
//! in between.
//! @note: The callback is not queued but immediately called from a random thread. This is done because this callback is typically
//! used before entities are spawned and if it's queued then the entities spawn before this callback is called.
//! @param rootSpawnable The new root spawnable that was assigned.
//! @param generation The generation of the root spawnable. This will increment every time a new spawnable is assigned.
virtual void OnRootSpawnableAssigned([[maybe_unused]] AZ::Data::Asset<Spawnable> rootSpawnable,
[[maybe_unused]] uint32_t generation) {}
//! Called when the root spawnable has completed spawning of entities. This may be called several times without a call to release
//! in between.
//! @note: This callback is queued and will be called with a delay and from the main thread.
//! @param rootSpawnable The new root spawnable that was used to spawn entities from.
//! @param generation The generation of the root spawnable. This will increment every time a new spawnable is assigned.
virtual void OnRootSpawnableReady(
[[maybe_unused]] AZ::Data::Asset<Spawnable> rootSpawnable, [[maybe_unused]] uint32_t generation) {}
//! Called when the root spawnable has Released. This will only be called if there's no root spawnable assigned to take the
//! place of the original root spawnable.
//! Note: This callback is queued and will be called with a delay and from the main thread.
//! @param generation The generation of the root spawnable that was released.
virtual void OnRootSpawnableReleased([[maybe_unused]] uint32_t generation) {}
};

@ -6,12 +6,473 @@
*
*/
#include <AzCore/Asset/AssetSerializer.h>
#include <AzCore/RTTI/ReflectContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/numeric.h>
#include <AzCore/std/sort.h>
#include <AzCore/std/typetraits/typetraits.h>
#include <AzFramework/Spawnable/Spawnable.h>
namespace AzFramework
{
//
// EntityAlias
//
bool Spawnable::EntityAlias::HasLowerIndex(const EntityAlias& other) const
{
return m_sourceIndex == other.m_sourceIndex ?
m_aliasType < other.m_aliasType :
m_sourceIndex < other.m_sourceIndex;
}
//
// EntityAliasVisitorBase
//
bool Spawnable::EntityAliasVisitorBase::IsValid(const EntityAliasList* aliases) const
{
return aliases != nullptr;
}
bool Spawnable::EntityAliasVisitorBase::HasAliases(const EntityAliasList* aliases) const
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
return !aliases->empty();
}
bool Spawnable::EntityAliasVisitorBase::AreAllSpawnablesReady(const EntityAliasList* aliases) const
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
for (const EntityAlias& alias : *aliases)
{
if (!alias.m_queueLoad ||
alias.m_aliasType == Spawnable::EntityAliasType::Original ||
alias.m_aliasType == Spawnable::EntityAliasType::Disable)
{
continue;
}
if (!alias.m_spawnable.IsReady() && !alias.m_spawnable.IsError())
{
return false;
}
}
return true;
}
auto Spawnable::EntityAliasVisitorBase::begin(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
return aliases->cbegin();
}
auto Spawnable::EntityAliasVisitorBase::end(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
return aliases->cend();
}
auto Spawnable::EntityAliasVisitorBase::cbegin(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
return aliases->cbegin();
}
auto Spawnable::EntityAliasVisitorBase::cend(const EntityAliasList* aliases) const -> EntityAliasList::const_iterator
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
return aliases->cend();
}
void Spawnable::EntityAliasVisitorBase::ListTargetSpawnables(
const EntityAliasList* aliases, const ListTargetSpawanblesCallback& callback) const
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
AZStd::unordered_set<AZ::Data::AssetId> spawnableIds;
for (const Spawnable::EntityAlias& alias : *aliases)
{
// If the spawnable id is not valid it means that the alias is referencing the spawnable it's stored on.
if (alias.m_spawnable.GetId().IsValid())
{
auto it = spawnableIds.find(alias.m_spawnable.GetId());
if (it == spawnableIds.end())
{
callback(alias.m_spawnable);
spawnableIds.emplace(alias.m_spawnable.GetId());
}
}
}
}
void Spawnable::EntityAliasVisitorBase::ListTargetSpawnables(
const EntityAliasList* aliases, AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const
{
AZ_Assert(aliases, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
AZStd::unordered_set<AZ::Data::AssetId> spawnableIds;
for (const Spawnable::EntityAlias& alias : *aliases)
{
// If the spawnable id is not valid it means that the alias is referencing the spawnable it's stored on.
if (alias.m_tag == tag && alias.m_spawnable.GetId().IsValid())
{
auto it = spawnableIds.find(alias.m_spawnable.GetId());
if (it == spawnableIds.end())
{
callback(alias.m_spawnable);
spawnableIds.emplace(alias.m_spawnable.GetId());
}
}
}
}
//
// EntityAliasVisitor
//
Spawnable::EntityAliasVisitor::EntityAliasVisitor(Spawnable& owner, EntityAliasList* entityAliasList)
: m_owner(owner)
, m_entityAliasList(entityAliasList)
{
}
Spawnable::EntityAliasVisitor::~EntityAliasVisitor()
{
if (IsValid())
{
Optimize();
AZ_Assert(
m_owner.m_shareState == ShareState::ReadWrite, "Attempting to unlock a spawnable that's not in the locked state (%i).",
m_owner.m_shareState.load());
m_owner.m_shareState = ShareState::NotShared;
}
}
Spawnable::EntityAliasVisitor::EntityAliasVisitor(EntityAliasVisitor&& rhs)
: m_owner(rhs.m_owner)
, m_entityAliasList(rhs.m_entityAliasList)
{
m_dirty = rhs.m_dirty;
rhs.m_entityAliasList = nullptr;
rhs.m_dirty = false;
}
auto Spawnable::EntityAliasVisitor::operator=(EntityAliasVisitor&& rhs) -> EntityAliasVisitor&
{
if (this != &rhs)
{
this->~EntityAliasVisitor();
new(this) EntityAliasVisitor(AZStd::move(rhs));
}
return *this;
}
bool Spawnable::EntityAliasVisitor::IsValid() const
{
return EntityAliasVisitorBase::IsValid(m_entityAliasList);
}
bool Spawnable::EntityAliasVisitor::HasAliases() const
{
return EntityAliasVisitorBase::HasAliases(m_entityAliasList);
}
bool Spawnable::EntityAliasVisitor::AreAllSpawnablesReady() const
{
return EntityAliasVisitorBase::AreAllSpawnablesReady(m_entityAliasList);
}
auto Spawnable::EntityAliasVisitor::begin() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::begin(m_entityAliasList);
}
auto Spawnable::EntityAliasVisitor::end() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::end(m_entityAliasList);
}
auto Spawnable::EntityAliasVisitor::cbegin() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::cbegin(m_entityAliasList);
}
auto Spawnable::EntityAliasVisitor::cend() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::cend(m_entityAliasList);
}
void Spawnable::EntityAliasVisitor::ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const
{
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, callback);
}
void Spawnable::EntityAliasVisitor::ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const
{
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, tag, callback);
}
void Spawnable::EntityAliasVisitor::AddAlias(
AZ::Data::Asset<Spawnable> targetSpawnable,
AZ::Crc32 tag,
uint32_t sourceIndex,
uint32_t targetIndex,
Spawnable::EntityAliasType aliasType,
bool queueLoad)
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
AZ_Assert(sourceIndex < m_owner.GetEntities().size(), "Invalid source index (%i) for entity alias", sourceIndex);
if (targetSpawnable.IsReady())
{
AZ_Assert(
targetIndex < targetSpawnable->GetEntities().size(), "Invalid target index (%i) for entity alias '%s'", targetIndex,
targetSpawnable.GetHint().c_str());
}
m_entityAliasList->push_back(Spawnable::EntityAlias{ targetSpawnable, tag, sourceIndex, targetIndex, aliasType, queueLoad });
m_dirty = true;
}
void Spawnable::EntityAliasVisitor::ListSpawnablesRequiringLoad(const ListSpawnablesRequiringLoadCallback& callback)
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
for (Spawnable::EntityAlias& alias : *m_entityAliasList)
{
if (alias.m_queueLoad &&
alias.m_aliasType != Spawnable::EntityAliasType::Original &&
alias.m_aliasType != Spawnable::EntityAliasType::Disable &&
!alias.m_spawnable.IsLoading() &&
!alias.m_spawnable.IsReady() &&
!alias.m_spawnable.IsError())
{
callback(alias.m_spawnable);
}
}
}
void Spawnable::EntityAliasVisitor::UpdateAliases(const UpdateCallback& callback)
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
for (Spawnable::EntityAlias& alias : *m_entityAliasList)
{
AZ::Data::Asset<Spawnable> targetSpawnable(alias.m_spawnable);
callback(alias.m_aliasType, alias.m_queueLoad, targetSpawnable, alias.m_tag, alias.m_sourceIndex, alias.m_targetIndex);
}
m_dirty = true;
}
void Spawnable::EntityAliasVisitor::UpdateAliases(AZ::Crc32 tag, const UpdateCallback& callback)
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
for (Spawnable::EntityAlias& alias : *m_entityAliasList)
{
if (alias.m_tag == tag)
{
AZ::Data::Asset<Spawnable> targetSpawnable(alias.m_spawnable);
callback(alias.m_aliasType, alias.m_queueLoad, targetSpawnable, alias.m_tag, alias.m_sourceIndex, alias.m_targetIndex);
m_dirty = true;
}
}
}
void Spawnable::EntityAliasVisitor::UpdateAliasType(uint32_t index, Spawnable::EntityAliasType newType)
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
AZ_Assert(
index < m_entityAliasList->size(), "Unable to update entity alias at index %i as there are only %zu aliases in spawnable.",
index, m_entityAliasList->size());
(*m_entityAliasList)[index].m_aliasType = newType;
m_dirty = true;
}
void Spawnable::EntityAliasVisitor::Optimize()
{
AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked.");
if (m_dirty)
{
AZStd::stable_sort(
m_entityAliasList->begin(), m_entityAliasList->end(),
[](const Spawnable::EntityAlias& lhs, const Spawnable::EntityAlias& rhs)
{
// Sort by source index from smallest to largest so during spawning the entities can be iterated linearly over.
// If the source index is the same then sort by alias type so the next steps can optimize away superfluous steps.
return lhs.HasLowerIndex(rhs);
});
// Remove aliases that are not going to have any practical effect and insert aliases where needed to simplify the spawning.
// This is done at runtime rather than at build time because the above ebus allows other systems to make adjustments to the
// aliases, for instance Networking can decide to disable certain aliases when running on a client. This in turn also requires
// the aliases to be in their recorded order during building as the ebus handlers may depend on that order to determine what
// entities need to be updated.
uint32_t previousIndex = AZStd::numeric_limits<uint32_t>::max();
Spawnable::EntityAliasType previousType =
static_cast<Spawnable::EntityAliasType>(AZStd::numeric_limits<AZStd::underlying_type_t<Spawnable::EntityAliasType>>::max());
Spawnable::EntityAlias* it = m_entityAliasList->begin();
Spawnable::EntityAlias* end = m_entityAliasList->end();
while (it < end)
{
// If there's a switch to a new source index and the previous index only had an original it can
// be removed.
if (previousType == Spawnable::EntityAliasType::Original && previousIndex != it->m_sourceIndex)
{
it = m_entityAliasList->erase(it - 1);
end = m_entityAliasList->end();
if (it == end)
{
break;
}
}
switch (it->m_aliasType)
{
case Spawnable::EntityAliasType::Original:
[[fallthrough]];
case Spawnable::EntityAliasType::Disable:
[[fallthrough]];
case Spawnable::EntityAliasType::Replace:
// If the previous entry was a disabled, original or replace alias then remove it as it will be overwritten by the
// current entry.
if (previousIndex == it->m_sourceIndex &&
(previousType == Spawnable::EntityAliasType::Original ||
previousType == Spawnable::EntityAliasType::Disable ||
previousType == Spawnable::EntityAliasType::Replace))
{
previousIndex = it->m_sourceIndex;
previousType = it->m_aliasType;
// Erase instead of a swap-and-pop in order to preserver the order.
it = m_entityAliasList->erase(it - 1) + 1;
end = m_entityAliasList->end();
}
else
{
previousIndex = it->m_sourceIndex;
previousType = it->m_aliasType;
++it;
}
break;
case Spawnable::EntityAliasType::Additional:
[[fallthrough]];
case Spawnable::EntityAliasType::Merge:
// If this is the first entry for this index then insert an original in front of it so the spawnable entity manager
// doesn't have to check for the case there's a merge and/or addition without an entity to extend.
if (previousIndex != it->m_sourceIndex)
{
Spawnable::EntityAlias insert;
// No load, as the asset is already loaded.
insert.m_spawnable = AZ::Data::Asset<Spawnable>({}, azrtti_typeid<Spawnable>());
insert.m_sourceIndex = it->m_sourceIndex;
insert.m_targetIndex = it->m_sourceIndex; // Source index as the original entry for this slot is added.
insert.m_aliasType = Spawnable::EntityAliasType::Original;
previousIndex = it->m_sourceIndex;
previousType = it->m_aliasType;
// Insert to maintain the order.
it = m_entityAliasList->insert(it, AZStd::move(insert));
it += 2;
end = m_entityAliasList->end();
}
else
{
previousType = it->m_aliasType;
++it;
}
break;
default:
AZ_Assert(false, "Invalid Spawnable entity alias type found during asset loading: %i", it->m_aliasType);
break;
}
}
// Check if the last entry is an "Original" in which case it can be removed.
if (!m_entityAliasList->empty() && m_entityAliasList->back().m_aliasType == Spawnable::EntityAliasType::Original)
{
m_entityAliasList->pop_back();
}
// Reclaim memory because after this point the aliases will not change anymore.
m_entityAliasList->shrink_to_fit();
m_dirty = false;
}
}
//
// EntityAliasConstVisitor
//
Spawnable::EntityAliasConstVisitor::EntityAliasConstVisitor(const Spawnable& owner, const EntityAliasList* entityAliasList)
: m_owner(owner)
, m_entityAliasList(entityAliasList)
{
}
Spawnable::EntityAliasConstVisitor::~EntityAliasConstVisitor()
{
if (IsValid())
{
AZ_Assert(
m_owner.m_shareState <= ShareState::Read, "Attempting to unlock a read shared spawnable that was not in a read shared mode (%i).",
m_owner.m_shareState.load());
m_owner.m_shareState++;
}
}
bool Spawnable::EntityAliasConstVisitor::IsValid() const
{
return EntityAliasVisitorBase::IsValid(m_entityAliasList);
}
bool Spawnable::EntityAliasConstVisitor::HasAliases() const
{
return EntityAliasVisitorBase::HasAliases(m_entityAliasList);
}
bool Spawnable::EntityAliasConstVisitor::AreAllSpawnablesReady() const
{
return EntityAliasVisitorBase::AreAllSpawnablesReady(m_entityAliasList);
}
auto Spawnable::EntityAliasConstVisitor::begin() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::begin(m_entityAliasList);
}
auto Spawnable::EntityAliasConstVisitor::end() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::end(m_entityAliasList);
}
auto Spawnable::EntityAliasConstVisitor::cbegin() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::cbegin(m_entityAliasList);
}
auto Spawnable::EntityAliasConstVisitor::cend() const -> EntityAliasList::const_iterator
{
return EntityAliasVisitorBase::cend(m_entityAliasList);
}
void Spawnable::EntityAliasConstVisitor::ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const
{
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, callback);
}
void Spawnable::EntityAliasConstVisitor::ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const
{
EntityAliasVisitorBase::ListTargetSpawnables(m_entityAliasList, tag, callback);
}
//
// Spawnable
//
Spawnable::Spawnable(const AZ::Data::AssetId& id, AssetStatus status)
: AZ::Data::AssetData(id, status)
{
@ -27,6 +488,33 @@ namespace AzFramework
return m_entities;
}
auto Spawnable::TryGetAliasesConst() const -> EntityAliasConstVisitor
{
int32_t expected = ShareState::NotShared;
do
{
// Try to set the lock to a negative number to indicate a shared read.
if (m_shareState.compare_exchange_strong(expected, expected - 1))
{
return EntityAliasConstVisitor(*this, &m_entityAliases);
}
// as long as the value is negative or not shared then keep trying to get a shared read lock.
} while (expected <= 0);
return EntityAliasConstVisitor(*this, nullptr);
}
auto Spawnable::TryGetAliases() const -> EntityAliasConstVisitor
{
return TryGetAliasesConst();
}
auto Spawnable::TryGetAliases() -> EntityAliasVisitor
{
int32_t expected = ShareState::NotShared;
return m_shareState.compare_exchange_strong(expected, ShareState::ReadWrite) ? EntityAliasVisitor(*this, &m_entityAliases)
: EntityAliasVisitor(*this, nullptr);
}
bool Spawnable::IsEmpty() const
{
return m_entities.empty();
@ -44,11 +532,29 @@ namespace AzFramework
void Spawnable::Reflect(AZ::ReflectContext* context)
{
EntityAlias::Reflect(context);
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context); serializeContext != nullptr)
{
serializeContext->Class<Spawnable, AZ::Data::AssetData>()->Version(1)
serializeContext->Class<Spawnable, AZ::Data::AssetData>()->Version(2)
->Field("Meta data", &Spawnable::m_metaData)
->Field("Entity aliases", &Spawnable::m_entityAliases)
->Field("Entities", &Spawnable::m_entities);
}
}
void Spawnable::EntityAlias::Reflect(AZ::ReflectContext* context)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(context); serializeContext != nullptr)
{
serializeContext->Class<Spawnable::EntityAlias>()
->Version(1)
->Field("Spawnable", &EntityAlias::m_spawnable)
->Field("Tag", &EntityAlias::m_tag)
->Field("Source Index", &EntityAlias::m_sourceIndex)
->Field("Target Index", &EntityAlias::m_targetIndex)
->Field("Alias Type", &EntityAlias::m_aliasType)
->Field("Queue Load", &EntityAlias::m_queueLoad);
}
}
} // namespace AzFramework

@ -11,6 +11,7 @@
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/std/parallel/atomic.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzFramework/Spawnable/SpawnableMetaData.h>
@ -29,7 +30,148 @@ namespace AzFramework
AZ_CLASS_ALLOCATOR(Spawnable, AZ::SystemAllocator, 0);
AZ_RTTI(AzFramework::Spawnable, "{855E3021-D305-4845-B284-20C3F7FDF16B}", AZ::Data::AssetData);
// The order is important for sorting in the SpawnableAssetHandler.
enum class EntityAliasType : uint8_t
{
Original, //!< The original entity is spawned.
Disable, //!< No entity will be spawned.
Replace, //!< The entity alias is spawned instead of the original.
Additional, //!< The original entity is spawned as well as the alias. The alias will get a new entity id.
Merge //!< The original entity is spawned and the components of the alias are added. The caller is responsible for
//!< maintaining a valid component list.
};
enum ShareState : int32_t
{
Read = -1,
NotShared = 0,
ReadWrite = 1
};
//! An entity alias redirects the spawning of an entity to another entity, possibly in another spawnable.
struct EntityAlias
{
AZ_CLASS_ALLOCATOR(EntityAlias, AZ::SystemAllocator, 0);
AZ_TYPE_INFO(AzFramework::Spawnable::EntityAlias, "{C8D0C5BC-1F0B-4572-98C1-73B2CA8C9356}");
bool HasLowerIndex(const EntityAlias& other) const;
AZ::Data::Asset<Spawnable> m_spawnable; //!< The spawnable containing the target entity to spawn.
uint32_t m_tag{ 0 }; //!< A unique tag to identify this alias with.
uint32_t m_sourceIndex{ 0 }; //!< The index of the entity in the original spawnable that will be replaced.
uint32_t m_targetIndex{ 0 }; //!< The index of the entity in the target spawnable that will be used to replace the original.
EntityAliasType m_aliasType{ EntityAliasType::Original }; //!< The kind of replacement.
bool m_queueLoad{ false }; //!< Whether or not to automatically queue the spawnable for loading.
static void Reflect(AZ::ReflectContext* context);
};
using EntityList = AZStd::vector<AZStd::unique_ptr<AZ::Entity>>;
using EntityAliasList = AZStd::vector<EntityAlias>;
private:
class EntityAliasVisitorBase
{
protected:
bool IsValid(const EntityAliasList* aliases) const;
bool HasAliases(const EntityAliasList* aliases) const;
bool AreAllSpawnablesReady(const EntityAliasList* aliases) const;
EntityAliasList::const_iterator begin(const EntityAliasList* aliases) const;
EntityAliasList::const_iterator end(const EntityAliasList* aliases) const;
EntityAliasList::const_iterator cbegin(const EntityAliasList* aliases) const;
EntityAliasList::const_iterator cend(const EntityAliasList* aliases) const;
using ListTargetSpawanblesCallback = AZStd::function<void(const AZ::Data::Asset<Spawnable>& targetSpawnable)>;
void ListTargetSpawnables(const EntityAliasList* aliases, const ListTargetSpawanblesCallback& callback) const;
void ListTargetSpawnables(const EntityAliasList* aliases, AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const;
};
public:
class EntityAliasVisitor final : public EntityAliasVisitorBase
{
public:
EntityAliasVisitor(Spawnable& owner, EntityAliasList* m_entityAliasList);
~EntityAliasVisitor();
EntityAliasVisitor(EntityAliasVisitor&& rhs);
EntityAliasVisitor& operator=(EntityAliasVisitor&& rhs);
EntityAliasVisitor(const EntityAliasVisitor& rhs) = delete;
EntityAliasVisitor& operator=(const EntityAliasVisitor& rhs) = delete;
//! Checks if the visitor was able to retrieve data. This needs to be checked before calling any other functions.
bool IsValid() const;
bool HasAliases() const;
bool AreAllSpawnablesReady() const;
// Modification of aliases is limited to specific changes that can only be done through the available modification functions.
// For this reason access through iterators is limited to unmodifiable constant iterators.
EntityAliasList::const_iterator begin() const;
EntityAliasList::const_iterator end() const;
EntityAliasList::const_iterator cbegin() const;
EntityAliasList::const_iterator cend() const;
void ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const;
void ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const;
void AddAlias(
AZ::Data::Asset<Spawnable> targetSpawnable,
AZ::Crc32 tag,
uint32_t sourceIndex,
uint32_t targetIndex,
Spawnable::EntityAliasType aliasType,
bool queueLoad);
using ListSpawnablesRequiringLoadCallback = AZStd::function<void(AZ::Data::Asset<Spawnable>& spawnablePendingLoad)>;
void ListSpawnablesRequiringLoad(const ListSpawnablesRequiringLoadCallback& callback);
using UpdateCallback = AZStd::function<void(
Spawnable::EntityAliasType& aliasType,
bool& queueLoad,
const AZ::Data::Asset<Spawnable>& aliasedSpawnable,
const AZ::Crc32 tag,
const uint32_t sourceIndex,
const uint32_t targetIndex)>;
void UpdateAliases(const UpdateCallback& callback);
void UpdateAliases(AZ::Crc32 tag, const UpdateCallback& callback);
void UpdateAliasType(uint32_t index, Spawnable::EntityAliasType newType);
void Optimize();
private:
Spawnable& m_owner;
EntityAliasList* m_entityAliasList{ nullptr };
bool m_dirty{ false };
};
class EntityAliasConstVisitor final : public EntityAliasVisitorBase
{
public:
EntityAliasConstVisitor(const Spawnable& owner, const EntityAliasList* entityAliasList);
~EntityAliasConstVisitor();
//! Checks if the visitor was able to retrieve data. This needs to be checked before calling any other functions.
bool IsValid() const;
bool HasAliases() const;
bool AreAllSpawnablesReady() const;
EntityAliasList::const_iterator begin() const;
EntityAliasList::const_iterator end() const;
EntityAliasList::const_iterator cbegin() const;
EntityAliasList::const_iterator cend() const;
void ListTargetSpawnables(const ListTargetSpawanblesCallback& callback) const;
void ListTargetSpawnables(AZ::Crc32 tag, const ListTargetSpawanblesCallback& callback) const;
private:
const Spawnable& m_owner;
const EntityAliasList* m_entityAliasList;
};
inline static constexpr const char* FileExtension = "spawnable";
inline static constexpr const char* DotFileExtension = ".spawnable";
@ -45,6 +187,9 @@ namespace AzFramework
const EntityList& GetEntities() const;
EntityList& GetEntities();
EntityAliasConstVisitor TryGetAliasesConst() const;
EntityAliasConstVisitor TryGetAliases() const;
EntityAliasVisitor TryGetAliases();
bool IsEmpty() const;
SpawnableMetaData& GetMetaData();
@ -55,11 +200,12 @@ namespace AzFramework
private:
SpawnableMetaData m_metaData;
// Aliases that optionally replace the ones stored in this spawnable.
EntityAliasList m_entityAliases;
// Container for keeping all entities of the prefab the Spawnable was created from.
// Includes both direct and nested entities of the prefab.
EntityList m_entities;
};
using SpawnableList = AZStd::vector<Spawnable>;
mutable AZStd::atomic<int32_t> m_shareState{ ShareState::NotShared };
};
} // namespace AzFramework

@ -0,0 +1,38 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/EBus/EBus.h>
#include <AzCore/std/parallel/mutex.h>
#include <AzFramework/Spawnable/Spawnable.h>
namespace AzFramework
{
class SpawnableAssetEvents : public AZ::EBusTraits
{
public:
using MutexType = AZStd::recursive_mutex;
//! Callback to allow the entity aliases in a spawnable to adjusted based on runtime requirements.
//! This will be called by the Asset Manager as part of the creation of the spawnable asset from loaded file data. Any work done
//! in this callback will be counted towards the maximum amount of time allocated to asset handlers to construct their assets,
//! it's recommended to keep work done in this callback to a minimum and prefer delaying any complex processing.
//!
//! ALERT: Do not start blocking asset requests in this callback.
//! Since this is part of the Asset Manager's asset streaming, doing a blocking load in this callback will cause the job
//! processing the spawnable asset to locked out of doing any asset streaming work. If there are more spawnables doing
//! this than there are job threads available the engine will enter a deadlock situation as no more assets can complete
//! loading and no job threads become free as they're all waiting for assets to complete. It is however safe to queue
//! an asset for loading.
virtual void OnResolveAliases(
Spawnable::EntityAliasVisitor& aliases, const SpawnableMetaData& metadata, const Spawnable::EntityList& entities) = 0;
};
using SpawnableAssetEventsBus = AZ::EBus<SpawnableAssetEvents>;
} // namespace AzFramework

@ -9,8 +9,10 @@
#include <AzCore/Casting/lossy_cast.h>
#include <AzCore/Serialization/Utils.h>
#include <AzCore/std/string/string.h>
#include <AzCore/std/sort.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzFramework/Spawnable/SpawnableAssetHandler.h>
#include <AzFramework/Spawnable/SpawnableAssetBus.h>
namespace AzFramework
{
@ -52,6 +54,7 @@ namespace AzFramework
AZ::ObjectStream::FilterDescriptor filter(assetLoadFilterCB);
if (AZ::Utils::LoadObjectFromStreamInPlace(*stream, *spawnable, nullptr /*SerializeContext*/, filter))
{
ResolveEntityAliases(spawnable, asset, stream->GetStreamingDeadline(), stream->GetStreamingPriority(), assetLoadFilterCB);
return AZ::Data::AssetHandler::LoadResult::LoadComplete;
}
else
@ -91,4 +94,41 @@ namespace AzFramework
AZ::Uuid subIdHash = AZ::Uuid::CreateData(id.data(), id.size());
return azlossy_caster(subIdHash.GetHash());
}
void SpawnableAssetHandler::ResolveEntityAliases(
Spawnable* spawnable,
[[maybe_unused]] const AZ::Data::Asset<AZ::Data::AssetData>& asset,
AZStd::chrono::milliseconds streamingDeadline,
AZ::IO::IStreamerTypes::Priority streamingPriority,
const AZ::Data::AssetFilterCB& assetLoadFilterCB)
{
Spawnable::EntityAliasVisitor aliases = spawnable->TryGetAliases();
AZ_Assert(aliases.IsValid(), "Newly created Spawnable '%s' was already locked.", asset.GetHint().c_str());
if (aliases.HasAliases())
{
AZ_Assert(
AZStd::is_sorted(
aliases.begin(), aliases.end(),
[](const Spawnable::EntityAlias& lhs, const Spawnable::EntityAlias& rhs)
{
return lhs.HasLowerIndex(rhs);
}),
"Spawnable '%s' has an unsorted entity alias list.", asset.GetHint().c_str());
SpawnableAssetEventsBus::Broadcast(
&SpawnableAssetEvents::OnResolveAliases, aliases, spawnable->GetMetaData(), spawnable->GetEntities());
// The aliases will only be optimized if OnResolveAliases has made any changes.
aliases.Optimize();
aliases.ListSpawnablesRequiringLoad(
[&assetLoadFilterCB, streamingDeadline, streamingPriority](AZ::Data::Asset<Spawnable>& assetPendingLoad)
{
AZ::Data::AssetLoadParameters loadInfo;
loadInfo.m_assetLoadFilterCB = assetLoadFilterCB;
loadInfo.m_deadline = streamingDeadline;
loadInfo.m_priority = streamingPriority;
assetPendingLoad.QueueLoad(loadInfo);
});
}
}
} // namespace AzFramework

@ -50,5 +50,13 @@ namespace AzFramework
const AZ::Data::Asset<AZ::Data::AssetData>& asset,
AZStd::shared_ptr<AZ::Data::AssetDataStream> stream,
const AZ::Data::AssetFilterCB& assetLoadFilterCB) override;
private:
void ResolveEntityAliases(
class Spawnable* spawnable,
const AZ::Data::Asset<AZ::Data::AssetData>& asset,
AZStd::chrono::milliseconds streamingDeadline,
AZ::IO::IStreamerTypes::Priority streamingPriority,
const AZ::Data::AssetFilterCB& assetLoadFilterCB);
};
} // namespace AzFramework

@ -26,7 +26,7 @@ namespace AzFramework
return m_threadData != nullptr;
}
uint64_t SpawnableEntitiesContainer::GetCurrentGeneration() const
uint32_t SpawnableEntitiesContainer::GetCurrentGeneration() const
{
return m_currentGeneration;
}
@ -37,7 +37,7 @@ namespace AzFramework
SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket);
}
void SpawnableEntitiesContainer::SpawnEntities(AZStd::vector<size_t> entityIndices)
void SpawnableEntitiesContainer::SpawnEntities(AZStd::vector<uint32_t> entityIndices)
{
AZ_Assert(m_threadData, "Calling SpawnEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->SpawnEntities(
@ -78,15 +78,21 @@ namespace AzFramework
}
}
void SpawnableEntitiesContainer::Alert(AlertCallback callback)
void SpawnableEntitiesContainer::Alert(AlertCallback callback, CheckIfSpawnableIsLoaded spawnableCheck)
{
AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->Barrier(
m_threadData->m_spawnedEntitiesTicket,
[generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket::Id)
{
callback(generation);
});
auto callbackWrapper = [generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket::Id)
{
callback(generation);
};
if (spawnableCheck == CheckIfSpawnableIsLoaded::No)
{
SpawnableEntitiesInterface::Get()->Barrier(m_threadData->m_spawnedEntitiesTicket, AZStd::move(callbackWrapper));
}
else
{
SpawnableEntitiesInterface::Get()->LoadBarrier(m_threadData->m_spawnedEntitiesTicket, AZStd::move(callbackWrapper));
}
}
void SpawnableEntitiesContainer::Connect(AZ::Data::Asset<Spawnable> spawnable)

@ -36,6 +36,12 @@ namespace AzFramework
public:
using AlertCallback = AZStd::function<void(uint32_t generation)>;
enum class CheckIfSpawnableIsLoaded : bool
{
Yes,
No
};
//! Constructs a new spawnables entity container that has not been connected.
SpawnableEntitiesContainer() = default;
//! Constructs a new spawnables entity container that connects to the provided spawnable.
@ -48,13 +54,13 @@ namespace AzFramework
//! Returns a number that identifies the current generation of the container with. The completion callback can still receive
//! calls from older generations as processing completes on those. The returned value can be used to help calls tell
//! older versions apart from newer ones.
[[nodiscard]] uint64_t GetCurrentGeneration() const;
[[nodiscard]] uint32_t GetCurrentGeneration() const;
//! Puts in a request to spawn entities using all entities in the provided spawnable as a template.
void SpawnAllEntities();
//! Puts in a request to spawn entities using the entities found in the spawnable at the provided indices as a template.
//! @param entityIndices A list of indices to the entities in the spawnable.
void SpawnEntities(AZStd::vector<size_t> entityIndices);
void SpawnEntities(AZStd::vector<uint32_t> entityIndices);
//! Puts in a request to despawn all previous spawned entities.
void DespawnAllEntities();
@ -73,7 +79,11 @@ namespace AzFramework
//! other than the calling thread including the main thread. Note that because the alert is queued it can still be called
//! after the container has been deleted or can be called for a previously assigned spawnable. In the latter case check
//! if the current generation matches the generation provided with the callback.
void Alert(AlertCallback callback);
//! @param callback The function called when the alert triggers. This can be called from a different thread than the one that
//! the one that made the call to Alert.
//! @param checkSpawnableIsLoaded If true the alert will also block until the spawnable has been loaded. If false then it will
//! be called after all previous calls have completed, but the spawnable may not be loaded at that point.
void Alert(AlertCallback callback, CheckIfSpawnableIsLoaded spawnableCheck = CheckIfSpawnableIsLoaded::No);
private:
void Connect(AZ::Data::Asset<Spawnable> spawnable);

@ -152,7 +152,7 @@ namespace AzFramework
// SpawnableIndexEntityPair
//
SpawnableIndexEntityPair::SpawnableIndexEntityPair(AZ::Entity** entityIterator, size_t* indexIterator)
SpawnableIndexEntityPair::SpawnableIndexEntityPair(AZ::Entity** entityIterator, uint32_t* indexIterator)
: m_entity(entityIterator)
, m_index(indexIterator)
{
@ -168,7 +168,7 @@ namespace AzFramework
return *m_entity;
}
size_t SpawnableIndexEntityPair::GetIndex() const
uint32_t SpawnableIndexEntityPair::GetIndex() const
{
return *m_index;
}
@ -177,7 +177,7 @@ namespace AzFramework
// SpawnableIndexEntityIterator
//
SpawnableIndexEntityIterator::SpawnableIndexEntityIterator(AZ::Entity** entityIterator, size_t* indexIterator)
SpawnableIndexEntityIterator::SpawnableIndexEntityIterator(AZ::Entity** entityIterator, uint32_t* indexIterator)
: m_value(entityIterator, indexIterator)
{
}
@ -248,7 +248,7 @@ namespace AzFramework
//
SpawnableConstIndexEntityContainerView::SpawnableConstIndexEntityContainerView(
AZ::Entity** beginEntity, size_t* beginIndices, size_t length)
AZ::Entity** beginEntity, uint32_t* beginIndices, size_t length)
: m_begin(beginEntity, beginIndices)
, m_end(beginEntity + length, beginIndices + length)
{

@ -85,19 +85,19 @@ namespace AzFramework
AZ::Entity* GetEntity();
const AZ::Entity* GetEntity() const;
size_t GetIndex() const;
uint32_t GetIndex() const;
private:
SpawnableIndexEntityPair() = default;
SpawnableIndexEntityPair(const SpawnableIndexEntityPair&) = default;
SpawnableIndexEntityPair(SpawnableIndexEntityPair&&) = default;
SpawnableIndexEntityPair(AZ::Entity** entityIterator, size_t* indexIterator);
SpawnableIndexEntityPair(AZ::Entity** entityIterator, uint32_t* indexIterator);
SpawnableIndexEntityPair& operator=(const SpawnableIndexEntityPair&) = default;
SpawnableIndexEntityPair& operator=(SpawnableIndexEntityPair&&) = default;
AZ::Entity** m_entity { nullptr };
size_t* m_index { nullptr };
uint32_t* m_index { nullptr };
};
class SpawnableIndexEntityIterator
@ -110,7 +110,7 @@ namespace AzFramework
using pointer = SpawnableIndexEntityPair*;
using reference = SpawnableIndexEntityPair&;
SpawnableIndexEntityIterator(AZ::Entity** entityIterator, size_t* indexIterator);
SpawnableIndexEntityIterator(AZ::Entity** entityIterator, uint32_t* indexIterator);
SpawnableIndexEntityIterator& operator++();
SpawnableIndexEntityIterator operator++(int);
@ -132,7 +132,7 @@ namespace AzFramework
class SpawnableConstIndexEntityContainerView
{
public:
SpawnableConstIndexEntityContainerView(AZ::Entity** beginEntity, size_t* beginIndices, size_t length);
SpawnableConstIndexEntityContainerView(AZ::Entity** beginEntity, uint32_t* beginIndices, size_t length);
const SpawnableIndexEntityIterator& begin();
const SpawnableIndexEntityIterator& end();
@ -144,6 +144,16 @@ namespace AzFramework
SpawnableIndexEntityIterator m_end;
};
//! Information used when updating the type of an entity alias.
struct EntityAliasTypeChange
{
//! The index of the alias in the spawnable. Note that due to optimizations done on the entity aliases the index of an alias
//! can change over time.
uint32_t m_aliasIndex;
//! The type to replace type stored in the spawnable at the index provided by m_aliasIndex.
Spawnable::EntityAliasType m_newAliasType;
};
//! Requests to the SpawnableEntitiesInterface require a ticket with a valid spawnable that is used as a template. A ticket can
//! be reused for multiple calls on the same spawnable and is safe to be used by multiple threads at the same time. Entities created
//! from the spawnable may be tracked by the ticket and so using the same ticket is needed to despawn the exact entities created
@ -178,6 +188,7 @@ namespace AzFramework
using EntityDespawnCallback = AZStd::function<void(EntitySpawnTicket::Id)>;
using RetrieveEntitySpawnTicketCallback = AZStd::function<void(EntitySpawnTicket*)>;
using ReloadSpawnableCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstEntityContainerView)>;
using UpdateEntityAliasTypesCallback = AZStd::function<void(EntitySpawnTicket::Id)>;
using ListEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstEntityContainerView)>;
using ListIndicesEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableConstIndexEntityContainerView)>;
using ClaimEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableEntityContainerView)>;
@ -247,6 +258,15 @@ namespace AzFramework
SpawnablePriority m_priority { SpawnablePriority_Default };
};
struct UpdateEntityAliasTypesOptionalArgs final
{
//! Callback that's called when entity aliases are updated. This can be triggered from a different thread than the one that
//! made the function call to update.
UpdateEntityAliasTypesCallback m_completionCallback;
//! The priority at which this call will be executed.
SpawnablePriority m_priority{ SpawnablePriority_Default };
};
struct ListEntitiesOptionalArgs final
{
//! The priority at which this call will be executed.
@ -265,6 +285,14 @@ namespace AzFramework
SpawnablePriority m_priority{ SpawnablePriority_Default };
};
struct LoadBarrierOptionalArgs final
{
//! The priority at which this call will be executed.
SpawnablePriority m_priority{ SpawnablePriority_Default };
//! Also checks if the spawnables referenced in the entity aliases that are marked to be loaded are loaded.
bool m_checkAliasSpawnables{ true };
};
//! Interface definition to (de)spawn entities from a spawnable into the game world.
//!
//! While the callbacks of the individual calls are being processed they will block processing any other request. Callbacks can be
@ -298,7 +326,7 @@ namespace AzFramework
//! @param entityIndices The indices into the template entities stored in the spawnable that will be used to spawn entities from.
//! @param optionalArgs Optional additional arguments, see SpawnEntitiesOptionalArgs.
virtual void SpawnEntities(
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) = 0;
EntitySpawnTicket& ticket, AZStd::vector<uint32_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) = 0;
//! Removes all entities in the provided list from the environment.
//! @param ticket The ticket previously used to spawn entities with.
//! @param optionalArgs Optional additional arguments, see DespawnAllEntitiesOptionalArgs.
@ -320,6 +348,16 @@ namespace AzFramework
virtual void ReloadSpawnable(
EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable, ReloadSpawnableOptionalArgs optionalArgs = {}) = 0;
//! Allows updating the entity alias on a spawnable. This allows the spawning behavior for all entities spawned from the used
//! spawnable to be changed and is not restricted to this ticket alone.
//! @param ticket Holds the information for the spawnable.
//! @param updateAliases An array of index and alias type values used to update the entity alias list.
//! @param optionalArgs Optional additional arguments, see UpdateEntityAliasTypesOptionalArgs.
virtual void UpdateEntityAliasTypes(
EntitySpawnTicket& ticket,
AZStd::vector<EntityAliasTypeChange> updatedAliases,
UpdateEntityAliasTypesOptionalArgs optionalArgs = {}) = 0;
//! List all entities that are spawned using this ticket.
//! @param ticket Only the entities associated with this ticket will be listed.
//! @param listCallback Required callback that will be called to list the entities on.
@ -351,31 +389,37 @@ namespace AzFramework
//! @param completionCallback Required callback that will be called as soon as the barrier has been reached.
//! @param optionalArgs Optional additional arguments, see BarrierOptionalArgs.
virtual void Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs = {}) = 0;
//! Blocks until the spawnable is loaded and all operations made on the provided ticket before the barrier call have completed.
//! @param ticket The ticket to monitor.
//! @param completionCallback Required callback that will be called as soon as the barrier has been reached.
//! @param optionalArgs Optional additional arguments, see BarrierOptionalArgs.
virtual void LoadBarrier(
EntitySpawnTicket& ticket, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs = {}) = 0;
protected:
[[nodiscard]] virtual AZStd::pair<EntitySpawnTicket::Id, void*> CreateTicket(AZ::Data::Asset<Spawnable>&& spawnable) = 0;
virtual void DestroyTicket(void* ticket) = 0;
template<typename T>
static T& GetTicketPayload(EntitySpawnTicket& ticket)
[[nodiscard]] static T& GetTicketPayload(EntitySpawnTicket& ticket)
{
return *reinterpret_cast<T*>(ticket.m_payload);
}
template<typename T>
static const T& GetTicketPayload(const EntitySpawnTicket& ticket)
[[nodiscard]] static const T& GetTicketPayload(const EntitySpawnTicket& ticket)
{
return *reinterpret_cast<const T*>(ticket.m_payload);
}
template<typename T>
static T* GetTicketPayload(EntitySpawnTicket* ticket)
[[nodiscard]] static T* GetTicketPayload(EntitySpawnTicket* ticket)
{
return reinterpret_cast<T*>(ticket->m_payload);
}
template<typename T>
static const T* GetTicketPayload(const EntitySpawnTicket* ticket)
[[nodiscard]] static const T* GetTicketPayload(const EntitySpawnTicket* ticket)
{
return reinterpret_cast<const T*>(ticket->m_payload);
}

@ -60,7 +60,7 @@ namespace AzFramework
}
void SpawnableEntitiesManager::SpawnEntities(
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs)
EntitySpawnTicket& ticket, AZStd::vector<uint32_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnEntities hasn't been initialized.");
@ -128,6 +128,20 @@ namespace AzFramework
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::UpdateEntityAliasTypes(
EntitySpawnTicket& ticket,
AZStd::vector<EntityAliasTypeChange> updatedAliases,
UpdateEntityAliasTypesOptionalArgs optionalArgs)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to ReloadSpawnable hasn't been initialized.");
UpdateEntityAliasTypesCommand queueEntry;
queueEntry.m_entityAliases = AZStd::move(updatedAliases);
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_completionCallback = AZStd::move(optionalArgs.m_completionCallback);
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::ListEntities(
EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs)
{
@ -175,6 +189,19 @@ namespace AzFramework
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::LoadBarrier(
EntitySpawnTicket& ticket, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs)
{
AZ_Assert(completionCallback, "Load barrier on spawnable entities called without a valid callback to use.");
AZ_Assert(ticket.IsValid(), "Ticket provided to LoadBarrier hasn't been initialized.");
LoadBarrierCommand queueEntry;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_completionCallback = AZStd::move(completionCallback);
queueEntry.m_checkAliasSpawnables = optionalArgs.m_checkAliasSpawnables;
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
auto SpawnableEntitiesManager::ProcessQueue(CommandQueuePriority priority) -> CommandQueueStatus
{
CommandQueueStatus result = CommandQueueStatus::NoCommandsLeft;
@ -203,13 +230,13 @@ namespace AzFramework
for (size_t i = 0; i < delayedSize; ++i)
{
Requests& request = queue.m_delayed.front();
bool result = AZStd::visit(
[this](auto&& args) -> bool
CommandResult result = AZStd::visit(
[this](auto&& args) -> CommandResult
{
return ProcessRequest(args);
},
request);
if (!result)
if (result == CommandResult::Requeue)
{
queue.m_delayed.emplace_back(AZStd::move(request));
}
@ -230,13 +257,13 @@ namespace AzFramework
while (!pendingRequestQueue.empty())
{
Requests& request = pendingRequestQueue.front();
bool result = AZStd::visit(
[this](auto&& args) -> bool
CommandResult result = AZStd::visit(
[this](auto&& args) -> CommandResult
{
return ProcessRequest(args);
},
request);
if (!result)
if (result == CommandResult::Requeue)
{
queue.m_delayed.emplace_back(AZStd::move(request));
}
@ -273,14 +300,73 @@ namespace AzFramework
}
}
AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate,
EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext)
AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityPrototype,
EntityIdMap& prototypeToCloneMap, AZ::SerializeContext& serializeContext)
{
// If the same ID gets remapped more than once, preserve the original remapping instead of overwriting it.
constexpr bool allowDuplicateIds = false;
return AZ::IdUtils::Remapper<AZ::EntityId, allowDuplicateIds>::CloneObjectAndGenerateNewIdsAndFixRefs(
&entityTemplate, templateToCloneMap, &serializeContext);
&entityPrototype, prototypeToCloneMap, &serializeContext);
}
AZ::Entity* SpawnableEntitiesManager::CloneSingleAliasedEntity(
const AZ::Entity& entityPrototype,
const Spawnable::EntityAlias& alias,
EntityIdMap& prototypeToCloneMap,
AZ::Entity* previouslySpawnedEntity,
AZ::SerializeContext& serializeContext)
{
AZ::Entity* clone = nullptr;
switch (alias.m_aliasType)
{
case Spawnable::EntityAliasType::Original:
// Behave as the original version.
clone = CloneSingleEntity(entityPrototype, prototypeToCloneMap, serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
return clone;
case Spawnable::EntityAliasType::Disable:
// Do nothing.
return nullptr;
case Spawnable::EntityAliasType::Replace:
clone = CloneSingleEntity(*(alias.m_spawnable->GetEntities()[alias.m_targetIndex]), prototypeToCloneMap, serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
return clone;
case Spawnable::EntityAliasType::Additional:
// The asset handler will have sorted and inserted a Spawnable::EntityAliasType::Original, so the just
// spawn the additional entity.
clone = CloneSingleEntity(*(alias.m_spawnable->GetEntities()[alias.m_targetIndex]), prototypeToCloneMap, serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
return clone;
case Spawnable::EntityAliasType::Merge:
AZ_Assert(previouslySpawnedEntity != nullptr, "Merging components but there's no entity to add to yet.");
AppendComponents(
*previouslySpawnedEntity, alias.m_spawnable->GetEntities()[alias.m_targetIndex]->GetComponents(), prototypeToCloneMap,
serializeContext);
return nullptr;
default:
AZ_Assert(false, "Unsupported spawnable entity alias type: %i", alias.m_aliasType);
return nullptr;
}
}
void SpawnableEntitiesManager::AppendComponents(
AZ::Entity& target,
const AZ::Entity::ComponentArrayType& componentPrototypes,
EntityIdMap& prototypeToCloneMap,
AZ::SerializeContext& serializeContext)
{
// Only components are added and entities are looked up so no duplicate entity ids should be encountered.
constexpr bool allowDuplicateIds = false;
for (const AZ::Component* component : componentPrototypes)
{
AZ::Component* clone = AZ::IdUtils::Remapper<AZ::EntityId, allowDuplicateIds>::CloneObjectAndGenerateNewIdsAndFixRefs(
component, prototypeToCloneMap, &serializeContext);
AZ_Assert(clone, "Unable to clone component for entity '%s' (%zu).", target.GetName().c_str(), target.GetId());
[[maybe_unused]] bool result = target.AddComponent(clone);
AZ_Assert(result, "Unable to add cloned component to entity '%s' (%zu).", target.GetName().c_str(), target.GetId());
}
}
void SpawnableEntitiesManager::InitializeEntityIdMappings(
@ -316,161 +402,264 @@ namespace AzFramework
}
}
bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<size_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
if (Spawnable::EntityAliasConstVisitor aliases = ticket.m_spawnable->TryGetAliasesConst();
aliases.IsValid() && aliases.AreAllSpawnablesReady())
{
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<uint32_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
// Keep track how many entities there were in the array initially
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
// Keep track how many entities there were in the array initially
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
// These are 'template' entities we'll be cloning from
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = entitiesToSpawn.size();
// These are 'prototype' entities we'll be cloning from
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
uint32_t entitiesToSpawnSize = aznumeric_caster(entitiesToSpawn.size());
// Reserve buffers
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
// Reserve buffers
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
// Pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
// any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
// We clear out and regenerate the set of IDs on every SpawnAllEntities call, because presumably every entity reference
// in every entity we're about to instantiate is intended to point to an entity in our newly-instantiated batch, regardless
// of spawn order. If we didn't clear out the map, it would be possible for some entities here to have references to
// previously-spawned entities from a previous SpawnEntities or SpawnAllEntities call.
InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
// Pre-generate the full set of entity-id-to-new-entity-id mappings, so that during the clone operation below,
// any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
// We clear out and regenerate the set of IDs on every SpawnAllEntities call, because presumably every entity reference
// in every entity we're about to instantiate is intended to point to an entity in our newly-instantiated batch, regardless
// of spawn order. If we didn't clear out the map, it would be possible for some entities here to have references to
// previously-spawned entities from a previous SpawnEntities or SpawnAllEntities call.
InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(entitiesToSpawn[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
auto aliasIt = aliases.begin();
auto aliasEnd = aliases.end();
if (aliasIt == aliasEnd)
{
for (uint32_t i = 0; i < entitiesToSpawnSize; ++i)
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
spawnedEntities.emplace_back(
CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext));
spawnedEntityIndices.push_back(i);
}
}
else
{
for (uint32_t i = 0; i < entitiesToSpawnSize; ++i)
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
spawnedEntities.emplace_back(clone);
spawnedEntityIndices.push_back(i);
}
if (aliasIt == aliasEnd || aliasIt->m_sourceIndex != i)
{
spawnedEntities.emplace_back(
CloneSingleEntity(*entitiesToSpawn[i], ticket.m_entityIdReferenceMap, *request.m_serializeContext));
spawnedEntityIndices.push_back(i);
}
else
{
// The list of entities has already been sorted and optimized (See SpawnableEntitiesAliasList:Optimize) so can
// be safely executed in order without risking an invalid state.
AZ::Entity* previousEntity = nullptr;
do
{
AZ::Entity* clone = CloneSingleAliasedEntity(
*entitiesToSpawn[i], *aliasIt, ticket.m_entityIdReferenceMap, previousEntity,
*request.m_serializeContext);
previousEntity = clone;
if (clone)
{
spawnedEntities.emplace_back(clone);
spawnedEntityIndices.push_back(i);
}
++aliasIt;
} while (aliasIt != aliasEnd && aliasIt->m_sourceIndex == i);
}
}
}
// loadAll is true if every entity has been spawned only once
ticket.m_loadAll = (spawnedEntities.size() == entitiesToSpawnSize);
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
// There were no initial entities then the ticket now holds exactly all entities. If there were already entities then
// a new set are not added so it no longer holds exactly the number of entities.
ticket.m_loadAll = spawnedEntitiesInitialCount == 0;
// Add to the game context, now the entities are active
for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it)
{
(*it)->SetSpawnTicketId(request.m_ticketId);
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
}
auto newEntitiesBegin = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount;
auto newEntitiesEnd = ticket.m_spawnedEntities.end();
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView(newEntitiesBegin, newEntitiesEnd));
}
// Let other systems know about newly spawned entities for any post-processing after adding to the scene/game context.
if (request.m_completionCallback)
{
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
// Add to the game context, now the entities are active
for (auto it = newEntitiesBegin; it != newEntitiesEnd; ++it)
{
AZ::Entity* clone = (*it);
// The entity component framework doesn't handle entities without TransformComponent safely.
if (!clone->GetComponents().empty())
{
clone->SetSpawnTicketId(request.m_ticketId);
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
}
}
ticket.m_currentRequestId++;
return true;
}
else
{
return false;
// Let other systems know about newly spawned entities for any post-processing after adding to the scene/game context.
if (request.m_completionCallback)
{
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(newEntitiesBegin, newEntitiesEnd));
}
ticket.m_currentRequestId++;
return CommandResult::Executed;
}
}
return CommandResult::Requeue;
}
bool SpawnableEntitiesManager::ProcessRequest(SpawnEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(SpawnEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<size_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
AZ_Assert(
spawnedEntities.size() == spawnedEntityIndices.size(),
"The indices for the spawned entities has gone out of sync with the entities.");
if (Spawnable::EntityAliasConstVisitor aliases = ticket.m_spawnable->TryGetAliasesConst();
aliases.IsValid() && aliases.AreAllSpawnablesReady())
{
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<uint32_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
AZ_Assert(
spawnedEntities.size() == spawnedEntityIndices.size(),
"The indices for the spawned entities has gone out of sync with the entities.");
// Keep track of how many entities there were in the array initially
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
// Keep track of how many entities there were in the array initially
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
// These are 'template' entities we'll be cloning from
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = request.m_entityIndices.size();
// These are 'prototype' entities we'll be cloning from
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = request.m_entityIndices.size();
if (ticket.m_entityIdReferenceMap.empty() || !request.m_referencePreviouslySpawnedEntities)
{
// This map keeps track of ids from template (spawnable) to clone (instance) allowing patch ups of fields referring
// to entityIds outside of a given entity.
// We pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
// any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
// By default, we only initialize this map once because it needs to persist across multiple SpawnEntities calls, so
// that reference fixups work even when the entity being referenced is spawned in a different SpawnEntities
// (or SpawnAllEntities) call.
// However, the caller can also choose to reset the map by passing in "m_referencePreviouslySpawnedEntities = false".
InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
}
if (ticket.m_entityIdReferenceMap.empty() || !request.m_referencePreviouslySpawnedEntities)
{
// This map keeps track of ids from prototype (spawnable) to clone (instance) allowing patch ups of fields referring
// to entityIds outside of a given entity.
// We pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
// any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
// By default, we only initialize this map once because it needs to persist across multiple SpawnEntities calls, so
// that reference fixups work even when the entity being referenced is spawned in a different SpawnEntities
// (or SpawnAllEntities) call.
// However, the caller can also choose to reset the map by passing in "m_referencePreviouslySpawnedEntities = false".
InitializeEntityIdMappings(entitiesToSpawn, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
}
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
for (size_t index : request.m_entityIndices)
{
if (index < entitiesToSpawn.size())
auto aliasBegin = aliases.begin();
auto aliasEnd = aliases.end();
if (aliasBegin == aliasEnd)
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
AZ::Entity* clone =
CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
for (uint32_t index : request.m_entityIndices)
{
if (index < entitiesToSpawn.size())
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
spawnedEntities.push_back(
CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext));
spawnedEntityIndices.push_back(index);
}
}
}
else
{
for (uint32_t index : request.m_entityIndices)
{
if (index < entitiesToSpawn.size())
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(
entitiesToSpawn[index].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
auto aliasIt = AZStd::lower_bound(
aliasBegin, aliasEnd, index,
[](const Spawnable::EntityAlias& lhs, uint32_t rhs)
{
return lhs.m_sourceIndex < rhs;
});
if (aliasIt == aliasEnd || aliasIt->m_sourceIndex != index)
{
spawnedEntities.emplace_back(
CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext));
spawnedEntityIndices.push_back(index);
}
else
{
// The list of entities has already been sorted and optimized (See SpawnableEntitiesAliasList:Optimize) so
// can be safely executed in order without risking an invalid state.
AZ::Entity* previousEntity = nullptr;
do
{
AZ::Entity* clone = CloneSingleAliasedEntity(
*entitiesToSpawn[index], *aliasIt, ticket.m_entityIdReferenceMap, previousEntity,
*request.m_serializeContext);
previousEntity = clone;
if (clone)
{
spawnedEntities.emplace_back(clone);
spawnedEntityIndices.push_back(index);
}
++aliasIt;
} while (aliasIt != aliasEnd && aliasIt->m_sourceIndex == index);
}
}
}
}
ticket.m_loadAll = false;
spawnedEntities.push_back(clone);
spawnedEntityIndices.push_back(index);
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(
request.m_ticketId,
SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
}
ticket.m_loadAll = false;
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(request.m_ticketId, SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
// Add to the game context, now the entities are active
for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it)
{
AZ::Entity* clone = (*it);
// The entity component framework doesn't handle entities without TransformComponent safely.
if (!clone->GetComponents().empty())
{
clone->SetSpawnTicketId(request.m_ticketId);
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
}
}
// Add to the game context, now the entities are active
for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it)
{
(*it)->SetSpawnTicketId(request.m_ticketId);
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
}
if (request.m_completionCallback)
{
request.m_completionCallback(
request.m_ticketId,
SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
if (request.m_completionCallback)
{
request.m_completionCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
ticket.m_currentRequestId++;
return CommandResult::Executed;
}
ticket.m_currentRequestId++;
return true;
}
else
{
return false;
}
return CommandResult::Requeue;
}
bool SpawnableEntitiesManager::ProcessRequest(DespawnAllEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(DespawnAllEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@ -495,15 +684,15 @@ namespace AzFramework
}
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(DespawnEntityCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(DespawnEntityCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@ -529,15 +718,15 @@ namespace AzFramework
}
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(ReloadSpawnableCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(ReloadSpawnableCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
AZ_Assert(ticket.m_spawnable.GetId() == request.m_spawnable.GetId(),
@ -564,7 +753,7 @@ namespace AzFramework
// Pre-generate the full set of entity id to new entity id mappings, so that during the clone operation below,
// any entity references that point to a not-yet-cloned entity will still get their ids remapped correctly.
// This map is intentionally cleared out and regenerated here to ensure that we're starting fresh with mappings that
// match the new set of template entities getting spawned.
// match the new set of prototype entities getting spawned.
InitializeEntityIdMappings(entities, ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
if (ticket.m_loadAll)
@ -574,7 +763,7 @@ namespace AzFramework
ticket.m_spawnedEntityIndices.clear();
size_t entitiesToSpawnSize = entities.size();
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
for (uint32_t i = 0; i < entitiesToSpawnSize; ++i)
{
// If this entity has previously been spawned, give it a new id in the reference map
RefreshEntityIdMapping(entities[i].get()->GetId(), ticket.m_entityIdReferenceMap, ticket.m_previouslySpawned);
@ -590,7 +779,7 @@ namespace AzFramework
{
size_t entitiesSize = entities.size();
for (size_t index : ticket.m_spawnedEntityIndices)
for (uint32_t index : ticket.m_spawnedEntityIndices)
{
// It's possible for the new spawnable to have a different number of entities, so guard against this.
// It's also possible that the entities have moved within the spawnable to a new index. This can't be
@ -616,15 +805,40 @@ namespace AzFramework
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
auto SpawnableEntitiesManager::ProcessRequest(UpdateEntityAliasTypesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
if (Spawnable::EntityAliasVisitor aliases = ticket.m_spawnable->TryGetAliases(); aliases.IsValid())
{
for (EntityAliasTypeChange& replacement : request.m_entityAliases)
{
aliases.UpdateAliasType(replacement.m_aliasIndex, replacement.m_newAliasType);
}
aliases.Optimize();
if (request.m_completionCallback)
{
request.m_completionCallback(request.m_ticketId);
}
ticket.m_currentRequestId++;
return CommandResult::Executed;
}
}
return CommandResult::Requeue;
}
bool SpawnableEntitiesManager::ProcessRequest(ListEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(ListEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@ -632,15 +846,15 @@ namespace AzFramework
request.m_listCallback(request.m_ticketId, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntities.end()));
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(ListIndicesEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(ListIndicesEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@ -651,15 +865,15 @@ namespace AzFramework
request.m_listCallback(request.m_ticketId, SpawnableConstIndexEntityContainerView(
ticket.m_spawnedEntities.begin(), ticket.m_spawnedEntityIndices.begin(), ticket.m_spawnedEntities.size()));
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(ClaimEntitiesCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(ClaimEntitiesCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@ -671,15 +885,15 @@ namespace AzFramework
ticket.m_spawnedEntityIndices.clear();
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(BarrierCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(BarrierCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@ -690,15 +904,39 @@ namespace AzFramework
}
ticket.m_currentRequestId++;
return true;
return CommandResult::Executed;
}
else
{
return CommandResult::Requeue;
}
}
auto SpawnableEntitiesManager::ProcessRequest(LoadBarrierCommand& request) -> CommandResult
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
if (request.m_checkAliasSpawnables)
{
if (Spawnable::EntityAliasConstVisitor visitor = ticket.m_spawnable->TryGetAliasesConst();
!visitor.IsValid() || !visitor.AreAllSpawnablesReady())
{
return CommandResult::Requeue;
}
}
request.m_completionCallback(request.m_ticketId);
ticket.m_currentRequestId++;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
bool SpawnableEntitiesManager::ProcessRequest(DestroyTicketCommand& request)
auto SpawnableEntitiesManager::ProcessRequest(DestroyTicketCommand& request) -> CommandResult
{
if (request.m_requestId == request.m_ticket->m_currentRequestId)
{
@ -706,19 +944,24 @@ namespace AzFramework
{
if (entity != nullptr)
{
// Setting it to 0 is needed to avoid the infite loop between GameEntityContext and SpawnableEntitiesManager.
// Setting it to 0 is needed to avoid the infinite loop between GameEntityContext and SpawnableEntitiesManager.
entity->SetSpawnTicketId(0);
GameEntityContextRequestBus::Broadcast(
&GameEntityContextRequestBus::Events::DestroyGameEntity, entity->GetId());
}
else
{
// Entities without components wouldn't have been send to the GameEntityContext.
delete entity;
}
}
delete request.m_ticket;
return true;
return CommandResult::Executed;
}
else
{
return false;
return CommandResult::Requeue;
}
}
} // namespace AzFramework

@ -55,13 +55,18 @@ namespace AzFramework
void SpawnAllEntities(EntitySpawnTicket& ticket, SpawnAllEntitiesOptionalArgs optionalArgs = {}) override;
void SpawnEntities(
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) override;
EntitySpawnTicket& ticket, AZStd::vector<uint32_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) override;
void DespawnAllEntities(EntitySpawnTicket& ticket, DespawnAllEntitiesOptionalArgs optionalArgs = {}) override;
void DespawnEntity(AZ::EntityId entityId, EntitySpawnTicket& ticket, DespawnEntityOptionalArgs optionalArgs = {}) override;
void RetrieveEntitySpawnTicket(EntitySpawnTicket::Id entitySpawnTicketId, RetrieveEntitySpawnTicketCallback callback) override;
void ReloadSpawnable(
EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable, ReloadSpawnableOptionalArgs optionalArgs = {}) override;
void UpdateEntityAliasTypes(
EntitySpawnTicket& ticket,
AZStd::vector<EntityAliasTypeChange> updatedAliases,
UpdateEntityAliasTypesOptionalArgs optionalArgs = {}) override;
void ListEntities(
EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs = {}) override;
void ListIndicesAndEntities(
@ -70,6 +75,8 @@ namespace AzFramework
EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback, ClaimEntitiesOptionalArgs optionalArgs = {}) override;
void Barrier(EntitySpawnTicket& spawnInfo, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs = {}) override;
void LoadBarrier(
EntitySpawnTicket& spawnInfo, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs = {}) override;
//
// The following function is thread safe but intended to be run from the main thread.
@ -78,14 +85,20 @@ namespace AzFramework
CommandQueueStatus ProcessQueue(CommandQueuePriority priority);
protected:
struct Ticket
enum class CommandResult : bool
{
Executed,
Requeue
};
struct Ticket final
{
AZ_CLASS_ALLOCATOR(Ticket, AZ::ThreadPoolAllocator, 0);
static constexpr uint32_t Processing = AZStd::numeric_limits<uint32_t>::max();
//! Map of template entity ids to their associated instance ids.
//! Tickets can be used to spawn the same template entities multiple times, in any order, across multiple calls.
//! Since template entities can reference other entities, this map is used to fix up those references across calls
//! Map of prototype entity ids to their associated instance ids.
//! Tickets can be used to spawn the same prototype entities multiple times, in any order, across multiple calls.
//! Since prototype entities can reference other entities, this map is used to fix up those references across calls
//! using the following policy:
//! - Entities referencing an entity that hasn't been spawned yet will get a reference to the id that *will* be used
//! the first time that entity will be spawned. The reference will be invalid until that entity is spawned, but
@ -100,14 +113,14 @@ namespace AzFramework
AZStd::unordered_set<AZ::EntityId> m_previouslySpawned;
AZStd::vector<AZ::Entity*> m_spawnedEntities;
AZStd::vector<size_t> m_spawnedEntityIndices;
AZStd::vector<uint32_t> m_spawnedEntityIndices;
AZ::Data::Asset<Spawnable> m_spawnable;
uint32_t m_nextRequestId{ 0 }; //!< Next id for this ticket.
uint32_t m_currentRequestId { 0 }; //!< The id for the command that should be executed.
bool m_loadAll{ true };
};
struct SpawnAllEntitiesCommand
struct SpawnAllEntitiesCommand final
{
EntitySpawnCallback m_completionCallback;
EntityPreInsertionCallback m_preInsertionCallback;
@ -116,9 +129,9 @@ namespace AzFramework
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct SpawnEntitiesCommand
struct SpawnEntitiesCommand final
{
AZStd::vector<size_t> m_entityIndices;
AZStd::vector<uint32_t> m_entityIndices;
EntitySpawnCallback m_completionCallback;
EntityPreInsertionCallback m_preInsertionCallback;
AZ::SerializeContext* m_serializeContext;
@ -127,7 +140,7 @@ namespace AzFramework
uint32_t m_requestId;
bool m_referencePreviouslySpawnedEntities;
};
struct DespawnAllEntitiesCommand
struct DespawnAllEntitiesCommand final
{
EntityDespawnCallback m_completionCallback;
Ticket* m_ticket;
@ -142,7 +155,7 @@ namespace AzFramework
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ReloadSpawnableCommand
struct ReloadSpawnableCommand final
{
AZ::Data::Asset<Spawnable> m_spawnable;
ReloadSpawnableCallback m_completionCallback;
@ -151,35 +164,51 @@ namespace AzFramework
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ListEntitiesCommand
struct UpdateEntityAliasTypesCommand final
{
AZStd::vector<EntityAliasTypeChange> m_entityAliases;
UpdateEntityAliasTypesCallback m_completionCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ListEntitiesCommand final
{
ListEntitiesCallback m_listCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ListIndicesEntitiesCommand
struct ListIndicesEntitiesCommand final
{
ListIndicesEntitiesCallback m_listCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct ClaimEntitiesCommand
struct ClaimEntitiesCommand final
{
ClaimEntitiesCallback m_listCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct BarrierCommand
struct BarrierCommand final
{
BarrierCallback m_completionCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
};
struct LoadBarrierCommand final
{
BarrierCallback m_completionCallback;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
bool m_checkAliasSpawnables;
};
struct DestroyTicketCommand
struct DestroyTicketCommand final
{
Ticket* m_ticket;
uint32_t m_requestId;
@ -191,10 +220,12 @@ namespace AzFramework
DespawnAllEntitiesCommand,
DespawnEntityCommand,
ReloadSpawnableCommand,
UpdateEntityAliasTypesCommand,
ListEntitiesCommand,
ListIndicesEntitiesCommand,
ClaimEntitiesCommand,
BarrierCommand,
LoadBarrierCommand,
DestroyTicketCommand>;
struct Queue
@ -212,18 +243,31 @@ namespace AzFramework
CommandQueueStatus ProcessQueue(Queue& queue);
AZ::Entity* CloneSingleEntity(
const AZ::Entity& entityTemplate, EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext);
const AZ::Entity& entityPrototype, EntityIdMap& prototypeToCloneMap, AZ::SerializeContext& serializeContext);
AZ::Entity* CloneSingleAliasedEntity(
const AZ::Entity& entityPrototype,
const Spawnable::EntityAlias& alias,
EntityIdMap& prototypeToCloneMap,
AZ::Entity* previouslySpawnedEntity,
AZ::SerializeContext& serializeContext);
void AppendComponents(
AZ::Entity& target,
const AZ::Entity::ComponentArrayType& componentPrototypes,
EntityIdMap& prototypeToCloneMap,
AZ::SerializeContext& serializeContext);
bool ProcessRequest(SpawnAllEntitiesCommand& request);
bool ProcessRequest(SpawnEntitiesCommand& request);
bool ProcessRequest(DespawnAllEntitiesCommand& request);
bool ProcessRequest(DespawnEntityCommand& request);
bool ProcessRequest(ReloadSpawnableCommand& request);
bool ProcessRequest(ListEntitiesCommand& request);
bool ProcessRequest(ListIndicesEntitiesCommand& request);
bool ProcessRequest(ClaimEntitiesCommand& request);
bool ProcessRequest(BarrierCommand& request);
bool ProcessRequest(DestroyTicketCommand& request);
CommandResult ProcessRequest(SpawnAllEntitiesCommand& request);
CommandResult ProcessRequest(SpawnEntitiesCommand& request);
CommandResult ProcessRequest(DespawnAllEntitiesCommand& request);
CommandResult ProcessRequest(DespawnEntityCommand& request);
CommandResult ProcessRequest(ReloadSpawnableCommand& request);
CommandResult ProcessRequest(UpdateEntityAliasTypesCommand& request);
CommandResult ProcessRequest(ListEntitiesCommand& request);
CommandResult ProcessRequest(ListIndicesEntitiesCommand& request);
CommandResult ProcessRequest(ClaimEntitiesCommand& request);
CommandResult ProcessRequest(BarrierCommand& request);
CommandResult ProcessRequest(LoadBarrierCommand& request);
CommandResult ProcessRequest(DestroyTicketCommand& request);
//! Generate a base set of original-to-new entity ID mappings to use during spawning.
//! Since Entity references get fixed up on an entity-by-entity basis while spawning, it's important to have the complete

@ -72,7 +72,7 @@ namespace AzFramework
uint64_t SpawnableSystemComponent::AssignRootSpawnable(AZ::Data::Asset<Spawnable> rootSpawnable)
{
uint64_t generation = 0;
uint32_t generation = 0;
if (m_rootSpawnableId == rootSpawnable.GetId())
{
@ -87,16 +87,25 @@ namespace AzFramework
// Suspend and resume processing in the container that completion calls aren't received until
// everything has been setup to accept callbacks from the call.
m_rootSpawnableContainer.Reset(rootSpawnable);
m_rootSpawnableContainer.SpawnAllEntities();
generation = m_rootSpawnableContainer.GetCurrentGeneration();
AZ_TracePrintf("Spawnables", "Root spawnable set to '%s' at generation %zu.\n", rootSpawnable.GetHint().c_str(),
generation);
// Don't send out the alert that the root spawnable has been assigned until the spawnable itself is ready. The common
// use case is for handlers to do something with the information in the spawnable before the entities get spawned.
m_rootSpawnableContainer.Alert(
[rootSpawnable](uint32_t generation)
{
RootSpawnableNotificationBus::Broadcast(
&RootSpawnableNotificationBus::Events::OnRootSpawnableAssigned, AZStd::move(rootSpawnable), generation);
}, SpawnableEntitiesContainer::CheckIfSpawnableIsLoaded::Yes);
m_rootSpawnableContainer.SpawnAllEntities();
m_rootSpawnableContainer.Alert(
[newSpawnable = AZStd::move(rootSpawnable)](uint32_t generation)
{
RootSpawnableNotificationBus::QueueBroadcast(
&RootSpawnableNotificationBus::Events::OnRootSpawnableAssigned, newSpawnable, generation);
&RootSpawnableNotificationBus::Events::OnRootSpawnableReady, AZStd::move(newSpawnable), generation);
});
AZ_TracePrintf("Spawnables", "Root spawnable set to '%s' at generation %zu.\n", rootSpawnable.GetHint().c_str(), generation);
}
else
{
@ -132,6 +141,12 @@ namespace AzFramework
AZ_TracePrintf("Spawnables", "New root spawnable '%s' assigned (generation: %i).\n", rootSpawnable.GetHint().c_str(), generation);
}
void SpawnableSystemComponent::OnRootSpawnableReady(
[[maybe_unused]] AZ::Data::Asset<Spawnable> rootSpawnable, [[maybe_unused]] uint32_t generation)
{
AZ_TracePrintf("Spawnables", "Entities from new root spawnable '%s' are ready (generation: %i).\n", rootSpawnable.GetHint().c_str(), generation);
}
void SpawnableSystemComponent::OnRootSpawnableReleased([[maybe_unused]] uint32_t generation)
{
AZ_TracePrintf("Spawnables", "Generation %i of the root spawnable has been released.\n", generation);

@ -82,6 +82,7 @@ namespace AzFramework
//
void OnRootSpawnableAssigned(AZ::Data::Asset<Spawnable> rootSpawnable, uint32_t generation) override;
void OnRootSpawnableReady(AZ::Data::Asset<Spawnable> rootSpawnable, uint32_t generation) override;
void OnRootSpawnableReleased(uint32_t generation) override;
protected:

@ -806,12 +806,20 @@ namespace AzFramework
[[maybe_unused]] float scrollDelta,
[[maybe_unused]] float deltaTime)
{
const auto pivot = m_pivotFn();
if (!pivot.has_value())
{
EndActivation();
return targetCamera;
}
if (Beginning())
{
// as the camera starts, record the camera we would like to end up as
m_nextCamera.m_offset = m_offsetFn(m_pivotFn().GetDistance(targetCamera.Translation()));
m_nextCamera.m_offset = m_offsetFn(pivot.value().GetDistance(targetCamera.Translation()));
const auto angles =
EulerAngles(AZ::Matrix3x3::CreateFromMatrix3x4(AZ::Matrix3x4::CreateLookAt(targetCamera.Translation(), m_pivotFn())));
EulerAngles(AZ::Matrix3x3::CreateFromMatrix3x4(AZ::Matrix3x4::CreateLookAt(targetCamera.Translation(), pivot.value())));
m_nextCamera.m_pitch = angles.GetX();
m_nextCamera.m_yaw = angles.GetZ();
m_nextCamera.m_pivot = targetCamera.m_pivot;

@ -651,7 +651,7 @@ namespace AzFramework
class FocusCameraInput : public CameraInput
{
public:
using PivotFn = AZStd::function<AZ::Vector3()>;
using PivotFn = AZStd::function<AZStd::optional<AZ::Vector3>()>;
FocusCameraInput(const InputChannelId& focusChannelId, FocusOffsetFn offsetFn);

@ -286,6 +286,7 @@ set(FILES
Spawnable/RootSpawnableInterface.h
Spawnable/Spawnable.cpp
Spawnable/Spawnable.h
Spawnable/SpawnableAssetBus.h
Spawnable/SpawnableAssetHandler.h
Spawnable/SpawnableAssetHandler.cpp
Spawnable/SpawnableEntitiesContainer.h

@ -11,6 +11,7 @@
#include <AzCore/Module/DynamicModuleHandle.h>
#include <AzCore/PlatformIncl.h>
#include <AzCore/std/containers/array.h>
#include <AzCore/std/string/conversions.h>
namespace AzFramework
@ -234,14 +235,14 @@ namespace AzFramework
const UINT rawInputHeaderSize = sizeof(RAWINPUTHEADER);
GetRawInputData((HRAWINPUT)lParam, RID_INPUT, NULL, &rawInputSize, rawInputHeaderSize);
LPBYTE rawInputBytes = new BYTE[rawInputSize];
AZStd::array<BYTE, sizeof(RAWINPUT)> rawInputBytesArray;
LPBYTE rawInputBytes = rawInputBytesArray.data();
GetRawInputData((HRAWINPUT)lParam, RID_INPUT, rawInputBytes, &rawInputSize, rawInputHeaderSize);
RAWINPUT* rawInput = (RAWINPUT*)rawInputBytes;
AzFramework::RawInputNotificationBusWindows::Broadcast(
&AzFramework::RawInputNotificationBusWindows::Events::OnRawInputEvent, *rawInput);
delete [] rawInputBytes;
break;
}
case WM_CHAR:

@ -48,6 +48,24 @@ namespace InputUnitTests
AZStd::unique_ptr<InputSystemComponent> m_inputSystemComponent;
};
////////////////////////////////////////////////////////////////////////////////////////////////
TEST_F(InputTest, InputChannelId_ConstExpression_CopyConstructorSuccessfull)
{
constexpr InputChannelId testInputChannelId1("TestInputChannelId");
constexpr InputChannelId testInputChannelId2(testInputChannelId1);
static_assert(testInputChannelId1 == testInputChannelId2);
EXPECT_EQ(testInputChannelId1, testInputChannelId2);
}
////////////////////////////////////////////////////////////////////////////////////////////////
TEST_F(InputTest, InputDeviceId_ConstExpression_CopyConstructorSuccessfull)
{
constexpr InputDeviceId testInputDeviceId1("TestInputDeviceId");
constexpr InputDeviceId testInputDeviceId2(testInputDeviceId1);
static_assert(testInputDeviceId1 == testInputDeviceId2);
EXPECT_EQ(testInputDeviceId1, testInputDeviceId2);
}
////////////////////////////////////////////////////////////////////////////////////////////////
TEST_F(InputTest, InputContext_InitWithDataStruct_InitializationSuccessfull)
{

@ -36,7 +36,7 @@ namespace AzFramework
MOCK_METHOD3(
SpawnEntities,
void(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs));
void(EntitySpawnTicket& ticket, AZStd::vector<uint32_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs));
MOCK_METHOD2(DespawnAllEntities, void(EntitySpawnTicket& ticket, DespawnAllEntitiesOptionalArgs optionalArgs));
@ -49,6 +49,13 @@ namespace AzFramework
ReloadSpawnable,
void(EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable, ReloadSpawnableOptionalArgs optionalArgs));
MOCK_METHOD3(
UpdateEntityAliasTypes,
void(
EntitySpawnTicket& ticket,
AZStd::vector<EntityAliasTypeChange> updatedAliases,
UpdateEntityAliasTypesOptionalArgs optionalArgs));
MOCK_METHOD3(
ListEntities, void(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs));
@ -61,6 +68,7 @@ namespace AzFramework
void(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback, ClaimEntitiesOptionalArgs optionalArgs));
MOCK_METHOD3(Barrier, void(EntitySpawnTicket& ticket, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs));
MOCK_METHOD3(LoadBarrier, void(EntitySpawnTicket& ticket, BarrierCallback completionCallback, LoadBarrierOptionalArgs optionalArgs));
MOCK_METHOD1(CreateTicket, AZStd::pair<EntitySpawnTicket::Id, void*>(AZ::Data::Asset<Spawnable>&& spawnable));
MOCK_METHOD1(DestroyTicket, void(void* ticket));

@ -55,6 +55,40 @@ namespace UnitTest
AZ::EntityId m_entityReference;
};
class SourceSpawnableComponent : public AZ::Component
{
public:
AZ_COMPONENT(SourceSpawnableComponent, "{47FF79CE-A95B-420E-8BEB-F1CC58087B87}");
void Activate() override {}
void Deactivate() override {}
static void Reflect(AZ::ReflectContext* reflection)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection))
{
serializeContext->Class<SourceSpawnableComponent, AZ::Component>();
}
}
};
class TargetSpawnableComponent : public AZ::Component
{
public:
AZ_COMPONENT(TargetSpawnableComponent, "{B4041561-63A7-4E1E-80F1-78C08D497960}");
void Activate() override {}
void Deactivate() override {}
static void Reflect(AZ::ReflectContext* reflection)
{
if (auto* serializeContext = azrtti_cast<AZ::SerializeContext*>(reflection))
{
serializeContext->Class<TargetSpawnableComponent, AZ::Component>();
}
}
};
class SpawnableEntitiesManagerTest : public AllocatorsFixture
{
public:
@ -66,6 +100,8 @@ namespace UnitTest
AZ::ComponentApplication::Descriptor descriptor;
m_application->Start(descriptor);
m_application->RegisterComponentDescriptor(ComponentWithEntityReference::CreateDescriptor());
m_application->RegisterComponentDescriptor(SourceSpawnableComponent::CreateDescriptor());
m_application->RegisterComponentDescriptor(TargetSpawnableComponent::CreateDescriptor());
// Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is
// shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash
@ -109,10 +145,126 @@ namespace UnitTest
entities.reserve(numElements);
for (size_t i=0; i<numElements; ++i)
{
entities.push_back(AZStd::make_unique<AZ::Entity>());
auto entry = AZStd::make_unique<AZ::Entity>();
entry->AddComponent(aznew SourceSpawnableComponent());
entities.push_back(AZStd::move(entry));
}
}
AZ::Data::Asset<AzFramework::Spawnable> CreateTargetSpawnable(size_t numElements)
{
auto target = aznew AzFramework::Spawnable(
AZ::Data::AssetId(AZ::Uuid("{716CD8C3-0BA8-4F32-B579-0EC7C967796F}")), AZ::Data::AssetData::AssetStatus::Ready);
AzFramework::Spawnable::EntityList& entities = target->GetEntities();
entities.reserve(numElements);
for (size_t i = 0; i < numElements; ++i)
{
auto entry = AZStd::make_unique<AZ::Entity>();
entry->AddComponent(aznew TargetSpawnableComponent());
entities.push_back(AZStd::move(entry));
}
return AZ::Data::Asset<AzFramework::Spawnable>(target, AZ::Data::AssetLoadBehavior::NoLoad);
}
template<size_t AliasCount>
void InsertEntityAliases(
const AZStd::array<uint32_t, AliasCount>& sourceIds,
const AZStd::array<uint32_t, AliasCount>& targetIds,
const AZStd::array<AzFramework::Spawnable::EntityAliasType, AliasCount>& aliasTypes,
AZ::Data::Asset<AzFramework::Spawnable>* target = nullptr)
{
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
for (uint32_t i = 0; i < AliasCount; ++i)
{
if (target)
{
visitor.AddAlias(*target, AZ::Crc32(i), sourceIds[i], targetIds[i], aliasTypes[i], false);
}
else
{
AZ::Data::Asset<AzFramework::Spawnable> spawnable(
AZ::Data::AssetId(AZ::Uuid("{4CBEC17A-52D6-42D5-9037-F4C05B9CE1D9}"), i), azrtti_typeid<AzFramework::Spawnable>());
visitor.AddAlias(AZStd::move(spawnable), AZ::Crc32(i), sourceIds[i], targetIds[i], aliasTypes[i], false);
}
}
}
static bool AreAllEntitiesReplaced(AzFramework::SpawnableConstEntityContainerView entities)
{
for (const AZ::Entity* entity : entities)
{
if (entity)
{
if (entity->FindComponent<SourceSpawnableComponent>() != nullptr ||
entity->FindComponent<TargetSpawnableComponent>() == nullptr)
{
return false;
}
}
else
{
return false;
}
}
return true;
}
static bool IsEveryOtherEntityAReplacement(AzFramework::SpawnableConstEntityContainerView entities)
{
bool onAlternative = true;
for (const AZ::Entity* entity : entities)
{
if (entity)
{
if (onAlternative)
{
if (entity->FindComponent<SourceSpawnableComponent>() == nullptr ||
entity->FindComponent<TargetSpawnableComponent>() != nullptr)
{
return false;
}
}
else
{
if (entity->FindComponent<SourceSpawnableComponent>() != nullptr ||
entity->FindComponent<TargetSpawnableComponent>() == nullptr)
{
return false;
}
}
onAlternative = !onAlternative;
}
else
{
return false;
}
}
return true;
}
static bool AreAllMerged(AzFramework::SpawnableConstEntityContainerView entities)
{
for (const AZ::Entity* entity : entities)
{
if (entity)
{
if (entity->FindComponent<SourceSpawnableComponent>() == nullptr ||
entity->FindComponent<TargetSpawnableComponent>() == nullptr)
{
return false;
}
}
else
{
return false;
}
}
return true;
}
void CreateRecursiveHierarchy()
{
AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities();
@ -245,6 +397,30 @@ namespace UnitTest
TestApplication* m_application { nullptr };
};
//
// Constructors
//
TEST_F(SpawnableEntitiesManagerTest, EntitySpawnTicket_Move_Works)
{
AzFramework::EntitySpawnTicket ticket1(*m_spawnableAsset);
AzFramework::EntitySpawnTicket ticket2(*m_spawnableAsset);
const AzFramework::EntitySpawnTicket::Id ticket1Id = ticket1.GetId();
const AzFramework::EntitySpawnTicket::Id ticket2Id = ticket2.GetId();
AzFramework::EntitySpawnTicket ticketMoveConstructor(AZStd::move(ticket1));
EXPECT_TRUE(ticketMoveConstructor.IsValid());
EXPECT_EQ(ticketMoveConstructor.GetId(), ticket1Id);
AzFramework::EntitySpawnTicket ticketMoveOperator;
ticketMoveOperator = AZStd::move(ticket2);
EXPECT_TRUE(ticketMoveOperator.IsValid());
EXPECT_EQ(ticketMoveOperator.GetId(), ticket2Id);
}
//
// SpawnAllEntitities
//
@ -366,33 +542,144 @@ namespace UnitTest
}
}
TEST_F(SpawnableEntitiesManagerTest, EntitySpawnTicket_Move_Works)
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_DeleteTicketBeforeCall_NoCrash)
{
AzFramework::EntitySpawnTicket ticket1(*m_spawnableAsset);
AzFramework::EntitySpawnTicket ticket2(*m_spawnableAsset);
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->SpawnAllEntities(ticket);
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
const AzFramework::EntitySpawnTicket::Id ticket1Id = ticket1.GetId();
const AzFramework::EntitySpawnTicket::Id ticket2Id = ticket2.GetId();
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithDisabled_NoEntitiesSpawned)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
InsertEntityAliases<NumEntities>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable,
Spawnable::EntityAliasType::Disable });
AzFramework::EntitySpawnTicket ticketMoveConstructor(AZStd::move(ticket1));
EXPECT_TRUE(ticketMoveConstructor.IsValid());
EXPECT_EQ(ticketMoveConstructor.GetId(), ticket1Id);
size_t spawnedEntitiesCount = 0;
auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
};
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
AzFramework::EntitySpawnTicket ticketMoveOperator;
ticketMoveOperator = AZStd::move(ticket2);
EXPECT_TRUE(ticketMoveOperator.IsValid());
EXPECT_EQ(ticketMoveOperator.GetId(), ticket2Id);
EXPECT_EQ(0, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_DeleteTicketBeforeCall_NoCrash)
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_SomeAliasesWithDisabled_RegularEntitiesAreSpawned)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 8;
FillSpawnable(NumEntities);
InsertEntityAliases<2>({ 1, 3 }, { 1, 3 }, { Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable });
size_t spawnedEntitiesCount = 0;
auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->SpawnAllEntities(ticket);
}
spawnedEntitiesCount += entities.size();
};
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(6, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithReplace_EntitiesSpawnedFromTarget)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZ::Data::Asset<Spawnable> target = CreateTargetSpawnable(4);
InsertEntityAliases<4>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace },
&target);
size_t spawnedEntitiesCount = 0;
bool allReplaced = false;
auto callback = [&spawnedEntitiesCount, &allReplaced](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allReplaced = AreAllEntitiesReplaced(entities);
};
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(4, spawnedEntitiesCount);
EXPECT_TRUE(allReplaced);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithAdditional_SourceAndTargetComponentsMerged)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZ::Data::Asset<Spawnable> target = CreateTargetSpawnable(4);
InsertEntityAliases<4>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional,
Spawnable::EntityAliasType::Additional },
&target);
size_t spawnedEntitiesCount = 0;
bool allAdded = false;
auto callback = [&spawnedEntitiesCount, &allAdded](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allAdded = IsEveryOtherEntityAReplacement(entities);
};
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(8, spawnedEntitiesCount);
EXPECT_TRUE(allAdded);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithMerge_SourceAndTargetComponentsMerged)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZ::Data::Asset<Spawnable> target = CreateTargetSpawnable(4);
InsertEntityAliases<4>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge,
Spawnable::EntityAliasType::Merge },
&target);
size_t spawnedEntitiesCount = 0;
bool allMerged = false;
auto callback = [&spawnedEntitiesCount, &allMerged](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allMerged = AreAllMerged(entities);
};
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(4, spawnedEntitiesCount);
EXPECT_TRUE(allMerged);
}
//
// SpawnEntities
@ -403,7 +690,7 @@ namespace UnitTest
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZStd::vector<size_t> indices = { 0, 2, 3, 1 };
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
@ -423,7 +710,7 @@ namespace UnitTest
static constexpr size_t NumEntities = 1;
FillSpawnable(NumEntities);
AZStd::vector<size_t> indices = { 0, 0 };
AZStd::vector<uint32_t> indices = { 0, 0 };
size_t spawnedEntitiesCount = 0;
auto callback =
@ -444,7 +731,7 @@ namespace UnitTest
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZStd::vector<size_t> indices = { 0, 2, 3, 1 };
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
auto callback =
@ -467,7 +754,7 @@ namespace UnitTest
FillSpawnable(NumEntities);
CreateSingleParent();
AZStd::vector<size_t> indices = { 0, 1, 2, 3 };
AZStd::vector<uint32_t> indices = { 0, 1, 2, 3 };
AZStd::vector<AZ::EntityId> parents;
auto callback = [&parents](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
@ -499,7 +786,7 @@ namespace UnitTest
FillSpawnable(NumEntities);
CreateSingleParent();
AZStd::vector<size_t> indices = { 0, 1, 2, 3 };
AZStd::vector<uint32_t> indices = { 0, 1, 2, 3 };
AZStd::vector<AZ::EntityId> parents;
auto callback =
@ -754,6 +1041,148 @@ namespace UnitTest
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithDisabled_NoEntitiesSpawned)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
InsertEntityAliases<NumEntities>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable,
Spawnable::EntityAliasType::Disable });
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(0, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_SomeAliasesWithDisabled_RegularEntitiesAreSpawned)
{
using namespace AzFramework;
FillSpawnable(8);
InsertEntityAliases<3>(
{ 1, 3, 6 }, { 1, 3, 6 },
{ Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Disable });
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1, 2, 3, 0, 1, 6, 4, 5, 7, 4, 1, 0, 6 };
size_t spawnedEntitiesCount = 0;
auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(9, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithReplace_EntitiesSpawnedFromTarget)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZ::Data::Asset<Spawnable> target = CreateTargetSpawnable(4);
InsertEntityAliases<4>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace },
&target);
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
bool allReplaced = false;
auto callback = [&spawnedEntitiesCount, &allReplaced](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allReplaced = AreAllEntitiesReplaced(entities);
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(4, spawnedEntitiesCount);
EXPECT_TRUE(allReplaced);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithAdditional_SourceAndTargetComponentsMerged)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZ::Data::Asset<Spawnable> target = CreateTargetSpawnable(4);
InsertEntityAliases<4>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional,
Spawnable::EntityAliasType::Additional },
&target);
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
bool allAdded = false;
auto callback =
[&spawnedEntitiesCount, &allAdded](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allAdded = IsEveryOtherEntityAReplacement(entities);
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(8, spawnedEntitiesCount);
EXPECT_TRUE(allAdded);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithMerge_SourceAndTargetComponentsMerged)
{
using namespace AzFramework;
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZ::Data::Asset<Spawnable> target = CreateTargetSpawnable(4);
InsertEntityAliases<4>(
{ 0, 1, 2, 3 }, { 0, 1, 2, 3 },
{ Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge,
Spawnable::EntityAliasType::Merge },
&target);
AZStd::vector<uint32_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
bool allMerged = false;
auto callback = [&spawnedEntitiesCount, &allMerged](
AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
allMerged = AreAllMerged(entities);
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(4, spawnedEntitiesCount);
EXPECT_TRUE(allMerged);
}
//
// DespawnAllEntities

@ -0,0 +1,513 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Casting/numeric_cast.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzTest/AzTest.h>
namespace UnitTest
{
class SpawnableTest : public AllocatorsFixture
{
public:
static constexpr size_t DefaultEntityAliasTestCount = 8;
void SetUp() override
{
AllocatorsFixture::SetUp();
m_spawnable = aznew AzFramework::Spawnable();
}
void TearDown() override
{
delete m_spawnable;
m_spawnable = nullptr;
AllocatorsFixture::TearDown();
}
void InsertEntities(size_t count)
{
AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities();
entities.reserve(entities.size() + count);
for (size_t i = 0; i < count; ++i)
{
entities.emplace_back(AZStd::make_unique<AZ::Entity>());
}
}
template<size_t Count>
void InsertEntityAliases(
const AZStd::array<uint32_t, Count>& sourceIds,
const AZStd::array<uint32_t, Count>& targetIds,
const AZStd::array<AzFramework::Spawnable::EntityAliasType, Count>& aliasTypes,
bool queueLoad = false)
{
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
for (uint32_t i = 0; i < Count; ++i)
{
AZ::Data::Asset<AzFramework::Spawnable> spawnable(
AZ::Data::AssetId(AZ::Uuid("{4CBEC17A-52D6-42D5-9037-F4C05B9CE1D9}"), i), azrtti_typeid<AzFramework::Spawnable>());
visitor.AddAlias(spawnable, AZ::Crc32(i), sourceIds[i], targetIds[i], aliasTypes[i], queueLoad);
}
}
template<size_t Count>
void InsertEntityAliases(bool queueLoad)
{
using namespace AzFramework;
AZStd::array<uint32_t, Count> ids;
for (uint32_t i=0; i<aznumeric_cast<uint32_t>(Count); ++i)
{
ids[i] = i;
}
AZStd::array<AzFramework::Spawnable::EntityAliasType, Count> aliasTypes;
for (uint32_t i = 0; i < aznumeric_cast<uint32_t>(Count); ++i)
{
aliasTypes[i] = Spawnable::EntityAliasType::Replace;
}
InsertEntityAliases<Count>(ids, ids, aliasTypes, queueLoad);
}
template<size_t Count>
void InsertEntityAliases()
{
InsertEntityAliases<Count>(false);
}
protected:
AzFramework::Spawnable* m_spawnable;
};
//
// TryGetAliasesConst
//
TEST_F(SpawnableTest, TryGetAliasesConst_GetVisitor_VisitorDataIsAvailable)
{
AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst();
EXPECT_TRUE(visitor.IsValid());
}
TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsNotReadShared_VisitorDataIsNotAvailable)
{
AzFramework::Spawnable::EntityAliasVisitor readWriteVisitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(readWriteVisitor.IsValid());
AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst();
EXPECT_FALSE(visitor.IsValid());
}
TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsAlreadyReadShared_VisitorDataIsAvailable)
{
AzFramework::Spawnable::EntityAliasConstVisitor readVisitor = m_spawnable->TryGetAliasesConst();
ASSERT_TRUE(readVisitor.IsValid());
AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst();
EXPECT_TRUE(visitor.IsValid());
}
//
// TryGetAliases
//
TEST_F(SpawnableTest, TryGetAliases_GetVisitor_VisitorDataIsAvailable)
{
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
EXPECT_TRUE(visitor.IsValid());
}
TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsAlreadyShared_VisitorDataNotIsAvailable)
{
AzFramework::Spawnable::EntityAliasConstVisitor readVisitor = m_spawnable->TryGetAliasesConst();
ASSERT_TRUE(readVisitor.IsValid());
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
EXPECT_FALSE(visitor.IsValid());
}
//
// EntityAliasVisitor
//
//
// HasAliases
//
TEST_F(SpawnableTest, EntityAliasVisitor_HasAliases_EmptyAliasList_ReturnsFalse)
{
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_FALSE(visitor.HasAliases());
}
TEST_F(SpawnableTest, EntityAliasVisitor_HasAliases_FilledInAliasList_ReturnsTrue)
{
InsertEntities(8);
InsertEntityAliases<8>();
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_TRUE(visitor.HasAliases());
}
//
// Optimize
//
TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_SortEntityAliases_AliasesAreSortedBySourceAndTargetId)
{
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>();
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
// Optimize doesn't need to be explicitly called because the setup of the aliases will cause the alias list to be sorted and optimized.
uint32_t sourceIndex = 0;
uint32_t targetIndex = 0;
for (const AzFramework::Spawnable::EntityAlias& alias : visitor)
{
if (alias.m_sourceIndex != sourceIndex)
{
ASSERT_LE(sourceIndex, alias.m_sourceIndex);
}
else
{
ASSERT_LE(targetIndex, alias.m_targetIndex);
}
sourceIndex = alias.m_sourceIndex;
targetIndex = alias.m_targetIndex;
}
}
TEST_F(
SpawnableTest, EntityAliasVisitor_Optimize_RemoveUnused_OnlySecondToLastAliasRemains)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disable,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disable,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_EQ(1, AZStd::distance(visitor.begin(), visitor.end()));
EXPECT_EQ(Spawnable::EntityAliasType::Replace, visitor.begin()->m_aliasType);
EXPECT_EQ(6, visitor.begin()->m_targetIndex);
}
TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_AddAdditional_ThreeAdditionalAliasesAreAdded)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 0, 0, 0, 1, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional,
Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional,
Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_EQ(11, AZStd::distance(visitor.begin(), visitor.end()));
EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()->m_aliasType);
EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()[5].m_aliasType);
EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()[7].m_aliasType);
}
TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_OriginalsOnly_AliasListIsEmpty)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original,
Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original,
Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_EQ(0, AZStd::distance(visitor.begin(), visitor.end()));
}
TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_MixedOriginals_AllOriginalsRemoved)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 0, 0, 1, 1, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original,
Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disable, Spawnable::EntityAliasType::Original,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_EQ(2, AZStd::distance(visitor.begin(), visitor.end()));
EXPECT_EQ(Spawnable::EntityAliasType::Disable, visitor.begin()->m_aliasType);
EXPECT_EQ(Spawnable::EntityAliasType::Replace, visitor.begin()[1].m_aliasType);
}
TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_MergeAfterOriginal_NoAdditionalOriginalIsInserted)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 0, 1, 1, 2, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original,
Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_EQ(4, AZStd::distance(visitor.begin(), visitor.end()));
EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()->m_aliasType);
EXPECT_EQ(Spawnable::EntityAliasType::Merge, visitor.begin()[1].m_aliasType);
EXPECT_EQ(Spawnable::EntityAliasType::Replace, visitor.begin()[2].m_aliasType);
EXPECT_EQ(Spawnable::EntityAliasType::Merge, visitor.begin()[3].m_aliasType);
}
//
// UpdateAliasType
//
TEST_F(SpawnableTest, EntityAliasVisitor_UpdateAliasType_AllToOriginal_NoAliasesAfterOptimization)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
for (uint32_t i = 0; i < 8; ++i)
{
visitor.UpdateAliasType(i, Spawnable::EntityAliasType::Original);
}
for (const Spawnable::EntityAlias& alias : visitor)
{
EXPECT_EQ(Spawnable::EntityAliasType::Original, alias.m_aliasType);
}
visitor.Optimize();
EXPECT_EQ(0, AZStd::distance(visitor.begin(), visitor.end()));
}
//
// UpdateAliases
//
TEST_F(SpawnableTest, EntityAliasVisitor_UpdateAliases_AllToOriginal_NoAliasesAfterOptimization)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
auto callback =
[](Spawnable::EntityAliasType& aliasType, bool& /*queueLoad*/, const AZ::Data::Asset<Spawnable>& /*aliasedSpawnable*/,
const AZ::Crc32 /*tag*/, const uint32_t /*sourceIndex*/, const uint32_t /*targetIndex*/)
{
aliasType = Spawnable::EntityAliasType::Original;
};
visitor.UpdateAliases(AZStd::move(callback));
for (const Spawnable::EntityAlias& alias : visitor)
{
EXPECT_EQ(Spawnable::EntityAliasType::Original, alias.m_aliasType);
}
visitor.Optimize();
EXPECT_EQ(0, AZStd::distance(visitor.begin(), visitor.end()));
}
TEST_F(SpawnableTest, EntityAliasVisitor_UpdateAliases_FilterByTag_OnlyOneAliasUpdated)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(
{ 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 },
{ Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace,
Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace });
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
bool correctTag = false;
size_t numberOfUpdates = 0;
auto callback = [&correctTag, &numberOfUpdates](Spawnable::EntityAliasType& aliasType, bool& /*queueLoad*/,
const AZ::Data::Asset<Spawnable>& /*aliasedSpawnable*/, const AZ::Crc32 tag, const uint32_t /*sourceIndex*/,
const uint32_t /*targetIndex*/)
{
correctTag = (tag == AZ::Crc32(3));
numberOfUpdates++;
aliasType = Spawnable::EntityAliasType::Original;
};
visitor.UpdateAliases(AZ::Crc32(3), AZStd::move(callback));
EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()[3].m_aliasType);
}
//
// AreAllSpawnablesReady
//
TEST_F(SpawnableTest, EntityAliasVisitor_AreAllSpawnablesReady_CheckFakeLoadedAssets_ReturnsTrue)
{
using namespace AzFramework;
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>();
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_TRUE(visitor.AreAllSpawnablesReady());
}
TEST_F(SpawnableTest, EntityAliasVisitor_AreAllSpawnablesReady_CheckFakeNotLoadedAssets_ReturnsFalse)
{
using namespace AzFramework;
InsertEntities(8);
InsertEntityAliases<8>(true);
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
EXPECT_FALSE(visitor.AreAllSpawnablesReady());
}
//
// ListTargetSpawnables
//
TEST_F(SpawnableTest, EntityAliasVisitor_ListTargetSpawnables_ListAllTargetAssets_AllTargetsListed)
{
using namespace AzFramework;
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>();
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
size_t count = 0;
bool correctAssets = true;
auto callback = [&count, &correctAssets](const AZ::Data::Asset<Spawnable>& targetSpawnable)
{
correctAssets = correctAssets && (targetSpawnable.GetId().m_subId == count);
count++;
};
visitor.ListTargetSpawnables(callback);
EXPECT_EQ(8, count);
EXPECT_TRUE(correctAssets);
}
TEST_F(SpawnableTest, EntityAliasVisitor_ListTargetSpawnables_ListTaggedTargetAssets_OneAssetListed)
{
using namespace AzFramework;
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>();
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
size_t count = 0;
bool correctAsset = false;
auto callback = [&count, &correctAsset](const AZ::Data::Asset<Spawnable>& targetSpawnable)
{
correctAsset = (targetSpawnable.GetId().m_subId == 3);
count++;
};
visitor.ListTargetSpawnables(AZ::Crc32(3), callback);
EXPECT_EQ(1, count);
EXPECT_TRUE(correctAsset);
}
//
// ListSpawnablesRequiringLoad
//
TEST_F(SpawnableTest, EntityAliasVisitor_ListSpawnablesRequiringLoad_AllSetToLoaded_AllTargetsListed)
{
using namespace AzFramework;
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>(true);
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
size_t count = 0;
bool correctAssets = true;
auto callback = [&count, &correctAssets](const AZ::Data::Asset<Spawnable>& targetSpawnable)
{
correctAssets = correctAssets && (targetSpawnable.GetId().m_subId == count);
count++;
};
visitor.ListSpawnablesRequiringLoad(callback);
EXPECT_EQ(8, count);
EXPECT_TRUE(correctAssets);
}
TEST_F(SpawnableTest, EntityAliasVisitor_ListSpawnablesRequiringLoad_AllSetToNotLoaded_NoTargetsListed)
{
using namespace AzFramework;
InsertEntities(DefaultEntityAliasTestCount);
InsertEntityAliases<DefaultEntityAliasTestCount>(false);
AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases();
ASSERT_TRUE(visitor.IsValid());
size_t count = 0;
auto callback = [&count](const AZ::Data::Asset<Spawnable>& /*targetSpawnable*/)
{
count++;
};
visitor.ListSpawnablesRequiringLoad(callback);
EXPECT_EQ(0, count);
}
} // namespace UnitTest

@ -10,6 +10,7 @@ set(FILES
Main.cpp
Spawnable/SpawnableEntitiesInterfaceTests.cpp
Spawnable/SpawnableEntitiesManagerTests.cpp
Spawnable/SpawnableTests.cpp
ArchiveCompressionTests.cpp
ArchiveTests.cpp
BehaviorEntityTests.cpp

@ -2785,7 +2785,7 @@ namespace AzQtComponents
RepaintFloatingIndicators();
}
break;
case QEvent::WindowDeactivate:
case QEvent::WindowBlocked:
// If our main window is deactivated while we are in the middle of
// a docking drag operation (e.g. popup dialog for new level), we
// should cancel our drag operation because the mouse release event

@ -140,6 +140,11 @@ namespace AZ
void GemTestEnvironment::TeardownEnvironment()
{
for (AZ::ComponentDescriptor* descriptor : m_parameters->m_componentDescriptors)
{
m_application->UnregisterComponentDescriptor(descriptor);
}
const AZ::Entity::ComponentArrayType& components = m_gemEntity->GetComponents();
for (auto itComponent = components.rbegin(); itComponent != components.rend(); ++itComponent)
{

@ -18,7 +18,7 @@ namespace AZ
/// A test environment which is intended to facilitate writing unit tests which require components from a gem.
class GemTestEnvironment
: public UnitTest::TraceBusHook
: public ::UnitTest::TraceBusHook
{
public:
GemTestEnvironment();

@ -40,9 +40,9 @@ namespace AzToolsFramework
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QModelIndex parent(const QModelIndex& child) const override;
QModelIndex sibling(int row, int column, const QModelIndex& idx) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
protected:
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role /* = Qt::DisplayRole */) const override;
////////////////////////////////////////////////////////////////////
@ -55,7 +55,7 @@ namespace AzToolsFramework
private slots:
void SourceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight);
private:
AZ::u64 m_numberOfItemsDisplayed = 0;
AZ::u64 m_numberOfItemsDisplayed = 50;
int m_displayedItemsCounter = 0;
QPointer<AssetBrowserFilterModel> m_filterModel;
QMap<int, QModelIndex> m_indexMap;

@ -581,15 +581,15 @@ namespace AzToolsFramework
{
if (m_rootInstance && m_playInEditorData.m_isEnabled)
{
AZ_Assert(m_playInEditorData.m_entities.IsSet(),
AZ_Assert(
m_playInEditorData.m_entities.IsSet(),
"Invalid Game Mode Entities Container encountered after play-in-editor stopped. "
"Confirm that the container was initialized correctly");
m_playInEditorData.m_entities.DespawnAllEntities();
m_playInEditorData.m_entities.Alert(
[assets = AZStd::move(m_playInEditorData.m_assets),
deactivatedEntities = AZStd::move(m_playInEditorData.m_deactivatedEntities)]
([[maybe_unused]]uint32_t generation) mutable
deactivatedEntities = AZStd::move(m_playInEditorData.m_deactivatedEntities)]([[maybe_unused]] uint32_t generation) mutable
{
auto end = deactivatedEntities.rend();
for (auto it = deactivatedEntities.rbegin(); it != end; ++it)
@ -614,15 +614,15 @@ namespace AzToolsFramework
AzFramework::GameEntityContextEventBus::Broadcast(&AzFramework::GameEntityContextEventBus::Events::OnGameEntitiesReset);
});
m_playInEditorData.m_entities.Clear();
}
// Game entity cleanup is queued onto the next tick via the DespawnEntities call.
// To avoid both game entities and Editor entities active at the same time
// we flush the tick queue to ensure the game entities are cleared first.
// The Alert callback that follows the DespawnEntities call will then reactivate the editor entities
// This should be considered temporary as a move to a less rigid event sequence that supports async entity clean up
// is the desired direction forward.
AZ::TickBus::ExecuteQueuedEvents();
// Game entity cleanup is queued onto the next tick via the DespawnEntities call.
// To avoid both game entities and Editor entities active at the same time
// we flush the tick queue to ensure the game entities are cleared first.
// The Alert callback that follows the DespawnEntities call will then reactivate the editor entities
// This should be considered temporary as a move to a less rigid event sequence that supports async entity clean up
// is the desired direction forward.
AZ::TickBus::ExecuteQueuedEvents();
}
m_playInEditorData.m_isEnabled = false;
}

@ -129,7 +129,7 @@ namespace AzToolsFramework
void Instance::SetLinkId(LinkId linkId)
{
m_linkId = AZStd::move(linkId);
m_linkId = linkId;
}
LinkId Instance::GetLinkId() const
@ -154,23 +154,26 @@ namespace AzToolsFramework
bool Instance::AddEntity(AZ::Entity& entity)
{
EntityAlias newEntityAlias = GenerateEntityAlias();
return AddEntity(entity, newEntityAlias);
return AddEntity(entity, GenerateEntityAlias());
}
bool Instance::AddEntity(AZ::Entity& entity, EntityAlias entityAlias)
bool Instance::AddEntity(AZStd::unique_ptr<AZ::Entity>&& entity)
{
if (!RegisterEntity(entity.GetId(), entityAlias))
{
return false;
}
return AddEntity(AZStd::move(entity), GenerateEntityAlias());
}
if (!m_entities.emplace(AZStd::make_pair(entityAlias, &entity)).second)
{
return false;
}
bool Instance::AddEntity(AZ::Entity& entity, EntityAlias entityAlias)
{
return
RegisterEntity(entity.GetId(), entityAlias) &&
m_entities.emplace(AZStd::move(entityAlias), &entity).second;
}
return true;
bool Instance::AddEntity(AZStd::unique_ptr<AZ::Entity>&& entity, EntityAlias entityAlias)
{
return
RegisterEntity(entity->GetId(), entityAlias) &&
m_entities.emplace(AZStd::move(entityAlias), AZStd::move(entity)).second;
}
AZStd::unique_ptr<AZ::Entity> Instance::DetachEntity(const AZ::EntityId& entityId)
@ -228,6 +231,23 @@ namespace AzToolsFramework
m_entities.clear();
}
AZStd::unique_ptr<AZ::Entity> Instance::ReplaceEntity(AZStd::unique_ptr<AZ::Entity>&& entity, EntityAliasView alias)
{
AZStd::unique_ptr<AZ::Entity> result;
auto it = m_entities.find(alias);
if (it != m_entities.end())
{
// Swap entity ids as these need to remain stable
AZ::EntityId originalId = it->second->GetId();
it->second->SetId(entity->GetId());
entity->SetId(originalId);
result = AZStd::move(it->second);
it->second = AZStd::move(entity);
}
return result;
}
void Instance::RemoveNestedEntities(
const AZStd::function<bool(const AZStd::unique_ptr<AZ::Entity>&)>& filter)
{
@ -377,7 +397,12 @@ namespace AzToolsFramework
return entityAliases;
}
void Instance::GetNestedEntityIds(const AZStd::function<bool(AZ::EntityId)>& callback)
size_t Instance::GetEntityAliasCount() const
{
return m_entities.size();
}
void Instance::GetNestedEntityIds(const AZStd::function<bool(AZ::EntityId)>& callback) const
{
GetEntityIds(callback);
@ -387,7 +412,7 @@ namespace AzToolsFramework
}
}
void Instance::GetEntityIds(const AZStd::function<bool(AZ::EntityId)>& callback)
void Instance::GetEntityIds(const AZStd::function<bool(AZ::EntityId)>& callback) const
{
for (auto&&[entityAlias, entityId] : m_templateToInstanceEntityIdMap)
{
@ -398,6 +423,17 @@ namespace AzToolsFramework
}
}
void Instance::GetEntityIdToAlias(const AZStd::function<bool(AZ::EntityId, EntityAliasView)>& callback) const
{
for (auto&& [entityAlias, entityId] : m_templateToInstanceEntityIdMap)
{
if (!callback(entityId, entityAlias))
{
break;
}
}
}
bool Instance::GetEntities_Impl(const AZStd::function<bool(AZStd::unique_ptr<AZ::Entity>&)>& callback)
{
for (auto& [entityAlias, entity] : m_entities)
@ -514,24 +550,81 @@ namespace AzToolsFramework
}
}
EntityAliasOptionalReference Instance::GetEntityAlias(const AZ::EntityId& id)
EntityAliasOptionalReference Instance::GetEntityAlias(AZ::EntityId id)
{
if (m_instanceToTemplateEntityIdMap.count(id))
auto it = m_instanceToTemplateEntityIdMap.find(id);
return it != m_instanceToTemplateEntityIdMap.end() ? EntityAliasOptionalReference(it->second)
: EntityAliasOptionalReference(AZStd::nullopt);
}
EntityAliasView Instance::GetEntityAlias(AZ::EntityId id) const
{
auto it = m_instanceToTemplateEntityIdMap.find(id);
return it != m_instanceToTemplateEntityIdMap.end() ? EntityAliasView(it->second) : EntityAliasView();
}
AZStd::pair<Instance*, EntityAliasView> Instance::FindInstanceAndAlias(AZ::EntityId entity)
{
auto it = m_instanceToTemplateEntityIdMap.find(entity);
if (it != m_instanceToTemplateEntityIdMap.end())
{
return m_instanceToTemplateEntityIdMap[id];
return AZStd::pair<Instance*, EntityAliasView>(this, it->second);
}
return AZStd::nullopt;
else
{
for (auto&& [_, instance] : m_nestedInstances)
{
AZStd::pair<Instance*, EntityAliasView> next = instance->FindInstanceAndAlias(entity);
if (next.first != nullptr)
{
return next;
}
}
}
return AZStd::pair<Instance*, EntityAliasView>(nullptr, "");
}
AZ::EntityId Instance::GetEntityId(const EntityAlias& alias)
AZStd::pair<const Instance*, EntityAliasView> Instance::FindInstanceAndAlias(AZ::EntityId entity) const
{
if (m_templateToInstanceEntityIdMap.count(alias))
return const_cast<Instance*>(this)->FindInstanceAndAlias(entity);
}
EntityOptionalReference Instance::GetEntity(const EntityAlias& alias)
{
auto it = m_entities.find(alias);
return it != m_entities.end() ? EntityOptionalReference(*it->second) : EntityOptionalReference(AZStd::nullopt);
}
EntityOptionalConstReference Instance::GetEntity(const EntityAlias& alias) const
{
auto it = m_entities.find(alias);
return it != m_entities.end() ? EntityOptionalConstReference(*it->second) : EntityOptionalConstReference(AZStd::nullopt);
}
AZ::EntityId Instance::GetEntityId(const EntityAlias& alias) const
{
auto it = m_templateToInstanceEntityIdMap.find(alias);
return it != m_templateToInstanceEntityIdMap.end() ? it->second : AZ::EntityId();
}
AZ::EntityId Instance::GetEntityIdFromAliasPath(AliasPathView relativeAliasPath) const
{
const Instance* instance = this;
AliasPathView path = relativeAliasPath.ParentPath();
for (auto it : path)
{
return m_templateToInstanceEntityIdMap[alias];
InstanceOptionalConstReference child = instance->FindNestedInstance(it.Native());
if (child.has_value())
{
instance = &(child->get());
}
else
{
return AZ::EntityId();
}
}
return AZ::EntityId();
return instance->GetEntityId(relativeAliasPath.Filename().Native());
}
AZStd::vector<InstanceAlias> Instance::GetNestedInstanceAliases(TemplateId templateId) const
@ -572,6 +665,32 @@ namespace AzToolsFramework
return aliasPathResult;
}
AliasPath Instance::GetAliasPathRelativeToInstance(const AZ::EntityId& entity) const
{
AliasPath result = AliasPath(s_aliasPathSeparator);
auto&& [instance, alias] = FindInstanceAndAlias(entity);
if (instance)
{
AZStd::vector<const Instance*> instanceChain;
while (instance && instance != this)
{
instanceChain.push_back(instance);
instance = instance->m_parent;
}
for (auto it = instanceChain.rbegin(); it != instanceChain.rend(); ++it)
{
result.Append((*it)->m_alias);
}
return result.Append(alias);
}
else
{
return result;
}
}
EntityAlias Instance::GenerateEntityAlias()
{
return AZStd::string::format("Entity_%s", AZ::Entity::MakeId().ToString().c_str());

@ -38,6 +38,7 @@ namespace AzToolsFramework
using AliasPath = AZ::IO::Path;
using AliasPathView = AZ::IO::PathView;
using EntityAlias = AZStd::string;
using EntityAliasView = AZStd::string_view;
using InstanceAlias = AZStd::string;
class Instance;
@ -83,9 +84,17 @@ namespace AzToolsFramework
void SetContainerEntityName(AZStd::string_view containerName);
bool AddEntity(AZ::Entity& entity);
bool AddEntity(AZStd::unique_ptr<AZ::Entity>&& entity);
bool AddEntity(AZ::Entity& entity, EntityAlias entityAlias);
bool AddEntity(AZStd::unique_ptr<AZ::Entity>&& entity, EntityAlias entityAlias);
AZStd::unique_ptr<AZ::Entity> DetachEntity(const AZ::EntityId& entityId);
void DetachEntities(const AZStd::function<void(AZStd::unique_ptr<AZ::Entity>)>& callback);
/**
* Replaces the entity stored under the provided alias with a new one.
*
* @return The original entity or a nullptr if not found.
*/
AZStd::unique_ptr<AZ::Entity> ReplaceEntity(AZStd::unique_ptr<AZ::Entity>&& entity, EntityAliasView alias);
/**
* Detaches all entities in the instance hierarchy.
@ -109,13 +118,15 @@ namespace AzToolsFramework
* @return The list of EntityAliases
*/
AZStd::vector<EntityAlias> GetEntityAliases();
size_t GetEntityAliasCount() const;
/**
* Gets the ids for the entities in the Instance DOM. Can recursively trace all nested instances.
*/
void GetNestedEntityIds(const AZStd::function<bool(AZ::EntityId)>& callback);
void GetNestedEntityIds(const AZStd::function<bool(AZ::EntityId)>& callback) const;
void GetEntityIds(const AZStd::function<bool(AZ::EntityId)>& callback);
void GetEntityIds(const AZStd::function<bool(AZ::EntityId)>& callback) const;
void GetEntityIdToAlias(const AZStd::function<bool(AZ::EntityId, EntityAliasView)>& callback) const;
/**
* Gets the entities in the Instance DOM. Can recursively trace all nested instances.
@ -131,14 +142,33 @@ namespace AzToolsFramework
*
* @return entityAlias via optional
*/
AZStd::optional<AZStd::reference_wrapper<EntityAlias>> GetEntityAlias(const AZ::EntityId& id);
EntityAliasOptionalReference GetEntityAlias(AZ::EntityId id);
EntityAliasView GetEntityAlias(AZ::EntityId id) const;
/**
* Searches for the entity in this instance and its nested instances.
*
* @return The instance that owns the entity and the alias under which the entity is known.
* If the entity isn't found then the instance will be null and the alias empty.
*/
AZStd::pair<Instance*, EntityAliasView> FindInstanceAndAlias(AZ::EntityId entity);
AZStd::pair<const Instance*, EntityAliasView> FindInstanceAndAlias(AZ::EntityId entity) const;
EntityOptionalReference GetEntity(const EntityAlias& alias);
EntityOptionalConstReference GetEntity(const EntityAlias& alias) const;
/**
* Gets the id for a given EnitityAlias in the Instance DOM.
*
* @return entityId, invalid ID if not found
*/
AZ::EntityId GetEntityId(const EntityAlias& alias);
AZ::EntityId GetEntityId(const EntityAlias& alias) const;
/**
* Retrieves the entity id from an alias path that's relative to this instance.
*
* @return entityId, invalid ID if not found
*/
AZ::EntityId GetEntityIdFromAliasPath(AliasPathView relativeAliasPath) const;
/**
@ -180,6 +210,7 @@ namespace AzToolsFramework
static EntityAlias GenerateEntityAlias();
AliasPath GetAbsoluteInstanceAliasPath() const;
AliasPath GetAliasPathRelativeToInstance(const AZ::EntityId& entity) const;
static InstanceAlias GenerateInstanceAlias();

@ -49,7 +49,7 @@ namespace AzToolsFramework
//! @param instanceToExclude An optional reference to an instance of the template being updated that should not be refreshes as part of propagation.
//! Defaults to nullopt, which means that all instances will be refreshed.
//! @return True if the template was patched correctly, false if the operation failed.
virtual bool PatchTemplate(PrefabDomValue& providedPatch, TemplateId templateId, InstanceOptionalReference instanceToExclude = AZStd::nullopt) = 0;
virtual bool PatchTemplate(PrefabDomValue& providedPatch, TemplateId templateId, InstanceOptionalConstReference instanceToExclude = AZStd::nullopt) = 0;
virtual void ApplyPatchesToInstance(const AZ::EntityId& entityId, PrefabDom& patches, const Instance& instanceToAddPatches) = 0;

@ -156,7 +156,7 @@ namespace AzToolsFramework
}
}
bool InstanceToTemplatePropagator::PatchTemplate(PrefabDomValue& providedPatch, TemplateId templateId, InstanceOptionalReference instanceToExclude)
bool InstanceToTemplatePropagator::PatchTemplate(PrefabDomValue& providedPatch, TemplateId templateId, InstanceOptionalConstReference instanceToExclude)
{
PrefabDom& templateDomReference = m_prefabSystemComponentInterface->FindTemplateDom(templateId);

@ -33,7 +33,7 @@ namespace AzToolsFramework
InstanceOptionalReference GetTopMostInstanceInHierarchy(AZ::EntityId entityId) override;
bool PatchTemplate(PrefabDomValue& providedPatch, TemplateId templateId, InstanceOptionalReference instanceToExclude = AZStd::nullopt) override;
bool PatchTemplate(PrefabDomValue& providedPatch, TemplateId templateId, InstanceOptionalConstReference instanceToExclude = AZStd::nullopt) override;
void ApplyPatchesToInstance(const AZ::EntityId& entityId, PrefabDom& patches, const Instance& instanceToAddPatches) override;

@ -52,7 +52,7 @@ namespace AzToolsFramework
AZ::Interface<InstanceUpdateExecutorInterface>::Unregister(this);
}
void InstanceUpdateExecutor::AddTemplateInstancesToQueue(TemplateId instanceTemplateId, InstanceOptionalReference instanceToExclude)
void InstanceUpdateExecutor::AddTemplateInstancesToQueue(TemplateId instanceTemplateId, InstanceOptionalConstReference instanceToExclude)
{
auto findInstancesResult =
m_templateInstanceMapperInterface->FindInstancesOwnedByTemplate(instanceTemplateId);
@ -66,7 +66,7 @@ namespace AzToolsFramework
return;
}
Instance* instanceToExcludePtr = nullptr;
const Instance* instanceToExcludePtr = nullptr;
if (instanceToExclude.has_value())
{
instanceToExcludePtr = &(instanceToExclude->get());

@ -31,7 +31,7 @@ namespace AzToolsFramework
explicit InstanceUpdateExecutor(int instanceCountToUpdateInBatch = 0);
void AddTemplateInstancesToQueue(TemplateId instanceTemplateId, InstanceOptionalReference instanceToExclude = AZStd::nullopt) override;
void AddTemplateInstancesToQueue(TemplateId instanceTemplateId, InstanceOptionalConstReference instanceToExclude = AZStd::nullopt) override;
bool UpdateTemplateInstancesInQueue() override;
virtual void RemoveTemplateInstanceFromQueue(const Instance* instance) override;

@ -23,7 +23,7 @@ namespace AzToolsFramework
virtual ~InstanceUpdateExecutorInterface() = default;
// Add all Instances of Template with given Id into a queue for updating them later.
virtual void AddTemplateInstancesToQueue(TemplateId instanceTemplateId, InstanceOptionalReference instanceToExclude = AZStd::nullopt) = 0;
virtual void AddTemplateInstancesToQueue(TemplateId instanceTemplateId, InstanceOptionalConstReference instanceToExclude = AZStd::nullopt) = 0;
// Update Instances in the waiting queue.
virtual bool UpdateTemplateInstancesInQueue() = 0;

@ -777,7 +777,7 @@ namespace AzToolsFramework
linkUpdate->SetParent(undoBatch);
linkUpdate->Capture(patch, linkId);
linkUpdate->Redo(parentInstance);
linkUpdate->Redo(parentInstance->get());
}
void PrefabPublicHandler::Internal_HandleEntityChange(
@ -788,8 +788,7 @@ namespace AzToolsFramework
PrefabUndoEntityUpdate* state = aznew PrefabUndoEntityUpdate(AZStd::to_string(static_cast<AZ::u64>(entityId)));
state->SetParent(undoBatch);
state->Capture(beforeState, afterState, entityId);
state->Redo(instance);
state->Redo(instance->get());
}
void PrefabPublicHandler::Internal_HandleInstanceChange(
@ -1167,88 +1166,83 @@ namespace AzToolsFramework
ScopedUndoBatch undoBatch("Delete Selected");
// In order to undo DeleteSelected, we have to create a selection command which selects the current selection
// and then add the deletion as children.
// Commands always execute themselves first and then their children (when going forwards)
// and do the opposite when going backwards.
EntityIdList selectedEntities;
ToolsApplicationRequestBus::BroadcastResult(selectedEntities, &ToolsApplicationRequests::GetSelectedEntities);
SelectionCommand* selCommand = aznew SelectionCommand(selectedEntities, "Delete Entities");
AZ_PROFILE_SCOPE(AzToolsFramework, "Internal::DeleteEntities:UndoCaptureAndPurgeEntities");
// We insert a "deselect all" command before we delete the entities. This ensures the delete operations aren't changing
// selection state, which triggers expensive UI updates. By deselecting up front, we are able to do those expensive
// UI updates once at the start instead of once for each entity.
{
EntityIdList deselection;
SelectionCommand* deselectAllCommand = aznew SelectionCommand(deselection, "Deselect Entities");
deselectAllCommand->SetParent(selCommand);
}
Prefab::PrefabDom instanceDomBefore;
m_instanceToTemplateInterface->GenerateDomForInstance(instanceDomBefore, commonOwningInstance->get());
if (deleteDescendants)
{
AZ_PROFILE_SCOPE(AzToolsFramework, "Internal::DeleteEntities:UndoCaptureAndPurgeEntities");
AZStd::vector<AZ::Entity*> entities;
AZStd::vector<Instance*> instances;
Prefab::PrefabDom instanceDomBefore;
m_instanceToTemplateInterface->GenerateDomForInstance(instanceDomBefore, commonOwningInstance->get());
PrefabOperationResult retrieveEntitiesAndInstancesOutcome =
RetrieveAndSortPrefabEntitiesAndInstances(inputEntityList, commonOwningInstance->get(), entities, instances);
if (deleteDescendants)
if (!retrieveEntitiesAndInstancesOutcome.IsSuccess())
{
AZStd::vector<AZ::Entity*> entities;
AZStd::vector<Instance*> instances;
PrefabOperationResult retrieveEntitiesAndInstancesOutcome =
RetrieveAndSortPrefabEntitiesAndInstances(inputEntityList, commonOwningInstance->get(), entities, instances);
if (!retrieveEntitiesAndInstancesOutcome.IsSuccess())
{
return AZStd::move(retrieveEntitiesAndInstancesOutcome);
}
return AZStd::move(retrieveEntitiesAndInstancesOutcome);
}
for (AZ::Entity* entity : entities)
{
commonOwningInstance->get().DetachEntity(entity->GetId()).release();
AZ::ComponentApplicationBus::Broadcast(&AZ::ComponentApplicationRequests::DeleteEntity, entity->GetId());
}
for (AZ::Entity* entity : entities)
{
commonOwningInstance->get().DetachEntity(entity->GetId()).release();
AZ::ComponentApplicationBus::Broadcast(&AZ::ComponentApplicationRequests::DeleteEntity, entity->GetId());
}
for (auto& nestedInstance : instances)
{
AZStd::unique_ptr<Instance> outInstance = commonOwningInstance->get().DetachNestedInstance(nestedInstance->GetInstanceAlias());
RemoveLink(outInstance, commonOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch());
outInstance.reset();
}
for (auto& nestedInstance : instances)
{
AZStd::unique_ptr<Instance> outInstance =
commonOwningInstance->get().DetachNestedInstance(nestedInstance->GetInstanceAlias());
RemoveLink(outInstance, commonOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch());
outInstance.reset();
}
else
}
else
{
for (AZ::EntityId entityId : entityIdsNoFocusContainer)
{
for (AZ::EntityId entityId : entityIdsNoFocusContainer)
InstanceOptionalReference owningInstance = m_instanceEntityMapperInterface->FindOwningInstance(entityId);
// If this is the container entity, it actually represents the instance so get its owner
if (owningInstance->get().GetContainerEntityId() == entityId)
{
InstanceOptionalReference owningInstance = m_instanceEntityMapperInterface->FindOwningInstance(entityId);
// If this is the container entity, it actually represents the instance so get its owner
if (owningInstance->get().GetContainerEntityId() == entityId)
{
auto instancePtr = commonOwningInstance->get().DetachNestedInstance(owningInstance->get().GetInstanceAlias());
RemoveLink(instancePtr, commonOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch());
}
else
{
commonOwningInstance->get().DetachEntity(entityId);
AZ::ComponentApplicationBus::Broadcast(&AZ::ComponentApplicationRequests::DeleteEntity, entityId);
}
auto instancePtr = commonOwningInstance->get().DetachNestedInstance(owningInstance->get().GetInstanceAlias());
RemoveLink(instancePtr, commonOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch());
}
else
{
commonOwningInstance->get().DetachEntity(entityId);
AZ::ComponentApplicationBus::Broadcast(&AZ::ComponentApplicationRequests::DeleteEntity, entityId);
}
}
Prefab::PrefabDom instanceDomAfter;
m_instanceToTemplateInterface->GenerateDomForInstance(instanceDomAfter, commonOwningInstance->get());
PrefabUndoInstance* command = aznew PrefabUndoInstance("Instance deletion");
command->Capture(instanceDomBefore, instanceDomAfter, commonOwningInstance->get().GetTemplateId());
command->SetParent(selCommand);
}
Prefab::PrefabDom instanceDomAfter;
m_instanceToTemplateInterface->GenerateDomForInstance(instanceDomAfter, commonOwningInstance->get());
// In order to undo DeleteSelected, we have to create a selection command which selects the current selection
// and then add the deletion as children.
// Commands always execute themselves first and then their children (when going forwards)
// and do the opposite when going backwards.
EntityIdList selectedEntities;
ToolsApplicationRequestBus::BroadcastResult(selectedEntities, &ToolsApplicationRequests::GetSelectedEntities);
SelectionCommand* selCommand = aznew SelectionCommand(selectedEntities, "Delete Entities");
selCommand->SetParent(undoBatch.GetUndoBatch());
{
AZ_PROFILE_SCOPE(AzToolsFramework, "Internal::DeleteEntities:RunRedo");
selCommand->RunRedo();
}
AZ_PROFILE_SCOPE(AzToolsFramework, "Internal::DeleteEntities:RunRedo");
selCommand->RunRedo();
// We insert a "deselect all" command before we delete the entities. This ensures the delete operations aren't changing
// selection state, which triggers expensive UI updates. By deselecting up front, we are able to do those expensive
// UI updates once at the start instead of once for each entity.
EntityIdList deselection;
SelectionCommand* deselectAllCommand = aznew SelectionCommand(deselection, "Deselect Entities");
deselectAllCommand->SetParent(undoBatch.GetUndoBatch());
PrefabUndoInstance* command = aznew PrefabUndoInstance("Instance deletion");
command->Capture(instanceDomBefore, instanceDomAfter, commonOwningInstance->get().GetTemplateId());
command->SetParent(undoBatch.GetUndoBatch());
command->Redo(commonOwningInstance->get());
return AZ::Success();
}
@ -1338,7 +1332,7 @@ namespace AzToolsFramework
command->SetParent(undoBatch.GetUndoBatch());
{
AZ_PROFILE_SCOPE(AzToolsFramework, "Internal::DetachPrefab:RunRedo");
command->RunRedo();
command->Redo(parentInstance);
}
instancePtr->DetachNestedInstances(

@ -159,11 +159,9 @@ namespace AzToolsFramework
newInstance->SetTemplateId(newTemplateId);
}
}
void PrefabSystemComponent::PropagateTemplateChanges(TemplateId templateId, InstanceOptionalReference instanceToExclude)
void PrefabSystemComponent::PropagateTemplateChanges(TemplateId templateId, InstanceOptionalConstReference instanceToExclude)
{
UpdatePrefabInstances(templateId, instanceToExclude);
auto templateIdToLinkIdsIterator = m_templateToLinkIdsMap.find(templateId);
if (templateIdToLinkIdsIterator != m_templateToLinkIdsMap.end())
{
@ -174,6 +172,7 @@ namespace AzToolsFramework
templateIdToLinkIdsIterator->second.end()));
UpdateLinkedInstances(linkIdsToUpdateQueue);
}
UpdatePrefabInstances(templateId, instanceToExclude);
}
void PrefabSystemComponent::UpdatePrefabTemplate(TemplateId templateId, const PrefabDom& updatedDom)
@ -191,7 +190,7 @@ namespace AzToolsFramework
}
}
void PrefabSystemComponent::UpdatePrefabInstances(TemplateId templateId, InstanceOptionalReference instanceToExclude)
void PrefabSystemComponent::UpdatePrefabInstances(TemplateId templateId, InstanceOptionalConstReference instanceToExclude)
{
m_instanceUpdateExecutor.AddTemplateInstancesToQueue(templateId, instanceToExclude);
}

@ -231,7 +231,7 @@ namespace AzToolsFramework
*/
void UpdatePrefabTemplate(TemplateId templateId, const PrefabDom& updatedDom) override;
void PropagateTemplateChanges(TemplateId templateId, InstanceOptionalReference instanceToExclude = AZStd::nullopt) override;
void PropagateTemplateChanges(TemplateId templateId, InstanceOptionalConstReference instanceToExclude = AZStd::nullopt) override;
/**
* Updates all Instances owned by a Template.
@ -240,7 +240,7 @@ namespace AzToolsFramework
* @param instanceToExclude An optional reference to an instance of the template being updated that should not be refreshed
* as part of propagation.Defaults to nullopt, which means that all instances will be refreshed.
*/
void UpdatePrefabInstances(TemplateId templateId, InstanceOptionalReference instanceToExclude = AZStd::nullopt);
void UpdatePrefabInstances(TemplateId templateId, InstanceOptionalConstReference instanceToExclude = AZStd::nullopt);
private:
AZ_DISABLE_COPY_MOVE(PrefabSystemComponent);

@ -67,7 +67,7 @@ namespace AzToolsFramework
virtual PrefabDom& FindTemplateDom(TemplateId templateId) = 0;
virtual void UpdatePrefabTemplate(TemplateId templateId, const PrefabDom& updatedDom) = 0;
virtual void PropagateTemplateChanges(TemplateId templateId, InstanceOptionalReference instanceToExclude = AZStd::nullopt) = 0;
virtual void PropagateTemplateChanges(TemplateId templateId, InstanceOptionalConstReference instanceToExclude = AZStd::nullopt) = 0;
virtual AZStd::unique_ptr<Instance> InstantiatePrefab(
AZ::IO::PathView filePath, InstanceOptionalReference parent = AZStd::nullopt) = 0;

@ -51,6 +51,10 @@ namespace AzToolsFramework
m_instanceToTemplateInterface->PatchTemplate(m_redoPatch, m_templateId);
}
void PrefabUndoInstance::Redo(InstanceOptionalConstReference instance)
{
m_instanceToTemplateInterface->PatchTemplate(m_redoPatch, m_templateId, instance);
}
//PrefabEntityUpdateUndo
PrefabUndoEntityUpdate::PrefabUndoEntityUpdate(const AZStd::string& undoOperationName)
@ -110,10 +114,10 @@ namespace AzToolsFramework
m_templateId);
}
void PrefabUndoEntityUpdate::Redo(InstanceOptionalReference instanceToExclude)
void PrefabUndoEntityUpdate::Redo(InstanceOptionalConstReference instance)
{
[[maybe_unused]] bool isPatchApplicationSuccessful =
m_instanceToTemplateInterface->PatchTemplate(m_redoPatch, m_templateId, instanceToExclude);
m_instanceToTemplateInterface->PatchTemplate(m_redoPatch, m_templateId, instance);
AZ_Error(
"Prefab", isPatchApplicationSuccessful,
@ -310,12 +314,12 @@ namespace AzToolsFramework
UpdateLink(m_linkDomNext);
}
void PrefabUndoLinkUpdate::Redo(InstanceOptionalReference instanceToExclude)
void PrefabUndoLinkUpdate::Redo(InstanceOptionalConstReference instanceToExclude)
{
UpdateLink(m_linkDomNext, instanceToExclude);
}
void PrefabUndoLinkUpdate::UpdateLink(PrefabDom& linkDom, InstanceOptionalReference instanceToExclude)
void PrefabUndoLinkUpdate::UpdateLink(PrefabDom& linkDom, InstanceOptionalConstReference instanceToExclude)
{
LinkReference link = m_prefabSystemComponentInterface->FindLink(m_linkId);

@ -53,6 +53,7 @@ namespace AzToolsFramework
void Undo() override;
void Redo() override;
void Redo(InstanceOptionalConstReference instance);
};
//! handles entity updates, such as when the values on an entity change
@ -72,7 +73,7 @@ namespace AzToolsFramework
void Undo() override;
void Redo() override;
//! Overload to allow to apply the change, but prevent instanceToExclude from being refreshed.
void Redo(InstanceOptionalReference instanceToExclude);
void Redo(InstanceOptionalConstReference instanceToExclude);
private:
InstanceEntityMapperInterface* m_instanceEntityMapperInterface = nullptr;
@ -137,10 +138,10 @@ namespace AzToolsFramework
void Undo() override;
void Redo() override;
//! Overload to allow to apply the change, but prevent instanceToExclude from being refreshed.
void Redo(InstanceOptionalReference instanceToExclude);
void Redo(InstanceOptionalConstReference instanceToExclude);
private:
void UpdateLink(PrefabDom& linkDom, InstanceOptionalReference instanceToExclude = AZStd::nullopt);
void UpdateLink(PrefabDom& linkDom, InstanceOptionalConstReference instanceToExclude = AZStd::nullopt);
LinkId m_linkId;
PrefabDom m_linkDomNext; //data for delete/update

@ -26,7 +26,7 @@ namespace AzToolsFramework
PrefabUndoInstance* state = aznew Prefab::PrefabUndoInstance(undoMessage);
state->Capture(instanceDomBeforeUpdate, instanceDomAfterUpdate, instance.GetTemplateId());
state->SetParent(undoBatch);
state->Redo();
state->Redo(instance);
}
LinkId CreateLink(

@ -13,9 +13,12 @@
#include <AzCore/Serialization/Utils.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzToolsFramework/Prefab/Instance/Instance.h>
#include <AzToolsFramework/Prefab/PrefabDomUtils.h>
#include <AzToolsFramework/Prefab/Spawnable/PrefabCatchmentProcessor.h>
#include <AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h>
namespace AzToolsFramework::Prefab::PrefabConversionUtils
{
void PrefabCatchmentProcessor::Process(PrefabProcessorContext& context)
@ -37,7 +40,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
->Value("Text", SerializationFormats::Text);
serializeContext->Class<PrefabCatchmentProcessor, PrefabProcessor>()
->Version(2)
->Version(3)
->Field("SerializationFormat", &PrefabCatchmentProcessor::m_serializationFormat);
}
}
@ -45,6 +48,8 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
void PrefabCatchmentProcessor::ProcessPrefab(PrefabProcessorContext& context, AZStd::string_view prefabName, PrefabDom& prefab,
AZ::DataStream::StreamType serializationFormat)
{
using namespace AzToolsFramework::Prefab::SpawnableUtils;
AZStd::string uniqueName = prefabName;
uniqueName += AzFramework::Spawnable::DotFileExtension;
@ -59,33 +64,38 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
AZStd::move(uniqueName), context.GetSourceUuid(), AZStd::move(serializer));
AZ_Assert(spawnable, "Failed to create a new spawnable.");
bool result = SpawnableUtils::CreateSpawnable(*spawnable, prefab, object.GetReferencedAssets());
if (result)
Instance instance;
if (Prefab::PrefabDomUtils::LoadInstanceFromPrefabDom(
instance, prefab, object.GetReferencedAssets(),
Prefab::PrefabDomUtils::LoadFlags::AssignRandomEntityId)) // Always assign random entity ids because the spawnable is
// going to be used to create clones of the entities.
{
// Resolve entity aliases that store PrefabDOM information to use the spawnable instead. This is done before the entities are
// moved from the instance as they'd otherwise can't be found.
context.ResolveSpawnableEntityAliases(prefabName, *spawnable, instance);
AzFramework::Spawnable::EntityList& entities = spawnable->GetEntities();
for (auto it = entities.begin(); it != entities.end(); )
{
if (*it)
instance.DetachAllEntitiesInHierarchy(
[&entities, &context](AZStd::unique_ptr<AZ::Entity> entity)
{
(*it)->InvalidateDependencies();
AZ::Entity::DependencySortOutcome evaluation = (*it)->EvaluateDependenciesGetDetails();
if (evaluation.IsSuccess())
if (entity)
{
++it;
entity->InvalidateDependencies();
AZ::Entity::DependencySortOutcome evaluation = entity->EvaluateDependenciesGetDetails();
if (evaluation.IsSuccess())
{
entities.emplace_back(AZStd::move(entity));
}
else
{
AZ_Error(
"Prefabs", false, "Entity '%s' %s cannot be activated for the following reason: %s",
entity->GetName().c_str(), entity->GetId().ToString().c_str(), evaluation.GetError().m_message.c_str());
context.ErrorEncountered();
}
}
else
{
AZ_Error(
"Prefabs", false, "Entity '%s' %s cannot be activated for the following reason: %s", (*it)->GetName().c_str(),
(*it)->GetId().ToString().c_str(), evaluation.GetError().m_message.c_str());
it = entities.erase(it);
}
}
else
{
it = entities.erase(it);
}
}
});
SpawnableUtils::SortEntitiesByTransformHierarchy(*spawnable);
context.GetProcessedObjects().push_back(AZStd::move(object));
}

@ -54,6 +54,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
{
processor->Process(context);
}
context.ResolveLinks();
}
size_t PrefabConversionPipeline::CalculateProcessorFingerprint(AZ::SerializeContext* context)
{

@ -6,12 +6,26 @@
*
*/
#include <AzCore/Interface/Interface.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzToolsFramework/Prefab/Instance/InstanceEntityMapperInterface.h>
#include <AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h>
#include <AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h>
namespace AzToolsFramework::Prefab::PrefabConversionUtils
{
EntityAliasSpawnableLink::EntityAliasSpawnableLink(AzFramework::Spawnable& spawnable, AZ::EntityId index)
: m_spawnable(spawnable)
, m_index(index)
{
}
EntityAliasPrefabLink::EntityAliasPrefabLink(AZStd::string prefabName, AzToolsFramework::Prefab::AliasPath alias)
: m_prefabName(AZStd::move(prefabName))
, m_alias(AZStd::move(alias))
{
}
PrefabProcessorContext::PrefabProcessorContext(const AZ::Uuid& sourceUuid)
: m_sourceUuid(sourceUuid)
{}
@ -45,7 +59,8 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
return !m_prefabs.empty();
}
bool PrefabProcessorContext::RegisterSpawnableProductAssetDependency(AZStd::string prefabName, AZStd::string dependentPrefabName)
bool PrefabProcessorContext::RegisterSpawnableProductAssetDependency(
AZStd::string prefabName, AZStd::string dependentPrefabName, EntityAliasSpawnableLoadBehavior loadBehavior)
{
using ConversionUtils = PrefabConversionUtils::ProcessedObjectStore;
@ -55,10 +70,11 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
dependentPrefabName += AzFramework::Spawnable::DotFileExtension;
uint32_t spawnablePrefabSubId = ConversionUtils::BuildSubId(AZStd::move(dependentPrefabName));
return RegisterSpawnableProductAssetDependency(spawnableSubId, spawnablePrefabSubId);
return RegisterSpawnableProductAssetDependency(spawnableSubId, spawnablePrefabSubId, loadBehavior);
}
bool PrefabProcessorContext::RegisterSpawnableProductAssetDependency(AZStd::string prefabName, const AZ::Data::AssetId& dependentAssetId)
bool PrefabProcessorContext::RegisterSpawnableProductAssetDependency(
AZStd::string prefabName, const AZ::Data::AssetId& dependentAssetId, EntityAliasSpawnableLoadBehavior loadBehavior)
{
using ConversionUtils = PrefabConversionUtils::ProcessedObjectStore;
@ -67,20 +83,78 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
AZ::Data::AssetId spawnableAssetId(GetSourceUuid(), spawnableSubId);
return RegisterProductAssetDependency(spawnableAssetId, dependentAssetId);
return RegisterProductAssetDependency(spawnableAssetId, dependentAssetId, ToAssetLoadBehavior(loadBehavior));
}
bool PrefabProcessorContext::RegisterSpawnableProductAssetDependency(uint32_t spawnableAssetSubId, uint32_t dependentSpawnableAssetSubId)
bool PrefabProcessorContext::RegisterSpawnableProductAssetDependency(
uint32_t spawnableAssetSubId, uint32_t dependentSpawnableAssetSubId, EntityAliasSpawnableLoadBehavior loadBehavior)
{
AZ::Data::AssetId spawnableAssetId(GetSourceUuid(), spawnableAssetSubId);
AZ::Data::AssetId dependentSpawnableAssetId(GetSourceUuid(), dependentSpawnableAssetSubId);
return RegisterProductAssetDependency(spawnableAssetId, dependentSpawnableAssetId);
return RegisterProductAssetDependency(spawnableAssetId, dependentSpawnableAssetId, ToAssetLoadBehavior(loadBehavior));
}
bool PrefabProcessorContext::RegisterProductAssetDependency(const AZ::Data::AssetId& assetId, const AZ::Data::AssetId& dependentAssetId)
{
return m_registeredProductAssetDependencies[assetId].emplace(dependentAssetId).second;
return RegisterProductAssetDependency(assetId, dependentAssetId, AZ::Data::AssetLoadBehavior::NoLoad);
}
bool PrefabProcessorContext::RegisterProductAssetDependency(
const AZ::Data::AssetId& assetId, const AZ::Data::AssetId& dependentAssetId, AZ::Data::AssetLoadBehavior loadBehavior)
{
auto dependencies = m_registeredProductAssetDependencies.equal_range(assetId);
if (dependencies.first != dependencies.second)
{
for (auto it = dependencies.first; it != dependencies.second; ++it)
{
if (it->second.m_assetId == dependentAssetId)
{
if (it->second.m_loadBehavior < loadBehavior)
{
it->second.m_loadBehavior = loadBehavior;
}
return true;
}
}
}
return m_registeredProductAssetDependencies.emplace(assetId, AssetDependencyInfo{ dependentAssetId, loadBehavior }).second;
}
void PrefabProcessorContext::RegisterSpawnableEntityAlias(EntityAliasStore link)
{
m_entityAliases.push_back(AZStd::move(link));
}
void PrefabProcessorContext::ResolveSpawnableEntityAliases(
AZStd::string_view prefabName, AzFramework::Spawnable& spawnable, const AzToolsFramework::Prefab::Instance& instance)
{
using namespace AzToolsFramework::Prefab;
for (EntityAliasStore& entityAlias : m_entityAliases)
{
auto sourcePrefab = AZStd::get_if<EntityAliasPrefabLink>(&entityAlias.m_source);
if (sourcePrefab != nullptr && sourcePrefab->m_prefabName == prefabName)
{
AZ::EntityId id = instance.GetEntityIdFromAliasPath(sourcePrefab->m_alias);
AZ_Assert(
id.IsValid(),
"Entity '%s' was not found in Prefab Instance created from '%s' even though it was previously found.",
sourcePrefab->m_alias.c_str(), sourcePrefab->m_prefabName.c_str());
entityAlias.m_source.emplace<EntityAliasSpawnableLink>(spawnable, id);
}
auto targetPrefab = AZStd::get_if<EntityAliasPrefabLink>(&entityAlias.m_target);
if (targetPrefab != nullptr && targetPrefab->m_prefabName == prefabName)
{
AZ::EntityId id = instance.GetEntityIdFromAliasPath(targetPrefab->m_alias);
AZ_Assert(
id.IsValid(), "Entity '%s' was not found in Prefab Instance created from '%s' even though it was previously found.",
targetPrefab->m_alias.c_str(), targetPrefab->m_prefabName.c_str());
entityAlias.m_target.emplace<EntityAliasSpawnableLink>(spawnable, id);
}
}
}
PrefabProcessorContext::ProcessedObjectStoreContainer& PrefabProcessorContext::GetProcessedObjects()
@ -118,6 +192,46 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
return m_sourceUuid;
}
void PrefabProcessorContext::ResolveLinks()
{
// Store the aliases visitor here when first encountered to avoid the visitor sorting the aliases for every addition.
// Once this map goes out of scope the visitors will be destroyed and in turn sort their aliases.
AZStd::unordered_map<AZ::Data::AssetId, AzFramework::Spawnable::EntityAliasVisitor> aliasVisitors;
for (EntityAliasStore& alias : m_entityAliases)
{
auto source = AZStd::get_if<EntityAliasSpawnableLink>(&alias.m_source);
AZ_Assert(source, "Entity alias found that has a source that's not yet resolved to a spawnable");
auto target = AZStd::get_if<EntityAliasSpawnableLink>(&alias.m_target);
AZ_Assert(target, "Entity alias found that has a target that's not yet resolved to a spawnable");
uint32_t sourceIndex = SpawnableUtils::FindEntityIndex(source->m_index, source->m_spawnable);
AZ_Assert(
sourceIndex != SpawnableUtils::InvalidEntityIndex, "Entity %zu not found in source spawnable while resolving to index.",
aznumeric_cast<AZ::u64>(source->m_index));
uint32_t targetIndex = SpawnableUtils::FindEntityIndex(target->m_index, target->m_spawnable);
AZ_Assert(
targetIndex != SpawnableUtils::InvalidEntityIndex, "Entity %zu not found in target spawnable while resolving to index.",
aznumeric_cast<AZ::u64>(target->m_index));
AZ::Data::AssetLoadBehavior loadBehavior = ToAssetLoadBehavior(alias.m_loadBehavior);
auto it = aliasVisitors.find(source->m_spawnable.GetId());
if (it == aliasVisitors.end())
{
AzFramework::Spawnable::EntityAliasVisitor visitor = source->m_spawnable.TryGetAliases();
AZ_Assert(visitor.IsValid(), "Unable to obtain lock for a newly create spawnable.");
it = aliasVisitors.emplace(source->m_spawnable.GetId(), AZStd::move(visitor)).first;
}
it->second.AddAlias(
AZ::Data::Asset<AzFramework::Spawnable>(&target->m_spawnable, loadBehavior), alias.m_tag, sourceIndex, targetIndex,
alias.m_aliasType, alias.m_loadBehavior == EntityAliasSpawnableLoadBehavior::QueueLoad);
// Register the dependency between the two spawnables.
RegisterProductAssetDependency(source->m_spawnable.GetId(), target->m_spawnable.GetId(), loadBehavior);
}
}
bool PrefabProcessorContext::HasCompletedSuccessfully() const
{
return m_completedSuccessfully;
@ -127,4 +241,10 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
{
m_completedSuccessfully = false;
}
AZ::Data::AssetLoadBehavior PrefabProcessorContext::ToAssetLoadBehavior(EntityAliasSpawnableLoadBehavior loadBehavior) const
{
return loadBehavior == EntityAliasSpawnableLoadBehavior::DependentLoad ? AZ::Data::AssetLoadBehavior::PreLoad
: AZ::Data::AssetLoadBehavior::NoLoad;
}
} // namespace AzToolsFramework::Prefab::PrefabConversionUtils

@ -9,24 +9,83 @@
#pragma once
#include <AzCore/Component/ComponentExport.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/std/containers/unordered_map.h>
#include <AzCore/std/containers/variant.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/functional.h>
#include <AzCore/std/string/string.h>
#include <AzCore/std/string/string_view.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzToolsFramework/Prefab/Instance/Instance.h>
#include <AzToolsFramework/Prefab/PrefabDomTypes.h>
#include <AzToolsFramework/Prefab/Spawnable/ProcesedObjectStore.h>
namespace AzToolsFramework::Prefab::PrefabConversionUtils
{
enum class EntityAliasType : uint8_t
{
Disable, //!< No alias is added.
OptionalReplace, //!< At runtime the entity might be replaced. If the alias is disabled the original entity will be spawned.
//!< The original entity will be left in the spawnable and a copy is returned.
Replace, //!< At runtime the entity will be replaced. If the alias is disabled nothing will be spawned. The original
//!< entity is returned and a blank entity is left.
Additional, //!< At runtime the alias entity will be added as an additional but unrelated entity with a new entity id.
//!< An empty entity will be returned.
Merge //!< At runtime the components in both entities will be merged. An empty entity will be returned. The added
//!< components may no conflict with the entities already in the root entity.
};
enum class EntityAliasSpawnableLoadBehavior : uint8_t
{
NoLoad, //!< Don't load the spawnable referenced in the entity alias. Loading will be up to the caller.
QueueLoad, //!< Queue the spawnable referenced in the entity alias for loading. This will be an async load because asset
//!< handlers aren't allowed to start a blocking load as this can lead to deadlocks. This option will allow
//!< to disable loading the referenced spawnable through the event fired from the spawnables asset handler.
DependentLoad //!< The spawnable referenced in the entity alias is made a dependency of the spawnable that holds the entity
//!< alias. This will cause the spawnable to be automatically loaded along with the owning spawnable.
};
struct EntityAliasSpawnableLink
{
EntityAliasSpawnableLink(AzFramework::Spawnable& spawnable, AZ::EntityId index);
AzFramework::Spawnable& m_spawnable;
AZ::EntityId m_index;
};
struct EntityAliasPrefabLink
{
EntityAliasPrefabLink(AZStd::string prefabName, AzToolsFramework::Prefab::AliasPath alias);
AZStd::string m_prefabName;
AzToolsFramework::Prefab::AliasPath m_alias;
};
struct EntityAliasStore
{
using LinkStore = AZStd::variant<AZStd::monostate, EntityAliasSpawnableLink, EntityAliasPrefabLink>;
LinkStore m_source;
LinkStore m_target;
uint32_t m_tag;
AzFramework::Spawnable::EntityAliasType m_aliasType;
EntityAliasSpawnableLoadBehavior m_loadBehavior;
};
struct AssetDependencyInfo
{
AZ::Data::AssetId m_assetId;
AZ::Data::AssetLoadBehavior m_loadBehavior;
};
class PrefabProcessorContext
{
public:
using ProcessedObjectStoreContainer = AZStd::vector<ProcessedObjectStore>;
using ProductAssetDependencyContainer =
AZStd::unordered_map<AZ::Data::AssetId, AZStd::unordered_set<AZ::Data::AssetId>>;
using ProductAssetDependencyContainer = AZStd::unordered_multimap<AZ::Data::AssetId, AssetDependencyInfo>;
AZ_CLASS_ALLOCATOR(PrefabProcessorContext, AZ::SystemAllocator, 0);
AZ_RTTI(PrefabProcessorContext, "{C7D77E3A-C544-486B-B774-7C82C38FE22F}");
@ -39,11 +98,20 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
virtual void ListPrefabs(const AZStd::function<void(AZStd::string_view, const PrefabDom&)>& callback) const;
virtual bool HasPrefabs() const;
virtual bool RegisterSpawnableProductAssetDependency(AZStd::string prefabName, AZStd::string dependentPrefabName);
virtual bool RegisterSpawnableProductAssetDependency(AZStd::string prefabName, const AZ::Data::AssetId& dependentAssetId);
virtual bool RegisterSpawnableProductAssetDependency(uint32_t spawnableAssetSubId, uint32_t dependentSpawnableAssetSubId);
virtual bool RegisterSpawnableProductAssetDependency(
AZStd::string prefabName, AZStd::string dependentPrefabName, EntityAliasSpawnableLoadBehavior loadBehavior);
virtual bool RegisterSpawnableProductAssetDependency(
AZStd::string prefabName, const AZ::Data::AssetId& dependentAssetId, EntityAliasSpawnableLoadBehavior loadBehavior);
virtual bool RegisterSpawnableProductAssetDependency(
uint32_t spawnableAssetSubId, uint32_t dependentSpawnableAssetSubId, EntityAliasSpawnableLoadBehavior loadBehavior);
virtual bool RegisterProductAssetDependency(const AZ::Data::AssetId& assetId, const AZ::Data::AssetId& dependentAssetId);
virtual bool RegisterProductAssetDependency(
const AZ::Data::AssetId& assetId, const AZ::Data::AssetId& dependentAssetId, AZ::Data::AssetLoadBehavior loadBehavior);
virtual void RegisterSpawnableEntityAlias(EntityAliasStore link);
virtual void ResolveSpawnableEntityAliases(
AZStd::string_view prefabName, AzFramework::Spawnable& spawnable, const AzToolsFramework::Prefab::Instance& instance);
virtual ProcessedObjectStoreContainer& GetProcessedObjects();
virtual const ProcessedObjectStoreContainer& GetProcessedObjects() const;
@ -54,13 +122,19 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
virtual const AZ::PlatformTagSet& GetPlatformTags() const;
virtual const AZ::Uuid& GetSourceUuid() const;
virtual void ResolveLinks();
virtual bool HasCompletedSuccessfully() const;
virtual void ErrorEncountered();
protected:
using NamedPrefabContainer = AZStd::unordered_map<AZStd::string, PrefabDom>;
using SpawnableEntityAliasStore = AZStd::vector<EntityAliasStore>;
AZ::Data::AssetLoadBehavior ToAssetLoadBehavior(EntityAliasSpawnableLoadBehavior loadBehavior) const;
NamedPrefabContainer m_prefabs;
SpawnableEntityAliasStore m_entityAliases;
ProcessedObjectStoreContainer m_products;
ProductAssetDependencyContainer m_registeredProductAssetDependencies;

@ -11,7 +11,16 @@
namespace AzToolsFramework::Prefab::PrefabConversionUtils
{
ProcessedObjectStore::ProcessedObjectStore(AZStd::string uniqueId, AZStd::unique_ptr<AZ::Data::AssetData> asset, SerializerFunction assetSerializer)
void ProcessedObjectStore::AssetSmartPtrDeleter::operator()(AZ::Data::AssetData* asset)
{
if (asset->GetUseCount() == 0)
{
// Only delete the asset if it wasn't turned into a full asset
delete asset;
}
}
ProcessedObjectStore::ProcessedObjectStore(AZStd::string uniqueId, AssetSmartPtr asset, SerializerFunction assetSerializer)
: m_uniqueId(AZStd::move(uniqueId))
, m_assetSerializer(AZStd::move(assetSerializer))
, m_asset(AZStd::move(asset))
@ -62,7 +71,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
return m_referencedAssets;
}
AZStd::unique_ptr<AZ::Data::AssetData> ProcessedObjectStore::ReleaseAsset()
auto ProcessedObjectStore::ReleaseAsset() -> AssetSmartPtr
{
return AZStd::move(m_asset);
}
@ -72,6 +81,11 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
return AzFramework::SpawnableAssetHandler::BuildSubId(id);
}
uint32_t ProcessedObjectStore::GetSubId() const
{
return m_asset->GetId().m_subId;
}
const AZStd::string& ProcessedObjectStore::GetId() const
{
return m_uniqueId;

@ -26,6 +26,12 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
public:
using SerializerFunction = AZStd::function<bool(AZStd::vector<uint8_t>&, const ProcessedObjectStore&)>;
struct AssetSmartPtrDeleter
{
void operator()(AZ::Data::AssetData* asset);
};
using AssetSmartPtr = AZStd::unique_ptr<AZ::Data::AssetData, AssetSmartPtrDeleter>;
//! Constructs a new instance.
//! @param uniqueId A name for the object that's unique within the scope of the Prefab. This name will be used to generate a sub id for the product
//! which requires that the name to be stable between runs.
@ -37,24 +43,24 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
bool Serialize(AZStd::vector<uint8_t>& output) const;
static uint32_t BuildSubId(AZStd::string_view id);
uint32_t GetSubId() const;
bool HasAsset() const;
const AZ::Data::AssetType& GetAssetType() const;
const AZ::Data::AssetData& GetAsset() const;
AZ::Data::AssetData& GetAsset();
AZStd::unique_ptr<AZ::Data::AssetData> ReleaseAsset();
AssetSmartPtr ReleaseAsset();
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& GetReferencedAssets();
const AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& GetReferencedAssets() const;
const AZStd::string& GetId() const;
private:
ProcessedObjectStore(AZStd::string uniqueId, AZStd::unique_ptr<AZ::Data::AssetData> asset, SerializerFunction assetSerializer);
ProcessedObjectStore(AZStd::string uniqueId, AssetSmartPtr asset, SerializerFunction assetSerializer);
SerializerFunction m_assetSerializer;
AZStd::unique_ptr<AZ::Data::AssetData> m_asset;
AssetSmartPtr m_asset;
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>> m_referencedAssets;
AZStd::string m_uniqueId;
};
@ -66,7 +72,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils
static_assert(AZStd::is_base_of_v<AZ::Data::AssetData, T>,
"ProcessedObjectStore can only be created from a class that derives from AZ::Data::AssetData.");
AZ::Data::AssetId assetId(sourceId, BuildSubId(uniqueId));
auto instance = AZStd::make_unique<T>(assetId, AZ::Data::AssetData::AssetStatus::Ready);
auto instance = AssetSmartPtr(aznew T(assetId, AZ::Data::AssetData::AssetStatus::Ready));
ProcessedObjectStore resultLeft(AZStd::move(uniqueId), AZStd::move(instance), AZStd::move(assetSerializer));
T* resultRight = static_cast<T*>(&resultLeft.GetAsset());
return AZStd::make_pair<ProcessedObjectStore, T*>(AZStd::move(resultLeft), resultRight);

@ -8,9 +8,12 @@
#include <AzToolsFramework/Prefab/Spawnable/SpawnableUtils.h>
#include <AzCore/Casting/numeric_cast.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Component/EntityUtils.h>
#include <AzCore/Component/TransformBus.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/algorithm.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzFramework/Spawnable/Spawnable.h>
@ -20,18 +23,134 @@
namespace AzToolsFramework::Prefab::SpawnableUtils
{
namespace Internal
{
AZ::SerializeContext* GetSerializeContext()
{
AZ::SerializeContext* result = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(result, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
AZ_Assert(result, "SpawnbleUtils was unable to locate the Serialize Context.");
return result;
}
AZ::Entity* FindEntity(AZ::EntityId entityId, AzToolsFramework::Prefab::Instance& source)
{
AZ::Entity* result = nullptr;
source.GetEntities(
[&result, entityId](AZStd::unique_ptr<AZ::Entity>& entity)
{
if (entity->GetId() != entityId)
{
return true;
}
else
{
result = entity.get();
return false;
}
});
return result;
}
AZ::Entity* FindEntity(AZ::EntityId entityId, AzFramework::Spawnable& source)
{
uint32_t index = AzToolsFramework::Prefab::SpawnableUtils::FindEntityIndex(entityId, source);
return index != InvalidEntityIndex ? source.GetEntities()[index].get() : nullptr;
}
template<typename T>
AZStd::unique_ptr<AZ::Entity> CloneEntity(AZ::EntityId entityId, T& source)
{
AZ::Entity* target = Internal::FindEntity(entityId, source);
AZ_Assert(
target, "SpawnbleUtils were unable to locate entity with id %zu in Instance or Spawnable for cloning.",
aznumeric_cast<AZ::u64>(entityId));
auto clone = AZStd::make_unique<AZ::Entity>();
static AZ::SerializeContext* sc = GetSerializeContext();
sc->CloneObjectInplace(*clone, target);
clone->SetId(AZ::Entity::MakeId());
return clone;
}
AZStd::unique_ptr<AZ::Entity> ReplaceEntityWithPlaceholder(AZ::EntityId entityId, AzToolsFramework::Prefab::Instance& source)
{
auto&& [instance, alias] = source.FindInstanceAndAlias(entityId);
AZ_Assert(
instance, "SpawnbleUtils were unable to locate entity alias with id %zu in Instance '%s' for replacing.",
aznumeric_cast<AZ::u64>(entityId), source.GetTemplateSourcePath().c_str());
EntityOptionalReference entityData = instance->GetEntity(alias);
AZ_Assert(
entityData.has_value(), "SpawnbleUtils were unable to locate entity '%.*s' in Instance '%s' for replacing.",
AZ_STRING_ARG(alias), source.GetTemplateSourcePath().c_str());
auto placeholder = AZStd::make_unique<AZ::Entity>(entityData->get().GetId(), entityData->get().GetName());
return instance->ReplaceEntity(AZStd::move(placeholder), alias);
}
AZStd::unique_ptr<AZ::Entity> ReplaceEntityWithPlaceholder(AZ::EntityId entityId, AzFramework::Spawnable& source)
{
uint32_t index = AzToolsFramework::Prefab::SpawnableUtils::FindEntityIndex(entityId, source);
AZ_Assert(
index != InvalidEntityIndex, "SpawnbleUtils were unable to locate entity alias with id %zu in Spawnable for replacing.",
aznumeric_cast<AZ::u64>(entityId));
AZStd::unique_ptr<AZ::Entity> original = AZStd::move(source.GetEntities()[index]);
AZ_Assert(
original, "SpawnbleUtils were unable to locate entity with id %zu in Spawnable for replacing.",
aznumeric_cast<AZ::u64>(entityId));
source.GetEntities()[index] = AZStd::make_unique<AZ::Entity>(original->GetId(), original->GetName());
return original;
}
template<typename Source>
AZStd::pair<AZStd::unique_ptr<AZ::Entity>, AzFramework::Spawnable::EntityAliasType> ApplyAlias(
Source& source, AZ::EntityId entityId, AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType)
{
namespace PCU = AzToolsFramework::Prefab::PrefabConversionUtils;
using ResultPair = AZStd::pair<AZStd::unique_ptr<AZ::Entity>, AzFramework::Spawnable::EntityAliasType>;
switch (aliasType)
{
case PCU::EntityAliasType::Disable:
// No need to do anything as the alias is disabled.
return ResultPair(nullptr, AzFramework::Spawnable::EntityAliasType::Disable);
case PCU::EntityAliasType::OptionalReplace:
return ResultPair(CloneEntity(entityId, source), AzFramework::Spawnable::EntityAliasType::Replace);
case PCU::EntityAliasType::Replace:
return ResultPair(ReplaceEntityWithPlaceholder(entityId, source), AzFramework::Spawnable::EntityAliasType::Replace);
case PCU::EntityAliasType::Additional:
ResultPair(AZStd::make_unique<AZ::Entity>(AZ::Entity::MakeId()), AzFramework::Spawnable::EntityAliasType::Additional);
case PCU::EntityAliasType::Merge:
// Use the same entity id as the original entity so at runtime the entity ids can be verified to match.
ResultPair(AZStd::make_unique<AZ::Entity>(entityId), AzFramework::Spawnable::EntityAliasType::Merge);
default:
AZ_Assert(
false, "Invalid PrefabProcessorContext::EntityAliasType type (%i) provided.", aznumeric_cast<uint64_t>(aliasType));
return ResultPair(nullptr, AzFramework::Spawnable::EntityAliasType::Disable);
}
}
}
bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom)
{
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>> referencedAssets;
return CreateSpawnable(spawnable, prefabDom, referencedAssets);
}
bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom, AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& referencedAssets)
bool CreateSpawnable(
AzFramework::Spawnable& spawnable,
const PrefabDom& prefabDom,
AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& referencedAssets)
{
Instance instance;
if (Prefab::PrefabDomUtils::LoadInstanceFromPrefabDom(instance, prefabDom, referencedAssets,
Prefab::PrefabDomUtils::LoadFlags::AssignRandomEntityId)) // Always assign random entity ids because the spawnable is
// going to be used to create clones of the entities.
if (Prefab::PrefabDomUtils::LoadInstanceFromPrefabDom(
instance, prefabDom, referencedAssets,
Prefab::PrefabDomUtils::LoadFlags::AssignRandomEntityId)) // Always assign random entity ids because the spawnable is
// going to be used to create clones of the entities.
{
AzFramework::Spawnable::EntityList& entities = spawnable.GetEntities();
instance.DetachAllEntitiesInHierarchy(
@ -47,6 +166,144 @@ namespace AzToolsFramework::Prefab::SpawnableUtils
}
}
AZ::Entity* CreateEntityAlias(
AZStd::string sourcePrefabName,
AzToolsFramework::Prefab::Instance& source,
AZStd::string targetPrefabName,
AzToolsFramework::Prefab::Instance& target,
AZ::EntityId entityId,
AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType,
AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior,
uint32_t tag,
AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext& context)
{
using namespace AzToolsFramework::Prefab::PrefabConversionUtils;
AliasPath alias = source.GetAliasPathRelativeToInstance(entityId);
if (!alias.empty())
{
auto&& [replacement, storedAliasType] = Internal::ApplyAlias(source, entityId, aliasType);
if (replacement)
{
AZ::Entity* result = replacement.get();
target.AddEntity(AZStd::move(replacement), alias.Filename().Native());
EntityAliasStore store;
store.m_aliasType = storedAliasType;
store.m_source.emplace<EntityAliasPrefabLink>(AZStd::move(sourcePrefabName), AZStd::move(alias));
store.m_target.emplace<EntityAliasPrefabLink>(
AZStd::move(targetPrefabName), target.GetAliasPathRelativeToInstance(result->GetId()));
store.m_loadBehavior = loadBehavior;
store.m_tag = tag;
context.RegisterSpawnableEntityAlias(AZStd::move(store));
return result;
}
else
{
AZ_Assert(false, "A replacement for entity with id %zu could not be created.", static_cast<AZ::u64>(entityId));
return nullptr;
}
}
else
{
AZ_Assert(false, "Entity with id %zu was not found in the source prefab.", static_cast<AZ::u64>(entityId));
return nullptr;
}
}
AZ::Entity* CreateEntityAlias(
AZStd::string sourcePrefabName,
AzToolsFramework::Prefab::Instance& source,
AzFramework::Spawnable& target,
AZ::EntityId entityId,
AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType,
AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior,
uint32_t tag,
AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext& context)
{
using namespace AzToolsFramework::Prefab::PrefabConversionUtils;
AliasPath alias = source.GetAliasPathRelativeToInstance(entityId);
if (!alias.empty())
{
auto&& [replacement, storedAliasType] = Internal::ApplyAlias(source, entityId, aliasType);
if (replacement)
{
AZ::Entity* result = replacement.get();
target.GetEntities().push_back(AZStd::move(replacement));
EntityAliasStore store;
store.m_aliasType = storedAliasType;
store.m_source.emplace<EntityAliasPrefabLink>(AZStd::move(sourcePrefabName), AZStd::move(alias));
store.m_target.emplace<EntityAliasSpawnableLink>(target, result->GetId());
store.m_tag = tag;
store.m_loadBehavior = loadBehavior;
context.RegisterSpawnableEntityAlias(AZStd::move(store));
return result;
}
else
{
AZ_Assert(false, "A replacement for entity with id %zu could not be created.", static_cast<AZ::u64>(entityId));
return nullptr;
}
}
else
{
AZ_Assert(false, "Entity with id %llu was not found in the source prefab.", static_cast<AZ::u64>(entityId));
return nullptr;
}
}
AZ::Entity* CreateEntityAlias(
AzFramework::Spawnable& source,
AzFramework::Spawnable& target,
AZ::EntityId entityId,
AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType,
AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior,
uint32_t tag,
AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext& context)
{
using namespace AzToolsFramework::Prefab::PrefabConversionUtils;
auto&& [replacement, storedAliasType] = Internal::ApplyAlias(source, entityId, aliasType);
if (replacement)
{
AZ::Entity* result = replacement.get();
target.GetEntities().push_back(AZStd::move(replacement));
EntityAliasStore store;
store.m_aliasType = storedAliasType;
store.m_source.emplace<EntityAliasSpawnableLink>(source, entityId);
store.m_target.emplace<EntityAliasSpawnableLink>(target, result->GetId());
store.m_tag = tag;
store.m_loadBehavior = loadBehavior;
context.RegisterSpawnableEntityAlias(AZStd::move(store));
return result;
}
else
{
AZ_Assert(false, "A replacement for entity with id %zu could not be created.", static_cast<AZ::u64>(entityId));
return nullptr;
}
}
uint32_t FindEntityIndex(AZ::EntityId entity, const AzFramework::Spawnable& spawnable)
{
auto begin = spawnable.GetEntities().begin();
auto end = spawnable.GetEntities().end();
for(auto it = begin; it != end; ++it)
{
if ((*it)->GetId() == entity)
{
return aznumeric_caster(AZStd::distance(begin, it));
}
}
return InvalidEntityIndex;
}
template<typename EntityPtr>
void OrganizeEntitiesForSorting(
AZStd::vector<EntityPtr>& entities,

@ -8,14 +8,59 @@
#pragma once
#include <AzCore/Component/EntityId.h>
#include <AzCore/std/limits.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzToolsFramework/Prefab/PrefabDomTypes.h>
#include <AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h>
namespace AZ
{
class Entity;
}
namespace AzToolsFramework::Prefab
{
class Instance;
}
namespace AzToolsFramework::Prefab::SpawnableUtils
{
static constexpr uint32_t InvalidEntityIndex = AZStd::numeric_limits<uint32_t>::max();
bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom);
bool CreateSpawnable(AzFramework::Spawnable& spawnable, const PrefabDom& prefabDom, AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& referencedAssets);
AZ::Entity* CreateEntityAlias(
AZStd::string sourcePrefabName,
AzToolsFramework::Prefab::Instance& source,
AZStd::string targetPrefabName,
AzToolsFramework::Prefab::Instance& target,
AZ::EntityId entityId,
AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType,
AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior,
uint32_t tag,
AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext& context);
AZ::Entity* CreateEntityAlias(
AZStd::string sourcePrefabName,
AzToolsFramework::Prefab::Instance& source,
AzFramework::Spawnable& target,
AZ::EntityId entityId,
AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType,
AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior,
uint32_t tag,
AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext& context);
AZ::Entity* CreateEntityAlias(
AzFramework::Spawnable& source,
AzFramework::Spawnable& target,
AZ::EntityId entityId,
AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasType aliasType,
AzToolsFramework::Prefab::PrefabConversionUtils::EntityAliasSpawnableLoadBehavior loadBehavior,
uint32_t tag,
AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext& context);
uint32_t FindEntityIndex(AZ::EntityId entity, const AzFramework::Spawnable& spawnable);
void SortEntitiesByTransformHierarchy(AzFramework::Spawnable& spawnable);
template <typename EntityPtr>

@ -0,0 +1,344 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzTest/AzTest.h>
#include <AzToolsFramework/AssetBrowser/AssetBrowserComponent.h>
#include <AzToolsFramework/AssetBrowser/AssetBrowserFilterModel.h>
#include <AzToolsFramework/AssetBrowser/AssetBrowserModel.h>
#include <AzToolsFramework/AssetBrowser/AssetBrowserTableModel.h>
#include <AzToolsFramework/AssetBrowser/Entries/AssetBrowserEntry.h>
#include <AzToolsFramework/AssetBrowser/Entries/FolderAssetBrowserEntry.h>
#include <AzToolsFramework/AssetBrowser/Entries/ProductAssetBrowserEntry.h>
#include <AzToolsFramework/AssetBrowser/Entries/RootAssetBrowserEntry.h>
#include <AzToolsFramework/AssetBrowser/Entries/SourceAssetBrowserEntry.h>
#include <AzToolsFramework/AssetBrowser/Search/SearchWidget.h>
#include <AzToolsFramework/AssetDatabase/AssetDatabaseConnection.h>
#include <AzToolsFramework/Entity/EditorEntityContextComponent.h>
#include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
#include <QAbstractItemModelTester>
namespace UnitTest
{
// Test fixture for the AssetBrowser model that uses a QAbstractItemModelTester to validate the state of the model
// when QAbstractItemModel signals fire. Tests will exit with a fatal error if an invalid state is detected.
class AssetBrowserTest
: public ToolsApplicationFixture
, public testing::WithParamInterface<const char*>
{
protected:
enum class AssetEntryType
{
Root,
Folder,
Source,
Product
};
enum class FolderType
{
Root,
File
};
void SetUpEditorFixtureImpl() override;
void TearDownEditorFixtureImpl() override;
//! Creates a Mock Scan Folder
void AddScanFolder(AZ::s64 folderID, AZStd::string folderPath, AZStd::string displayName, FolderType folderType = FolderType::File);
//! Creates a Source entry from a mock file
AZ::Uuid CreateSourceEntry(
AZ::s64 fileID, AZ::s64 parentFolderID, AZStd::string filename, AssetEntryType sourceType = AssetEntryType::Source);
//! Creates a product from a given sourceEntry
void CreateProduct(AZ::s64 productID, AZ::Uuid sourceUuid, AZStd::string productName);
void SetupAssetBrowser();
void PrintModel(const QAbstractItemModel* model, AZStd::function<void(const QString&)> printer);
QModelIndex GetModelIndex(const QAbstractItemModel* model, int targetDepth, int row = 0);
AZStd::shared_ptr<AzToolsFramework::AssetBrowser::RootAssetBrowserEntry> GetRootEntry();
AZStd::vector<QString> GetVectorFromFormattedString(const QString& formattedString);
protected:
QString m_assetBrowserHierarchy = QString();
AZStd::unique_ptr<AzToolsFramework::AssetBrowser::SearchWidget> m_searchWidget;
AZStd::unique_ptr<AzToolsFramework::AssetBrowser::AssetBrowserComponent> m_assetBrowserComponent;
AZStd::unique_ptr<AzToolsFramework::AssetBrowser::AssetBrowserFilterModel> m_filterModel;
AZStd::unique_ptr<AzToolsFramework::AssetBrowser::AssetBrowserTableModel> m_tableModel;
AZStd::unique_ptr<QAbstractItemModelTester> m_modelTesterAssetBrowser;
AZStd::unique_ptr<QAbstractItemModelTester> m_modelTesterFilterModel;
AZStd::unique_ptr<QAbstractItemModelTester> m_modelTesterTableModel;
QVector<int> m_folderIds = { 13, 14, 15 };
QVector<int> m_sourceIDs = { 1, 2, 3, 4, 5 };
QVector<int> m_productIDs = { 1, 2, 3, 4, 5 };
};
void AssetBrowserTest::SetUpEditorFixtureImpl()
{
GetApplication()->RegisterComponentDescriptor(AzToolsFramework::EditorEntityContextComponent::CreateDescriptor());
m_assetBrowserComponent = AZStd::make_unique<AzToolsFramework::AssetBrowser::AssetBrowserComponent>();
m_assetBrowserComponent->Activate();
m_filterModel = AZStd::make_unique<AzToolsFramework::AssetBrowser::AssetBrowserFilterModel>();
m_tableModel = AZStd::make_unique<AzToolsFramework::AssetBrowser::AssetBrowserTableModel>();
m_filterModel->setSourceModel(m_assetBrowserComponent->GetAssetBrowserModel());
m_tableModel->setSourceModel(m_filterModel.get());
m_modelTesterAssetBrowser = AZStd::make_unique<QAbstractItemModelTester>(m_assetBrowserComponent->GetAssetBrowserModel());
m_modelTesterFilterModel = AZStd::make_unique<QAbstractItemModelTester>(m_filterModel.get());
m_modelTesterTableModel = AZStd::make_unique<QAbstractItemModelTester>(m_tableModel.get());
m_searchWidget = AZStd::make_unique<AzToolsFramework::AssetBrowser::SearchWidget>();
// Setup String filters
m_searchWidget->Setup(true, true);
m_filterModel->SetFilter(m_searchWidget->GetFilter());
SetupAssetBrowser();
}
void AssetBrowserTest::TearDownEditorFixtureImpl()
{
m_modelTesterAssetBrowser.reset();
m_modelTesterFilterModel.reset();
m_modelTesterTableModel.reset();
m_tableModel.reset();
m_filterModel.reset();
m_assetBrowserComponent->Deactivate();
m_assetBrowserComponent.reset();
m_searchWidget.reset();
}
void AssetBrowserTest::AddScanFolder(
AZ::s64 folderID, AZStd::string folderPath, AZStd::string displayName, FolderType folderType /*= FolderType::File*/)
{
AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry scanFolder = AzToolsFramework::AssetDatabase::ScanFolderDatabaseEntry();
scanFolder.m_scanFolderID = folderID;
scanFolder.m_scanFolder = folderPath;
scanFolder.m_displayName = displayName;
scanFolder.m_isRoot = folderType == FolderType::Root;
GetRootEntry()->AddScanFolder(scanFolder);
}
AZ::Uuid AssetBrowserTest::CreateSourceEntry(
AZ::s64 fileID, AZ::s64 parentFolderID, AZStd::string filename, AssetEntryType sourceType /*= AssetEntryType::Source*/)
{
AzToolsFramework::AssetDatabase::FileDatabaseEntry entry = AzToolsFramework::AssetDatabase::FileDatabaseEntry();
entry.m_scanFolderPK = parentFolderID;
entry.m_fileID = fileID;
entry.m_fileName = filename;
entry.m_isFolder = sourceType == AssetEntryType::Folder;
GetRootEntry()->AddFile(entry);
if (!entry.m_isFolder)
{
AzToolsFramework::AssetBrowser::SourceWithFileID entrySource = AzToolsFramework::AssetBrowser::SourceWithFileID();
entrySource.first = entry.m_fileID;
entrySource.second = AzToolsFramework::AssetDatabase::SourceDatabaseEntry();
entrySource.second.m_scanFolderPK = parentFolderID;
entrySource.second.m_sourceName = filename;
entrySource.second.m_sourceID = fileID;
entrySource.second.m_sourceGuid = AZ::Uuid::CreateRandom();
GetRootEntry()->AddSource(entrySource);
return entrySource.second.m_sourceGuid;
}
return AZ::Uuid::CreateNull();
}
void AssetBrowserTest::CreateProduct(AZ::s64 productID, AZ::Uuid sourceUuid, AZStd::string productName)
{
AzToolsFramework::AssetBrowser::ProductWithUuid product = AzToolsFramework::AssetBrowser::ProductWithUuid();
product.first = sourceUuid;
product.second = AzToolsFramework::AssetDatabase::ProductDatabaseEntry();
product.second.m_productID = productID;
product.second.m_subID = aznumeric_cast<AZ::u32>(productID);
product.second.m_productName = productName;
GetRootEntry()->AddProduct(product);
}
void AssetBrowserTest::SetupAssetBrowser()
{
// RootEntries : 1 | Folders : 4 | SourceEntries : 5 | ProductEntries : 9
m_assetBrowserHierarchy = R"(
D:
\
dev
o3de
GameProject
Assets
Source_1
Product_1_1
Product_1_0
Source_0
Product_0_3
Product_0_2
Product_0_1
Product_0_0
Scripts
Source_3
Source_2
Product_2_2
Product_2_1
Product_2_0
Misc
Source_4
Product_4_2
Product_4_1
Product_4_0 )";
namespace AzAssetBrowser = AzToolsFramework::AssetBrowser;
AddScanFolder(m_folderIds.at(2), "D:/dev/o3de/GameProject/Misc", "Misc");
AZ::Uuid sourceUuid_4 = CreateSourceEntry(m_sourceIDs.at(4), m_folderIds.at(2), "Source_4");
CreateProduct(m_productIDs.at(0), sourceUuid_4, "Product_4_0");
CreateProduct(m_productIDs.at(1), sourceUuid_4, "Product_4_1");
CreateProduct(m_productIDs.at(2), sourceUuid_4, "Product_4_2");
AddScanFolder(m_folderIds.at(1), "D:/dev/o3de/GameProject/Scripts", "Scripts");
AZ::Uuid sourceUuid_2 = CreateSourceEntry(m_sourceIDs.at(2), m_folderIds.at(1), "Source_2");
CreateProduct(m_productIDs.at(0), sourceUuid_2, "Product_2_0");
CreateProduct(m_productIDs.at(1), sourceUuid_2, "Product_2_1");
CreateProduct(m_productIDs.at(2), sourceUuid_2, "Product_2_2");
CreateSourceEntry(m_sourceIDs.at(3), m_folderIds.at(1), "Source_3");
AddScanFolder(m_folderIds.at(0), "D:/dev/o3de/GameProject/Assets", "Assets");
AZ::Uuid sourceUuid_0 = CreateSourceEntry(m_sourceIDs.at(0), m_folderIds.at(0), "Source_0");
CreateProduct(m_productIDs.at(0), sourceUuid_0, "Product_0_0");
CreateProduct(m_productIDs.at(1), sourceUuid_0, "Product_0_1");
CreateProduct(m_productIDs.at(2), sourceUuid_0, "Product_0_2");
CreateProduct(m_productIDs.at(3), sourceUuid_0, "Product_0_3");
AZ::Uuid sourceUuid_1 = CreateSourceEntry(m_sourceIDs.at(1), m_folderIds.at(0), "Source_1");
CreateProduct(m_productIDs.at(0), sourceUuid_1, "Product_1_0");
CreateProduct(m_productIDs.at(1), sourceUuid_1, "Product_1_1");
}
void AssetBrowserTest::PrintModel(const QAbstractItemModel* model, AZStd::function<void(const QString&)> printer)
{
AZStd::deque<AZStd::pair<QModelIndex, int>> indices;
indices.push_back({ model->index(0, 0), 0 });
while (!indices.empty())
{
auto [index, depth] = indices.front();
indices.pop_front();
QString indentString;
for (int i = 0; i < depth; ++i)
{
indentString += " ";
}
const QString message = indentString + index.data(Qt::DisplayRole).toString();
printer(message);
for (int i = 0; i < model->rowCount(index); ++i)
{
indices.emplace_front(model->index(i, 0, index), depth + 1);
}
}
}
QModelIndex AssetBrowserTest::GetModelIndex(const QAbstractItemModel* model, int targetDepth, int row)
{
AZStd::deque<AZStd::pair<QModelIndex, int>> indices;
indices.push_back({ model->index(0, 0), 0 });
while (!indices.empty())
{
auto [index, depth] = indices.front();
indices.pop_front();
for (int i = 0; i < model->rowCount(index); ++i)
{
if (depth + 1 == targetDepth && row == i)
{
return model->index(i, 0, index);
}
indices.emplace_front(model->index(i, 0, index), depth + 1);
}
}
return QModelIndex();
}
AZStd::shared_ptr<AzToolsFramework::AssetBrowser::RootAssetBrowserEntry> AssetBrowserTest::GetRootEntry()
{
return m_assetBrowserComponent->GetAssetBrowserModel()->GetRootEntry();
}
AZStd::vector<QString> AssetBrowserTest::GetVectorFromFormattedString(const QString& formattedString)
{
AZStd::vector<QString> hierarchySections;
QStringList splittedList = formattedString.split('\n', Qt::SkipEmptyParts);
for (auto& str : splittedList)
{
str.replace(" ", "");
hierarchySections.push_back(str);
}
return hierarchySections;
}
TEST_F(AssetBrowserTest, CheckCorrectNumberOfEntriesInTableView)
{
m_filterModel->FilterUpdatedSlotImmediate();
const int tableViewRowcount = m_tableModel->rowCount();
// RowCount should be 17 -> 5 SourceEntries + 12 ProductEntries)
EXPECT_EQ(tableViewRowcount, 17);
}
TEST_F(AssetBrowserTest, CheckCorrectNumberOfEntriesInTableViewAfterStringFilter)
{
/*
*-Source_1
* |
* |-product_1_0
* |-product_1_1
*
*
* Matching entries = 3
*/
// Apply string filter
m_searchWidget->SetTextFilter(QString("source_1"));
m_filterModel->FilterUpdatedSlotImmediate();
const int tableViewRowcount = m_tableModel->rowCount();
EXPECT_EQ(tableViewRowcount, 3);
}
TEST_F(AssetBrowserTest, CheckScanFolderAddition)
{
EXPECT_EQ(m_assetBrowserComponent->GetAssetBrowserModel()->rowCount(), 1);
const int newFolderId = 20;
AddScanFolder(newFolderId, "E:/TestFolder/TestFolder2", "TestFolder");
// Since the folder is empty it shouldn't be added to the model.
EXPECT_EQ(m_assetBrowserComponent->GetAssetBrowserModel()->rowCount(), 1);
CreateSourceEntry(123, newFolderId, "DummyFile");
// When we add a file to the folder it should be added to the model
EXPECT_EQ(m_assetBrowserComponent->GetAssetBrowserModel()->rowCount(), 2);
}
} // namespace UnitTest

@ -82,8 +82,8 @@ namespace UnitTest
}
auto transform = aznew AzToolsFramework::Components::TransformComponent;
transform->SetParent(parentId);
entity->AddComponent(transform);
transform->SetParent(parentId);
entity->Activate();

@ -126,6 +126,7 @@ set(FILES
UI/EntityIdQLineEditTests.cpp
UI/EntityOutlinerTests.cpp
UI/EntityPropertyEditorTests.cpp
UI/AssetBrowserTests.cpp
UndoStack.cpp
Viewport/ClusterTests.cpp
Viewport/ViewportEditorModeTests.cpp

@ -25,7 +25,6 @@
#include <AzGameFramework/Application/GameApplication.h>
#include <CryLibrary.h>
#include <ISystem.h>
#include <ITimer.h>
#include <LegacyAllocator.h>
@ -80,146 +79,6 @@ namespace
}
}
#if AZ_TRAIT_LAUNCHER_USE_CRY_DYNAMIC_MODULE_HANDLE
// mimics AZ::DynamicModuleHandle but uses CryLibrary under the hood,
// which is necessary to properly load legacy Cry libraries on some platforms
class DynamicModuleHandle
{
public:
AZ_CLASS_ALLOCATOR(DynamicModuleHandle, AZ::OSAllocator, 0)
static AZStd::unique_ptr<DynamicModuleHandle> Create(const char* fullFileName)
{
return AZStd::unique_ptr<DynamicModuleHandle>(aznew DynamicModuleHandle(fullFileName));
}
DynamicModuleHandle(const DynamicModuleHandle&) = delete;
DynamicModuleHandle& operator=(const DynamicModuleHandle&) = delete;
~DynamicModuleHandle()
{
Unload();
}
// argument is strictly to match the API of AZ::DynamicModuleHandle
bool Load(bool unused)
{
AZ_UNUSED(unused);
if (IsLoaded())
{
return true;
}
m_moduleHandle = CryLoadLibrary(m_fileName.c_str());
return IsLoaded();
}
bool Unload()
{
if (!IsLoaded())
{
return false;
}
return CryFreeLibrary(m_moduleHandle);
}
bool IsLoaded() const
{
return m_moduleHandle != nullptr;
}
const AZ::OSString& GetFilename() const
{
return m_fileName;
}
template<typename Function>
Function GetFunction(const char* functionName) const
{
if (IsLoaded())
{
return reinterpret_cast<Function>(CryGetProcAddress(m_moduleHandle, functionName));
}
else
{
return nullptr;
}
}
private:
DynamicModuleHandle(const char* fileFullName)
: m_fileName()
, m_moduleHandle(nullptr)
{
m_fileName = AZ::OSString::format("%s%s%s",
CrySharedLibraryPrefix, fileFullName, CrySharedLibraryExtension);
}
AZ::OSString m_fileName;
HMODULE m_moduleHandle;
};
#else
// mimics AZ::DynamicModuleHandle but also calls InjectEnvironmentFunction on
// the loaded module which is necessary to properly load legacy Cry libraries
class DynamicModuleHandle
{
public:
AZ_CLASS_ALLOCATOR(DynamicModuleHandle, AZ::OSAllocator, 0);
static AZStd::unique_ptr<DynamicModuleHandle> Create(const char* fullFileName)
{
return AZStd::unique_ptr<DynamicModuleHandle>(aznew DynamicModuleHandle(fullFileName));
}
bool Load(bool isInitializeFunctionRequired)
{
const bool loaded = m_moduleHandle->Load(isInitializeFunctionRequired);
if (loaded)
{
// We need to inject the environment first thing so that allocators are available immediately
InjectEnvironmentFunction injectEnv = GetFunction<InjectEnvironmentFunction>(INJECT_ENVIRONMENT_FUNCTION);
if (injectEnv)
{
auto env = AZ::Environment::GetInstance();
injectEnv(env);
}
}
return loaded;
}
bool Unload()
{
bool unloaded = m_moduleHandle->Unload();
if (unloaded)
{
DetachEnvironmentFunction detachEnv = GetFunction<DetachEnvironmentFunction>(DETACH_ENVIRONMENT_FUNCTION);
if (detachEnv)
{
detachEnv();
}
}
return unloaded;
}
template<typename Function>
Function GetFunction(const char* functionName) const
{
return m_moduleHandle->GetFunction<Function>(functionName);
}
private:
DynamicModuleHandle(const char* fileFullName)
: m_moduleHandle(AZ::DynamicModuleHandle::Create(fileFullName))
{
}
AZStd::unique_ptr<AZ::DynamicModuleHandle> m_moduleHandle;
};
#endif // AZ_TRAIT_LAUNCHER_USE_CRY_DYNAMIC_MODULE_HANDLE
void RunMainLoop(AzGameFramework::GameApplication& gameApplication)
{
// Ideally we'd just call GameApplication::RunMainLoop instead, but
@ -649,13 +508,13 @@ namespace O3DELauncher
// Create CrySystem.
#if !defined(AZ_MONOLITHIC_BUILD)
AZStd::unique_ptr<DynamicModuleHandle> crySystemLibrary;
PFNCREATESYSTEMINTERFACE CreateSystemInterface = nullptr;
crySystemLibrary = DynamicModuleHandle::Create("CrySystem");
if (crySystemLibrary->Load(false))
constexpr const char* crySystemLibraryName = AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX "CrySystem" AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION;
AZStd::unique_ptr<AZ::DynamicModuleHandle> crySystemLibrary = AZ::DynamicModuleHandle::Create(crySystemLibraryName);
if (crySystemLibrary->Load(true))
{
CreateSystemInterface = crySystemLibrary->GetFunction<PFNCREATESYSTEMINTERFACE>("CreateSystemInterface");
PFNCREATESYSTEMINTERFACE CreateSystemInterface =
crySystemLibrary->GetFunction<PFNCREATESYSTEMINTERFACE>("CreateSystemInterface");
if (CreateSystemInterface)
{
systemInitParams.pSystem = CreateSystemInterface(systemInitParams);

@ -12,8 +12,6 @@
#include <AzCore/IO/SystemFile.h> // for AZ_MAX_PATH_LEN
#include <AzCore/Math/Vector2.h>
#include <CryLibrary.h>
#include <execinfo.h>
#include <libgen.h>
#include <netdb.h>
@ -86,17 +84,6 @@ int main(int argc, char** argv)
using namespace O3DELauncher;
#if !defined(AZ_MONOLITHIC_BUILD)
char exePath[AZ_MAX_PATH_LEN] = { 0 };
if (readlink("/proc/self/exe", exePath, AZ_MAX_PATH_LEN) == -1)
{
return static_cast<int>(ReturnCode::ErrExePath);
}
char* runDir = dirname(exePath);
SetModulePath(runDir);
#endif // !defined(AZ_MONOLITHIC_BUILD)
PlatformMainInfo mainInfo;
mainInfo.m_updateResourceLimits = IncreaseResourceLimits;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save