From f6fc425a1ac3b14feae2243c8484a7cd5157ce82 Mon Sep 17 00:00:00 2001 From: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> Date: Thu, 17 Jun 2021 16:24:33 -0500 Subject: [PATCH] Prefab support for dynamic vegetation (#1374) * First version of prefab support for dynamic vegetation * Addressed PR feedback - Made MockSpawnableEntitiesInterface a proper GMock in AzFramework - Added Get/SetSpawnableAssetId - Added lots of comments to better explain things that were asked about in the PR * Exposed AzFrameworkTestShared on all platforms, not just host platforms --- Code/Framework/Tests/CMakeLists.txt | 72 ++-- .../Mocks/MockSpawnableEntitiesInterface.h | 84 ++++ .../Tests/framework_shared_tests_files.cmake | 1 + Gems/Vegetation/Code/CMakeLists.txt | 1 + .../Vegetation/PrefabInstanceSpawner.h | 101 +++++ .../Code/Source/PrefabInstanceSpawner.cpp | 401 ++++++++++++++++++ .../Code/Source/VegetationSystemComponent.cpp | 2 + .../Code/Tests/PrefabInstanceSpawnerTests.cpp | 347 +++++++++++++++ Gems/Vegetation/Code/vegetation_files.cmake | 2 + .../Code/vegetation_tests_files.cmake | 1 + 10 files changed, 977 insertions(+), 35 deletions(-) create mode 100644 Code/Framework/Tests/Mocks/MockSpawnableEntitiesInterface.h create mode 100644 Gems/Vegetation/Code/Include/Vegetation/PrefabInstanceSpawner.h create mode 100644 Gems/Vegetation/Code/Source/PrefabInstanceSpawner.cpp create mode 100644 Gems/Vegetation/Code/Tests/PrefabInstanceSpawnerTests.cpp diff --git a/Code/Framework/Tests/CMakeLists.txt b/Code/Framework/Tests/CMakeLists.txt index 491f6d8e14..7cb243d69b 100644 --- a/Code/Framework/Tests/CMakeLists.txt +++ b/Code/Framework/Tests/CMakeLists.txt @@ -9,7 +9,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # -if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS) +if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) ly_get_list_relative_pal_filename(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME}) @@ -27,40 +27,42 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS) AZ::AzFramework ) - ly_add_target( - NAME ProcessLaunchTest EXECUTABLE - NAMESPACE AZ - FILES_CMAKE - process_launch_test_files.cmake - INCLUDE_DIRECTORIES - PRIVATE - . - BUILD_DEPENDENCIES - PRIVATE - AZ::AzCore - AZ::AzFramework - ) + if(PAL_TRAIT_BUILD_HOST_TOOLS) + ly_add_target( + NAME ProcessLaunchTest EXECUTABLE + NAMESPACE AZ + FILES_CMAKE + process_launch_test_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + . + BUILD_DEPENDENCIES + PRIVATE + AZ::AzCore + AZ::AzFramework + ) - ly_add_target( - NAME Framework.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} - NAMESPACE AZ - FILES_CMAKE - frameworktests_files.cmake - INCLUDE_DIRECTORIES - PRIVATE - . - ${pal_dir} - BUILD_DEPENDENCIES - PRIVATE - AZ::AzTest - AZ::AzToolsFramework - AZ::AzTestShared - AZ::AzFrameworkTestShared - RUNTIME_DEPENDENCIES - AZ::ProcessLaunchTest - ) - ly_add_googletest( - NAME AZ::Framework.Tests - ) + ly_add_target( + NAME Framework.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} + NAMESPACE AZ + FILES_CMAKE + frameworktests_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + . + ${pal_dir} + BUILD_DEPENDENCIES + PRIVATE + AZ::AzTest + AZ::AzToolsFramework + AZ::AzTestShared + AZ::AzFrameworkTestShared + RUNTIME_DEPENDENCIES + AZ::ProcessLaunchTest + ) + ly_add_googletest( + NAME AZ::Framework.Tests + ) + endif() endif() diff --git a/Code/Framework/Tests/Mocks/MockSpawnableEntitiesInterface.h b/Code/Framework/Tests/Mocks/MockSpawnableEntitiesInterface.h new file mode 100644 index 0000000000..51063a49be --- /dev/null +++ b/Code/Framework/Tests/Mocks/MockSpawnableEntitiesInterface.h @@ -0,0 +1,84 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ +#pragma once + +#include +#include +#include +#include + +namespace AzFramework +{ + class MockSpawnableEntitiesInterface; + using NiceSpawnableEntitiesInterfaceMock = ::testing::NiceMock; + + class MockSpawnableEntitiesInterface : public SpawnableEntitiesDefinition + { + public: + AZ_RTTI(MockSpawnableEntitiesInterface, "{2A20FF73-C445-4F32-ABB9-5CF0A5778404}", SpawnableEntitiesDefinition); + + MockSpawnableEntitiesInterface() + { + AZ::Interface::Register(this); + } + + virtual ~MockSpawnableEntitiesInterface() + { + AZ::Interface::Unregister(this); + } + + MOCK_METHOD2(SpawnAllEntities, void(EntitySpawnTicket& ticket, SpawnAllEntitiesOptionalArgs optionalArgs)); + + MOCK_METHOD3( + SpawnEntities, + void(EntitySpawnTicket& ticket, AZStd::vector entityIndices, SpawnEntitiesOptionalArgs optionalArgs)); + + MOCK_METHOD2(DespawnAllEntities, void(EntitySpawnTicket& ticket, DespawnAllEntitiesOptionalArgs optionalArgs)); + + MOCK_METHOD3( + ReloadSpawnable, + void(EntitySpawnTicket& ticket, AZ::Data::Asset spawnable, ReloadSpawnableOptionalArgs optionalArgs)); + + MOCK_METHOD3( + ListEntities, void(EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs)); + + MOCK_METHOD3( + ListIndicesAndEntities, + void(EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs)); + + MOCK_METHOD3( + ClaimEntities, + void(EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback, ClaimEntitiesOptionalArgs optionalArgs)); + + MOCK_METHOD3(Barrier, void(EntitySpawnTicket& ticket, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs)); + + MOCK_METHOD1(CreateTicket, AZStd::pair(AZ::Data::Asset&& spawnable)); + MOCK_METHOD1(DestroyTicket, void(void* ticket)); + + /** Installs some default result values for the above functions. + * Note that you can always override these in scope of your test by adding additional ON_CALL / EXPECT_CALL + * statements in the body of your test or after calling this function, and yours will take precedence. + **/ + static void InstallDefaultReturns(NiceSpawnableEntitiesInterfaceMock& target) + { + using namespace ::testing; + + // The ID and pointer are completely arbitrary, they just need to both be non-zero to look like a valid ticket. + constexpr EntitySpawnTicket::Id ticketId(1); + static int ticketPayload = 0; + ON_CALL(target, CreateTicket(_)).WillByDefault( + Return(AZStd::make_pair(ticketId, &ticketPayload))); + } + + }; + +} // namespace AzFramework diff --git a/Code/Framework/Tests/framework_shared_tests_files.cmake b/Code/Framework/Tests/framework_shared_tests_files.cmake index 57345ee630..8cf1a2e3fd 100644 --- a/Code/Framework/Tests/framework_shared_tests_files.cmake +++ b/Code/Framework/Tests/framework_shared_tests_files.cmake @@ -10,6 +10,7 @@ # set(FILES + Mocks/MockSpawnableEntitiesInterface.h Utils/Utils.h Utils/Utils.cpp ) diff --git a/Gems/Vegetation/Code/CMakeLists.txt b/Gems/Vegetation/Code/CMakeLists.txt index 7283f53c97..10d54da78e 100644 --- a/Gems/Vegetation/Code/CMakeLists.txt +++ b/Gems/Vegetation/Code/CMakeLists.txt @@ -105,6 +105,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) BUILD_DEPENDENCIES PRIVATE AZ::AzTest + AZ::AzFrameworkTestShared Gem::Vegetation.Static ) ly_add_googletest( diff --git a/Gems/Vegetation/Code/Include/Vegetation/PrefabInstanceSpawner.h b/Gems/Vegetation/Code/Include/Vegetation/PrefabInstanceSpawner.h new file mode 100644 index 0000000000..3f65352abc --- /dev/null +++ b/Gems/Vegetation/Code/Include/Vegetation/PrefabInstanceSpawner.h @@ -0,0 +1,101 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +#pragma once + +#include +#include +#include +#include + +namespace Vegetation +{ + /** + * Instance spawner of prefab instances. + */ + class PrefabInstanceSpawner + : public InstanceSpawner + , private AZ::Data::AssetBus::MultiHandler + { + public: + AZ_RTTI(PrefabInstanceSpawner, "{74BEEDB5-81CF-409F-B375-0D93D81EF2E3}", InstanceSpawner); + AZ_CLASS_ALLOCATOR(PrefabInstanceSpawner, AZ::SystemAllocator, 0); + static void Reflect(AZ::ReflectContext* context); + + PrefabInstanceSpawner(); + virtual ~PrefabInstanceSpawner(); + + //! Start loading any assets that the spawner will need. + void LoadAssets() override; + + //! Unload any assets that the spawner loaded. + void UnloadAssets() override; + + //! Perform any extra initialization needed at the point of registering with the vegetation system. + void OnRegisterUniqueDescriptor() override; + + //! Perform any extra cleanup needed at the point of unregistering with the vegetation system. + void OnReleaseUniqueDescriptor() override; + + //! Does this exist but have empty asset references? + bool HasEmptyAssetReferences() const override; + + //! Has this finished loading any assets that are needed? + bool IsLoaded() const override; + + //! Are the assets loaded, initialized, and spawnable? + bool IsSpawnable() const override; + + //! Display name of the instances that will be spawned. + AZStd::string GetName() const override; + + //! Create a single instance. + InstancePtr CreateInstance(const InstanceData& instanceData) override; + + //! Destroy a single instance. + void DestroyInstance(InstanceId id, InstancePtr instance) override; + + AZStd::string GetSpawnableAssetPath() const; + void SetSpawnableAssetPath(const AZStd::string& assetPath); + + AZ::Data::AssetId GetSpawnableAssetId() const; + void SetSpawnableAssetId(const AZ::Data::AssetId& assetId); + + private: + bool DataIsEquivalent(const InstanceSpawner& rhs) const override; + + ////////////////////////////////////////////////////////////////////////// + // AZ::Data::AssetBus::Handler + void OnAssetReady(AZ::Data::Asset asset) override; + void OnAssetReloaded(AZ::Data::Asset asset) override; + + AZ::u32 SpawnableAssetChanged(); + void ResetSpawnableAsset(); + + void UpdateCachedValues(); + + //! Verify that the spawnable asset only contains data compatible with the dynamic vegetation system. + bool ValidateAssetContents(const AZ::Data::Asset asset) const; + + //! Despawn an instance of a spawnable asset + void DespawnAssetInstance(AzFramework::EntitySpawnTicket* ticket); + + //! Cached values so that asset isn't accessed on other threads + bool m_assetLoadedAndSpawnable = false; + + //! Collection of spawned instance tickets, needed for destroying the instances. + AZStd::unordered_set m_instanceTickets; + + //! asset data + AZ::Data::Asset m_spawnableAsset; + }; + +} // namespace Vegetation diff --git a/Gems/Vegetation/Code/Source/PrefabInstanceSpawner.cpp b/Gems/Vegetation/Code/Source/PrefabInstanceSpawner.cpp new file mode 100644 index 0000000000..f22dbb61aa --- /dev/null +++ b/Gems/Vegetation/Code/Source/PrefabInstanceSpawner.cpp @@ -0,0 +1,401 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ + +#include "Vegetation_precompiled.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +namespace Vegetation +{ + + PrefabInstanceSpawner::PrefabInstanceSpawner() + { + UnloadAssets(); + } + + PrefabInstanceSpawner::~PrefabInstanceSpawner() + { + UnloadAssets(); + AZ_Assert(m_instanceTickets.empty(), "Destroying spawner while %u spawn tickets still exist!", m_instanceTickets.size()); + } + + void PrefabInstanceSpawner::Reflect(AZ::ReflectContext* context) + { + AZ::SerializeContext* serialize = azrtti_cast(context); + if (serialize) + { + serialize->Class() + ->Version(0)->Field( + "SpawnableAsset", &PrefabInstanceSpawner::m_spawnableAsset) + ; + + AZ::EditContext* edit = serialize->GetEditContext(); + if (edit) + { + edit->Class( + "Prefab", "Prefab Instance") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + + ->DataElement(AZ::Edit::UIHandlers::Default, &PrefabInstanceSpawner::m_spawnableAsset, "Prefab Asset", "Prefab asset") + ->Attribute(AZ::Edit::Attributes::ShowProductAssetFileName, false) + ->Attribute(AZ::Edit::Attributes::HideProductFilesInAssetPicker, true) + ->Attribute(AZ::Edit::Attributes::AssetPickerTitle, "a Prefab") + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &PrefabInstanceSpawner::SpawnableAssetChanged) + ; + } + } + if (auto behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class() + ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common) + ->Attribute(AZ::Script::Attributes::Category, "Vegetation") + ->Attribute(AZ::Script::Attributes::Module, "vegetation") + ->Constructor() + ->Method("GetPrefabAssetPath", &PrefabInstanceSpawner::GetSpawnableAssetPath) + ->Method("SetPrefabAssetPath", &PrefabInstanceSpawner::SetSpawnableAssetPath) + ->Method("GetPrefabAssetId", &PrefabInstanceSpawner::GetSpawnableAssetId) + ->Method("SetPrefabAssetId", &PrefabInstanceSpawner::SetSpawnableAssetId); + } + } + + bool PrefabInstanceSpawner::DataIsEquivalent(const InstanceSpawner& baseRhs) const + { + if (const auto* rhs = azrtti_cast(&baseRhs)) + { + return m_spawnableAsset == rhs->m_spawnableAsset; + } + + // Not the same subtypes, so definitely not a data match. + return false; + } + + void PrefabInstanceSpawner::LoadAssets() + { + UnloadAssets(); + + // Note that the spawnable tickets manage and track asset loading as well. We *could* just rely on that and mark + // the spawner as immediately ready for use (i.e. always return "true" in IsLoaded() and IsSpawnable() ), but this + // would cause us to wait until the first instance is spawned to load the asset, creating a delay right at the point + // that the vegetation is becoming visible. It would also cause the asset to get auto-unloaded every time all the + // instances using it are despawned. By loading it *prior* to marking things as ready, we can ensure that we have the + // asset at the point that the first instance is spawned, and that it won't get auto-unloaded every time the instances + // are despawned. + m_spawnableAsset.QueueLoad(); + AZ::Data::AssetBus::MultiHandler::BusConnect(m_spawnableAsset.GetId()); + } + + void PrefabInstanceSpawner::UnloadAssets() + { + // It's possible under some circumstances that we might unload assets before destroying all spawned instances + // due to the way the vegetation system queues up delete requests and descriptor unregistrations. If so, + // despawn the actual spawned instances here, but leave the ticket entries in the instance ticket map and don't + // delete the ticket pointers. The tickets will get cleaned up when the vegetation system gets around to requesting + // the instance destroy. + if (!m_instanceTickets.empty()) + { + for (auto& ticket : m_instanceTickets) + { + DespawnAssetInstance(ticket); + } + } + ResetSpawnableAsset(); + NotifyOnAssetsUnloaded(); + } + + void PrefabInstanceSpawner::ResetSpawnableAsset() + { + AZ::Data::AssetBus::MultiHandler::BusDisconnect(); + + m_spawnableAsset.Release(); + UpdateCachedValues(); + m_spawnableAsset.SetAutoLoadBehavior(AZ::Data::AssetLoadBehavior::QueueLoad); + } + + void PrefabInstanceSpawner::UpdateCachedValues() + { + // Once our assets are loaded and at the point that they're getting registered, + // cache off the spawnable state for use from multiple threads. + + m_assetLoadedAndSpawnable = m_spawnableAsset.IsReady(); + } + + void PrefabInstanceSpawner::OnRegisterUniqueDescriptor() + { + UpdateCachedValues(); + } + + void PrefabInstanceSpawner::OnReleaseUniqueDescriptor() + { + } + + bool PrefabInstanceSpawner::HasEmptyAssetReferences() const + { + // If we don't have a valid Spawnable Asset, then that means we're expecting to spawn empty instances. + return !m_spawnableAsset.GetId().IsValid(); + } + + bool PrefabInstanceSpawner::IsLoaded() const + { + return m_assetLoadedAndSpawnable; + } + + bool PrefabInstanceSpawner::IsSpawnable() const + { + return m_assetLoadedAndSpawnable; + } + + AZStd::string PrefabInstanceSpawner::GetName() const + { + AZStd::string assetName; + if (!HasEmptyAssetReferences()) + { + // Get the asset file name + assetName = m_spawnableAsset.GetHint(); + if (!m_spawnableAsset.GetHint().empty()) + { + AzFramework::StringFunc::Path::GetFileName(m_spawnableAsset.GetHint().c_str(), assetName); + } + } + else + { + assetName = ""; + } + + return assetName; + } + + bool PrefabInstanceSpawner::ValidateAssetContents(const AZ::Data::Asset asset) const + { + bool validAsset = true; + + // Basic safety check: Make sure the asset is a spawnable. + auto spawnableAsset = azrtti_cast(asset.GetData()); + if (!spawnableAsset) + { + return false; + } + + // Loop through all the components on all the entities in the spawnable, looking for any type of Vegetation Area. + // If we try to dynamically spawn vegetation areas, as they spawn in they will non-deterministically start spawning + // (or blocking) other vegetation while we're in the midst of spawning the higher-level vegetation area. Threading + // and timing affects which one wins out. It may also cause other bugs. + + const AzFramework::Spawnable::EntityList& entities = spawnableAsset->GetEntities(); + for (auto& entity : entities) + { + auto components = entity->GetComponents(); + for (auto component : components) + { + if (azrtti_istypeof(component)) + { + validAsset = false; + AZ_Error("Vegetation", false, + "Vegetation system cannot spawn prefabs containing a component of type '%s'", + component->RTTI_GetTypeName()); + } + } + } + + return validAsset; + } + + void PrefabInstanceSpawner::OnAssetReady(AZ::Data::Asset asset) + { + if (m_spawnableAsset.GetId() == asset.GetId()) + { + // Make sure that the spawnable asset we're loading doesn't contain any data incompatible with + // the dynamic vegetation system. + // This check needs to be performed at asset loading time as opposed to authoring / configuration + // time because the spawnable asset can be changed independently from the authoring of this component. + bool validAsset = ValidateAssetContents(asset); + + ResetSpawnableAsset(); + if (validAsset) + { + m_spawnableAsset = asset; + } + UpdateCachedValues(); + NotifyOnAssetsLoaded(); + } + } + + void PrefabInstanceSpawner::OnAssetReloaded(AZ::Data::Asset asset) + { + OnAssetReady(asset); + } + + AZStd::string PrefabInstanceSpawner::GetSpawnableAssetPath() const + { + AZStd::string assetPathString; + AZ::Data::AssetCatalogRequestBus::BroadcastResult( + assetPathString, &AZ::Data::AssetCatalogRequests::GetAssetPathById, m_spawnableAsset.GetId()); + return assetPathString; + } + + void PrefabInstanceSpawner::SetSpawnableAssetPath(const AZStd::string& assetPath) + { + if (!assetPath.empty()) + { + AZ::Data::AssetId assetId; + AZ::Data::AssetCatalogRequestBus::BroadcastResult( + assetId, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath, assetPath.c_str(), + AZ::Data::s_invalidAssetType, false); + if (assetId.IsValid()) + { + SetSpawnableAssetId(assetId); + } + else + { + AZ_Error("Vegetation", false, "Asset '%s' is invalid.", assetPath.c_str()); + } + } + else + { + SetSpawnableAssetId(AZ::Data::AssetId()); + } + } + + AZ::Data::AssetId PrefabInstanceSpawner::GetSpawnableAssetId() const + { + return m_spawnableAsset.GetId(); + } + + void PrefabInstanceSpawner::SetSpawnableAssetId(const AZ::Data::AssetId& assetId) + { + if (assetId.IsValid()) + { + AZ::Data::AssetInfo assetInfo; + AZ::Data::AssetCatalogRequestBus::BroadcastResult( + assetInfo, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetInfoById, assetId); + if (assetInfo.m_assetType == m_spawnableAsset.GetType()) + { + m_spawnableAsset.Create(assetId, false); + LoadAssets(); + } + else + { + AZ_Error( + "Vegetation", false, "Asset '%s' is of type %s, but expected a Spawnable type.", + assetId.ToString().c_str(), assetInfo.m_assetType.ToString().c_str()); + } + } + else + { + // An invalid asset ID is treated as a valid way to spawn "empty" instances, so don't print an error, just clear out + // the asset to that it has an invalid asset reference. (See also HasEmptyAssetReferences() above) + m_spawnableAsset = AZ::Data::Asset(); + LoadAssets(); + } + } + + AZ::u32 PrefabInstanceSpawner::SpawnableAssetChanged() + { + // Whenever we change the spawnable asset, force a refresh of the Entity Inspector + // since we want the Descriptor List to refresh the name of the entry. + NotifyOnAssetsUnloaded(); + return AZ::Edit::PropertyRefreshLevels::AttributesAndValues; + } + + InstancePtr PrefabInstanceSpawner::CreateInstance(const InstanceData& instanceData) + { + InstancePtr opaqueInstanceData = nullptr; + + // Create a Transform that represents our instance. + AZ::Transform world = AZ::Transform::CreateFromQuaternionAndTranslation( + instanceData.m_alignment * instanceData.m_rotation, instanceData.m_position); + world.MultiplyByUniformScale(instanceData.m_scale); + + // Create a callback for SpawnAllEntities that will set the transform of the root entity to the correct position / rotation / scale + // for our spawned instance. + auto preSpawnCB = [this, world]( + [[maybe_unused]] AzFramework::EntitySpawnTicket::Id ticketId, AzFramework::SpawnableEntityContainerView view) + { + AZ::Entity* rootEntity = *view.begin(); + + AzFramework::TransformComponent* entityTransform = rootEntity->FindComponent(); + + if (entityTransform) + { + entityTransform->SetWorldTM(world); + } + }; + + // Create the EntitySpawnTicket here. This pointer is going to get handed off to the vegetation system as opaque instance data, + // where it will be tracked and held onto for the lifetime of the vegetation instance. The vegetation system will pass it back + // in to DestroyInstance at the end of the lifetime, so that's the one place where we will delete the ticket pointers. + AzFramework::EntitySpawnTicket* ticket = new AzFramework::EntitySpawnTicket(m_spawnableAsset); + if (ticket->IsValid()) + { + // Track the ticket that we've created. + m_instanceTickets.emplace(ticket); + + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; + optionalArgs.m_preInsertionCallback = AZStd::move(preSpawnCB); + AzFramework::SpawnableEntitiesInterface::Get()->SpawnAllEntities(*ticket, AZStd::move(optionalArgs)); + + opaqueInstanceData = ticket; + } + else + { + // Something went wrong! + AZ_Assert(ticket->IsValid(), "Unable to instantiate spawnable asset"); + delete ticket; + } + + return opaqueInstanceData; + } + + void PrefabInstanceSpawner::DespawnAssetInstance(AzFramework::EntitySpawnTicket* ticket) + { + if (ticket->IsValid()) + { + AzFramework::SpawnableEntitiesInterface::Get()->DespawnAllEntities(*ticket); + } + } + + void PrefabInstanceSpawner::DestroyInstance([[maybe_unused]] InstanceId id, InstancePtr instance) + { + if (instance) + { + auto ticket = reinterpret_cast(instance); + + // If the spawnable asset instantiated successfully, we should have a record of it. + auto foundInstance = m_instanceTickets.find(ticket); + AZ_Assert(foundInstance != m_instanceTickets.end(), "Couldn't find CreateInstance entry for the EntitySpawnTicket."); + if (foundInstance != m_instanceTickets.end()) + { + // The call to DespawnAssetInstance above is technically redundant right now, because when we delete the ticket pointer + // below it will automatically despawn everything anyways. However, it's nice to have a single explicit call to despawn, + // in case we ever need a place to add logging, or have a callback when despawning is complete, etc. + DespawnAssetInstance(ticket); + m_instanceTickets.erase(foundInstance); + } + + // The vegetation system has stopped tracking this instance, so it's now safe to delete the ticket pointer. + delete ticket; + } + } +} // namespace Vegetation diff --git a/Gems/Vegetation/Code/Source/VegetationSystemComponent.cpp b/Gems/Vegetation/Code/Source/VegetationSystemComponent.cpp index 7cad51e025..a3a1ff4616 100644 --- a/Gems/Vegetation/Code/Source/VegetationSystemComponent.cpp +++ b/Gems/Vegetation/Code/Source/VegetationSystemComponent.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include @@ -73,6 +74,7 @@ namespace Vegetation InstanceSpawner::Reflect(context); EmptyInstanceSpawner::Reflect(context); DynamicSliceInstanceSpawner::Reflect(context); + PrefabInstanceSpawner::Reflect(context); Descriptor::Reflect(context); AreaConfig::Reflect(context); AreaComponentBase::Reflect(context); diff --git a/Gems/Vegetation/Code/Tests/PrefabInstanceSpawnerTests.cpp b/Gems/Vegetation/Code/Tests/PrefabInstanceSpawnerTests.cpp new file mode 100644 index 0000000000..368615ea42 --- /dev/null +++ b/Gems/Vegetation/Code/Tests/PrefabInstanceSpawnerTests.cpp @@ -0,0 +1,347 @@ +/* +* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +* its licensors. +* +* For complete copyright and license terms please see the LICENSE at the root of this +* distribution (the "License"). All use of this software is governed by the License, +* or, if provided, by the license below or the license accompanying this file. Do not +* remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* +*/ +#include "Vegetation_precompiled.h" + +#include "VegetationTest.h" +#include "VegetationMocks.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace UnitTest +{ + // Mock VegetationSystemComponent is needed to reflect only the PrefabInstanceSpawner. + class MockPrefabInstanceVegetationSystemComponent + : public AZ::Component + { + public: + AZ_COMPONENT(MockPrefabInstanceVegetationSystemComponent, "{5EC9AA35-2653-4326-853F-F2056F0DE36C}", AZ::Component); + + void Activate() override {} + void Deactivate() override {} + + static void Reflect(AZ::ReflectContext* reflect) + { + Vegetation::InstanceSpawner::Reflect(reflect); + Vegetation::PrefabInstanceSpawner::Reflect(reflect); + Vegetation::EmptyInstanceSpawner::Reflect(reflect); + } + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("VegetationSystemService")); + } + }; + + // To test prefab spawning, we need to mock up enough of the asset management system and the spawnable + // asset handling to pretend like we're loading/unloading spawnables successfully. + class PrefabInstanceSpawnerTests + : public VegetationComponentTests + , public UnitTest::SetRestoreFileIOBaseRAII + , public Vegetation::DescriptorNotificationBus::Handler + , public AZ::Data::AssetCatalogRequestBus::Handler + , public AZ::Data::AssetHandler + , public AZ::Data::AssetCatalog + { + public: + PrefabInstanceSpawnerTests() + : UnitTest::SetRestoreFileIOBaseRAII(m_fileIOMock) + { + AZ::IO::MockFileIOBase::InstallDefaultReturns(m_fileIOMock); + AzFramework::MockSpawnableEntitiesInterface::InstallDefaultReturns(m_spawnableEntitiesInterfaceMock); + } + + void RegisterComponentDescriptors() override + { + m_app.RegisterComponentDescriptor(MockPrefabInstanceVegetationSystemComponent::CreateDescriptor()); + } + + void SetUp() override + { + VegetationComponentTests::SetUp(); + + // Create a real Asset Mananger, and point to ourselves as the handler for Spawnable. + AZ::AllocatorInstance::Create(); + AZ::AllocatorInstance::Create(); + + + // Initialize the job manager with 1 thread for the AssetManager to use. + AZ::JobManagerDesc jobDesc; + AZ::JobManagerThreadDesc threadDesc; + jobDesc.m_workerThreads.push_back(threadDesc); + m_jobManager = aznew AZ::JobManager(jobDesc); + m_jobContext = aznew AZ::JobContext(*m_jobManager); + AZ::JobContext::SetGlobalContext(m_jobContext); + + AZ::Data::AssetManager::Descriptor descriptor; + AZ::Data::AssetManager::Create(descriptor); + AZ::Data::AssetManager::Instance().RegisterHandler(this, AZ::AzTypeInfo::Uuid()); + AZ::Data::AssetManager::Instance().RegisterCatalog(this, AZ::AzTypeInfo::Uuid()); + + // Intercept messages for finding assets by name. + AZ::Data::AssetCatalogRequestBus::Handler::BusConnect(); + } + + void TearDown() override + { + // Give the AssetManager a chance to fire off any lingering events and perform cleanup for any + // spawnable assets we loaded. + AZ::Data::AssetManager::Instance().DispatchEvents(); + + AZ::Data::AssetManager::Instance().UnregisterCatalog(this); + AZ::Data::AssetManager::Instance().UnregisterHandler(this); + + AZ::Data::AssetCatalogRequestBus::Handler::BusDisconnect(); + + AZ::Data::AssetManager::Destroy(); + + AZ::JobContext::SetGlobalContext(nullptr); + delete m_jobContext; + delete m_jobManager; + + AZ::AllocatorInstance::Destroy(); + AZ::AllocatorInstance::Destroy(); + + VegetationComponentTests::TearDown(); + } + + // Helper methods: + + // Set up a mock asset with the given name and id and direct the instance spawner to use it. + void CreateAndSetMockAsset(Vegetation::PrefabInstanceSpawner& instanceSpawner, AZ::Data::AssetId assetId, AZStd::string assetPath) + { + // Save these off for use from our mock AssetCatalogRequestBus + m_assetId = assetId; + m_assetPath = assetPath; + + Vegetation::DescriptorNotificationBus::Handler::BusConnect(&instanceSpawner); + + // Tell the spawner to use this asset. Note that this also triggers a LoadAssets() call internally. + instanceSpawner.SetSpawnableAssetPath(m_assetPath); + + // Our instance spawner should now have a valid asset reference. + // It may or may not be loaded already by the time we get here, + // depending on how quickly the Asset Processor job thread picks it up. + EXPECT_FALSE(instanceSpawner.HasEmptyAssetReferences()); + + // Since the asset load is going through the real AssetManager, there's a delay while a separate + // job thread executes and actually loads our mock spawnable asset. + // If our asset hasn't loaded successfully after 5 seconds, it's unlikely to succeed. + // This choice of delay should be *reasonably* safe because it's all CPU-based processing, + // no actual I/O occurs as a part of the test. + constexpr int sleepMs = 10; + constexpr int totalWaitTimeMs = 5000; + int numRetries = totalWaitTimeMs / sleepMs; + while ((m_numOnLoadedCalls < 1) && (numRetries >= 0)) + { + AZ::Data::AssetManager::Instance().DispatchEvents(); + AZ::SystemTickBus::Broadcast(&AZ::SystemTickBus::Events::OnSystemTick); + AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(sleepMs)); + numRetries--; + } + + ASSERT_TRUE(m_numOnLoadedCalls == 1); + EXPECT_TRUE(instanceSpawner.IsLoaded()); + EXPECT_TRUE(instanceSpawner.IsSpawnable()); + + Vegetation::DescriptorNotificationBus::Handler::BusDisconnect(); + } + + + // AssetHandler + // Minimalist mocks to look like a Spawnable has been created/loaded/destroyed successfully + AZ::Data::AssetPtr CreateAsset(const AZ::Data::AssetId& id, [[maybe_unused]] const AZ::Data::AssetType& type) override + { + AzFramework::Spawnable* spawnableAsset = new AzFramework::Spawnable(id); + MockAssetData* temp = reinterpret_cast(spawnableAsset); + temp->SetStatus(AZ::Data::AssetData::AssetStatus::NotLoaded); + + return spawnableAsset; + } + + void DestroyAsset(AZ::Data::AssetPtr ptr) override { delete ptr; } + void GetHandledAssetTypes(AZStd::vector& assetTypes) override + { + assetTypes.push_back(AZ::AzTypeInfo::Uuid()); + } + AZ::Data::AssetHandler::LoadResult LoadAssetData( + const AZ::Data::Asset& asset, + AZStd::shared_ptr stream, + [[maybe_unused]] const AZ::Data::AssetFilterCB& assetLoadFilterCB) + { + MockAssetData* temp = reinterpret_cast(asset.GetData()); + temp->SetStatus(AZ::Data::AssetData::AssetStatus::Ready); + return AZ::Data::AssetHandler::LoadResult::LoadComplete; + } + + // DescriptorNotificationBus + // Keep track of whether or not the Spawner successfully loaded the asset and notified listeners + void OnDescriptorAssetsLoaded() override { m_numOnLoadedCalls++; } + + // AssetCatalogRequestBus + // Minimalist mocks to provide our desired asset path or asset id + AZStd::string GetAssetPathById(const AZ::Data::AssetId& /*id*/) override { return m_assetPath; } + AZ::Data::AssetId GetAssetIdByPath(const char* /*path*/, const AZ::Data::AssetType& /*typeToRegister*/, bool /*autoRegisterIfNotFound*/) override { return m_assetId; } + AZ::Data::AssetInfo GetAssetInfoById(const AZ::Data::AssetId& /*id*/) override + { + AZ::Data::AssetInfo assetInfo; + assetInfo.m_assetId = m_assetId; + assetInfo.m_assetType = AZ::AzTypeInfo::Uuid(); + assetInfo.m_relativePath = m_assetPath; + return assetInfo; + } + + // AssetCatalog + // Minimalist mock to pretend like we've loaded a Spawnable asset + AZ::Data::AssetStreamInfo GetStreamInfoForLoad( + [[maybe_unused]] const AZ::Data::AssetId& id, const AZ::Data::AssetType& type) override + { + EXPECT_TRUE(type == AZ::AzTypeInfo::Uuid()); + AZ::Data::AssetStreamInfo info; + info.m_dataOffset = 0; + info.m_streamName = m_assetPath; + info.m_dataLen = 0; + info.m_streamFlags = AZ::IO::OpenMode::ModeRead; + + return info; + } + + AZStd::string m_assetPath; + AZ::Data::AssetId m_assetId; + int m_numOnLoadedCalls = 0; + + AZ::JobManager* m_jobManager{ nullptr }; + AZ::JobContext* m_jobContext{ nullptr }; + ::testing::NiceMock m_fileIOMock; + ::testing::NiceMock m_spawnableEntitiesInterfaceMock; + }; + + TEST_F(PrefabInstanceSpawnerTests, BasicInitializationTest) + { + // Basic test to make sure we can construct / destroy without errors. + + Vegetation::PrefabInstanceSpawner instanceSpawner; + } + + TEST_F(PrefabInstanceSpawnerTests, DefaultSpawnersAreEqual) + { + // Two different instances of the default PrefabInstanceSpawner should be considered data-equivalent. + + Vegetation::PrefabInstanceSpawner instanceSpawner1; + Vegetation::PrefabInstanceSpawner instanceSpawner2; + + EXPECT_TRUE(instanceSpawner1 == instanceSpawner2); + } + + TEST_F(PrefabInstanceSpawnerTests, DifferentSpawnersAreNotEqual) + { + // Two spawners with different data should *not* be data-equivalent. + + Vegetation::PrefabInstanceSpawner instanceSpawner1; + Vegetation::PrefabInstanceSpawner instanceSpawner2; + + // Give the second instance spawner a non-default asset reference. + CreateAndSetMockAsset(instanceSpawner2, AZ::Uuid::CreateRandom(), "test"); + + // The test is written this way because only the == operator is overloaded. + EXPECT_TRUE(!(instanceSpawner1 == instanceSpawner2)); + } + + TEST_F(PrefabInstanceSpawnerTests, LoadAndUnloadAssets) + { + // The spawner should successfully load/unload assets without errors. + + Vegetation::PrefabInstanceSpawner instanceSpawner; + + // Our instance spawner should be empty before we set the assets. + EXPECT_TRUE(instanceSpawner.HasEmptyAssetReferences()); + + // This will test the asset load. + CreateAndSetMockAsset(instanceSpawner, AZ::Uuid::CreateRandom(), "test"); + + // Test the asset unload works too. + Vegetation::DescriptorNotificationBus::Handler::BusConnect(&instanceSpawner); + instanceSpawner.UnloadAssets(); + EXPECT_FALSE(instanceSpawner.IsLoaded()); + EXPECT_FALSE(instanceSpawner.IsSpawnable()); + Vegetation::DescriptorNotificationBus::Handler::BusDisconnect(); + } + + TEST_F(PrefabInstanceSpawnerTests, CreateAndDestroyInstance) + { + // The spawner should successfully create and destroy an instance without errors. + + Vegetation::PrefabInstanceSpawner instanceSpawner; + + CreateAndSetMockAsset(instanceSpawner, AZ::Uuid::CreateRandom(), "test"); + + instanceSpawner.OnRegisterUniqueDescriptor(); + + Vegetation::InstanceData instanceData; + Vegetation::InstancePtr instance = instanceSpawner.CreateInstance(instanceData); + EXPECT_TRUE(instance); + instanceSpawner.DestroyInstance(0, instance); + + instanceSpawner.OnReleaseUniqueDescriptor(); + } + + TEST_F(PrefabInstanceSpawnerTests, SpawnerRegisteredWithDescriptor) + { + // Validate that the Descriptor successfully gets PrefabInstanceSpawner registered with it, + // as long as InstanceSpawner and PrefabInstanceSpawner have been reflected. + + MockPrefabInstanceVegetationSystemComponent* component = nullptr; + auto entity = CreateEntity(&component); + + Vegetation::Descriptor descriptor; + descriptor.RefreshSpawnerTypeList(); + auto spawnerTypes = descriptor.GetSpawnerTypeList(); + EXPECT_TRUE(spawnerTypes.size() > 0); + const auto& prefabSpawnerEntry = AZStd::find( + spawnerTypes.begin(), spawnerTypes.end(), + AZStd::pair(Vegetation::PrefabInstanceSpawner::RTTI_Type(), "PrefabInstanceSpawner")); + EXPECT_NE(prefabSpawnerEntry, spawnerTypes.end()); + } + + TEST_F(PrefabInstanceSpawnerTests, DescriptorCreatesCorrectSpawner) + { + // Validate that the Descriptor successfully creates a new PrefabInstanceSpawner if we change + // the spawner type on the Descriptor. + + MockPrefabInstanceVegetationSystemComponent* component = nullptr; + auto entity = CreateEntity(&component); + + // We expect the Descriptor to start off with something other than Prefab spawner, but then should correctly get an + // PrefabInstanceSpawner after we change spawnerType. + Vegetation::Descriptor descriptor; + EXPECT_NE(azrtti_typeid(*(descriptor.GetInstanceSpawner())),Vegetation::PrefabInstanceSpawner::RTTI_Type()); + descriptor.m_spawnerType = Vegetation::PrefabInstanceSpawner::RTTI_Type(); + descriptor.RefreshSpawnerTypeList(); + descriptor.SpawnerTypeChanged(); + EXPECT_EQ(azrtti_typeid(*(descriptor.GetInstanceSpawner())), Vegetation::PrefabInstanceSpawner::RTTI_Type()); + } +} diff --git a/Gems/Vegetation/Code/vegetation_files.cmake b/Gems/Vegetation/Code/vegetation_files.cmake index abfd568862..d06a2d6927 100644 --- a/Gems/Vegetation/Code/vegetation_files.cmake +++ b/Gems/Vegetation/Code/vegetation_files.cmake @@ -17,6 +17,7 @@ set(FILES Include/Vegetation/InstanceSpawner.h Include/Vegetation/DynamicSliceInstanceSpawner.h Include/Vegetation/EmptyInstanceSpawner.h + Include/Vegetation/PrefabInstanceSpawner.h Include/Vegetation/AreaComponentBase.h Include/Vegetation/Ebuses/AreaSystemRequestBus.h Include/Vegetation/Ebuses/AreaNotificationBus.h @@ -104,6 +105,7 @@ set(FILES Source/Descriptor.cpp Source/DynamicSliceInstanceSpawner.cpp Source/EmptyInstanceSpawner.cpp + Source/PrefabInstanceSpawner.cpp Source/VegetationSystemComponent.cpp Source/VegetationSystemComponent.h Source/InstanceData.cpp diff --git a/Gems/Vegetation/Code/vegetation_tests_files.cmake b/Gems/Vegetation/Code/vegetation_tests_files.cmake index 2ce54d3033..08656de9d2 100644 --- a/Gems/Vegetation/Code/vegetation_tests_files.cmake +++ b/Gems/Vegetation/Code/vegetation_tests_files.cmake @@ -17,6 +17,7 @@ set(FILES Tests/VegetationComponentFilterTests.cpp Tests/DynamicSliceInstanceSpawnerTests.cpp Tests/EmptyInstanceSpawnerTests.cpp + Tests/PrefabInstanceSpawnerTests.cpp Tests/VegetationAreaSystemComponentTest.cpp Tests/VegetationTest.cpp Tests/VegetationTest.h