Version:

第36章 多人游戏物理

第36章 多人游戏物理

介绍

Note:
本章随附的源代码和资源可以在 GitHub 上找到: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch36_multiplayer_physics

实现多人游戏玩家移动后,下一个优先事项是固定球。它是一个不尊重服务器的刚体。游戏或 Editor 的每个实例都是独立模拟的,目标检测器也是如此。

我们的第一步是将足球实体转换为网络刚体,然后实现服务器权威的进球检测,同时仍将比分更新提供给客户端的用户界面。

网络球

为了将球转化为网络刚体,我们必须做以下步骤:

  1. 添加 Network Binding 组件以将实体标记为网络实体。
  2. 添加 Network Transform 组件以同步其位置。
  3. 添加 Network Rigid Body (网络刚体) 组件,让服务器驱动模拟,同时在客户端上禁用此实体的物理特性。
  4. 将 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 将允许我们注册更改通知。方法如下。

  1. 创建事件处理程序。
AZ::Event<int>::Handler m_scoreChanged;
  1. 创建回调以处理更改。
void OnScoreChanged(int newScore);
  1. 将回调分配给组件构造函数中的处理程序。
GoalDetectorComponent::GoalDetectorComponent()
: m_scoreChanged([this](int newScore)
{
OnScoreChanged(newScore);
}) {}
  1. 将处理程序连接到激活组件时的事件。
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