언리얼 엔진 네트워크 FPS 게임 개발 일지#3 무기 반동

김진우·2022년 6월 11일
0

반동 애니메이션

발로란트에서는 무기 반동 애니메이션이 3가지로 나뉜다. 첫 번째는 총을 쏘기 시작할 때 총기가 들어올려지는 것, 두 번째는 발사를 종료했을 때 총기가 내려오는 것, 마지막으로 반동 중 총기가 좌우로 흔들리는 것이다.

반동 시작 애니메이션이다. 자세히 보면 총기를 발사하기 시작할 때 총이 위로 천천히 올라가는 모습을 확인할 수 있다. 최대 높이로 총의 각도가 올라간 후 좌우로 움직이는 것을 확인할 수 있다.

반동 종료 애니메이션이다. 자세히 보면 발사를 종료한 후 총기가 빠르게 아래로 내려가는 모습을 확인할 수 있다.


현재 프로젝트에는 무기를 발사할 때 반동 애니메이션만 있고, 시작과 종료 애니메이션이 존재하지 않는다. 애니메이션을 추가한 다음 에임이 흔들리는 실제 반동도 구현해야 할 필요성이 있다.

무기 발사 애니메이션

발사 애니메이션은 지금까지 총을 단순히 뒤로 후퇴하는 몽타주를 사용했다. 하지만 발로란트에서는 총을 한 번 발사하더라도 다양한 애니메이션이 복합적으로 재생되기 때문에, 이것을 개선할 필요가 있다.

CameraShakes 기능을 활용한 반동 구현(보류)

CameraShakes 기능은 특정한 패턴으로 카메라를 흔드는 기능이다. 총을 발사했을 때 이 기능을 활용해서 카메라 각도를 변경해준다면 반동을 구현할 수 있을 것이라고 생각했다.
아래는 관련 자료를 찾던 중 발견한 글이다. 나와 같이 무기 반동 시스템을 구현하기 위해 질문한 사람이 레딧에 쓴 것이다.

CameraShakes 기능은 카메라를 실제로 움직이는 것이 아니기 때문에 마우스 커서가 움직이지 않는다고 한다. 카메라의 회전값을 이용한 기능을 만들 때 문제가 생길 수 있었다.

반동 컴포넌트 추가

초기 아이디어 (실패)

결론부터 말하자면 초기 아이디어는 실패했다. 아래는 어떤 방식으로 시도했는가에 대한 기록이다.

ASceneComponent를 상속받는 ARecoilComponent를 만든 다음 카메라의 부모 컴포넌트로 추가했다. 총을 쐈을 때 반동 수치만큼 ASceneComponent의 회전 Y값을 증가시키면 카메라가 위로 올라가는 효과를 얻을 수 있다.

발로란트 인게임에서 플레이어에게 제공하는 무기 반동에 관한 데이터는 다음과 같다.

  • 반동 데이터 배열
    • 카메라 반동 각도 : FVector
    • 카메라 반동 오차 : FVector
    • 총알 오프셋 각도 : FVector2D
    • 총알 오프셋 오차 : FVector2D
    • 반동 회복 시간 : float
  • 반동 인덱스 : int
  • 현재 반동 회복 시간 : float

반동 시스템의 핵심은 반동 배열의 인덱스를 정하는 기준이다. 총알을 쏠 때마다 반동 인덱스 값을 증가시키고, 기준에 따라 인덱스 값을 감소시키는 방법이 필요하다. 연발로 총을 쏠 때는 반동 데이터 배열을 순서대로 적용시키면 되지만, 단발을 빠르게 쏠 때는 회복과 반동을 동시에 적용하는 것이 필요하기 때문이다.

반동 인덱스의 조건을 정리해봤다.
1. 현재 반동 회복 시간이 0 이상일 때 총을 쏘면 반동 인덱스를 1 증가시킨다.
2. 현재 반동 회복 시간은 지나간 시간만큼 감소한다.
3. 현재 반동 회복 시간이 반동 데이터 배열 [i-1]의 반동 회복 시간보다 낮으면 반동 인덱스를 i로 설정한다.

반동 인덱스가 배열의 길이를 넘어서는 지점에서도 적용할 반동 데이터가 있어야 한다. 발로란트의 경우는 반동의 최좌상단과 최우상단을 0.5초 ~ 1초 간격으로 왕복하며 반동을 유지한다.

위 기능을 실제로 구현하고 테스트했지만 부모 컴포넌트의 Rotation 값이 변해도 카메라 컴포넌트에 영향을 주지 못했다. 또, 만약 부모 컴포넌트 Rotation 값의 영향을 카메라 컴포넌트가 받았다고 하더라도 비연속적인 값을 사용해서 반동을 구현한다면 부드럽지 않고 뚝뚝 끊기는 반동이 만들어질 것이다. 연속적인 Curve를 사용하면서 카메라 컴포넌트의 회전값에 영향을 줄 수 있는 방법을 찾아야한다.

아이디어 수정 및 구체화

UE 포럼에 올라온 질문글 CameraComponent relative rotation overwritten by CapsuleComponent parent에 올라온 답변글에 따르면 카메라 컴포넌트의 Use Pawn Control Rotation 옵션이 켜져있을 때 카메라 컴포넌트는 부모의 Rotation 값에 영향을 받지 않는다고 한다. Use Pawn Control Rotation 옵션을 해제하고 컨트롤러의 회전 입력에 따라서 액터의 Yaw와 카메라의 Pitch 값을 조절하는 코드를 삽입해야 한다. 추가로 그럴 경우 BodyMeshComponent의 AimOffset 기능을 다시 만들어야 한다. 반동 기능을 완성한 다음 어떻게 AimOffset을 만드는지 글을 올릴 예정이다.

Timeline 이라는 기능을 활용해 반동 애니메이션을 구현할 수 있다. Curve를 생성하고 콜백 객체에 연결한 다음 타임라인에 등록해주면 시간이 지남에 따라 변화하는 Curve값을 가져올 수 있다. 이 값을 반동 컴포넌트의 회전 값으로 설정해준다면 반동을 구현할 수 있을 것이다.

WeaponBase.H

	UPROPERTY(EditDefaultsOnly, Category = "properties")
	float RecoilRecoveryTime;

	float CurrentRecoilRecoveryTime;

	UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Timeline", Meta = (AllowPrivateAccess = "true"))
	UCurveVector* CameraRecoilCurve;

	UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Timeline", Meta = (AllowPrivateAccess = "true"))
	UCurveVector* BulletRecoilCurve;

	FTimeline RecoilTimeline;
    
public:
	UFUNCTION()
	void OnCameraRecoilProgress(FVector CameraRecoil);
	
	UFUNCTION()
	void OnBulletRecoilProgress(FVector BulletRecoil);

	UFUNCTION()
	void OnRecoilTimelineFinish();
WeaponBase.CPP

void AWeaponBase::BeginPlay()
{
	Super::BeginPlay();

	InitializeRecoilTimeline();
}

void AWeaponBase::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	RecoilTimeline.TickTimeline(DeltaTime);

	//Recover recoil
	if (!RecoilTimeline.IsPlaying() && 0.f <= CurrentRecoilRecoveryTime)
	{
		CurrentRecoilRecoveryTime -= DeltaTime;
		float ReducedTimelinePlayback = RecoilTimeline.GetPlaybackPosition() * (CurrentRecoilRecoveryTime/RecoilRecoveryTime);
		RecoilTimeline.SetPlaybackPosition(ReducedTimelinePlayback, false);
	}
}

void AWeaponBase::InitializeRecoilTimeline()
{
	if (CameraRecoilCurve == nullptr || BulletRecoilCurve == nullptr)
	{
		return;
	}

	FOnTimelineVector CameraRecoilCallback;
	FOnTimelineVector BulletRecoilCallback;

	FOnTimelineEventStatic TimelineFinishCallback;

	// `FName("함수 이름")` 함수 이름에 해당하는 함수가 실행됨
	CameraRecoilCallback.BindUFunction(this, FName("OnCameraRecoilProgress"));
	BulletRecoilCallback.BindUFunction(this, FName("OnBulletRecoilProgress"));
	TimelineFinishCallback.BindUFunction(this, FName("OnRecoilTimelineFinish"));

	RecoilTimeline.AddInterpVector(CameraRecoilCurve, CameraRecoilCallback);
	RecoilTimeline.AddInterpVector(BulletRecoilCurve, BulletRecoilCallback);
	RecoilTimeline.SetTimelineFinishedFunc(TimelineFinishCallback);
}

void AWeaponBase::OnCameraRecoilProgress(FVector CameraRecoil)
{
//Do Something
}

void AWeaponBase::OnBulletRecoilProgress(FVector BulletRecoil)
{
//Do Something
}

void AWeaponBase::OnRecoilTimelineFinish()
{
	CurrentRecoilRecoveryTime = RecoilRecoveryTime;
//Do something
}

CameraRecoilComponent를 옵저버패턴으로 추가한 다음 이벤트를 실행하는 코드를 콜백 함수에 넣으면 완성이다.

  1. 카메라 반동만 우선 적용한 화면

  2. 카메라 반동에 총알 반동을 적용한 화면

다음 글은 반동의 구간 반복과 탄퍼짐을 구현하는 것이다.

profile
게임 개발자입니다.

0개의 댓글