この記事はUnrealEngine 4(UE4)AdventCalender 2020の15日目の投稿記事です。
qiita.com
やることはタイトルの通りコンボをUE4で実装する方法の一つの紹介みたいな感じです。
今回のアドベントカレンダーでネタ被りをしてしまって申し訳ない・・・。
qiita.com
GameplayAbilityの概要やら導入について説明してくださっている先駆者の方々や公式のドキュメント
docs.unrealengine.com
okawari-hakumai.hatenablog.com
historia.co.jp
GameplayAbilitySystemの概要と導入は上記の記事を参考にしてください。
アニメーションのアセットは下記のものを使用しました。
Dynamic Sword Animset:アニメーション - UE マーケットプレイス
また、このコンボ実装は公式のActionRPGプロジェクトを参考にさせてもらいました。
Action RPG:Epic コンテンツ - UE マーケットプレイス
AnimNotifyとAnimNotifyStateが存在するので、それについて軽く説明と使い方をまとめます。
docs.unrealengine.com
AnimNotifyとAnimNotifyStateはアニメーションでの通知イベントのクラスです。両者の違いは1フレームか複数フレームにまたがるかです。
AnimNotifyは挿入したフレームの際にフックするReceiveNotifyイベントが、
AnimNotifyStateはBegin、Tick、Endイベントが実装されています。
これを利用して入力受付の時間の調整などの処理を実装していきます。
プロジェクトはUE4.26のC++のBlankで作成しました。C++のThirdPersonでもいいと思います。
プラグインを有効化したあと、プロジェクトのBuild.csを編集します。
using UnrealBuildTool;
public class MyProject4 : ModuleRules
{
public MyProject4(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "GameplayAbilities", "GameplayTags", "GameplayTasks" });
PrivateDependencyModuleNames.AddRange(new string[] { });
}
}
GameplayAbilitiesとGameplayTags、GameplayTasksをPublicDependencyModuleNamesに追加しました。
次にキャラクターの実装をします。冒頭の記事を参考にC++で作成します。適宜自身のプロジェクトに置き換えてください。
MyCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "AbilitySystemInterface.h"
#include "MyAbilitySystemComponent.h"
#include "MyCharacter.generated.h"
class UMyGameplayAbility;
UCLASS()
class MYPROJECT4_API AMyCharacter : public ACharacter, public IAbilitySystemInterface
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
class USpringArmComponent* CameraBoom;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
class UCameraComponent* FollowCamera;
public:
AMyCharacter();
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
float BaseTurnRate;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera)
float BaseLookUpRate;
protected:
virtual void BeginPlay() override;
void MoveForward(float Value);
void MoveRight(float Value);
void TurnAtRate(float Rate);
void LookUpAtRate(float Rate);
void TouchStarted(ETouchIndex::Type FingerIndex, FVector Location);
void TouchStopped(ETouchIndex::Type FingerIndex, FVector Location);
public:
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
public:
FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Abilities", meta = (AllowPrivateAccess = "true"))
class UMyAbilitySystemComponent* AbilitySystem;
UAbilitySystemComponent* GetAbilitySystemComponent() const
{
return Cast<UAbilitySystemComponent>(AbilitySystem);
};
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Abilities")
TArray<TSubclassOf<class UGameplayAbility>> AbilityList;
virtual void PossessedBy(AController* NewController) override;
public:
UFUNCTION(BlueprintCallable, Category = "Abilities")
bool ActivateAbilitiesWithTags(FGameplayTagContainer AbilityTags, bool bAllowRemoteActivation = true);
UFUNCTION(BlueprintCallable, Category = "Abilities")
void GetActiveAbilitiesWithTags(FGameplayTagContainer AbilityTags, TArray<UMyGameplayAbility*>& ActiveAbilities);
};
MyCharacter.cpp
#include "MyCharacter.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/InputComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/Controller.h"
#include "GameFramework/SpringArmComponent.h"
AMyCharacter::AMyCharacter()
{
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
BaseTurnRate = 45.f;
BaseLookUpRate = 45.f;
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f);
GetCharacterMovement()->JumpZVelocity = 600.f;
GetCharacterMovement()->AirControl = 0.2f;
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 300.0f;
CameraBoom->bUsePawnControlRotation = true;
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
AbilitySystem = CreateDefaultSubobject<UMyAbilitySystemComponent>(TEXT("AbilitySystem"));
}
void AMyCharacter::BeginPlay()
{
Super::BeginPlay();
if (AbilitySystem)
{
int32 inputID(0);
if (HasAuthority() && AbilityList.Num() > 0)
{
for (auto Ability : AbilityList)
{
if (Ability)
{
AbilitySystem->GiveAbility(FGameplayAbilitySpec(Ability.GetDefaultObject(), 1, inputID++));
}
}
}
AbilitySystem->InitAbilityActorInfo(this, this);
}
}
void AMyCharacter::MoveForward(float Value)
{
if ((Controller != nullptr) && (Value != 0.0f))
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
AddMovementInput(Direction, Value);
}
}
void AMyCharacter::MoveRight(float Value)
{
if ((Controller != nullptr) && (Value != 0.0f))
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(Direction, Value);
}
}
void AMyCharacter::TurnAtRate(float Rate)
{
AddControllerYawInput(Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds());
}
void AMyCharacter::LookUpAtRate(float Rate)
{
AddControllerPitchInput(Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds());
}
void AMyCharacter::TouchStarted(ETouchIndex::Type FingerIndex, FVector Location)
{
}
void AMyCharacter::TouchStopped(ETouchIndex::Type FingerIndex, FVector Location)
{
}
void AMyCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
check(PlayerInputComponent);
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
PlayerInputComponent->BindAxis("MoveForward", this, &AMyCharacter::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &AMyCharacter::MoveRight);
PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
PlayerInputComponent->BindAxis("TurnRate", this, &AMyCharacter::TurnAtRate);
PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
PlayerInputComponent->BindAxis("LookUpRate", this, &AMyCharacter::LookUpAtRate);
}
void AMyCharacter::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
AbilitySystem->RefreshAbilityActorInfo();
}
bool AMyCharacter::ActivateAbilitiesWithTags(FGameplayTagContainer AbilityTags, bool bAllowRemoteActivation)
{
if (AbilitySystem)
{
return AbilitySystem->TryActivateAbilitiesByTag(AbilityTags, bAllowRemoteActivation);
}
return false;
}
void AMyCharacter::GetActiveAbilitiesWithTags(FGameplayTagContainer AbilityTags, TArray<UMyGameplayAbility*>& ActiveAbilities)
{
if (AbilitySystem)
{
AbilitySystem->GetActiveAbilitiesWithTags(AbilityTags, ActiveAbilities);
}
}
基本的にはThirdpersonのC++からパクってきてます。一部いらなさそうなやつは削除したりしてます。
キャラクターで使っている独自AbilitySystemComponentとGameplayAbilityを追加で作ります。
MyGameplayAbility.h
#pragma once
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "MyGameplayAbility.generated.h"
UCLASS()
class MYPROJECT4_API UMyGameplayAbility : public UGameplayAbility
{
GENERATED_BODY()
UFUNCTION(BlueprintCallable, Category = "GameplayAbility")
virtual void AddGameplayTags(const FGameplayTagContainer GameplayTags);
UFUNCTION(BlueprintCallable, Category = "GameplayAbility")
virtual void RemoveGameplayTags(const FGameplayTagContainer GameplayTags);
};
MyGameplayAbility.cpp
#include "MyGameplayAbility.h"
#include "AbilitySystemComponent.h"
void UMyGameplayAbility::AddGameplayTags(const FGameplayTagContainer GameplayTags)
{
UAbilitySystemComponent* Comp = GetAbilitySystemComponentFromActorInfo();
Comp->AddLooseGameplayTags(GameplayTags);
}
void UMyGameplayAbility::RemoveGameplayTags(const FGameplayTagContainer GameplayTags)
{
UAbilitySystemComponent* Comp = GetAbilitySystemComponentFromActorInfo();
Comp->RemoveLooseGameplayTags(GameplayTags);
}
MyAbilitySystemComponent.h
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystemComponent.h"
#include "MyAbilitySystemComponent.generated.h"
class UMyGameplayAbility;
class UGameplayAbility;
UCLASS()
class MYPROJECT4_API UMyAbilitySystemComponent : public UAbilitySystemComponent
{
GENERATED_BODY()
public:
UMyAbilitySystemComponent();
void GetActiveAbilitiesWithTags(const FGameplayTagContainer& GameplayTagContainer, TArray<UMyGameplayAbility*>& ActiveAbilities);
static UMyAbilitySystemComponent* GetAbilitySystemComponentFromActor(const AActor* Actor, bool LookForComponent = false);
};
MyAbilitySystemComponent.cpp
#include "MyAbilitySystemComponent.h"
#include "MyGameplayAbility.h"
#include "AbilitySystemGlobals.h"
UMyAbilitySystemComponent::UMyAbilitySystemComponent() {}
void UMyAbilitySystemComponent::GetActiveAbilitiesWithTags(const FGameplayTagContainer& GameplayTagContainer, TArray<UMyGameplayAbility*>& ActiveAbilities)
{
TArray<FGameplayAbilitySpec*> AbilitiesToActivate;
GetActivatableGameplayAbilitySpecsByAllMatchingTags(GameplayTagContainer, AbilitiesToActivate, false);
for (FGameplayAbilitySpec* spec : AbilitiesToActivate)
{
TArray<UGameplayAbility*> AbilityInstances = spec->GetAbilityInstances();
for (UGameplayAbility* ActiveAbility : AbilityInstances)
{
ActiveAbilities.Add(Cast<UMyGameplayAbility>(ActiveAbility));
}
}
}
UMyAbilitySystemComponent* UMyAbilitySystemComponent::GetAbilitySystemComponentFromActor(const AActor* Actor, bool LookForComponent)
{
return Cast<UMyAbilitySystemComponent>(UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Actor, LookForComponent));
}
VisualStudioのビルドが通れば、C++側の実装は終わりになります。
UE4Editorに移り、AnimNotifyStateを作ります。
名前はNS_JumpSectionにしておきます。
グラフが開いたら、FunctionsでReceivedNotifyBegin/EndをOverrideで作ります。
また、VariablesにName型の変数を一つ追加します。ここでは名前をNextSectionNameとしました。
End側ではコンボ用のプロパティを初期化しています。
Start側ではMontageの次のセクションを初期化し、後述で作るプレイヤークラスのコンボ入力の受付可能フラグを立て、現在実行中のAnimNotifyStateクラスである自身をプレイヤークラスのプロパティにセットしています。
次に再生するアニメーションモンタージュを作成します。
名前はAM_Swordとします。
いい感じに攻撃モーションを並べて、先ほど作成したAnimNotifyStateを配置します。
今回はループで延々と攻撃が続けられるように二つの配置とNS_JumpSectionをそれぞれに配置していますが、ループせずに終わる場合は、最後のモーションには通知を置かないようにします。
AnimationBlueprintでは作成したアニメーションモンタージュを再生できるようにしておきます。
GameplayAbilityを作ります。名前はGA_Testとしておきます。
変数にAnimationMontageを追加します。名前はanimMontageで、デフォルトの値には先ほど作成したアニメーションモンタージュをセットしておきます。
やってることはAnimationMontageを再生開始しています。
最後に操作するキャラクタークラスを作成します。MyCharacterクラスを基底クラスとしたBlueprintクラスを作成します。名前はBP_Characterにしておきます。
VariablesのAbilities/AbilityListに先ほど作成したGameplayAbilityを追加します。これで実行時にAbilitySystemComponentに自動で登録されます。
また、変数にコンボ受付可能フラグのEnableComboPeriod、現在実行中のAnimNotifyStateクラスを格納するCurrentNotifyを追加します。
再生用にDoMeleeという関数を作成します。
DoMelee関数の中身です。
現在アニメーション通知が存在するなら後述するコンボ処理関数に処理を移します。
存在しない場合はニュートラル状態なので、新規でGameplayAbilityを実行します。
コンボ処理のJumpSectionForCombo関数の中身です。
処理としてはコンボ受付可能状態であれば、次のセクションにアニメーション通知に設定されたセクションを次のセクションとしてMontageに設定しています。
そして、コンボ入力の受付フラグを折ります。
以上のものを実装した結果、下記の動画のような動作をします。
明日は@dgtanakaさんの記事になります。