本文内容
第36章 多人游戏物理
第36章 多人游戏物理
介绍
Note:本章随附的源代码和资源可以在 GitHub 上找到: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch36_multiplayer_physics
实现多人游戏玩家移动后,下一个优先事项是固定球。它是一个不尊重服务器的刚体。游戏或 Editor 的每个实例都是独立模拟的,目标检测器也是如此。
我们的第一步是将足球实体转换为网络刚体,然后实现服务器权威的进球检测,同时仍将比分更新提供给客户端的用户界面。
网络球
为了将球转化为网络刚体,我们必须做以下步骤:
- 添加 Network Binding 组件以将实体标记为网络实体。
- 添加 Network Transform 组件以同步其位置。
- 添加 Network Rigid Body (网络刚体) 组件,让服务器驱动模拟,同时在客户端上禁用此实体的物理特性。
- 将 Ball 实体转换为预制件。将预制件另存为 Network_Ball.prefab。
Note:我们必须使用预制件包装网络实体,否则关卡将无法加载,因为关卡中不直接支持网络实体。
通过这些更改,球已成为服务器权威实体,但目标检测器逻辑现在已中断,因为客户端不再运行球的物理模拟。物理形状触发器不再在客户端上针对被 Network Rigid Body (网络刚体) 组件禁用了物理特性的球触发。
解决方案是将分数通知从服务器发送到客户端。
Note:Network Rigid Body (网络刚体) 组件仍会在服务器上运行物理模拟,但会在客户端上对同一实体禁用它。这样,客户端实体将遵循服务器模拟。
网络属性
由于我们正在构建一个服务器权威游戏,因此服务器必须决定何时进球。为此,我们将 Goal Detector 组件转换为 Multiplayer 组件。进球后,需要将分数复制到客户端,以便他们知道何时进球。这需要我们在服务器和客户端上添加游戏逻辑,这意味着新的 Goal Detector 多人游戏组件将覆盖其生成的组件和控制器。
什么是网络属性?它是多人游戏组件上的一个属性,通常在服务器上控制并复制到客户端。您可以使用 Network Property 元素在多人游戏组件的 XML 定义中定义此类属性。
<NetworkProperty Type="int" Name="Score" Init="0"
ReplicateFrom="Authority" ReplicateTo="Client"
这是一个网络属性示例,该属性保留权威服务器可以修改并复制到客户端的游戏分数,如 Replicate From 和 Replicate To 字段所定义。在本章中,我们将在构建多人游戏目标检测器时看到它的实际应用。
多人游戏 Goal Detector
Note:将组件转换为多人游戏组件时,请将其从 AZ::Module 中手动注册的组件列表中删除。代码生成部分将为我们注册它。
以下是 Goal Detector 组件的单人游戏和多人游戏版本之间的差异。
团队作为原型属性
单人 Goal Detector 组件反映了团队标识符。
->DataElement(0, &GoalDetectorComponent::m_team,
"Team", "Which team is this goal line for?")
我们将使用 Archetype Property 执行相同的工作。
<ArchetypeProperty Type="int" Name="Team" Init="0"
ExposeToEditor="true" />
可以使用生成的 GetTeam() 方法访问此值。
void GoalDetectorComponent::OnScoreChanged(int newScore)
{
UiScoreNotificationBus::Broadcast(
&UiScoreNotificationBus::Events::OnTeamScoreChanged,
GetTeam(), newScore);
}
得分
旧的目标检测器将 score 值保留在 UI 组件中,但这不适用于服务器权威设计,因为 UI 甚至未加载到服务器启动器中。因此,我们将此值移动到新 Goal Detector 组件的控制器。这样,该值将由服务器控制,当进球得分时,客户端会收到 score 值的更新。
例 36.1.完成GoalDetectorComponent.AutoComponent.xml
<?xml version="1.0"?>
<Component
Name="GoalDetectorComponent"
Namespace="MyGem"
OverrideComponent="true"
OverrideController="true"
OverrideInclude="Multiplayer/GoalDetectorComponent.h"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" >
<NetworkProperty Type="int" Name="Score" Init="0"
ReplicateFrom="Authority" ReplicateTo="Client"
IsRewindable="false" IsPredictable="false" IsPublic="true"
Container="Object" ExposeToEditor="false"
ExposeToScript="true" GenerateEventBindings="true"
Description="Current score on this side" />
<ArchetypeProperty Type="int" Name="Team" Init="0"
ExposeToEditor="true" />
</Component>
这将为控制器类提供 ModifyScore()方法。检测目标的服务器端逻辑与 Goal Detector 组件的单人版本相同。
void GoalDetectorComponentController::OnTriggerEvents(
const AzPhysics::TriggerEventList& tel)
{
const AZ::EntityId me = GetEntity()->GetId();
using namespace AzPhysics;
for (const TriggerEvent& te : tel)
{
if (te.m_triggerBody && te.m_triggerBody->GetEntityId() == me)
{
if (te.m_type == TriggerEvent::Type::Enter)
{
// TODO respawn the ball
ModifyScore()++;
break;
}
}
}
}
Note:我们将在下一章中为足球添加重生逻辑。
生成事件绑定
Network Property 的 Generate Event Bindings 字段添加了一种注册更改通知的方法。
<NetworkProperty Name="Score" ... GenerateEventBindings="true" />
注册方法的名称是带有 “AddEvent” 后缀的属性的名称。
void ScoreAddEvent(AZ::Event<int>::Handler& handler);
在客户端上,ScoreAddEvent 将允许我们注册更改通知。方法如下。
- 创建事件处理程序。
AZ::Event<int>::Handler m_scoreChanged;
- 创建回调以处理更改。
void OnScoreChanged(int newScore);
- 将回调分配给组件构造函数中的处理程序。
GoalDetectorComponent::GoalDetectorComponent()
: m_scoreChanged([this](int newScore)
{
OnScoreChanged(newScore);
}) {}
- 将处理程序连接到激活组件时的事件。
void GoalDetectorComponent::OnActivate(
Multiplayer::EntityIsMigrating)
{
ScoreAddEvent(m_scoreChanged);
}
图 36.1.从 服务器到客户端的网络属性
实体更改
作为网络实体,每个目标实体都需要一个 Network Binding 和一个 Network Transform 组件。
图 36.2.更新了目标实体
由于目标实体现在是网络实体,因此我们需要将它们包装在预制件中。事实上,我们需要两个预制件,足球场的每一侧各一个。唯一的区别是分配给 Goal Detector 组件的团队值。其中一个的团队值为零 (0),另一个值的值为 1 (1)
小结
Note:本章随附的源代码和资源可以在 GitHub 上找到: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch36_multiplayer_physics
目标检测已成功移动到服务器。在 Editor 游戏模式中再次进球。UI 已更新。但是,进球后,足球不再被移动到场地中央。在下一章中,我将向您展示一种不同的方法,通过删除旧球并生成一个全新的球来重新启动球的位置。
在此之前,这里是多人游戏 Goal Detector 组件的源代码。
例 36.2.GoalDetectorComponent.AutoComponent.h
<?xml version="1.0"?>
<Component
Name="GoalDetectorComponent"
Namespace="MyGem"
OverrideComponent="true"
OverrideController="true"
OverrideInclude="Multiplayer/GoalDetectorComponent.h"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<NetworkProperty Type="int" Name="Score" Init="0"
ReplicateFrom="Authority" ReplicateTo="Client"
IsRewindable="false" IsPredictable="false" IsPublic="true"
Container="Object" ExposeToEditor="false"
ExposeToScript="true" GenerateEventBindings="true"
Description="Current score on this side"/>
<ArchetypeProperty Type="int" Name="Team" Init="0"
ExposeToEditor="true" />
</Component>
例 36.3. GoalDetectorComponent.h
#pragma once
#include <AzCore/Component/Component.h>
#include <AzFramework/Physics/Common/PhysicsEvents.h>
#include <Source/AutoGen/GoalDetectorComponent.AutoComponent.h>
namespace MyGem
{
class GoalDetectorComponent
:
public GoalDetectorComponentBase
{
public:
AZ_MULTIPLAYER_COMPONENT(MyGem::GoalDetectorComponent,
s_goalDetectorComponentConcreteUuid,
MyGem::GoalDetectorComponentBase);
GoalDetectorComponent();
static void Reflect(AZ::ReflectContext* context);
void OnInit() override {}
void OnActivate(Multiplayer::EntityIsMigrating) override;
void OnDeactivate(Multiplayer::EntityIsMigrating) override{}
private:
AZ::Event<int>::Handler m_scoreChanged;
void OnScoreChanged(int newScore);
};
class GoalDetectorComponentController
:
public GoalDetectorComponentControllerBase
{
public:
GoalDetectorComponentController(GoalDetectorComponent& p);
void OnActivate(Multiplayer::EntityIsMigrating) override;
void OnDeactivate(Multiplayer::EntityIsMigrating) override{}
private:
AzPhysics::SceneEvents::
OnSceneTriggersEvent::Handler m_trigger;
void OnTriggerEvents(
const AzPhysics::TriggerEventList& tel);
};
} // namespace MyGem
例 36.4. GoalDetectorComponent.cpp
#include <AzCore/Component/TransformBus.h>
#include <AzCore/Interface/Interface.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzFramework/Physics/PhysicsScene.h>
#include <Multiplayer/GoalDetectorComponent.h>
#include <MyGem/UiScoreBus.h>
namespace MyGem
{
GoalDetectorComponent::GoalDetectorComponent()
: m_scoreChanged([this](int newScore)
{
OnScoreChanged(newScore);
}) {}
void GoalDetectorComponent::OnScoreChanged(int newScore)
{
UiScoreNotificationBus::Broadcast(
&UiScoreNotificationBus::Events::OnTeamScoreChanged,
GetTeam(), newScore);
}
void GoalDetectorComponent::Reflect(AZ::ReflectContext* rc)
{
auto sc = azrtti_cast<AZ::SerializeContext*>(rc);
if (sc)
{
sc->Class<GoalDetectorComponent,
GoalDetectorComponentBase>()->Version(1);
}
GoalDetectorComponentBase::Reflect(rc);
if (auto bc = azrtti_cast<AZ::BehaviorContext*>(rc))
{
bc->EBus<UiScoreNotificationBus>("ScoreNotificationBus")
->Handler<ScoreNotificationHandler>();
}
}
void GoalDetectorComponent::OnActivate(
Multiplayer::EntityIsMigrating)
{
ScoreAddEvent(m_scoreChanged);
}
// Controller
GoalDetectorComponentController::GoalDetectorComponentController( GoalDetectorComponent& parent)
: GoalDetectorComponentControllerBase(parent)
, m_trigger([this](
AzPhysics::SceneHandle,
const AzPhysics::TriggerEventList& tel)
{
OnTriggerEvents(tel);
}) {}
void GoalDetectorComponentController::OnActivate(
Multiplayer::EntityIsMigrating)
{
auto* si = AZ::Interface<AzPhysics::SceneInterface>::Get();
if (si != nullptr)
{
AzPhysics::SceneHandle sh = si->GetSceneHandle(
AzPhysics::DefaultPhysicsSceneName);
si->RegisterSceneTriggersEventHandler(sh, m_trigger);
}
}
void GoalDetectorComponentController::OnTriggerEvents(
const AzPhysics::TriggerEventList& tel)
{
const AZ::EntityId me = GetEntity()->GetId();
using namespace AzPhysics;
for (const TriggerEvent& te : tel)
{
if (te.m_triggerBody &&
te.m_triggerBody->GetEntityId() == me)
{
if (te.m_type == TriggerEvent::Type::Enter)
{
// TODO respawn the ball
ModifyScore()++;
break;
}
}
}
}
} // namespace MyGem