角色动作——Equip/Crouch/Aim(1)

写在前面

在接下来的几个部分中,我们将来处理角色的动作,在此期间我们会用上动画蓝图。在这篇文章中,我们会来处理角色Equip的动作。

动作,意味着什么呢,我们在之前的动画蓝图动画蓝图中已经说过,动作是由状态来决定的------ 一个状态决定了一个动作,不同状态之间的转变,决定了动作的转变。在没有装备上武器之前,我们是空手的状态,而在装备上武器之后,我们就会进入装备上武器之后的步行状态。

玩过一些FPS的同学都知道,比如说《战地》,以及刚出的《幻兽帕鲁》,一般而言,在持枪状态下,我们的角色朝向会和controller和摄像一致,前后左右的移动不再是人直接面朝前后左右直接进行移动,而是面朝控制器,双脚进行移动,这样才可以在逻辑上保持人物的瞄准方向和玩家的观感一致。

状态准备------是否装备了武器

首先,我们需要一个状态位来确定角色是否已经进入了武器装备状态。这个状态位当然在AnimeInstance里,毕竟我们会在我们的动画蓝图中用。

C++ 复制代码
// BlasterAnimation.h
UPROPERTY(BlueprintReadOnly, Category = Character, meta = (AllowPrivateAccess = "true"))
bool bIsEquippedWeapon;

// BlasterAnimation.cpp
if (BlasterCharacter->GetEquipWeaponComponent() && BlasterCharacter->GetEquipWeaponComponent()->GetEquippedWeapon())
        bIsEquippedWeapon = true; // 是否装备了武器

逻辑非常简单,我们确认下角色的战斗组件里面是否已经有装备武器了,有的话,那就说明已经有武器装备了。

但是,这里有个问题,在装备武器那一章节我们已经提到过,我们为了保持同步,我们在客户端装备武器时,我们本质是让服务端去帮我们做事,我们客户端的战斗模块实例上的EquipWeapon其实只在服务端进行了更新(UE帮我们已经做了很多同步行为,但是貌似在一些细节上还需要我们自己同步),这就意味着,客户端的AnimeInstance上依赖于EquipWeaponComponent组件的状态位是没法更新的。

那怎么办的?依然是老办法,Property Replicated, 将服务端的EquipWeaponComponent上的EquipWeapon直接同步给客户端的就得了。

c++ 复制代码
// 装备的武器
UPROPERTY(Replicated, VisibleAnyWhere)
AWeapon *EquippedWeapon;

void UEquipWeaponComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty> &OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	// EquippedWeapon 同步
	DOREPLIFETIME(ThisClass, EquippedWeapon);
}

这样客户端和服务端的动画蓝图都能检测到正确的装备状态了。

状态准备------奔跑方向和倾斜

我们已经说过,在装备了武器时,我们的角色的奔跑逻辑会和没有装备武器时很不相同,我们得做到角色面部朝向的同时,让角色的腿部动作往前后左右移动,同时在角色转向时,我们可以给予角色一个倾斜的效果,因此我们得设置出Direction和Lean两个量来关联动作。

C++ 复制代码
// BlasterAnimation.h
// 移动方向
UPROPERTY(BlueprintReadOnly, Category = Character, meta = (AllowPrivateAccess = "true"))
float Direction;

UPROPERTY(BlueprintReadOnly, Category = Character, meta = (AllowPrivateAccess = "true"))
float Lean; // 倾斜度

FRotator CharacterRotationLastFrame;
FRotator CharacterRotation;
FRotator DeltaRotation;

// BlasterAnimation.cpp
void UBlasterAnimation::NativeUpdateAnimation(float DeltaSeconds)
{
    Super::NativeUpdateAnimation(DeltaSeconds);

// ......

    auto AimRotation = BlasterCharacter->GetBaseAimRotation();                                 // 你照相机的视角还有Aim offset 全局
    auto MovementRotation = UKismetMathLibrary::MakeRotFromX(BlasterCharacter->GetVelocity()); // 你角色mesh的朝向                       全局
    auto DeltaRot = UKismetMathLibrary::NormalizedDeltaRotator(AimRotation, MovementRotation); // 你controller的方向 角度 - 你角色方向角度 = 你要去走的角度
    DeltaRotation = FMath::RInterpTo(DeltaRotation, DeltaRot, DeltaSeconds, 6.f);              // 插值
    Direction = DeltaRotation.Yaw;                                                             // 你要去走的角度的Yaw

    CharacterRotationLastFrame = CharacterRotation;
    CharacterRotation = BlasterCharacter->GetActorRotation();
    const auto Delta = UKismetMathLibrary::NormalizedDeltaRotator(CharacterRotation, CharacterRotationLastFrame);
    const float Target = Delta.Yaw / DeltaSeconds;
    const float Interp = FMath::FInterpTo(Lean, Target, DeltaSeconds, 6.f);
    Lean = FMath::Clamp(Interp, -90, 90);
}

可以这么理解,Direction的计算方式主要取决于你角色本身的瞄准方向角色的移动方向的差,角色的瞄准方向和你的屏幕的方向是一致的,角色移动的方向就是你键盘按下的方向,两个的差就是最后的方向。

第二个Lean是由加速度来得到的,在这一帧我是朝向正面的,到了下一帧我朝向了左面,我们计算出二者之间转化的加速度,加速度越大就越斜,然后把加速度給映射到-90-90的范围内。怎么样,很合理吧!

当然,为了让之间的转变更自然,两者都加入了插值。所谓插值就是中间值,举个例子,我以非常快的速度从正面转到了后面,那势必会显得非常突兀,那我们可以在这两者中间加一个中间值,比如说左边,这样一来,玩家在快速转向时,就可以看到转动的一个中间状态,虽然时间是拉长了那么一点,但是效果就变好了。

再拿Lean倾斜举例子,我从左边突然歪到右边,是不是也很突兀?因为缺乏中间值,玩家会感觉人物在抖动,非常影响观感,那我也加一个插值就可以缓解了。

动画准备------建立角色移动混合空间

状态准备完毕后,那就该动画出场了。

首先,在角色的基础动画包里面找到基础的拿枪移动动画,然后复制一份,接着我们要对他们进行左右倾斜调整。 比如:

让它歪向左边或右边20度左右。

首先,点开Skeleton Tree按钮,接着点击root:

然后就可以调整了,导出时,点击key,然后导出

对于每个hip进行如此操作后,我们会得到大约12份动画。

建立Blend Space, 以Direction(-180-180)为横轴,Lean(-90-90)为纵轴。把这些动画贴上即可。

动画蓝图

首先在动画蓝图第一层,我们通过判断是否装备武器来判断进入哪个状态机。

在Equipped状态机中,我们把Standing状态和Crouching状态分开。

在Idle内,我们添加普通的站立Idle动画,在Jog内,我们加入刚才的混合空间。

Idle -> Jog

Jog->Idle

注意我们在写状态转变时,可以尽可能地加入多个状态判断来缩减转变范围。

END

相关推荐
前端之虎陈随易6 小时前
编程语言级别的Skill市场,AI Agent 的未来形态
前端·vue.js·人工智能·typescript·node.js
一路向北he6 小时前
字节钢铁军团--“提供情境,而非控制”
java·开发语言·前端
kyriewen6 小时前
豆包和千问同时关了智能体,我用它们搭的 3 个自动化全废了——迁移方案整理
前端·javascript·ai编程
前端一小卒6 小时前
我用 TypeScript 从零手写了一个 Claude Code,然后发现它的核心只有 30 行
前端·agent
想做后端的前端7 小时前
游戏里的水面是怎么做的
游戏
大圣编程8 小时前
Python中continue语句的用法是什么?
开发语言·前端·python
yuhaiqiang8 小时前
随手 vibecoding 的浏览器插件已经 6000 多次下载,聊聊他的产品设计
前端·后端·面试
之歆8 小时前
Vue商品详情与放大镜组件
前端·javascript·vue.js
再吃一根胡萝卜9 小时前
如何把小米 MiMo 接入 CodeBuddy,打造私有 Agent
前端
leoZ23110 小时前
Claude 全面解析:从基础原理到实战应用指南
人工智能·游戏