本文内容
第23章 在C++中与UI交互
第23章 在C++中与UI交互
注意:本章的代码和资源可以在 GitHub 上找到,网址为: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch23_ui_gameplay
介绍
在本章中,我们将构建游戏逻辑,以便在球实体击中其中一个球门时更新 UI 分数。 为了实现这一目标,我们需要以下要素:
- 在球门线上进行物理触发。
- 每当物理身体进入触发卷时,注册来自物理触发器的触发通知。
- 创建新的 User Interface 组件。
- 将 UI 组件添加到画布以更新分数文本值。
物理触发器
图 23.1.PhysX 触发器形状
- 创建新实体 Goal 1 和 Goal 2。
- 为每个组件添加 PhysX Collider (PhysX 碰撞器) 组件。
- 启用 Trigger 开关。这会将碰撞体从静态碰撞体转变为触发体积。
- 选择 box shape 并将其与目标空间匹配。
- 为了正确计算进球数,我们的触发器应该只检测球,而不检测其他物理对象。我们将尝试使用碰撞过滤来实现这一目标。单击 Colliders With (带碰撞器) 右侧的图标,或者从主菜单 Tools → PhysX Configuration (PhysX 配置工具) 中选择 Collision Filtering (碰撞过滤) 选项卡。
- 在 Layers (图层) 下,创建 Ball (球) 图层。
- 在 Groups (组) 下,创建 Only Ball (仅球) 组,并仅启用 “Ball” 图层。
- 现在我们可以使用这些组和图层。转到 Ball (球) 实体,选择其 PhysX Collider (PhysX 碰撞器) 组件,并将 “Collision Layer” (碰撞层) 设置为 “Ball”(球),并将 “Collides With” (碰撞对象) 设置为 “All”(全部)。这样,球将与所有对象碰撞,但会由不同的碰撞层标记,这将使我们能够检测球与关卡中的其他对象
- 对于目标 1 和目标 2 实体,其 PhysX Collider 组件应将“Collision Layer”(碰撞层)设置为“Default”(默认),并将“Collides With”(碰撞对象)设置为“Only Ball”(仅球)。
注册 Trigger Events
我们将使用 GoalDetectorComponent 在 C++ 中监听碰撞通知。
图 23.2.注册碰撞器体积触发器通知
- 创建 trigger 事件处理程序。
AzPhysics::SceneEvents::OnSceneTriggersEvent::Handler m_trigger;
提示:我们正在使用 AZ::Event 来注册通知。
- 将 lambda 分配给处理程序。
GoalDetectorComponent::GoalDetectorComponent()
: m_trigger([this](
AzPhysics::SceneHandle,
const AzPhysics::TriggerEventList& tel)
{
OnTriggerEvents(tel);
})
{
}
- 向 PhysX 系统注册处理程序。
void GoalDetectorComponent::Activate()
{
auto* si = AZ::Interface<AzPhysics::SceneInterface>::Get();
if (si != nullptr)
{
AzPhysics::SceneHandle sh = si->GetSceneHandle(
AzPhysics::DefaultPhysicsSceneName);
si->RegisterSceneTriggersEventHandler(sh, m_trigger);
}
}
- 现在我们可以在 OnTriggerEvents 方法中处理触发器事件。它需要执行以下作:
- 迭代所有发生的触发器事件
- 找到与我们所在的组件匹配的触发器。
- 检查事件类型是否为 Enter,否则也会计算 Exit 事件并错误计算分数。
- 如果一切正常,则将球放到起点并更新 UI 分数。
void GoalDetectorComponent::OnTriggerEvents(
const AzPhysics::TriggerEventList& tel)
{
using namespace AzPhysics;
for (const TriggerEvent& te : tel)
{
if (te.m_triggerBody &&
te.m_triggerBody->GetEntityId() == GetEntityId())
{
if (te.m_type == TriggerEvent::Type::Enter)
{
AZ::Vector3 v = GetRespawnPoint();
RespawnBall(v);
UpdateUi();
}
- 通过在重生位置放置实体,然后将其传递给 GoalDetectorComponent 来设置重生点。这样,我们就可以通过查询实体的位置来获取重生位置,而不是弄乱世界坐标。在关卡中,我们可以通过移动重生实体并在重生点所在的关卡中直观地查看它来轻松调整重生实体。
- 球实体 ID 也保存在 Editor 的 GoalDetectorComponent 上,以便 Goal Detector 组件可以将球移动回重生位置。
- Team 属性将用于更新相应团队的 UI 分数。目标 1 实体的 Team 设置为零,而目标 2 实体的 Team 设置为 1。它只是一个内部标识符。
- UpdateUI 方法使用我们将创建的新事件总线 UiScoreNotificationBus 发送通知事件。
void GoalDetectorComponent::UpdateUi()
{
UiScoreNotificationBus::Broadcast(
&UiScoreNotificationBus::Events::OnTeamScored, m_team);
}
在 UI 中接收更新
为了在 UiScoreNotificationBus 上接收通知,我们将创建将接收通知的 UiScoreComponent。
图 23.3.更新用户界面中的文本字段
- 创建从总线的处理程序继承的 UiScoreComponent。
class UiScoreComponent
: public AZ::Component
, public UiScoreNotificationBus::Handler
- 实现在 UI 中更新 text 值的通知回调。
void UiScoreComponent::OnTeamScored(int team)
{
if (team >= 0 && team <= 1)
{
m_teams[team]++;
char buffer[10];
azsnprintf(buffer, 10,
"%d : %d", m_teams[0], m_teams[1]);
UiTextBus::Event(GetEntityId(),
&UiTextBus::Events::SetText, AZStd::string(buffer));
}
}
- 请注意,它在同一实体上调用 UiTextBus。将 UI Editor 中的 UiScoreComponent 添加到实体 Score Text 中,以便 UiScoreComponent 可以与文本组件通信。
UiTextBus 是 UI 文本组件的公共接口。
注意:如果组件在其 Reflect 方法中标记为“UI”,则可以将其添加到 UI 实体中。
Attribute(AppearsInAddComponentMenu, AZ_CRC_CE("UI"))
移动刚体
在本章中,一个可能会让你绊倒的陷阱是移动一个刚性的物理身体。如果尝试仅通过调用 AZ::TransformBus::Event::SetWorldTranslation 来移动它,则可能会发现球根本没有移动。原因是,在修改转换时,它被物理仿真覆盖。
纠正此问题的一种简单方法是禁用球的物理特性,移动它,然后重新启用物理特性。
例 23.1.移动刚体
void GoalDetectorComponent::RespawnBall(const AZ::Vector3& v)
{
Physics::RigidBodyRequestBus::Event(m_ball,
&Physics::RigidBodyRequestBus::Events::DisablePhysics);
AZ::TransformBus::Event(m_ball,
&AZ::TransformBus::Events::SetWorldTranslation, v);
Physics::RigidBodyRequestBus::Event(m_ball,
&Physics::RigidBodyRequestBus::Events::EnablePhysics);
}
小结
注意:本章的代码和资源可以在 GitHub 上找到,网址为: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch23_ui_gameplay
此时,当球击中足球场两侧的进球触发器时,游戏会更新比分 UI。
例 23.2.GoalDetectorComponent.h
#pragma once
#include <AzCore/Component/Component.h>
#include <AzFramework/Physics/Common/PhysicsEvents.h>
namespace MyGem
{
class GoalDetectorComponent
:
public AZ::Component
{
public:
AZ_COMPONENT(GoalDetectorComponent,
"{eaf6ae0a-7444-47fb-a759-8d7b8a6f3356}");
static void Reflect(AZ::ReflectContext* rc);
GoalDetectorComponent();
// AZ::Component interface implementation
void Activate() override;
void Deactivate() override {}
private:
AzPhysics::SceneEvents::
OnSceneTriggersEvent::Handler m_trigger;
void OnTriggerEvents(
const AzPhysics::TriggerEventList& tel);
AZ::EntityId m_ball;
AZ::EntityId m_reset;
int m_team = 0;
AZ::Vector3 GetRespawnPoint() const;
void RespawnBall(const AZ::Vector3& v);
void UpdateUi();
};
} // namespace MyGem
例 23.3.GoalDetectorComponent.cpp
#include <GoalDetectorComponent.h>
#include <AzCore/Component/TransformBus.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzFramework/Physics/PhysicsScene.h>
#include <AzFramework/Physics/RigidBodyBus.h>
#include <MyGem/UiScoreBus.h>
namespace MyGem
{
void GoalDetectorComponent::Reflect(AZ::ReflectContext* rc)
{
if (auto sc = azrtti_cast<AZ::SerializeContext*>(rc))
{
sc->Class<GoalDetectorComponent, AZ::Component>()
->Field("Team", &GoalDetectorComponent::m_team)
->Field("Ball", &GoalDetectorComponent::m_ball)
->Field("Respawn", &GoalDetectorComponent::m_reset)
->Version(1);
if (AZ::EditContext* ec = sc->GetEditContext())
{
using namespace AZ::Edit;
ec->Class<GoalDetectorComponent>(
"Goal Detector",
"[Detects when a goal is scored]")
->ClassElement(ClassElements::EditorData, "")
->Attribute(
Attributes::AppearsInAddComponentMenu,
AZ_CRC_CE("Game"))
->DataElement(0, &GoalDetectorComponent::m_team,
"Team", "Which team is this goal line for?")
->DataElement(0, &GoalDetectorComponent::m_ball,
"Ball", "Ball entity")
->DataElement(0, &GoalDetectorComponent::m_reset,
"Respawn", "where to put the ball after")
;
}
}
}
GoalDetectorComponent::GoalDetectorComponent()
: m_trigger([this](
AzPhysics::SceneHandle,
const AzPhysics::TriggerEventList& tel)
{
})
{
}
OnTriggerEvents(tel);
void GoalDetectorComponent::Activate()
{
auto* si = AZ::Interface<AzPhysics::SceneInterface>::Get();
if (si != nullptr)
{
AzPhysics::SceneHandle sh = si->GetSceneHandle(
AzPhysics::DefaultPhysicsSceneName);
si->RegisterSceneTriggersEventHandler(sh, m_trigger);
}
}
void GoalDetectorComponent::OnTriggerEvents(
const AzPhysics::TriggerEventList& tel)
{
using namespace AzPhysics;
for (const TriggerEvent& te : tel)
{
if (te.m_triggerBody &&
te.m_triggerBody->GetEntityId() == GetEntityId())
{
if (te.m_type == TriggerEvent::Type::Enter)
{
AZ::Vector3 respawnLocation = GetRespawnPoint();
RespawnBall(respawnLocation);
UpdateUi();
}
}
}
}
AZ::Vector3 GoalDetectorComponent::GetRespawnPoint() const
{
AZ::Vector3 respawnLocation = AZ::Vector3::CreateZero();
AZ::TransformBus::EventResult(respawnLocation, m_reset,
&AZ::TransformBus::Events::GetWorldTranslation);
return respawnLocation;
}
void GoalDetectorComponent::RespawnBall(const AZ::Vector3& v)
{
Physics::RigidBodyRequestBus::Event(m_ball,
&Physics::RigidBodyRequestBus::Events::DisablePhysics);
AZ::TransformBus::Event(m_ball,
&AZ::TransformBus::Events::SetWorldTranslation, v);
Physics::RigidBodyRequestBus::Event(m_ball,
&Physics::RigidBodyRequestBus::Events::EnablePhysics);
}
void GoalDetectorComponent::UpdateUi()
{
UiScoreNotificationBus::Broadcast(
&UiScoreNotificationBus::Events::OnTeamScored, m_team);
}
} // namespace MyGem
例 23.4. UiScoreComponent.h
#pragma once
#include <AzCore/Component/Component.h>
#include <MyGem/UiScoreBus.h>
namespace MyGem
{
class UiScoreComponent
:
public AZ::Component
,
public UiScoreNotificationBus::Handler
{
public:
AZ_COMPONENT(UiScoreComponent,
"{49b2e5e8-e028-48b1-bc69-82c73b32422b}");
static void Reflect(AZ::ReflectContext* rc);
// AZ::Component interface implementation
void Activate() override;
void Deactivate() override;
// UiScoreNotificationBus interface
void OnTeamScored(int team) override;
private:
int m_teams[2] = { 0, 0 };
};
} // namespace MyGem
例 23.5. UiScoreComponent.cpp
#include <UiScoreComponent.h>
#include <AzCore/Serialization/EditContext.h>
#include <LyShine/Bus/UiTextBus.h>
namespace MyGem
{
void UiScoreComponent::Reflect(AZ::ReflectContext* rc)
{
if (auto sc = azrtti_cast<AZ::SerializeContext*>(rc))
{
sc->Class<UiScoreComponent, AZ::Component>()
->Version(1);
if (AZ::EditContext* ec = sc->GetEditContext())
{
using namespace AZ::Edit;
ec->Class<UiScoreComponent>(
"Ui Score Component",
"[Updates score text]")
->ClassElement(ClassElements::EditorData, "")
->Attribute(
Attributes::AppearsInAddComponentMenu,
AZ_CRC_CE("UI"));
}
}
}
void UiScoreComponent::Activate()
{
UiScoreNotificationBus::Handler::BusConnect(GetEntityId());
}
void UiScoreComponent::Deactivate()
{
UiScoreNotificationBus::Handler::BusDisconnect();
}
void UiScoreComponent::OnTeamScored(int team)
{
if (team >= 0 && team <= 1)
{
m_teams[team]++;
char buffer[10];
azsnprintf(buffer, 10,
"%d : %d", m_teams[0], m_teams[1]);
UiTextBus::Event(GetEntityId(),
&UiTextBus::Events::SetText, AZStd::string(buffer));
}
}
} // namespace MyGem