本文内容
第24章 踢球
第24章 踢球
介绍
对我来说,下一个明显的游戏问题是鸡没有很好地推动球。有时它会,有时球会忽略 完全是鸡。
本章将通过执行以下作来解决此问题:
- 在鸡上添加物理触发器。
- 使用扳机检测鸡何时与球相撞。
- 计算从鸡到球的冲力方向。
- 对球施加脉冲以“踢”球。
注意:本章的代码和资源可以在 GitHub 上找到,网址为: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch24_kicking_ball
问题的根本原因
角色没有很好地推球的原因(如果有的话)实际上是物理模拟的设计决策。O3DE 使用 PhysX 作为您目前所看到的碰撞器和刚体组件背后的模拟引擎。以下是 PhysX SDK 页面中关于角色和动态角色之间交互的引述。
让物理引擎通过在接触点施加力来推动动态对象是很诱人的。然而,这通常不是一个非常有说服力的解决方案。角色周围的边界体积是人工的(框、胶囊体等)且不可见的,因此物理引擎在边界体积与其周围对象之间计算的力无论如何都不会是真实的。它们将无法正确模拟实际角色与这些对象之间的交互。 —PhysX 4.1 SDK Guide, Character Controllers
长话短说,这取决于我们实现鸡角色和球之间的互动。这不是问题。我们可以使用前几章的知识来解决它。
在鸡上触发形状
以下是在鸡上添加触发器形状的步骤。
- 在 Chicken_Actor 下创建一个子实体。
- 将新实体命名为 Kick Trigger。
- 将 PhysX Collider (PhysX 碰撞器) 组件添加到新实体。
- 我们还将创建一个新组件 KickingComponent 并将其添加到此实体中
- 在第 23 章 在 C++ 中与 UI 交互 中,我们创建了一个新的碰撞层和一个组。我们可以将踢球扳机音量的碰撞过滤到球上。将 Collision Layer 设置为 “Default”。
- 将 Collides with (碰撞对象) 设置为 “Only Ball(仅球)”。
- 在碰撞器上启用 Trigger 属性。
- 将碰撞体的形状设置为球体。您可能需要修改球体的大小和偏移量以匹配鸡的身体。
上述步骤应在鸡上创建以下触发器形状。
图 24.1.扳机形状的鸡
Kicking Component
扳机形状将检测球何时紧挨着鸡。我们需要处理触发事件并将 impulse 应用于球。
图 24.2.KickingComponent (踢组件)
Kicking (踢球) 组件将具有可配置的冲量以应用于球。注册碰撞通知的逻辑与第 23 章 在 C++ 中与 UI 交互 中的 GoalDetectorComponent 非常相似。
图 24.3.踢脚组件的设计
- 创建 trigger 事件处理程序。
AzPhysics::SceneEvents::OnSceneTriggersEvent::Handler m_trigger;
- 分配一个 lambda 以将事件的处理传递给成员函数。
KickingComponent::KickingComponent()
: m_trigger([this](
AzPhysics::SceneHandle,
const AzPhysics::TriggerEventList& tel)
{
OnTriggerEvents(tel);
})
{
}
- 向 PhysX 子系统注册事件处理程序。
void KickingComponent::Activate()
{
auto* si = AZ::Interface<AzPhysics::SceneInterface>::Get();
if (si != nullptr)
{
AzPhysics::SceneHandle sh = si->GetSceneHandle(
AzPhysics::DefaultPhysicsSceneName);
si->RegisterSceneTriggersEventHandler(sh, m_trigger);
}
}
- 当触发事件发生时,检查球是否进入了触发体积,如果是,则踢出它。
void KickingComponent::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)
{
KickBall(te.m_otherBody->GetEntityId());
}
}
}
}
- 踢球是通过计算从鸡到球的方向,沿该方向创建向量,并在球的刚体组件上使用 ApplyLinearImpulse 来完成的。
void KickingComponent::KickBall(AZ::EntityId b)
{
AZ::Vector3 impulse = GetBallPosition(b) - GetSelfPosition();
impulse.Normalize();
impulse *= m_kickForce;
AddImpulseToBall(impulse, b);
}
void KickingComponent::AddImpulseToBall(AZ::Vector3 v, AZ::EntityId ball)
{
Physics::RigidBodyRequestBus::Event(ball,
&Physics::RigidBodyRequestBus::Events::ApplyLinearImpulse,
v);
}
重量与力
您将不得不调整对球施加多少冲量与球的重量。
力可以在我们的 KickingComponent 上修改。
可以在 Physx Rigid Body (Physx 刚体) 组件上修改球的质量,方法是通过禁用 Compute Mass (计算质量) 并手动指定 Mass (质量) 值来关闭自动质量计算。
我发现球的质量为 200 (200),脉冲值为 1000 对我们的游戏很有效。
源代码
注意:本章的代码和资源可以在 GitHub 上找到,网址为: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch24_kicking_ball
在本章中,我们添加了一个新组件 KickingComponent,以使鸡以适合游戏玩法的一致方式踢球。
例 24.1.KickingComponent.h
#pragma once
#include <AzCore/Component/Component.h>
#include <AzFramework/Physics/Common/PhysicsEvents.h>
namespace MyGem
{
class KickingComponent
:
public AZ::Component
{
public:
AZ_COMPONENT(KickingComponent,
"{73A60188-9BFB-4168-A733-8A06BC500ECB}");
static void Reflect(AZ::ReflectContext* rc);
KickingComponent();
// AZ::Component interface implementation
void Activate() override;
void Deactivate() override {}
private:
AzPhysics::SceneEvents::
OnSceneTriggersEvent::Handler m_trigger;
void OnTriggerEvents(
const AzPhysics::TriggerEventList& tel);
float m_kickForce = 1000.f;
void KickBall(AZ::EntityId ball);
AZ::Vector3 GetBallPosition(AZ::EntityId ball);
AZ::Vector3 GetSelfPosition();
void AddImpulseToBall(AZ::Vector3 v, AZ::EntityId ball);
};
} // namespace MyGem
例 24.2.KickingComponent.cpp
#include <KickingComponent.h>
#include <AzCore/Component/Entity.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>
namespace MyGem
{
void KickingComponent::Reflect(AZ::ReflectContext* rc)
{
if (auto sc = azrtti_cast<AZ::SerializeContext*>(rc))
{
sc->Class<KickingComponent, AZ::Component>()
->Field("Kick force", &KickingComponent::m_kickForce)
->Version(1);
if (AZ::EditContext* ec = sc->GetEditContext())
{
using namespace AZ::Edit;
ec->Class<KickingComponent>(
"Kicking",
"[Kicking the ball when it comes close]")
->ClassElement(ClassElements::EditorData, "")
->Attribute(
Attributes::AppearsInAddComponentMenu,
AZ_CRC_CE("Game"))
->DataElement(0, &KickingComponent::m_kickForce,
"Kick force", "impulse strength");
}
}
}
KickingComponent::KickingComponent()
: m_trigger([this](
AzPhysics::SceneHandle,
const AzPhysics::TriggerEventList& tel)
{
OnTriggerEvents(tel);
}) {}
void KickingComponent::Activate()
{
auto* si = AZ::Interface<AzPhysics::SceneInterface>::Get();
if (si != nullptr)
{
AzPhysics::SceneHandle sh = si->GetSceneHandle(
AzPhysics::DefaultPhysicsSceneName);
si->RegisterSceneTriggersEventHandler(sh, m_trigger);
}
}
void KickingComponent::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)
{
KickBall(te.m_otherBody->GetEntityId());
}
}
}
}
void KickingComponent::KickBall(AZ::EntityId b)
{
AZ::Vector3 impulse = GetBallPosition(b) - GetSelfPosition();
impulse.Normalize();
impulse *= m_kickForce;
AddImpulseToBall(impulse, b);
}
AZ::Vector3 KickingComponent::GetBallPosition(AZ::EntityId ball)
{
AZ::Vector3 ballPosition = AZ::Vector3::CreateZero();
AZ::TransformBus::EventResult(ballPosition, ball,
&AZ::TransformBus::Events::GetWorldTranslation);
return ballPosition;
}
AZ::Vector3 KickingComponent::GetSelfPosition()
{
return GetEntity()->GetTransform()->
GetWorldTM().GetTranslation();
}
void KickingComponent::AddImpulseToBall(
AZ::Vector3 v, AZ::EntityId ball)
{
Physics::RigidBodyRequestBus::Event(ball,
&Physics::RigidBodyRequestBus::Events::ApplyLinearImpulse,
v);
}
} // namespace MyGem