目录
1. 添加动画
我们在之前的课程中实现了从角色右手发射魔法粒子,但角色并没有播放相应的动画,导致角色攻击看起来十分生硬。
在我们引入的资产Gideon角色中,开发者已经做了充分的工作,使我们可以十分简单地调用函数来直接播放动画。要让骨骼体动起来,就需要使用动画序列,它是可在骨架网格体上播放的单个动画资源。这些序列包含各个关键帧,依次回放这些关键帧(相互合成)就可以实现骨骼体动画。而UE中控制动画播放的系统称为蒙太奇(Montage),它可以分别通过蓝图或C++来控制动画序列播放。
在了解相关原理后,我们在SurCharacter.cpp的PrimaryAttack函数实现播放动画的功能。在.h和.cpp的相应位置分别添加如下代码:
// SurCharacter.cpp -> PrimaryAttack()
PlayAnimMontage(AttackAnim);
// SurCharacter.h
// Category指定在蓝图编辑工具中显示时的属性类别
UPROPERTY(EditAnywhere, Category = "Attack")
UAnimMontage* AttackAnim;
// 之前的投射体子类,也加上Category
UPROPERTY(EditAnywhere, Category = "Attack")
SubclassOf<AActor> ProjectileClass;
然后在UE中给AttackAnim选择任意动画即可。需要注意的是,我们在使用UPROPERTY宏时加上Category,可以使相关的变量在蓝图编辑器中显示在Attack类别下。
运行测试,可以发现动画可以正常播放,不过魔法粒子发射的位置还是角色在静止状态下右手的位置,而没有从角色抬手后的手掌中发出来。(虽然这看起来也算另一种很酷的效果)
产生这个问题的原因,在于计算粒子起点的时候,即执行 FVector RightHandLoc = GetMesh()->GetSocketLocation("Muzzle_01"); 这条语句时,角色才刚刚开始播放动画,得到的位置自然在下方。解决方法也很简单,根据动画的速度在这条语句前设置相应延迟即可,所以我们利用UE中的计时器来实现这个效果:
// 左键攻击
void ASurCharacter::PrimaryAttack() {
PlayAnimMontage(AttackAnim);
// 0.18s延迟后触发魔法粒子生成
GetWorldTimerManager().SetTimer(TimerHandle_PrimaryAttack, this, &ASurCharacter::PrimaryAttack_TimeElapsed, 0.18f);
}
// 原先控制魔法粒子的内容移到这个函数中
void ASurCharacter::PrimaryAttack_TimeElapsed() {
// 获取模型右手位置
FVector RightHandLoc = GetMesh()->GetSocketLocation("Muzzle_01");
// 朝向角色方向,在角色的右手位置生成
FTransform SpawnTM = FTransform(GetActorRotation(), RightHandLoc);
// 此处设置碰撞检测规则为:即使碰撞也总是生成
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
GetWorld()->SpawnActor<AActor>(ProjectileClass, SpawnTM, SpawnParams);
}
要注意传入的定时器句柄TimerHandle_PrimaryAttack是FTimerHandle类,需要在SurCharacter.h文件中声明。只要定时器句柄存在,过0.2s后就会执行PrimaryAttack_TimeElapsed()中发射粒子的代码。这样做的好处是,当角色在攻击的一瞬间死亡时,其TimerHandle_PrimaryAttack将会自动取消,那之后的动画也就不会再播放。
现在运行代码,可以发现粒子发射的位置成功更换到了Gideon的手上。
进一步,如果想要取得更精细的调整结果,你可以尝试把0.18f这个值定义成变量并暴露在UE中,直接在UE中边运行边调整。
2. 完整代码
SurCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SurCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class USurInteractionComponent;
class UAnimMontage;
UCLASS()
class SURKEAUE_API ASurCharacter : public ACharacter
{
GENERATED_BODY()
public:
ASurCharacter();
protected:
//弹簧臂组件
UPROPERTY(VisibleAnywhere)
USpringArmComponent* SpringArmComp;
//相机组件
UPROPERTY(VisibleAnywhere)
UCameraComponent* CameraComp;
// 投射体子类
UPROPERTY(EditAnywhere, Category = "Attack")
TSubclassOf<AActor> ProjectileClass;
// 界面
UPROPERTY(VisibleAnywhere)
USurInteractionComponent* InteractionComp;
virtual void BeginPlay() override;
void MoveForward(float value);
void MoveRight(float value);
void PrimaryAttack();
void PrimaryInteract();
void PrimaryAttack_TimeElapsed();
// 动画
UPROPERTY(EditAnywhere, Category = "Attack")
UAnimMontage* AttackAnim;
FTimerHandle TimerHandle_PrimaryAttack;
public:
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};
SurCharacter.cpp
#include "SurCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "SurInteractionComponent.h"
ASurCharacter::ASurCharacter()
{
PrimaryActorTick.bCanEverTick = true;
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>("SpringArmComp");
SpringArmComp->bUsePawnControlRotation = true;
SpringArmComp->SetupAttachment(RootComponent);
CameraComp = CreateDefaultSubobject<UCameraComponent>("CameraComp");
CameraComp->SetupAttachment(SpringArmComp);
InteractionComp = CreateDefaultSubobject<USurInteractionComponent>("InteractionComp");
GetCharacterMovement()->bOrientRotationToMovement = true;
bUseControllerRotationYaw = false;
}
void ASurCharacter::BeginPlay()
{
Super::BeginPlay();
}
void ASurCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void ASurCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// 移动控制
PlayerInputComponent->BindAxis("MoveForward", this, &ASurCharacter::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &ASurCharacter::MoveRight);
PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
// 跳跃
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
// 攻击
PlayerInputComponent->BindAction("PrimaryAttack", IE_Pressed, this, &ASurCharacter::PrimaryAttack);
// 交互
PlayerInputComponent->BindAction("PrimaryInteract", IE_Pressed, this, &ASurCharacter::PrimaryInteract);
}
// 角色向前移动
void ASurCharacter::MoveForward(float value)
{
FRotator ControlRot = GetControlRotation();
// 转向只关注水平Yaw方向,因此置0防止影响
ControlRot.Pitch = 0;
ControlRot.Roll = 0;
// 获取相机(鼠标控制器)的朝向,并朝这个方向移动
AddMovementInput(ControlRot.Vector(), value);
}
// 角色向右移动
void ASurCharacter::MoveRight(float value)
{
FRotator ControlRot = GetControlRotation();
ControlRot.Pitch = 0;
ControlRot.Roll = 0;
// 获取相机(鼠标控制器)的朝向,转向右侧,并朝这个方向移动
FVector RightVector = FRotationMatrix(ControlRot).GetScaledAxis(EAxis::Y);
AddMovementInput(RightVector, value);
}
// 左键攻击
void ASurCharacter::PrimaryAttack() {
PlayAnimMontage(AttackAnim);
GetWorldTimerManager().SetTimer(TimerHandle_PrimaryAttack, this, &ASurCharacter::PrimaryAttack_TimeElapsed, 0.18f);
}
void ASurCharacter::PrimaryAttack_TimeElapsed() {
// 获取模型右手位置
FVector RightHandLoc = GetMesh()->GetSocketLocation("Muzzle_01");
// 朝向角色方向,在角色的右手位置生成
FTransform SpawnTM = FTransform(GetActorRotation(), RightHandLoc);
// 此处设置碰撞检测规则为:即使碰撞也总是生成
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
GetWorld()->SpawnActor<AActor>(ProjectileClass, SpawnTM, SpawnParams);
}
// 交互
void ASurCharacter::PrimaryInteract() {
InteractionComp->PrimaryInteract();
}