本文为B站系列教学视频 《UE5_C++多人TPS完整教程》 —— 《P43 瞄准(Aiming)》 的学习笔记,该系列教学视频为计算机工程师、程序员、游戏开发者、作家(Engineer, Programmer, Game Developer, Author) Stephen Ulibarri 发布在 Udemy 上的课程 《Unreal Engine 5 C++ Multiplayer Shooter》 的中文字幕翻译版,UP主(也是译者)为 游戏引擎能吃么。
文章目录
- P43 瞄准(Aiming)
- 43.1 创建瞄准动作事件
- 43.2 创建站立、蹲伏瞄准动画蓝图
- 43.3 瞄准相关的变量复制及 RPC 函数
- 43.4 Summary
P43 瞄准(Aiming)
本节课我们将学习瞄准,这涉及瞄准动画姿势的改变。瞄准与枪战功能组件有关,我们将在枪战功能组件中实现瞄准,以适配多人游戏。
43.1 创建瞄准动作事件
-
在虚幻引擎打开 “项目设置”(Project Settings),在 “引擎”(Engine)下找到 “输入”(Input),添加 “动作映射”(Action Mappings)“
Aim
”,当我们单击 “鼠标右键”(Right Mouse Button) 时能使得人物角色进行瞄准。
-
我们想持续按住 “鼠标右键” 时进行瞄准,松开 “鼠标右键” 时停止瞄准。因此,在 “
BlasterCharacter.h
” 中声明动作映射 “Aim
” 的回调函数 “AimButtonPressed()
” 和 “AimButtonReleased()
”,接着在 “BlasterCharacter.cpp
” 的 “SetupPlayerInputComponent()
” 函数中绑定回调函数 “AimButtonPressed()
” 和 “AimButtonReleased()
”。/*** BlasterCharacter.h ***/...UCLASS() class BLASTER_API ABlasterCharacter : public ACharacter {GENERATED_BODY()...protected:// Called when the game starts or when spawnedvirtual void BeginPlay() override;// 与轴映射相对应的回调函数void MoveForward(float Value); // 人物角色前进或后退void MoveRight(float Value); // 人物角色左移或右移void Turn(float Value); // 人物角色视角左转或右转void LookUp(float Value); // 人物角色俯视或仰视// 与动作映射相对应的回调函数void EquipButtonPressed(); // 人物角色装备武器void CrouchButtonPressed(); // 人物角色蹲伏/* P43 瞄准(Aiming)*/void AimButtonPressed(); // 人物角色开始瞄准void AimButtonReleased(); // 人物角色停止瞄准/* P43 瞄准(Aiming)*/...};
/*** BlasterCharacter.cpp ***/...// Called to bind functionality to input void ABlasterCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) {Super::SetupPlayerInputComponent(PlayerInputComponent);// 绑定动作映射PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);PlayerInputComponent->BindAction("Equip", IE_Pressed, this, &ABlasterCharacter::EquipButtonPressed);PlayerInputComponent->BindAction("Crouch", IE_Pressed, this, &ABlasterCharacter::CrouchButtonPressed);/* P43 瞄准(Aiming)*/PlayerInputComponent->BindAction("Aim", IE_Pressed, this, &ABlasterCharacter::AimButtonPressed);PlayerInputComponent->BindAction("Aim", IE_Released, this, &ABlasterCharacter::AimButtonReleased);/* P43 瞄准(Aiming)*/// 绑定轴映射PlayerInputComponent->BindAxis("MoveForward", this, &ABlasterCharacter::MoveForward);PlayerInputComponent->BindAxis("MoveRight", this, &ABlasterCharacter::MoveRight);PlayerInputComponent->BindAxis("Turn", this, &ABlasterCharacter::Turn);PlayerInputComponent->BindAxis("LookUp", this, &ABlasterCharacter::LookUp); }.../* P43 瞄准(Aiming)*/ // 按住鼠标右键开始瞄准 void ABlasterCharacter::AimButtonPressed() {}// 松开鼠标右键停止瞄准 void ABlasterCharacter::AimButtonReleased() {} /* P43 瞄准(Aiming)*/
-
我们添加一些变量或函数来记录我们是否在瞄准。在 “
CombatComponent.h
” 中定义一个布尔变量 “bAiming
”,接着在 “BlasterCharacter.h
” 中创建一个返回值为布尔变量的函数 “IsAiming()
” 并在 “BlasterCharacter.cpp
” 中仿照函数 “IsWeaponEquipped()
” 完成 “IsAiming()
” 定义。在 “BlasterAnimInstance.h
” 中定义一个布尔变量 “bIsAiming
”,用以记录人物角色是否在瞄准;随后,在 “BlasterAnimInstance.cpp
” 的 “NativeUpdateAnimation()
” 函数中通过调用 “BlasterCharacter
” 的 “IsAiming()
” 函数更新 “bAiming
” 的值。/*** CombatComponent.h ***/...UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) class BLASTER_API UCombatComponent : public UActorComponent {GENERATED_BODY()...private:class ABlasterCharacter* Character; // 声明人物角色类,避免反复 casting 到 ABlasterCharacterUPROPERTY(Replicated)class AWeapon* EquippedWeapon; // 保存当前装备的武器/* P43 瞄准(Aiming)*/bool bAiming; // 是否在瞄准/* P43 瞄准(Aiming)*/ };
/*** BlasterCharacter.h ***/UCLASS() class BLASTER_API ABlasterCharacter : public ACharacter {GENERATED_BODY()...public: ...bool IsWeaponEquipped(); // 判断是否装备了武器/* P43 瞄准(Aiming)*/bool IsAiming(); // 判断是否在瞄准/* P43 瞄准(Aiming)*/ };
/*** BlasterCharacter.cpp ***/...bool ABlasterCharacter::IsWeaponEquipped() {return (Combat && Combat->EquippedWeapon); // 返回值为是否装备了武器 }/* P43 瞄准(Aiming)*/ bool ABlasterCharacter::IsAiming() {return (Combat && Combat->bAiming); // 返回值为是否装备了武器 } /* P43 瞄准(Aiming)*/
/*** BlasterAnimInstance.h ***/...UCLASS() class BLASTER_API UBlasterAnimInstance : public UAnimInstance {GENERATED_BODY()...private:...UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;bool bWeaponEquipped; // 是否装备了武器UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;bool bIsCrouched; // 是否在蹲伏/* P43 瞄准(Aiming)*/UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;bool bAiming;/* P43 瞄准(Aiming)*/ };
/*** BlasterAnimInstance.cpp ***/...void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime) // 原生(Native)类更新函数 NativeUpdateAnimation() 覆写,用于在每一帧调用以更新动画 {...bWeaponEquipped = BlasterCharacter->IsWeaponEquipped(); // 调用 BlasterCharacter 的 IsWeaponEquipped() 函数判断人物角色是否装备了武器bIsCrouched = BlasterCharacter->bIsCrouched; // 访问 BlasterCharacter 的 bIsCrouched 变量值来判断人物角色是否在蹲伏/* P43 瞄准(Aiming)*/bAiming = BlasterCharacter->IsAiming(); // 调用 BlasterCharacter 的 IsAiming() 函数来判断人物角色是否在瞄准/* P43 瞄准(Aiming)*/ }...
-
回到 “
BlasterCharacter.cpp
” 中对动作映射 “Aim
” 的回调函数 “AimButtonPressed()
” 和 “AimButtonReleased()
” 进行简单的定义,随后进行编译。/*** BlasterCharacter.cpp ***/.../* P43 瞄准(Aiming)*/ // 按住鼠标右键进行瞄准 void ABlasterCharacter::AimButtonPressed() {if (Combat) {Combat->bAiming = true;} }// 松开鼠标右键取消瞄准 void ABlasterCharacter::AimButtonReleased() {if (Combat) {Combat->bAiming = false;} } /* P43 瞄准(Aiming)*/...
43.2 创建站立、蹲伏瞄准动画蓝图
-
在虚幻引擎中打开动画蓝图 “
BlasterAnimBP
”,然后在 “AnimGraph
” 面板中打开状态机 “Equipped
” 的 “Idle
” 状态编辑界面,在右侧内容浏览器中将动画资产 “Idle_Ironsights
” 拖拽至面板中生成 “序列播放器 Idle_Rifle_Ironsights”(Play Idle_Rifle_Ironsights) 蓝图节点;接着,新增节点 “按布尔混合姿势”(Blend Poses by bool)以及 “获取 Aiming”(Get Aiming),并将生成的蓝图变量 “Aiming
” 连接到 “按布尔混合姿势” 节点的 “Active Value
” 引脚;然后,将 “序列播放器 Idle_Rifle_Ironsights” 节点的输出引脚连接到 “按布尔混合姿势” 节点的 “真 姿势”(True Pose)引脚,将原先的 “序列播放器 Idle_Rifle_Hip” 节点的输出引脚连接到 “按布尔混合姿势” 节点的 “False 姿势”(False Pose)引脚,最后将 “按布尔混合姿势” 的输出引脚连接到 “输出动画姿势”(Output Animation Pose) 节点的 “Result
” 输入引脚上。这段蓝图表示如果 “Aiming
” 的值为 “true
” 则输出人物角色站立瞄准姿势,否则输出站立待机姿势。
-
在 “
AnimGraph
” 面板中打开状态机 “Equipped
” 的 “CrouchIdle
” 状态编辑界面,在右侧内容浏览器中将动画资产 “Crouch_Idle_Rifle_Ironsights
” 拖拽至面板中生成 “序列播放器 Crouch_Idle_Rifle_Ironsights”(Play Crouch_Idle_Rifle_Ironsights) 蓝图节点,然后仿照 步骤 1 绘制如下图所示的蓝图。这段蓝图表示如果 “Aiming
” 的值为 “true
” 则输出人物角色蹲伏瞄准姿势,否则输出蹲伏待机姿势。
-
编译、保存后进行测试。我们操控一个客户端上的人物角色进行瞄准时,在本地可以看到站立和蹲伏的瞄准动画姿势,但是在另一个客户端和服务器上均看不到本地的人物角色在瞄准,这说明我们需要对瞄准动作事件中的变量进行复制,创建相应的 RPC 函数,以实现瞄准动画姿势的网络同步。
43.3 瞄准相关的变量复制及 RPC 函数
-
在 “
CombatComponent.h
” 中将 “bAiming
” 重新定义为可复制的变量,在 “CombatComponent.cpp
” 的 “GetLifetimeReplicatedProps
” 函数中添加 “DOREPLIFETIME()
” 宏调用,以在派生的 “UCombatComponent
” 实例的生命周期内复制 “bAming
”。/*** CombatComponent.h ***/...UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) class BLASTER_API UCombatComponent : public UActorComponent {GENERATED_BODY()...private:class ABlasterCharacter* Character; // 声明人物角色类,避免反复 casting 到 ABlasterCharacterUPROPERTY(Replicated)class AWeapon* EquippedWeapon; // 保存当前装备的武器/* P43 瞄准(Aiming)*/UPROPERTY(Replicated) // 可复制变量bool bAiming; // 是否在瞄准/* P43 瞄准(Aiming)*/ };
/*** CombatComponent.cpp ***/...// 重写复制属性函数 void UCombatComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const {// 调用SuperSuper::GetLifetimeReplicatedProps(OutLifetimeProps);// 添加要为派生的类 UCombatComponent 复制的属性,需要添加头文件 "Net/UnrealNetwork.h"// DOREPLIFETIME 宏用于指定 UCombatComponent 哪些属性需要被复制,以及复制的条件。DOREPLIFETIME(UCombatComponent, EquippedWeapon);/* P43 瞄准(Aiming)*/DOREPLIFETIME(UCombatComponent, bAiming);/* P43 瞄准(Aiming)*/ }
-
编译后进行测试,当我们操控服务器上的人物角色进行站立和蹲伏的瞄准时,所有的客户端都可以看到服务器上的人物角色在进行瞄准;但当我们在客户端上进行瞄准时,只有本地的客户端自己可见,另一个客户端和服务器上均不可见。这再次说明变量复制是单向的,只能从服务器到客户端,因此需要实现从客户端到服务器 RPC 函数。
-
在 “
CombatComponent.h
” 中声明一个可以设置 “bAiming
” 的值的函数 “SetAiming()
”,接着使用带有 “Server
” 和 “Reliable
” 关键字的 “UFUNCTION()
” 宏声明服务器可靠 RPC 函数 “ServerSetAiming()
”。接着,在 “CombatComponent.cpp
” 中完成 “SetAiming()
” 和 “ServerSetAiming()
” 的定义,并在 “ServerSetAiming()
” 函数名后添加 “_Implementation
” 使之成为 “实施函数”。随后,修改 “BlasterCharacter.cpp
” 中动作映射 “Aim
” 的回调函数 “AimButtonPressed()
” 和 “AimButtonReleased()
” 的定义,以调用 “SetAiming()
” 函数。/*** CombatComponent.h ***/...UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) class BLASTER_API UCombatComponent : public UActorComponent {GENERATED_BODY()...protected:// Called when the game startsvirtual void BeginPlay() override;/* P43 瞄准(Aiming)*/void SetAiming(bool bIsAiming);UFUNCTION(Server, Reliable)void ServerSetAiming(bool bIsAiming);/* P43 瞄准(Aiming)*/private:class ABlasterCharacter* Character; // 声明人物角色类,避免反复 casting 到 ABlasterCharacterUPROPERTY(Replicated)class AWeapon* EquippedWeapon; // 保存当前装备的武器/* P43 瞄准(Aiming)*/UPROPERTY(Replicated) // 可复制变量bool bAiming; // 是否在瞄准/* P43 瞄准(Aiming)*/ };
/*** CombatComponent.cpp ***/.../* P43 瞄准(Aiming)*/ // 设置 bAiming void UCombatComponent::SetAiming(bool bIsAiming) {bAiming = bIsAiming;// 由虚幻引擎官方文档 “从服务器调用的 RPC” 的表格 “Server” 这一列和 “从客户端调用的 RPC” 的表格第一行 “Server” 列可知,// 以 Server 关键字声明的 RPC 函数无论在客户端还是服务器上调用都是在服务器上执行,因此无需对当前机器是客户端还是服务器端进行判断。//if (!Character->HasAuthority()) {// ServerSetAiming(bIsAiming);//}ServerSetAiming(bIsAiming); }// 服务器可靠 RPC 函数,bAiming 会被服务器复制到所有客户端 void UCombatComponent::ServerSetAiming_Implementation(bool bIsAiming) {bAiming = bIsAiming; // 由于函数在服务器上执行且 bAiming 可复制,服务器会将 bAiming 值的更新复制到所有客户端 } /* P43 瞄准(Aiming)*/
/*** BlasterCharacter.cpp ***/ /* P43 瞄准(Aiming)*/ // 按住鼠标右键进行瞄准 void ABlasterCharacter::AimButtonPressed() {if (Combat) {/*Combat->bAiming = true;*/Combat->SetAiming(true);} }// 松开鼠标右键取消瞄准 void ABlasterCharacter::AimButtonReleased() {if (Combat) {/*Combat->bAiming = false;*/Combat->SetAiming(false);} } /* P43 瞄准(Aiming)*/
要求和注意事项
您必须满足一些要求才能充分发挥 RPC 的作用:- 它们必须从 Actor 上调用。
- Actor 必须被复制。
- 如果 RPC 是从服务器调用并在客户端上执行,则只有实际拥有这个 Actor 的客户端才会执行函数。
- 如果 RPC 是从客户端调用并在服务器上执行,客户端就必须拥有调用 RPC 的 Actor。
多播 RPC 则是个例外:
- 如果它们是从服务器调用,服务器将在本地和所有已连接的客户端上执行它们。
- 如果它们是从客户端调用,则只在本地而非服务器上执行。
现在,我们有了一个简单的多播事件限制机制:在特定 Actor 的网络更新期内,多播函数将不会复制两次以上。按长期计划,我们会对此进行改善,同时更好的支持跨通道流量管理与限制。
下面的表格根据执行调用的 actor 的所有权(最左边的一列),总结了特定类型的 RPC 将在哪里执行。
从服务器调用的 RPC
Actor 所有权 未复制 NetMulticast Server Client Client-owned actor 在服务器上运行 在服务器和所有客户端上运行 在服务器上运行 在 actor 的所属客户端上运行 Server-owned actor 在服务器上运行 在服务器和所有客户端上运行 在服务器上运行 在服务器上运行 Unowned actor 在服务器上运行 在服务器和所有客户端上运行 在服务器上运行 在服务器上运行 从客户端调用的 RPC
Actor 所有权 未复制 NetMulticast Server Client Owned by invoking client 在执行调用的客户端上运行 在执行调用的客户端上运行 在服务器上运行 在执行调用的客户端上运行 Owned by a different client 在执行调用的客户端上运行 在执行调用的客户端上运行 丢弃 在执行调用的客户端上运行 Server-owned actor 在执行调用的客户端上运行 在执行调用的客户端上运行 丢弃 在执行调用的客户端上运行 Unowned actor 在执行调用的客户端上运行 在执行调用的客户端上运行 丢弃 在执行调用的客户端上运行
—— 虚幻引擎官方文档《虚幻引擎中的 RPC》
-
编译后进行测试,当我们操控一个客户端上的人物角色进行瞄准时,在本地客户端可以看到站立和蹲伏的瞄准动画姿势,在另一个客户端和服务器上也能都能看到。
43.4 Summary
本节课我们实现了人物角色的瞄准功能及其网络同步机制。首先,在项目设置中添加 “Aim
” 动作映射,绑定为鼠标右键,然后在人物角色类 “ABlasterCharacter
” 中声明并实现了该动作映射下按住与释放鼠标右键的回调函数。接着,在枪战组件 “UCombatComponent
”中创建了布尔变量 “bAiming
” 表示人物角色的瞄准状态,并通过人物角色类 “ABlasterCharacter
” 的 “IsAiming()
” 方法供动画实例 “UBlasterAnimInstance
” 进行访问,同时 “UBlasterAnimInstance
” 中新增 “bIsAiming
” 变量,在每帧更新时同步人物角色瞄准状态。然后,在动画蓝图中重构了持枪状态机的动画逻辑,即人物角色在站立与蹲伏状态下,根据“bIsAiming
” 变量切换待机姿势与瞄准姿势。
在进行测试时,我们发现本地操控人物角色进行瞄准时可以看到对应的动画姿势,但其他机器上却无法看到,因而需要对瞄准相关的变量进行网络同步。首先,将枪战组件的 “bAiming
” 注册为复制属性,这样服务器上人物角色的瞄准状态能复制、更新到所有客户端,当服务器上的人物角色进行瞄准时,所有客户端也能看到;随后,创建服务器可靠 的 RPC 函数 “ServerSetAiming()
”,当客户端操控人物角色进行瞄准时通过“SetAiming()
” 方法调用该 RPC 函数,并在服务器执行,保证将客户端上人物角色的瞄准状态更新到其他客户端。最终,测试结果验证了所有客户端都能正确显示本地及其他玩家的瞄准状态,实现多人游戏下瞄准状态及动画姿势的网络同步。