/* * 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 #include namespace Vegetation { DynamicSliceInstanceSpawner::DynamicSliceInstanceSpawner() { UnloadAssets(); } DynamicSliceInstanceSpawner::~DynamicSliceInstanceSpawner() { UnloadAssets(); AzFramework::SliceInstantiationResultBus::MultiHandler::BusDisconnect(); AZ_Assert(m_ticketToEntityMap.empty(), "Destroying spawner while instances still exist!"); } void DynamicSliceInstanceSpawner::Reflect(AZ::ReflectContext* context) { AZ::SerializeContext* serialize = azrtti_cast(context); if (serialize) { serialize->Class() ->Version(0) ->Field("SliceAsset", &DynamicSliceInstanceSpawner::m_sliceAsset) ; AZ::EditContext* edit = serialize->GetEditContext(); if (edit) { edit->Class( "Dynamic Slice", "Dynamic Slice Instance") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->DataElement(0, &DynamicSliceInstanceSpawner::m_sliceAsset, "Slice Asset", "Dynamic slice asset") ->Attribute(AZ::Edit::Attributes::ChangeNotify, &DynamicSliceInstanceSpawner::SliceAssetChanged) ; } } if (auto behaviorContext = azrtti_cast(context)) { behaviorContext->Class() ->Attribute(AZ::Script::Attributes::ExcludeFrom, AZ::Script::Attributes::ExcludeFlags::Preview) ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common) ->Attribute(AZ::Script::Attributes::Category, "Vegetation") ->Attribute(AZ::Script::Attributes::Module, "vegetation") ->Constructor() ->Method("GetSliceAssetPath", &DynamicSliceInstanceSpawner::GetSliceAssetPath) ->Method("SetSliceAssetPath", &DynamicSliceInstanceSpawner::SetSliceAssetPath) ; } } bool DynamicSliceInstanceSpawner::DataIsEquivalent(const InstanceSpawner & baseRhs) const { if (const auto* rhs = azrtti_cast(&baseRhs)) { return m_sliceAsset == rhs->m_sliceAsset; } // Not the same subtypes, so definitely not a data match. return false; } void DynamicSliceInstanceSpawner::LoadAssets() { UnloadAssets(); m_sliceAsset.QueueLoad(); AZ::Data::AssetBus::MultiHandler::BusConnect(m_sliceAsset.GetId()); } void DynamicSliceInstanceSpawner::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, delete the actual // spawned instances here, but leave the ticket entries in the slice ticket map. // The ticket entries will get cleaned up when the vegetation system gets around // to requesting the instance destroy. if (!m_ticketToEntityMap.empty()) { for (auto& entry : m_ticketToEntityMap) { DeleteSliceInstance(entry.first, entry.second); entry.second = AZ::EntityId(); } } ResetSliceAsset(); NotifyOnAssetsUnloaded(); } void DynamicSliceInstanceSpawner::ResetSliceAsset() { AZ::Data::AssetBus::MultiHandler::BusDisconnect(); m_sliceAsset.Release(); UpdateCachedValues(); m_sliceAsset.SetAutoLoadBehavior(AZ::Data::AssetLoadBehavior::QueueLoad); } void DynamicSliceInstanceSpawner::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_sliceLoadedAndSpawnable = m_sliceAsset.IsReady(); } void DynamicSliceInstanceSpawner::OnRegisterUniqueDescriptor() { UpdateCachedValues(); } void DynamicSliceInstanceSpawner::OnReleaseUniqueDescriptor() { } bool DynamicSliceInstanceSpawner::HasEmptyAssetReferences() const { // If we don't have a valid Slice Asset, then we're spawning empty instances. return !m_sliceAsset.GetId().IsValid(); } bool DynamicSliceInstanceSpawner::IsLoaded() const { return m_sliceLoadedAndSpawnable; } bool DynamicSliceInstanceSpawner::IsSpawnable() const { return m_sliceLoadedAndSpawnable; } AZStd::string DynamicSliceInstanceSpawner::GetName() const { AZStd::string assetName; if (!HasEmptyAssetReferences()) { // Get the asset file name assetName = m_sliceAsset.GetHint(); if (!m_sliceAsset.GetHint().empty()) { AzFramework::StringFunc::Path::GetFileName(m_sliceAsset.GetHint().c_str(), assetName); } } else { assetName = ""; } return assetName; } bool DynamicSliceInstanceSpawner::ValidateSliceContents(const AZ::Data::Asset asset) const { bool validSlice = true; // Basic safety check: Make sure the asset is a dynamic slice. auto sliceAsset = azrtti_cast(asset.GetData()); if (!sliceAsset) { return false; } // Make sure the dynamic slice has a slice component. auto slice = sliceAsset->GetComponent(); if (!slice) { return false; } // Loop through all the components on all the entities in the slice, looking for the following // incompatible components: // 1) Any type of Vegetation Area. If we try to dynamically spawn vegetation areas, as they spawn // in they will non-deterministically start spawning other vegetation where we're trying to spawn // vegetation areas. Threading and timing affects which one wins out. It may also cause other bugs. // 2) Gameplay Spawner components. These can spawn dynamic slices with vegetation areas, which leads // back to problem #1, but in a way that's even harder to detect. Also, if "destroy on deactivate" is // unselected on the component, they will spawn entities that continue to remain in the level even after // the dynamic vegetation system destroys the spawner. AZ::SliceComponent::EntityList entities; slice->GetEntities(entities); for (auto entity : entities) { auto components = entity->GetComponents(); for (auto component : components) { if (azrtti_istypeof(component) || (azrtti_typeid(component) == LmbrCentral::SpawnerComponentTypeId)) { validSlice = false; AZ_Error("Vegetation", false, "Vegetation system cannot spawn dynamic slices containing a component of type '%s'", component->RTTI_GetTypeName()); } } } return validSlice; } void DynamicSliceInstanceSpawner::OnAssetReady(AZ::Data::Asset asset) { if (m_sliceAsset.GetId() == asset.GetId()) { // Make sure that the slice we're loading doesn't contain any data incompatible with // the dynamic vegetation system. // This check needs to be performed at slice loading time as opposed to authoring / configuration // time because the slice can be changed independently from the authoring of this component. bool validSlice = ValidateSliceContents(asset); ResetSliceAsset(); if (validSlice) { m_sliceAsset = asset; } UpdateCachedValues(); NotifyOnAssetsLoaded(); } } void DynamicSliceInstanceSpawner::OnAssetReloaded(AZ::Data::Asset asset) { OnAssetReady(asset); } AZStd::string DynamicSliceInstanceSpawner::GetSliceAssetPath() const { AZStd::string assetPathString; AZ::Data::AssetCatalogRequestBus::BroadcastResult(assetPathString, &AZ::Data::AssetCatalogRequests::GetAssetPathById, m_sliceAsset.GetId()); return assetPathString; } void DynamicSliceInstanceSpawner::SetSliceAssetPath(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()) { AZ::Data::AssetInfo assetInfo; AZ::Data::AssetCatalogRequestBus::BroadcastResult(assetInfo, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetInfoById, assetId); if (assetInfo.m_assetType == m_sliceAsset.GetType()) { m_sliceAsset.Create(assetId, false); LoadAssets(); } else { AZ_Error("Vegetation", false, "Asset '%s' is of type %s, but expected a DynamicSliceAsset type.", assetPath.c_str(), assetInfo.m_assetType.ToString().c_str()); } } else { AZ_Error("Vegetation", false, "Asset '%s' is invalid.", assetPath.c_str()); } } else { m_sliceAsset = AZ::Data::Asset(); LoadAssets(); } } AZ::u32 DynamicSliceInstanceSpawner::SliceAssetChanged() { // Whenever we change the slice 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 DynamicSliceInstanceSpawner::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.MultiplyByScale(AZ::Vector3(instanceData.m_scale)); // Request a new dynamic slice instance. AzFramework::SliceInstantiationTicket* ticket = new AzFramework::SliceInstantiationTicket(); AzFramework::GameEntityContextRequestBus::BroadcastResult( *ticket, &AzFramework::GameEntityContextRequestBus::Events::InstantiateDynamicSlice, m_sliceAsset, world, nullptr); if (*ticket) { // Create entry for ticket, with no entities listed yet. These will get filled in // once the slice is fully instantiated. m_ticketToEntityMap.emplace(*ticket); // Listen for completion / failure events. AzFramework::SliceInstantiationResultBus::MultiHandler::BusConnect(*ticket); opaqueInstanceData = ticket; } else { // Something went wrong! AZ_Assert(*ticket, "Unable to instantiate dynamic slice"); delete ticket; } return opaqueInstanceData; } void DynamicSliceInstanceSpawner::DeleteSliceInstance(const AzFramework::SliceInstantiationTicket &ticket, AZ::EntityId firstEntityInSlice) { // Stop listening for instantiation events, and cancel the instantiation if it's still in-flight. // (If not, the cancel just won't do anything, but there's no harm in calling it anyways.) AzFramework::GameEntityContextRequestBus::Broadcast(&AzFramework::GameEntityContextRequestBus::Events::CancelDynamicSliceInstantiation, ticket); AzFramework::SliceInstantiationResultBus::MultiHandler::BusDisconnect(ticket); // If we have a list of entities from our slice, use the first one to look up and destroy our instance. if (firstEntityInSlice.IsValid()) { bool result = false; AzFramework::GameEntityContextRequestBus::BroadcastResult(result, &AzFramework::GameEntityContextRequestBus::Events::DestroyDynamicSliceByEntity, firstEntityInSlice); AZ_Assert(result, "Failed to destroy slice instance."); } } void DynamicSliceInstanceSpawner::DestroyInstance(InstanceId id, InstancePtr instance) { if (instance) { auto ticket = reinterpret_cast(instance); // If this slice instantiated successfully, we should have a record of it. auto foundInstance = m_ticketToEntityMap.find(*ticket); AZ_Assert(foundInstance != m_ticketToEntityMap.end(), "Couldn't find CreateInstance entry for slice instance"); if (foundInstance != m_ticketToEntityMap.end()) { AZ::EntityId firstEntityInSlice = foundInstance->second; DeleteSliceInstance(*ticket, firstEntityInSlice); m_ticketToEntityMap.erase(foundInstance); } delete ticket; } } void DynamicSliceInstanceSpawner::OnSliceInstantiated(const AZ::Data::AssetId & sliceAssetId, const AZ::SliceComponent::SliceInstanceAddress & sliceAddress) { const AzFramework::SliceInstantiationTicket ticket = (*AzFramework::SliceInstantiationResultBus::GetCurrentBusId()); // Stop listening for this ticket (since it's done). We can have have multiple tickets in flight. AzFramework::SliceInstantiationResultBus::MultiHandler::BusDisconnect(ticket); // Keep a record of the first entity in our slice instance, we'll need this later to be able to look up // and destroy the instance. const AZ::SliceComponent::EntityList& entities = sliceAddress.GetInstance()->GetInstantiated()->m_entities; AZ_Assert(!entities.empty(), "No entities found in the instantiated slice."); if (!entities.empty()) { AZ_Assert(!m_ticketToEntityMap[ticket].IsValid(), "Slice entry already had a valid entity ID"); m_ticketToEntityMap[ticket] = entities[0]->GetId(); } } void DynamicSliceInstanceSpawner::OnSliceInstantiationFailedOrCanceled(const AZ::Data::AssetId & sliceAssetId, bool cancelled) { const AzFramework::SliceInstantiationTicket ticket = *AzFramework::SliceInstantiationResultBus::GetCurrentBusId(); AzFramework::SliceInstantiationResultBus::MultiHandler::BusDisconnect(ticket); if (!cancelled) { AZ_Error("DynamicSliceInstanceSpawner", false, "Slice %s failed to instantiate", m_sliceAsset.ToString().c_str()); AZ_Assert(sliceAssetId == m_sliceAsset.GetId(), "Current slice asset doesn't match slice that failed to instantiate"); } } } // namespace Vegetation