안선생의 개발 블로그
[UE5 멀티플레이 게임 만들기]3. TopDownGame 형식 구현 본문
음 이번에는 플레이어 컨트롤러를 만들고 마우스 커서를 보이게 해서 마우스로 움직이게 해볼게요.
탑다운 형식으로 만들려고 합니다.
저도 공부하면서 하는거라 틀릴수도 있습니다 댓글로 알려주시면 감사하겠습니다!!
먼저 PlayerController클래스를 만들어줍니다.
먼저 헤더파일입니다.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "JH_PlayerController.generated.h"
class USplineComponent;
/**
*
*/
UCLASS()
class JH_MULTI_RPG_API AJH_PlayerController : public APlayerController
{
GENERATED_BODY()
protected:
AJH_PlayerController();
virtual void BeginPlay() override;
public:
virtual void PlayerTick(float DeltaTime) override;
void AutoRun();
void OnSetDestinationReleased(); // 목적지 설정
void OnTouchTriggered(); // 마우스 계속 클릭중
private:
FHitResult MouseHitResult;
/** 캐릭터 클릭 위치*/
FVector CachedDestination = FVector::ZeroVector;
/** 얼마나 눌렀는지*/
float FollowTime = 0.f;
/** 얼마나 짧게 눌렀는지 기준점*/
float ShortPressThreshold = 0.5f;
/** 언제 자동으로 움직여야하는지*/
bool bAutoRunning = false;
/** 얼마큼 와야 자동움직임 멈추는지*/
float AutoRunAcceptanceRadius = 50.f;
UPROPERTY(VisibleAnywhere)
TObjectPtr<USplineComponent> Spline;
public:
FORCEINLINE FHitResult GetMouseHitReulst() const { return MouseHitResult; }
};
CPP
// Fill out your copyright notice in the Description page of Project Settings.
#include "Player/JH_PlayerController.h"
#include "Components/SplineComponent.h"
#include "NavigationSystem.h"
#include "NavigationPath.h"
AJH_PlayerController::AJH_PlayerController()
{
Spline = CreateDefaultSubobject<USplineComponent>(TEXT("Spline"));
}
void AJH_PlayerController::BeginPlay()
{
Super::BeginPlay();
if (IsLocalController())
{
bShowMouseCursor = true; //마우스 커서 보이게함
}
}
void AJH_PlayerController::PlayerTick(float DeltaTime)
{
Super::PlayerTick(DeltaTime);
GetHitResultUnderCursor(ECC_Visibility, false, MouseHitResult);
}
void AJH_PlayerController::AutoRun()
{
//클라이언트도 실행하라면 에디터에서 프로젝트 세팅 Nvation System-> Allow Clinet Side Navigation 체크해줘야함
if (!bAutoRunning) return;
APawn* ControllerPawn = GetPawn();
//ControlledPawn의 위치를 기반으로 Spline 상에서 가장 가까운 위치를 찾아 LocationOnSpline에 저장합니다.
const FVector LocationOnSpline = Spline->FindLocationClosestToWorldLocation(ControllerPawn->GetActorLocation(), ESplineCoordinateSpace::World);
// LocationOnSpline 위치에서 가장 가까운 지점의 방향을 찾아내줌
const FVector Direction = Spline->FindDirectionClosestToWorldLocation(LocationOnSpline, ESplineCoordinateSpace::World);
//그 방향으로 이동
ControllerPawn->AddMovementInput(Direction);
//캐릭터 위치에서 다음 Spline위치 사이 거리
const float DistanceToDestination = (LocationOnSpline - CachedDestination).Length();
//위치에 도착하면
if (DistanceToDestination <= AutoRunAcceptanceRadius)
{
bAutoRunning = false;
}
}
void AJH_PlayerController::OnSetDestinationReleased()
{
const APawn* ControllerPawn = GetPawn();
if (FollowTime <= ShortPressThreshold)
{
//포인트를 지워줌
Spline->ClearSplinePoints();
/**
* 내비게이션 시스템에서 특정 위치로 경로를 동기적으로 찾는 함수입니다.
*이 함수는 주어진 시작 위치에서 목적지 위치까지의 경로를 계산하고 그 결과를 즉시 반환합니다.
*동기적이라는 것은 함수가 경로를 계산할 때까지 대기한다는 것을 의미합니다.
*따라서 이 함수가 완료될 때까지 프로그램이 다음 코드로 진행하지 않습니다.
*/
if (UNavigationPath* NavPath = UNavigationSystemV1::FindPathToLocationSynchronously(this, ControllerPawn->GetActorLocation(), CachedDestination))
{
for (const FVector& PointLoc : NavPath->PathPoints)
{
//스필라인 포인트를 추가시켜줌
Spline->AddSplinePoint(PointLoc, ESplineCoordinateSpace::World);
}
if (NavPath->PathPoints.Num() > 0)
{
//캐릭터 클릭 위치를 마지막 지점으로 해줌
CachedDestination = NavPath->PathPoints[NavPath->PathPoints.Num() - 1];
// 자동으로 그위치로 감
bAutoRunning = true;
}
}
}
FollowTime = 0.f;
}
void AJH_PlayerController::OnTouchTriggered()
{
APawn* ControllerPawn = GetPawn();
FollowTime += GetWorld()->GetDeltaSeconds();
if (MouseHitResult.bBlockingHit)
{
CachedDestination = MouseHitResult.ImpactPoint;
}
const FVector WorldDirection = (CachedDestination - ControllerPawn->GetActorLocation()).GetSafeNormal();
ControllerPawn->AddMovementInput(WorldDirection);
}
음 저는 SplineComponet를 추가해서 최적의 경로를 구했습니다.
Cpp하나하나 알아 볼게요
생성자
생성자는 Spline컴포넌트를 추가해줬어요
Beginplay
마우스가 보여야 가고싶은 위치를 선택할 수 있으니 마우스를 보이게 해줬습니다.
OnTouchTriggered()
void AJH_PlayerController::OnTouchTriggered()
{
APawn* ControllerPawn = GetPawn();
FollowTime += GetWorld()->GetDeltaSeconds(); // 몇초동안 누르고 있었나?
if (MouseHitResult.bBlockingHit) // 마우스 아래 뭔가 있으면
{
CachedDestination = MouseHitResult.ImpactPoint; // 위치를 마우스 히트지점으로 바꿔준다.
}
const FVector WorldDirection = (CachedDestination - ControllerPawn->GetActorLocation()).GetSafeNormal();
ControllerPawn->AddMovementInput(WorldDirection);
}
이 함수는 마우스를 계속 누르고 있으면 실행되는 함수에요 마우스를 쭉 누르고 있으면 그 지역으로 가야겠죠?
1. 몇초동안 눌렀는지 FollowTime변수에 저장(짧게 눌렀으면 그지역으로 자동으로 가야하기 때문)
2. 마우스 지점아래 Hit가 있으면 CachedDestination변수에 마우스 히트위치 저장
3. CachedDestination - ControllerPawn->GetActorLocation()에 뺀 값을 정규화 해줌(내 캐릭터에서 마우스 아래지점 방향으로 정규화)
4. 정규화된 방향으로 이동
OnSetDestinationReleased()
void AJH_PlayerController::OnSetDestinationReleased()
{
const APawn* ControllerPawn = GetPawn();
if (FollowTime <= ShortPressThreshold)
{
//포인트를 지워줌
Spline->ClearSplinePoints();
/**
* 내비게이션 시스템에서 특정 위치로 경로를 동기적으로 찾는 함수입니다.
*이 함수는 주어진 시작 위치에서 목적지 위치까지의 경로를 계산하고 그 결과를 즉시 반환합니다.
*동기적이라는 것은 함수가 경로를 계산할 때까지 대기한다는 것을 의미합니다.
*따라서 이 함수가 완료될 때까지 프로그램이 다음 코드로 진행하지 않습니다.
*/
if (UNavigationPath* NavPath = UNavigationSystemV1::FindPathToLocationSynchronously(this, ControllerPawn->GetActorLocation(), CachedDestination))
{
for (const FVector& PointLoc : NavPath->PathPoints)
{
//스필라인 포인트를 추가시켜줌
Spline->AddSplinePoint(PointLoc, ESplineCoordinateSpace::World);
}
if (NavPath->PathPoints.Num() > 0)
{
//캐릭터 클릭 위치를 마지막 지점으로 해줌
CachedDestination = NavPath->PathPoints[NavPath->PathPoints.Num() - 1];
// 자동으로 그위치로 감
bAutoRunning = true;
}
}
}
FollowTime = 0.f;
}
이 함수는 마우스 우클릭이 끝나면 실행되는 함수입니다.
만약 우클릭이 시간이 짧았으면 네비게이션시스템을 이용하여 동기적으로 최적의 경로를 찾아줍니다.
그 후 Spline포인트에 추가 시켜줍니다.
캐릭터 클릭 위치를 NavPath포인트 마지막 지점으로 해주고
bAutoRunning true로 해줍니다(Tick함수에서 실행할려고)
FollowTime을 다시 초기화(다음 우클릭 시간을 알아야 함)
AutoRun()
void AJH_PlayerController::AutoRun()
{
//클라이언트도 실행하라면 에디터에서 프로젝트 세팅 Nvation System-> Allow Clinet Side Navigation 체크해줘야함
if (!bAutoRunning) return;
APawn* ControllerPawn = GetPawn();
//ControlledPawn의 위치를 기반으로 Spline 상에서 가장 가까운 위치를 찾아 LocationOnSpline에 저장합니다.
const FVector LocationOnSpline = Spline->FindLocationClosestToWorldLocation(ControllerPawn->GetActorLocation(), ESplineCoordinateSpace::World);
// LocationOnSpline 위치에서 가장 가까운 지점의 방향을 찾아내줌
const FVector Direction = Spline->FindDirectionClosestToWorldLocation(LocationOnSpline, ESplineCoordinateSpace::World);
//그 방향으로 이동
ControllerPawn->AddMovementInput(Direction);
//캐릭터 위치에서 다음 Spline위치 사이 거리
const float DistanceToDestination = (LocationOnSpline - CachedDestination).Length();
//위치에 도착하면
if (DistanceToDestination <= AutoRunAcceptanceRadius)
{
bAutoRunning = false;
}
캐릭터가 우클릭 한 시간이 짧았으면 자동으로 그 지역까지 가게 해주는 함수 입니다. 주석을 보시면 이해 되실거라 생각합니다.
그다음 캐릭터 클래스에서 호출해줘야 합니다. InputAction이 다 거기 있기 때문(저는 캐릭터를 3개정도 구현해볼라 합니다. 그래서 캐릭터마다 InputAction이 달라야기 때문에 캐릭터에 구현했음, 캐릭터 하나만 구현할거면 PlayerController에서 Input을 하는게 일방적)
캐릭터 클래스.h
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Logging/LogMacros.h"
#include "JHCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputMappingContext;
class UInputAction;
struct FInputActionValue;
class AJH_PlayerController;
DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
UCLASS(config=Game)
class AJHCharacter : public ACharacter
{
GENERATED_BODY()
/** Camera boom positioning the camera behind the character */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
USpringArmComponent* CameraBoom;
/** Follow camera */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
UCameraComponent* FollowCamera;
/** MappingContext */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputMappingContext* DefaultMappingContext;
/** Jump Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* JumpAction;
/** Move Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* MoveAction;
/** Look Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* LookAction;
/** 카메라 확대 액션 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* CameraZoomInAction;
/** 카매라 축소 액션 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* CameraZoomOutAction;
/** 캐릭터 시점 액션 */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* FreeViewAction;
public:
AJHCharacter();
virtual void Tick(float DeltaTime) override;
protected:
/** Called for movement input */
void Move();
void MoveRelesed();
/** Called for looking input */
void Look(const FInputActionValue& Value);
/** 카메라줌인 콜백함수*/
void CameraZoomIn();
/** 카메라줌아웃 콜백함수*/
void CameraZoomOut();
///** 자유시점 콜백함수*/
//void FreeView();
///** 원래시점 콜백함수*/
//void OriginalView();
protected:
// APawn interface
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
// To add mapping context
virtual void BeginPlay();
private:
/** 카메라 줌 업 다운 값*/
UPROPERTY(EditDefaultsOnly,Category = "Camera")
float CameraZoomValue = 40;
/** 알트키를 눌렀는 가*/
UPROPERTY(EditDefaultsOnly, Category = "Camera")
bool bIsAltKeyPressing = false;
UPROPERTY()
TObjectPtr<AJH_PlayerController> JH_PlayerController;
public:
/** Returns CameraBoom subobject **/
FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
/** Returns FollowCamera subobject **/
FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
};
Private에 PlayerController포인터를 추가해줬습니다.
cpp
// Copyright Epic Games, Inc. All Rights Reserved.
#include "JH_Multi_RPG/Public/Character/JHCharacter.h"
#include "Engine/LocalPlayer.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/Controller.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"
#include "Player/JH_PlayerController.h"
DEFINE_LOG_CATEGORY(LogTemplateCharacter);
AJHCharacter::AJHCharacter()
{
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
GetCharacterMovement()->JumpZVelocity = 700.f;
GetCharacterMovement()->AirControl = 0.35f;
GetCharacterMovement()->MaxWalkSpeed = 500.f;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f;
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 400.0f;
CameraBoom->bUsePawnControlRotation = true;
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
}
void AJHCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if(IsLocallyControlled() && JH_PlayerController)
{
JH_PlayerController->AutoRun();
}
}
void AJHCharacter::BeginPlay()
{
Super::BeginPlay();
if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(DefaultMappingContext, 0);
}
}
if (IsLocallyControlled())
{
JH_PlayerController = Cast<AJH_PlayerController>(Controller);
}
}
void AJHCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
// Set up action bindings
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {
// Jumping
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
// Moving
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AJHCharacter::Move);
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Completed, this, &AJHCharacter::MoveRelesed);
// Looking
// EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AJHCharacter::Look);
// 카메라 줌업
EnhancedInputComponent->BindAction(CameraZoomInAction, ETriggerEvent::Started, this, &AJHCharacter::CameraZoomIn);
// 카메라 줌다운
EnhancedInputComponent->BindAction(CameraZoomOutAction, ETriggerEvent::Started, this, &AJHCharacter::CameraZoomOut);
}
else
{
UE_LOG(LogTemplateCharacter, Error, TEXT("'%s' Failed to find an Enhanced Input component! This template is built to use the Enhanced Input system. If you intend to use the legacy system, then you will need to update this C++ file."), *GetNameSafe(this));
}
}
void AJHCharacter::Move()
{
if (JH_PlayerController)
{
JH_PlayerController->OnTouchTriggered();
}
}
void AJHCharacter::MoveRelesed()
{
if (JH_PlayerController)
{
JH_PlayerController->OnSetDestinationReleased();
}
}
//void AJHCharacter::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);
// }
//}
void AJHCharacter::CameraZoomIn()
{
if (IsLocallyControlled())
{
CameraBoom->TargetArmLength = CameraBoom->TargetArmLength - CameraZoomValue;
}
}
void AJHCharacter::CameraZoomOut()
{
CameraBoom->TargetArmLength = CameraBoom->TargetArmLength + CameraZoomValue;
}
//void AJHCharacter::FreeView()
//{
// bIsAltKeyPressing = true;
//}
//
//void AJHCharacter::OriginalView()
//{
// bIsAltKeyPressing = false;
//}
Tick함수에서 AutoRun을 실행줬고,
BeginPlay에서 Cast를 통해 컨트롤러를 가져왔습니다.
SetInputComponet에서 Triggered과 Competed를 만들었습니다.
MoveAction콜백 함수인
Move는 OnTouchTriggered()를 호출해주고
MoveReleased호출
실행해보겠습니다.
먼저 InputAction에 들어갈게요
값타입을 bool로 바꿔줍니다. 그 다음 트리거 두개를 추가하고 길게 누르기 시간 한계치를 0.1로 바꿔줍니다.
그다음에
입력 매핑 컨텍스트에 들어가서 Move를 우클릭으로 바꿔줄게요
그런다음 내비메시 바운드 볼륨을 추가해줄게요
추가 해준다음에 P를 누르면 초록색으로 볼 수 있는게 초록부분이 활성화된 부분입니다. 이 지역에 있어야 움직일 수 있으니 늘려줄게요
그런다음에 실행해보면 마우스 우클릭으로 움직일 수 있습니다!
서버와 클라이언트 둘다에서 실행해볼게요
플레이어 3명으로 리슨서버에서 실행해볼게요
근데 클라이언트에서 실행하면 자동으로 가는게 안될거에요
그 이유는 클라이언트에서 내비게이션 볼륨이 있는걸 몰라서 그래요
알 수 있게 해주겠습니다.
프로젝트 -> 프로젝트 세팅 -> 내비게이션 시스템
클라이언트 측면 내비게이션 허용 체크해주시면 클라이언트도 움직일 수 있습니다.!
다음에 계속 해볼게요!
'언리얼 > 멀티플레이 게임 만들어보자' 카테고리의 다른 글
[UE5 멀티플레이 게임 만들기] 6. 스킬 실행(RPC) (0) | 2024.01.05 |
---|---|
[UE5 멀티플레이 게임 만들기] 5. 스킬 만들기2 (2) | 2023.12.29 |
[UE5 멀티플레이 게임 만들기] 4. 스킬 만들기 (1) | 2023.12.26 |
[UE5 멀리플레이 게임 만들기] 2. 에셋 및 애니메이션 (0) | 2023.12.18 |
[UE5 멀티플레이 게임 만들기] 1. 프로젝트 생성 (0) | 2023.11.20 |