本文为B站系列教学视频 《UE5_C++多人TPS完整教程》 —— 《P41 装备(武器)姿势(Equipped Pose)》 的学习笔记,该系列教学视频为计算机工程师、程序员、游戏开发者、作家(Engineer, Programmer, Game Developer, Author) Stephen Ulibarri 发布在 Udemy 上的课程 《Unreal Engine 5 C++ Multiplayer Shooter》 的中文字幕翻译版,UP主(也是译者)为 游戏引擎能吃么。
本节课我们将在装备武器时更改动画姿势,我们将学习动画蓝图在多人游戏中的运用。
在人物角色动画实例类头文件 “BlasterAnimInstance.h
” 中新建一个布尔变量 “bWeaponEquipped
”,用以记录人物角色是否装备武器。接着,在 “BlasterCharacter.h
” 和 “BlasterCharacter.cpp
” 中声明和定义函数 “IsWeaponEquipped()
”,通过枪战功能组件类 “UCombatComponent
” 的 “EquippedWeapon
” 变量来改变 “bWeaponEquipped
” 这个布尔值。然后,在 “BlasterAnimInstance.cpp
” 的 “NativeUpdateAnimation()
” 函数中通过调用 “BlasterCharacter
” 的 “IsWeaponEquipped()
” 函数更新 “bWeaponEquipped
” 的值。
/*** BlasterAnimInstance.h ***/
...
UCLASS()
class BLASTER_API UBlasterAnimInstance : public UAnimInstance
{
GENERATED_BODY()
...
private:
// UPROPERTY:https://dev.epicgames.com/documentation/zh-cn/unreal-engine/unreal-engine-uproperties?application_version=5.4
UPROPERTY(BlueprintReadOnly, Category = Character, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Character”;
// 元数据说明符:由于变量为私有变量,需要设置 AllowPrivateAccess 为 true 才能在蓝图中可读
class ABlasterCharacter* BlasterCharacter; // 使用动画实例的角色类
UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;
float Speed; // 运动速度
UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;
bool bIsInAir; // 是否在空中
UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;
bool bIsAccelerating; // 是否在加速
/* P41 装备(武器)姿势(Equipped Pose)*/
UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 “Movemonet”;
bool bWeaponEquipped; // 是否装备了武器
/* P41 装备(武器)姿势(Equipped Pose)*/
};
...
/*** BlasterCharacter.h ***/
...
UCLASS()
class BLASTER_API ABlasterCharacter : public ACharacter
{
GENERATED_BODY()
...
public:
// 设置重叠的武器实例,以便在武器类 Weapon 的 OnSphereOverlap() 函数中调用
// FORCEINLINE void SetOverlappingWeapon(AWeapon* Weapon) { OverlappingWeapon = Weapon; }
void SetOverlappingWeapon(AWeapon* Weapon);
/* forceinline 是编程中用于强制内联函数的关键字或注解,主要用于减少函数调用开销,但需谨慎使用以避免代码膨胀或性能下降。
/* P41 装备(武器)姿势(Equipped Pose)*/
bool IsWeaponEquipped(); // 判断是否装备了武器
/* P41 装备(武器)姿势(Equipped Pose)*/
};
/*** BlasterCharacter.cpp ***/
...
/* P41 装备(武器)姿势(Equipped Pose)*/
bool ABlasterCharacter::IsWeaponEquipped()
{
return (Combat && Combat->EquippedWeapon); // 返回值为是否装备了武器
}
/* P41 装备(武器)姿势(Equipped Pose)*/
...
/*** BlasterAnimInstance.cpp ***/
...
void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime) // 原生(Native)类更新函数 NativeUpdateAnimation() 覆写,用于在每一帧调用以更新动画
{
Super::NativeUpdateAnimation(DeltaTime); // 调用父类 AnimInstance 的 NativeUpdateAnimation() 函数
if (BlasterCharacter == nullptr) { // 检查 BlasterCharacter 是否声明
BlasterCharacter = Cast<ABlasterCharacter>(TryGetPawnOwner()); // 获取动画蓝图实例所属,并向下强制转换(Cast)为 ABlasterCharacter 类
}
if (BlasterCharacter == nullptr) return;
FVector Velocity = BlasterCharacter->GetVelocity(); // 获取人物角色速度向量
Velocity.Z = 0.f; // 不关心 Z 轴速度,设置为 0
Speed = Velocity.Size(); // 获取人物角色速度向量的模(大小),少了这一行代码在人物角色从怠速到走路到跑步的动画将无法实现转化
bIsInAir = BlasterCharacter->GetCharacterMovement()->IsFalling(); // 调用 GetCharacterMovement()->IsFalling() 函数判断人物角色是否掉落从而判断人物角色是否在空中,
// 需要添加头文件 "GameFramework/CharacterMovementComponent.h"
bIsAccelerating = BlasterCharacter->GetCharacterMovement()->GetCurrentAcceleration().Size() > 0 ? true : false; // 调用 GetCharacterMovement()->GetCurrentAcceleration() 获取人物角色加速度
// 判断加速度是否的维度是否大于 0 ,大于 0 说明某个方向上有加速度
/* P41 装备(武器)姿势(Equipped Pose)*/
bWeaponEquipped = BlasterCharacter->IsWeaponEquipped(); // 调用 IsWeaponEquipped() 函数判断人物角色是否装备了武器
/* P41 装备(武器)姿势(Equipped Pose)*/
}
...
现在需要解决的问题是我们的动画实例与变量复制没有太大的关系:所有机器上的每个人物角色都存在动画实例和动画蓝图,并且动画实例只能访问该机器上的变量。当我们操控的当前人物角色装备武器时,人物角色的动画姿势将发生改变,此时就需要一个方式来让其他玩家(如对手)实例来知道我们的人物角色是否装备了武器,以便在他们的机器上可以看到我们的人物角色处于正确的装备了武器的动画姿势。
通过查看我们在 “BlasterCharacter
” 中定义的装备武器动作映射的回调函数 “EquipButtonPressed()
” 以及上节课创建的 RPC 函数 “ServerEquipButtonPressed()
” 的定义可知,我们只在服务器上设置了要装备的武器,枪战功能组件类 “UCombatComponent
” 的 “EquippedWeapon
” 变量并没有很好地被复制。因此,我们只要将 “EquippedWeapon
” 重新定义为可复制的变量,重写 “GetLifetimeReplicatedProps
” 函数、注册 “EquippedWeapon
” 属性,然后编译即可。
/*** CombatComponent.h ***/
...
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class BLASTER_API UCombatComponent : public UActorComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
UCombatComponent();
friend class ABlasterCharacter; // 将人物角色类 ABlasterCharcter 设置为枪战功能组件类 UCombatComponent 的友元类
// void EquipWeapon(AWeapon* WeaponToEquip);
void EquipWeapon(class AWeapon* WeaponToEquip); // 指定要装备的武器
// Called every frame
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
/* P41 装备(武器)姿势(Equipped Pose)*/
// 重写复制属性函数
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
/* P41 装备(武器)姿势(Equipped Pose)*/
protected:
// Called when the game starts
virtual void BeginPlay() override;
private:
class ABlasterCharacter* Character; // 声明人物角色类,避免反复 casting 到 ABlasterCharacter
/* P41 装备(武器)姿势(Equipped Pose)*/
UPROPERTY(Replicated)
class AWeapon* EquippedWeapon; // 保存当前装备的武器
/* P41 装备(武器)姿势(Equipped Pose)*/
};
/*** CombatComponent.cpp ***/
...
// Fill out your copyright notice in the Description page of Project Settings.
#include "CombatComponent.h" // 原来自动生成的代码是 #include "BlasterComponents/CombatComponent.h",这里需要把 "BlasterComponents/" 去掉,否则找不到文件 "CombatComponent.h"
#include "Blaster/Weapon/Weapon.h"
#include "Blaster/Character/BlasterCharacter.h"
#include "Engine/SkeletalMeshSocket.h"
/* P41 装备(武器)姿势(Equipped Pose)*/
#include
/* P41 装备(武器)姿势(Equipped Pose)*/
...
/* P41 装备(武器)姿势(Equipped Pose)*/
// 重写复制属性函数
void UCombatComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
// 调用Super
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 添加要为派生的类 AUCombatComponent 复制的属性,需要添加头文件 "Net/UnrealNetwork.h"
// DOREPLIFETIME 宏用于指定 UCombatComponent 哪些属性需要被复制,以及复制的条件。
DOREPLIFETIME(UCombatComponent, EquippedWeapon);
}
/* P41 装备(武器)姿势(Equipped Pose)*/
BlasterAnimBP
”,然后在 “AnimGraph
” 面板中修改蓝图,断开原先蓝图节点间的引脚连接,新增节点 “按布尔混合姿势”(Blend Poses by bool),添加变量 “Weapon Equipped
”,并连接到 “按布尔混合姿势” 节点的 “Active Value
” 引脚;接着新增状态机()节点,重命名为 “Equipped
”,将 “Equipped
” 节点的输出引脚连接到 “按布尔混合姿势” 节点的 “真 姿势”(True Pose)引脚,将原先的 “Unequipped
” 节点的输出引脚连接到 “按布尔混合姿势” 节点的 “False 姿势”(False Pose)引脚,最后将 “按布尔混合姿势” 的输出引脚连接到 “OutputPose
” 节点的 “Result
” 输入引脚。Weapon Equipped
” 的布尔值来判断输出的动画姿势,如果 “Weapon Equipped
” 为真,则最终的输出姿势为 “Equipped
” 状态机中的动画姿势,即人物角色已装备武器的动画姿势,如果 “Weapon Equipped
” 为假,则最终的输出姿势为 “Unequipped
” 状态机中的动画姿势(我们在《UE5_C++多人TPS完整教程》学习笔记32 ——《P33 动画蓝图(Animation Blueprint)》 中已为 “Unequipped
” 状态机构建了状态节点转换蓝图),即人物角色未装备武器的动画姿势。Equipped
” 状态机节点,进入状态机编辑界面,在右侧内容浏览器中将动画资产 “Idle_Rifle_Hip
” 拖拽至面板中,将生成的节点重命名为 “Idle
”,并与 “Entry
” 节点进行连接。本节课实现人物角色装备武器时的动画状态切换,通过在动画实例 C++ 类上进行网络变量复制以及动画蓝图的协同工作,确保了多人游戏中角色姿势的准确同步。首先,我们在动画实例类 “BlasterAnimInstance
” 中添加了标识武器装备状态的布尔变量 “bWeaponEquipped
”,并在角色类 “BlasterCharacter
”中 创建了 “IsWeaponEquipped()
” 方法,通过检查枪战功能组件的 “EquippedWeapon
” 变量来动态更新装备状态。为保障多人游戏中的动画同步,我们将枪战功能组件的 “EquippedWeapon
” 变量设为复制属性,重写 “GetLifetimeReplicatedProps
” 函数实现变量注册,确保所有客户端都能准确获取武器装备状态。接着,在动画蓝图 “BlasterAnimBP
” 中我们重构了输出姿势的逻辑,通过引入“按布尔混合姿势” 节点,实现以蓝图变量 “Weapon Equipped
” 的布尔值作为 “Active Value
” 来控制最终的输出姿势,当 “Weapon Equipped
” 的值为真时输出的 “Equipped
” 状态机对应的动画姿势(在状态机编辑面板中添加、连接由持枪动画 “Idle_Rifle_Hip
” 生成的节点),为假时则维持原有的 “Unequipped
” 状态机对于的动画姿势。最终测试的测试结果验证了无论我们操控客户端还是服务器上的人物角色去装备武器,在其他机器上都能同步看到本地人物角色的动画姿势。