캐릭터 컨트롤

  • 일반적으로 컨트롤러, 폰, 카메라, 스프링암, 캐릭터 무브먼트 요소를 사용해 설정
  • 컨트롤러 : 입력자의 목표 지점을 지정할 때 사용, ControlRotation 속성
  • 폰 : 폰의 트랜스폼 지정
  • 카메라 : 화면 구도를 설정하기 위해 사용, 주로 1인칭 시점에서 사용
  • 스프링 암 : 화면 구도를 설정하기 위해 사용, 주로 3인칭 시점에서 사용
  • 캐릭터 무브먼트 : 캐릭터의 이동과 회전을 조정하는 용도로 사용

Rotation(현재 상태)에서 Desired Rotation(목표 상태)로 한번에 회전하면 부자연스러우므로 각속도로 회전해야 함

 

폰의 이동 함수

  • Look : 마우스 입력으로부터 컨트롤러의 컨트롤 회전을 설정
  • Move : 컨트롤러의 컨트롤 회전으로부터 Yaw 값을 참고해 이동 방향을 설정
  • 콘솔 커맨드 창(단축키 ~)에서 Control Rotation 값을 확인할 수 있음
    • 콘솔 창에 Display 클래스이름 속성이름을 입력하면 속성 값을 확인할 수 있음

폰의 컨트롤 옵션

  • Use Controller Rotation (Pitch/Yaw/Roll)
  • 폰의 회전을 컨트롤러의 Control Rotation과 동기화됨
    • 일정 속도로 회전하는 것이 아닌 바로 동기화

스프링암의 컨트롤 옵션

  • Use Pawn Control Rotation : 스프링 암의 회전을 컨트롤러의 Control Rotation과 동기화됨
  • Inherit (Yaw/Roll/Pitch) : 부모 컴포넌트의 회전 값을 상속받아 최종 회전을 구함
  • Do Collision Test : 카메라와 캐릭터 사이를 지탱해주는 스프링 암 중간에 장애물이 생겼을 때 장애물 앞으로 카메라를 당기는 옵션

카메라의 컨트롤 옵션

  • Use Pawn Control Rotation : 스프링 암에 달린 카메라의 회전을 컨트롤러의 Control Rotation과 동기화, 주로 1인칭

캐릭터 무브먼트 옵션

  • MovementMode : 각 모드별로 이동이 다름, 이동 기능을 끄고 싶으면 None
  • MaxWalkSpeed : 이동 모드에서의 이동 수치
  • JumpZVelocity : 점프의 속도
  • Rotation Rate : 회전 속도
  • Use Controller Desired Rotation : 컨트롤 회전을 목표 회전으로 삼고 지정한 속도로 돌리기
  • Orient Rotation To Movement : 캐릭터 이동 방향에 회전을 일치시키기
  • 폰의 회전 옵션과 충돌나지 않도록 주의

Look 함수의 AddControllerYaw를 살펴보자

// ABCharacterPlayer.cpp
void AABCharacterPlayer::Look(const FInputActionValue& Value)
{
	FVector2D LookAxisVector = Value.Get<FVector2D>();

	AddControllerYawInput(LookAxisVector.X);
	AddControllerPitchInput(LookAxisVector.Y);
}

// Pawn.cpp
void APawn::AddControllerYawInput(float Val)
{
	if (Val != 0.f && Controller && Controller->IsLocalPlayerController())
	{
		APlayerController* const PC = CastChecked<APlayerController>(Controller);
		PC->AddYawInput(Val);
	}
}

// PlayerController.cpp
// Input으로 들어온 Yaw 값을 RotationInput에 저장
void APlayerController::AddYawInput(float Val)
{
	RotationInput.Yaw += !IsLookInputIgnored() ? Val * (GetDefault<UInputSettings>()->bEnableLegacyInputScales ? InputYawScale_DEPRECATED : 1.0f) : 0.0f;
}

// 저장된 RotationInput로 Delta Rotation을 구하고, 변화한 Rotation 값을 설정
void APlayerController::UpdateRotation( float DeltaTime )
{
	// Calculate Delta to be applied on ViewRotation
	FRotator DeltaRot(RotationInput);

	FRotator ViewRotation = GetControlRotation();

	if (PlayerCameraManager)
	{
		PlayerCameraManager->ProcessViewRotation(DeltaTime, ViewRotation, DeltaRot);
	}

	...
    
	SetControlRotation(ViewRotation);
	
    ...
}

 

 

데이터 에셋

  • UDataAsset을 상속받은 언리얼 오브젝트 클래스
  • 주요 옵션을 모아 에디터에서 에셋 형태로 편리하게 데이터를 관리
  • 런타임에 에셋을 교체하여 설정을 변경할 수 있음
  • 각 섹션(Pawn, Character Movement, Input, SpirngArm) 별로 데이터를 저장

데이터 에셋으로 카메라 뷰 변경하는 방법

  • 데이터 에셋 생성 후 뷰 타입(Quater, Shoulder)에 따른 데이터 에셋을 저장
  • 쿼터 뷰일 때 화면을 회전할 필요는 없으므로 Input Mapping Context를 분리
  • 키를 입력했을 때 뷰가 변경되어 데이터 에셋에 따른 설정, Input Mapping Context 변경
// UABCharacterControllData.h
// 해당 클래스로 데이터 에셋을 생성 후 뷰에 맞게 설정 변경
class ARENABATTLE_API UABCharacterControllData : public UPrimaryDataAsset
{
	GENERATED_BODY()
	
public:
	UPROPERTY(EditAnywhere, Category = Pawn)
	uint32 bUseControllerRotationYaw : 1;

	UPROPERTY(EditAnywhere, Category = CharacterMovement)
	uint32 bOrientRotationToMovement : 1;

	UPROPERTY(EditAnywhere, Category = CharacterMovement)
	uint32 bUseControllerDesiredRotation : 1;

	UPROPERTY(EditAnywhere, Category = CharacterMovement)
	FRotator RotationRate;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
	TObjectPtr<class UInputMappingContext> InputMappingContext;

	UPROPERTY(EditAnywhere, Category = SpringArm)
	float TargetArmLength = 400.0f;

	UPROPERTY(EditAnywhere, Category = SpringArm)
	FRotator RelativeRotation;

	UPROPERTY(EditAnywhere, Category = SpringArm)
	uint32 bUsePawnControlRotation : 1;

	UPROPERTY(EditAnywhere, Category = SpringArm)
	uint32 bInheritPitch : 1;

	UPROPERTY(EditAnywhere, Category = SpringArm)
	uint32 bInheritYaw : 1;

	UPROPERTY(EditAnywhere, Category = SpringArm)
	uint32 bInheritRoll : 1;

	UPROPERTY(EditAnywhere, Category = SpringArm)
	uint32 bDoCollisionTest : 1;
};

// ABCharacterBase.cpp
void AABCharacterPlayer::BeginPlay()
{
	Super::BeginPlay();

	SetCharacterControl(CurrentCharacterControlType);
}

void AABCharacterPlayer::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	// EnhancedInput을 사용하지 않을 경우 에러를 발생하도록 CastChecked 사용
	UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent);

	EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ACharacter::Jump);
	EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
	EnhancedInputComponent->BindAction(ChangeControlAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::ChangeCharacterControl);
	EnhancedInputComponent->BindAction(ShoulderMoveAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::ShoulderMove);
	EnhancedInputComponent->BindAction(ShoulderLookAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::ShoulderLook);
	EnhancedInputComponent->BindAction(QuaterMoveAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::QuaterMove);
}

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

	float MovementVectorSize = 1.0f;
	float MovementVectorSizeSqaured = MovementVector.SquaredLength();
	
	if (MovementVectorSizeSqaured > 1.0f)
	{
		MovementVector.Normalize();
		MovementVectorSizeSqaured = 1.0f;
	}
	else
	{
		MovementVectorSize = FMath::Sqrt(MovementVectorSizeSqaured);
	}

	const FVector MoveDirection = FVector(MovementVector.X, MovementVector.Y, 0.0f);
	GetController()->SetControlRotation(FRotationMatrix::MakeFromX(MoveDirection).Rotator());
	AddMovementInput(MoveDirection, MovementVectorSize);
}

void AABCharacterPlayer::ChangeCharacterControl()
{
	if (CurrentCharacterControlType == ECharacterControlType::Quater)
	{
		SetCharacterControl(ECharacterControlType::Shoulder);
	}
	else if (CurrentCharacterControlType == ECharacterControlType::Shoulder)
	{
		SetCharacterControl(ECharacterControlType::Quater);
	}
}

void AABCharacterPlayer::SetCharacterControl(ECharacterControlType NewCharacterControlType)
{
	UABCharacterControllData* NewCharacterControlData = CharacterControlManager[NewCharacterControlType];
	check(NewCharacterControlData);

	SetCharacterControlData(NewCharacterControlData);

	APlayerController* PlayerController = CastChecked<APlayerController>(GetController());
	if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
	{
		Subsystem->ClearAllMappings();
		UInputMappingContext* NewMappingContext = NewCharacterControlData->InputMappingContext;
		if (NewMappingContext)
		{
			Subsystem->AddMappingContext(NewMappingContext, 0); // 두 번째 매개변수는 우선순위로 다양한 입력이 겹칠 때 우선 순위 높은 것이 작동
		}
	}

	CurrentCharacterControlType = NewCharacterControlType;
}

Roll : X축을 기준으로 회전반경

Yaw : Z축을 기준으로 회전반경

Pitch : Y축을 기준으로 회전반경

액터의 구조

  • 월드에 속한 컨텐츠의 기본 단위
  • 트랜스폼을 가지며 월드로부터 틱과 시간 서비스를 제공받음
  • 논리적 개념일뿐 컴포넌트를 감싼 포장 박스에 불과
  • 실질적인 구현은 컴포넌트가 진행하고 액터는 다수의 컴포넌트를 소유하고 있음
  • 다수의 컴포넌트를 대표하는 컴포넌트를 루트 컴포넌트라고 함
  • 액터는 루트 컴포넌트를 가져야 하며 루트 컴포넌트의 트랜스폼은 액터의 트랜스폼을 의미

C++ 액터의 컴포넌트 생성

  • 컴포넌트는 언리얼 오브젝트이므로 UPROPERTY를 설정하고 TObjectPtr로 포인터를 선언한다
  • 컴포넌트 등록
    • CDO에서 생성한 컴포넌트는 자동으로 월드에 등록됨
    • NewObject로 생성한 컴포넌트는 반드시 등록절차를 거쳐야 함(예) RegisterComponent)
    • 등록된 컴포넌트는 월드의 기능을 사용할 수 있으며, 물리와 렌더링 처리에 합류함
  • UPROPERTY 지정자
    • Visible(객체) / Edit(값)
    • Anyware / DefaultsOnly / InstanceOnly : 에디터에서 편집 가능 영역
    • BlueprintReadOnly / BlueprintReadWrite : 블루프린트로 확장시 읽기 혹은 읽기쓰기 권한 부여
    • Category : 에디터 편집 영역(Detail)에서의 카테고리 지정

ABFountain.cpp

#include "Components/StaticMeshComponent.h"

// Sets default values
AABFountain::AABFountain()
{
 	// 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;
	
    // 컴포넌트 생성
	Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Body"));
	Water = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Water"));

	RootComponent = Body;
	Water->SetupAttachment(Body);
	Water->SetRelativeLocation(FVector(0.0f, 0.0f, 132.0f));
	
    // 컴포넌트의 StaticMesh 지정
	static ConstructorHelpers::FObjectFinder<UStaticMesh> BodyMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Castle_Fountain_01.SM_Plains_Castle_Fountain_01'"));
	if (BodyMeshRef.Object)
	{
		Body->SetStaticMesh(BodyMeshRef.Object);
	}

	static ConstructorHelpers::FObjectFinder<UStaticMesh> WaterMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Plains_Fountain_02.SM_Plains_Fountain_02'"));
	if (WaterMeshRef.Object)
	{
		Water->SetStaticMesh(WaterMeshRef.Object);
	}
}

 

C++ 액터를 부모로 하는 블루프린트 액터를 만들 수 있음

  • C++로 기반을 충분히 다져놓고 추가적인 로직은 블루프린트를 사용하여 필요한 만큼만 확장하도록 설계해야 함

  • 액터를 상속받은 특별한 액터, 플레이어가 빙의해 입출력을 처리하도록 설계됨
  • 길찾기를 할 수 있음
  • 일반적으로 세 가지 주요 컴포넌트로 구성됨
    • 기믹과 상호작용을 담당하는 충돌 컴포넌트(루트 컴포넌트)
    • 시각적인 비주얼을 담당하는 메시 컴포넌트
    • 움직임을 담당하는 컴포넌트
    • 컴포넌트 중에서 트랜스폼 없이 기능만 제공하는 컴포넌트를 액터 컴포넌트라고 함

캐릭터

  • 인간형 폰을 구성하도록 언리얼이 제공하는 전문 폰 클래스를 의미
  • 세 가지 주요 컴포넌트로 구성됨
    • 기믹과 상호작용을 담당하는 캡슐 컴포넌트(루트 컴포넌트)
    • 애니메이션 캐릭터를 표현하는 스켈레탈 메시 컴포넌트
    • 캐릭터의 움직임을 담당하는 캐릭터 무브먼트 컴포넌트

캐릭터 생성

ABCharacterBase.cpp

#include "Character/ABCharacterBase.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"

AABCharacterBase::AABCharacterBase()
{
	// Pawn
		...

	// Capsule
		...

	// Movement
		...

	// Mesh
	GetMesh()->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, -100.0f), FRotator(0.0f, -90.0f, 0.0f));
	GetMesh()->SetAnimationMode(EAnimationMode::AnimationBlueprint);
	GetMesh()->SetCollisionProfileName(TEXT("CharacterMesh"));

	static ConstructorHelpers::FObjectFinder<USkeletalMesh> CharacterMeshRef(TEXT("/Script/Engine.SkeletalMesh'/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple.SKM_Quinn_Simple'"));
	if (CharacterMeshRef.Object)
	{
		GetMesh()->SetSkeletalMesh(CharacterMeshRef.Object);
	}

	static ConstructorHelpers::FClassFinder<UAnimInstance> AnimInstanceClassRef(TEXT("/Game/Characters/Mannequins/Animations/ABP_Quinn.ABP_Quinn_C"));
	if (AnimInstanceClassRef.Class)
	{
		GetMesh()->SetAnimInstanceClass(AnimInstanceClassRef.Class);
	}
}

 

ABGameMode.cpp

#include "Game/ABGameMode.h"

AABGameMode::AABGameMode()
{
	static ConstructorHelpers::FClassFinder<APawn> DefaultPawnClassRef(TEXT("/Script/ArenaBattle.ABCharacterPlayer"));
	if (DefaultPawnClassRef.Class)
	{
		DefaultPawnClass = DefaultPawnClassRef.Class;
	}

	static ConstructorHelpers::FClassFinder<APlayerController> PlayerControllerClassRef(TEXT("/Script/ArenaBattle.ABPlayerController"));
	if (PlayerControllerClassRef.Class)
	{
		PlayerControllerClass = PlayerControllerClassRef.Class;
	}
}

 

입력시스템의 동작 방식

https://dev.epicgames.com/documentation/en-us/unreal-engine/input-overview-in-unreal-engine?application_version=5.0

  • 플레이어의 입력은 컨트롤러를 통해 폰으로 전달됨
  • 입력을 컨트롤러가 처리할 수도, 폰이 처리할 수도 있지만 일반적으로 폰이 처리하도록 설정
    • 다양한 탑승을 하는 경우 플레이어 컨트롤러 코드가 커지기 때문에 코드가 분산되도록 폰에서 처리하는 것이 유리

향상된 입력시스템

  • 기존 입력시스템을 대체하기 위해 언리얼 5.1부터 도입
  • 사용자의 입력 설정 변경에 유연하게 대처할 수 있도록 함
  • 사용자 입력 처리를 네 단계로 세분화하고 각 설정을 독립적인 에셋으로 대체
  • 플레이어의 최종 입력을 게임 로직에서 진행
  • 동작 구성
    • 입력 맵핑 컨텍스트 : 게임 패드용 입력 맵핑 컨텍스트, 키보드용 입력 맵핑 컨텍스트를 런타임에서 바꿀 수 있도록 설계 가능
    • 액션
      • 입력값 변조 : 입력을 변조 하여 조정할 수 있음
      • 이벤트 활성화 : 일반 버튼 / 축 이동 버튼 / 일정 이상 누르는 등 조건을 설정

  • Swizzle Input Axis Values를 통해 XY 입력을 실제 이동에 YX로 변환하는 기능
    • 즉, D를 누르게 된다면 (1, 0) 이라는 값이 전달되면 Swizzle Input Axis Value 라는 Modifier를 통해 로직에서는 (0, 1)로 전달되고 언리얼 엔진에서 양의 y축은 right 이므로 우측으로 이동하게 될 것이다.

 

ArenaBattle.Build.cs

// EnhancedInput 모듈을 추가해야 함
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput" });

 

ABCharacterPlayer.cpp

#include "Character/ABCharacterPlayer.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "InputMappingContext.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"

AABCharacterPlayer::AABCharacterPlayer()
{
	// Camera
	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;

	// Input
	static ConstructorHelpers::FObjectFinder<UInputMappingContext> InputMappingContextRef(TEXT("/Script/EnhancedInput.InputMappingContext'/Game/ArenaBattle/Input/IMC_Default.IMC_Default'"));
	if (InputMappingContextRef.Object)
	{
		DefaultMappingContext = InputMappingContextRef.Object;
	}

	static ConstructorHelpers::FObjectFinder<UInputAction> InputActionMoveRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_Move.IA_Move'"));
	if (InputActionMoveRef.Object)
	{
		MoveAction = InputActionMoveRef.Object;
	}

	static ConstructorHelpers::FObjectFinder<UInputAction> InputActionJumpRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_Jump.IA_Jump'"));
	if (InputActionJumpRef.Object)
	{
		JumpAction = InputActionJumpRef.Object;
	}

	static ConstructorHelpers::FObjectFinder<UInputAction> InputActionLookRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_Look.IA_Look'"));
	if (InputActionLookRef.Object)
	{
		LookAction = InputActionLookRef.Object;
	}
}

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

	APlayerController* PlayerController = CastChecked<APlayerController>(GetController());
	if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
	{
		Subsystem->AddMappingContext(DefaultMappingContext, 0); // 두 번째 매개변수는 우선순위로 다양한 입력이 겹칠 때 우선 순위 높은 것이 작동
		//Subsystem->RemoveMappingContext(DefaultMappingContext);
	}

}

void AABCharacterPlayer::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	// EnhancedInput을 사용하지 않을 경우 에러를 발생하도록 CastChecked 사용
	UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent);

	EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ACharacter::Jump);
	EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
	EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::Move);
	EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::Look);
}

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

	const FRotator Rotation = Controller->GetControlRotation();
	const FRotator YawRotation(0, Rotation.Yaw, 0);

	const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
	const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

	AddMovementInput(ForwardDirection, MovementVector.X);
	AddMovementInput(RightDirection, MovementVector.Y);
}

void AABCharacterPlayer::Look(const FInputActionValue& Value)
{
	FVector2D LookAxisVector = Value.Get<FVector2D>();

	AddControllerYawInput(LookAxisVector.X);
	AddControllerPitchInput(LookAxisVector.Y);
}

블루프린트 클래스는 이름 마지막에 _C를 꼭 붙여야 함, 이유는 없음

static ConstructorHelpers::FClassFinder<APawn> ThirdPersonClassRef(TEXT("/Game/ThirdPerson/Blueprints/BP_ThirdPersonCharacter.BP_ThirdPersonCharacter_C"));

 

게임플레이 프레임워크

 

게임

월드

  • 게임 컨텐츠를 담기 위한 가상의 공간
  • 시간, 트랜스폼, 틱을 제공
  • 월드 세팅이라는 컨텐츠 제작을 위한 기본 환경 설정 제공
  • 기본 단위는 액터

 

게임 모드

오직 서버에만 존재

언리얼 엔진에서 하나의 게임에는 반드시 하나의 게임 모드만 존재

게임 규칙을 지정하고 판정하는 최고 관리자 액터, 형태가 없음

게임 모드에서 입장할 사용자의 규격을 지정할 수 있음

 

게임모드가 관리하는 정보

현재 게임에 참여하고 있는 플레이어의 수

플레이어가 게임에 입장하는 방법과 스폰, 리스폰 규칙

게임에 관련된 중요한 처리 : 스코어에 관련된 로직 처리 등

 

게임 스테이트

서버와 모든 클라이언트에 존재

클라이언트는 게임 스테이트를 사용해 현재 게임의 상태를 파악할 수 있음

 

게임 스테이트가 관리하는 정보

현재 월드의 시간

플레이어 스테이트의 배열

 

기믹

  • 게임 진행을 위한 이벤트를 발생시키는 사물 액터
  • 주로 이벤트 발생을 위한 충돌 영역(트리거) 설정
  • 트리거를 통해 캐릭터와 상호 작용하고, 월드에 액터를 스폰해 컨텐츠 전개

 

플레이어

  • 게임에 입장한 사용자 액터, 형태가 없음
  • 게임 모드의 로그인을 통해 사용자가 게임 월드에 입장하면 플레이어 생성됨
  • 싱글 플레이 게임에는 0번 플레이어가 설정됨
  • 사용자와의 최종 커뮤니케이션 담당

 

플레이어 스테이트

서버와 모든 클라이언트에 존재

클라이언트는 플레이어 스테이트를 사용해 플레이어의 상태를 파악할 수 있음

다른 사람들과 공유할 플레이어 정보를 관리하는 데 유용

 

  • 길찾기를 사용할 수 있으며 기믹 및 다른 폰과 상호 작용
  • 폰 중에서 인간형 폰을 별도로 캐릭터라고 지칭

 

Actor

독립적으로 동작하는 개체를 나타내는 개념

게임 월드에 배치될 수 있는 모든 객체를 의미

독자적인 동작과 상태를 가질 수 있음

다른 Actor들과 상호작용 가능

 

게임 제작 과정

1. 월드 설정과 게임 모드 생성

2. 플레이어 폰의 입출력 및 모션 설정

3. 캐릭터에 반응하는 기믹 설계

4. 게임 데이터와 NPC 인공 지능

5. UI 연동 및 게임 제작 마무리

'언리얼 > 언리얼 C++ 및 개념' 카테고리의 다른 글

언리얼 C++ 코딩 규칙  (1) 2024.12.10
언리얼 C++ 기본 타입과 문자열  (0) 2024.12.10
언리얼 빌드 시스템  (0) 2024.07.24
언리얼 오브젝트 패키지  (0) 2024.07.23
언리얼 오브젝트 직렬화  (0) 2024.07.20

언리얼 엔진의 구성

에디터

게임 제작을 위해 제공되는 응용 프로그램

게임 개발 작업을 위해 다양한 폴더와 파일 이름 규칙이 미리 설정됨

프로젝트 폴더의 uproject 확장자 파일을 실행하면 에디터가 실행됨

프로젝트.uproject 파일에 버전 정보가 JSON 형식으로 있음

uproject 확장자는 윈도우 레지스트리에 등록됨, 등록이 안되어 있으면 런처를 실행해 등록

 

게임 빌드

EXE 파일과 리소스로 이루어진 독립적으로 동작하는 게임 클라이언트

 

폴더

Config : 프로젝트 설정에 필요한 정보를 보관

Content : Asset 보관

DerivedDataCache : 에셋들의 주요 정보를 미리 캐싱, 로딩 속도에 도움, 삭제 가능

Intermediate : 임시적으로 사용되는 중간 결과물을 보관, 삭제 가능

Saved : 임시로 무언가를 저장하는 용도, 의도적으로 데이터를 저장하지 않았으면 삭제 가능

 

블루프린트 프로젝트

C++ 코드가 없는 언리얼 프로젝트

언리얼 엔진이 제공하는 기본 기능을 활용해 게임을 제작하는 프로젝트

언리얼 엔진은 게임 제작에 필요한 기능을 모듈이라는 단위로 제공

언리얼 엔진의 모듈을 상속받아 블루프린트를 활용해 모든 기능과 로직을 구현하는 방식

 

블루프린트 프로젝트 추가 방법
1. 빈 폴더에 프로젝트이름.uproject 파일 추가
2. Json 형식으로 버전 추가 후 실행

{
	"FileVersion" : 3,
	"EngineAssociation" : "5.1"
}

 

언리얼 C++ 프로젝트

언리얼 엔진 C++ 모듈에 개발자가 추가로 자신만의 C++ 모듈을 추가할 수 있음

 

언리얼 C++ 모듈

언리얼 엔진의 소스코드는 모듈 단위로 구성됨

모듈을 컴파일하여 에디터 및 게임에 로직 공급

 

에디터용 DLL, 게임용 정적 라이브러리

에디터용 모듈의 이름 규칙은 UnrealEditor-{모듈이름}.DLL

 

에디터 모듈(DLL)을 빌드 폴더(Binaries/Win64)에 넣어야 C++ 모듈을 추가해 에디터를 실행

빌드된 모듈 목록이 있는 UnrealEditor.modules 파일도 같은 폴더에 넣어야 함

uproject에 모듈 이름을 지정하고 에디터 실행

 

언리얼 C++ 모듈 추가 방법
1. Binary/Win64에 모듈 추가
2. uproject에 모듈 추가

{
	"FileVersion" : 3,
	"EngineAssociation" : "5.1"
	"Modules" : [
		{
			"Name" : "UnrealSerialization",
			"Type" :	"Runtime"
		}
	]
}

 

모듈 C++ 코드 관리

언리얼 엔진 소스 코드 구조는 멀티 플랫폼 빌드를 지원하기 때문에 특정 프로그램의 구조를 따르지 않고 언리얼만의 규칙을 가짐

실제 빌드를 진행하는 주체는 Unreal Build Tool C# 프로그램

 

Soruce 폴더 구조

타겟 설정 파일 필요

전체 솔루션이 다룰 빌드 대상을 지정

{프로젝트이름}.Target.cs : 게임 빌드 설정

{프로젝트이름}Editor.Target.cs : 에디터 빌드 설정

 

모듈 폴더

모듈 설정 파일 : {모듈이름}.build.cs

보통은 프로젝트 이름으로 모듈 이름을 지정

모듈을 빌드하기 위한 C++ 프로젝트 설정 정보

외부 라이브러리에서 제공하는 헤더가 담긴 폴더 정보 및 함께 링크할 라이브러리 정보 지정

소스 코드 파일

 

모듈 이름으로 된 헤더와 소스 파일을 지정

 

모듈 뼈대를 매크로를 통해 제작

IMPLEMENT_MODULE : 유용한 기능을 제공해주는 c++ 코드 라이브러리

IMPLEMENT_GAME_MODULE : 게임 제작에 관련있는 모듈

IMPLEMENT_PRIMARY_GAME_MODULE : 전체적인 게임을 동작하는 데 사용하는 로직들을 모아둔 모듈, 일반적으로 게임이 만들어지기 위해서는 선언해야 함

 

uproject 파일 우클릭 후 generate visual studio project files 메뉴를 선택하면 언리얼 버전 셀렉터가 언리얼 빌드 툴을 가동해 intermediate 폴더에 관련된 프로젝트 파일을 자동으로 생성해 줌

 

C#을 사용하는 이유는 C#은 실행 중에 코드를 바로 컴파일하고 결과를 반영할 수 있음

// UnrealBuildSystmeEditor.Target.cs

using UnrealBuildTool;
using System.Collections.Generic;

// 클래스 이름을 모듈이름EditorTarget으로 지정해야 함
public class UnrealBuildSystemEditorTarget : TargetRules
{
    public UnrealBuildSystemEditorTarget(TargetInfo Target) : base(Target)
    {
        // 타입 정보를 에디터로 명시해야 함
        Type = TargetType.Editor;
        DefaultBuildSettings = BuildSettingsVersion.V2;
        IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_1;
        ExtraModuleNames.Add("UnrealBuildSystem");
    }
}

 

// UnrealBuildSystem.Build.cs

using UnrealBuildTool;

// 모듈 이름을 클래스로 지정
public class UnrealBuildSystem : ModuleRules
{
    public UnrealBuildSystem(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        // 언리얼 엔진이 제공하는 기본 모듈들, 필수
        PublicDependencyModuleNames.AddRange(new string[]{ "Core", "CoreUObject", "Engine", "InputCore", "CommonUtility" });

        PrivateDependencyModuleNames.AddRange(new string[]{ });

        // Uncomment if you are using Slate UI
        // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });

        // Uncomment if you are using online features
        // PrivateDependencyModuleNames.Add("OnlineSubsystem");

        // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
    }
}

 

// UnrealBuildSystem.cpp
#include "UnrealBuildSystem.h"
#include "Modules/ModuleManager.h"

// FDefaultGameModuleImpl는 언리얼이 제공하는 게임 모듈 기본 클래스
IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, UnrealBuildSystem, "UnrealBuildSystem");

 

모듈 종속 관계

만든 모듈도 언리얼 엔진이 만든 모듈을 활용해야 함

언리얼 엔진이 제공하는 모듈 사이에 종속 관계가 있음

 

하나의 모듈에 너무 많은 코드가 들어가면 언리얼 엔진이 빌드 방식을 변경함

프로젝트가 커질수록 모듈을 나누어서 관리하는 것이 유리

 

모듈 내 소스를 필요한 만큼만 공개해야 모듈 간 의존성을 줄이고 컴파일 타임을 최소화

공개할 파일은 모두 public 폴더, 숨길 파일은 모두 private 폴더로

공개할 클래스나 함수에 모듈이름_API 매크로를 선언하여 다른 모듈에 노출할지 추가로 지정해야 함

 

build.cs에 서브 모듈 이름만 넣어주면 public 폴더의 include 디렉토리와 라이브러리를 링크할 수 있도록 언리얼 빌드 툴이 자동으로 설정

주 게임 모듈을 참조할 다른 모듈이 있는 것은 좋지 않으므로 주 게임 모듈 안에서 public / private 나눌 필요는 없음

 

플러그인 시스템

게임 프로젝트 소스에 모듈을 추가하는 방법은 분업이 어렵다는 단점

공용 기능들을 모아둔 모듈은 프로젝트로부터 독립적으로 동작

다수의 모듈과 게임 컨텐츠를 포함하는 포장 단위

에디터 설정을 통해 유연하게 플러그인을 추가하거나 삭제할 수 있음

 

구조

uplugin 파일

Resource 폴더, 에디터 메뉴용 아이콘

컨텐츠

모듈 폴더

 

마켓 플레이스 판매로도 이어질 수 있도록 여러 설정을 추가할 수 있음

 

게임 빌드

게임 타겟 설정을 추가하면 게임 빌드 옵션이 추가됨

게임 타겟으로 빌드된 모듈은 정적 라이브러리로 실행 파일에 포함됨

게임이 실행되기 위해서는 실행 파일과 컨텐츠 에셋이 함께 있어야 함

빌드 : 실행 파일을 생성하기 위한 컴파일

쿠킹 : 지정한 플랫폼에 맞춰 컨텐츠 에셋을 변환하는 작업

패키징 : 이들을 모두 모아서 하나의 프로그램으로 만드는 작업

 

Shipping 빌드

사용자에게 배포할 최종 게임의 코드를 만들어내는 작업

check, ensure 같은 assertion 매크로들이 제외됨

platfrom의 shipping 설정 후 Package Project 메뉴 클릭

패키지

언리얼 오브젝트 패키지는 다수의 언리얼 오브젝트를 포장하는 단위로, 모든 언리얼 오브젝트는 패키지에 포함됨

언리얼 오브젝트 패키지의 언리얼 오브젝트를 에셋이라고 하며, 에디터에 이것이 노출됨

구조상 패키지는 다수의 언리얼 오브젝트를 소유할 수 있으나 일반적으로 하나의 에셋만 가지고, 에셋은 다수의 서브 오브젝트 보유 가능

 

패키지의 중의적 개념

언리얼 오브젝트를 감싼 포장 오브젝트

개발된 최종 컨텐츠를 정리해 프로그램으로 만드는 작업(게임 패키징)

DLC(DownLoadable Contents)와 같이 향후 확장 컨텐츠에 사용되는 별도의 데이터 묶음(패키지 파일)

 

// 프로젝트는 고유한 경로를 가짐
// Game은 게임에서 사용되는 에셋들을 모아놓은 대표 폴더
// Temp는 Saved 폴더
const FString UMyGameInstance::PackageName = TEXT("/Game/Student");

// 패키지 저장
void UMyGameInstance::SaveStudentPackage() const
{
	// 기존 패키지가 있는 경우 안전하게 하기 위함
	TObjectPtr<UPackage> StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
	if (StudentPackage)
	{
		StudentPackage->FullyLoad();
	}
	else
	{
		StudentPackage = CreatePackage(*PackageName);
	}

	EObjectFlags ObjectFlag = RF_Public | RF_Standalone;

	// 에셋 설정
	TObjectPtr<UStudent> TopStudent = NewObject<UStudent>(StudentPackage, UStudent::StaticClass(), *AssetName, ObjectFlag);
	TopStudent->SetName(TEXT("이득우"));
	TopStudent->SetOrder(36);

	// 서브 오브젝트 설정
	const uint8 NumOfSubSize = 10;
	for (uint8 Index = 1; Index <= NumOfSubSize; ++Index)
	{
		FString SubObjectName = FString::Printf(TEXT("Stduent%d"), Index);
		TObjectPtr<UStudent> SubStudent = NewObject<UStudent>(TopStudent, UStudent::StaticClass(), *SubObjectName, ObjectFlag);
		SubStudent->SetName(FString::Printf(TEXT("학생%d"), Index));
		SubStudent->SetOrder(Index);
	}

	// 패키지 저장
	const FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
	FSavePackageArgs SaveArgs;
	SaveArgs.TopLevelFlags = ObjectFlag;

	if (UPackage::SavePackage(StudentPackage, nullptr, *PackageFileName, SaveArgs))
	{
		UE_LOG(LogTemp, Log, TEXT("패키지 저장"));
	}
}

// 패키지 로드
void UMyGameInstance::LoadStudentPackage() const
{
	TObjectPtr<UPackage> StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
	if (StudentPackage == nullptr)
	{
		UE_LOG(LogTemp, Log, TEXT("패키지 없음"));
		return;
	}

	StudentPackage->FullyLoad();

	TObjectPtr<UStudent> TopStudent = FindObject<UStudent>(StudentPackage, *AssetName);
	PrintStudentInfo(TopStudent, TEXT("FindObject Asset"));
}

 

에셋 저장/로딩 전략

패키지를 불러 할당하는 작업은 부하가 크기 때문에 오브젝트의 경로로 대체해 사용

경로는 프로젝트 내에 유일함을 보장하기 때문에 에셋의 키로 사용

패키지 내 데이터를 모두 로드하지 않고 오브젝트 경로를 사용해 필요한 에셋만 로드가능 : {에셋클래스정보}'{패키지이름}.{에셋이름}' 또는 {패키지이름}.{에셋이름}

 

프로젝트에서 에셋이 반드시 필요한 경우 생성자 코드에서 미리 로딩

런타임에서 필요할 때 로딩하는 경우는 런타임 로직에서 관리자를 사용해 비동기 로딩

 

에셋 참조

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/referencing-assets-in-unreal-engine?application_version=5.1

 

강참조

직접 프로퍼티 참조

언리얼 오브젝트를 선언할 때 해당 타입을 명시적으로 지정한 것

 

생성 시간 참조

생성자 코드에서 해당 오브젝트가 가리키고 있는 에셋을 생성

엔진이 초기화 될 때 생성자가 실행되므로 게임이 실행되기 전에 해당 에셋이 로딩

 

약참조

간접 프로퍼티 참조 : TSoftObjectPtr 사용하여 필요할 때 로딩

오브젝트 검색/로드 : 로드되어 있지 않은 오브젝트는 LoadObject<>()를 사용

 

에셋 스트리밍 관리자

에셋의 비동기 로딩을 지원하는 객체

컨텐츠 제작과 무관한 싱글톤 객체(GameInstance 등)에 FStreamableManager를 선언

다수의 오브젝트 경로를 입력해 다수의 에셋을 로딩하는 것도 가능

// 에셋 비동기 로드
const FString TopSoftObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName);
StreamableHandle = StreamableManager.RequestAsyncLoad(TopSoftObjectPath,
	[&]()
	{
		if (StreamableHandle.IsValid() && StreamableHandle->HasLoadCompleted())
		{
			TObjectPtr<UStudent> TopStudent = Cast<UStudent>(StreamableHandle->GetLoadedAsset());
			if (TopStudent)
			{
				PrintStudentInfo(TopStudent, TEXT("AsyncLoad"));

				StreamableHandle->ReleaseHandle();
				StreamableHandle.Reset();
			}
		}
	}
);

직렬화(Serialization)

오브젝트나 연결된 오브젝트 묶음(오브젝트 그래프)을 바이트 스트림으로 변환하는 과정

복잡한 데이터를 일렬로 세우기 때문에 직렬화라 부름

 

현재 프로그램 상태를 저장하고 복원할 수 있음 (게임 저장)

현재 객체의 정보를 클립보드에 복사해서 다른 프로그램으로 전송가능

네트워크를 통해 현재 프로그램의 상태를 다른 프로그램에 복원 (멀티플레이 게임)

데이터 압축, 암호화를 통해 데이터를 효율적이고 안전하게 보관

 

구현시 고려할 점

데이터 레이아웃 : 오브젝트가 소유한 다양한 데이터를 어떤 식으로 일렬로 세울 것인가

이식성 : 서로 다른 시스템에서 전송이 가능한가

 

버전 관리

새로운 기능이 추가될 때 이를 어떻게 확장하고 처리할 것인가

새로운 기능이 추가될 때 데이터 레이아웃이 변경됨

 

성능 : 네트워크 비용을 줄이기 위해 어떤 데이터 형식을 사용할 것인가

보안 : 데이터를 어떻게 보호할 것인가

에러 처리 : 전송 과정에서 문제가 발생할 경우 어떻게 인식하고 처리할 것인가

 

Deserialization : 바이트 스트림에서 오브젝트 그래프로 변환

 

언리얼 직렬화 시스템

FArchive 클래스 사용, shift(<<) operator 지원해야 함

메모리 아카이브(FMemoryReader, FMemoryWriter)

파일 아카이브(FArchiveFileReaderGeneric, FArchiveFileWriterGeneric)

기타 언리얼 오브젝트와 관련된 아카이브(FArchiveUObject)

Json(JavaScript Object Notation) 직렬화 기능

 

Json 직렬화 기능

웹 통신의 표준으로, 웹 환경에서 서버와 클라이언트 사이에 데이터를 주고받을 때 사용하는 텍스트 기반 데이터 포맷

 

텍스트임에도 데이터 크기가 가벼움

읽기 편해서 데이터를 보고 이해할 수 있음

 

지원하는 타입이 별로 안되므로(문자, 숫자, 불리언, 널, 배열, 오브젝트만 가능) 숫자의 타입을 구분하기 힘듦

텍스트 형식으로만 사용 가능하므로 네트워크 통신에서 효율 추구 불가

언리얼 엔진의 Json, JsonUtilities 라이브러리 활용

 

데이터 유형

오브젝트 : 오브젝트 내 데이터 키, 밸류 조합으로 구성됨 { "key" : 10 }

배열 : 배열 내 데이터는 밸류로만 구성됨 [ "value1", "value2", "value3" ]

기타 데이터 : 문자열("string"), 숫자(10), 불리언(true/false), 널(null)

 

JsonObjectConverter.h include해야 함

프로젝트.build.cs 파일에 Json 관련 라이브러리(모듈)를 연동해야 함

// Json, JsonUtilties를 추가해야 함
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Json", "JsonUtilties" });

 

// 프로젝트 경로 가져오기 및 경로 설정
const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved"));

// 프로젝트 상위 폴더들이 /../로 표시되므로 이를 정확히 표시하는 함수
FPaths::MakeStandardFilename(RawDataAbsolutePath);

// WriterArchive 생성
FArchive* RawFileWriterAr = IFileManager::Get().CreateFileWriter(*RawDataAbsolutePath);

// ReaderArchive 생성
FArchive* RawFileReaderAr = IFileManager::Get().CreateFileReader(*RawDataAbsolutePath);

// 일반 C++ 데이터 Serialize
// 일일이 멤버를 넣으면 번거로우므로 operator<< 함수 구현
// 프로퍼티의 순번만 지정해주면 됨
friend FArchive& operator<< (FArchive& Ar, FStudnetData& InStudnetData)
{
	Ar << InStudnetData.Order;
	Ar << InStudnetData.Name;

	return Ar;
}

// 파일 읽을 때도 동일한 연산 사용
*RawFileReaderAr << RawDataDest;

// 파일을 읽거나 쓰는 것을 모두 완료했다면 닫아야함
FileWriteAr->Close();

// 언리얼 오브젝트 Serialize
//읽기
TArray<uint8> BufferArray; // 직렬화를 위한 버퍼
FMemoryWriter MemoryWriterAr(BufferArray);
StudentSrc->Serialize(MemoryWriterAr);

//쓰기
FMemoryReader MemoryReaderAr(BufferArrayFromFile);
TObjectPtr<UStudent> StudentDest(NewObject<UStudent>());
StudentDest->Serialize(MemoryReaderAr);

//Json 변환
TSharedRef<FJsonObject> JsonObjectSrc = MakeShared<FJsonObject>();
FJsonObjectConverter::UStructToJsonObject(StudentSrc->GetClass(), StudentSrc, JsonObjectSrc);

//Json Serialize
FString JsonOutString;
TSharedRef<TJsonWriter<TCHAR>> JsonWriterAr = TJsonWriterFactory<TCHAR>::Create(&JsonOutString);
if (FJsonSerializer::Serialize(JsonObjectSrc, JsonWriterAr))
{
	FFileHelper::SaveStringToFile(JsonOutString, *JsonDataAbsolutePath);

	FString JsonInString;
	FFileHelper::LoadFileToString(JsonInString, *JsonDataAbsolutePath);
	// 문자열이 이상한 값이 들어오면 안 만들어지면 null이기 때문에 포인터로
    TSharedRef<TJsonReader<TCHAR>> JsonReaderAr = TJsonReaderFactory<TCHAR>::Create(JsonInString);
	
    //Json Deserialize
	TSharedPtr<FJsonObject> JsonObjectDest;
	if (FJsonSerializer::Deserialize(JsonReaderAr, JsonObjectDest))
	{
		TObjectPtr<UStudent> JsonStudentDest(NewObject<UStudent>());
		if (FJsonObjectConverter::JsonObjectToUStruct(JsonObjectDest.ToSharedRef(), JsonStudentDest->GetClass(), JsonStudentDest.Get()))
		{
			PrintStudentInfo(JsonStudentDest.Get(), TEXT("JsonData"));
		}
	}
}

+ Recent posts

목차