게임 개발/학습 일지

언리얼 내배캠 마스터반 수업 과제 - 샌드박스 패턴으로 기존 과제 구조 변경 / 우클릭 조준 구현

이개 2026. 5. 7. 08:25

캐릭터 스켈레탈 메시에 소켓 추가하기

캐릭터 스켈레탈 메시를 연 후 Skeleton Tree에서 손을 찾아 우클릭한다. Add Socket을 선택한다.
소켓이 생성되면 코드에서 호출하기위해 이름을 바꾼다. 기본 캐릭터의 경우 weapon_r_muzzle 소켓이 붙어있다.

그런데 하다보니 카메라에 달게 되어서 소켓이 상관 없어졌다.

 

샌드박스 무기 클래스 만들기

액터 WeaponBase파일과 이를 상속받은 SandboxWeaponBase, 또 SandboxWeaponBase를 상속받은 BP_SandboxWeaponBase_Shotgun을 만든다.

 

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

#pragma once

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

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

	UFUNCTION(BlueprintCallable)
	virtual void Fire();


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

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Stats")
	float MaxAmmo;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon Stats")
	float CurrentAmmo;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Weapon Stats")
	int32 PelletCount; // 한 발당 총알 개수
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Weapon Stats")
	float SpreadAngle; // 탄이 퍼지는 각도
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Weapon Stats")
	float Range; // 사거리
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Weapon Stats|Recoil")
	float VerticalRecoil; // 위로 튀는 정도
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Weapon Stats|Recoil")
	float RecoilDuration; // 반동이 일어나는 시간


public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

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


#include "WeaponBase.h"

// Sets default values
AWeaponBase::AWeaponBase()
{
 	// 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;
	
}

void AWeaponBase::Fire()
{
	
}

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

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

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

#pragma once

#include "CoreMinimal.h"
#include "WeaponBase.h"
#include "SandboxWeaponBase.generated.h"

/**
 * 
 */
UCLASS()
class ADVANCEDHOMEWORK1_API ASandboxWeaponBase : public AWeaponBase
{
	GENERATED_BODY()

public:
	virtual void Fire() override;

	UFUNCTION(BlueprintImplementableEvent)
	void SandboxFire();

	UFUNCTION(BlueprintCallable)
	void LinetraceOneShot(FVector Direction);

	// 사운드
	UFUNCTION(BlueprintCallable)
	void PlaySound(USoundBase* Sound);

	UFUNCTION(BlueprintCallable)
	void Reload();

	UFUNCTION(BlueprintCallable)
	void UpdateAmmo();

	UFUNCTION(BlueprintCallable)
	void SetAiming(bool bNewAiming);

protected:

	UPROPERTY(BlueprintReadOnly, Category = "Combat")
	bool bIsAiming = false;

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

#include "SandboxWeaponBase.h"
#include "GameFramework/Character.h"        // ACharacter 클래스 정의
#include "Camera/CameraComponent.h"
#include "Kismet/GameplayStatics.h"

void ASandboxWeaponBase::Fire()
{
	SandboxFire();
}

void ASandboxWeaponBase::LinetraceOneShot(FVector Direction)
{

	GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Red, FString::Printf(TEXT("LineTrace")));
	AActor* MyOwner = GetOwner();
	if (MyOwner)
	{
		ACharacter* MyCharacter = Cast<ACharacter>(MyOwner);
		if (MyCharacter)
		{
			// 캐릭터의 GetMesh()를 호출
			USkeletalMeshComponent* CharacterMesh = MyCharacter->GetMesh();
			FVector SocketLocation = CharacterMesh->GetSocketLocation(FName("weapon_r_muzzle"));
			UE_LOG(LogTemp, Warning, TEXT("Socket Location: %s"), *SocketLocation.ToString());	
			// 소켓 위치 가져오기
			FHitResult Hit(ForceInit); // ForceInit으로 강제 초기화 가능
			UCameraComponent* Camera = MyCharacter->FindComponentByClass<UCameraComponent>();
			if (!Camera) return;

			// 2. 조준점은 카메라의 위치에서 시작합니다.
			FVector Start = Camera->GetComponentLocation();

			// 3. 방향은 카메라가 바라보는 방향(ForwardVector)으로 고정!
			FVector End = Start + (Camera->GetForwardVector() * Range);

			UKismetSystemLibrary::LineTraceSingle(
				this,
				Start,
				End,
				UEngineTypes::ConvertToTraceType(ECC_Visibility),
				false,
				{ this, GetOwner() },
				EDrawDebugTrace::ForDuration,
				Hit,
				true,
				FLinearColor::Red,
				FLinearColor::Green,
				5.f
			);
		}
	}
}

void ASandboxWeaponBase::PlaySound(USoundBase* Sound)
{
	UGameplayStatics::PlaySoundAtLocation(this, Sound, GetActorLocation());
}


void ASandboxWeaponBase::Reload()
{
	CurrentAmmo = MaxAmmo;
}

void ASandboxWeaponBase::UpdateAmmo()
{
	CurrentAmmo -= PelletCount;
}

void ASandboxWeaponBase::SetAiming(bool bNewAiming)
{
	bIsAiming = bNewAiming;

	// 조준 중일 때의 로직 (예: 탄퍼짐 감소)
	if (bIsAiming) {
		// CurrentSpread *= 0.5f; 
	}
}

SandboxFire는 적당히 조립해줬다.

 

캐릭터 BP 클래스 만들기

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

#pragma once

#include "CoreMinimal.h"
#include "SandboxWeaponBase.h"
#include "Camera/CameraComponent.h"         
#include "GameFramework/SpringArmComponent.h" 
#include "GameFramework/Character.h"
#include "SandboxPlayerCharacter.generated.h"

UCLASS()
class ADVANCEDHOMEWORK1_API ASandboxPlayerCharacter : public ACharacter
{
	GENERATED_BODY()

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

	// 무기 발사 함수
	UFUNCTION(BlueprintCallable)
	void StartFire();

	UFUNCTION(BlueprintCallable)
	void OnStartAiming();

	UFUNCTION(BlueprintCallable)
	void OnStopAiming();

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

	// 에디터에서 무기 클래스를 선택하기 위한 변수
	UPROPERTY(EditAnywhere, Category = "Combat")
	TSubclassOf<ASandboxWeaponBase> WeaponClass;

	UPROPERTY()
	ASandboxWeaponBase* CurrentWeapon;

	// C++에서 카메라와 스프링암을 제어할 수 있도록 선언
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
	UCameraComponent* FollowCamera;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
	USpringArmComponent* SpringArm;

	float TargetFOV = 90.0f;
	float TargetArmLength = 300.0f;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

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


#include "SandboxPlayerCharacter.h"

// Sets default values
ASandboxPlayerCharacter::ASandboxPlayerCharacter()
{
 	// 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;

    WeaponClass = nullptr;

    // 1. 스프링암 생성 및 설정
    SpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArm"));
    SpringArm->SetupAttachment(RootComponent); // 캡슐 컴포넌트(Root)에 부착
    SpringArm->TargetArmLength = 300.0f;       // 기본 거리

    // 2. 카메라 생성 및 스프링암에 부착
    FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
    FollowCamera->SetupAttachment(SpringArm);
}

void ASandboxPlayerCharacter::StartFire()
{
    // 무기가 유효한지 확인 후 발사
    if (CurrentWeapon)
    {
        CurrentWeapon->Fire();
    }
}

void ASandboxPlayerCharacter::OnStartAiming()
{
    if (CurrentWeapon) CurrentWeapon->SetAiming(true);

    // 조준 시작: 타겟 FOV를 60으로 설정 (Tick이 알아서 90 -> 60으로 이동시킴)
    TargetFOV = 60.0f;
    TargetArmLength = 100.f;
}

void ASandboxPlayerCharacter::OnStopAiming()
{
    if (CurrentWeapon) CurrentWeapon->SetAiming(false);

    // 조준 해제: 타겟 FOV를 다시 90으로 설정 (Tick이 알아서 60 -> 90으로 복구시킴)
    TargetFOV = 90.0f;
    TargetArmLength = 300.f;
}

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


    // 무기 스폰 및 부착 로직
    if (WeaponClass)
    {
        FActorSpawnParameters SpawnParams;
        SpawnParams.Owner = this; // 캐릭터를 주인으로 설정
        SpawnParams.Instigator = GetInstigator();

        // 무기 스폰
        CurrentWeapon = GetWorld()->SpawnActor<ASandboxWeaponBase>(WeaponClass, FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams);

        // 캐릭터 메쉬의 소켓에 부착
        if (CurrentWeapon)
        {
            FAttachmentTransformRules AttachmentRules(EAttachmentRule::SnapToTarget, true);
            CurrentWeapon->AttachToComponent(GetMesh(), AttachmentRules, FName("weapon_r_muzzle"));
        }
    }
}

// Called every frame
void ASandboxPlayerCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
    // 현재 FOV에서 타겟 FOV까지 부드럽게 보간 (FInterpTo)
    float CurrentFOV = FollowCamera->FieldOfView;
    FollowCamera->SetFieldOfView(FMath::FInterpTo(CurrentFOV, TargetFOV, DeltaTime, 10.0f));

    // 2. SpringArm 거리 보간
    float CurrentArmLength = SpringArm->TargetArmLength;
    SpringArm->TargetArmLength = FMath::FInterpTo(CurrentArmLength, TargetArmLength, DeltaTime, 10.0f);
}

// Called to bind functionality to input
void ASandboxPlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}

EAttachmentRule::SnapToTarget의 의미

이 규칙은 이름 그대로 "대상(소켓)에 딱 붙여버려라(Snap)"는 뜻.

주의할 점:

AttachToComponent는 액터의 Root Component를 소켓에 붙인다. 만약 무기 블루프린트의 Root는 DefaultSceneRoot이고, 실제 총 모델(Static Mesh)이 그 아래에서 멀리 떨어져(Offset) 있다면, 소켓에는 DefaultSceneRoot가 붙고 총 모델은 여전히 엉뚱한 곳에 떠 있게 된다.

 

1. FOV (Field of View, 시야각)

카메라 렌즈의 화각을 의미.

  • 90° (기본값): 일반적인 사람의 시야와 비슷하며, 주변 상황을 넓게 파악하기 좋습니다.
  • 60° (조준 시): 화면이 확대(Zoom-in)되는 효과를 줍니다.
    • 심리적 효과: 시야가 좁아지면서 주변 정보가 차단되고, 전방의 적에게만 집중하게 만드는 긴장감을 줍니다.
    • 물리적 효과: 멀리 있는 적이 더 크게 보여 정밀 조준이 가능해집니다.

2. TargetArmLength (스프링암 길이)

캐릭터와 카메라 사이의 물리적 거리.

  • 300 (기본값): 캐릭터의 전신이나 뒷모습이 시원하게 보이는 거리입니다.
  • 100~150 (조준 시): 카메라가 캐릭터의 어깨 쪽으로 바짝 다가붙습니다. (흔히 말하는 '숄더 뷰')

 

조준 기능은 캐릭터에 OnStartAiming과 OnStopAiming 함수를 만들고, 우클릭 인풋을 넣어 만들어준다.

Camera->FieldOfView 값을 60.f로 줄였다가 90.f로 돌린다. TargetArmLength는 300과 100을 왔다갔다하게 한다.

 

 

우클릭에 IA_Aim 할당