언리얼4 멀티플레이 슈팅게임 캐릭터 구성

Intro

  • 멀티플레이 슈팅게임 캐릭터 구성

캐릭터 개요

3인칭과 1인칭(조준)을 사용하는 멀티플레이 캐릭터 구성하기

생성자

GetCharacterMovement()->JumpZVelocity = 350.0f;
GetCharacterMovement()->AirControl = 0.2f;
GetCharacterMovement()->MaxWalkSpeed = 300.0f;
GetCharacterMovement()->bUseControllerDesiredRotation = false;
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f);
bUseControllerRotationYaw = false;

// 무기 메쉬
WeaponMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Weapon"));
WeaponMesh->SetupAttachment(GetMesh(), TEXT("Weapon_Socket"));

// 스프링암 생성
TPSpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("TPSpringArm"));
TPSpringArm->SetupAttachment(RootComponent);
TPSpringArm->TargetArmLength = 300.0f;
TPSpringArm->bUsePawnControlRotation = true;
TPSpringArm->bInheritPitch = true;
TPSpringArm->bInheritRoll = true;
TPSpringArm->bInheritYaw = true;
TPSpringArm->bDoCollisionTest = true;

// 총구
Sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere"));
Sphere->SetupAttachment(WeaponMesh);


// 카메라 생성
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(TPSpringArm, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;

FPCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FPCamera"));
FPCamera->SetupAttachment(TPSpringArm, USpringArmComponent::SocketName);
FPCamera->bUsePawnControlRotation = false;

WeaponCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("WeaponCamera"));
WeaponCamera->SetupAttachment(Sphere);
WeaponCamera->bUsePawnControlRotation = true;

// 오디오 컴포넌트
PlayerAudio = CreateDefaultSubobject<UAudioComponent>(TEXT("PlayerAudio"));
PlayerAudio->SetupAttachment(GetMesh());

파생 블루프린트 생성

1

파생 블루프린트 캐릭터를 생성하고 캐릭터 메쉬 무기 메쉬 카메라 위치등을 조절해준다.

애니메이션 블루프린트

AnimInstance 클래스

void UMPPlayerAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
	auto Player = Cast<AMPPlayer>(TryGetPawnOwner());
	
	if (::IsValid(Player))
	{
		CurrentPawnSpeed = Player->GetVelocity().Size();
		IsInAir = Player->GetMovementComponent()->IsFalling();
		IsCrouch = Player->bIsCrouched;
		IsProne = Player->IsProne;
		IsAiming = Player->IsAiming;
		ControllerPitch = Player->WraistPitch;

		if (IsAiming)
		{
			ControllerPitch = Player->WraistPitch;
		}
		else
		{
			ControllerPitch = 0.0f;
		}
	}
}

NativeupdateAnimation을 통해 애니메이션에 필요한 변수들을 업데이트해준다.

스테이트 머신 구성

2

캐릭터 상태변수들을 이용해 스테이트 머신을 구성한다.

애님 그래프

3

FABRIK을 이용해 무기를 잡는 손위치를 조절해주고 조준상태일때 Pitch에 따라 상체를 움직이도록 구성한다음 캐시에 포즈를 저장

저장한 포즈와 몽타주 애니메이션(총 발사)가 적절히 블렌드되도록 구성한다.

캐릭터 기능

조준시 캐릭터 상체 조절

void AMPPlayer::AddControllerPitchInput(float Val)
{
	if (Val != 0.f && Controller && Controller->IsLocalPlayerController())
	{
		APlayerController* const PC = CastChecked<APlayerController>(Controller);
		PC->AddPitchInput(Val);
	}
	WraistPitch = GetControlRotation().Pitch;
	if(FMath::Abs(WraistPitch - PreviousWraistPitch)>1)
	{
		WraistPitchServer(WraistPitch);
		PreviousWraistPitch = WraistPitch;
	}
	
}

void AMPPlayer::WraistPitchServer_Implementation(float pitch)
{
	WraistPitch = pitch;
}

bool AMPPlayer::WraistPitchServer_Validate(float pitch)
{
	return true;
}

Pitch값을 애니메이션 인스턴스로 전달해 상체의 Roll값을 회전시켜주며 현재 Pitch값과 저장된 이전 Pitch값의 차이(절댓값)가 1이상 벌어지면 서버에서 실행하여 상대방에게도 전달한다.

이때 차이를 크게할수록 서버에서 실행되는 빈도수가 낮아지는 대신 상대방의 화면에선 부드럽게 상체가 움직이지않는다.

GIF

달리기

이전 글과 같은 방식으로 구현

멀티플레이 달리기

앉기/눕기

//////////// Crouch ///////////////////////
void AMPPlayer::CrouchServer_Implementation()
{
	CrouchMulticast();
}

bool AMPPlayer::CrouchServer_Validate()
{
	return true;
}

void AMPPlayer::CrouchMulticast_Implementation()
{
	if (!IsSprint)
	{
		if (IsProne)
		{
			GetCharacterMovement()->MaxWalkSpeedCrouched = 0;
			IsCrouch = true;
			IsProne = false;
			GetWorld()->GetTimerManager().SetTimer(timer, this, &AMPPlayer::SetCrouchMovement, 1.3f, false);
			
		}
		else
		{
			if (!IsCrouch)
			{
				Crouch();
				IsCrouch = true;
				
			}
			else
			{
				UnCrouch();
				IsCrouch = false;
				
			}

		}
	}
}


//////////// Prone ///////////////////////


void AMPPlayer::ProneServer_Implementation()
{
	ProneMulticast();
}

bool AMPPlayer::ProneServer_Validate()
{
	return true;
}

void AMPPlayer::ProneMulticast_Implementation()
{
	if (!IsProne && !IsSprint && IsCrouch)
	{
		GetCharacterMovement()->MaxWalkSpeedCrouched = 0;
		IsProne = true;
	
		GetWorld()->GetTimerManager().SetTimer(timer, this, &AMPPlayer::SetProneMovement, 1.3f, false);
	}
}

앉기는 C키 눕기는 앉은상태에서 Z키를 눌렀을때 실행되도록 했으며 타이머를 통해 애니메이션이 끝나면 해당 상태의 Movement를 세팅한다.

void AMPPlayer::SetCrouchMovement()
{
	GetCharacterMovement()->MaxWalkSpeedCrouched = 200.0f;
	GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f);
}

void AMPPlayer::SetProneMovement()
{
	GetCharacterMovement()->MaxWalkSpeedCrouched = 70.0f;
	GetCharacterMovement()->RotationRate = FRotator(0.0f, 270.0f, 0.0f);
}

실행결과

gif4

무기 조준

////////Aiming ///////////////////////

void AMPPlayer::Aiming()
{
	if (!IsAiming)
	{
		AimingServer(true); 
		IsAiming = true;
		
		GetCharacterMovement()->RotationRate = FRotator(0.0f, 270.0f, 0.0f);
		GetCharacterMovement()->bUseControllerDesiredRotation = true;
		GetCharacterMovement()->bOrientRotationToMovement = false;
		ScopeWidget->SetVisibility(ESlateVisibility::Visible);
		FollowCamera->Deactivate();
		WeaponCamera->Activate();

		PlayerAudio->SetSound(AimCue);
		PlayerAudio->Play();
	}
	else
	{
		AimingServer(false);
		IsAiming = false;
	
		
		GetCharacterMovement()->bUseControllerDesiredRotation = false;
		GetCharacterMovement()->bOrientRotationToMovement = true;
		ScopeWidget->SetVisibility(ESlateVisibility::Hidden);
		WeaponCamera->Deactivate();
		FollowCamera->Activate();
	
		if (IsProne)
		{
			GetCharacterMovement()->RotationRate = FRotator(0.0f, 270.0f, 0.0f);
		}
		else
		{
			GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f);
		}
	}
}

void AMPPlayer::AimingServer_Implementation(bool Aiming)
{
	IsAiming = Aiming;
	if (IsAiming)
	{
		GetCharacterMovement()->bUseControllerDesiredRotation = true;
		GetCharacterMovement()->bOrientRotationToMovement = false;
	}
	else
	{
		GetCharacterMovement()->bUseControllerDesiredRotation = false;
		GetCharacterMovement()->bOrientRotationToMovement = true;
	}
}

bool AMPPlayer::AimingServer_Validate(bool Aiming)
{
	return true;
}

IsAiming 값을 서버에서 실행되는 함수에도 전달하여 다른 클라이언트에서도 조준하는 상태가 출력되도록한다.

조준시 bUseControllerDesiredRotation를 true bOrientRotationToMovement false 상태로 만들어 마우스방향으로 캐릭터가 회전하도록 하며 조준경 위젯을 표시하고 현재 카메라를 DeActive시키고 무기에 달려있는 WeaponCamera를 Active시킨다.

이미 조준된 상태에서 다시 입력받으면 원래의 상태로 돌아가도록한다.

실행결과

gif2

gif3

총알 발사

//////// Fire /////////////////////////////

void AMPPlayer::Fire()
{
	bool IsMove = GetVelocity().Size()>0;
	
	if (IsAiming&!IsMove)
	{
		LOG(Warning, TEXT("Fire"));
		OnFire();
		if (IsCrouch)
		{
			if (IsProne)
			{
				PlayerAnim->PlayProneFire();//ProneFire
				ProneFireServer();
			}
			else
			{
				PlayerAnim->PlayCrouchFire();//CrouchFire
				CrouchFireServer();
			}
		}
		else
		{
			PlayerAnim->PlayFireMontage(); //StandFire
			StandFireAnimServer();
		}
	}
}

void AMPPlayer::OnFire()
{
	FVector SphereLocation= Sphere->GetComponentLocation();
	FRotator CameraRotation = WeaponCamera->GetComponentRotation();
	FVector CameraRotated = CameraRotation.RotateVector(FVector(0, 0, 0));
	FVector StartLocation = (SphereLocation + CameraRotated);

	FTransform Transform=UKismetMathLibrary::MakeTransform(StartLocation, CameraRotation,FVector(1,1,1));

	GameStatic->PlaySoundAtLocation(this, ShotCue, StartLocation);

	FireWeapon(Transform);
}

void AMPPlayer::FireWeapon(FTransform trans)
{

	UWorld* const World = GetWorld();
	if (World != nullptr)
	{
		
		World->SpawnActor<AActor>(AmmoClass,trans);
		if (HasAuthority())
		{
			FireWeaponMulticast(AmmoClass, trans);
		}
		else
		{
			FireWeaponServer(AmmoClass, trans);
		}
	}
}

void AMPPlayer::FireWeaponServer_Implementation(TSubclassOf<AActor> Ammo, FTransform trans)
{
	FireWeaponMulticast(AmmoClass, trans);
	UWorld* const World = GetWorld();
	if (World != nullptr)
	{
		World->SpawnActor<AActor>(AmmoClass, trans);
	}
}

bool AMPPlayer::FireWeaponServer_Validate(TSubclassOf<AActor> Ammo, FTransform trans)
{
	return true;
}

void AMPPlayer::FireWeaponMulticast_Implementation(TSubclassOf<AActor> Ammo, FTransform trans)
{
	if (!IsValid(GetController()))
	{
		UWorld* const World = GetWorld();
		if (World != nullptr)
		{
			World->SpawnActor<AActor>(AmmoClass, trans);
		}
	}
	
}

좌클릭으로 Fire 함수가 실행되면 OnFire 함수가 실행되고 자세에 따른 발사 몽타주를 실행시킨다.

OnFire 함수에서는 총알이 발사될 위치(총구(Sphere))와 방향(WeaponCamera)을 통해 Transform을 계산하고 이를 FireWeapon 함수의 파라메터로 전달해준다.

Transform 파라메터를 이용해 총알을 자신의 클라이언트와 상대 클라이언트에 스폰시킨다.

실행결과

gif4

gif5

변수 리플리케이트

생성자

bReplicates = true;

생성자에서 bReplicates를 활성화해준다음

//Property Replicate
void AMPPlayer::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	DOREPLIFETIME(AMPPlayer, IsProne);
	DOREPLIFETIME(AMPPlayer, IsCrouch);
	DOREPLIFETIME(AMPPlayer, IsAiming);
	DOREPLIFETIME(AMPPlayer, IsSprint);
	DOREPLIFETIME(AMPPlayer, WraistPitch);
	
}

리플리케이트해줄 프로퍼티들을 추가해준다.

이때 UPROPETYreplicated를 붙여주어야한다.

댓글남기기