角色动作——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

相关推荐
编程零零七3 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
(⊙o⊙)~哦5 小时前
JavaScript substring() 方法
前端
无心使然云中漫步5 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者5 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_6 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js
麒麟而非淇淋7 小时前
AJAX 入门 day1
前端·javascript·ajax
2401_858120537 小时前
深入理解MATLAB中的事件处理机制
前端·javascript·matlab
阿树梢7 小时前
【Vue】VueRouter路由
前端·javascript·vue.js
随笔写8 小时前
vue使用关于speak-tss插件的详细介绍
前端·javascript·vue.js
史努比.8 小时前
redis群集三种模式:主从复制、哨兵、集群
前端·bootstrap·html