Animation Montage는 여러 애니메이션을 담아두는 컨테이너다.
두 개의 공격 애니메이션을 하나의 Animation Montage에 넣어두고 필요에 따라 원하는 애니메이션을 실행시킬 수 있다.
Action Mapping, 콜백함수 바인딩
Project Settings
에서 새로 Action Mapping을 생성한다.
AnimationMontage를 생성한다.
파일을 열고 우측 하단의Asset Browser
에서 필요한 공격 애니메이션들을 추가해준다.
Montage Section
을 적절히 추가해주고 Default는 삭제해준다.
우측 하단의 Montage Section에서 Attack1 애니메이션이 끝나고 Attack2 애니메이션이 따로 재생될 수 있도록Clear
를 눌러준다.Blueprint에서 구현
ABP_WraithCharacter에 들어가 Ouput Pose전에
Slot
을 검색하고DefaultSlot
을 이어준다.
여기서의DefaultSlot
은 여기에 있는 Slot을 의미한다. 추후 슬롯명을 정하고 필요에 따른 애니메이션 재생이 가능하도록 할 것이다.
캐릭터 블루프린트로 들어가 InputAction Attack노드를 생성한다.
Montage 실행 함수는 캐릭터 메시에서 AnimInstance에 접근가능하므로 우측의 Components에서 Mesh를 끌고와 노드를 생성한다.
Mesh에서GetAnimInstance
노드를 검색해 연결시키고, AnimInstance에서Montage Play
노드를 생성해 연결시킨다.
Montage to Play 핀에 생성해둔 AM_AttackMontage를 넣고 실행하면 캐릭터가 공격 애니메이션을 재생하는 것을 확인할 수 있다.cpp에서 구현
먼저 WraithChracter의 헤더파일에 Attack함수 선언과 Montage 선언을 진행한다.
// WraithCharacter.h ... class UAnimMontage; ... protected: ... // 공격 함수 void Attack(); private: ... // Attack Animation Montage UPROPERTY(EditDefaultsOnly, Category = "Montages") UAnimMontage* AttackMontage;
cpp파일에 Attack함수의 정의와 콜백함수 바인딩을 진행한다.
... #include "Animation/AnimMontage.h" ... // WraithCharacter.cpp ... void AWraithCharacter::Attack() { // Montage_Play 사용을 위한 AnimIntance UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance(); if(AnimInstance && AttackMontage) { // 재생할 montage와 속도 지정 AnimInstance->Montage_Play(AttackMontage, 2.f); // 랜덤값 지정을 위한 변수 int32 Selection = FMath::RandRange(0, 1); // 재생할 애니메이션 섹션을 위한 변수 FName SectionName = FName(); // 랜덤을 통해 Switch를 이용하여 SectionName 할당 switch (Selection) { case 0: SectionName = FName("Attack1"); break; case 1: SectionName = FName("Attack2"); break; default: break; } // 랜덤으로 선택된 번호의 애니메이션으로 점프하여 재생 AnimInstance->Montage_JumpToSection(SectionName, AttackMontage); } } ... void AWraithCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { ... // ActionBinding PlayerInputComponent->BindAction(FName("Attack"), IE_Pressed, this, &AWraithCharacter::Attack); ... }
실행하면 랜덤값에 의해 캐릭터가 공격하는 애니메이션을 확인할 수 있다.
좀더 깔끔하게 코드를 정리한다.// WraithCharacter.h ... protected: ... void PlayAttackMontage();
// WraithCharacter.cpp ... // 공격 함수 void AWraithCharacter::Attack() { PlayAttackMontage(); } // AttackMontage play 함수 void AWraithCharacter::PlayAttackMontage() { UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance(); if(AnimInstance && AttackMontage) { AnimInstance->Montage_Play(AttackMontage, 2.f); const int32 RandomSection = FMath::RandRange(0, 1); FName SectionName = FName(); switch (RandomSection) { case 0: SectionName = FName("Attack1"); break; case 1: SectionName = FName("Attack2"); break; default: break; } AnimInstance->Montage_JumpToSection(SectionName, AttackMontage); } } ...
공격 애니메이션 겹침문제 해결
위의 과정대로 진행하였을 경우, 마우스 우클릭을 계속하면 애니메이션이 겹쳐서 끊기는 문제가 발생한다.
해결하기 위해 캐릭터의 상태를 추가해주고 특정 상태(공격애니메이션 진행)중일 경우 추가 재생이 불가능하게 한다.
먼저 CharacterStates에 상태를 추가해준다.// CharacterTypes.h ... UENUM(BlueprintType) enum class EActionStates : uint8 { EAS_UnOccupied UMETA(DisplayName = "UnOccupied"), EAS_Attack UMETA(DisplayName = "Attack") }
이어서 헤더파일에 캐릭터의 액션 상태에 대한 값을 가질 ActionState를 추가해준다.
// WraithCharacter.h ... private: // 캐릭터 액션 상태 UPROPERTY(VisibleAnywhere) EActionStates ActionState = EAS_UnOccupied; 이어서 캐릭터가 공격할때 조건을 달아준다. ```cpp // WraithCharacter.cpp ... void AWraithCharacter::Attack() { if(ActionState = EActionStateS::EAS_UnOccupied) { PlayAttackMontage(); ActionState = EActionStates::EAS_Attacking; } }
여기까지 진행하면 공격 도중에 캐릭터가 다시 공격하는 것을 방지할 수 있다.
이제 공격이 끝나면 다시 공격이 가능한 상태로 바꿔야 한다.
먼저 Animation Montage에 AttackEnd notify를 추가한다.
이어서 캐릭터 헤더파일에 함수를 추가한다.... protected: // 공격이 끝나면 실행될 함수 UFUNCTION(BlueprintCallable) void AttackEnd(); ...
캐릭터 cpp파일도 작성한다.
void AWraithCharacter::AttackEnd() { ActionState = EActionStates::EAS_UnOccupied; }
캐릭터 애니메이션 블루프린트에서
AttackEnd
를 검색하면 위와 같이AnimNotify_AttackEnd
노드를 생성할 수 있다. 해당 notify에 도달할 경우 캐릭터 상태가 UnOccupied로 바뀌도록 한다.
WraithCharacter가 유효한지 검사하고, 유효할 경우 AttackEnd가 실행되도록 하면 깔끔하게 공격 애니메이션을 계속 재생할 수 있다.
추가로 무기를 장착하지 않았을 경우 공격하지 못하도록 수정해야한다.
우선 헤더파일에 공격 가능 여부를 파악할 수 있는 함수를 선언한다.// WraithCharacter.h ... protected: bool CanAttack(); ...
cpp파일에도 코드를 추가한다.
// WraithCharacter.cpp if (CanAttack()) { ... } // 공격 가능 여부 확인 함수 bool AWraithCharacter::CanAttack() { return ActionState == EActionStates::EAS_UnOccupied && CharacterState != ECharacterStates::ECS_UnEquipped; } ...
이제 무기 장착을 하지 않았을 경우 캐릭터가 공격 애니메이션을 재생하지 못한다.
움직일 때 공격애니메이션 재생x
움직임 관련 함수 실행함수에서 캐릭터의
ActionStates
가EAC_Unoccupied
일 때 실행되도록 변경하면 된다.void AWraithCharacter::MoveForward(float Value) { // 공격중인 상태일 경우 return으로 함수를 끝냄 if(ActionState == EActionStates::EAS_Attack) return; if((Controller != nullptr) && (Value != 0.f)) { ... } }
무장 및 무장해제
무장 애니메이션과 무장해제 애니메이션으로
AM_Arming
AnimMonatge를 생성한다.
섹션을 나눠주고,Clear
를 눌러 연결을 끊어준다.
그리고 코드를 수정한다.// AwraithCharacter.h ... class AWeapon; ... protected: ... // 무장해제 가능 여부 리턴 bool CanDisArm(); // 무장 가능 여부 리턴 bool CanArm(); // ArmingMontage 플레이 함수 void PlayArmingMontage(FName Section); ... private: // 무장, 무장해재 AnimMontage UPROPERTY(EditDefaultOnly, Category = "Montage") UAnimMontage* ArmingMontage; // 캐릭터가 장착중인 무기 UPROPERTY(VisibleAnywhere, Category "Weapon") AWeapon* EquippedWeapon; ...
cpp 파일도 수정한다.
// AWraithCharacter.cpp void AWraithCharacter::PickUp() { // OverlappingItem을 Weapon으로 캐스팅 AWeapon* OverlappingWeapon = Cast<AWeapon>(OverlappingItem); if (OverlappingWeapon) { OverlappingWeapon->EquipWeapon(GetMesh(), FName("RightHandSocket")); CharacterState = ECharacterStates::ECS_EquippedOneHandedWeapon; /* * 추가된 코드 */ // nullptr할당해서 이미 잡은 아이템 다시 잡는 것 방지(새아이템만 겹칠때 EquipWeapon) OverlappingItem = nullptr; // 장착중인 무기에 오버랩된 무기 할당 Equippedweapon = Overlappingweapon; } /* * 추가된 코드 */ else { // 무장해제 가능 여부 : 캐릭터가 공격하지 않는 상태 + 캐릭터가 무기를 가지고 있는 상태 if(CanDisArm()) { // DisArming section에 있는 애니메이션 재생 PlayArmingMontage(FName("DisArming")); // 캐릭터 상태 변경 CharacterState = ECharacterStates::ECS_UnEquipped; } else if(CanArm()) { // Arming Section에 있는 애니메이션 재생 PlayArmingMontage(FName("Arming")); // 캐릭터 상태 변경 CharacterState = ECharacterStates::ECS_EquippedOneHandWeapon; } } ... // 무장해제 가능 여부 리턴 bool AWraithCharacter::CanDisArm() { // 공격하지 않는 상태 + 무기를 장착한 상태 return ActionState == EActionStates::EAS_Unoccupied && CharacterState != ECharacterState::ECS_UnEquipped; } // 무장 가능 여부 리턴 bool AWraithCharacter::CanArm() { // 공격하지 않는 상태 + 무기를 장착하지 않은 상태 + 무기를 가지고 있는 상태 return ActionState == EActionStates::EAS_UnOccupied && CharacterState == ECharacterStates::ECS_UnEquipped && EquippedWeapon; } // ArmingMontage 플레이 함수 void AWraithCharacter::PlayArmingMontage(FName Section) { UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance; if(AnimInstnace && ArmingMontage) { AnimInstnace->Montage_Play(ArmingMontage); AnimInstance->Montage_JumpToSection(Section, ArmingMontage); } }
마지막으로 Character 블루프린트의 motage쪽에 AM_ArmingMontage를 적용시키면 캐릭터가 무장해제와 재무장이 가능하다.
무장 해제시 등에 칼 꼽기
무장해제시 칼을 등에 꼽아두는 것을 구현한다.
먼저 칼을 부착할 소켓을 생성한다.
우클릭후Add Preview Asset
을 클릭하고 칼 에셋을 추가해준 뒤 소켓 transform을 조정한다(우측의Preveiw Scene Settings
에서 애니메이션을 재생시켜 위치 파악 가능,RightHandSocket
에도 프리뷰 에셋 설정해두면 포지셔닝하기 쉬움).
이제AM_ArmingMontage
로 돌아가 무장과 무장해제를 알려줄 Notify를 생성한다.
cpp파일로 돌아가 기존에 Equip함수에 있던 부분을 재활용하기 위해Extract Function
기능을 이용해서 함수로 만들고 사용한다.// AWeapon.cpp void AWeapon::EquipWeapon(USceneComponent* InParent, FName InSocketName) { /* 여기서부터 FAttachmentTransformRules TransformRules(EAttachmentRule::SnapToTarget, true); ItemMesh->AttachToComponent(InParent, TransformRules, InSocketName); */ //여기까지 드래그->우클릭->빠른 작업 및 리팩터링->함수 추출->함수명: AttachMeshToSocket AttachMeshTosocket(InParent, InSocketName); // Item 클래스에서 protected에 선언되어있음. 파생클래스이므로 사용가능 ItemState = EItemStates::EIS_Equipped; }
Socket에 Mesh를 부착하는 함수를 따로 정의했으므로 이제 캐릭터에서 호출하여 사용할 수 있다.
// AWraithCharacter.h ... protected: // 무장해제시 SpineWeaponSocket에 무기 부착 // UFUNTION(BlueprintCallable) void DetachWeaponToSpine(); // 무장시 RightHandSocket에 무기 부착 void AttachWeaponToHand(); ...
cpp파일도 수정한다.
// AWraithCharacter.cpp ... // 무장해제시 SpineWeaponSocket에 무기 부착 void AWraithCharacter::DetachWeaponToSpine() { if(EquippedWeapon) { EquippedWeapon->AttachMeshToSocket(GetMesh(), FName("SpineWeaponSocket")); } } // 무장시 RightHandSocket에 무기 부착 void AWraithCharacter::AttachWeaponToHand() { if(EquippedWeapon) { EquippedWeapon->AttachMeshComSocket(GetMesh(), FName("RightHandSocket")); } } ...
Animation Blueprint
의Event Graph
에서 설정해둔 notify를 생성하고, WraithCharacter의 유효성 검사를 한 뒤 DetachWeaponToSpine함수를 연결한다(Get WraithCharacter
노드를 우클릭하고Convert to Validated Get
을 누르면 유효성 검사를 노드 자체에서 실행함).
컴파일 후 실행하면 무장해제시 검을 등 뒤에 붙이는 것을 확인할 수 있다.
문제점이 하나 있는데, 달리는 도중에 무장해제와 무장을 시도하면 움직이는 상태에서 무장해제와 무장 애니메이션이 재생되는 부자연스러운 연출이 발생한다.
EActionStates에 무장해제/무장관련 상태를 추가하고, 움직임 관련 함수에서 해당상태일때 움직이지 못하도록 코드를 수정해야한다.// CharacterTypes.h ... UENUM(BlueprintType) enum class EActionStates : uint8 { ... EAS_DetachOrAttachWeapon UMETA(DisplayName = "DetachOrAttachWeapon") };
움직임 관련 코드도 수정한다.
// AWraithCharacter.h // 무장해제 및 무장 종료시 ActionState 변경을 위한 함수 UFUNCTION(BlueprintCallable) void FinishADetachOrAttachWeapon();
cpp 파일도 수정해준다.
// AWraithCharacter.cpp void AWraithCharacter::PickUp() { ... else { if(CanDisArm()) { ... ActionState = EActionStates::EAS_DetachOrAttachWeapon; } else if(CanArm()) { ... ActionState = EActionStates::EAS_DetachOrAttachWeapon; } } } ... // 무장해제 및 무장 끝나면 ActionState 변경을 위한 함수 void AWraithCharacter::FinishDetachOrAttachWeapon() { ActionState = EActionStates::UnOccupied; } ... void MoveForward(float Value) { if(ActionState != EAS_UnOccupied) return; ... }
AM_ArmingMontage에서 무장해제와 무장이 끝나는 지점에 Notify를 추가해준다.
Animation Blueprint
의EventGraph
로 돌아가서 해당 노티파이에 도달하면 ActionState를 변경하도록 노드를 연결해준다.
무장해제와 무장시 사운드 추가부터는 메타사운드에 작성.