장전(Reload) 시스템 및 애니메이션까지 구현해보는 내용입니다.
1. Input Action Mapping 추가
장전(Reload) 시스템을 만들기 위해서 Input Action Mapping에 키를 추가한다.
Action Mapping을 추가하였다면 PlayerCharacter 스크립트에서 바인딩을 해준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// header
class TPS_PROTOTYPE_API APlayerCharacter : public ACharacter
{
GENERATED_BODY()
// 중간 생략
public:
void StartReload();
};
// cpp
void APlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// 중간 생략
PlayerInputComponent->BindAction("Reload", IE_Pressed, this, &APlayerCharacter::StartReload);
}
2. Reload Event Delegate 생성
StartReload
메소드를 구현하기 위해 Delegate를 사용하였다.
Delegate를 사용한 이유는
장전을 시작할 때 UI, 사운드, 애니메이션 등 다양한 행위들이 작동해야 하는데 이를 Delegate를 이용하여 CallBack 함수로 사용하기 위함과 Delegate를 사용하면 여러 이벤트들을 넣고 빼고가 쉽게 가능하기 때문이다.
Delegate를 사용하기 위해 header파일에 아래와 같이 Delegate를 선언해준다.
Dynamic Delegate를 사용한 이유는 C++ 뿐만 아니라 블루프린트에서도 사용하기 위함이다.
다이내믹 델리게이트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 멀티 캐스트가 가능한 델리게이트
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FDele_Dynamic);
UCLASS()
class TPS_PROTOTYPE_API APlayerCharacter : public ACharacter
{
// ....
public:
UPROPERTY(VisibleAnywhere, BlueprintAssignable, Category = "Event")
FDele_Dynamic FDele_Start_Reload;
UFUNCTION()
void StartReload();
}
FDele_Start_Reload
를 이용하여 StartReload()
를 구현해준다.
1
2
3
4
5
6
7
void APlayerCharacter::StartReload()
{
if (FDele_Start_Reload.IsBound())
{
FDele_Start_Reload.Broadcast();
}
}
이번에는 장전이 끝났을 때 처리를 담당하는 FinishReload()
메소드를 구현해주자.
메소드 정의는 아직 작성할 내용이 없음으로 공백으로 둔다.
5번
내용에서 메소드를 정의합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
UCLASS()
class TPS_PROTOTYPE_API APlayerCharacter : public ACharacter
{
// ....
public:
UFUNCTION()
void FinishReload() const;
}
void APlayerCharacter::FinishReload() const
{
}
3. Reload Animation 구현
AnimInstance에 StartReloading() 메소드 추가하기
애니메이션에 장전 모션을 넣기 위해서는 애니메이션 트랜지션 룰(Transition Rules)에 사용할 boolean 변수를 생성해줘야 한다.
그리고 장전이 시작됨을 알리는 메소드도 추가해준다.
1
2
3
4
5
6
7
8
9
10
11
12
class TPS_PROTOTYPE_API UCharacterAnimInstance : public UAnimInstance
{
GENERATED_BODY()
// 중간 생략
private:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Reload Properties", meta = (AllowPrivateAccess = "true"))
bool bIsReload;
/** 장전이 시작될 때 호출 */
UFUNCTION()
void StartReloading();
1
2
3
4
void UCharacterAnimInstance::StartReloading()
{
bIsReload = true;
}
StartReloading()
메소드를 위에서 만든 Delegate에 연결시켜준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void UCharacterAnimInstance::NativeInitializeAnimation()
{
Super::NativeInitializeAnimation();
if (PlayerPawn == nullptr)
PlayerPawn = TryGetPawnOwner();
if (PlayerPawn)
PlayerCharacter = Cast<APlayerCharacter>(PlayerPawn);
if (PlayerCharacter)
{
// StartReloading() 메소드를 Delegate에 바인딩해주었다.
PlayerCharacter->FDele_Start_Reload.AddDynamic(this, &UCharacterAnimInstance::StartReloading);
}
}
이를 통해 APlayerCharacter::StartReload()
를 호출하면 Delegate를 통해 UCharacterAnimInstance::StartReloading
도 호출된다.
AnimInstance에 EndReloading() 메소드 추가하기
이번에는 장전이 끝날 때 처리를 추가해준다.
장전이 끝나는 건 장전 애니메이션이 종료되었을 때이기 때문에 블루프린트에서 호출할 수 있도록 BlueprintCallable
를 추가하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// header
UFUNCTION(BlueprintCallable, Category = "Reloading")
void EndReloading();
// cpp
void UCharacterAnimInstance::EndReloading()
{
bIsReload = false;
if (PlayerCharacter)
{
PlayerCharacter->FinishReload();
}
}
Animation Blueprint에 Reload 추가하기
여러 삽질(?)을 통해 구현한 내용입니다. 그러므로 올바른 구현 방법이 아닐 수 있습니다.
더 좋은 방법이 있다면 댓글로 남겨주세요.
이미지는 클릭하면 크게 보실 수 있습니다.
트랜지션에 사용할 boolean 변수도 만들었으니 이제 Animation Blueprint에 Reload 애니메이션을 추가해보자.
추가하는 방법은 아래와 같다.
- Reload State Machine 생성 및 캐싱하기
- 아래와 같이 Reload State Machine 구현하기
- 기존 State Machine과 Reload State Machine을 블렌딩하기
본 별로 레이어를 블렌딩
하는 State를 아래와 같이 세팅한다.
현재 사용하고 있는 캐릭터 스켈레톤에서spine_01
은 아래와 같다.
- 장전 애니메이션 시퀀스를 연 뒤, 마지막 프레임에
End_Reload
노티파이를 추가해준다.
- Animation Blueprint Event Graph에
End_Reload
노티파이를 추가한 후EndReloading()
메소드를 연결해준다.
4. 1-3까지 작업 결과
1부터 3까지 완료하였다면 아래와 같이 정상적으로 애니메이션이 작동되는 것을 볼 수 있다.
현재까지 작업으로는 유저가 R
키를 눌러 직접 장전을 했을 때만 장전이 완료됨으로
총알이 다 떨어졌을 때 자동으로 장전되는 기능과 총알이 충전되는 기능도 추가해주자.
5. 총알이 0개일 때 자동 장전 기능 추가
아래 글을 기반으로 구현되어 있습니다.
[UE5 TPS 제작기] 12. Line Trace를 이용한 총 쏘기 구현
AAssaultRifle 클래스 수정
총을 발사하는 기능을 담당하는
AAssaultRifle::FireWithLineTrace(APlayerCharacter* owner)
AAssaultRifle::FireWithProjectile(APlayerCharacter* owner)
메소드에 총알이 떨어졌을 때 바로 장전이 가능하도록 코드를 추가해주자.
예시로 AAssaultRifle::FireWithLineTrace(APlayerCharacter* owner)
를 살펴보면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void AAssaultRifle::FireWithLineTrace(APlayerCharacter* owner)
{
if (_ammoRemainCount <= 0)
{
StopFire();
return;
}
if (owner)
{
const AController* ownerController = owner->GetController();
if (ownerController)
{
const FVector start = _skeletalMeshComponent->GetSocketLocation("FireSocket");
const FVector end = (ownerController->GetControlRotation().Vector() * _traceDistance) + start;
FHitResult hitResult;
FCollisionQueryParams collisionParams;
collisionParams.AddIgnoredActor(this);
const UWorld* currentWorld = GetWorld();
if (currentWorld)
{
DrawDebugLine(currentWorld, start, end, FColor::Red, false, 1.0f);
if (currentWorld->LineTraceSingleByChannel(
hitResult,
start,
end,
ECC_Visibility,
collisionParams))
{
if (hitResult.GetActor())
{
auto* hitActor = hitResult.GetActor();
GEngine->AddOnScreenDebugMessage(-1, 1.f, FColor::Red, FString::Printf(TEXT("Hit Actor Name: %s"), *hitActor->GetName()));
}
}
--_ammoRemainCount;
GEngine->AddOnScreenDebugMessage(-1, 1.f, FColor::Blue, FString::Printf(TEXT("Ammo Remain: %d"), _ammoRemainCount));
// 남은 탄약이 0개일 때 APlayerCharacter::StartReload() 를 호출한다.
if (_ammoRemainCount == 0)
owner->StartReload();
}
}
}
}
이를 통해 총알을 전부 소진하면 자동으로 장전을 진행하게 된다.
장전이 끝날 때 총알 개수 리셋시키기
장전이 모두 끝날 때 모든 탄약이 충전될 수 있도록 FinsihReloading()
메소드를 추가해주자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
UCLASS()
class TPS_PROTOTYPE_API AAssaultRifle : public AWeapon
{
GENERATED_BODY()
private:
FTimerHandle FireTimerHandle;
void FireWithLineTrace(class APlayerCharacter* owner);
void FireWithProjectile(class APlayerCharacter* owner);
public:
explicit AAssaultRifle();
virtual void StartFire(class APlayerCharacter* owner) override;
virtual void StopFire() override;
// 장전이 끝날 때 처리를 담당하는 메소드 추가
virtual void FinishReloading() override;
};
// cpp file
void AAssaultRifle::FinishReloading()
{
_ammoRemainCount = _ammoMaxCount;
}
장전이 끝날 때의 처리는 APlayerCharacter::FinishReload()
를 통해서 처리되기 때문에 FinishReload()
메소드를 아래와 같이 메소드를 정의해주자.
StartReloading()
메소드와 다르게 Delegate 처리를 하지 않았는데 이유는
현재 장착한 무기의 정보를 가지고 있는 변수(_equipWeapon)가 있고 그 총에서만 Reload 기능이 작동되어야 하기 때문입니다.만약, Delegate를 사용했다면 장전 때마다 바인딩된 모든 총이 장전 될 수 있기에 장착 중인 총이 바뀔 때마다 바인딩을 해체시켜줘야 할 수 있습니다.
1
2
3
4
5
6
7
void APlayerCharacter::FinishReload() const
{
if (_equipWeapon)
{
_equipWeapon->FinishReloading();
}
}
6. 결과
5번
내용 작업 결과로 총알을 모두 소진하면 자동으로 장전하는 것을 확인할 수 있다.