Showing

[Unreal] Line Trace를 이용하여 3D Player에서 NPC로의 시점 전환 본문

Unreal

[Unreal] Line Trace를 이용하여 3D Player에서 NPC로의 시점 전환

RabbitCode 2023. 11. 28. 14:36

3D 게임 상에서 플레이어가 NPC 혹은 상점에 닿으면 NPC의 시점으로 전환되는 연출을 종종 본 적이 있을 것이다.

Npc 혹은 상점에 다다가면, 화면이 전환되는 연출.

key point

-NPC도 카메라 소유

-NPC에게 트리거 박스 설치

-트리거 박스에 닿으면 플레이어 컨트롤러의 view를 NPC의 view로 switch

따라서 NPC를 따로 제작해주면 아래와 같다.

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

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "NPC.generated.h"
class UboxComponent;
UCLASS()
class THIRDPERSONTEMPLATE_API ANPC : public ACharacter
{
	GENERATED_BODY()

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

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

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

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

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
	class UCameraComponent* CameraComp;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
	class UBoxComponent* triggerBox;

	UFUNCTION()
	void OnOverlapBeginTB(class UPrimitiveComponent* OverlappedComponent, class AActor* OtherActor, class UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "NPC.h"
#include "Camera/CameraComponent.h"
#include "Components/BoxComponent.h"
#include <Kismet/GameplayStatics.h>

// Sets default values
ANPC::ANPC()
{
 	// 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;
	CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("ZoomCamera"));
	CameraComp->SetupAttachment(GetRootComponent());
	triggerBox = CreateDefaultSubobject<UBoxComponent>(TEXT("trigger Box"));
	triggerBox->SetupAttachment(GetRootComponent());
}

// Called when the game starts or when spawned
void ANPC::BeginPlay()
{
	Super::BeginPlay();
	triggerBox->OnComponentBeginOverlap.AddDynamic(this, &ANPC::OnOverlapBeginTB);
}

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

}

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

}

void ANPC::OnOverlapBeginTB(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (GEngine) {
		GEngine->AddOnScreenDebugMessage(-1, 0.5f, FColor::Yellow, TEXT("Debug"));
	}
	APlayerController* OurPlayerController = UGameplayStatics::GetPlayerController(this, 0);
	OurPlayerController->SetViewTargetWithBlend(this, 1.f);
	OurPlayerController->SetInputMode(FInputModeUIOnly());
	OurPlayerController->SetShowMouseCursor(true);
}

 

BP에서 콜리전 세팅

bp_에서 카메라 rot값을 적절히 변경해준다.

 

기존 3D Player의 코드는 아래와 같다

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

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "InputAction.h"
#include "NiagaraComponent.h"
#include "NiagaraFunctionLibrary.h"
#include "MyPlayer.generated.h"

class UInputMappingContext;
class UInputAction;
class UNiagaraComponent;

UCLASS()
class THIRDPERSONTEMPLATE_API AMyPlayer : public ACharacter
{
	GENERATED_BODY()

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

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

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

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

	UPROPERTY(EditAnywhere, Category = "Anim")
	UAnimMontage* attackMontage;

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Camera")
	class USpringArmComponent* springArmComp;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera")
	class UCameraComponent* cameraComp;

	UPROPERTY(EditAnywhere, Category = "Input")
	UInputMappingContext* PlayerMappingContext;

	UPROPERTY(VisibleAnywhere, Category = "Weapon")
	class UStaticMeshComponent* staffMesh;

	UPROPERTY(EditAnywhere, Category = "FX")
	class UNiagaraComponent* jumpNiagara;

	UPROPERTY(EditAnywhere, Category = "Input")
	UInputAction* MoveIA;

	UPROPERTY(EditAnywhere, Category = "Input")
	UInputAction* LookUpIA;

	UPROPERTY(EditAnywhere, Category = "Input")
	UInputAction* TurnIA;

	UPROPERTY(EditAnywhere, Category = "Input")
	UInputAction* JumpIA;

	UPROPERTY(EditAnywhere, Category = "Input")
	UInputAction* FireIA;

	UPROPERTY(EditAnywhere, Category = "Input")
	UInputAction* InteractionIA;

public:
	void Move(const FInputActionValue& Value);
	void LookUp(const FInputActionValue& Value);
	void Turn(const FInputActionValue& Value);
	void InputJump(const FInputActionValue& Value);
	void InputFire(const FInputActionValue& Value);
	void InteractionPositive(const FInputActionValue& Value);
	void Fire();
	float fireTimerTime; // 타이머의 시간
	bool fireReady; // 타이머 도달 여부 
public:
	UPROPERTY(EditAnywhere,BlueprintReadOnly, Category = "Status")
	float maxHp;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Status")
	float currentHp;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Status")
	float maxMp;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Status")
	float currentMp;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
	float interactionDistance;

	UPROPERTY(EditAnywhere, Category = "Move")
	float moveSpeed;

	UPROPERTY(EditAnywhere)
	TSubclassOf<class APBullet> bulletFactory;

	UPROPERTY(EditAnyWhere)
	float fireCoolTime = 2.0f;

	bool isJump = false;

private:
	FVector moveDirection;

protected:
	void FireCoolTimer(float cooltime, float deltaTime);
public:
	void ShowFX(bool show);
};
// Fill out your copyright notice in the Description page of Project Settings.

#include "MyPlayer.h"
#include "Components/ArrowComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "EnhancedInputSubsystems.h" //https://docs.unrealengine.com/5.0/en-US/enhanced-input-in-unreal-engine/
#include "EnhancedInputComponent.h" //https://docs.unrealengine.com/4.26/en-US/InteractiveExperiences/Input/EnhancedInput/
#include "PBullet.h"
#include "NiagaraComponent.h"
#include "NiagaraFunctionLibrary.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "DrawDebugHelpers.h"

// Sets default values
AMyPlayer::AMyPlayer()
{
 	// 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;
	//ConstructorHelpers 라는 유틸리티로 에셋 가져오기(찾기)
	// InitMesh 굳이 이렇게 하기는 했으나 콘텐츠 브라우저에서 선택하는게 사실 더 좋음
	ConstructorHelpers::FObjectFinder<USkeletalMesh> InitMesh(TEXT("/Script/Engine.SkeletalMesh'/Game/MyResouce/unitychan.unitychan'"));
	if (InitMesh.Succeeded()) { // 제대로 오브젝트를 가져왔다면~
		// GetMesh는 actor(근본)의 상속자인 character에서 제공!
		GetMesh()->SetSkeletalMesh(InitMesh.Object);
		GetMesh()->SetRelativeLocationAndRotation(
			FVector(0, 0, -88)
			, FRotator(0, -90, 0));
	}
	springArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArmComp"));
	springArmComp->SetupAttachment(RootComponent);
	springArmComp->SetRelativeLocationAndRotation(FVector(0, 0, 50), FRotator(-20,0,0));
	springArmComp->TargetArmLength = 530;
	springArmComp->bUsePawnControlRotation = true;

	//카메라도 생성 초기화
	cameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComponent"));
	cameraComp->SetupAttachment(springArmComp);
	cameraComp->bUsePawnControlRotation = false;

	//staff mesh
	staffMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("staff meshComp"));// 스태프 외관 컴포넌트 등록
	// 두번째 인자 캐릭터 메쉬에 부착, FName이 문자열 속도가 빠름
	staffMesh->SetupAttachment(GetMesh(), FName("Character1_RightHandSocket"));
	//staffMesh->SetRelativeLocationAndRotation

	//pawn이 들고 있음
	bUseControllerRotationYaw = true;
	moveSpeed = 100.0f;

	maxHp = 100;
	maxMp = 100;
	currentHp = 50;
	currentMp = 50;
	fireTimerTime = 0;
	fireCoolTime = 1.85f;
	fireReady = true;    // 발사 준비 On
}

// Called when the game starts or when spawned
void AMyPlayer::BeginPlay()
{
	Super::BeginPlay();
	if (APlayerController* PlayerController = Cast<APlayerController>(GetController()))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem
			= ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer())) {
			Subsystem->AddMappingContext(PlayerMappingContext, 0);
		}
	}
	jumpNiagara = GetComponentByClass<UNiagaraComponent>();
	jumpNiagara->SetVisibility(false);
}

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

	if (fireReady == false) {
		FireCoolTimer(fireCoolTime, DeltaTime);
	}

	const TArray<USceneComponent*>& AttachChildren = GetRootComponent()->GetAttachChildren();
	const int32 Count = AttachChildren.Num();


	// 이동방향을 컨트롤 방향 기준으로 변환
	moveDirection = FTransform(GetControlRotation()).TransformVector(moveDirection);

	/*
	// 플레이어 이동 - 등속운동
	FVector P0 = GetActorLocation();//현재 위치
	FVector vt = moveDirection * moveSpeed * DeltaTime; //이동거리
	FVector P = P0 + vt;
	SetActorLocation(P);
	*/

	AddMovementInput(moveDirection);

	// 방향 초기화
	// 방향이 누적되지 않게
	moveDirection = FVector::ZeroVector;
}

// Called to bind functionality to input
void AMyPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent)) {
		EnhancedInputComponent->BindAction(MoveIA, ETriggerEvent::Triggered, this, &AMyPlayer::Move);
		EnhancedInputComponent->BindAction(LookUpIA, ETriggerEvent::Triggered, this, &AMyPlayer::LookUp);
		EnhancedInputComponent->BindAction(TurnIA, ETriggerEvent::Triggered, this, &AMyPlayer::Turn);
		EnhancedInputComponent->BindAction(JumpIA, ETriggerEvent::Triggered, this, &AMyPlayer::InputJump);
		EnhancedInputComponent->BindAction(FireIA, ETriggerEvent::Triggered, this, &AMyPlayer::InputFire);
		EnhancedInputComponent->BindAction(InteractionIA, ETriggerEvent::Started, this, &AMyPlayer::InteractionPositive);
	}
}

void AMyPlayer::Move(const FInputActionValue& Value)
{
	UE_LOG(LogTemp, Log, TEXT("Log Message %f"), moveSpeed);
	const FVector _currentValue = Value.Get<FVector>();
	if (Controller) {
		moveDirection.Y = _currentValue.X;
		moveDirection.X = _currentValue.Y;
	}
	//FVector newLocation = GetActorLocation() + _currentValue;
	//SetActorLocation(newLocation);
}

void AMyPlayer::LookUp(const FInputActionValue& Value)
{
	// mouse y - 한 축의 값(float)
	const float _currentValue = Value.Get<float>();
	APawn::AddControllerPitchInput(_currentValue);
}

void AMyPlayer::Turn(const FInputActionValue& Value)
{
	// mouse x - 한 축의 값(float)
	const float _currentValue = Value.Get<float>();
	APawn::AddControllerYawInput(_currentValue);
}

void AMyPlayer::InputJump(const FInputActionValue& Value)
{
	Jump();
}

void AMyPlayer::InputFire(const FInputActionValue& Value)
{
	if (fireReady == true) {
		UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
		if (AnimInstance) {
			AnimInstance->Montage_Play(attackMontage);
		}
		fireReady = false;
	}
}

void AMyPlayer::InteractionPositive(const FInputActionValue& Value)
{
	/*
	//start와 end 가 중요
	FVector _Location;
	FRotator _Rotation;
	FHitResult _HitOut;

	//카메라가 보고 있는 위치
	GetController()->GetPlayerViewPoint(_Location, _Rotation);
	FVector _Start = _Location;
	int ViewDis = 2000;
	FVector _End = (_Rotation.Vector() * ViewDis); // 2000은 무작위 거리
	FCollisionQueryParams _traceParams; //trace의 params들
	GetWorld()->LineTraceSingleByChannel(_HitOut, _Start, _End, ECC_Visibility, _traceParams); // 충돌 결과, 시작점, 결과, 콜리전채널, params(안넣으면 디폴트)
	DrawDebugLine(GetWorld(), _Start, _End, FColor::Green, false, 2.0f);
	*/
	FHitResult _HitOut;
	FVector _Start = GetActorLocation();
	FVector _End = GetActorLocation() + (GetActorForwardVector() * interactionDistance);

	FCollisionQueryParams _TraceParams;
	GetWorld()->LineTraceSingleByChannel(_HitOut, _Start, _End, ECC_Visibility, _TraceParams);
	DrawDebugLine(GetWorld(), _Start, _End, FColor::Magenta, false, 3.0f);
}

void AMyPlayer::FireCoolTimer(float cooltime, float deltaTime)
{
	if (fireTimerTime < cooltime) {
		fireTimerTime += deltaTime;
	}
	else
	{
		fireTimerTime = 0;
		fireReady = true;
	}
}

void AMyPlayer::ShowFX(bool show)
{
	if (nullptr == jumpNiagara) {
		jumpNiagara = GetComponentByClass<UNiagaraComponent>();
	}
	jumpNiagara->SetVisibility(show);
}

void AMyPlayer::Fire()
{
	FTransform firePosition = staffMesh->GetSocketTransform(TEXT("FirePosition"));
	GetWorld()->SpawnActor<APBullet>(bulletFactory, firePosition);
}