本文为B站系列教学视频 《UE5_C++多人TPS完整教程》 ------ 《P50 应用瞄准偏移(Aim Offset)》 的学习笔记,该系列教学视频为计算机工程师、程序员、游戏开发者、作家(Engineer, Programmer, Game Developer, Author)Stephen Ulibarri 发布在 Udemy 上的课程 《Unreal Engine 5 C++ Multiplayer Shooter》 的中文字幕翻译版,UP主(也是译者)为 游戏引擎能吃么。
文章目录
- [P50 应用瞄准偏移(Aim Offset)](#P50 应用瞄准偏移(Aim Offset))
- [50.1 在蓝图中应用瞄准偏移](#50.1 在蓝图中应用瞄准偏移)
- [50.2 使用 C++ 代码驱动瞄准偏移](#50.2 使用 C++ 代码驱动瞄准偏移)
- [50.3 Summary](#50.3 Summary)
P50 应用瞄准偏移(Aim Offset)
本节课我们将对创建好的瞄准偏移进行应用,这包括在动画蓝图中使用相关变量去驱动瞄准偏移。
50.1 在蓝图中应用瞄准偏移
-
在 "
BlasterAnimBP
" 的 "AnimGraph
" 蓝图面板,添加蓝图节点 "新保存的缓存姿势 "(New Save cached pose),将其重命名为 "Equipped
",并与原先的 "Equipped
" 状态机节点相连,这样我们就可以在蓝图的多个位置通过添加蓝图界定啊 "使用缓存姿势"Equipped" (Use cached pose 'Equipped')" 节点来使用 "Equipped
" 状态机节点。 -
每个骨骼的分层混合 (Layered blend per bone)
-
在骨骼 "
SK_EpicCharacter_Skeleton
" 中,"spine_01
" 是分开上下半身的骨骼节点(spine_01 is what separates the lower half from the upper half of the body),因此我们选择这个骨骼节点在 "每个骨骼的分层混合 " 蓝图节点中进行混合。
-
选择 "每个骨骼的分层混合 " 蓝图节点,在右侧细节面板中展开 "层设置 "(Layer Setup),接着展开 "索引 [0] "(Index [0]),然后再展开 "分支过滤器 "(Branch Filters)、"索引 [0] "(Index [0]),设置 "骨骼名称 "(Bone Name)为 "
spine_01
"。
-
在右侧资产浏览器中将 "
HipAimOffset
" 移动至面板中生成蓝图节点,并绘制下图中的蓝图。
50.2 使用 C++ 代码驱动瞄准偏移
-
蓝图节点 "
HipAimOffset
" 需要与 "Yaw
" 和 "Pitch
" 相关的变量进行驱动。打开 Visual Studio,在 "BlasterAinmInstance.h
" 中声明变量 "AO_Yaw
"(记录人物角色瞄准偏移偏航角)和 "AO_Pitch
"(记录瞄准偏移俯仰角);在 "BlasterCharacter.h
" 中同样声明 "AO_Yaw
" 和 "AO_Pitch
",并声明人物角色瞄准起始旋转 "人物角色的起始瞄准旋转
" 以及瞄准偏移函数 "AimOffset()
"。cpp/*** BlasterAinmInstance.h ***/ ... UCLASS() class BLASTER_API UBlasterAnimInstance : public UAnimInstance { GENERATED_BODY() ... private: ... UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 "Movemonet"; bool bAiming; // 是否在瞄准 UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 "Movemonet"; float YawOffset; // 人物角色水平方向偏航角 UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 "Movemonet"; float Leaning; // 人物角色身体倾斜度 FRotator CharacterRotationLastFrame; // 人物角色上一帧的旋转 FRotator CharacterRotation; // 人物角色当前的旋转 FRotator DeltaRotation; // 旋转体插值的当前值 /* P50 应用瞄准偏移(Applying Aim Offset)*/ UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 "Movemonet"; float AO_Yaw; // 瞄准偏移偏航角 UPROPERTY(BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) // 属性说明符:仅在蓝图中可读,类别为 "Movemonet"; float AO_Pitch; // 瞄准偏移俯仰角 /* P50 应用瞄准偏移(Applying Aim Offset)*/ };
cpp/*** BlasterCharacter.h ***/ ... UCLASS() class BLASTER_API ABlasterCharacter : public ACharacter { GENERATED_BODY() ... protected: // Called when the game starts or when spawned virtual void BeginPlay() override; // 与轴映射相对应的回调函数 void MoveForward(float Value); // 人物角色前进或后退 void MoveRight(float Value); // 人物角色左移或右移 void Turn(float Value); // 人物角色视角左转或右转 void LookUp(float Value); // 人物角色俯视或仰视 // 与动作映射相对应的回调函数 void EquipButtonPressed(); // 人物角色装备武器 void CrouchButtonPressed(); // 人物角色蹲伏 void AimButtonPressed(); // 人物角色开始瞄准 void AimButtonReleased(); // 人物角色停止瞄准 /* P50 应用瞄准偏移(Applying Aim Offset)*/ void AimOffset(float DeltaTime); /* P50 应用瞄准偏移(Applying Aim Offset)*/ ... private: ... UFUNCTION() void OnRep_OverlappingWeapon(AWeapon* LastWeapon); // OverlappingWeapon 的 Repnotify 函数 UPROPERTY(VisibleAnyWhere) class UCombatComponent* Combat; // 添加枪战功能组件类 UFUNCTION(Server, Reliable) void ServerEquipButtonPressed(); // 人物角色装备武器的 RPC 函数 /* P50 应用瞄准偏移(Applying Aim Offset)*/ float AO_Yaw; // 人物角色瞄准偏移偏航角 float AO_Pitch; // 人物角色瞄准偏移俯仰角 FRotator StartingAimRotation; // 人物角色的起始瞄准旋转 /* P50 应用瞄准偏移(Applying Aim Offset)*/ ... };
-
在 "
BlasterCharacter.cpp
" 中完成 "AimOffset()
" 的定义,获取人物角色瞄准偏航角 "AO_Yaw
" 及俯仰角 "AO_Pitch
",并且使得人物角色在静止站立且不跳跃时,人物角色的正前方朝向不跟随跟随我们的控制器(鼠标)改变;而在奔跑和跳跃时,人物角色的正前方朝向跟随我们的控制器进行改变,始终为控制器的当前朝向。接下来,不要忘记在 "Tick()
" 函数中调用 "AimOffset()
"。cpp/*** BlasterCharacter.cpp ***/ ... /* P50 应用瞄准偏移(Applying Aim Offset)*/ #include "Kismet/KismetMathLibrary.h" /* P50 应用瞄准偏移(Applying Aim Offset)*/ ... /* P50 应用瞄准偏移(Applying Aim Offset)*/ // 瞄准偏移 void ABlasterCharacter::AimOffset(float DeltaTime) { if (Combat && Combat->EquippedWeapon == nullptr) return; FVector Velocity = GetVelocity(); // 获取人物角色速度向量 Velocity.Z = 0.f; // 不关心 Z 轴速度,设置为 0 float Speed = Velocity.Size(); // 获取人物角色速度向量的模(大小) bool bIsInAir = GetCharacterMovement()->IsFalling(); // 判断人物角色是否掉落从而判断人物角色是否在空中 if (Speed == 0.f && !bIsInAir) { // 当人物角色静止站立且不跳跃时 FRotator CurrentAimRotation = FRotator(0.f, GetBaseAimRotation().Yaw, 0.f); // 获取人物角色当前瞄准旋转 FRotator DeltaAimRotation = UKismetMathLibrary::NormalizedDeltaRotator(CurrentAimRotation, StartingAimRotation); // 标准化获取 CurrentAimRotation 和 StartingAimRotation 的差量 AO_Yaw = DeltaAimRotation.Yaw; // 获取人物角色瞄准偏航角 bUseControllerRotationYaw = false; // 禁用控制器旋转偏航 } if (Speed > 0.f || bIsInAir) { // 当奔跑或跳跃时 StartingAimRotation = FRotator(0.f, GetBaseAimRotation().Yaw, 0.f); // 改变奔跑或跳跃状态转换为静止站立状态时的起始瞄准旋转 AO_Yaw = 0.f; // 由于启用了控制器旋转偏航,人物角色朝向始终面向控制器当前朝向,因此设置 AO_Yaw 为 0 bUseControllerRotationYaw = true; // 启用控制器旋转偏航 } } /* P50 应用瞄准偏移(Applying Aim Offset)*/ ... // Called every frame void ABlasterCharacter::Tick(float DeltaTime) { Super::Tick(DeltaTime); // // 如果有与人物角色重叠的武器,则在每个 tick 都显示拾取组件 // if (OverlappingWeapon) OverlappingWeapon->ShowPickupWidget(true); /* P50 应用瞄准偏移(Applying Aim Offset)*/ AimOffset(DeltaTime); /* P50 应用瞄准偏移(Applying Aim Offset)*/ } ...
-
在 "
BlasterCharacter.h
" 定义内联函数 "GetAO_Yaw()
" 和 "GetAO_Pitch()
",以便在 "BlasterAinmInstance.cpp
" 的 函数 "NativeUpdateAnimation()
" 中进行调用,获取 "AO_Yaw
" 和 "AO_Pitch
" 的值。cpp/*** BlasterCharacter.h ***/ ... UCLASS() class BLASTER_API ABlasterCharacter : public ACharacter { GENERATED_BODY() ... public: ... bool IsWeaponEquipped(); // 判断是否装备了武器 bool IsAiming(); // 判断是否在瞄准 /* P50 应用瞄准偏移(Applying Aim Offset)*/ FORCEINLINE float GetAO_Yaw() const { return AO_Yaw; } FORCEINLINE float GetAO_Pitch() const { return AO_Pitch; } /* P50 应用瞄准偏移(Applying Aim Offset)*/ };
cpp/*** BlasterAinmInstance.cpp ***/ ... // 原生(Native)类更新函数 NativeUpdateAnimation() 覆写,用于在每一帧调用以更新动画 void UBlasterAnimInstance::NativeUpdateAnimation(float DeltaTime) { ... // 获取人物角色的倾斜度 CharacterRotationLastFrame = CharacterRotation; // 保存人物角色上一帧的旋转 CharacterRotation = BlasterCharacter->GetActorRotation(); // 获取当前人物角色的旋转 const FRotator Delta = UKismetMathLibrary::NormalizedDeltaRotator(CharacterRotation, CharacterRotationLastFrame); // 标准化获取人物角色当前旋转与上一帧旋转的差量 const float Target = Delta.Yaw / DeltaTime; // Delta 的 Yaw 值可能会很小,需要除以 DeltaTime(每一帧的时间)进行缩放,获取目标 Yaw 值 const float Interp = FMath::FInterpTo(Leaning, Target, DeltaTime, 6.f); // FMath::FInterpTo() 实现 Lean 到 Target 插值(Interporlation),保证平滑过渡,这里最后一个参数 6.f 是插值速度 Leaning = FMath::Clamp(Interp, -90.f, 90.f); // FMath::Clamp() 限制数值范围,如果 Interp 的值超过设定的最小值 -90.0 或最大值 90.0,就将 Lean 赋值为设定的最小值或最大值;否则直接将 Interp 赋值给 Lean。 /* P50 应用瞄准偏移(Applying Aim Offset)*/ AO_Yaw = BlasterCharacter->GetAO_Yaw(); // 从 BlasterCharacter 获取 AO_Yaw AO_Pitch = BlasterCharacter->GetAO_Pitch(); // 从 BlasterCharacter 获取 AO_Pitch /* P50 应用瞄准偏移(Applying Aim Offset)*/ }
-
编译后,回到虚幻引擎中 "
BlasterAnimBP
" 的 "AnimGraph
" 蓝图面板,添加 "获取 AO Yaw "(Get AO Yaw)和 "获取 AO Yaw "(Get AO Yaw)节点,并分别与 "HipAimOffset
" 节点的 "Yaw
" 和 "Pitch
" 引脚进行连接。
-
编译、保存后进行测试,可以观察到人物角色在静止站立且不跳跃时,人物角色的前方朝向不跟随跟随我们的控制器,其向左、向右的瞄准动画可以正确播放。在奔跑和跳跃时,人物角色的正前方朝向跟随我们的控制器进行改变,始终为控制器的当前朝向。
-
在人物角色蓝图类 "
BP_BlasterCharacter
" 中将人物角色网格体旋转 90°,再次进行测试,可以观察向上、向下瞄准动画能正确播放。
-
回到虚幻引擎中 "
BlasterAnimBP
" 的 "AnimGraph
" 蓝图面板,在右侧资产浏览器中将 "AimAimOffset
" 移动至面板中生成蓝图节点,绘制下图所示的蓝图。
-
编译、保存后进行测试,可以观察到人物角色在静止站立且不跳跃进行瞄准时,人物角色的前方朝向不跟随跟随我们的控制器,其向左、向右、向上、向下的瞄准动画可以正确播放。
-
当我们控制服务器上的人物角色进行向下瞄准时,会在客户端上发现服务器的人物角色会向上瞄准,即出现服务器和客户端动画不同步的问题,我们将在下节课中探讨并解决这个问题
50.3 Summary
本节课我们成功实现了瞄准偏移(Aim Offset)系统在角色动画中的应用。首先,我们在动画蓝图中创建了缓存姿势节点,将装备武器的状态机输出保存为 "Equipped
" 缓存姿势,以便在多个位置复用。接着,使用 "每个骨骼的分层混合"(Layered blend per bone)节点,基于 "spine_01
" 骨骼将上半身瞄准动画与下半身移动动画进行混合,确保瞄准动作只影响上半身。
在 C++ 代码中,我们在 "BlasterCharacter
" 类中添加了 "AO_Yaw
" 和 "AO_Pitch
" 变量来控制瞄准偏移的角度,完成了 "AimOffset()
" 函数的定义以实现当人物角色静止站立时禁用控制器偏航旋转,基于起始旋转计算偏航差量;当奔跑或跳跃时,启用控制器偏航旋转,重置偏航值为 0。同时,我们还通过定义内联函数 "GetAO_Yaw()
" 和 "GetAO_Pitch()
" 向动画实例 "BlasterAnimInstance
" 传递 "AO_Yaw
" 和 "AO_Pitch
" 的值。
在动画蓝图中,我们将获取到的 "AO_Yaw
" 和 "AO_Pitch
" 值分别连接到 "HipAimOffset
" 或 "AimAimOffset
" 的对应输入引脚,驱动人物角色能够在静止时身体保持面向原始方向,仅武器随鼠标移动瞄准,并正确播放各个方向(左、右、上、下)的瞄准动画。
当我们控制服务器上的人物角色进行向下瞄准时,会在客户端上发现服务器的人物角色会向上瞄准,即出现服务器和客户端动画不同步的问题,这将在后续课程中解决。