/* * 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 "LyShine_precompiled.h" #include "UiCanvasComponent.h" #include "UiElementComponent.h" #include "UiTransform2dComponent.h" #include "UiSerialize.h" #include "UiCanvasFileObject.h" #include "UiGameEntityContext.h" #include "UiNavigationHelpers.h" #include "UiRenderer.h" #include "LyShine.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Animation/UiAnimationSystem.h" #include #include #ifndef _RELEASE #include #include #include #include #include #include #endif //////////////////////////////////////////////////////////////////////////////////////////////////// //! UiCanvasNotificationBus Behavior context handler class class UiCanvasNotificationBusBehaviorHandler : public UiCanvasNotificationBus::Handler , public AZ::BehaviorEBusHandler { public: AZ_EBUS_BEHAVIOR_BINDER(UiCanvasNotificationBusBehaviorHandler, "{64014B4F-E12F-4839-99B0-426B5717DB44}", AZ::SystemAllocator, OnAction); void OnAction(AZ::EntityId entityId, const LyShine::ActionName& actionName) override { Call(FN_OnAction, entityId, actionName); } }; //////////////////////////////////////////////////////////////////////////////////////////////////// //! UiCanvasInputNotificationBus Behavior context handler class class UiCanvasInputNotificationBusBehaviorHandler : public UiCanvasInputNotificationBus::Handler , public AZ::BehaviorEBusHandler { public: AZ_EBUS_BEHAVIOR_BINDER(UiCanvasInputNotificationBusBehaviorHandler, "{76042EFA-0B61-4E7A-ACC8-296382D46881}", AZ::SystemAllocator, OnCanvasPrimaryPressed, OnCanvasPrimaryReleased, OnCanvasMultiTouchPressed, OnCanvasMultiTouchReleased, OnCanvasHoverStart, OnCanvasHoverEnd, OnCanvasEnterPressed, OnCanvasEnterReleased); void OnCanvasPrimaryPressed(AZ::EntityId entityId) override { Call(FN_OnCanvasPrimaryPressed, entityId); } void OnCanvasPrimaryReleased(AZ::EntityId entityId) override { Call(FN_OnCanvasPrimaryReleased, entityId); } void OnCanvasMultiTouchPressed(AZ::EntityId entityId, int multiTouchIndex) override { Call(FN_OnCanvasMultiTouchPressed, entityId, multiTouchIndex); } void OnCanvasMultiTouchReleased(AZ::EntityId entityId, int multiTouchIndex) override { Call(FN_OnCanvasMultiTouchReleased, entityId, multiTouchIndex); } void OnCanvasHoverStart(AZ::EntityId entityId) override { Call(FN_OnCanvasHoverStart, entityId); } void OnCanvasHoverEnd(AZ::EntityId entityId) override { Call(FN_OnCanvasHoverEnd, entityId); } void OnCanvasEnterPressed(AZ::EntityId entityId) override { Call(FN_OnCanvasEnterPressed, entityId); } void OnCanvasEnterReleased(AZ::EntityId entityId) override { Call(FN_OnCanvasEnterReleased, entityId); } }; //////////////////////////////////////////////////////////////////////////////////////////////////// //! UiAnimationNotificationBus Behavior context handler class class UiAnimationNotificationBusBehaviorHandler : public UiAnimationNotificationBus::Handler , public AZ::BehaviorEBusHandler { public: AZ_EBUS_BEHAVIOR_BINDER(UiAnimationNotificationBusBehaviorHandler, "{35D19FE8-5F31-426E-877A-8EEF3A42F99F}", AZ::SystemAllocator, OnUiAnimationEvent, OnUiTrackEvent); void OnUiAnimationEvent(IUiAnimationListener::EUiAnimationEvent uiAnimationEvent, AZStd::string animSequenceName) override { Call(FN_OnUiAnimationEvent, uiAnimationEvent, animSequenceName); } void OnUiTrackEvent(AZStd::string eventName, AZStd::string valueName, AZStd::string animSequenceName) override { Call(FN_OnUiTrackEvent, eventName, valueName, animSequenceName); } }; //////////////////////////////////////////////////////////////////////////////////////////////////// //! UiInitializationBus Behavior context handler class class UiInitializationBusBehaviorHandler : public UiInitializationBus::Handler , public AZ::BehaviorEBusHandler { public: AZ_EBUS_BEHAVIOR_BINDER(UiInitializationBusBehaviorHandler, "{2978A8A2-1A88-40C2-A299-ECA68AD1C519}", AZ::SystemAllocator, InGamePostActivate); void InGamePostActivate() override { Call(FN_InGamePostActivate); } }; //////////////////////////////////////////////////////////////////////////////////////////////////// // Anonymous namespace //////////////////////////////////////////////////////////////////////////////////////////////////// namespace { LyShine::CanvasId s_lastCanvasId = 0; //////////////////////////////////////////////////////////////////////////////////////////////// // test if the given text file starts with the given text string bool TestFileStartString(const string& pathname, const char* expectedStart) { // Open the file using CCryFile, this supports it being in the pak file or a standalone file CCryFile file; if (!file.Open(pathname, "r")) { return false; } // get the size of the file and the length of the expected start string size_t fileSize = file.GetLength(); size_t expectedStartLen = strlen(expectedStart); // if the file is smaller than the expected start string then it is not a valid file if (fileSize < expectedStartLen) { return false; } // read in the length of the expected start string char* buffer = new char[expectedStartLen]; size_t bytesRead = file.ReadRaw(buffer, expectedStartLen); // match is true if the string read from the file matches the expected start string bool match = strncmp(expectedStart, buffer, expectedStartLen) == 0; delete [] buffer; return match; } //////////////////////////////////////////////////////////////////////////////////////////////// // Check if the given file was saved using AZ serialization bool IsValidAzSerializedFile(const string& pathname) { return TestFileStartString(pathname, " void ReuseOrGenerateNewIdsAndFixRefs(T* object, MapType& newIdMap, AZ::SerializeContext* context) { AZ::EntityUtils::ReplaceEntityIds( object, [&newIdMap](const AZ::EntityId& originalId, bool /*isEntityId*/) -> AZ::EntityId { auto findIt = newIdMap.find(originalId); if (findIt == newIdMap.end()) { AZ::EntityId newId = AZ::Entity::MakeId(); newIdMap.insert(AZStd::make_pair(originalId, newId)); return newId; } else { return findIt->second; // return the previously remapped id } }, context); AZ::EntityUtils::ReplaceEntityRefs( object, [&newIdMap](const AZ::EntityId& originalId, bool /*isEntityId*/) -> AZ::EntityId { auto findIt = newIdMap.find(originalId); if (findIt == newIdMap.end()) { return originalId; // entityId is not being remapped } else { return findIt->second; // return the remapped id } }, context); } UiRenderer* GetUiRenderer() { CLyShine* lyShine = static_cast(gEnv->pLyShine); return lyShine->GetUiRenderer(); } bool IsValidInteractable(const AZ::EntityId& entityId) { if (!entityId.IsValid()) { return false; } // Check if element is enabled bool isEnabled = false; EBUS_EVENT_ID_RESULT(isEnabled, entityId, UiElementBus, IsEnabled); if (!isEnabled) { return false; } // Check if element is handling events and therefore also an interactable bool canHandleEvents = false; EBUS_EVENT_ID_RESULT(canHandleEvents, entityId, UiInteractableBus, IsHandlingEvents); return canHandleEvents; } } //////////////////////////////////////////////////////////////////////////////////////////////////// // STATIC MEMBER DATA //////////////////////////////////////////////////////////////////////////////////////////////////// const AZ::Vector2 UiCanvasComponent::s_defaultCanvasSize(1280.0f, 720.0f); const AZ::Color UiCanvasComponent::s_defaultGuideColor(0.25f,1.0f,0.25f,1.0f); bool UiCanvasComponent::s_handleHoverInputEvents = true; bool UiCanvasComponent::s_allowClearingHoverInteractableOnHoverInput = true; //////////////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// UiCanvasComponent::UiCanvasComponent() : m_uniqueId(0) , m_rootElement() , m_lastElementId(0) , m_canvasToViewportMatrix(AZ::Matrix4x4::CreateIdentity()) , m_viewportToCanvasMatrix(AZ::Matrix4x4::CreateIdentity()) , m_activeInteractableShouldStayActive(false) , m_isActiveInteractablePressed(false) , m_lastMousePosition(-1.0f, -1.0f) , m_id(++s_lastCanvasId) , m_drawOrder(0) , m_canvasSize(UiCanvasComponent::s_defaultCanvasSize) , m_targetCanvasSize(m_canvasSize) , m_deviceScale(1.0f, 1.0f) , m_isLoadedInGame(false) , m_keepLoadedOnLevelUnload(false) , m_enabled(true) , m_renderToTexture(false) , m_isSnapEnabled(false) , m_snapDistance(10.0f) , m_snapRotationDegrees(10.0f) , m_guideColor(s_defaultGuideColor) , m_guidesAreLocked(false) , m_entityContext(nullptr) { m_navCommandStatus[UiNavigationHelpers::Command::Up] = {0, 0, true}; m_navCommandStatus[UiNavigationHelpers::Command::Down] = {0, 0, true}; m_navCommandStatus[UiNavigationHelpers::Command::Left] = {0, 0, true}; m_navCommandStatus[UiNavigationHelpers::Command::Right] = {0, 0, true}; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiCanvasComponent::~UiCanvasComponent() { DestroyScheduledElements(); m_uiAnimationSystem.RemoveAllSequences(); // remove all entries from m_elementsNeedingTransformRecompute list, can't use clear since that doesn't set the // m_next pointers to null except in a debug build. while (!m_elementsNeedingTransformRecompute.empty()) { UiElementComponent& elementComponent = m_elementsNeedingTransformRecompute.front(); m_elementsNeedingTransformRecompute.pop_front(); elementComponent.m_next = nullptr; // needed in order to be able to test if an element is in the list. } if (m_entityContext) { // deactivate all UI elements, this is so that we can detect improper deletion of UI elements by users // during game play DeactivateElements(); // Destroy the entity context, this will delete all the UI elements m_entityContext->DestroyUiContext(); } if (m_isLoadedInGame) { delete m_entityContext; } // Unload any active texture atlases UnloadAtlases(); } //////////////////////////////////////////////////////////////////////////////////////////////////// const AZStd::string& UiCanvasComponent::GetPathname() { return m_pathname; } //////////////////////////////////////////////////////////////////////////////////////////////////// LyShine::CanvasId UiCanvasComponent::GetCanvasId() { return m_id; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::u64 UiCanvasComponent::GetUniqueCanvasId() { return m_uniqueId; } //////////////////////////////////////////////////////////////////////////////////////////////////// int UiCanvasComponent::GetDrawOrder() { return m_drawOrder; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetDrawOrder(int drawOrder) { m_drawOrder = drawOrder; EBUS_EVENT(UiCanvasOrderNotificationBus, OnCanvasDrawOrderChanged, GetEntityId()); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::GetKeepLoadedOnLevelUnload() { return m_keepLoadedOnLevelUnload; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetKeepLoadedOnLevelUnload(bool keepLoaded) { m_keepLoadedOnLevelUnload = keepLoaded; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::RecomputeChangedLayouts() { SendRectChangeNotificationsAndRecomputeLayouts(); } //////////////////////////////////////////////////////////////////////////////////////////////////// int UiCanvasComponent::GetNumChildElements() { int numChildElements = 0; EBUS_EVENT_ID_RESULT(numChildElements, m_rootElement, UiElementBus, GetNumChildElements); return numChildElements; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiCanvasComponent::GetChildElement(int index) { AZ::Entity* child = nullptr; EBUS_EVENT_ID_RESULT(child, m_rootElement, UiElementBus, GetChildElement, index); return child; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasComponent::GetChildElementEntityId(int index) { AZ::EntityId childEntityId; EBUS_EVENT_ID_RESULT(childEntityId, m_rootElement, UiElementBus, GetChildEntityId, index); return childEntityId; } //////////////////////////////////////////////////////////////////////////////////////////////////// LyShine::EntityArray UiCanvasComponent::GetChildElements() { LyShine::EntityArray childElements; EBUS_EVENT_ID_RESULT(childElements, m_rootElement, UiElementBus, GetChildElements); return childElements; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZStd::vector UiCanvasComponent::GetChildElementEntityIds() { AZStd::vector childElementEntityIds; EBUS_EVENT_ID_RESULT(childElementEntityIds, m_rootElement, UiElementBus, GetChildEntityIds); return childElementEntityIds; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiCanvasComponent::CreateChildElement(const LyShine::NameType& name) { AZ::Entity* child = nullptr; EBUS_EVENT_ID_RESULT(child, m_rootElement, UiElementBus, CreateChildElement, name); return child; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiCanvasComponent::FindElementById(LyShine::ElementId id) { AZ::Entity* element = nullptr; EBUS_EVENT_ID_RESULT(element, m_rootElement, UiElementBus, FindDescendantById, id); return element; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiCanvasComponent::FindElementByName(const LyShine::NameType& name) { AZ::Entity* entity = nullptr; EBUS_EVENT_ID_RESULT(entity, m_rootElement, UiElementBus, FindDescendantByName, name); return entity; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasComponent::FindElementEntityIdByName(const LyShine::NameType& name) { AZ::EntityId entityId; EBUS_EVENT_ID_RESULT(entityId, m_rootElement, UiElementBus, FindDescendantEntityIdByName, name); return entityId; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::FindElementsByName(const LyShine::NameType& name, LyShine::EntityArray& result) { // find all elements with the given name EBUS_EVENT_ID(m_rootElement, UiElementBus, FindDescendantElements, [&name](const AZ::Entity* entity) { return name == entity->GetName(); }, result); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiCanvasComponent::FindElementByHierarchicalName(const LyShine::NameType& name) { // start at the root AZ::Entity* currentEntity = GetRootElement(); bool found = false; std::size_t lastPos = 0; while (currentEntity) { std::size_t pos = name.find('/', lastPos); if (pos == lastPos) { // skip over any double '/' characters or '/' characters at the start lastPos++; } else if (pos == LyShine::NameType::npos) { // '/' not found, use whole remaining string AZ::Entity* entity = nullptr; EBUS_EVENT_ID_RESULT(entity, currentEntity->GetId(), UiElementBus, FindChildByName, name.substr(lastPos)); currentEntity = entity; if (currentEntity) { found = true; } break; } else { // use the part of the string between lastPos and pos (between the '/' characters) AZ::Entity* entity = nullptr; EBUS_EVENT_ID_RESULT(entity, currentEntity->GetId(), UiElementBus, FindChildByName, name.substr(lastPos, pos - lastPos)); currentEntity = entity; lastPos = pos + 1; } } return (found) ? currentEntity : nullptr; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::FindElements(AZStd::function predicate, LyShine::EntityArray& result) { // find all matching elements EBUS_EVENT_ID(m_rootElement, UiElementBus, FindDescendantElements, predicate, result); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiCanvasComponent::PickElement(AZ::Vector2 point) { AZ::Entity* element = nullptr; EBUS_EVENT_ID_RESULT(element, m_rootElement, UiElementBus, FindFrontmostChildContainingPoint, point, m_isLoadedInGame); return element; } //////////////////////////////////////////////////////////////////////////////////////////////////// LyShine::EntityArray UiCanvasComponent::PickElements(const AZ::Vector2& bound0, const AZ::Vector2& bound1) { LyShine::EntityArray elements; EBUS_EVENT_ID_RESULT(elements, m_rootElement, UiElementBus, FindAllChildrenIntersectingRect, bound0, bound1, m_isLoadedInGame); return elements; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasComponent::FindInteractableToHandleEvent(AZ::Vector2 point) { AZ::EntityId interactable; EBUS_EVENT_ID_RESULT(interactable, m_rootElement, UiElementBus, FindInteractableToHandleEvent, point); return interactable; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::SaveToXml(const string& assetIdPathname, const string& sourceAssetPathname) { PrepareAnimationSystemForCanvasSave(); // We are saving to the dev assets (source) not the cache so we use the sourceAssetPathname to save // the file bool result = SaveCanvasToFile(sourceAssetPathname, AZ::ObjectStream::ST_XML); if (result) { // We store the asset ID so that we can tell if the same file is being loaded from the game m_pathname = assetIdPathname; } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiCanvasInterface::ErrorCode UiCanvasComponent::CheckElementValidToSaveAsPrefab(AZ::Entity* entity) { AZ_Assert(entity, "null entity ptr passed to SaveAsPrefab"); // Check that none of the EntityId's in this entity or its children reference entities that // are not part of the prefab. // First make a list of all entityIds that will be in the prefab AZStd::vector entitiesInPrefab = GetEntityIdsOfElementAndDescendants(entity); // Next check all entity refs in the element to see if any are externel // We use ReplaceEntityRefs even though we don't want to change anything bool foundRefOutsidePrefab = false; AZ::SerializeContext* context = nullptr; EBUS_EVENT_RESULT(context, AZ::ComponentApplicationBus, GetSerializeContext); AZ_Assert(context, "No serialization context found"); AZ::EntityUtils::ReplaceEntityRefs(entity, [&](const AZ::EntityId& key, bool /*isEntityId*/) -> AZ::EntityId { if (key.IsValid()) { auto iter = AZStd::find(entitiesInPrefab.begin(), entitiesInPrefab.end(), key); if (iter == entitiesInPrefab.end()) { foundRefOutsidePrefab = true; } } return key; // always leave key unchanged }, context); if (foundRefOutsidePrefab) { return UiCanvasInterface::ErrorCode::PrefabContainsExternalEntityRefs; } return UiCanvasInterface::ErrorCode::NoError; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::SaveAsPrefab(const string& pathname, AZ::Entity* entity) { AZ_Assert(entity, "null entity ptr passed to SaveAsPrefab"); AZ::SerializeContext* context = nullptr; EBUS_EVENT_RESULT(context, AZ::ComponentApplicationBus, GetSerializeContext); AZ_Assert(context, "No serialization context found"); // To be sure that we do not save an invalid prefab, if this entity contains entity references // outside of the prefab set them to invalid references // First make a list of all entityIds that will be in the prefab AZStd::vector entitiesInPrefab = GetEntityIdsOfElementAndDescendants(entity); // Next make a serializable object containing all the entities to save (in order to check for invalid refs) AZ::SliceComponent::InstantiatedContainer sourceObjects(false); for (const AZ::EntityId& id : entitiesInPrefab) { AZ::Entity* sourceEntity = nullptr; EBUS_EVENT_RESULT(sourceEntity, AZ::ComponentApplicationBus, FindEntity, id); if (sourceEntity) { sourceObjects.m_entities.push_back(sourceEntity); } } // clone all the objects in order to replace external references AZ::SliceComponent::InstantiatedContainer* clonedObjects = context->CloneObject(&sourceObjects); AZ::Entity* clonedRootEntity = clonedObjects->m_entities[0]; // use ReplaceEntityRefs to replace external references with invalid IDs // Note that we are not generating new IDs so we do not need to fixup internal references AZ::EntityUtils::ReplaceEntityRefs(clonedObjects, [&](const AZ::EntityId& key, bool /*isEntityId*/) -> AZ::EntityId { if (key.IsValid()) { auto iter = AZStd::find(entitiesInPrefab.begin(), entitiesInPrefab.end(), key); if (iter == entitiesInPrefab.end()) { return AZ::EntityId(); } } return key; // leave key unchanged }, context); // make a wrapper object around the prefab entity so that we have an opportunity to change what // is in a prefab file in future. UiSerialize::PrefabFileObject fileObject; fileObject.m_rootEntityId = clonedRootEntity->GetId(); // add all of the entities that are not the root entity to a childEntities list for (auto descendant : clonedObjects->m_entities) { fileObject.m_entities.push_back(descendant); } bool result = AZ::Utils::SaveObjectToFile(pathname.c_str(), AZ::ObjectStream::ST_XML, &fileObject); // now delete the cloned entities we created, fixed up and saved delete clonedObjects; return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiCanvasComponent::LoadFromPrefab(const string& pathname, bool makeUniqueName, AZ::Entity* optionalInsertionPoint) { AZ::Entity* newEntity = nullptr; // Currently LoadObjectFromFile will hang if the file cannot be parsed // (LMBR-10078). So first check that it is in the right format if (!IsValidAzSerializedFile(pathname)) { return nullptr; } // The top level object in the file is a wrapper object called PrefabFileObject // this is to give us more protection against changes to what we store in the file in future // NOTE: this read doesn't support pak files but that is OK because prefab files are an // editor only feature. UiSerialize::PrefabFileObject* fileObject = AZ::Utils::LoadObjectFromFile(pathname.c_str()); AZ_Assert(fileObject, "Failed to load prefab"); if (fileObject) { // We want new IDs so generate them and fixup all references within the list of entities { AZ::SerializeContext* context = nullptr; EBUS_EVENT_RESULT(context, AZ::ComponentApplicationBus, GetSerializeContext); AZ_Assert(context, "No serialization context found"); AZ::SliceComponent::EntityIdToEntityIdMap entityIdMap; AZ::IdUtils::Remapper::GenerateNewIdsAndFixRefs(fileObject, entityIdMap, context); } // add all of the entities to this canvases EntityContext m_entityContext->AddUiEntities(fileObject->m_entities); EBUS_EVENT_RESULT(newEntity, AZ::ComponentApplicationBus, FindEntity, fileObject->m_rootEntityId); delete fileObject; // we do not keep the file wrapper object around if (makeUniqueName) { AZ::EntityId parentEntityId; if (optionalInsertionPoint) { parentEntityId = optionalInsertionPoint->GetId(); } AZStd::string uniqueName = GetUniqueChildName(parentEntityId, newEntity->GetName(), nullptr); newEntity->SetName(uniqueName); } UiElementComponent* elementComponent = newEntity->FindComponent(); AZ_Assert(elementComponent, "No element component found on prefab entity"); AZ::Entity* parent = (optionalInsertionPoint) ? optionalInsertionPoint : GetRootElement(); // recursively visit all the elements and set their canvas and parent pointers elementComponent->FixupPostLoad(newEntity, this, parent, true); // add this new entity as a child of the parent (insertionPoint or root) UiElementComponent* parentElementComponent = parent->FindComponent(); AZ_Assert(parentElementComponent, "No element component found on parent entity"); parentElementComponent->AddChild(newEntity); } return newEntity; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::FixupCreatedEntities(LyShine::EntityArray topLevelEntities, bool makeUniqueNamesAndIds, AZ::Entity* optionalInsertionPoint) { if (makeUniqueNamesAndIds) { AZ::EntityId parentEntityId; if (optionalInsertionPoint) { parentEntityId = optionalInsertionPoint->GetId(); } LyShine::EntityArray namedChildren; for (auto entity : topLevelEntities) { AZStd::string uniqueName = GetUniqueChildName(parentEntityId, entity->GetName(), &namedChildren); entity->SetName(uniqueName); namedChildren.push_back(entity); } } AZ::Entity* parent = (optionalInsertionPoint) ? optionalInsertionPoint : GetRootElement(); for (auto entity : topLevelEntities) { UiElementComponent* elementComponent = entity->FindComponent(); AZ_Assert(elementComponent, "No element component found on prefab entity"); // recursively visit all the elements and set their canvas and parent pointers elementComponent->FixupPostLoad(entity, this, parent, makeUniqueNamesAndIds); } if (m_isLoadedInGame) { // Call InGamePostActivate on all the created entities for (auto entity : topLevelEntities) { InGamePostActivateBottomUp(entity); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::AddElement(AZ::Entity* element, AZ::Entity* parent, AZ::Entity* insertBefore) { if (!parent) { parent = GetRootElement(); } // add this new entity as a child of the parent (insertionPoint or root) UiElementComponent* parentElementComponent = parent->FindComponent(); AZ_Assert(parentElementComponent, "No element component found on parent entity"); parentElementComponent->AddChild(element, insertBefore); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::ReinitializeElements() { // This gets called when a canvas or a slice in the canvas is reloaded. So, for example, // a Push to Slice in the editor causes a reload of that slice. It is only used in the editor. AZ::Entity* rootElement = GetRootElement(); UiElementComponent* elementComponent = rootElement->FindComponent(); AZ_Assert(elementComponent, "No element component found on root element entity"); elementComponent->FixupPostLoad(rootElement, this, nullptr, false); // All or some elements in the UI canvas have been recreated when ReinitializeElements is called. // This likely requires recompute of the transforms (in particular UiTextComponent requires this // if text is being wrapped, due to its delayed initialization that relies on OnCanvasSpaceRectChanged // being called). EBUS_EVENT_ID(m_rootElement, UiTransformBus, SetRecomputeFlags, UiTransformInterface::Recompute::RectAndTransform); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZStd::string UiCanvasComponent::SaveToXmlString() { PrepareAnimationSystemForCanvasSave(); AZStd::string charBuffer; AZ::IO::ByteContainerStream charStream(&charBuffer); bool success = SaveCanvasToStream(charStream, AZ::ObjectStream::ST_XML); AZ_Assert(success, "Failed to serialize canvas entity to XML"); return charBuffer; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZStd::string UiCanvasComponent::GetUniqueChildName(AZ::EntityId parentEntityId, AZStd::string baseName, const LyShine::EntityArray* includeChildren) { // Get a list of children that the name needs to be unique to LyShine::EntityArray children; if (parentEntityId.IsValid()) { EBUS_EVENT_ID_RESULT(children, parentEntityId, UiElementBus, GetChildElements); } else { children = GetChildElements(); } if (includeChildren) { children.push_back(*includeChildren); } // First, check if base name is unique if (IsElementNameUnique(baseName, children)) { return baseName; } // Count trailing digits in base name int i; for (i = baseName.length() - 1; i >= 0; i--) { if (!isdigit(baseName[i])) { break; } } int startDigitIndex = i + 1; int numDigits = baseName.length() - startDigitIndex; int suffix = 1; if (numDigits > 0) { // Set starting suffix suffix = AZStd::stoi(baseName.substr(startDigitIndex, numDigits)); // Trim the digits from the base name baseName.erase(startDigitIndex, numDigits); } // Keep incrementing suffix until a unique name is found // NOTE: This could cause a performance issue when large copies are being made in a large canvas AZStd::string proposedChildName; do { ++suffix; proposedChildName = baseName; AZStd::string suffixString = AZStd::string::format("%d", suffix); // Append leading zeros int numLeadingZeros = (suffixString.length() < numDigits) ? numDigits - suffixString.length() : 0; for (int zeroes = 0; zeroes < numLeadingZeros; zeroes++) { proposedChildName.push_back('0'); } // Append suffix proposedChildName.append(suffixString); } while (!IsElementNameUnique(proposedChildName, children)); return proposedChildName; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiCanvasComponent::CloneElement(AZ::Entity* sourceEntity, AZ::Entity* parentEntity) { return CloneAndAddElementInternal(sourceEntity, parentEntity, nullptr); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasComponent::CloneElementEntityId(AZ::EntityId sourceEntityId, AZ::EntityId parentEntityId, AZ::EntityId insertBeforeId) { AZ::EntityId result; AZ::Entity* sourceEntity = nullptr; EBUS_EVENT_RESULT(sourceEntity, AZ::ComponentApplicationBus, FindEntity, sourceEntityId); if (!sourceEntity) { AZ_Warning("UI", false, "CloneElementEntityId: Cannot find entity to clone."); return result; } AZ::Entity* parentEntity = nullptr; if (parentEntityId.IsValid()) { EBUS_EVENT_RESULT(parentEntity, AZ::ComponentApplicationBus, FindEntity, parentEntityId); if (!parentEntity) { AZ_Warning("UI", false, "CloneElementEntityId: Cannot find parent entity."); return result; } } else { parentEntity = GetRootElement(); } AZ::Entity* insertBeforeEntity = nullptr; if (insertBeforeId.IsValid()) { EBUS_EVENT_RESULT(insertBeforeEntity, AZ::ComponentApplicationBus, FindEntity, insertBeforeId); if (!insertBeforeEntity) { AZ_Warning("UI", false, "CloneElementEntityId: Cannot find insertBefore entity."); return result; } } AZ::Entity* clonedEntity = CloneAndAddElementInternal(sourceEntity, parentEntity, insertBeforeEntity); if (clonedEntity) { result = clonedEntity->GetId(); } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiCanvasComponent::CloneCanvas(const AZ::Vector2& canvasSize) { UiGameEntityContext* entityContext = new UiGameEntityContext(); UiCanvasComponent* canvasComponent = CloneAndInitializeCanvas(entityContext, m_pathname, &canvasSize); AZ::Entity* newCanvasEntity = nullptr; if (canvasComponent) { newCanvasEntity = canvasComponent->GetEntity(); canvasComponent->m_isLoadedInGame = true; // The game entity context needs to know its corresponding canvas entity for instantiating dynamic slices entityContext->SetCanvasEntity(newCanvasEntity->GetId()); } else { delete entityContext; } return newCanvasEntity; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetCanvasToViewportMatrix(const AZ::Matrix4x4& matrix) { if (!m_canvasToViewportMatrix.IsClose(matrix)) { m_canvasToViewportMatrix = matrix; m_viewportToCanvasMatrix = m_canvasToViewportMatrix.GetInverseTransform(); EBUS_EVENT_ID(GetRootElement()->GetId(), UiTransformBus, SetRecomputeFlags, UiTransformInterface::Recompute::ViewportTransformOnly); } } //////////////////////////////////////////////////////////////////////////////////////////////////// const AZ::Matrix4x4& UiCanvasComponent::GetCanvasToViewportMatrix() { return m_canvasToViewportMatrix; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::GetViewportToCanvasMatrix(AZ::Matrix4x4& matrix) { matrix = m_viewportToCanvasMatrix; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Vector2 UiCanvasComponent::GetCanvasSize() { return m_targetCanvasSize; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetCanvasSize(const AZ::Vector2& canvasSize) { m_canvasSize = canvasSize; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetTargetCanvasSize(bool isInGame, const AZ::Vector2& targetCanvasSize) { if (m_renderToTexture) { // when a canvas is set to render to texture the target canvas size is always the authored canvas size SetTargetCanvasSizeAndUniformScale(isInGame, m_canvasSize); } else { SetTargetCanvasSizeAndUniformScale(isInGame, targetCanvasSize); } } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Vector2 UiCanvasComponent::GetDeviceScale() { return m_deviceScale; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::GetIsPixelAligned() { return m_isPixelAligned; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetIsPixelAligned(bool isPixelAligned) { m_isPixelAligned = isPixelAligned; EBUS_EVENT_ID(GetEntityId(), UiCanvasPixelAlignmentNotificationBus, OnCanvasPixelAlignmentChange); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::GetIsTextPixelAligned() { return m_isTextPixelAligned; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetIsTextPixelAligned(bool isTextPixelAligned) { m_isTextPixelAligned = isTextPixelAligned; EBUS_EVENT_ID(GetEntityId(), UiCanvasPixelAlignmentNotificationBus, OnCanvasTextPixelAlignmentChange); } //////////////////////////////////////////////////////////////////////////////////////////////////// IUiAnimationSystem* UiCanvasComponent::GetAnimationSystem() { return &m_uiAnimationSystem; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::GetEnabled() { return m_enabled; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetEnabled(bool enabled) { if (m_enabled != enabled) { m_enabled = enabled; MarkRenderGraphDirty(); EBUS_EVENT(UiCanvasEnabledStateNotificationBus, OnCanvasEnabledStateChanged, GetEntityId(), m_enabled); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::GetIsRenderToTexture() { return m_renderToTexture; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetIsRenderToTexture(bool isRenderToTexture) { m_renderToTexture = isRenderToTexture; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZStd::string UiCanvasComponent::GetRenderTargetName() { return m_renderTargetName; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetRenderTargetName(const AZStd::string& name) { if (name != m_renderTargetName && !name.empty()) { DestroyRenderTarget(); m_renderTargetName = name; CreateRenderTarget(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::GetIsPositionalInputSupported() { return m_isPositionalInputSupported; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetIsPositionalInputSupported(bool isSupported) { m_isPositionalInputSupported = isSupported; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::GetIsConsumingAllInputEvents() { return m_isConsumingAllInputEvents; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetIsConsumingAllInputEvents(bool isConsuming) { m_isConsumingAllInputEvents = isConsuming; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::GetIsMultiTouchSupported() { return m_isMultiTouchSupported; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetIsMultiTouchSupported(bool isSupported) { m_isMultiTouchSupported = isSupported; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::GetIsNavigationSupported() { return m_isNavigationSupported; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetIsNavigationSupported(bool isSupported) { m_isNavigationSupported = isSupported; SetFirstHoverInteractable(); } //////////////////////////////////////////////////////////////////////////////////////////////////// float UiCanvasComponent::GetNavigationThreshold() { return m_navigationThreshold; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetNavigationThreshold(float navigationThreshold) { m_navigationThreshold = navigationThreshold; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::u64 UiCanvasComponent::GetNavigationRepeatDelay() { return m_navigationRepeatDelay; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetNavigationRepeatDelay(AZ::u64 navigationRepeatDelay) { m_navigationRepeatDelay = navigationRepeatDelay; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::u64 UiCanvasComponent::GetNavigationRepeatPeriod() { return m_navigationRepeatPeriod; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetNavigationRepeatPeriod(AZ::u64 navigationRepeatPeriod) { m_navigationRepeatPeriod = navigationRepeatPeriod; } //////////////////////////////////////////////////////////////////////////////////////////////////// AzFramework::LocalUserId UiCanvasComponent::GetLocalUserIdInputFilter() { return m_localUserIdInputFilter; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetLocalUserIdInputFilter(AzFramework::LocalUserId localUserId) { m_localUserIdInputFilter = localUserId; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::HandleInputEvent(const AzFramework::InputChannel::Snapshot& inputSnapshot, const AZ::Vector2* viewportPos, AzFramework::ModifierKeyMask activeModifierKeys) { // Ignore input events if we're not enabled if (!m_enabled) { return false; } if (m_localUserIdInputFilter != AzFramework::LocalUserIdAny && m_localUserIdInputFilter != inputSnapshot.m_localUserId) { // Ignore input events if they were not generated by the desired local user id return false; } if (inputSnapshot.m_channelId == AzFramework::InputDeviceMouse::Movement::X || inputSnapshot.m_channelId == AzFramework::InputDeviceMouse::Movement::Y || inputSnapshot.m_channelId == AzFramework::InputDeviceMouse::Movement::Z) { // Ignore the individual mouse movement input channels. // X, Y are handled through the SystemCursorPosition input channel. // Z (scroll wheel) functionality is not currently supported on the canvas level return m_isConsumingAllInputEvents; } if (AzFramework::InputDeviceKeyboard::IsKeyboardDevice(inputSnapshot.m_deviceId) || AzFramework::InputDeviceVirtualKeyboard::IsVirtualKeyboardDevice(inputSnapshot.m_deviceId) || AzFramework::InputDeviceGamepad::IsGamepadDevice(inputSnapshot.m_deviceId)) { return HandleKeyInputEvent(inputSnapshot, activeModifierKeys) || m_isConsumingAllInputEvents; } else if (viewportPos) { if (!m_renderToTexture && m_isPositionalInputSupported) { if (HandleInputPositionalEvent(inputSnapshot, *viewportPos)) { return true; } } } return m_isConsumingAllInputEvents; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::HandleTextEvent(const AZStd::string& textUTF8) { // Ignore input events if we're not enabled if (!m_enabled) { return false; } if (m_activeInteractable.IsValid()) { EBUS_EVENT_ID(m_activeInteractable, UiInteractableBus, HandleTextInput, textUTF8); return true; } return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::HandleInputPositionalEvent(const AzFramework::InputChannel::Snapshot& inputSnapshot, AZ::Vector2 viewportPos) { if (AzFramework::InputDeviceMouse::IsMouseDevice(inputSnapshot.m_deviceId)) { if (m_lastMousePosition != viewportPos) { // Check if the mouse position has been initialized if (m_lastMousePosition.GetX() >= 0.0f && m_lastMousePosition.GetY() >= 0.0f) { // Mouse moved, resume handling hover input events if there is no active interactable if (!m_activeInteractable.IsValid()) { s_handleHoverInputEvents = true; } } m_lastMousePosition = viewportPos; } } // Currently we are just interested in mouse events and the primary touch for hover events if (AzFramework::InputDeviceMouse::IsMouseDevice(inputSnapshot.m_deviceId) || inputSnapshot.m_channelId == AzFramework::InputDeviceTouch::Touch::Index0) { if (s_handleHoverInputEvents) { HandleHoverInputEvent(viewportPos); } } // Currently we are just interested in mouse button 1 events and UI events here if (inputSnapshot.m_channelId == AzFramework::InputDeviceMouse::Button::Left || inputSnapshot.m_channelId == AzFramework::InputDeviceTouch::Touch::Index0) { if (inputSnapshot.m_state == AzFramework::InputChannel::State::Began) { return HandlePrimaryPress(viewportPos); } else if (inputSnapshot.m_state == AzFramework::InputChannel::State::Ended) { if (inputSnapshot.m_channelId == AzFramework::InputDeviceTouch::Touch::Index0) { ClearHoverInteractable(); } return HandlePrimaryRelease(viewportPos); } else if (inputSnapshot.m_state == AzFramework::InputChannel::State::Updated) { return HandlePrimaryUpdate(viewportPos); } } // ...while all other events from touch devices should be treated as multi-touch else if (AzFramework::InputDeviceTouch::IsTouchDevice(inputSnapshot.m_deviceId)) { const auto& touchIndexIt = AZStd::find(AzFramework::InputDeviceTouch::Touch::All.cbegin(), AzFramework::InputDeviceTouch::Touch::All.cend(), inputSnapshot.m_channelId); if (touchIndexIt != AzFramework::InputDeviceTouch::Touch::All.cend()) { const int touchindex = static_cast(touchIndexIt - AzFramework::InputDeviceTouch::Touch::All.cbegin()); if (inputSnapshot.m_state == AzFramework::InputChannel::State::Began) { return HandleMultiTouchPress(viewportPos, touchindex); } else if (inputSnapshot.m_state == AzFramework::InputChannel::State::Ended) { return HandleMultiTouchRelease(viewportPos, touchindex); } else if (inputSnapshot.m_state == AzFramework::InputChannel::State::Updated) { return HandleMultiTouchUpdated(viewportPos, touchindex); } } } return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Vector2 UiCanvasComponent::GetMousePosition() { return m_lastMousePosition; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasComponent::GetTooltipDisplayElement() { return m_tooltipDisplayElement; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetTooltipDisplayElement(AZ::EntityId entityId) { m_tooltipDisplayElement = entityId; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::ForceFocusInteractable(AZ::EntityId interactableId) { if (interactableId.IsValid()) { AZ::EntityId lastHoverInteractable = m_hoverInteractable; // Force the interactable to have the hover. Will also auto activate the // interactable if the flag is set ForceHoverInteractable(interactableId); // Will also set as active interactable CheckHoverInteractableAndAutoActivate(lastHoverInteractable, UiNavigationHelpers::Command::Unknown, true); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::ForceActiveInteractable(AZ::EntityId interactableId, bool shouldStayActive, AZ::Vector2 point) { SetHoverInteractable(interactableId); SetActiveInteractable(interactableId, shouldStayActive); m_lastMousePosition = point; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasComponent::GetHoverInteractable() { return m_hoverInteractable; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::ForceHoverInteractable(AZ::EntityId newHoverInteractable) { if (!m_isNavigationSupported) { AZ_Warning("UI", false, "This UI canvas does not support keyboard/gamepad input events"); return; } if (newHoverInteractable.IsValid()) { // Make sure the element is an interactable that is handling events if (!IsValidInteractable(newHoverInteractable)) { AZ_Warning("UI", false, "Entity is either not an interactable, not enabled or is not accepting events"); return; } // Make sure the active interactable and the hover interactible are the same if (m_activeInteractable.IsValid() && (m_activeInteractable != newHoverInteractable)) { ClearActiveInteractable(); } } SetHoverInteractable(newHoverInteractable); if (m_hoverInteractable.IsValid()) { s_handleHoverInputEvents = false; s_allowClearingHoverInteractableOnHoverInput = false; AZ::EntityId ancestorInteractable = FindAncestorInteractable(m_hoverInteractable); if (ancestorInteractable.IsValid()) { // Send an event that the descendant interactable became the hover interactable via navigation EBUS_EVENT_ID(ancestorInteractable, UiInteractableBus, HandleDescendantReceivedHoverByNavigation, m_hoverInteractable); } CheckHoverInteractableAndAutoActivate(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::ClearAllInteractables() { m_multiTouchInteractablesByTouchIndex.clear(); ClearActiveInteractable(); // Clear hover interactable if last input was positional (mouse/touch) if (s_handleHoverInputEvents) { ClearHoverInteractable(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::ForceEnterInputEventOnInteractable(AZ::EntityId interactableId) { if (!m_isNavigationSupported) { AZ_Warning("UI", false, "This UI canvas does not support keyboard/gamepad input events"); return; } if (!interactableId.IsValid()) { AZ_Warning("UI", false, "EntityId is not valid"); return; } // Make sure the element is an interactable that is handling events if (!IsValidInteractable(interactableId)) { AZ_Warning("UI", false, "Entity is either not an interactable, not enabled or is not accepting events"); return; } // Set the hover interactable to accept the events if (m_hoverInteractable != interactableId) { ForceHoverInteractable(interactableId); } // Generate Enter key pressed input event AzFramework::InputChannel::Snapshot snapshot(AzFramework::InputDeviceKeyboard::Key::EditEnter, AzFramework::InputDeviceKeyboard::Id, AzFramework::InputChannel::State::Began); HandleEnterInputEvent(UiNavigationHelpers::Command::Enter, snapshot); // Generate Enter key released input event snapshot.m_state = AzFramework::InputChannel::State::Ended; HandleEnterInputEvent(UiNavigationHelpers::Command::Enter, snapshot); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::LoadAtlases() { if (m_atlases.size() > 0) { // Atlases already loaded return; } for (int i = 0; i < m_atlasPathNames.size(); ++i) { const AZStd::string& atlasAssetPath = m_atlasPathNames[i].GetAssetPath(); if (!atlasAssetPath.empty()) // no need to print an error message for empty strings { TextureAtlasNamespace::TextureAtlas* atlas = nullptr; TextureAtlasNamespace::TextureAtlasRequestBus::BroadcastResult(atlas, &TextureAtlasNamespace::TextureAtlasRequests::LoadAtlas, atlasAssetPath); if (atlas != nullptr) { m_atlases.push_back(atlas); } else { AZ_Error("UI", false, "UI canvas: %s failed to load texture atlas: %s", m_pathname.c_str(), atlasAssetPath.c_str()); } } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::UnloadAtlases() { while (m_atlases.size() > 0) { TextureAtlasNamespace::TextureAtlasRequestBus::Broadcast(&TextureAtlasNamespace::TextureAtlasRequests::UnloadAtlas, m_atlases.back()); m_atlases.pop_back(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::ReloadAtlases() { UnloadAtlases(); LoadAtlases(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::OnEntityDeactivated(const AZ::EntityId& entityId) { AZ::EntityBus::Handler::BusDisconnect(entityId); if (entityId == m_hoverInteractable) { ClearHoverInteractable(); // If we are using keyboard/gamepad navigation we should set a new hover interactable SetFirstHoverInteractable(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::StartSequence(const AZStd::string& sequenceName) { IUiAnimSequence* sequence = m_uiAnimationSystem.FindSequence(sequenceName.c_str()); if (sequence) { m_uiAnimationSystem.AddUiAnimationListener(sequence, this); m_uiAnimationSystem.PlaySequence(sequence, nullptr, false, false); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::PlaySequenceRange(const AZStd::string& sequenceName, float startTime, float endTime) { IUiAnimSequence* sequence = m_uiAnimationSystem.FindSequence(sequenceName.c_str()); if (sequence) { m_uiAnimationSystem.AddUiAnimationListener(sequence, this); m_uiAnimationSystem.PlaySequence(sequence, nullptr, false, false, startTime, endTime); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::StopSequence(const AZStd::string& sequenceName) { IUiAnimSequence* sequence = m_uiAnimationSystem.FindSequence(sequenceName.c_str()); if (sequence) { m_uiAnimationSystem.StopSequence(sequence); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::AbortSequence(const AZStd::string& sequenceName) { IUiAnimSequence* sequence = m_uiAnimationSystem.FindSequence(sequenceName.c_str()); if (sequence) { m_uiAnimationSystem.AbortSequence(sequence); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::PauseSequence(const AZStd::string& sequenceName) { IUiAnimSequence* sequence = m_uiAnimationSystem.FindSequence(sequenceName.c_str()); if (sequence) { sequence->Pause(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::ResumeSequence(const AZStd::string& sequenceName) { IUiAnimSequence* sequence = m_uiAnimationSystem.FindSequence(sequenceName.c_str()); if (sequence) { sequence->Resume(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::ResetSequence(const AZStd::string& sequenceName) { IUiAnimSequence* sequence = m_uiAnimationSystem.FindSequence(sequenceName.c_str()); if (sequence) { sequence->Reset(true); } } //////////////////////////////////////////////////////////////////////////////////////////////////// float UiCanvasComponent::GetSequencePlayingSpeed(const AZStd::string& sequenceName) { IUiAnimSequence* sequence = m_uiAnimationSystem.FindSequence(sequenceName.c_str()); return m_uiAnimationSystem.GetPlayingSpeed(sequence); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetSequencePlayingSpeed(const AZStd::string& sequenceName, float speed) { IUiAnimSequence* sequence = m_uiAnimationSystem.FindSequence(sequenceName.c_str()); m_uiAnimationSystem.SetPlayingSpeed(sequence, speed); } //////////////////////////////////////////////////////////////////////////////////////////////////// float UiCanvasComponent::GetSequencePlayingTime(const AZStd::string& sequenceName) { IUiAnimSequence* sequence = m_uiAnimationSystem.FindSequence(sequenceName.c_str()); return m_uiAnimationSystem.GetPlayingTime(sequence); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::IsSequencePlaying(const AZStd::string& sequenceName) { IUiAnimSequence* sequence = m_uiAnimationSystem.FindSequence(sequenceName.c_str()); if (sequence) { return m_uiAnimationSystem.IsPlaying(sequence); } return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// float UiCanvasComponent::GetSequenceLength(const AZStd::string& sequenceName) { float length = 0.f; IUiAnimSequence* sequence = m_uiAnimationSystem.FindSequence(sequenceName.c_str()); if (sequence) { auto range = sequence->GetTimeRange(); length = range.Length(); } return length; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetSequenceStopBehavior(IUiAnimationSystem::ESequenceStopBehavior stopBehavior) { m_uiAnimationSystem.SetSequenceStopBehavior(stopBehavior); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::ActiveCancelled() { // currently we are only connected to one UiInteractableActiveNotificationBus so we know it is the // pressed interactable. If we could be connected to several we would need to change // the ActiveCancelled method to pass the EntityId. if (m_activeInteractable.IsValid()) { UiInteractableActiveNotificationBus::Handler::BusDisconnect(m_activeInteractable); m_activeInteractable.SetInvalid(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// // change the active interactable to the given one void UiCanvasComponent::ActiveChanged(AZ::EntityId m_newActiveInteractable, bool shouldStayActive) { // There should always be an active interactable at this point, disconnect from it if (m_activeInteractable.IsValid()) { UiInteractableActiveNotificationBus::Handler::BusDisconnect(m_activeInteractable); m_activeInteractable.SetInvalid(); } // The m_newActiveInteractable should always be valid but check anyway if (m_newActiveInteractable.IsValid()) { m_activeInteractable = m_newActiveInteractable; UiInteractableActiveNotificationBus::Handler::BusConnect(m_activeInteractable); m_activeInteractableShouldStayActive = shouldStayActive; } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::OnPreRender() { RenderCanvasToTexture(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::OnUiAnimationEvent(EUiAnimationEvent uiAnimationEvent, IUiAnimSequence* pAnimSequence) { // Queue the event to prevent deletions during the canvas update EBUS_QUEUE_EVENT_ID(GetEntityId(), UiAnimationNotificationBus, OnUiAnimationEvent, uiAnimationEvent, pAnimSequence->GetName()); // Stop listening to events if ((uiAnimationEvent == EUiAnimationEvent::eUiAnimationEvent_Stopped) || (uiAnimationEvent == EUiAnimationEvent::eUiAnimationEvent_Aborted)) { m_uiAnimationSystem.RemoveUiAnimationListener(pAnimSequence, this); } } void UiCanvasComponent::OnUiTrackEvent(AZStd::string eventName, AZStd::string valueName, IUiAnimSequence* pAnimSequence) { // Queue the event to prevent deletions during the canvas update EBUS_QUEUE_EVENT_ID(GetEntityId(), UiAnimationNotificationBus, OnUiTrackEvent, eventName, valueName, pAnimSequence->GetName()); } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::GetIsSnapEnabled() { return m_isSnapEnabled; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetIsSnapEnabled(bool enabled) { m_isSnapEnabled = enabled; } //////////////////////////////////////////////////////////////////////////////////////////////////// float UiCanvasComponent::GetSnapDistance() { return m_snapDistance; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetSnapDistance(float distance) { m_snapDistance = distance; } //////////////////////////////////////////////////////////////////////////////////////////////////// float UiCanvasComponent::GetSnapRotationDegrees() { return m_snapRotationDegrees; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetSnapRotationDegrees(float degrees) { m_snapRotationDegrees = degrees; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZStd::vector UiCanvasComponent::GetHorizontalGuidePositions() { return m_horizontalGuidePositions; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::AddHorizontalGuide(float position) { m_horizontalGuidePositions.push_back(position); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::RemoveHorizontalGuide(int index) { if (index < m_horizontalGuidePositions.size()) { m_horizontalGuidePositions.erase(m_horizontalGuidePositions.begin() + index); } else { AZ_Warning("UI", false, "Index out of range in RemoveHorizontalGuide"); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetHorizontalGuidePosition(int index, float position) { if (index < m_horizontalGuidePositions.size()) { m_horizontalGuidePositions[index] = position; } else { AZ_Warning("UI", false, "Index out of range in SetHorizontalGuidePosition"); } } //////////////////////////////////////////////////////////////////////////////////////////////////// AZStd::vector UiCanvasComponent::GetVerticalGuidePositions() { return m_verticalGuidePositions; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::AddVerticalGuide(float position) { m_verticalGuidePositions.push_back(position); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::RemoveVerticalGuide(int index) { if (index < m_verticalGuidePositions.size()) { m_verticalGuidePositions.erase(m_verticalGuidePositions.begin() + index); } else { AZ_Warning("UI", false, "Index out of range in RemoveVerticalGuide"); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetVerticalGuidePosition(int index, float position) { if (index < m_verticalGuidePositions.size()) { m_verticalGuidePositions[index] = position; } else { AZ_Warning("UI", false, "Index out of range in SetVerticalGuidePosition"); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::RemoveAllGuides() { m_horizontalGuidePositions.clear(); m_verticalGuidePositions.clear(); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Color UiCanvasComponent::GetGuideColor() { return m_guideColor; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetGuideColor(const AZ::Color& color) { m_guideColor = color; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::GetGuidesAreLocked() { return m_guidesAreLocked; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetGuidesAreLocked(bool areLocked) { m_guidesAreLocked = areLocked; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::CheckForOrphanedElements() { AZ::SliceComponent::EntityList orphanedEntities; GetOrphanedElements(orphanedEntities); return !orphanedEntities.empty(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::RecoverOrphanedElements() { AZ::SliceComponent::EntityList orphanedEntities; GetOrphanedElements(orphanedEntities); // we will put the orphaned elements under a top-level element called this: const char* recoveredOrphansName = "RecoveredOrphans"; // If the recovered orphans element does not already at the top-level of the canvas exist then create it AZ::Entity* recoveredOrphansElement = FindElementByHierarchicalName(recoveredOrphansName); if (!recoveredOrphansElement) { recoveredOrphansElement = CreateChildElement(recoveredOrphansName); recoveredOrphansElement->Deactivate(); recoveredOrphansElement->CreateComponent(LyShine::UiTransform2dComponentUuid); recoveredOrphansElement->Activate(); } // We have to find the top-level elements within the orphans and add them as children of recoveredOrphansElement. // First we make a set of all the orphans that are referenced as children of other orphans. AZStd::unordered_set referencedChildren; for (AZ::Entity* orphan : orphanedEntities) { UiElementComponent* orphanElementComponent = orphan->FindComponent(); int numChildren = orphanElementComponent->GetNumChildElements(); for (int i = 0; i < numChildren; ++i) { AZ::EntityId childId = orphanElementComponent->GetChildEntityId(i); referencedChildren.insert(childId); } } // Any orphans that are not in the set are top-level orphans and should be added. UiElementComponent* recoveredOrphansElementComponent = recoveredOrphansElement->FindComponent(); for (AZ::Entity* orphan : orphanedEntities) { if (referencedChildren.find(orphan->GetId()) == referencedChildren.end()) { // First add the orphan as a child of the recoveredOrphansElement recoveredOrphansElementComponent->AddChild(orphan); // Then fixup all the parent, canvas, child pointers in the orphan and its children UiElementComponent* orphanElementComponent = orphan->FindComponent(); orphanElementComponent->FixupPostLoad(orphan, this, recoveredOrphansElement, false); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::RemoveOrphanedElements() { // get the orphaned entities AZ::SliceComponent::EntityList orphanedEntities; GetOrphanedElements(orphanedEntities); // remove the entities from the entity context, this will remove any slice instances and references that become empty for (AZ::Entity* orphan : orphanedEntities) { m_entityContext->DestroyEntity(orphan); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::UpdateCanvasInEditorViewport(float deltaTime, bool isInGame) { UpdateCanvas(deltaTime, isInGame); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::RenderCanvasInEditorViewport(bool isInGame, AZ::Vector2 viewportSize) { GetUiRenderer()->BeginUiFrameRender(); RenderCanvas(isInGame, viewportSize); GetUiRenderer()->EndUiFrameRender(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::MarkRenderGraphDirty() { // It is possible that the loading screen can result in this being called while we are already // rendering this canvas. We never want to set the dirty flag while rendering, if the dirty // flag is not already set it could result in an incomplete renderGraph being created since // the render graph will be cleared. if (!m_isRendering) { m_renderGraph.SetDirtyFlag(true); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::UpdateCanvas(float deltaTime, bool isInGame) { // Ignore update if we're not enabled if (!m_enabled) { return; } if (isInGame) { EBUS_EVENT_ID(GetEntityId(), UiCanvasUpdateNotificationBus, Update, deltaTime); // update the animation system m_uiAnimationSystem.PreUpdate(deltaTime); m_uiAnimationSystem.PostUpdate(deltaTime); } else { EBUS_EVENT_ID(GetEntityId(), UiCanvasUpdateNotificationBus, UpdateInEditor, deltaTime); } DestroyScheduledElements(); SendRectChangeNotificationsAndRecomputeLayouts(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::RenderCanvas(bool isInGame, AZ::Vector2 viewportSize) { // Ignore render ops if we're not enabled if (!m_enabled) { return; } // It is possible, due to the LoadScreenComponent, for this canvas to have Render called while it is rendering. // This is rare but can happen because rendering of text can call FontCreateTexture which results in CreateTextureObject // being called, which has a load scren update in it. Rendering the canvas to the render graph while already in the // process of doing so can corrupt the render graph by adding an element to an intrusive list that is already in the // list. // We could prevent this at the CLyShine::Render level. But doing it here with an m_isRendering flag also allows us to // check for the error condition where MarkRenderGraphDirty (which clears the render graph) is called during rendering. if (m_isRendering) { return; } m_isRendering = true; if (m_renderGraph.GetDirtyFlag()) { m_renderGraph.ResetGraph(); EBUS_EVENT_ID(m_rootElement, UiElementBus, RenderElement, &m_renderGraph, isInGame); m_renderGraph.SetDirtyFlag(false); m_renderGraph.FinalizeGraph(); } if (!m_renderGraph.IsEmpty()) { GetUiRenderer()->BeginCanvasRender(); m_renderGraph.Render(GetUiRenderer(), viewportSize); GetUiRenderer()->EndCanvasRender(); } m_isRendering = false; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiCanvasComponent::GetRootElement() const { AZ::Entity* rootEntity = nullptr; EBUS_EVENT_RESULT(rootEntity, AZ::ComponentApplicationBus, FindEntity, m_rootElement); return rootEntity; } //////////////////////////////////////////////////////////////////////////////////////////////////// LyShine::ElementId UiCanvasComponent::GenerateId() { return ++m_lastElementId; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Vector2 UiCanvasComponent::GetTargetCanvasSize() { return m_targetCanvasSize; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::ScheduleElementForTransformRecompute(UiElementComponent* elementComponent) { // Do not add if already in the list if (!elementComponent->m_next) { m_elementsNeedingTransformRecompute.push_back(*elementComponent); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::UnscheduleElementForTransformRecompute(UiElementComponent* elementComponent) { // Do not erase if not in list if (elementComponent->m_next) { m_elementsNeedingTransformRecompute.erase(ElementComponentSlist::const_iterator_impl(elementComponent)); elementComponent->m_next = nullptr; } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::ScheduleElementDestroy(AZ::EntityId entityId) { m_elementsScheduledForDestroy.push_back(entityId); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::DestroyScheduledElements() { for (auto entityId : m_elementsScheduledForDestroy) { UiElementComponent::DestroyElementEntity(entityId); } m_elementsScheduledForDestroy.clear(); } //////////////////////////////////////////////////////////////////////////////////////////////////// #ifndef _RELEASE void UiCanvasComponent::GetDebugInfoInteractables(AZ::EntityId& activeInteractable, AZ::EntityId& hoverInteractable) const { activeInteractable = m_activeInteractable; hoverInteractable = m_hoverInteractable; } void UiCanvasComponent::GetDebugInfoNumElements(DebugInfoNumElements& info) const { info.m_numElements = 0; info.m_numEnabledElements = 0; info.m_numRenderElements = 0; info.m_numRenderControlElements = 0; info.m_numImageElements = 0; info.m_numTextElements = 0; info.m_numMaskElements = 0; info.m_numFaderElements = 0; info.m_numInteractableElements = 0; info.m_numUpdateElements = UiCanvasUpdateNotificationBus::GetNumOfEventHandlers(GetEntityId()); DebugInfoCountChildren(m_rootElement, true, info); } void UiCanvasComponent::GetDebugInfoRenderGraph(LyShineDebug::DebugInfoRenderGraph& info) const { m_renderGraph.GetDebugInfoRenderGraph(info); } void UiCanvasComponent::DebugInfoCountChildren(const AZ::EntityId entity, bool parentEnabled, DebugInfoNumElements& info) const { int numChildElements = 0; EBUS_EVENT_ID_RESULT(numChildElements, entity, UiElementBus, GetNumChildElements); info.m_numElements += numChildElements; for (int i = 0; i < numChildElements; ++i) { AZ::EntityId child; EBUS_EVENT_ID_RESULT(child, entity, UiElementBus, GetChildEntityId, i); bool isEnabled = false; EBUS_EVENT_ID_RESULT(isEnabled, child, UiElementBus, IsEnabled); if (isEnabled && parentEnabled) { ++info.m_numEnabledElements; if (UiRenderBus::FindFirstHandler(child)) { ++info.m_numRenderElements; } if (UiRenderControlBus::FindFirstHandler(child)) { ++info.m_numRenderControlElements; } if (UiImageBus::FindFirstHandler(child)) { ++info.m_numImageElements; } if (UiTextBus::FindFirstHandler(child)) { ++info.m_numTextElements; } if (UiMaskBus::FindFirstHandler(child)) { ++info.m_numMaskElements; } if (UiFaderBus::FindFirstHandler(child)) { ++info.m_numFaderElements; } if (UiInteractableBus::FindFirstHandler(child)) { ++info.m_numInteractableElements; } } DebugInfoCountChildren(child, isEnabled && parentEnabled, info); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::DebugReportDrawCalls(AZ::IO::HandleType fileHandle, LyShineDebug::DebugInfoDrawCallReport& reportInfo, void* context) const { m_renderGraph.DebugReportDrawCalls(fileHandle, reportInfo, context); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::DebugDisplayElemBounds(IDraw2d* draw2d) const { DebugDisplayChildElemBounds(draw2d, m_rootElement); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::DebugDisplayChildElemBounds(IDraw2d* draw2d, const AZ::EntityId entity) const { AZ::u64 time = AZStd::GetTimeUTCMilliSecond(); uint32 fractionsOfOneSecond = time % 1000; uint32 fractionsOfHalfSecond = (fractionsOfOneSecond > 500) ? 1000 - fractionsOfOneSecond : fractionsOfOneSecond; float brightness = fractionsOfHalfSecond / 500.0f; UiTransformInterface::RectPoints points; int numChildElements = 0; EBUS_EVENT_ID_RESULT(numChildElements, entity, UiElementBus, GetNumChildElements); for (int i = 0; i < numChildElements; ++i) { AZ::EntityId child; EBUS_EVENT_ID_RESULT(child, entity, UiElementBus, GetChildEntityId, i); bool isEnabled = false; EBUS_EVENT_ID_RESULT(isEnabled, child, UiElementBus, IsEnabled); if (isEnabled) { EBUS_EVENT_ID(entity, UiTransformBus, GetViewportSpacePoints, points); AZ::Color color(brightness, brightness, brightness, 1.0f); draw2d->DrawLine(points.TopLeft(), points.TopRight(), color); draw2d->DrawLine(points.TopRight(), points.BottomRight(), color); draw2d->DrawLine(points.BottomRight(), points.BottomLeft(), color); draw2d->DrawLine(points.BottomLeft(), points.TopLeft(), color); DebugDisplayChildElemBounds(draw2d, child); } } } #endif //////////////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC STATIC MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::Reflect(AZ::ReflectContext* context) { AZ::SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) { UiAnimationSystem::Reflect(serializeContext); serializeContext->Class() ->Version(3, &VersionConverter) // Not in properties pane ->Field("UniqueId", &UiCanvasComponent::m_uniqueId) ->Field("RootElement", &UiCanvasComponent::m_rootElement) ->Field("LastElement", &UiCanvasComponent::m_lastElementId) ->Field("CanvasSize", &UiCanvasComponent::m_canvasSize) ->Field("IsSnapEnabled", &UiCanvasComponent::m_isSnapEnabled) // Rendering group ->Field("DrawOrder", &UiCanvasComponent::m_drawOrder) ->Field("IsPixelAligned", &UiCanvasComponent::m_isPixelAligned) ->Field("IsTextPixelAligned", &UiCanvasComponent::m_isTextPixelAligned) ->Field("RenderToTexture", &UiCanvasComponent::m_renderToTexture) ->Field("RenderTargetName", &UiCanvasComponent::m_renderTargetName) // Input group ->Field("IsPosInputSupported", &UiCanvasComponent::m_isPositionalInputSupported) ->Field("IsConsumingAllInput", &UiCanvasComponent::m_isConsumingAllInputEvents) ->Field("IsMultiTouchSupported", &UiCanvasComponent::m_isMultiTouchSupported) ->Field("IsNavigationSupported", &UiCanvasComponent::m_isNavigationSupported) ->Field("NavigationThreshold", &UiCanvasComponent::m_navigationThreshold) ->Field("NavigationRepeatDelay", &UiCanvasComponent::m_navigationRepeatDelay) ->Field("NavigationRepeatPeriod", &UiCanvasComponent::m_navigationRepeatPeriod) ->Field("FirstHoverElement", &UiCanvasComponent::m_firstHoverInteractable) ->Field("AnimSystem", &UiCanvasComponent::m_uiAnimationSystem) ->Field("AnimationData", &UiCanvasComponent::m_serializedAnimationData) // Tooltips group ->Field("TooltipDisplayElement", &UiCanvasComponent::m_tooltipDisplayElement) // Editor settings ->Field("SnapDistance", &UiCanvasComponent::m_snapDistance) ->Field("SnapRotationDegrees", &UiCanvasComponent::m_snapRotationDegrees) ->Field("HorizontalGuides", &UiCanvasComponent::m_horizontalGuidePositions) ->Field("VerticalGuides", &UiCanvasComponent::m_verticalGuidePositions) ->Field("GuideColor", &UiCanvasComponent::m_guideColor) ->Field("GuidesLocked", &UiCanvasComponent::m_guidesAreLocked) // Texture Atlases ->Field("TextureAtlases", &UiCanvasComponent::m_atlasPathNames) ; AZ::EditContext* ec = serializeContext->GetEditContext(); if (ec) { auto editInfo = ec->Class("UI Canvas", "These are the properties of the UI canvas."); editInfo->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::AddableByUser, false) ->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/UiCanvas.png") ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/UiCanvas.png") ->Attribute(AZ::Edit::Attributes::AutoExpand, true); editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Rendering") ->Attribute(AZ::Edit::Attributes::AutoExpand, true); editInfo->DataElement(AZ::Edit::UIHandlers::Default, &UiCanvasComponent::m_drawOrder, "Draw order", "The order, relative to other canvases, in which this canvas will draw (higher numbers on top)."); editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiCanvasComponent::m_isPixelAligned, "Is pixel aligned", "When checked, all corners of all elements will be rounded to the nearest pixel.") ->Attribute(AZ::Edit::Attributes::ChangeNotify, &UiCanvasComponent::OnPixelAlignmentChange); editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiCanvasComponent::m_isTextPixelAligned, "Is text pixel aligned", "When checked, all text will be rounded to the nearest pixel.") ->Attribute(AZ::Edit::Attributes::ChangeNotify, &UiCanvasComponent::OnTextPixelAlignmentChange); editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiCanvasComponent::m_renderToTexture, "Render to texture", "When checked, the canvas is rendered to a texture instead of the full screen.") ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c)); editInfo->DataElement(0, &UiCanvasComponent::m_renderTargetName, "Render target", "The name of the texture that is created when this canvas renders to a texture.") ->Attribute(AZ::Edit::Attributes::Visibility, &UiCanvasComponent::m_renderToTexture); editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Input") ->Attribute(AZ::Edit::Attributes::AutoExpand, true); editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiCanvasComponent::m_isPositionalInputSupported, "Handle positional", "When checked, positional input (mouse/touch) will automatically be handled.") ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c)); editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiCanvasComponent::m_isConsumingAllInputEvents, "Consume all input", "When checked, all input events will be consumed by this canvas while it is enabled.") ->Attribute(AZ::Edit::Attributes::Visibility, &UiCanvasComponent::GetIsPositionalInputSupported); editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiCanvasComponent::m_isMultiTouchSupported, "Handle multi-touch", "When checked, multi-touch input will automatically be handled.") ->Attribute(AZ::Edit::Attributes::Visibility, &UiCanvasComponent::GetIsPositionalInputSupported); editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiCanvasComponent::m_isNavigationSupported, "Handle navigation", "When checked, keyboard/gamepad events will automatically be used for navigation."); editInfo->DataElement(AZ::Edit::UIHandlers::Default, &UiCanvasComponent::m_navigationThreshold, "Navigation threshold", "The analog (eg. thumb-stick) input value that must be exceeded before a navigation command will be processed.") ->Attribute(AZ::Edit::Attributes::Min, 0.0f) ->Attribute(AZ::Edit::Attributes::Max, 1.0f); editInfo->DataElement(AZ::Edit::UIHandlers::Default, &UiCanvasComponent::m_navigationRepeatDelay, "Navigation repeat delay", "The delay (milliseconds) before a held navigation command will begin repeating."); editInfo->DataElement(AZ::Edit::UIHandlers::Default, &UiCanvasComponent::m_navigationRepeatPeriod, "Navigation repeat period", "The delay (milliseconds) before a held navigation command will continue repeating."); editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiCanvasComponent::m_firstHoverInteractable, "First focus elem", "The element to receive focus when the canvas loads.") ->Attribute("EnumValues", &UiCanvasComponent::PopulateNavigableEntityList); editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Tooltips") ->Attribute(AZ::Edit::Attributes::AutoExpand, true); editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiCanvasComponent::m_tooltipDisplayElement, "Tooltip display elem", "The element to be displayed when hovering over an interactable.") ->Attribute("EnumValues", &UiCanvasComponent::PopulateTooltipDisplayEntityList); editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Editor settings") ->Attribute(AZ::Edit::Attributes::AutoExpand, true); editInfo->DataElement(AZ::Edit::UIHandlers::Default, &UiCanvasComponent::m_snapDistance, "Snap distance", "The snap grid spacing.") ->Attribute(AZ::Edit::Attributes::Min, 1.0f); editInfo->DataElement(AZ::Edit::UIHandlers::Default, &UiCanvasComponent::m_snapRotationDegrees, "Snap rotation", "The degrees of rotation to snap to.") ->Attribute(AZ::Edit::Attributes::Min, 1.0f) ->Attribute(AZ::Edit::Attributes::Max, 359.0f) ->Attribute(AZ::Edit::Attributes::Suffix, " degrees"); editInfo->DataElement(AZ::Edit::UIHandlers::Default, &UiCanvasComponent::m_guideColor, "Guide color", "The color to draw the guide lines on this canvas."); editInfo->DataElement("SimpleAssetRef", &UiCanvasComponent::m_atlasPathNames, "Texture atlases", "The texture atlases that this canvas loads.") ->Attribute("ChangeNotify", &UiCanvasComponent::ReloadAtlases); } } AZ::BehaviorContext* behaviorContext = azrtti_cast(context); if (behaviorContext) { behaviorContext->EBus("UiCanvasBus") ->Event("GetDrawOrder", &UiCanvasBus::Events::GetDrawOrder) ->Event("SetDrawOrder", &UiCanvasBus::Events::SetDrawOrder) ->Event("GetKeepLoadedOnLevelUnload", &UiCanvasBus::Events::GetKeepLoadedOnLevelUnload) ->Event("SetKeepLoadedOnLevelUnload", &UiCanvasBus::Events::SetKeepLoadedOnLevelUnload) ->Event("RecomputeChangedLayouts", &UiCanvasBus::Events::RecomputeChangedLayouts) ->Event("GetNumChildElements", &UiCanvasBus::Events::GetNumChildElements) ->Event("GetChildElement", &UiCanvasBus::Events::GetChildElementEntityId) ->Event("GetChildElements", &UiCanvasBus::Events::GetChildElementEntityIds) ->Event("FindElementByName", &UiCanvasBus::Events::FindElementEntityIdByName) ->Event("CloneElement", &UiCanvasBus::Events::CloneElementEntityId) ->Event("GetIsPixelAligned", &UiCanvasBus::Events::GetIsPixelAligned) ->Event("SetIsPixelAligned", &UiCanvasBus::Events::SetIsPixelAligned) ->Event("GetIsTextPixelAligned", &UiCanvasBus::Events::GetIsTextPixelAligned) ->Event("SetIsTextPixelAligned", &UiCanvasBus::Events::SetIsTextPixelAligned) ->Event("GetEnabled", &UiCanvasBus::Events::GetEnabled) ->Event("SetEnabled", &UiCanvasBus::Events::SetEnabled) ->Event("GetIsRenderToTexture", &UiCanvasBus::Events::GetIsRenderToTexture) ->Event("SetIsRenderToTexture", &UiCanvasBus::Events::SetIsRenderToTexture) ->Event("GetRenderTargetName", &UiCanvasBus::Events::GetRenderTargetName) ->Event("SetRenderTargetName", &UiCanvasBus::Events::SetRenderTargetName) ->Event("GetIsPositionalInputSupported", &UiCanvasBus::Events::GetIsPositionalInputSupported) ->Event("SetIsPositionalInputSupported", &UiCanvasBus::Events::SetIsPositionalInputSupported) ->Event("GetIsConsumingAllInputEvents", &UiCanvasBus::Events::GetIsConsumingAllInputEvents) ->Event("SetIsConsumingAllInputEvents", &UiCanvasBus::Events::SetIsConsumingAllInputEvents) ->Event("GetIsMultiTouchSupported", &UiCanvasBus::Events::GetIsMultiTouchSupported) ->Event("SetIsMultiTouchSupported", &UiCanvasBus::Events::SetIsMultiTouchSupported) ->Event("GetIsNavigationSupported", &UiCanvasBus::Events::GetIsNavigationSupported) ->Event("SetIsNavigationSupported", &UiCanvasBus::Events::SetIsNavigationSupported) ->Event("GetNavigationThreshold", &UiCanvasBus::Events::GetNavigationThreshold) ->Event("SetNavigationThreshold", &UiCanvasBus::Events::SetNavigationThreshold) ->Event("GetNavigationRepeatDelay", &UiCanvasBus::Events::GetNavigationRepeatDelay) ->Event("SetNavigationRepeatDelay", &UiCanvasBus::Events::SetNavigationRepeatDelay) ->Event("GetNavigationRepeatPeriod", &UiCanvasBus::Events::GetNavigationRepeatPeriod) ->Event("SetNavigationRepeatPeriod", &UiCanvasBus::Events::SetNavigationRepeatPeriod) ->Event("GetTooltipDisplayElement", &UiCanvasBus::Events::GetTooltipDisplayElement) ->Event("SetTooltipDisplayElement", &UiCanvasBus::Events::SetTooltipDisplayElement) ->Event("GetHoverInteractable", &UiCanvasBus::Events::GetHoverInteractable) ->Event("ForceHoverInteractable", &UiCanvasBus::Events::ForceHoverInteractable) ->Event("ForceEnterInputEventOnInteractable", &UiCanvasBus::Events::ForceEnterInputEventOnInteractable); behaviorContext->EBus("UiCanvasNotificationBus") ->Handler(); behaviorContext->EBus("UiAnimationBus") ->Event("StartSequence", &UiAnimationBus::Events::StartSequence) ->Event("PlaySequenceRange", &UiAnimationBus::Events::PlaySequenceRange) ->Event("StopSequence", &UiAnimationBus::Events::StopSequence) ->Event("AbortSequence", &UiAnimationBus::Events::AbortSequence) ->Event("PauseSequence", &UiAnimationBus::Events::PauseSequence) ->Event("ResumeSequence", &UiAnimationBus::Events::ResumeSequence) ->Event("ResetSequence", &UiAnimationBus::Events::ResetSequence) ->Event("GetSequencePlayingSpeed", &UiAnimationBus::Events::GetSequencePlayingSpeed) ->Event("SetSequencePlayingSpeed", &UiAnimationBus::Events::SetSequencePlayingSpeed) ->Event("GetSequencePlayingTime", &UiAnimationBus::Events::GetSequencePlayingTime) ->Event("IsSequencePlaying", &UiAnimationBus::Events::IsSequencePlaying) ->Event("GetSequenceLength", &UiAnimationBus::Events::GetSequenceLength) ->Event("SetSequenceStopBehavior", &UiAnimationBus::Events::SetSequenceStopBehavior); behaviorContext->Enum<(int)IUiAnimationListener::EUiAnimationEvent::eUiAnimationEvent_Started>("eUiAnimationEvent_Started") ->Enum<(int)IUiAnimationListener::EUiAnimationEvent::eUiAnimationEvent_Stopped>("eUiAnimationEvent_Stopped") ->Enum<(int)IUiAnimationListener::EUiAnimationEvent::eUiAnimationEvent_Aborted>("eUiAnimationEvent_Aborted") ->Enum<(int)IUiAnimationListener::EUiAnimationEvent::eUiAnimationEvent_Updated>("eUiAnimationEvent_Updated"); behaviorContext->Enum<(int)IUiAnimationSystem::ESequenceStopBehavior::eSSB_LeaveTime>("eSSB_LeaveTime") ->Enum<(int)IUiAnimationSystem::ESequenceStopBehavior::eSSB_GotoEndTime>("eSSB_GotoEndTime") ->Enum<(int)IUiAnimationSystem::ESequenceStopBehavior::eSSB_GotoStartTime>("eSSB_GotoStartTime"); behaviorContext->EBus("UiAnimationNotificationBus") ->Handler(); behaviorContext->EBus("UiInitializationBus") ->Handler(); behaviorContext->EBus("UiCanvasInputNotificationBus") ->Handler(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::Initialize() { s_handleHoverInputEvents = true; s_allowClearingHoverInteractableOnHoverInput = true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::Shutdown() { } //////////////////////////////////////////////////////////////////////////////////////////////////// // PROTECTED MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::Init() { // We don't know whether we're in editor or game yet, but if we're in the editor // we need to know the authored canvas size to ensure certain properties are displayed // correctly in the editor window. If we're in game, the target canvas size will be // initialized to the viewport on the first render loop. m_targetCanvasSize = m_canvasSize; if (m_uniqueId == 0) { // Initialize unique Id m_uniqueId = CreateUniqueId(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::Activate() { UiCanvasBus::Handler::BusConnect(m_entity->GetId()); UiCanvasComponentImplementationBus::Handler::BusConnect(m_entity->GetId()); UiEditorCanvasBus::Handler::BusConnect(m_entity->GetId()); UiAnimationBus::Handler::BusConnect(m_entity->GetId()); // Reconnect to buses that we connect to intermittently // This will only happen if we have been deactivated and reactivated at runtime if (m_hoverInteractable.IsValid()) { AZ::EntityBus::Handler::BusConnect(m_hoverInteractable); } if (m_activeInteractable.IsValid()) { UiInteractableActiveNotificationBus::Handler::BusConnect(m_activeInteractable); } // Note: this will create a render target even when the canvas is being used in the editor which is // unnecessary but harmless. It will not actually be used as a render target unless we are running in game. // An alternative would be to create in on first use. if (m_renderToTexture) { CreateRenderTarget(); } LoadAtlases(); m_layoutManager = new UiLayoutManager(GetEntityId()); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::Deactivate() { UiCanvasBus::Handler::BusDisconnect(); UiCanvasComponentImplementationBus::Handler::BusDisconnect(); UiEditorCanvasBus::Handler::BusDisconnect(); UiAnimationBus::Handler::BusDisconnect(); // disconnect from any other buses we could be connected to if (m_hoverInteractable.IsValid() && AZ::EntityBus::Handler::BusIsConnectedId(m_hoverInteractable)) { AZ::EntityBus::Handler::BusDisconnect(m_hoverInteractable); } if (m_activeInteractable.IsValid() && UiInteractableActiveNotificationBus::Handler::BusIsConnectedId(m_activeInteractable)) { UiInteractableActiveNotificationBus::Handler::BusDisconnect(m_activeInteractable); } m_multiTouchInteractablesByTouchIndex.clear(); if (m_renderToTexture) { DestroyRenderTarget(); } delete m_layoutManager; m_layoutManager = nullptr; m_renderGraph.ResetGraph(); } //////////////////////////////////////////////////////////////////////////////////////////////////// // PRIVATE MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::HandleHoverInputEvent(AZ::Vector2 point) { bool result = false; // We don't change the active interactable here. Some interactables may want to still be considered // pressed if the mouse moves outside their bounds while they are pressed. // However, the active interactable does influence how hover works, if there is an active // interactable then that is the only one that can be the hoverInteractable AZ::EntityId latestHoverInteractable; if (m_activeInteractable.IsValid()) { // check if the mouse is hovering over the active interactable bool hoveringOnActive = false; EBUS_EVENT_ID_RESULT(hoveringOnActive, m_activeInteractable, UiTransformBus, IsPointInRect, point); if (hoveringOnActive) { latestHoverInteractable = m_activeInteractable; } } else { // there is no active interactable // find the interactable that the mouse is hovering over (if any) EBUS_EVENT_ID_RESULT(latestHoverInteractable, m_rootElement, UiElementBus, FindInteractableToHandleEvent, point); } if (latestHoverInteractable.IsValid()) { s_allowClearingHoverInteractableOnHoverInput = true; } if (m_hoverInteractable.IsValid() && m_hoverInteractable != latestHoverInteractable) { // we were hovering over an interactable but now we are hovering over nothing or a different interactable if (s_allowClearingHoverInteractableOnHoverInput) { ClearHoverInteractable(); } } if (latestHoverInteractable.IsValid() && !m_hoverInteractable.IsValid()) { // we are now hovering over something and we aren't tracking that yet SetHoverInteractable(latestHoverInteractable); EBUS_EVENT_ID_RESULT(result, m_hoverInteractable, UiInteractableBus, IsHandlingEvents); } // if there is an active interactable then we send mouse position updates to that interactable if (m_activeInteractable.IsValid()) { EBUS_EVENT_ID(m_activeInteractable, UiInteractableBus, InputPositionUpdate, point); } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::HandleKeyInputEvent(const AzFramework::InputChannel::Snapshot& inputSnapshot, AzFramework::ModifierKeyMask activeModifierKeys) { bool result = false; // Allow the active interactable to handle the key input first if (m_activeInteractable.IsValid()) { if (inputSnapshot.m_state == AzFramework::InputChannel::State::Began || AzFramework::InputDeviceVirtualKeyboard::IsVirtualKeyboardDevice(inputSnapshot.m_deviceId)) // Virtual keyboard events don't have state { EBUS_EVENT_ID_RESULT(result, m_activeInteractable, UiInteractableBus, HandleKeyInputBegan, inputSnapshot, activeModifierKeys); } } if (!result && m_isNavigationSupported) { const UiNavigationHelpers::Command command = UiNavigationHelpers::MapInputChannelIdToUiNavigationCommand(inputSnapshot.m_channelId, activeModifierKeys); if (command != UiNavigationHelpers::Command::Unknown) { // Handle directional navigation input. Navigation is performed if there is no active interactable, or if the // active interactable is not pressed and is set to auto-activate bool handleDirectionalNavigation = false; if (!m_activeInteractable.IsValid()) { handleDirectionalNavigation = true; } else if (!m_isActiveInteractablePressed) { // Check if the active interactable automatically goes to an active state EBUS_EVENT_ID_RESULT(handleDirectionalNavigation, m_activeInteractable, UiInteractableBus, GetIsAutoActivationEnabled); } if (handleDirectionalNavigation) { AZ::EntityId oldHoverInteractable = m_hoverInteractable; result = HandleNavigationInputEvent(command, inputSnapshot); if (m_hoverInteractable != oldHoverInteractable) { s_handleHoverInputEvents = false; s_allowClearingHoverInteractableOnHoverInput = false; AZ::EntityId ancestorInteractable = FindAncestorInteractable(m_hoverInteractable); if (ancestorInteractable.IsValid()) { // Send an event that the descendant interactable became the hover interactable via navigation EBUS_EVENT_ID(ancestorInteractable, UiInteractableBus, HandleDescendantReceivedHoverByNavigation, m_hoverInteractable); } ClearActiveInteractable(); // Check if this hover interactable should automatically go to an active state CheckHoverInteractableAndAutoActivate(oldHoverInteractable, command); } } if (!result) { // Handle enter input result = HandleEnterInputEvent(command, inputSnapshot); } if (!result) { // Handle back input result = HandleBackInputEvent(command, inputSnapshot); } if (!result) { // If there is any active or hover interactable then we consider this event handled. // Otherwise we can end up sending events to underlying canvases even though there // is an interactable in this canvas that should block the events if (m_activeInteractable.IsValid() || m_hoverInteractable.IsValid()) { result = true; } } } } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::HandleEnterInputEvent(UiNavigationHelpers::Command command, const AzFramework::InputChannel::Snapshot& inputSnapshot) { bool result = false; if (command == UiNavigationHelpers::Command::Enter) { // The key is the Enter key. If there is any active or hover interactable then we consider this event handled. // Otherwise we can end up sending Enter events to underlying canvases even though there is an interactable // in this canvas that should block the events if (m_activeInteractable.IsValid() || m_hoverInteractable.IsValid()) { result = true; } if (inputSnapshot.m_state == AzFramework::InputChannel::State::Began) { AZ::EntityId prevHoverInteractable = m_hoverInteractable; // Enter key was pressed. The press can activate an interactable and also deactivate an interactable // Check if there's an interactable to deactivate if (m_activeInteractable.IsValid() && m_activeInteractableShouldStayActive) { DeactivateInteractableByKeyInput(inputSnapshot); } else { // Check if there's a hover interactable to make active if (m_hoverInteractable.IsValid()) { // clear any active interactable ClearActiveInteractable(); // if the hover interactable can handle enter pressed events then // it becomes the currently pressed interactable for the canvas bool handled = false; bool shouldStayActive = false; EBUS_EVENT_ID_RESULT(handled, m_hoverInteractable, UiInteractableBus, HandleEnterPressed, shouldStayActive); if (handled) { SetActiveInteractable(m_hoverInteractable, shouldStayActive); s_handleHoverInputEvents = false; s_allowClearingHoverInteractableOnHoverInput = false; m_isActiveInteractablePressed = true; } } } // Send a notification to listeners telling them who was just pressed (can be noone) EBUS_EVENT_ID(GetEntityId(), UiCanvasInputNotificationBus, OnCanvasEnterPressed, prevHoverInteractable); } else if (inputSnapshot.m_state == AzFramework::InputChannel::State::Ended) { AZ::EntityId prevActiveInteractable = m_activeInteractable; // Enter key has been released. Check if the active interactable should stay active if (m_activeInteractable.IsValid() && (m_activeInteractable == m_hoverInteractable)) { EBUS_EVENT_ID(m_activeInteractable, UiInteractableBus, HandleEnterReleased); if (!m_activeInteractableShouldStayActive) { ClearActiveInteractable(); } else { // Interactable should stay active, so check if if it has a descendant interactable // that it should pass the hover to CheckActiveInteractableAndPassHoverToDescendant(); } m_isActiveInteractablePressed = false; } // Send a notification to listeners telling them who was just released (can be noone) EBUS_EVENT_ID(GetEntityId(), UiCanvasInputNotificationBus, OnCanvasEnterReleased, prevActiveInteractable); } } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::HandleBackInputEvent(UiNavigationHelpers::Command command, const AzFramework::InputChannel::Snapshot& inputSnapshot) { bool result = false; if (command == UiNavigationHelpers::Command::Back) { if (inputSnapshot.m_state == AzFramework::InputChannel::State::Began) { // Back has two purposes: // 1. If there is an active interactable, and it's not set to auto-activate, pressing back deactivates the interactable // 2. If there is a hover interactable, and it's a child of another interactable, then pressing back moves focus from the child // to the parent // First check if there is an active interactable to deactivate if (m_activeInteractable.IsValid()) { // Deactivate this interactable result = DeactivateInteractableByKeyInput(inputSnapshot); } else if (m_hoverInteractable.IsValid()) { result = PassHoverToAncestorByKeyInput(inputSnapshot); } } } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::HandleNavigationInputEvent(UiNavigationHelpers::Command command, const AzFramework::InputChannel::Snapshot& inputSnapshot) { bool result = false; if (command == UiNavigationHelpers::Command::Up || command == UiNavigationHelpers::Command::Down || command == UiNavigationHelpers::Command::Left || command == UiNavigationHelpers::Command::Right) { // If the stick is no longer pushed, we allow navigating in that direction again auto navCommandStatus = m_navCommandStatus.find(command); if (inputSnapshot.m_state == AzFramework::InputChannel::State::Ended) { navCommandStatus->second.navigationCount = 0; navCommandStatus->second.allowNavigation = true; } // Prevent navigation in this direction for the specified period of time const AZ::u64 time = AZStd::GetTimeUTCMilliSecond(); if (!navCommandStatus->second.allowNavigation) { // The 'navigation repeat delay' is different to the 'navigation repeat period' so that we // can have a longer delay before the first repeated navigation command vs all subsequent // navigation command repeats. For example, the default values result in a delay of 300ms // before a held navigation command will begin repeated, but then while it remains held it // will continue to repeat every 150ms const AZ::u64 timeSinceLastNavigation = time - navCommandStatus->second.lastNavigationTime; if ((navCommandStatus->second.navigationCount > 1 && timeSinceLastNavigation >= m_navigationRepeatPeriod) || (timeSinceLastNavigation >= m_navigationRepeatDelay)) { navCommandStatus->second.allowNavigation = true; } else { return false; } } // Check if the thumb-stick was pushed far enough if (inputSnapshot.m_value >= m_navigationThreshold) { // Don't allow navigating in this direction again until the stick is released or enough time has elapsed. navCommandStatus->second.lastNavigationTime = time; navCommandStatus->second.allowNavigation = false; ++navCommandStatus->second.navigationCount; AZ::EntityId firstHoverInteractable = GetFirstHoverInteractable(); // Find the interactable to navigate to if (!m_hoverInteractable.IsValid()) { SetHoverInteractable(firstHoverInteractable); } else { AZ::EntityId curInteractable = m_hoverInteractable; while (curInteractable.IsValid()) { AZ::EntityId ancestorInteractable = UiNavigationHelpers::FindAncestorNavigableInteractable(curInteractable); LyShine::EntityArray navigableElements; UiNavigationHelpers::FindNavigableInteractables(ancestorInteractable.IsValid() ? ancestorInteractable : m_rootElement, curInteractable, navigableElements); AZ::EntityId nextEntityId = UiNavigationHelpers::GetNextElement(curInteractable, command, navigableElements, firstHoverInteractable, IsValidInteractable, ancestorInteractable); if (nextEntityId.IsValid()) { SetHoverInteractable(nextEntityId); break; } else { // Check if parent interactable was auto-activated bool autoActivated = false; EBUS_EVENT_ID_RESULT(autoActivated, ancestorInteractable, UiInteractableBus, GetIsAutoActivationEnabled); if (autoActivated) { curInteractable = ancestorInteractable; } else { break; } } } } result = m_hoverInteractable.IsValid(); } } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::DeactivateInteractableByKeyInput(const AzFramework::InputChannel::Snapshot& inputSnapshot) { // Check if the active interactable automatically went to an active state. If it did // not automatically go into its active state, then we deactivate the active interactable. // Otherwise, the only way to deactivate the interactable is by navigating away from it // using the directional keys bool autoActivated = false; EBUS_EVENT_ID_RESULT(autoActivated, m_activeInteractable, UiInteractableBus, GetIsAutoActivationEnabled); if (!autoActivated) { // Clear the active interactable AZ::EntityId prevActiveInteractable = m_activeInteractable; ClearActiveInteractable(); if (AzFramework::InputDeviceGamepad::IsGamepadDevice(inputSnapshot.m_deviceId)) { SetHoverInteractable(prevActiveInteractable); s_handleHoverInputEvents = false; s_allowClearingHoverInteractableOnHoverInput = false; } return true; } return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::PassHoverToAncestorByKeyInput(const AzFramework::InputChannel::Snapshot& inputSnapshot) { bool result = false; // Check if the hover interactable has an ancestor that's also an interactable AZ::EntityId ancestorInteractable = UiNavigationHelpers::FindAncestorNavigableInteractable(m_hoverInteractable, true); if (ancestorInteractable.IsValid()) { AZ::EntityId descendantInteractable = m_hoverInteractable; SetHoverInteractable(ancestorInteractable); EBUS_EVENT_ID(ancestorInteractable, UiInteractableBus, HandleReceivedHoverByNavigatingFromDescendant, descendantInteractable); result = true; } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::HandlePrimaryPress(AZ::Vector2 point) { bool result = false; // use the pressed position to select the interactable being pressed AZ::EntityId interactableEntity; EBUS_EVENT_ID_RESULT(interactableEntity, m_rootElement, UiElementBus, FindInteractableToHandleEvent, point); // Clear the previous active interactable if it's different from the new active interactable if (!interactableEntity.IsValid() || (interactableEntity != m_activeInteractable)) { if (m_activeInteractable.IsValid()) { ClearActiveInteractable(); } } if (interactableEntity.IsValid()) { // if there is an interactable at that point and it can handle pressed events then // it becomes the currently pressed interactable for the canvas bool handled = false; bool shouldStayActive = false; EBUS_EVENT_ID_RESULT(handled, interactableEntity, UiInteractableBus, HandlePressed, point, shouldStayActive); if (handled) { SetActiveInteractable(interactableEntity, shouldStayActive); m_isActiveInteractablePressed = true; result = true; } } // Resume handling hover input events s_handleHoverInputEvents = true; s_allowClearingHoverInteractableOnHoverInput = true; // Send a notification to listeners telling them who was just pressed (can be noone) EBUS_EVENT_ID(GetEntityId(), UiCanvasInputNotificationBus, OnCanvasPrimaryPressed, interactableEntity); return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::HandlePrimaryUpdate(AZ::Vector2 point) { bool result = false; if (m_activeInteractable.IsValid()) { result = true; } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::HandlePrimaryRelease(AZ::Vector2 point) { bool result = false; AZ::EntityId prevActiveInteractable = m_activeInteractable; // touch was released, if there is a currently pressed interactable let it handle the release if (m_activeInteractable.IsValid()) { EBUS_EVENT_ID(m_activeInteractable, UiInteractableBus, HandleReleased, point); if (!m_activeInteractableShouldStayActive) { UiInteractableActiveNotificationBus::Handler::BusDisconnect(m_activeInteractable); m_activeInteractable.SetInvalid(); } m_isActiveInteractablePressed = false; result = true; } // Send a notification to listeners telling them who was just released EBUS_EVENT_ID(GetEntityId(), UiCanvasInputNotificationBus, OnCanvasPrimaryReleased, prevActiveInteractable); return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::HandleMultiTouchPress(AZ::Vector2 point, int multiTouchIndex) { bool result = false; if (m_isMultiTouchSupported) { AZ::EntityId interactableEntity; EBUS_EVENT_ID_RESULT(interactableEntity, m_rootElement, UiElementBus, FindInteractableToHandleEvent, point); if (interactableEntity.IsValid() && !IsInteractableActiveOrPressed(interactableEntity)) { EBUS_EVENT_ID_RESULT(result, interactableEntity, UiInteractableBus, HandleMultiTouchPressed, point, multiTouchIndex); if (result) { m_multiTouchInteractablesByTouchIndex[multiTouchIndex] = interactableEntity; } } // Send a notification to listeners telling them who was just pressed (can be noone) EBUS_EVENT_ID(GetEntityId(), UiCanvasInputNotificationBus, OnCanvasMultiTouchPressed, interactableEntity, multiTouchIndex); } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::HandleMultiTouchRelease(AZ::Vector2 point, int multiTouchIndex) { bool result = false; if (m_isMultiTouchSupported) { // Get the corresponding interactable from the map before removing it. It should always already // exist in the map, but if not this will just insert an invalid entity id then remove it again AZ::EntityId multiTouchInteractable = m_multiTouchInteractablesByTouchIndex[multiTouchIndex]; m_multiTouchInteractablesByTouchIndex.erase(multiTouchIndex); if (multiTouchInteractable.IsValid()) { EBUS_EVENT_ID(multiTouchInteractable, UiInteractableBus, HandleMultiTouchReleased, point, multiTouchIndex); result = true; } // Send a notification to listeners telling them who was just released EBUS_EVENT_ID(GetEntityId(), UiCanvasInputNotificationBus, OnCanvasMultiTouchReleased, multiTouchInteractable, multiTouchIndex); } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::HandleMultiTouchUpdated(AZ::Vector2 point, int multiTouchIndex) { bool result = false; if (m_isMultiTouchSupported) { auto it = m_multiTouchInteractablesByTouchIndex.find(multiTouchIndex); if (it != m_multiTouchInteractablesByTouchIndex.end() && it->second.IsValid()) { EBUS_EVENT_ID(it->second, UiInteractableBus, MultiTouchPositionUpdate, point, multiTouchIndex); result = true; } } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::IsInteractableActiveOrPressed(AZ::EntityId interactableId) const { if (interactableId == m_activeInteractable) { return true; } for (const auto& multiTouchInteractableByTouchIndex : m_multiTouchInteractablesByTouchIndex) { if (interactableId == multiTouchInteractableByTouchIndex.second) { return true; } } return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetHoverInteractable(AZ::EntityId newHoverInteractable) { if (m_hoverInteractable != newHoverInteractable) { ClearHoverInteractable(); m_hoverInteractable = newHoverInteractable; if (m_hoverInteractable.IsValid()) { EBUS_EVENT_ID(m_hoverInteractable, UiInteractableBus, HandleHoverStart); EBUS_EVENT_ID(GetEntityId(), UiCanvasInputNotificationBus, OnCanvasHoverStart, m_hoverInteractable); // we want to know if this entity is deactivated or destroyed // (unlikely: while hovered over we can't be in edit mode, could happen from C++ interface though) AZ::EntityBus::Handler::BusConnect(m_hoverInteractable); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::ClearHoverInteractable() { if (m_hoverInteractable.IsValid()) { EBUS_EVENT_ID(m_hoverInteractable, UiInteractableBus, HandleHoverEnd); EBUS_EVENT_ID(GetEntityId(), UiCanvasInputNotificationBus, OnCanvasHoverEnd, m_hoverInteractable); AZ::EntityBus::Handler::BusDisconnect(m_hoverInteractable); m_hoverInteractable.SetInvalid(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetActiveInteractable(AZ::EntityId newActiveInteractable, bool shouldStayActive) { if (m_activeInteractable != newActiveInteractable) { ClearActiveInteractable(); m_activeInteractable = newActiveInteractable; if (m_activeInteractable.IsValid()) { UiInteractableActiveNotificationBus::Handler::BusConnect(m_activeInteractable); m_activeInteractableShouldStayActive = shouldStayActive; } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::ClearActiveInteractable() { if (m_activeInteractable.IsValid()) { EBUS_EVENT_ID(m_activeInteractable, UiInteractableBus, LostActiveStatus); UiInteractableActiveNotificationBus::Handler::BusDisconnect(m_activeInteractable); m_activeInteractable.SetInvalid(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::CheckHoverInteractableAndAutoActivate(AZ::EntityId prevHoverInteractable, UiNavigationHelpers::Command command, bool forceAutoActivate) { // Check if this hover interactable should automatically go to an active state bool autoActivate = false; EBUS_EVENT_ID_RESULT(autoActivate, m_hoverInteractable, UiInteractableBus, GetIsAutoActivationEnabled); if (autoActivate || forceAutoActivate) { bool handled = false; EBUS_EVENT_ID_RESULT(handled, m_hoverInteractable, UiInteractableBus, HandleAutoActivation); if (handled) { SetActiveInteractable(m_hoverInteractable, true); CheckActiveInteractableAndPassHoverToDescendant(prevHoverInteractable, command); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::CheckActiveInteractableAndPassHoverToDescendant(AZ::EntityId prevHoverInteractable, UiNavigationHelpers::Command command) { AZ::EntityId hoverInteractable; if (prevHoverInteractable.IsValid()) { LyShine::EntityArray navigableElements; UiNavigationHelpers::FindNavigableInteractables(m_activeInteractable, AZ::EntityId(), navigableElements); if (!navigableElements.empty()) { hoverInteractable = UiNavigationHelpers::SearchForNextElement(prevHoverInteractable, command, navigableElements, m_activeInteractable); } } if (!hoverInteractable.IsValid()) { hoverInteractable = FindFirstHoverInteractable(m_activeInteractable); } if (hoverInteractable.IsValid()) { // Send an event that the descendant interactable became the hover interactable via navigation EBUS_EVENT_ID(m_activeInteractable, UiInteractableBus, HandleDescendantReceivedHoverByNavigation, hoverInteractable); ClearActiveInteractable(); SetHoverInteractable(hoverInteractable); CheckHoverInteractableAndAutoActivate(prevHoverInteractable, command); } } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasComponent::FindAncestorInteractable(AZ::EntityId entityId) { AZ::EntityId parent; EBUS_EVENT_ID_RESULT(parent, entityId, UiElementBus, GetParentEntityId); while (parent.IsValid()) { if (UiInteractableBus::FindFirstHandler(parent)) { break; } AZ::EntityId newParent = parent; parent.SetInvalid(); EBUS_EVENT_ID_RESULT(parent, newParent, UiElementBus, GetParentEntityId); } return parent; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasComponent::GetFirstHoverInteractable() { AZ::EntityId hoverInteractable; if (m_firstHoverInteractable.IsValid()) { // Make sure that this interactable exists AZ::Entity* hoverEntity = nullptr; EBUS_EVENT_RESULT(hoverEntity, AZ::ComponentApplicationBus, FindEntity, m_firstHoverInteractable); if (hoverEntity) { if (UiNavigationHelpers::IsElementInteractableAndNavigable(m_firstHoverInteractable)) { hoverInteractable = m_firstHoverInteractable; } } } if (!hoverInteractable.IsValid()) { hoverInteractable = FindFirstHoverInteractable(); } return hoverInteractable; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiCanvasComponent::FindFirstHoverInteractable(AZ::EntityId parentElement) { if (!parentElement.IsValid()) { parentElement = m_rootElement; } LyShine::EntityArray navigableElements; UiNavigationHelpers::FindNavigableInteractables(parentElement, AZ::EntityId(), navigableElements); UiTransformInterface::Rect parentRect; AZ::Matrix4x4 transformFromViewport; if (parentElement != m_rootElement) { EBUS_EVENT_ID(parentElement, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, parentRect); EBUS_EVENT_ID(parentElement, UiTransformBus, GetTransformFromViewport, transformFromViewport); } else { transformFromViewport = AZ::Matrix4x4::CreateIdentity(); parentRect.Set(0.0f, m_targetCanvasSize.GetX(), 0.0f, m_targetCanvasSize.GetY()); } // Go through the navigable elements and find the closest element to the top left of its parent float shortestDist = FLT_MAX; float shortestOutsideDist = FLT_MAX; AZ::EntityId closestElement; AZ::EntityId closestOutsideElement; for (auto navigableElement : navigableElements) { UiTransformInterface::RectPoints points; EBUS_EVENT_ID(navigableElement->GetId(), UiTransformBus, GetViewportSpacePoints, points); points = points.Transform(transformFromViewport); AZ::Vector2 topLeft = points.GetAxisAlignedTopLeft() - AZ::Vector2(parentRect.left, parentRect.top); AZ::Vector2 center = points.GetCenter(); float dist = topLeft.GetLength(); bool inside = (center.GetX() >= parentRect.left && center.GetX() <= parentRect.right && center.GetY() >= parentRect.top && center.GetY() <= parentRect.bottom); if (inside) { // Calculate a value from 0 to 1 representing how close the element is to the top of the screen float yDistValue = topLeft.GetY() / parentRect.GetHeight(); // Calculate final distance value biased by y distance value const float distMultConstant = 1.0f; dist += dist * distMultConstant * yDistValue; if (dist < shortestDist) { shortestDist = dist; closestElement = navigableElement->GetId(); } } else { if (dist < shortestOutsideDist) { shortestOutsideDist = dist; closestOutsideElement = navigableElement->GetId(); } } } if (!closestElement.IsValid()) { closestElement = closestOutsideElement; } return closestElement; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetFirstHoverInteractable() { bool setHoverInteractable = false; if (s_handleHoverInputEvents) { // Check if there is a mouse or touch input device const AzFramework::InputDevice* mouseDevice = AzFramework::InputDeviceRequests::FindInputDevice(AzFramework::InputDeviceMouse::Id); const AzFramework::InputDevice* touchDevice = AzFramework::InputDeviceRequests::FindInputDevice(AzFramework::InputDeviceTouch::Id); if ((!mouseDevice || !mouseDevice->IsConnected()) && (!touchDevice || !touchDevice->IsConnected())) { // No mouse or touch input device available so set a hover interactable setHoverInteractable = true; } } else { // Not handling hover input events so set a hover interactable setHoverInteractable = true; } if (setHoverInteractable) { AZ::EntityId hoverInteractable = GetFirstHoverInteractable(); if (hoverInteractable.IsValid()) { SetHoverInteractable(hoverInteractable); s_handleHoverInputEvents = false; s_allowClearingHoverInteractableOnHoverInput = false; CheckHoverInteractableAndAutoActivate(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::PrepareAnimationSystemForCanvasSave() { m_serializedAnimationData.m_serializeData.clear(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::RestoreAnimationSystemAfterCanvasLoad(bool remapIds, UiElementComponent::EntityIdMap* entityIdMap) { // NOTE: this is legacy code for loading old format animation data. The latest canvas format // uses the AZ serialization system for animation data. const char* buffer = m_serializedAnimationData.m_serializeData.c_str(); size_t size = m_serializedAnimationData.m_serializeData.length(); if (size > 0) { // found old format animation data // serialize back from loaded string and then clear string XmlNodeRef xmlNode = gEnv->pSystem->LoadXmlFromBuffer(buffer, size); m_uiAnimationSystem.Serialize(xmlNode, true); m_serializedAnimationData.m_serializeData.clear(); } // go through the sequences and fixup the entity Ids // NOTE: for a latest format canvas these have probably already been remapped by ReplaceEntityRefs // This function will leave them alone if that are not in the remap table m_uiAnimationSystem.InitPostLoad(remapIds, entityIdMap); } //////////////////////////////////////////////////////////////////////////////////////////////////// UiCanvasComponent* UiCanvasComponent::CloneAndInitializeCanvas(UiEntityContext* entityContext, const AZStd::string& assetIdPathname, const AZ::Vector2* canvasSize) { UiCanvasComponent* canvasComponent = nullptr; // Clone the root slice entity // Do this in a way that handles this canvas being a Editor canvas. // If it is an editor canvas then slices will be flattened and Editor components will be // replaced with runtime components. AZ::Entity* clonedRootSliceEntity = nullptr; AZStd::string rootSliceBuffer; AZ::IO::ByteContainerStream rootSliceStream(&rootSliceBuffer); if (m_entityContext->SaveToStreamForGame(rootSliceStream, AZ::ObjectStream::ST_XML)) { rootSliceStream.Seek(0, AZ::IO::GenericStream::ST_SEEK_BEGIN); clonedRootSliceEntity = AZ::Utils::LoadObjectFromStream(rootSliceStream); } // Clone the canvas entity AZ::Entity* sourceCanvasEntity = GetEntity(); AZ::Entity* clonedCanvasEntity = nullptr; AZStd::string canvasBuffer; AZ::IO::ByteContainerStream canvasStream(&canvasBuffer); if (m_entityContext->SaveCanvasEntityToStreamForGame(sourceCanvasEntity, canvasStream, AZ::ObjectStream::ST_XML)) { canvasStream.Seek(0, AZ::IO::GenericStream::ST_SEEK_BEGIN); clonedCanvasEntity = AZ::Utils::LoadObjectFromStream(canvasStream); } if (clonedCanvasEntity && clonedRootSliceEntity) { // complete initialization of cloned entities, we assume this is NOT for editor // since we only do this when using canvas in game that is already loaded in editor canvasComponent = FixupPostLoad(clonedCanvasEntity, clonedRootSliceEntity, false, entityContext, canvasSize); } if (canvasComponent) { canvasComponent->m_pathname = assetIdPathname; canvasComponent->m_isLoadedInGame = true; } return canvasComponent; } void UiCanvasComponent::DeactivateElements() { AZ::SliceComponent::EntityIdSet entities; AZ::SliceComponent* rootSlice = m_entityContext->GetRootSlice(); bool result = rootSlice->GetEntityIds(entities); if (result) { for (AZ::EntityId& entityId : entities) { // Look up the entity by ID, as sometimes one of the entities owns others // that will be destroyed when it's destroyed. Since we store pointers, // those will point to freed memory. AZ::Entity* entity = nullptr; AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationBus::Events::FindEntity, entityId); if (entity && entity->GetState() == AZ::Entity::ES_ACTIVE) { entity->Deactivate(); } } } } //////////////////////////////////////////////////////////////////////////////////////////////////// AZStd::vector UiCanvasComponent::GetEntityIdsOfElementAndDescendants(AZ::Entity* entity) { AZStd::vector entitiesInPrefab; entitiesInPrefab.push_back(entity->GetId()); LyShine::EntityArray descendantEntities; EBUS_EVENT_ID(entity->GetId(), UiElementBus, FindDescendantElements, [](const AZ::Entity*) { return true; }, descendantEntities); for (auto descendant : descendantEntities) { entitiesInPrefab.push_back(descendant->GetId()); } return entitiesInPrefab; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SetTargetCanvasSizeAndUniformScale(bool isInGame, AZ::Vector2 canvasSize) { AZ::Vector2 oldTargetCanvasSize = m_targetCanvasSize; AZ::Vector2 oldDeviceScale = m_deviceScale; if (isInGame) { // Set the target canvas size to the canvas size specified by the caller m_targetCanvasSize = canvasSize; // set the device scale m_deviceScale.SetX(m_targetCanvasSize.GetX() / m_canvasSize.GetX()); m_deviceScale.SetY(m_targetCanvasSize.GetY() / m_canvasSize.GetY()); } else { // While in the editor, the only resolution we care about is the canvas' authored // size, so we set that as our target size for display purposes. m_targetCanvasSize = m_canvasSize; } // if the target canvas size or the uniform device scale changed then this will affect the // element transforms so force them to recompute if (oldTargetCanvasSize != m_targetCanvasSize || oldDeviceScale != m_deviceScale) { UiTransformInterface::Recompute recompute; if (oldTargetCanvasSize != m_targetCanvasSize) { recompute = (oldDeviceScale != m_deviceScale) ? UiTransformInterface::Recompute::RectAndTransform : UiTransformInterface::Recompute::RectOnly; } else { recompute = UiTransformInterface::Recompute::TransformOnly; } EBUS_EVENT_ID(GetRootElement()->GetId(), UiTransformBus, SetRecomputeFlags, recompute); EBUS_EVENT(UiCanvasSizeNotificationBus, OnCanvasSizeOrScaleChange, GetEntityId()); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::IsElementNameUnique(const AZStd::string& elementName, const LyShine::EntityArray& elements) { for (auto element : elements) { if (element->GetName() == elementName) { return false; } } return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiCanvasComponent::EntityComboBoxVec UiCanvasComponent::PopulateNavigableEntityList() { EntityComboBoxVec result; // Add a first entry for "None" result.push_back(AZStd::make_pair(AZ::EntityId(), "")); // Get a list of all navigable elements LyShine::EntityArray navigableElements; auto checkNavigable = [](const AZ::Entity* entity) { UiNavigationInterface::NavigationMode navigationMode = UiNavigationInterface::NavigationMode::None; EBUS_EVENT_ID_RESULT(navigationMode, entity->GetId(), UiNavigationBus, GetNavigationMode); return (navigationMode != UiNavigationInterface::NavigationMode::None); }; FindElements(checkNavigable, navigableElements); // Sort the elements by name AZStd::sort(navigableElements.begin(), navigableElements.end(), [](const AZ::Entity* e1, const AZ::Entity* e2) { return e1->GetName() < e2->GetName(); }); // Add their names to the StringList and their IDs to the id list for (auto navigableEntity : navigableElements) { result.push_back(AZStd::make_pair(navigableEntity->GetId(), navigableEntity->GetName())); } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiCanvasComponent::EntityComboBoxVec UiCanvasComponent::PopulateTooltipDisplayEntityList() { EntityComboBoxVec result; // Add a first entry for "None" result.push_back(AZStd::make_pair(AZ::EntityId(), "")); // Get a list of all tooltip display elements LyShine::EntityArray tooltipDisplayElements; auto checkTooltipDisplay = [](const AZ::Entity* entity) { // Check for component on entity return UiTooltipDisplayBus::FindFirstHandler(entity->GetId()) ? true : false; }; FindElements(checkTooltipDisplay, tooltipDisplayElements); // Sort the elements by name AZStd::sort(tooltipDisplayElements.begin(), tooltipDisplayElements.end(), [](const AZ::Entity* e1, const AZ::Entity* e2) { return e1->GetName() < e2->GetName(); }); // Add their names to the StringList and their IDs to the id list for (auto tooltipDisplayEntity : tooltipDisplayElements) { result.push_back(AZStd::make_pair(tooltipDisplayEntity->GetId(), tooltipDisplayEntity->GetName())); } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::OnPixelAlignmentChange() { EBUS_EVENT_ID(GetEntityId(), UiCanvasPixelAlignmentNotificationBus, OnCanvasPixelAlignmentChange); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::OnTextPixelAlignmentChange() { EBUS_EVENT_ID(GetEntityId(), UiCanvasPixelAlignmentNotificationBus, OnCanvasTextPixelAlignmentChange); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::CreateRenderTarget() { if (m_canvasSize.GetX() <= 0 || m_canvasSize.GetY() <= 0) { gEnv->pSystem->Warning(VALIDATOR_MODULE_SHINE, VALIDATOR_WARNING, VALIDATOR_FLAG_FILE, m_pathname.c_str(), "Invalid render target width/height for UI canvas: %s", m_pathname.c_str()); return; } // Create a render target that this canvas will be rendered to. // The render target size is the canvas size. m_renderTargetHandle = gEnv->pRenderer->CreateRenderTarget(m_renderTargetName.c_str(), static_cast(m_canvasSize.GetX()), static_cast(m_canvasSize.GetY()), Clr_Empty, eTF_R8G8B8A8); if (m_renderTargetHandle <= 0) { gEnv->pSystem->Warning(VALIDATOR_MODULE_SHINE, VALIDATOR_WARNING, VALIDATOR_FLAG_FILE, m_pathname.c_str(), "Failed to create render target for UI canvas: %s", m_pathname.c_str()); } else { // Also create a depth surface to render the canvas to, we need depth for masking // since that uses the stencil buffer m_renderTargetDepthSurface = gEnv->pRenderer->CreateDepthSurface( static_cast(m_canvasSize.GetX()), static_cast(m_canvasSize.GetY())); // Register this canvas component as a game framework listener so that we can render // it to a texture on the PreRender event if (gEnv->pGame && gEnv->pGame->GetIGameFramework()) { gEnv->pGame->GetIGameFramework()->RegisterListener(this, "UiCanvasComponent", FRAMEWORKLISTENERPRIORITY_HUD); } else // CryAction has been moved to the optional CryLegacy Gem, so if it doesn't exist we need to do this instead { ISystem::CrySystemNotificationBus::Handler::BusConnect(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::DestroyRenderTarget() { if (m_renderTargetHandle > 0) { if (gEnv->pGame && gEnv->pGame->GetIGameFramework()) { gEnv->pGame->GetIGameFramework()->UnregisterListener(this); } else // CryAction has been moved to the optional CryLegacy Gem, so if it doesn't exist we need to do this instead { ISystem::CrySystemNotificationBus::Handler::BusDisconnect(); } gEnv->pRenderer->DestroyDepthSurface(m_renderTargetDepthSurface); m_renderTargetDepthSurface = nullptr; gEnv->pRenderer->DestroyRenderTarget(m_renderTargetHandle); m_renderTargetHandle = -1; } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::RenderCanvasToTexture() { if (m_renderTargetHandle <= 0) { return; } ISystem* system = gEnv->pSystem; if (system && !gEnv->IsDedicated()) { GetUiRenderer()->BeginUiFrameRender(); gEnv->pRenderer->SetRenderTarget(m_renderTargetHandle, m_renderTargetDepthSurface); // clear the render target before rendering to it // NOTE: the FRT_CLEAR_IMMEDIATE is required since we will have already set the render target // In theory we could call this before setting the render target without the immediate flag // but that doesn't work. Perhaps because FX_Commit is not called. ColorF viewportBackgroundColor(0, 0, 0, 0); // if clearing color we want to set alpha to zero also gEnv->pRenderer->ClearTargetsImmediately(FRT_CLEAR, viewportBackgroundColor); // we are writing to a linear texture gEnv->pRenderer->SetSrgbWrite(false); RenderCanvas(true, m_canvasSize); gEnv->pRenderer->SetRenderTarget(0); // restore render target GetUiRenderer()->EndUiFrameRender(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::SaveCanvasToFile(const string& pathname, AZ::DataStream::StreamType streamType) { // Note: This is ok for saving in tools, but we should use the streamer to write objects directly (no memory store) AZStd::vector dstData; AZ::IO::ByteContainerStream > dstByteStream(&dstData); if (!SaveCanvasToStream(dstByteStream, streamType)) { return false; } AZ::IO::SystemFile file; file.Open(pathname.c_str(), AZ::IO::SystemFile::SF_OPEN_CREATE | AZ::IO::SystemFile::SF_OPEN_CREATE_PATH | AZ::IO::SystemFile::SF_OPEN_WRITE_ONLY); if (!file.IsOpen()) { file.Close(); return false; } file.Write(dstData.data(), dstData.size()); file.Close(); return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::SaveCanvasToStream(AZ::IO::GenericStream& stream, AZ::DataStream::StreamType streamType) { UiCanvasFileObject fileObject; fileObject.m_canvasEntity = this->GetEntity(); fileObject.m_rootSliceEntity = m_entityContext->GetRootAssetEntity(); if (!AZ::Utils::SaveObjectToStream(stream, streamType, &fileObject)) { return false; } return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SendRectChangeNotificationsAndRecomputeLayouts() { // Send canvas space rect change notifications. Handlers may mark // layouts for a recompute SendRectChangeNotifications(); // Recompute invalid layouts if (m_layoutManager->HasMarkedLayouts()) { m_layoutManager->RecomputeMarkedLayouts(); // The layout recompute may have caused child size changes, so // send canvas space rect change notifications again // NOTE: this is expensive so we don't do it unless there were marked layouts SendRectChangeNotifications(); // Remove the newly marked layouts since they have been marked due // to their parents recomputing them m_layoutManager->UnmarkAllLayouts(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::SendRectChangeNotifications() { // While we know there are transforms that need re-computing while (!m_elementsNeedingTransformRecompute.empty()) { // get the front element from the list and remove it from the list UiElementComponent& elementComponent = m_elementsNeedingTransformRecompute.front(); m_elementsNeedingTransformRecompute.pop_front(); elementComponent.m_next = nullptr; // needed in order to be able to test if an element is in the list. // Get the transform component from the element and (if valid) recompute its transforms UiTransform2dComponent* transformComponent = elementComponent.GetTransform2dComponent(); if (transformComponent) { // tell this transform to recompute (this can result in other elements being added to m_elementsNeedingTransformRecompute) transformComponent->RecomputeTransformsAndSendNotifications(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::InitializeLayouts() { m_layoutManager->ComputeLayoutForElementAndDescendants(GetRootElement()->GetId()); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::InGamePostActivateBottomUp(AZ::Entity* entity) { if (!entity) { return; } LyShine::EntityArray childElements; EBUS_EVENT_ID_RESULT(childElements, entity->GetId(), UiElementBus, GetChildElements); for (auto child : childElements) { InGamePostActivateBottomUp(child); } EBUS_EVENT_ID(entity->GetId(), UiInitializationBus, InGamePostActivate); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiCanvasComponent::CloneAndAddElementInternal(AZ::Entity* sourceEntity, AZ::Entity* parentEntity, AZ::Entity* insertBeforeEntity) { // first check that the given entity really is a UI element - i.e. it has a UiElementComponent UiElementComponent* sourceElementComponent = sourceEntity->FindComponent(); if (!sourceElementComponent) { AZ_Warning("UI", false, "CloneElement: The entity to be cloned must have an element component"); return nullptr; } // also check that the given parent entity is part of this canvas (if one is specified) if (parentEntity) { AZ::EntityId parentCanvasId; EBUS_EVENT_ID_RESULT(parentCanvasId, parentEntity->GetId(), UiElementBus, GetCanvasEntityId); if (parentCanvasId != GetEntityId()) { AZ_Warning("UI", false, "CloneElement: The parent entity must belong to this canvas"); return nullptr; } } // If no parent entity specified then the parent is the root element AZ::Entity* parent = (parentEntity) ? parentEntity : GetRootElement(); // also check that the given InsertBefore entity is a child of the parent if (insertBeforeEntity) { AZ::Entity* insertBeforeParent; EBUS_EVENT_ID_RESULT(insertBeforeParent, insertBeforeEntity->GetId(), UiElementBus, GetParent); if (insertBeforeParent != parent) { AZ_Warning("UI", false, "CloneElement: The insertBefore entity must be a child of the parent"); return nullptr; } } AZ::SerializeContext* context = nullptr; EBUS_EVENT_RESULT(context, AZ::ComponentApplicationBus, GetSerializeContext); AZ_Assert(context, "No serialization context found"); AZStd::vector entitiesToClone = GetEntityIdsOfElementAndDescendants(sourceEntity); AzFramework::EntityContext::EntityList clonedEntities; m_entityContext->CloneUiEntities(entitiesToClone, clonedEntities); AZ::Entity* clonedRootEntity = clonedEntities[0]; UiElementComponent* elementComponent = clonedRootEntity->FindComponent(); AZ_Assert(elementComponent, "The cloned entity must have an element component"); // recursively set the canvas and parent pointers elementComponent->FixupPostLoad(clonedRootEntity, this, parent, true); // add this new entity as a child of the parent (parentEntity or root) UiElementComponent* parentElementComponent = parent->FindComponent(); AZ_Assert(parentElementComponent, "No element component found on parent entity"); parentElementComponent->AddChild(clonedRootEntity, insertBeforeEntity); if (m_isLoadedInGame) { // Call InGamePostActivate on all the created entities InGamePostActivateBottomUp(clonedRootEntity); } return clonedRootEntity; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasComponent::GetOrphanedElements(AZ::SliceComponent::EntityList& orphanedEntities) { AZ::SliceComponent::EntityList entities; AZ::SliceComponent* rootSlice = m_entityContext->GetRootSlice(); bool result = rootSlice->GetEntities(entities); // We want to quickly check that every UiElement entity is referenced from the canvas. // We know that at this point all referenced elements have had FixupPostLoad called but // and orphans would not have had it called. // This means that referenced children have a non-null parent (except the root element). // We can use this data to make a list of all orphaned children. for (AZ::Entity* entity : entities) { AZ::Entity* parent = nullptr; EBUS_EVENT_ID_RESULT(parent, entity->GetId(), UiElementBus, GetParent); if (!parent) { if (m_rootElement != entity->GetId()) { // This is an entity that is not referenced by the canvas. // If it has a UiElementComponent on it then it is definitely an orphan UiElementComponent* elementComponent = entity->FindComponent(); if (elementComponent) { // Add to ths list of orphans orphanedEntities.push_back(entity); } else { // This is some unknown entity. In theory the slice system could create such things but it does // not appear to. AZ_Warning("UI", false, "Found entity with no UiElementComponent in a UI canvas root slice."); } } } } } //////////////////////////////////////////////////////////////////////////////////////////////////// // PRIVATE STATIC MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::u64 UiCanvasComponent::CreateUniqueId() { AZ::u64 utcTime = AZStd::GetTimeUTCMilliSecond(); uint32 r = cry_random_uint32(); AZ::u64 id = utcTime << 32 | r; return id; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiCanvasComponent* UiCanvasComponent::CreateCanvasInternal(UiEntityContext* entityContext, bool forEditor) { // create a new empty canvas, give it a name to avoid serialization generating one based on // the ID (which in some cases caused diffs to fail in the editor) AZ::Entity* canvasEntity = aznew AZ::Entity("UiCanvasEntity"); UiCanvasComponent* canvasComponent = canvasEntity->CreateComponent(); // Initialize the UiEntityContext canvasComponent->m_entityContext = entityContext; canvasComponent->m_entityContext->InitUiContext(); // Give the canvas a unique identifier. Used for canvas metrics canvasComponent->m_uniqueId = CreateUniqueId(); // This is the dummy root node of the canvas. // It needs an element component and a transform component. AZ::Entity* rootEntity = canvasComponent->m_entityContext->CreateEntity("_root"); canvasComponent->m_rootElement = rootEntity->GetId(); AZ_Assert(rootEntity, "Failed to create root element entity"); rootEntity->Deactivate(); // so we can add components UiElementComponent* elementComponent = rootEntity->CreateComponent(); AZ_Assert(elementComponent, "Failed to add UiElementComponent to entity"); elementComponent->SetCanvas(canvasComponent, canvasComponent->GenerateId()); AZ::Component* transformComponent = rootEntity->CreateComponent(); AZ_Assert(transformComponent, "Failed to add transform2d component to entity"); rootEntity->Activate(); // re-activate // init the canvas entity (the canvas entity is not part of the EntityContext so is not automatically initialized) canvasEntity->Init(); canvasEntity->Activate(); canvasComponent->m_isLoadedInGame = !forEditor; return canvasComponent; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiCanvasComponent* UiCanvasComponent::LoadCanvasInternal(const string& pathnameToOpen, bool forEditor, const string& assetIdPathname, UiEntityContext* entityContext, const AZ::SliceComponent::EntityIdToEntityIdMap* previousRemapTable, AZ::EntityId previousCanvasId) { UiCanvasComponent* canvasComponent = nullptr; // Currently LoadObjectFromFile will hang if the file cannot be parsed // (LMBR-10078). So first check that it is in the right format if (IsValidAzSerializedFile(pathnameToOpen)) { // Open a stream on the input path AZ::IO::FileIOStream stream(pathnameToOpen.c_str(), AZ::IO::OpenMode::ModeRead | AZ::IO::OpenMode::ModeBinary); if (!stream.IsOpen()) { AZ_Warning("UI", false, "Cannot open UI canvas file \"%s\".", pathnameToOpen.c_str()); } else { // Read in the canvas from the stream UiCanvasFileObject* canvasFileObject = UiCanvasFileObject::LoadCanvasFromStream(stream); AZ_Assert(canvasFileObject, "Failed to load canvas"); if (canvasFileObject) { AZ::Entity* canvasEntity = canvasFileObject->m_canvasEntity; AZ::Entity* rootSliceEntity = canvasFileObject->m_rootSliceEntity; AZ_Assert(canvasEntity && rootSliceEntity, "Failed to load canvas"); if (canvasEntity && rootSliceEntity) { // file loaded OK // no need to check if a canvas with this EntityId is already loaded since we are going // to generate new entity IDs for all entities loaded from the file. // complete initialization of loaded entities canvasComponent = FixupPostLoad(canvasEntity, rootSliceEntity, forEditor, entityContext, nullptr, previousRemapTable, previousCanvasId); if (canvasComponent) { // The canvas size may get reset on the first call to RenderCanvas to set the size to // viewport size. So we'll recompute again on first render. EBUS_EVENT_ID(canvasComponent->GetRootElement()->GetId(), UiTransformBus, SetRecomputeFlags, UiTransformInterface::Recompute::RectAndTransform); canvasComponent->m_pathname = assetIdPathname; canvasComponent->m_isLoadedInGame = !forEditor; } else { // cleanup, don't delete rootSliceEntity, deleting the canvasEntity cleans up the EntityContext and root slice delete canvasEntity; } } // UiCanvasFileObject is a simple container for the canvas pointers, its destructor // doesn't destroy the canvas, but we need to delete it nonetheless to avoid leaking. delete canvasFileObject; } } } else { // this file is not a valid canvas file gEnv->pSystem->Warning(VALIDATOR_MODULE_SHINE, VALIDATOR_WARNING, VALIDATOR_FLAG_FILE, pathnameToOpen.c_str(), "Invalid XML format or couldn't load file for UI canvas file: %s", pathnameToOpen.c_str()); } return canvasComponent; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiCanvasComponent* UiCanvasComponent::FixupReloadedCanvasForEditorInternal(AZ::Entity* newCanvasEntity, AZ::Entity* rootSliceEntity, UiEntityContext* entityContext, LyShine::CanvasId existingId, const AZStd::string& existingPathname) { UiCanvasComponent* newCanvasComponent = FixupPostLoad(newCanvasEntity, rootSliceEntity, true, entityContext); if (newCanvasComponent) { newCanvasComponent->m_id = existingId; newCanvasComponent->m_pathname = existingPathname; } return newCanvasComponent; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiCanvasComponent* UiCanvasComponent::FixupPostLoad(AZ::Entity* canvasEntity, AZ::Entity* rootSliceEntity, bool forEditor, UiEntityContext* entityContext, const AZ::Vector2* canvasSize, const AZ::SliceComponent::EntityIdToEntityIdMap* previousRemapTable, AZ::EntityId previousCanvasId) { // When we load in game we always generate new entity IDs. bool makeNewEntityIds = (forEditor) ? false : true; // When we load in the editor, check if there is another canvas open that has the same entityId if (forEditor) { AZ::Entity* foundEntity = nullptr; EBUS_EVENT_RESULT(foundEntity, AZ::ComponentApplicationBus, FindEntity, canvasEntity->GetId()); if (foundEntity) { makeNewEntityIds = true; } } UiCanvasComponent* canvasComponent = canvasEntity->FindComponent(); AZ_Assert(canvasComponent, "No canvas component found on loaded entity"); if (!canvasComponent) { return nullptr; // unlikely to happen but perhaps possible if a non-canvas file was opened } // Initialize the entity context for the new canvas and init and activate all the entities // in the root slice canvasComponent->m_entityContext = entityContext; canvasComponent->m_entityContext->InitUiContext(); // Load atlases before initializing child components canvasComponent->LoadAtlases(); if (previousRemapTable) { // We are reloading a canvas and we want to use the same entity ID mapping (from editor entity to game entity) as in the previously // loaded canvas. The code below is similar to what HandleLoadedRootSliceEntity does for remapping except that if the existing // mapping table already contains an entry for an editor entity ID it will use the existing mapping AZ::SliceComponent* newRootSlice = rootSliceEntity->FindComponent(); AZ::SerializeContext* context = nullptr; EBUS_EVENT_RESULT(context, AZ::ComponentApplicationBus, GetSerializeContext); AZ_Assert(context, "No serialization context found"); AZ::SliceComponent::InstantiatedContainer entityContainer(false); newRootSlice->GetEntities(entityContainer.m_entities); canvasComponent->m_editorToGameEntityIdMap = *previousRemapTable; ReuseOrGenerateNewIdsAndFixRefs(&entityContainer, canvasComponent->m_editorToGameEntityIdMap, context); if (!canvasComponent->m_entityContext->HandleLoadedRootSliceEntity(rootSliceEntity, false, nullptr)) { return nullptr; } } else { // we are not reloading a canvas so we let the EntityContext HandleLoadedRootSliceEntity do the entity ID remapping as usual if (!canvasComponent->m_entityContext->HandleLoadedRootSliceEntity(rootSliceEntity, makeNewEntityIds, &canvasComponent->m_editorToGameEntityIdMap)) { return nullptr; } } // For the canvas entity itself, handle ID mapping and initialization { if (previousCanvasId.IsValid()) { canvasEntity->SetId(previousCanvasId); } else if (makeNewEntityIds) { AZ::EntityId newId = AZ::Entity::MakeId(); canvasEntity->SetId(newId); } // remap entity IDs such as m_rootElement and any entity IDs in the animation data if (makeNewEntityIds) { // new IDs were generated so we should fix up any internal EntityRefs AZ::SerializeContext* context = nullptr; EBUS_EVENT_RESULT(context, AZ::ComponentApplicationBus, GetSerializeContext); AZ_Assert(context, "No serialization context found"); ReuseOrGenerateNewIdsAndFixRefs(canvasEntity, canvasComponent->m_editorToGameEntityIdMap, context); } canvasEntity->Init(); canvasEntity->Activate(); } AZ::Entity* rootElement = canvasComponent->GetRootElement(); UiElementComponent* elementComponent = rootElement->FindComponent(); AZ_Assert(elementComponent, "No element component found on root element entity"); // Need to remapIds too (actually I don't think this needs to remap anymore) canvasComponent->RestoreAnimationSystemAfterCanvasLoad(makeNewEntityIds, &canvasComponent->m_editorToGameEntityIdMap); bool fixupSuccess = elementComponent->FixupPostLoad(rootElement, canvasComponent, nullptr, false); if (!fixupSuccess) { return nullptr; } // Initialize the target canvas size and uniform scale // This should be done before calling InGamePostActivate so that the // canvas space rects of the elements are accurate AZ_Assert(gEnv->pRenderer, "Attempting to access IRenderer before it has been initialized"); if (gEnv->pRenderer) { AZ::Vector2 targetCanvasSize; if (canvasSize) { targetCanvasSize = *canvasSize; } else { targetCanvasSize.SetX(static_cast(gEnv->pRenderer->GetOverlayWidth())); targetCanvasSize.SetY(static_cast(gEnv->pRenderer->GetOverlayHeight())); } canvasComponent->SetTargetCanvasSizeAndUniformScale(!forEditor, targetCanvasSize); } // Set this before calling InGamePostActivate on the created entities. InGamePostActivate could // call CloneElement which checks this flag canvasComponent->m_isLoadedInGame = !forEditor; // Initialize transform properties of children of layout elements canvasComponent->InitializeLayouts(); if (!forEditor) { // Call InGamePostActivate on all the created entities when loading in game canvasComponent->InGamePostActivateBottomUp(rootElement); } // Set the first hover interactable if (canvasComponent->m_isNavigationSupported) { canvasComponent->SetFirstHoverInteractable(); } return canvasComponent; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiCanvasComponent::VersionConverter(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement) { // conversion from version 1 to 2: if (classElement.GetVersion() < 2) { // No need to actually convert anything because the CanvasFileObject takes care of it // But it makes sense to bump the version number because the m_rootElement is now an EntityId // rather than an Entity* } // conversion from version 2 to 3: // - Need to convert Vec2 to AZ::Vector2 if (classElement.GetVersion() < 3) { if (!LyShine::ConvertSubElementFromVec2ToVector2(context, classElement, "CanvasSize")) { return false; } } return true; }