Unreal C++로 TPS 캐릭터 조작하기

민트맛치킨·2025년 5월 1일

Unreal

목록 보기
6/26

C++ 클래스 (설명은 주석 참고)

CharacterBase.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "CharacterBase.generated.h"

UCLASS()
class BASIS_API ACharacterBase : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	ACharacterBase();
	

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:
	virtual void Attack(); // 상속받는 자식이 다르게 구현될 수 있음 virtual
	virtual void Hit(int32 Damage, AActor* ByWho);
	virtual void IncreaseKillCount();

	UFUNCTION(BlueprintCallable)
	void Heal(int HealAmount);

public:	
	UPROPERTY(EditAnywhere)
		int32 FullHP;
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
		int32 AttackCount;
	UPROPERTY(EditAnywhere)
		int32 Strength;

	int32 CurrentHP;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 KillCount;
};

CharacterBase.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "CharacterBase.h"

// Sets default values
ACharacterBase::ACharacterBase()
{
 	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
	FullHP = 100;
	Strength = 10;
	KillCount = 0;
}


// Called when the game starts or when spawned
void ACharacterBase::BeginPlay()
{
	Super::BeginPlay();
	
	SpawnDefaultController(); // 캐릭터가 생성되었을 때 그에 맞는 컨트롤러를 자동으로 인식
	CurrentHP = FullHP;
	KillCount = 0;
}

void ACharacterBase::Attack()
{
	AttackCount++;
}

void ACharacterBase::Hit(int32 Damage, AActor* ByWho)
{
	CurrentHP -= Damage;
	if (CurrentHP < 0)
	{
		CurrentHP = 0;
	}
	if (CurrentHP <= 0)
	{
		ACharacterBase* CB= Cast<ACharacterBase>(ByWho);
		if (IsValid(CB))
		{
			CB->IncreaseKillCount();
		}
	}
}

void ACharacterBase::IncreaseKillCount()
{
	KillCount++;
}

void ACharacterBase::Heal(int HealAmount)
{
	CurrentHP += HealAmount;
	if (CurrentHP > FullHP)
	{
		CurrentHP = FullHP;
	}
}

PlayerBase.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "CharacterBase.h"
#include "InputMappingContext.h"
#include "EnhancedInputSubsystems.h"
#include "PlayerBase.generated.h"

class UInputAction;
struct FInputActionValue;
class AWeapon;
class UInputMappingContext;

UCLASS()
class BASIS_API APlayerBase : public ACharacterBase
{
	GENERATED_BODY()

public:
	APlayerBase();
	virtual void BeginPlay() override;
	virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override; // input 관련 컴포넌트
	virtual void Hit(int32 Damage, AActor* ByWho) override; // Hit 덮어씌우기
	virtual void IncreaseKillCount() override;
	virtual void Attack() override;

private:
	bool Zoomed = false;

	// USpringArmComponent* a; 를
	// TObjectPtr<USpringArmComponent> a; 로 변경
	UPROPERTY(VisibleAnywhere)
		TObjectPtr<class USpringArmComponent> CameraBoom; // 스프링암 컴포넌트
	UPROPERTY(VisibleAnywhere)
		TObjectPtr<class UCameraComponent> FollowCamera; // 카메라

	UPROPERTY(EditAnywhere)
		TSubclassOf<AWeapon> Weapon; // 액터의 클래스 부분
	UPROPERTY()
		TObjectPtr<AWeapon> WeaponActor; // 실제 스폰되는 액터
	UPROPERTY(EditAnywhere)
		TObjectPtr<UInputMappingContext> InputMappingContext;


	UPROPERTY(EditAnywhere)
		TObjectPtr<UInputAction> MoveAction;
	UPROPERTY(EditAnywhere)
		TObjectPtr<UInputAction> ZoomAction;
	UPROPERTY(EditAnywhere)
		TObjectPtr<UInputAction> LookAction;
	UPROPERTY(EditAnywhere)
		TObjectPtr<UInputAction> FireAction;

	void Move(const FInputActionValue& Value);
	void Look(const FInputActionValue& Value);
	void Fire(const FInputActionValue& Value);
	void Zoom(const FInputActionValue& Value);
};

PlayerBase.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "PlayerBase.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include <EnhancedInputComponent.h> //EnhancedInputComponent를 사용하려면 모듈도 필요함(Basis.Build.cs에)
#include "Weapon.h"
#include "EnhancedInputSubsystems.h"
#include "InputMappingContext.h"
#include <MainGameMode.h>

APlayerBase::APlayerBase()
{
	CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
	CameraBoom->SetupAttachment(RootComponent);

	FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
	FollowCamera->SetupAttachment(CameraBoom);
}

void APlayerBase::BeginPlay()
{
	Super::BeginPlay();
	CameraBoom->TargetArmLength = 120;
	CameraBoom->SocketOffset = FVector(0, 60, 60);
	
	APlayerController* PC = Cast<APlayerController>(GetController()); // 컨트롤러 가져오기
	if (IsValid(PC))
	{
		ULocalPlayer* Player = PC->GetLocalPlayer();
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(Player))
		{
			Subsystem->AddMappingContext(InputMappingContext, 0); // PlayerBase에서 설정한 InputMappingContext를 등록
		}
	}

	WeaponActor = GetWorld()->SpawnActor<AWeapon>(Weapon); // 세계를 가져오고 AWeapon의 하위 클래스 중 Weapon을 SpawnActor
	if (IsValid(WeaponActor))
	{
		FAttachmentTransformRules TransformRules(EAttachmentRule::SnapToTarget, true); // SnapToTarget으로 붙이기
		WeaponActor->AttachToComponent(GetMesh(), TransformRules, TEXT("WeaponSocket")); // 메시를 가져오고 WeaponSocket이란 이름으로 붙임
		WeaponActor->SetOwner(this); // 소유자는 자기자신 플레이어
	}
}

void APlayerBase::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
	{ 
		EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &APlayerBase::Move);
		EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &APlayerBase::Look);
		EnhancedInputComponent->BindAction(FireAction, ETriggerEvent::Started, this, &APlayerBase::Fire);
		EnhancedInputComponent->BindAction(ZoomAction, ETriggerEvent::Started, this, &APlayerBase::Zoom);
	}
}

void APlayerBase::Hit(int32 Damage, AActor* ByWho)

{
	Super::Hit(Damage, ByWho);

	if (CurrentHP > 0) {
		return;
	}

	if (IsValid(GetWorld()))
	{
		AMainGameMode* GameMode = Cast<AMainGameMode>(GetWorld()->GetAuthGameMode());
		if (IsValid(GameMode))
		{
			GameMode->ChangeToEnd();
		}
	}

	if (IsValid(WeaponActor))
	{
		WeaponActor->Destroy();
	}

	Destroy();
}

void APlayerBase::IncreaseKillCount()
{
	Super::IncreaseKillCount();
}

void APlayerBase::Attack()
{
	Super::Attack();
	
	if (IsValid(WeaponActor))
	{
		WeaponActor->Fire();
	}
}

void APlayerBase::Move(const FInputActionValue& Value)
{
	FVector2D MovementVector = Value.Get<FVector2D>();

	if (Controller != nullptr)
	{
		AddMovementInput(GetActorForwardVector(), MovementVector.Y);
		AddMovementInput(GetActorRightVector(), MovementVector.X);
	}
}
void APlayerBase::Look(const FInputActionValue& Value)
{
	FVector2D LookAxisVector = Value.Get<FVector2D>();

	if (Controller != nullptr)
	{
		AddControllerYawInput(LookAxisVector.X);
		AddControllerPitchInput(-LookAxisVector.Y);
	}
}
void APlayerBase::Fire(const FInputActionValue& Value)
{
	UE_LOG(LogTemp, Warning, TEXT("Fire Called!"));
	Attack();
}
void APlayerBase::Zoom(const FInputActionValue& Value)
{
	if (!IsValid(CameraBoom)) // IsValid로 null이 아닌지와 삭제된것인지 아닌지 판단
	{
		return;
	}
	Zoomed = !Zoomed;

	if (Zoomed) // 카메라 붐이 있고 Zoomed값이 참이면 카메라붐을 가까이
	{
		CameraBoom->TargetArmLength = 40;
		CameraBoom->SocketOffset = FVector(0, 40, 60);
	}
	else
	{
		CameraBoom->TargetArmLength = 120;
		CameraBoom->SocketOffset = FVector(0, 60, 60);
	}
}

Weapon.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

UCLASS()
class BASIS_API AWeapon : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AWeapon();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:
	void Fire();

	UPROPERTY(VisibleAnywhere)
		TObjectPtr<class USkeletalMeshComponent > Mesh; // 총의 스켈레탈 메시 외형

	UPROPERTY(VisibleAnywhere)
		TObjectPtr<USceneComponent> MuzzleOffset; // 총알의 발사 위치

	UPROPERTY(EditAnywhere)
		TObjectPtr<class UAnimMontage> FireMontage; // 총이 발사되는 애니메이션 몽타주

	UPROPERTY(EditAnywhere)
		TSubclassOf<class ABullet> Bullet; // 총알

};

Weapon.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "Weapon.h"
#include "Bullet.h"
#include <PlayerBase.h>
#include "Kismet/KismetMathLibrary.h"

// Sets default values
AWeapon::AWeapon()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	Mesh = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Mesh")); // CreateDefaultSubobject
	RootComponent = Mesh; // RootComponent는 이 액터의 최상위 항목으로 지정

	MuzzleOffset = CreateDefaultSubobject<USceneComponent>(TEXT("MuzzleOffset"));
	MuzzleOffset->SetupAttachment(Mesh); // MuzzleOffset(총알의 발사 위치)를 메시의 하위항목으로 설정
}

// Called when the game starts or when spawned
void AWeapon::BeginPlay()
{
	Super::BeginPlay();
	
}

void AWeapon::Fire()
{
	UAnimInstance* AnimInstance = Mesh->GetAnimInstance(); // 메시의 애니메이션을 가져오는 GetAnimInstance
	if (IsValid(AnimInstance) && IsValid(FireMontage))
	{
		AnimInstance->Montage_Play(FireMontage); // AnimInstance와 파이어몽타주가 존재하면 파이어몽타주 재생
	}
	if (IsValid(Bullet))
	{
		FRotator SpawnRotation = MuzzleOffset->GetComponentRotation(); // MuzzleOffset(총알 발사 위치)의 각도 가져오기
		FVector SpawnLocation = MuzzleOffset->GetComponentLocation(); // MuzzleOffset(총알 발사 위치)의 위치 가져오기

		FActorSpawnParameters SpawnParams; // Spawn할때(총알)
		SpawnParams.Owner = this; // 총알의 Owner는 this(총)으로 설정

		APlayerBase* PB = Cast<APlayerBase>(GetOwner()); // GetOwner() 즉 총의 주인인 플레이어를 APlayerBase로 캐스팅, PB에 대입

		if (!IsValid(PB))
		{
			GetWorld()->SpawnActor<ABullet>(Bullet, SpawnLocation, SpawnRotation, SpawnParams); // 총의 주인이 없으면 총알만 생성
			return;
		}

		APlayerController* PC = Cast<APlayerController>(PB->GetController()); // GetController로 플레이어의 컨트롤러를 가져옴
		int32 x, y;

		if (!IsValid(PC))
		{
			GetWorld()->SpawnActor<ABullet>(Bullet, SpawnLocation, SpawnRotation, SpawnParams);
			return;
		}

		PC->GetViewportSize(x, y); // 플레이어 컨트롤러의 뷰포트 사이즈
		FVector WorldCenter;
		FVector WorldFront;
		PC->DeprojectScreenPositionToWorld(x / 2.0f, y / 2.0f, WorldCenter, WorldFront); // 각각 2로 나눠 화면의 정중앙
		WorldCenter += WorldFront * 10000;
		SpawnRotation = UKismetMathLibrary::FindLookAtRotation(SpawnLocation, WorldCenter); // 총구 위치에서 조준점 방향으로의 회전값 계산
		GetWorld()->SpawnActor<ABullet>(Bullet, SpawnLocation, SpawnRotation, SpawnParams); // 총알을 SpawnLocation의 위치에서 SpawnRotation의 각도로 spawn
	}
}

Bullet.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

UCLASS()
class BASIS_API ABullet : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ABullet();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;
	
	UFUNCTION()
	void OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit);

private:
	UPROPERTY(VisibleAnywhere)
		TObjectPtr<class UStaticMeshComponent> StaticMeshComponent; // 모양을 나타내는 스태틱메시 컴포넌트

	UPROPERTY(VisibleAnywhere)
		TObjectPtr<class UProjectileMovementComponent> ProjectileMovementComponent; // 발사체의 움직임을 담당하는 컴포넌트
};

Bullet.cpp

//Fill out your copyright notice in the Description page of Project Settings.


#include "Bullet.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include <CharacterBase.h>

// Sets default values
ABullet::ABullet()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	StaticMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMeshComponent"));
	RootComponent = StaticMeshComponent;

	ProjectileMovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovementComponent"));
	ProjectileMovementComponent->InitialSpeed = 20000.0f; // 총알 초기 속도
	ProjectileMovementComponent->MaxSpeed = 20000.0f; // 총알 최대 속도

}

// Called when the game starts or when spawned
void ABullet::BeginPlay()
{
	Super::BeginPlay();
	StaticMeshComponent->OnComponentHit.AddDynamic(this, &ABullet::OnHit);
}

// Called every frame
void ABullet::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

void ABullet::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComponent, FVector NormalImpulse, const FHitResult& Hit)
{
	UE_LOG(LogTemp, Warning, TEXT("OnHit Called")); // 로그

	ACharacterBase* CB = Cast<ACharacterBase>(OtherActor); // OnHit된 OtherActor가 ACharacterBase로 Cast되고
	AActor* Actor = GetOwner(); // 총알의 주인인 총
	if (!IsValid(Actor)) // 총이 없으면 리턴
	{
		return;
	}
	ACharacterBase* OwnerCB = Cast <ACharacterBase>(Actor->GetOwner()); // 총의 주인인 액터를 Owner에 대입
	if (!IsValid(OwnerCB))
	{
		return;
	}
	if (IsValid(CB)) // 맞은 대상인 CB가
	{
		CB->Hit(OwnerCB->Strength, OwnerCB); // CB에게 Owner가 Owner의 Strength 만큼 Hit
	}
	Destroy();
}

CharacterAnimInstance.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "CharacterAnimInstance.generated.h"

UCLASS()
class BASIS_API UCharacterAnimInstance : public UAnimInstance
{
	GENERATED_BODY()
	
public:
	virtual void NativeInitializeAnimation() override; // 애니메이션이 초기화 될때 실행
	virtual void NativeUpdateAnimation(float DeltaSeconds) override; // 애니메이션이 업데이트 될때마다 실행(DeltaSeconds는 전 업데이트부터 지금까지 걸린시간)

	UFUNCTION()
		void AnimNotify_Attack();

	UPROPERTY(BlueprintReadOnly)
		TObjectPtr<class ACharacterBase> Character;

	UPROPERTY(BlueprintReadOnly)
		TObjectPtr<class UCharacterMovementComponent> MovementComponent; // 캐릭터의 움직임 저장

	UPROPERTY(BlueprintReadOnly)
		FRotator ControlRot;

	UPROPERTY(BlueprintReadOnly)
		FRotator ActorRot;

	UPROPERTY(BlueprintReadOnly)
		float Speed;

	UPROPERTY(BlueprintReadOnly)
		int32 AttackCount;
};

CharacterAnimInstance.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "CharacterAnimInstance.h"
#include "CharacterBase.h"
#include "GameFramework/CharacterMovementComponent.h"

void UCharacterAnimInstance::NativeInitializeAnimation()
{
	Super::NativeInitializeAnimation();

	Character = Cast<ACharacterBase>(TryGetPawnOwner()); // 애니메이션을 가진 주인을 가져움
	if (IsValid(Character))
	{
		MovementComponent = Character->GetCharacterMovement(); // 캐릭터의 움직임을 MovementComponent에 대입
	}
}

void UCharacterAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
	Super::NativeUpdateAnimation(DeltaSeconds);

	if (IsValid(MovementComponent))
	{
		Speed = MovementComponent->Velocity.Size();
		AttackCount = Character->AttackCount;
	}
	if (IsValid(Character))
	{
		ActorRot = Character->GetActorRotation();
		ControlRot = Character->GetControlRotation();
	}
}

void UCharacterAnimInstance::AnimNotify_Attack()
{
	Character->AttackCount = 0;
	AttackCount = 0;
}

C++로 Character와 Weapon, Bullet을 구현하고 C++ 기반으로 블루프린트 클래스 생성 후 메시 또는 스켈레탈 메시와 애니메이션을 할당한다.

Input Action과 InputMappingContext도 설정하게 되면 캐릭터를 움직이고 좌클릭으로 총 발사, 우클릭으로 zoom까지 할 수 있다.

0개의 댓글