《UE5_C++多人TPS完整教程》学习笔记49 ——《P50 应用瞄准偏移(Applying Aim Offset)》


本文为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 在蓝图中应用瞄准偏移

  1. 在 "BlasterAnimBP" 的 "AnimGraph" 蓝图面板,添加蓝图节点 "新保存的缓存姿势 "(New Save cached pose),将其重命名为 "Equipped",并与原先的 "Equipped" 状态机节点相连,这样我们就可以在蓝图的多个位置通过添加蓝图界定啊 "使用缓存姿势"Equipped" (Use cached pose 'Equipped')" 节点来使用 "Equipped" 状态机节点。

  2. 每个骨骼的分层混合 (Layered blend per bone)

  3. 在骨骼 "SK_EpicCharacter_Skeleton" 中,"spine_01" 是分开上下半身的骨骼节点(spine_01 is what separates the lower half from the upper half of the body),因此我们选择这个骨骼节点在 "每个骨骼的分层混合 " 蓝图节点中进行混合。

  4. 选择 "每个骨骼的分层混合 " 蓝图节点,在右侧细节面板中展开 "层设置 "(Layer Setup),接着展开 "索引 [0] "(Index [0]),然后再展开 "分支过滤器 "(Branch Filters)、"索引 [0] "(Index [0]),设置 "骨骼名称 "(Bone Name)为 "spine_01"。

  5. 在右侧资产浏览器中将 "HipAimOffset" 移动至面板中生成蓝图节点,并绘制下图中的蓝图。


50.2 使用 C++ 代码驱动瞄准偏移

  1. 蓝图节点 "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)*/
    
        ...
    };
  2. 在 "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)*/
    }
    
    ...
  3. 在 "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)*/
    }
  4. 编译后,回到虚幻引擎中 "BlasterAnimBP" 的 "AnimGraph" 蓝图面板,添加 "获取 AO Yaw "(Get AO Yaw)和 "获取 AO Yaw "(Get AO Yaw)节点,并分别与 "HipAimOffset" 节点的 "Yaw" 和 "Pitch" 引脚进行连接。

  5. 编译、保存后进行测试,可以观察到人物角色在静止站立且不跳跃时,人物角色的前方朝向不跟随跟随我们的控制器,其向左、向右的瞄准动画可以正确播放。在奔跑和跳跃时,人物角色的正前方朝向跟随我们的控制器进行改变,始终为控制器的当前朝向。

  6. 在人物角色蓝图类 "BP_BlasterCharacter" 中将人物角色网格体旋转 90°,再次进行测试,可以观察向上、向下瞄准动画能正确播放。


  7. 回到虚幻引擎中 "BlasterAnimBP" 的 "AnimGraph" 蓝图面板,在右侧资产浏览器中将 "AimAimOffset" 移动至面板中生成蓝图节点,绘制下图所示的蓝图。

  8. 编译、保存后进行测试,可以观察到人物角色在静止站立且不跳跃进行瞄准时,人物角色的前方朝向不跟随跟随我们的控制器,其向左、向右、向上、向下的瞄准动画可以正确播放。

  9. 当我们控制服务器上的人物角色进行向下瞄准时,会在客户端上发现服务器的人物角色会向上瞄准,即出现服务器和客户端动画不同步的问题,我们将在下节课中探讨并解决这个问题


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" 的对应输入引脚,驱动人物角色能够在静止时身体保持面向原始方向,仅武器随鼠标移动瞄准,并正确播放各个方向(左、右、上、下)的瞄准动画。

当我们控制服务器上的人物角色进行向下瞄准时,会在客户端上发现服务器的人物角色会向上瞄准,即出现服务器和客户端动画不同步的问题,这将在后续课程中解决。


相关推荐
m0_552200823 小时前
《UE5_C++多人TPS完整教程》学习笔记50 ——《P51 多人游戏中的俯仰角(Pitch in Multiplayer)》
c++·游戏·ue5
CandyU23 小时前
UE5 基础应用 —— 09 - 展示类小项目
ue5
vonlycn3 小时前
UE5 性能优化(1) 模型合并,材质合并
ue5·材质
The Chosen One9855 小时前
C++ : AVL树-详解
开发语言·c++
zzyzxb5 小时前
std::enable_shared_from_this
c++
SNAKEpc121385 小时前
QML和Qt Quick
c++·qt
hansang_IR5 小时前
【题解】洛谷 P4286 [SHOI2008] 安全的航线 [递归分治]
c++·数学·算法·dfs·题解·向量·点积
GanGuaGua5 小时前
Linux系统:线程的互斥和安全
linux·运维·服务器·c语言·c++·安全
怀旧,5 小时前
【C++】18. 红⿊树实现
开发语言·c++