언리얼 엔진 자체가 사실은 언리얼 토너먼트라는 FPS게임을 만들면서 발전되었기 때문에 일인칭 게임을 만드는데 가장 대표적인 템플릿이라고 볼 수있다.
언리얼 프로젝트를 FirstPlayer라는 이름으로 일인칭을 선택해 만들었다.
실행하면 기본적으로 w,a,s,d키로 움직이고 마우스로 시선을 움직이며 좌클릭시 발사하여 공이 나가고 소리와 물체에 부딪칠시 충격도 있고 반동도 있다. 기본적인 슈팅게임의 요소를 다 가지고 있다.
1. 이번엔 FirstPlayerCharacter클래스 파일부터 먼저 보자.
PlayerController가 따로 없이 입력을 받는 부분이 함께 들어가있다.
생성자에서 VR게임인지 여부를 판단해 모션게임인가를 세팅도한다.
// [FirstPlayer]Character.h
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "FirstPlayerCharacter.generated.h"
class UInputComponent;
class USkeletalMeshComponent;
class USceneComponent;
class UCameraComponent;
class UMotionControllerComponent;
class UAnimMontage;
class USoundBase;
UCLASS(config=Game)
class AFirstPlayerCharacter : public ACharacter
{
GENERATED_BODY()
/** Pawn mesh: 1st person view (arms; seen only by self) */
UPROPERTY(VisibleDefaultsOnly, Category=Mesh)
USkeletalMeshComponent* Mesh1P;
/** Gun mesh: 1st person view (seen only by self) */
UPROPERTY(VisibleDefaultsOnly, Category = Mesh)
USkeletalMeshComponent* FP_Gun;
/** Location on gun mesh where projectiles should spawn. */
UPROPERTY(VisibleDefaultsOnly, Category = Mesh)
USceneComponent* FP_MuzzleLocation;
/** Gun mesh: VR view (attached to the VR controller directly, no arm, just the actual gun) */
UPROPERTY(VisibleDefaultsOnly, Category = Mesh)
USkeletalMeshComponent* VR_Gun;
/** Location on VR gun mesh where projectiles should spawn. */
UPROPERTY(VisibleDefaultsOnly, Category = Mesh)
USceneComponent* VR_MuzzleLocation;
/** First person camera */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
UCameraComponent* FirstPersonCameraComponent;
/** Motion controller (right hand) */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
UMotionControllerComponent* R_MotionController;
/** Motion controller (left hand) */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
UMotionControllerComponent* L_MotionController;
public:
AFirstPlayerCharacter();
protected:
virtual void BeginPlay();
public:
/** Base turn rate, in deg/sec. Other scaling may affect final turn rate. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
float BaseTurnRate;
/** Base look up/down rate, in deg/sec. Other scaling may affect final rate. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
float BaseLookUpRate;
/** Gun muzzle's offset from the characters location */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Gameplay)
FVector GunOffset;
/** Projectile class to spawn */
UPROPERTY(EditDefaultsOnly, Category=Projectile)
TSubclassOf<class AFirstPlayerProjectile> ProjectileClass;
/** Sound to play each time we fire */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Gameplay)
USoundBase* FireSound;
/** AnimMontage to play each time we fire */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
UAnimMontage* FireAnimation;
/** Whether to use motion controller location for aiming. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
uint8 bUsingMotionControllers : 1;
//...
}
가지고 있는 컴포넌트들을 보면 VR사용을 위한 모션컨트롤러 외에도 스켈레탈메쉬가 두 개(캐릭터, 무기), 카메라 컴포넌트, 스폰할 발사체 클래스, 사운드컴포넌트, 애니메이션 등을 가지고있다.
입력을 처리하는 SetupPlayerInputComponent도 직접 가지고 있다.
FirstPlayerCharacter블루프린트 클래스도 살펴보자.
슈팅을 할 때 애니메이션과 사운드를 플레이하는 부분이 블루프린트에서 지정되어 있다.
또 일인칭이므로 캐릭터의 스켈레탈메쉬를 보면 팔 모양만 있다. 그리고 블루프린트에서 위 컴포넌트들의 값을 채우고 있다.
발사체인 ProjectileClass를 블루프린트에서 지정해주고 있는데 여러 모양이나 특징마다 클래스를 가지고 바꿔 줄 수도 있다.
-> 캐릭터 블루프린트 클래스를 여럿 만들어 각각 다른 Projectile Class를 지정해줄 수도 있다.
주로 전에 하지 않았던 FirstPlayer만의 특징을 살펴보자.
2. 좌클릭을 했을 때 슈팅되기.
FirstPlayerCharacter의 SetupPlayerInputComponent에서
// Bind fire event
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &AFirstPlayerCharacter::OnFire);
이렇게 액션맵핑되어있는 OnFire함수를 보면
void AFirstPlayerCharacter::OnFire()
{
// try and fire a projectile
if (ProjectileClass != nullptr)
{
UWorld* const World = GetWorld();
if (World != nullptr)
{
if (bUsingMotionControllers)
{
// ...
}
else
{
const FRotator SpawnRotation = GetControlRotation();
// MuzzleOffset is in camera space, so transform it to world space before offsetting from the character location to find the final muzzle position
const FVector SpawnLocation = ((FP_MuzzleLocation != nullptr) ? FP_MuzzleLocation->GetComponentLocation() : GetActorLocation()) + SpawnRotation.RotateVector(GunOffset);
//Set Spawn Collision Handling Override
FActorSpawnParameters ActorSpawnParams;
ActorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding;
// spawn the projectile at the muzzle
World->SpawnActor<AFirstPlayerProjectile>(ProjectileClass, SpawnLocation, SpawnRotation, ActorSpawnParams);
}
}
}
// try and play the sound if specified
if (FireSound != nullptr)
{
UGameplayStatics::PlaySoundAtLocation(this, FireSound, GetActorLocation());
}
// try and play a firing animation if specified
if (FireAnimation != nullptr)
{
// Get the animation object for the arms mesh
UAnimInstance* AnimInstance = Mesh1P->GetAnimInstance();
if (AnimInstance != nullptr)
{
AnimInstance->Montage_Play(FireAnimation, 1.f);
}
}
}
순서대로
발사체 클래스를 컴포넌트로 가지고 있는데(블루프린트에서 지정한 Projectile Class) 이 클래스가 널이 아니라면 모션 컨트롤러(VR관련)인지 검사하고 FP_MuzzleLocation총구를 쏠 때 파박 튀는 부분을 찾아서 위치와 회전값을 가져온다.
그리고 그 값을 발사체를 SpawnActor할 때 써서 월드에 발사체를 배치시킨다. 그리고 소리와 애니메이션을 재생시킨다.
발사체 클래스 Projectile Class가 어떻게 되어있나 보자.
3. 발사체 클래스 Projectile Class
// [FirstPlayer프로젝트이름]ProjectileClass.h
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "FirstPlayerProjectile.generated.h"
class USphereComponent;
class UProjectileMovementComponent;
UCLASS(config=Game)
class AFirstPlayerProjectile : public AActor
{
GENERATED_BODY()
/** Sphere collision component */
UPROPERTY(VisibleDefaultsOnly, Category=Projectile)
USphereComponent* CollisionComp;
/** Projectile movement component */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true"))
UProjectileMovementComponent* ProjectileMovement;
public:
AFirstPlayerProjectile();
/** called when projectile hits something */
UFUNCTION()
void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);
/** Returns CollisionComp subobject **/
USphereComponent* GetCollisionComp() const { return CollisionComp; }
/** Returns ProjectileMovement subobject **/
UProjectileMovementComponent* GetProjectileMovement() const { return ProjectileMovement; }
};
Actor클래스를 상속받아서 SphereComponent를 가지고 있고 또 UProjectileMovementComponent를 가지고 있다.
언리얼 에디터에서도 보이게 하는 등 UPROPERTY를 설정했다. 또 충돌했을 때의 처리를 위해 OnHit도 가지고있다.
// [FirstPlayer.cpp]
#include "FirstPlayerProjectile.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "Components/SphereComponent.h"
AFirstPlayerProjectile::AFirstPlayerProjectile()
{
// Use a sphere as a simple collision representation
CollisionComp = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComp"));
CollisionComp->InitSphereRadius(5.0f);
CollisionComp->BodyInstance.SetCollisionProfileName("Projectile");
CollisionComp->OnComponentHit.AddDynamic(this, &AFirstPlayerProjectile::OnHit); // set up a notification for when this component hits something blocking
// Players can't walk on it
CollisionComp->SetWalkableSlopeOverride(FWalkableSlopeOverride(WalkableSlope_Unwalkable, 0.f));
CollisionComp->CanCharacterStepUpOn = ECB_No;
// Set as root component
RootComponent = CollisionComp;
// Use a ProjectileMovementComponent to govern this projectile's movement
ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileComp"));
ProjectileMovement->UpdatedComponent = CollisionComp;
ProjectileMovement->InitialSpeed = 3000.f;
ProjectileMovement->MaxSpeed = 3000.f;
ProjectileMovement->bRotationFollowsVelocity = true;
ProjectileMovement->bShouldBounce = true;
// Die after 3 seconds by default
InitialLifeSpan = 3.0f;
}
void AFirstPlayerProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
// Only add impulse and destroy projectile if we hit a physics
if ((OtherActor != nullptr) && (OtherActor != this) && (OtherComp != nullptr) && OtherComp->IsSimulatingPhysics())
{
OtherComp->AddImpulseAtLocation(GetVelocity() * 100.0f, GetActorLocation());
Destroy();
}
}
생성자에선 컴포넌트들의 성질을 정의한다 .특히 SphereComponent->OnComponentHit시 델리게이트로 AddDynamic으로 OnHit를 호출하게 연결하였다. 또 ProjectileMovement컴포넌트의 성질들도 세팅하는데 속도와 성질은 파일에서 읽어와 여러 Projectile을 다르게 설정할 수도 있다.
위 클래스를 부모로 가지고 있는 블루프린트인 FirstPersonProjectile를 열어보면
OnHit을 보면
콜리전 프리셋이 Projectile로 설정되어있는걸 확인할수 있다. 그리고 프로젝트 세팅> 콜리전에도 Projectile 프로파일이 위와 같이 설정되어있다. Proejctile이 일종의 액터라서 트레이스 채널이 아닌 오브젝트 채널에 생성이 되어있다.
그리고 이렇게 생성된 채널로 충돌 판정을 하는 것이다.
호출되는 OnHit을 보면 충돌되는 다른 상대를 확인하고 AddImpulseAtLocation으로 그 상대에게 충격을 주는데 첫번째 인자로는 FVector타입의 충격의 강도와 방향이고, 두번째는 FVector타입의 발생지점이다. 그리고 이 발사체를 Destroy()로 삭제해버린다.
이런 발사체는 Fireball이나 화살같은 투사체에도 적용할 수 있다.
4. CrossHair
항상 가운데에 있는걸 보면 UI라는걸 추측할 수 있다.
게임모드에 보면
// [FirstPlayer]GameMode.cpp
#include "FirstPlayerGameMode.h"
#include "FirstPlayerHUD.h"
#include "FirstPlayerCharacter.h"
#include "UObject/ConstructorHelpers.h"
AFirstPlayerGameMode::AFirstPlayerGameMode()
: Super()
{
// set default pawn class to our Blueprinted character
static ConstructorHelpers::FClassFinder<APawn> PlayerPawnClassFinder(TEXT("/Game/FirstPersonCPP/Blueprints/FirstPersonCharacter"));
DefaultPawnClass = PlayerPawnClassFinder.Class;
// use our custom HUD class
HUDClass = AFirstPlayerHUD::StaticClass();
}
HUDClass를 지정해주고 있다. 그것도 Static으로 .
전에는 그냥 HPBar를 캐릭터에 붙여놨는데 인벤토리 버튼이나 다른 게이지, 캐릭터 상태창같은 UI들도 같이 HUD로 하나로 이렇게 관리할수도 있다.
이 샘플에선 DrawHUD에서 간단히 그리고 있지만 위젯블루프린트로 거의 관리한다.
'[Unreal]' 카테고리의 다른 글
[Unreal 4] 샘플 둘러보기 - 내려보기 샘플 (0) | 2023.07.17 |
---|