Version:

第37章 移除与生成

第37章 移除与生成

介绍

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

第 36 章 多人游戏物理特性 重新实现了目标检测,以使用服务器授权机制。

但是,足球并没有被放回足球场的中间。在本章中,我将向您展示如何通过删除旧球并生成一个新球来做到这一点。

图 37.1.新 Ball Spawner 的设计

  1. 以前,足球是直接在 Editor 中放置在水平面上的。这一次,具有新 Ball Spawner 组件的新实体将在实体激活时执行此作。
  2. 我在第 11 章 预制件简介中介绍了如何生成预制件。
  3. 生成 Network_Ball.prefab 后,Ball 组件将使用 OnBallSpawned 回调生成器。生成器将保存实体指针以管理其生命周期。
  4. 当进球后需要重新生成球时,新的 EBus 请求将要求生成器使用 RespawnBall 重新生成球。
  5. MarkForRemoval 是本章中介绍的新多人游戏 Gem API。它能够删除网络实体。
注意:
不得直接停用和删除网络实体。必须使用 Network Entity Manager 中的 MarkForRemoval 方法,如本章后面所示。
  1. 游戏循环通过生成一个新球来继续

球组件

通过创建 Ball 组件并将其放置在 Ball 实体上,我们可以在生成后在关卡中激活新 Ball 时发送事件。由于生成和删除是在服务器上启动的,因此此多人游戏组件需要一个控制器。在服务器上创建新的网络实体时,多人游戏系统将负责在客户端上创建实体。在服务器上删除网络实体时也是如此。

例 37.1.BallComponent.AutoComponent.xml

<?xml version="1.0"?>
 <Component
 Name="BallComponent"
 Namespace="MyGem"
 OverrideComponent="false"
 OverrideController="true"
 OverrideInclude="Multiplayer/BallComponent.h"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 </Component>

当 Ball 组件的 controller 在其实体激活时,会发出通知。

void BallComponentController::OnActivate(
        Multiplayer::EntityIsMigrating)
    {
        BallNotificationBus::Broadcast(
            &BallNotificationBus::Events::OnBallSpawned,
            GetEntity());
    }

这就是 Ball Component 所做的全部工作。

Ball Spawner 组件

我们已经在第 11 章 预制件简介中实现了一个生成器。这一次,我们将在 Multiplayer 组件中执行此作,该组件会删除旧实体并生成新实体。

为了循环遍历足球实体的引用,Ball Spawner 组件会将其指针保存在一个小的环形缓冲区中。

AZStd::ring_buffer<AZ::Entity*> m_balls;
m_balls.set_capacity(2);
提示:
环形缓冲区是具有可配置常量大小的圆形容器。新条目将首先覆盖最旧的元素。

删除网络实体

提示:

不得通过直接停用网络实体来删除网络实体。网络实体在幕后有许多系统处理其状态和更新。必须使用 MarkForRemoval 从 INetworkEntityManager 接口请求删除它们。

//! Marks the specified entity for removal and deletion.
//! @param entityHandle the entity to remove and delete
void MarkForRemoval(const ConstNetworkEntityHandle& h);

Multiplayer::ConstNetworkEntityHandle 是引用网络实体的包装器。可以通过为其提供指向网络实体的指针来构造它。

注意:
网络实体是具有 Network Binding 组件的实体。 以下是使用指向其实体的指针删除旧球的方法。
AZ::Entity* previousBall = m_balls.back();
Multiplayer::ConstNetworkEntityHandle oldBall(previousBall);
AZ::Interface<IMultiplayer>::Get()->
GetNetworkEntityManager()->MarkForRemoval(oldBall);
注意:
标记的实体不会立即消失,可能需要另一个游戏时钟周期才能清理和删除实体和各种网络资源。

为了禁用与旧球的任何不需要的交互,我们将停用物理,以便旧球不再激活目标触发器。

using RigidBus = Physics::RigidBodyRequestBus;
RigidBus::Event(previousBall->GetId(),
&RigidBus::Events::DisablePhysics);

生成网络实体

生成网络实体使用的方法与第 11 章 预制件简介中的方法相同,但有一个有趣的细节。我们将为要生成的球预制件指定一个 Spawnable 字段。

图 37.2.具有预制件的 Ball Spawner

在第 11 章“预制件简介”中,我们自己反射了 AZ::Data::Asset AzFramework::Spawnable但我们也可以使用自动组件 XML 定义中的 Archetype 属性来实现这一点。

<ArchetypeProperty
 Type="AZ::Data::Asset&lt;AzFramework::Spawnable&gt;"
 Name="BallAsset" Init="" ExposeToEditor="true" />

我们将能够通过其生成的 getter 方法 GetBallAsset() 获取此资产。

注意:

XML 不允许在元素属性值中使用小于 (“<”) 和大于 (“>”) 符号,因此我们必须用 “<” 和 “>” 来转义它们

此外,对于特殊类型(如 AzFramework::Spawnable),您必须为其包含文件提供 Include 元素,以便生成的基类可以编译。

<Include File="AzCore/Asset/AssetSerializer.h"/>
<Include File="AzFramework/Spawnable/Spawnable.h"/>

我们将在 Ball Spawner (球生成器) 组件所在的位置生成球。我们可以通过要求 auto-component generator 为我们提供 transform 组件的 getter 来帮助自己。

<ComponentRelation Constraint="Weak" HasController="false"
Name="TransformComponent" Namespace="AzFramework"
Include="AzFramework/Components/TransformComponent.h"/>

这将使我们能够通过调用 GetParent() 来获取世界变换。GetTransformComponent()->GetWorldTM()的 GetWorldTM()。这是完整的 RespawnBall 方法。

例 37.2.RespawnBall 方法

void BallSpawnerComponentController::RespawnBall()
    {
        RemoveOldBall();
 using namespace AzFramework;
        AZ::Transform world = GetParent().GetTransformComponent()->
            GetWorldTM();
        m_ticket = AzFramework::EntitySpawnTicket{ GetBallAsset() };
 // place the ball
 auto cb = [world](
            EntitySpawnTicket::Id /*ticketId*/,
            SpawnableEntityContainerView view)
        {
 const AZ::Entity* e = *view.begin();
 if (auto* tc = e->FindComponent<TransformComponent>())
            {
                tc->SetWorldTM(world);
            }
        };
        SpawnAllEntitiesOptionalArgs optionalArgs;
        optionalArgs.m_preInsertionCallback = AZStd::move(cb);
        SpawnableEntitiesInterface::Get()->SpawnAllEntities(
            m_ticket, AZStd::move(optionalArgs));
    }

实体更改

  1. 通过将 Ball 组件添加到 Ball 实体来修改 Network_Ball.prefab。
  2. 由于我们要生成球,因此请从关卡中删除 Network_Ball.prefab。
  3. 创建一个名为 Ball Spawner 的新实体。
  4. 向其添加一个 Network Binding(网络绑定)、一个 Network Transform(网络转换)和一个 Ball Spawner (球生成器) 组件。
  5. 将Network_Ball分配给 Ball Spawner 组件的 Ball Asset 字段。

图 37.3.Ball Spawner 实体

  1. 从中创建一个名为 Ball_Spawner.prefab 的预制件。

小结

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

在本章中,我们为足球添加了重生逻辑。进球的那一刻,当前球将被标记为移除,这会在一个游戏帧左右的时间内将其移除,然后在场地中间生成一个新球。

以下是 Ball 和 Ball Spawner 组件的完整源代码。

例 37.3.BallComponent.AutoComponent.h

<?xml version="1.0"?>
 <Component
 Name="BallComponent"
 Namespace="MyGem"
 OverrideComponent="false"
 OverrideController="true"
 OverrideInclude="Multiplayer/BallComponent.h"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
</Component>

例 37.4. BallBus.h

 #pragma once
 #include <AzCore/Component/ComponentBus.h>
 namespace MyGem
 {
 class BallNotifications
        : 
public AZ::ComponentBus
    {
 public:
 virtual void OnBallSpawned(AZ::Entity* ballEntity) = 0;
    };
 using BallNotificationBus = AZ::EBus<BallNotifications>;
 }

例 37.5. BallComponent.h

#pragma once
#include <Source/AutoGen/BallComponent.AutoComponent.h>
namespace MyGem
{
class BallComponentController
:
public BallComponentControllerBase
{
public:
BallComponentController(BallComponent& parent);
void OnActivate(Multiplayer::EntityIsMigrating) override;
void OnDeactivate(Multiplayer::EntityIsMigrating) override{}
};
}

例 37.6. BallComponent.cpp

#include <Multiplayer/BallComponent.h>
#include <MyGem/BallBus.h>
namespace MyGem
{
BallComponentController::BallComponentController(
BallComponent& parent)
: BallComponentControllerBase(parent) {}
void BallComponentController::OnActivate(
Multiplayer::EntityIsMigrating)
{
  BallNotificationBus::Broadcast(
            &BallNotificationBus::Events::OnBallSpawned,
            GetEntity());
    }
 }

例 37.7. BallSpawnerComponent.AutoComponent.h

 <?xml version="1.0"?>
<Component
Name="BallSpawnerComponent"
Namespace="MyGem"
OverrideComponent="false"
OverrideController="true"
OverrideInclude="Multiplayer/BallSpawnerComponent.h"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ComponentRelation Constraint="Weak" HasController="false"
Name="TransformComponent" Namespace="AzFramework"
Include="AzFramework/Components/TransformComponent.h"/>
<Include File="AzCore/Asset/AssetSerializer.h"/>
<Include File="AzFramework/Spawnable/Spawnable.h"/>
<ArchetypeProperty
Type="AZ::Data::Asset&lt;AzFramework::Spawnable&gt;"
Name="BallAsset" Init="" ExposeToEditor="true" />
</Component>

例 37.8. BallSpawnerBus.h

#pragma once
#include <AzCore/Component/ComponentBus.h>
namespace MyGem
{
}
class BallSpawnerRequests
:
public AZ::ComponentBus
{
public:
virtual void RespawnBall() = 0;
};
using BallSpawnerRequestBus = AZ::EBus<BallSpawnerRequests>;

例 37.9. BallSpawnerComponent.h

#pragma once
#include <AzCore/std/containers/ring_buffer.h>
#include <MyGem/BallBus.h>
#include <MyGem/BallSpawnerBus.h>
#include <Source/AutoGen/BallSpawnerComponent.AutoComponent.h>
namespace MyGem
{
class BallSpawnerComponentController
        : 
public BallSpawnerComponentControllerBase
        , 
        , 
public BallSpawnerRequestBus::Handler
 public BallNotificationBus::Handler
    {
 public:
        BallSpawnerComponentController(BallSpawnerComponent& parent);
 void OnActivate(Multiplayer::EntityIsMigrating) override;
 void OnDeactivate(Multiplayer::EntityIsMigrating) override;
 // BallRequestBus
 void RespawnBall() override;
 // BallNotificationBus
 void OnBallSpawned(AZ::Entity* e) override;
 private:
        AzFramework::EntitySpawnTicket m_ticket;
        AZStd::ring_buffer<AZ::Entity*> m_balls;
 void RemoveOldBall();
    };
 }

例 37.10. BallSpawnerComponent.cpp

 #include <AzFramework/Components/TransformComponent.h>
 #include <AzFramework/Physics/RigidBodyBus.h>
 #include <Multiplayer/BallSpawnerComponent.h>
 #include <Multiplayer/Components/NetworkTransformComponent.h>
 #include <MyGem/BallSpawnerBus.h>
 namespace MyGem
 {
    BallSpawnerComponentController::BallSpawnerComponentController(
        BallSpawnerComponent& parent)
        : BallSpawnerComponentControllerBase(parent){}
 void BallSpawnerComponentController::OnActivate(
        Multiplayer::EntityIsMigrating)
    {
 const AZ::EntityId me = GetEntity()->GetId();
        BallSpawnerRequestBus::Handler::BusConnect(me);
        BallNotificationBus::Handler::BusConnect(me);
        m_balls.set_capacity(2);
        RespawnBall();
    }
 void BallSpawnerComponentController::OnDeactivate(
        Multiplayer::EntityIsMigrating)
    {
        BallSpawnerRequestBus::Handler::BusDisconnect();
        BallNotificationBus::Handler::BusDisconnect();
  }
 void BallSpawnerComponentController::RespawnBall()
    {
        RemoveOldBall();
 using namespace AzFramework;
        AZ::Transform world = GetParent().GetTransformComponent()->
            GetWorldTM();
        m_ticket = AzFramework::EntitySpawnTicket{ GetBallAsset() };
 auto cb = [world](
            EntitySpawnTicket::Id /*ticketId*/,
            SpawnableEntityContainerView view)
        {
 const AZ::Entity* e = *view.begin();
 if (auto* tc = e->FindComponent<TransformComponent>())
            {
                tc->SetWorldTM(world);
            }
        };
        SpawnAllEntitiesOptionalArgs optionalArgs;
        optionalArgs.m_preInsertionCallback = AZStd::move(cb);
        SpawnableEntitiesInterface::Get()->SpawnAllEntities(
            m_ticket, AZStd::move(optionalArgs));
    }
 void BallSpawnerComponentController::OnBallSpawned(AZ::Entity* e)
    {
        m_balls.push_back(e);
    }
 void BallSpawnerComponentController::RemoveOldBall()
    {
 if (m_balls.empty() == false)
        {
            AZ::Entity* previousBall = m_balls.back();
 using RigidBus = Physics::RigidBodyRequestBus;
            RigidBus::Event(previousBall->GetId(),
                &RigidBus::Events::DisablePhysics);
 using namespace Multiplayer;
 const ConstNetworkEntityHandle oldBall(previousBall);
            AZ::Interface<IMultiplayer>::Get()->
                GetNetworkEntityManager()->MarkForRemoval(oldBall);
        }
    }
 }