本文内容
第37章 移除与生成
第37章 移除与生成
介绍
注意:本章随附的源代码和资源可以在 GitHub 上找到: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch37_removal_and_spawning
第 36 章 多人游戏物理特性 重新实现了目标检测,以使用服务器授权机制。
但是,足球并没有被放回足球场的中间。在本章中,我将向您展示如何通过删除旧球并生成一个新球来做到这一点。
图 37.1.新 Ball Spawner 的设计
- 以前,足球是直接在 Editor 中放置在水平面上的。这一次,具有新 Ball Spawner 组件的新实体将在实体激活时执行此作。
- 我在第 11 章 预制件简介中介绍了如何生成预制件。
- 生成 Network_Ball.prefab 后,Ball 组件将使用 OnBallSpawned 回调生成器。生成器将保存实体指针以管理其生命周期。
- 当进球后需要重新生成球时,新的 EBus 请求将要求生成器使用 RespawnBall 重新生成球。
- MarkForRemoval 是本章中介绍的新多人游戏 Gem API。它能够删除网络实体。
注意:不得直接停用和删除网络实体。必须使用 Network Entity Manager 中的 MarkForRemoval 方法,如本章后面所示。
- 游戏循环通过生成一个新球来继续
球组件
通过创建 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<AzFramework::Spawnable>"
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));
}
实体更改
- 通过将 Ball 组件添加到 Ball 实体来修改 Network_Ball.prefab。
- 由于我们要生成球,因此请从关卡中删除 Network_Ball.prefab。
- 创建一个名为 Ball Spawner 的新实体。
- 向其添加一个 Network Binding(网络绑定)、一个 Network Transform(网络转换)和一个 Ball Spawner (球生成器) 组件。
- 将Network_Ball分配给 Ball Spawner 组件的 Ball Asset 字段。
图 37.3.Ball Spawner 实体
- 从中创建一个名为 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<AzFramework::Spawnable>"
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);
}
}
}