본문 바로가기

언리얼러닝/C++스크립트게임개발

MyShooter프로젝트 생성 - 기본 클래스 분석

책의 앞부분은 일인칭템플릿의 C++ 코드를 분석해 주고 있다. 블러거는 5.3.2를 사용해 따라해봤는데 코드가 달라 그냥 혼자 분석해 봤다

클래스는 크게

ShooterCharacter : 처음에는 총을 가지고 있지 않다. 팔메시는 Character Mesh가 아니고 Mesh1P에 지정된다.

BP_Picup_Rifle : TP_Weapon, TP_PickUp, 플레이어와 충돌하면 OnPickUp()델리게이션으로 BP_Pickup_Rifle에 메세지를 보내 AttachWeapon()을 통해 캐릭터에 부착되고 발사액션도 바인딩된다.

BP_FirstPersonProjectile - 발사버튼이 눌리면 TP_Weapon컴포넌트의 Fire()에서 ShooterProjectile을 Spawn한다.

 

전체적인 로직은 팔만 가지고 있는 캐릭터는 총과 충돌하면 총이 OnPickUp()델리게인션으로 블루프린트과 통신하여 AttachWeapon()함수를 통해 Character에 붙인다.이때 Fire Input Action도 Biding된다. 이 Action은 별도의 IMC에 들어있고 IMC도 add해준다.

Character가 총을쏘면 Action으로 바인딩된 Fire()가 실행되고 ShooterProjectile을 총이 생성하는데 캐릭터의 위치와 앞방향으로 나가게 한다.

총알은 다른 액터와 충돌시 AddImpulse()로 힘을 전달한다.

.

Shooter 로 프로젝트를 만들경우

ShooterCharacter class가 생성된다. Character를 상속받아 만들어졌다.

ShooterCharacter를 상속받아 BP_FirstPersonCharacter가 만들어져있다. 카메라와 팔이 있는 이상한 모양이다. 무기Mesh는 없다.

무기는 BP_PicUp_Rifle이라는 블루프린트인데 TP_Weapon,TP_Picup컴포넌트에 RifleMesh가 추가되어져 있다.

충돌처리가 일어나면 OnPickUp Deligation이 실행되어 획득한 캐릭터에 Attach된다. 코드는 아래에서 확인하자

총알은 BP_FirstPersonProjectile인데 특별한건 없다. 총알은 WeaponComponent,cpp Fire()에서 Spawn된다.

 

일인칭 템플릿으로 C++ Shooter 생성

 

교재와 버전이 달라 코드가 다르다 간단하게 코드를 살펴보자

우선 ShooterCharater.h 를 보면

Character에서 상속받은 SkeletaMeshComponent는 안쓰고 Mesh1P라는 SkeletaMeshComponent변수를 마련하였다.

카메라및 입력 IMC을 위한 변수, JumpAction,MoveAction을 위한 변수가 있다. 코드는 이해를 위해 생략되어 있어 이대로는 실행이 안된다.

UCLASS(config=Game)
class AShooterCharacter : public ACharacter
{
	USkeletalMeshComponent* Mesh1P;
	UCameraComponent* FirstPersonCameraComponent;
	UInputMappingContext* DefaultMappingContext;
	UInputAction* JumpAction;
	UInputAction* MoveAction;
	
public:
	AShooterCharacter();

protected:
	virtual void BeginPlay();

Character는 기본적으로 총을 보유하고 있지 않지만 총을 획득했을때의 관리를 위한 bHasRifle변수가 있다.

Character의 움작임을 위한 Move(), Look() 함수가 있다.

IMC등록을 위해 SetupPlayerInputComponent()

public:
	bool bHasRifle;
	void SetHasRifle(bool bNewHasRifle);
	bool GetHasRifle();

protected:
	/** Called for movement input */
	void Move(const FInputActionValue& Value);

	/** Called for looking input */
	void Look(const FInputActionValue& Value);

protected:
	// APawn interface
	virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) override;
	// End of APawn interface

public:
	/** Returns Mesh1P subobject **/
	USkeletalMeshComponent* GetMesh1P() const { return Mesh1P; }
	/** Returns FirstPersonCameraComponent subobject **/
	UCameraComponent* GetFirstPersonCameraComponent() const { return FirstPersonCameraComponent; }
};

Shooter.cpp를 보면

생성자에서 카메라를 만들어 붙이고 Mesh1P 컴포넌트를 생성해 카메라에 붙이고 그림자 설정 위치를 설정한다

AShooterCharacter::AShooterCharacter()
{
	// Character doesnt have a rifle at start
	bHasRifle = false;
	
	// Set size for collision capsule
	GetCapsuleComponent()->InitCapsuleSize(55.f, 96.0f);
		
	// Create a CameraComponent	
	FirstPersonCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("FirstPersonCamera"));
	FirstPersonCameraComponent->SetupAttachment(GetCapsuleComponent());
	FirstPersonCameraComponent->SetRelativeLocation(FVector(-10.f, 0.f, 60.f)); // Position the camera
	FirstPersonCameraComponent->bUsePawnControlRotation = true;

	// Create a mesh component that will be used when being viewed from a '1st person' view (when controlling this pawn)
	Mesh1P = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("CharacterMesh1P"));
	Mesh1P->SetOnlyOwnerSee(true);
	Mesh1P->SetupAttachment(FirstPersonCameraComponent);
	Mesh1P->bCastDynamicShadow = false;
	Mesh1P->CastShadow = false;
	//Mesh1P->SetRelativeRotation(FRotator(0.9f, -19.19f, 5.2f));
	Mesh1P->SetRelativeLocation(FVector(-30.f, 0.f, -150.f));
}

InputAction이 설정되어 있는 IMC를 설정한다. 총을쏘는 Fire()를 포함하는 다른IMC는 총을 획득할 경우 추가한다.

void AShooterCharacter::BeginPlay()
{
	// Call the base class  
	Super::BeginPlay();

	// Add Input Mapping Context
	if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
		{
			Subsystem->AddMappingContext(DefaultMappingContext, 0);
		}
	}
}

Move InputAction에 반응해서 Character를 움직이게 한다. AddMovementInput은 CharacterMovementComponent의 메서드다

void AShooterCharacter::Move(const FInputActionValue& Value)
{
	// input is a Vector2D
	FVector2D MovementVector = Value.Get<FVector2D>();

	if (Controller != nullptr)
	{
		// add movement 
		AddMovementInput(GetActorForwardVector(), MovementVector.Y);
		AddMovementInput(GetActorRightVector(), MovementVector.X);
	}
}

회전을 처리한다.

void AShooterCharacter::Look(const FInputActionValue& Value)
{
	// input is a Vector2D
	FVector2D LookAxisVector = Value.Get<FVector2D>();

	if (Controller != nullptr)
	{
		// add yaw and pitch input to controller
		AddControllerYawInput(LookAxisVector.X);
		AddControllerPitchInput(LookAxisVector.Y);
	}
}

라이플을 획득했을 경우 bHasRifle을 Set한다.

void AShooterCharacter::SetHasRifle(bool bNewHasRifle)
{
	bHasRifle = bNewHasRifle;
}

bool AShooterCharacter::GetHasRifle()
{
	return bHasRifle;
}

BP_Picup_Rifle 블루프린트를 보면 TP_Weapon, TP_Picup, RifleMesh 3개의 컴포넌트가 포함되어 있다

이벤트 그래프를 보면 획득하였을 경우 TP_Weapon을 획득한 Character에 Attach해주는 델리게이트가 들어 있다.

플레이어와의 충돌을 체크하는 TP_Pickup컴포넌트를 살펴보면

생성자에서 충돌체의 크기를 설정한다.

BeginPlay()에서 충돌시 실행될 OnSphereBeginOverlap() 함수를 연결한다.

OnSphereBeginOverlap() 에서는 플레이어의 정보를 얻어서 델리게이션 브로드캐스트한다. OnPickUp.Broadcast(Character);

BP_PicupRifle블루프린트가 메세지를 받아 OnPickUp()이  TP_WeaponComponent의 메쏘드인 AttachWepon()을 실행한다. AttachWepon()는 아래에 설명이 있다.

 

헤더파일

TP_Pickup.h

#include "TP_PickUpComponent.h"

UTP_PickUpComponent::UTP_PickUpComponent()
{
	// Setup the Sphere Collision
	SphereRadius = 32.f;
}

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

	// Register our Overlap Event
	OnComponentBeginOverlap.AddDynamic(this, &UTP_PickUpComponent::OnSphereBeginOverlap);
}

void UTP_PickUpComponent::OnSphereBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	// Checking if it is a First Person Character overlapping
	AShooterCharacter* Character = Cast<AShooterCharacter>(OtherActor);
	if(Character != nullptr)
	{
		// Notify that the actor is being picked up
		OnPickUp.Broadcast(Character);

		// Unregister from the Overlap Event so it is no longer triggered
		OnComponentBeginOverlap.RemoveAll(this);
	}
}

TP_Pickup.cpp

#pragma once

#include "CoreMinimal.h"
#include "Components/SphereComponent.h"
#include "ShooterCharacter.h"
#include "TP_PickUpComponent.generated.h"

// Declaration of the delegate that will be called when someone picks this up
// The character picking this up is the parameter sent with the notification
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPickUp, AShooterCharacter*, PickUpCharacter);

UCLASS(Blueprintable, BlueprintType, ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class SHOOTER_API UTP_PickUpComponent : public USphereComponent
{
	GENERATED_BODY()

public:
	
	/** Delegate to whom anyone can subscribe to receive this event */
	UPROPERTY(BlueprintAssignable, Category = "Interaction")
	FOnPickUp OnPickUp;

	UTP_PickUpComponent();
protected:

	/** Called when the game starts */
	virtual void BeginPlay() override;

	/** Code for when something overlaps this component */
	UFUNCTION()
	void OnSphereBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
};

TP_Weapon, 

총알을 스폰할때 사용할 ProjectileClass ;

FireSound  - 발사소리를 담당하는 변수

FireAnimation - 총 발사동작

MuzzleOffset - 발사위치 (Projectile Spawn위치)

UTP_WeaponComponent() : FPC에 총 컴포넌트를 붙여준다.

Fire() : Projectile을 Spawn해주는  발사기

UCLASS(Blueprintable, BlueprintType, ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class SHOOTER_API UTP_WeaponComponent : public USkeletalMeshComponent
public:
	/** Projectile class to spawn */
	TSubclassOf<class AShooterProjectile> ProjectileClass;
	/** Sound to play each time we fire */
	USoundBase* FireSound;
	/** AnimMontage to play each time we fire */
	UAnimMontage* FireAnimation;
	/** Gun muzzle's offset from the characters location */
	FVector MuzzleOffset;
	/** MappingContext */
	class UInputMappingContext* FireMappingContext;
	/** Fire Input Action */
	class UInputAction* FireAction;
	/** Sets default values for this component's properties */
	UTP_WeaponComponent();
	/** Attaches the actor to a FirstPersonCharacter */
	void AttachWeapon(AShooterCharacter* TargetCharacter);
	/** Make the weapon Fire a Projectile */
	UFUNCTION(BlueprintCallable, Category="Weapon")
	void Fire();

protected:
	/** Ends gameplay for this component. */
	UFUNCTION()
	virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

private:
	/** The Character holding this weapon*/
	AShooterCharacter* Character;
};

생성자에서는 발사위치를 설정한다.

Fire()에서는 플레이어를 얻어와 위치와 Forward방향을 얻는다  SpawnActor로 ProjectileClass를 생성한다.

소리와 애니메이션도 재생한다. GetMesh1P()함수를 사용해서 Weapon() StaticMesh의 인스턴스를 이용해 애님인스턴스를 얻어와 애님몽타지를 재생한다.

AttachWeapon()함수는 플레이어가 무기와 충돌이 일어 났을때 실행되며 AttachToComponent를 사용해 총기를 Mesh1(팔)에 붙인다.

// Attach the weapon to the First Person Character
FAttachmentTransformRules AttachmentRules(EAttachmentRule::SnapToTarget, true);
AttachToComponent(Character->GetMesh1P(), AttachmentRules, FName(TEXT("GripPoint")));

총을 어태치하면 상태를 업데이트 해준다.

Character->SetHasRifle(true);

PlayerController를 얻어와 총을 발하는 Action을 처리하는 MappingContext를 추가해주고, Binding 처리를 추가해준다.

// Set up action bindings
if (APlayerController* PlayerController = Cast<APlayerController>(Character->GetController()))
{
    if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
    {
        // Set the priority of the mapping to 1, so that it overrides the Jump action with the Fire action when using touch input
        Subsystem->AddMappingContext(FireMappingContext, 1);
    }

    if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerController->InputComponent))
    {
        // Fire
        EnhancedInputComponent->BindAction(FireAction, ETriggerEvent::Triggered, this, &UTP_WeaponComponent::Fire);
    }
}

TP_WeaponComponent.cpp

// Copyright Epic Games, Inc. All Rights Reserved.


#include "TP_WeaponComponent.h"
#include "ShooterCharacter.h"
#include "ShooterProjectile.h"
#include "GameFramework/PlayerController.h"
#include "Camera/PlayerCameraManager.h"
#include "Kismet/GameplayStatics.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"

// Sets default values for this component's properties
UTP_WeaponComponent::UTP_WeaponComponent()
{
	// Default offset from the character location for projectiles to spawn
	MuzzleOffset = FVector(100.0f, 0.0f, 10.0f);
}


void UTP_WeaponComponent::Fire()
{
	if (Character == nullptr || Character->GetController() == nullptr)
	{
		return;
	}

	// Try and fire a projectile
	if (ProjectileClass != nullptr)
	{
		UWorld* const World = GetWorld();
		if (World != nullptr)
		{
			APlayerController* PlayerController = Cast<APlayerController>(Character->GetController());
			const FRotator SpawnRotation = PlayerController->PlayerCameraManager->GetCameraRotation();
			// 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 = GetOwner()->GetActorLocation() + SpawnRotation.RotateVector(MuzzleOffset);
	
			//Set Spawn Collision Handling Override
			FActorSpawnParameters ActorSpawnParams;
			ActorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding;
	
			// Spawn the projectile at the muzzle
			World->SpawnActor<AShooterProjectile>(ProjectileClass, SpawnLocation, SpawnRotation, ActorSpawnParams);
		}
	}
	
	// Try and play the sound if specified
	if (FireSound != nullptr)
	{
		UGameplayStatics::PlaySoundAtLocation(this, FireSound, Character->GetActorLocation());
	}
	
	// Try and play a firing animation if specified
	if (FireAnimation != nullptr)
	{
		// Get the animation object for the arms mesh
		UAnimInstance* AnimInstance = Character->GetMesh1P()->GetAnimInstance();
		if (AnimInstance != nullptr)
		{
			AnimInstance->Montage_Play(FireAnimation, 1.f);
		}
	}
}

void UTP_WeaponComponent::AttachWeapon(AShooterCharacter* TargetCharacter)
{
	Character = TargetCharacter;

	// Check that the character is valid, and has no rifle yet
	if (Character == nullptr || Character->GetHasRifle())
	{
		return;
	}

	// Attach the weapon to the First Person Character
	FAttachmentTransformRules AttachmentRules(EAttachmentRule::SnapToTarget, true);
	AttachToComponent(Character->GetMesh1P(), AttachmentRules, FName(TEXT("GripPoint")));
	
	// switch bHasRifle so the animation blueprint can switch to another animation set
	Character->SetHasRifle(true);

	// Set up action bindings
	if (APlayerController* PlayerController = Cast<APlayerController>(Character->GetController()))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
		{
			// Set the priority of the mapping to 1, so that it overrides the Jump action with the Fire action when using touch input
			Subsystem->AddMappingContext(FireMappingContext, 1);
		}

		if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerController->InputComponent))
		{
			// Fire
			EnhancedInputComponent->BindAction(FireAction, ETriggerEvent::Triggered, this, &UTP_WeaponComponent::Fire);
		}
	}
}

void UTP_WeaponComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	if (Character == nullptr)
	{
		return;
	}

	if (APlayerController* PlayerController = Cast<APlayerController>(Character->GetController()))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
		{
			Subsystem->RemoveMappingContext(FireMappingContext);
		}
	}
}

 

WeaponComponent.cpp를 보면

생성자에서 머즐(총구)위치를 설정하고

UTP_WeaponComponent::UTP_WeaponComponent()
{
	// Default offset from the character location for projectiles to spawn
	MuzzleOffset = FVector(100.0f, 0.0f, 10.0f);
}

총알을 발사하는 Fire()함수는  SapwnActor<AShooterProjectile>로 총알을 생성한다. AShooterProjectile는 BP_PickupRifle에서 BP_FristPersonProjectil을 지정해줘야 한다. 

void UTP_WeaponComponent::Fire()
{
if (Character == nullptr || Character->GetController() == nullptr)
{
    return;
}

// Try and fire a projectile
if (ProjectileClass != nullptr)
{
    UWorld* const World = GetWorld();
    if (World != nullptr)
    {
        APlayerController* PlayerController = Cast<APlayerController>(Character->GetController());
        const FRotator SpawnRotation = PlayerController->PlayerCameraManager->GetCameraRotation();
        // 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 = GetOwner()->GetActorLocation() + SpawnRotation.RotateVector(MuzzleOffset);

        //Set Spawn Collision Handling Override
        FActorSpawnParameters ActorSpawnParams;
        ActorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding;

        // Spawn the projectile at the muzzle
        World->SpawnActor<AShooterProjectile>(ProjectileClass, SpawnLocation, SpawnRotation, ActorSpawnParams);
    }
}

발사 소리도 처리하고 Character의 AnimInstace를 가져와 Mntage도 플레이 해준다. 총에서 Character의 애니메이션을 처리해준다.

ShooterProjectile.h와 cpp는 Fire()시 생성되는 총알이다.

ShooterProjectile.h는 Actor를 상속받고 콜리전을 담당하는 변수가 있고, 움직임은 ProjectileMovementComponent를 사용한다.

메쏘드는 OnHit(), - cpp에서 구현

상태를 얻는 GetCollisionComp(), GetProjectileMovement().가 있다

 

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ShooterProjectile.generated.h"

class USphereComponent;
class UProjectileMovementComponent;

UCLASS(config=Game)
class AShooterProjectile : 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:
	AShooterProjectile();

	/** 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; }
};

cpp은 생성자에서 충돌체와 진행스피드를 성정하고

OnHit()에서는 피직스가 활성화된 컴포넌트가 있는 액터라면 임팩트를 주어 충돌물리를 일어나게 한다. 자신은 파괴한다.

// Copyright Epic Games, Inc. All Rights Reserved.

#include "ShooterProjectile.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "Components/SphereComponent.h"

AShooterProjectile::AShooterProjectile() 
{
	// 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, &AShooterProjectile::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 AShooterProjectile::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();
	}
}

StaticMesh는 BP_FirstPersonProjectile 블루프린트에서 구현되어있다. 이벤트그래프는 특별히 없다.