本文内容
第35章 多人游戏输入控制
第35章 多人游戏输入控制
介绍
注意:本章随附的源代码和资源可以在 GitHub 上找到: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch35_multiplayer_input
我们的鸡似乎已经在自行移动了。毕竟,它确实会在 Editor 中移动。但是,如果您要启动独立服务器,您会注意到 chicken 实体仅在客户端上移动。我们仍在使用直接控制实体位置的单人输入组件。
到目前为止,服务器不知道您正在发出 input。
注意:在所有多人游戏主题中,本章将是最复杂的,因为它涉及将旧的单人玩家输入逻辑重写到支持本地预测、校正和服务器端回滚的系统中。 好消息是,实际逻辑与第 18 章 角色移动 非常相似。它只需要放在不同的方法下。在本章中,我将并排介绍单人游戏和多人游戏的实现。
鸡移动组件
本章将创建一个新组件 ChickenMovementComponent 来替换 ChickenControllerComponent。它将是一个多人游戏组件,可以捕获和处理输入,使多人游戏 Gem 可以在本地预测鸡的运动,而无需我们执行任何额外的工作。
与多人游戏组件一样,我们将需要三 (3) 个新文件。
例 35.1.Gems\MyGem\Code\mygem_files.cmake 的新增功能
set(FILES
...
# new
Source/AutoGen/ChickenMovementComponent.AutoComponent.xml
Source/Multiplayer/ChickenMovementComponent.h
Source/Multiplayer/ChickenMovementComponent.cpp
)
XML定义
ChickenMovementComponent 的 XML 定义将包含许多新元素。本节将介绍每种新类型:
- 组件关系
- 原型属性
- 网络输入
组件关系
多人游戏控制器可以声明对同一实体上另一个组件的控制器的依赖关系。这将生成一个 getter 方法。例如,我们需要使用 NetworkCharacter 组件的控制器来移动我们的多人游戏鸡。其 API 是 NetworkCharacterComponentController::TryMoveWithVelocity。我们将声明一个组件关系,而不是尝试获取实体,然后获取网络角色组件,然后与 API 对抗以获取另一个实体的控制器。
<ComponentRelation Constraint="Required" HasController="true"
Name="NetworkCharacterComponent" Namespace="Multiplayer"
Include="Multiplayer/Components/NetworkCharacterComponent.h"/>
这将允许我们直接从 Chicken Movement Component Controller 调用 TryMoveWithVelocity。
GetNetworkCharacterComponentController()->
TryMoveWithVelocity(m_velocity, deltaTime);
注意:这类似于在 AZ::Component 上提供 GetRequiredServices() 方法,但还有一个额外的好处,即接收有用的 getter 方法。
Archetype 属性
在第 18 章 角色移动中,ChickenControllerComponent 向编辑器公开了 Speed、Turn Speed 和 Gravity 设置。我们自己必须反映出来。
class ChickenControllerComponent
{
float m_speed = 6.f;
float m_turnSpeed = 0.005.f;
float m_gravity = -9.8f;
//...
->DataElement(nullptr,
&ChickenControllerComponent::m_turnSpeed,
"Turn Speed", "Chicken's turning speed")
/// and so on
使用 Archetype Property,我们可以节省精力并改用代码生成器。
<ArchetypeProperty Type="float" Name="WalkSpeed"
Init="6.0f" ExposeToEditor="true" />
<ArchetypeProperty Type="float" Name="TurnSpeed"
Init="0.005f" ExposeToEditor="true" />
<ArchetypeProperty Type="float" Name="Gravity"
Init="-9.8f" ExposeToEditor="true" />
Archetype Property 将为我们完成将细节反映给编辑器的所有工作。
网络输入
在第 18 章 角色移动 中,ChickenControllerComponent 负责处理玩家输入并将其存储在 ChickenInput 结构中
class ChickenInput
{
public:
float m_forwardAxis = 0;
float m_strafeAxis = 0;
float m_viewYaw = 0;
};
对于多人游戏输入,我们将这些声明为 NetworkInput 的。
<NetworkInput Type="float" Name="ForwardAxis" Init="0.0f" />
<NetworkInput Type="float" Name="StrafeAxis" Init="0.0f" />
<NetworkInput Type="float" Name="ViewYaw" Init="0.0f" />
<NetworkInput Type="uint8_t" Name="ResetCount" Init="0"/>
注意:Reset Count 是一个特殊变量,每当实体以常规方式移动 (例如传送到新位置或踏上发射台) 时,该变量都会递增。如果没有此计数器,本地预测将存在突然移动和状态更改的问题。
注意:具有 Network Input 的多人游戏组件需要实体上的 Local Prediction Player Input 组件。
创建输入
当您将 Network Input 添加到多人游戏组件时,代码生成器将添加两 (2) 种新方法供您实施。
//! Common input creation logic for the NetworkInput.
//! Fill out the input struct and the MultiplayerInputDriver
//! will send the input data over the network to ensure
//! it's processed.
//! @param input input structure which to store input data
//! for sending to the authority
//! @param deltaTime amount of time to integrate
//! the provided inputs over
void CreateInput(
Multiplayer::NetworkInput& input,
float deltaTime) override;
//! Common input processing logic for the NetworkInput.
//! @param input input structure to process
//! @param deltaTime amount of time to integrate the
//! provided inputs over
void ProcessInput(
Multiplayer::NetworkInput& input,
float deltaTime) override;
输入的创建发生在玩家拥有的实体(也称为自治实体)上的客户端上。
他们的任务是收集玩家的输入并将其发送到服务器。
图 35.1.将玩家输入发送到服务器
- UpdateAutonomous 以指定的客户端输入速率调用。您可以使用 Multiplayer (多人游戏) Gem 变量 cl_InputRateMs 来控制此速率。默认值为每 33 毫秒收集一次 Input。
AZ_CVAR(AZ::TimeMs, cl_InputRateMs, AZ::TimeMs{ 33 }, nullptr,
AZ::ConsoleFunctorFlags::Null,
"Rate at which to sample and process client inputs");
- NetworkInputArray 是一个容器,其中包含当前输入和之前的七 (7) 个输入。我们的工作是收集当前的输入值。Local Prediction Player Input 组件将代表我们从本地输入历史记录中填充较旧的条目。
为什么我们每次都发送七 (7) 个较旧的输入?这是因为输入是使用不可靠的远程过程调用发送的。以下是多人游戏 Gem 中 LocalPredictionPlayerInputComponent 的 XML 定义的代码段,其中 IsReliable 设置为 false。
例 35.2. LocalPredictionPlayerInputComponent.AutoComponent.xml
<RemoteProcedure Name="SendClientInput" InvokeFrom="Autonomous"
HandleOn="Authority" IsPublic="true" IsReliable="false"
GenerateEventBindings="false"
Description="Client to server move / input RPC">
<Param Type="Multiplayer::NetworkInputArray" Name="inputArray" />
<Param Type="AZ::HashValue32" Name="stateHash" />
</RemoteProcedure>
多人游戏复制使用 UDP1 协议,该协议在设计上不可靠,无法实现更快的交付并减少停顿。多人游戏 Gem 通过发送多个输入来克服此问题。这意味着我们最多可以丢失七 (7) 个输入,但仍然能够恢复。给定 33 毫秒的客户端输入速率,则允许服务器无法接收所有输入之前有近四分之一秒的恢复时间(231 毫秒)。这对于大多数实际网络场景来说已经足够了 3. 从 Network Input Array 中,多人游戏系统将获取第一项(在第 0 个索引处)作为 NetworkInput,并将其传递给所有相关的多人游戏组件。 4. CreateInput 的工作是收集最后一个输入时间段的输入。 5. ProcessInput 立即应用于客户端。这是本地预测步骤,客户端猜测输入将采用何处。如果服务器不同意,客户端将在稍后得到纠正。 6. 然后将输入发送到服务器。
注意:这是对 input processing logic(输入处理逻辑)的幕后介绍。用户只需担心实现 CreateInput 和 ProcessInput 方法。其余工作将由 Multiplayer Gem 完成。
以下是单人创建输入法和多人创建输入法之间的并排比较
例 35.3.单人创建输入逻辑
ChickenInput ChickenControllerComponent::CreateInput()
{
ChickenInput input;
input.m_forwardAxis = m_forward;
input.m_strafeAxis = m_strafe;
input.m_viewYaw = m_yaw;
return input;
}
这是多人游戏
例 35.4.多人游戏创建输入逻辑
void ChickenMovementComponentController::CreateInput(
Multiplayer::NetworkInput& input,
[[maybe_unused]] float deltaTime)
{
auto chickenInput = input.FindComponentInput<
ChickenMovementComponentNetworkInput>();
chickenInput->m_forwardAxis = m_forward;
chickenInput->m_strafeAxis = m_strafe;
chickenInput->m_viewYaw = m_yaw;
chickenInput->m_resetCount =
GetNetworkTransformComponentController()->GetResetCount();
}
ChickenMovementComponentNetworkInput 是根据列出所有网络输入的 XML 定义为我们生成的。
例 35.5.来自 ChickenMovementComponent.AutoComponent.h 的片段
class ChickenMovementComponentNetworkInput
:
public Multiplayer::IMultiplayerComponentInput
{
public:
...
float m_forwardAxis = float(0.0f);
float m_strafeAxis = float(0.0f);
float m_viewYaw = float(0.0f);
uint8_t m_resetCount = uint8_t(0);
};
总的来说,与单人游戏输入的唯一区别是,我们有一个特殊的输入重置计数器来处理特殊的移动情况,但其他方面它们是相同的。
处理输入
一旦 input 到达服务器,它将使用 ProcessInput 中编写的 logic 来应用它。
图 35.2.在服务器上处理输入
- HandleSendClientInput 是远程过程的句柄。
- 没有创建输入,因为这是在服务器上进行的。ProcessInput 为在客户端上创建输入的每个相关多人游戏组件调用。
- 如果发现客户端和服务器结果不匹配,则使用来自 Local Prediction Player Input 组件的不可靠的远程过程将更正发送回客户端。
<RemoteProcedure Name="SendClientInputCorrection"
InvokeFrom="Authority" HandleOn="Autonomous" IsPublic="true"
IsReliable="false" GenerateEventBindings="false"
Description="Autonomous proxy correction RPC">
<Param Type="Multiplayer::ClientInputId" Name="inputId" />
<Param Type="AzNetworking::PacketEncodingBuffer"
Name="correction" />
</RemoteProcedure>
注意:上述工作由 Multiplayer gem 为我们完成。作为用户,我们实现 ProcessInput 并让多人游戏系统处理本地预测和更正。
为了进行比较,以下是处理玩家输入的单人游戏和多人游戏方法。
例 35.6.单人进程输入
void ChickenControllerComponent::ProcessInput(
const ChickenInput& input)
{
UpdateRotation(input);
UpdateVelocity(input);
Physics::CharacterRequestBus::Event(GetEntityId(),
&Physics::CharacterRequestBus::Events::AddVelocity,
m_velocity);
Physics::CharacterRequestBus::Event(GetEntityId(),
&Physics::CharacterRequestBus::Events::AddVelocity,
AZ::Vector3::CreateAxisZ(m_gravity));
}
例 35.7.多人游戏进程输入
void ChickenMovementComponentController::ProcessInput(
Multiplayer::NetworkInput& input,
[[maybe_unused]] float deltaTime)
{
auto chickenInput = input.FindComponentInput<
ChickenMovementComponentNetworkInput>();
if (chickenInput->m_resetCount !=
GetNetworkTransformComponentController()->GetResetCount())
{
return;
}
UpdateRotation(chickenInput);
UpdateVelocity(chickenInput);
GetNetworkCharacterComponentController()->
TryMoveWithVelocity(m_velocity, deltaTime);
}
UpdateRotation 和 UpdateVelocity 方法未更改。我们在每种方法的末尾使用不同的方法来应用 speed,但其他方面 logic 本质上是相同的。
注意:这就是 Reset Count 属性发挥作用的地方。这里的想法如下。如果移动计数被重置,则意味着我们不应该尝试根据具有旧计数的输入进行本地预测。
想象一下,我们的角色踩到了一个跳板,现在正在空中航行。我们不希望我们的本地预测犯了没有踏上发射台的错误。这是我们在本地预测中无法承受的一种错误。
if (chickenInput->m_resetCount != GetNetworkTransformComponentController()->GetResetCount()) { return; }
此 logic 将跳过太旧且不应重新应用的 input。
CreateInput 将输入收集到结构中,然后 ProcessInput 应用它。在后台,输入被存储并从客户端发送到服务器,在那里使用 Process Input 的相同逻辑处理输入。以这种方式,客户端和服务器都应该得到相同的结果。如果存在任何差异,服务器将向客户端发送更正,并使用 ProcessInput 重新应用输入。
Multiplayer Gem 如何通过生成的代码为我们处理玩家输入的本地预测,这几乎是神奇的。
什么是 Find Component Input?
你可能已经注意到,我们以通常的方式获取 input 数据类。它是通过使用 NetworkInput 的 FindComponentInput 方法获取的。
void ChickenMovementComponentController::ProcessInput(
Multiplayer::NetworkInput& input, float)
{
auto chickenInput = input.FindComponentInput<
ChickenMovementComponentNetworkInput>();
在幕后,任何具有具有网络输入的组件的网络实体都会获得一个关联的组件输入结构的 Multiplayer::NetworkInput 容器,每个此类多人游戏组件一个容器。
这些输入结构是根据需要根据每个多人游戏组件的 XML 定义生成的。
ChickenMovementComponentNetworkInput 可以在 ChickenMovementComponent.AutoComponent.h 中找到。
class ChickenMovementComponentNetworkInput
: public Multiplayer::IMultiplayerComponentInput
{
public:
bool Serialize(AzNetworking::ISerializer& serializer);
float m_forwardAxis = float(0.0f);
float m_strafeAxis = float(0.0f);
float m_viewYaw = float(0.0f);
uint8_t m_resetCount = uint8_t(0);
//...
};
图 35.3.网络输入
Network Binding 和 Local Prediction Player Input 组件协同工作,为每个具有输入的多人游戏组件创建网络输入结构。然后,这些结构将在 NetworkInput 中提供给您,您可以使用 FindComponentInput 查询这些结构。
小结
注意:本章随附的源代码和资源可以在 GitHub 上找到: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch35_multiplayer_input
该组件的其余部分执行与第 18 章 角色移动 中的 ChickenControllerComponent 完全相同的工作,即在 InputEventNotificationBus 上注册事件以记录按键和鼠标移动。
注意:您需要修改 Chicken 实体以删除旧的 Chicken Controller 组件,并将其替换为 Chicken Movement 组件。
您应该验证鸡是否确实以与在客户端上移动相同的方式随服务器移动。以下是启动服务器和客户端的命令。
MyProject.ServerLauncher.exe +host +loadlevel mylevel
MyProject.GameLauncher.exe +connect
例 35.8. ChickenMovementComponent.h
#pragma once
#include <Source/AutoGen/ChickenMovementComponent.AutoComponent.h>
#include <StartingPointInput/InputEventNotificationBus.h>
namespace MyGem
{
const StartingPointInput::InputEventNotificationId
MoveFwdEventId("move forward");
const StartingPointInput::InputEventNotificationId
MoveRightEventId("move right");
const StartingPointInput::InputEventNotificationId
RotateYawEventId("rotate yaw");
class ChickenMovementComponentController
:
public ChickenMovementComponentControllerBase
,
public StartingPointInput::
InputEventNotificationBus::MultiHandler
{
public:
ChickenMovementComponentController(
ChickenMovementComponent& parent);
void OnActivate(Multiplayer::EntityIsMigrating) override;
void OnDeactivate(Multiplayer::EntityIsMigrating) override;
//! Common input creation logic for the NetworkInput.
//! Fill out the input struct and the MultiplayerInputDriver
//! will send the input data over the network to ensure
//! it's processed.
//! @param input input structure which to store input data
//! for sending to the authority
//! @param deltaTime amount of time to integrate
//! the provided inputs over
void CreateInput(
Multiplayer::NetworkInput& input,
float deltaTime) override;
//! Common input processing logic for the NetworkInput.
//! @param input input structure to process
//! @param deltaTime amount of time to integrate the
//! provided inputs over
void ProcessInput(
Multiplayer::NetworkInput& input,
float deltaTime) override;
// AZ::InputEventNotificationBus interface
void OnPressed(float value) override;
void OnReleased(float value) override;
void OnHeld(float value) override;
protected:
void UpdateRotation(
const ChickenMovementComponentNetworkInput* input);
void UpdateVelocity(
const ChickenMovementComponentNetworkInput* input);
float m_forward = 0;
float m_strafe = 0;
float m_yaw = 0;
AZ::Vector3 m_velocity = AZ::Vector3::CreateZero();
};
} // namespace MyGem
例 35.9. ChickenMovementComponent.cpp
#include <AzCore/Component/Entity.h>
#include <AzCore/Component/TransformBus.h>
#include <Multiplayer/ChickenMovementComponent.h>
#include <Multiplayer/Components/NetworkCharacterComponent.h>
#include <Multiplayer/Components/NetworkTransformComponent.h>
namespace MyGem
{
using namespace StartingPointInput;
ChickenMovementComponentController::
ChickenMovementComponentController(ChickenMovementComponent& p)
: ChickenMovementComponentControllerBase(p) {}
void ChickenMovementComponentController::OnActivate(
Multiplayer::EntityIsMigrating)
{
InputEventNotificationBus::MultiHandler::BusConnect(
MoveFwdEventId);
InputEventNotificationBus::MultiHandler::BusConnect(
MoveRightEventId);
InputEventNotificationBus::MultiHandler::BusConnect(
RotateYawEventId);
}
void ChickenMovementComponentController::OnDeactivate(
Multiplayer::EntityIsMigrating)
{
InputEventNotificationBus::MultiHandler::BusDisconnect();
}
void ChickenMovementComponentController::CreateInput(
Multiplayer::NetworkInput& input,
[[maybe_unused]] float deltaTime)
{
auto chickenInput = input.FindComponentInput<
ChickenMovementComponentNetworkInput>();
chickenInput->m_forwardAxis = m_forward;
chickenInput->m_strafeAxis = m_strafe;
chickenInput->m_viewYaw = m_yaw;
chickenInput->m_resetCount =
GetNetworkTransformComponentController()->GetResetCount();
}
void ChickenMovementComponentController::ProcessInput(
Multiplayer::NetworkInput& input,
[[maybe_unused]] float deltaTime)
{
auto chickenInput = input.FindComponentInput<
ChickenMovementComponentNetworkInput>();
if (chickenInput->m_resetCount !=
GetNetworkTransformComponentController()->GetResetCount())
{
return;
}
UpdateRotation(chickenInput);
UpdateVelocity(chickenInput);
GetNetworkCharacterComponentController()->
TryMoveWithVelocity(m_velocity, deltaTime);
}
void ChickenMovementComponentController::OnPressed(float value)
{
const InputEventNotificationId* inputId =
InputEventNotificationBus::GetCurrentBusId();
if (inputId == nullptr)
{
return;
}
if (*inputId == MoveFwdEventId)
{
m_forward = value;
}
else if (*inputId == MoveRightEventId)
{
m_strafe = value;
}
else if (*inputId == RotateYawEventId)
{
}
}
m_yaw = value;
void ChickenMovementComponentController::OnHeld(float value)
{
const InputEventNotificationId* inputId =
InputEventNotificationBus::GetCurrentBusId();
if (inputId == nullptr)
{
return;
}
if (*inputId == RotateYawEventId)
{
m_yaw = value;
}
}
void ChickenMovementComponentController::OnReleased(float value)
{
const InputEventNotificationId* inputId =
InputEventNotificationBus::GetCurrentBusId();
if (inputId == nullptr)
{
return;
}
if (*inputId == MoveFwdEventId)
{
m_forward = value;
}
else if (*inputId == MoveRightEventId)
{
m_strafe = value;
}
else if (*inputId == RotateYawEventId)
{
m_yaw = value;
}
}
void ChickenMovementComponentController::UpdateRotation(
const ChickenMovementComponentNetworkInput* input)
{
AZ::TransformInterface* t = GetEntity()->GetTransform();
float currentHeading = t->GetWorldRotationQuaternion().
GetEulerRadians().GetZ();
currentHeading += input->m_viewYaw * GetTurnSpeed();
AZ::Quaternion q =
AZ::Quaternion::CreateRotationZ(currentHeading);
t->SetWorldRotationQuaternion(q);
}
void ChickenMovementComponentController::UpdateVelocity(
const ChickenMovementComponentNetworkInput* input)
{
const float currentHeading = GetEntity()->GetTransform()->
GetWorldRotationQuaternion().GetEulerRadians().GetZ();
const AZ::Vector3 fwd = AZ::Vector3::CreateAxisY(
input->m_forwardAxis);
const AZ::Vector3 strafe = AZ::Vector3::CreateAxisX(
input->m_strafeAxis);
AZ::Vector3 combined = fwd + strafe;
if (combined.GetLength() > 1.f)
{
combined.Normalize();
}
m_velocity = AZ::Quaternion::CreateRotationZ(currentHeading).
TransformVector(combined) * GetWalkSpeed() +
AZ::Vector3::CreateAxisZ(GetGravity());
}
} // namespace MyGem