일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- URP
- 주식
- GenAI
- 리팩터링 3장
- 448일선
- 2023 게이밍 인 구글 클라우드
- 이득우의 언리얼 프로그래밍1
- 2023 Gaming
- 언리얼5
- 리팩터링
- shader
- 112일선
- 이득우의 언리얼 프로그래밍 1
- 공부
- JavaScript
- 구글 컨퍼런스
- 2023 게이밍
- 스포일러 주의
- 스즈메의 문단속
- 언리얼 5
- 상계9동
- 주식단테
- 리팩터링 4장
- 224일선
- 1일차
- 2023 구글 클라우드
- 전주비빔 라이스 버거
- 작계훈련
- unity
- 산토리 하이볼
- Today
- Total
개발 이야기 안하는 개발자
언리얼5_2_1 : 게임 컨텐츠의 기본 구조 본문
월드
- 게임 컨텐츠를 담기 위해 제공되는 가상의 공간
- 공간(Transform), 진행(Tick), 시간(Time), 환경 설정(WorldSetting), 구성요소(Actor) 등을 담고있음.
게임 모드
- 게임 규칙을 지정하고 게임을 판정하는 최고 관리자 액터
- 멀티게임에선 판정을 처리하고, 게임 데이터를 검증하기도 한다. (로그인)
- 하나의 게임모드만 존재한다. (각 Maps마다)
기믹
- 게임 진행을 위해 이벤트를 발생시키는 사물 액터이다.
- 트리거나, 스폰을 활용하여 상호작용과 컨텐츠를 전개한다.
플레이어
- 게임 캐릭터와는 별개의 개념이다. 게임에 입장한 사용자 엑터이다.
- 게임에 입장한 사용자와의 1대1 대응이 가능하고, 사용자의 상태 및 데이터를 관리한다.
- 사용자의 입력을 1차 처리하며 카메라관리,UI관리 등을 진행한다.
- 싱글 플레이 게임에선 0번 플레이어가 자동으로 지정된다.
폰
- 무형의 액터인 플레이어가 빙의해 조종하는 액터
- 빙의(Possess)를 통해 플레이어와 연결됨.
- 사용자의 입력 실제 처리
- 기믹과 상호작용하며 이동, 모션, 애니메이션 재생 등을 진행한다.
- 폰 중에선 인간형 폰을 별도로 캐릭터라고 지칭한다.
프로젝트 셋팅
프로젝트 셋팅에서 Maps&Modes에서 프로젝트 기본 셋팅을 진행할 수 있다.
해당 내용은 특별한 로직이 없다면 프로젝트가 켜질때 최초로 활성화 되는 Map과 Mode를 지정한다.
C++ 클래스 생성
상단 Tools에서 생성이 가능한데, 이때 파일 경로를 게임 프로젝트내에 지정하면 에러가 발생한다.
이유는 경로가 UnrealProject에서 지정받지 않았기 때문인데, 이를 위해서 Build.cs 에 특정 내용을 추가해야 한다.
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class ArenaBattle : ModuleRules
{
public ArenaBattle(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.AddRange(new string[] { "ArenaBattle" }); //추가
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput" });
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
}
}
생성자에서 경로를 통해 데이터를 가져올때
대상으로 하는 에셋의 경로를 복사한다.
이후 경로를 붙여넣으면 되는데, 이때 class만 가져오는 것이라면 앞에 따옴표 경로는 필요없다.
단, class는 뒤에 _C 를 붙여야 한다.
static ConstructorHelpers::FObjectFinder<USkeletalMesh> CharacterMeshRef(TEXT("/Script/Engine.SkeletalMesh'실제경로'"));
if (CharacterMeshRef.Object)
{
...
}
static ConstructorHelpers::FClassFinder<UAnimInstance> AnimInstanceClassRef(TEXT("실제경로_C"));
if (AnimInstanceClassRef.Class)
{
...
}
게임에 있어서 마우스를 사용하는지, 키보드만 사용하는지 지정을 할 수 있다.
해당 내용은 PlayerController에서 지정이 가능하다.
void AABPlayerController::BeginPlay()
{
Super::BeginPlay();
FInputModeGameOnly GameOnlyInputMode;
SetInputMode(GameOnlyInputMode);
}
액터
월드에 속한 콘텐츠의 기본 단위이다.
트랜스폼을 가지고, 월드로부터 틱과 시간 서비스를 갖는다.
Details에 나오는 엑터의 내부 구성으로 최상단이 RootComponent이고, 하위 컴포넌트들은 이 RootComponent 하위에 들어간다.
이런 엑터들은 블루프린트를 통해 쉽게 제작할 수 있다.
제작하고 싶은 위치에 우클릭트로 Blueprints-Blueprint Class 로 제작한다.
생성하고자 하는 부모 클래스를 지정해준다.
그럼 좌측에 Components가 보이는데, 여기서 Add를 통해 추가하고자 하는 컴포넌트를 추가하면 된다.
스태틱 메쉬를 추가하고, 라이트를 추가해서 제작한 분수대이다.
이는 스크립트로도 제작이 가능하다.
C++액터에서 컴포넌트 생성
언리얼5 부터 헤더에 언리얼 오브젝트를 선언할때 일반 포인터에서 UPROPERTY 설정, TObjectPtr로 감싸서 선언해야 한다.
이후, 런타임중에 추가하는 경우엔 NewObject로 생성해야 한다.
NewObject로 성공적으로 제작했다면 RegisterComponent로 등록해야 하고, 붙을 위치를 지정해야 한다.
만약 CDO에서 생성한다면 자동으로 등록된다.
컴포넌트 지정자
Visible / Edit : 크게 객체타입과 값타입으로 사용
Anywhere / DefaultsOnly / InstanceOnly : 에디터에서 편집 가능 영역
BlueprintReadOnly / BlueprintReadWrite : 블루프린트로 확장시 읽기 혹은 읽기쓰기 권한을 부여
Category : 에디터 편집영역(Detail)에서의 카테고리 지정
<<header>>
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Mesh)
TObjectPtr<class UStaticMeshComponent> Body;
...
<<cpp - 생성자>>
Body = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Body"));
RootComponent = Body;
"다른서브"->SetupAttachment(Body);
static ConstructorHelpers::FObjectFinder<UStaticMesh> BodyMeshRef(TEXT("/Script/Engine.StaticMesh'경로'"));
if (BodyMeshRef.Object)
{
Body->SetStaticMesh(BodyMeshRef.Object);
}
만약 런타임중에 컴포넌트를 추가하고 싶은 경우에는 아래 코드처럼 진행하면 된다.
Water = NewObject<UStaticMeshComponent>(this, TEXT("Water22"));
if (Water != nullptr)
{
Water->RegisterComponent();
Water->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);
}
//만약 특정 기능이 있는 컴포넌트 일 경우엔 아래처럼 추가해서 해당 CDO를 가져와서 쓸수 있음.
//Water = NewObject<UStaticMeshComponent>(this, UStaticMeshComponent::StaticClass(), TEXT("Water22"));
폰
액터를 상속받은 특별한 엑터이며, 플레이어가 빙의해 입출력을 처리하도록 설계되어 있음.
길찾기 기능이 가능하다.
컴포넌트중에서 트랜스폼 없이 기능만 제공하는 컴포넌트를 액터컴포넌트라 한다.
보통 아래 3가지 주요 컴포넌트로 구성되어 있다.
기믹과 상호작용하는 충돌(Root)
- 비쥬얼
- 움직임담당
캐릭터
인간형 폰을 구성하도록 언리얼이 제공한다.
세가지 주요 3가지 컴포넌트로 구성되어 있다.
기믹과 상호작용을 담당하는 캡슐 컵포넌트(Root)
- 애니메이션 캐릭터를 표현하는 스켈레탈 메시
- 움직임을 담당하는 캐릭터 무브먼
<<cpp-생성자>>
// Pawn
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
// Capsule
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
GetCapsuleComponent()->SetCollisionProfileName(TEXT("Pawn"));
// Movement
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;
// 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);
}
입력 작동
입력이 들어오면 PlayerController에서 입력을 처음 처리함.
다양한 사물에 빙의하는 게임인 경우엔 폰이 처리하는것이 더 유리함. 따라서, 입력 코드를 분산하는게 유리할수있다.
향상된 입력시스템(Enhanced Input System)
기존 입력시스템을 대체하는 새로운 기능.
키가 눌린 입력 매핑 컨텍스트(키보드, 조이스틱 등)에 대해 입력값을 변조해서 연결된 기능을 활성화 하도록 한다.
언리얼 기본 ThirdPerson IMC_Default에 해당하는 InputMappingContext이다.
(마우스 Y축 반전은 코드에서 보기 편하려고 방향을 일치한 것)
<<header>>
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UInputMappingContext> DefaultMappingContext;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UInputAction> JumpAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UInputAction> MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, Meta = (AllowPrivateAccess = "true"))
TObjectPtr<class UInputAction> LookAction;
void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
<<cpp-생성자>>
static ConstructorHelpers::FObjectFinder<UInputMappingContext> InputMappingContextRef(TEXT("/Script/EnhancedInput.InputMappingContext'/Game/ArenaBattle/Input/IMC_Default.IMC_Default'"));
if (nullptr != InputMappingContextRef.Object)
{
DefaultMappingContext = InputMappingContextRef.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction> InputActionMoveRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_Move.IA_Move'"));
if (nullptr != InputActionMoveRef.Object)
{
MoveAction = InputActionMoveRef.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction> InputActionJumpRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_Jump.IA_Jump'"));
if (nullptr != InputActionJumpRef.Object)
{
JumpAction = InputActionJumpRef.Object;
}
static ConstructorHelpers::FObjectFinder<UInputAction> InputActionLookRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_Look.IA_Look'"));
if (nullptr != InputActionLookRef.Object)
{
LookAction = InputActionLookRef.Object;
}
<<cpp-BeginPlay>>
APlayerController* PlayerController = CastChecked<APlayerController>(GetController());
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(DefaultMappingContext, 0);
//Subsystem->RemoveMappingContext(DefaultMappingContext);
}
<<cpp-SetupPlayerInputComponent_override>>
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);
}
EnhancedInputComponent에서 읽어온 액션과 정의한 메소드를 연결한다.
이렇게 해서 액션이 호출될때, 해당 메소드가 호출되는 방식이며,
이는 액션에 연결되어 있는 다양한 플랫폼(키보드, 조이스틱)에 모두 대응이 가능한 방식이다.
데이터 에셋
UDataAssets을 상속받은 언리얼 오브젝트 클래스
만들어 놓은 셋팅값을 불러와 게임에 적용할 예정.
데이터 폼을 제작
<<UPrimaryDataAsset를 상속받음_header>>
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;
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;
스크립트로 데이터 폼을 제작했다면 Editor에서 DataAsset을 제작할 수 있음.
타겟은 제작한 데이터 폼으로 만든다.
그럼 위와 같이 스크립트에서 지정한 변수들이 뜨는것을 볼 수 있다.
셋팅할 값들을 대입한다.
이렇게 2개의 데이터 에셋을 만들었다.
그리고 각각엔 Input Mapping Context를 만들어서 넣었다.
물론 각각의 Input Mapping Context안에는 새로운 Input Action들이 변경되어 추가되었다.
데이터 에셋 관리
컨트롤 관리자가 위 두개의 데이터 에셋을 관리할 예정이다.
지정한 데이터를 사용하게 된다면 데이터에서 값들을 불러와 필요한 Character에 데이터를 넣을 예정이다.
<<캐릭터_header>>
UENUM()
enum class ECharacterControlType : uint8
{
Shoulder,
Quater
};
...
virtual void SetCharacterControlData(const class UABCharacterControlData* CharacterControlData);
UPROPERTY(EditAnywhere, Category = CharacterControl, Meta = (AllowPrivateAccess = "true"))
TMap<ECharacterControlType, class UABCharacterControlData*> CharacterControlManager;
<<cpp>>
void AABCharacterBase::SetCharacterControlData(const UABCharacterControlData* CharacterControlData)
{
// Pawn
bUseControllerRotationYaw = CharacterControlData->bUseControllerRotationYaw;
// CharacterMovement
GetCharacterMovement()->bOrientRotationToMovement = CharacterControlData->bOrientRotationToMovement;
GetCharacterMovement()->bUseControllerDesiredRotation = CharacterControlData->bUseControllerDesiredRotation;
GetCharacterMovement()->RotationRate = CharacterControlData->RotationRate;
}
위와 같이 데이터를 불러와서 적용하는 메소드를 제작한다.
이때, CharacterControlManager는 TMap으로 제작했는데, 이 데이터들은 Blueprint에서 넣을 예정이다.
이후에 특정키를 누를때마다 데이터를 다르게 읽어올 예정이다.
제작했던 Enhanced Input 시스템에 키를 추가하고 이벤트를 Binding한다.
<<캐릭터 cpp>>
...
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::ChangeCharacterControl()
{
if (CurrentCharacterControlType == ECharacterControlType::Quater)
{
SetCharacterControl(ECharacterControlType::Shoulder);
}
else if (CurrentCharacterControlType == ECharacterControlType::Shoulder)
{
SetCharacterControl(ECharacterControlType::Quater);
}
}
void AABCharacterPlayer::SetCharacterControl(ECharacterControlType NewCharacterControlType)
{
UABCharacterControlData* NewCharacterControl = CharacterControlManager[NewCharacterControlType];
check(NewCharacterControl);
SetCharacterControlData(NewCharacterControl);
APlayerController* PlayerController = CastChecked<APlayerController>(GetController());
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->ClearAllMappings();
UInputMappingContext* NewMappingContext = NewCharacterControl->InputMappingContext;
if (NewMappingContext)
{
Subsystem->AddMappingContext(NewMappingContext, 0);
}
}
CurrentCharacterControlType = NewCharacterControlType;
}
void AABCharacterPlayer::SetCharacterControlData(const UABCharacterControlData* CharacterControlData)
{
Super::SetCharacterControlData(CharacterControlData);
CameraBoom->TargetArmLength = CharacterControlData->TargetArmLength;
CameraBoom->SetRelativeRotation(CharacterControlData->RelativeRotation);
CameraBoom->bUsePawnControlRotation = CharacterControlData->bUsePawnControlRotation;
CameraBoom->bInheritPitch = CharacterControlData->bInheritPitch;
CameraBoom->bInheritYaw = CharacterControlData->bInheritYaw;
CameraBoom->bInheritRoll = CharacterControlData->bInheritRoll;
CameraBoom->bDoCollisionTest = CharacterControlData->bDoCollisionTest;
}
void AABCharacterPlayer::ShoulderMove(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::ShoulderLook(const FInputActionValue& Value)
{
FVector2D LookAxisVector = Value.Get<FVector2D>();
AddControllerYawInput(LookAxisVector.X);
AddControllerPitchInput(LookAxisVector.Y);
}
void AABCharacterPlayer::QuaterMove(const FInputActionValue& Value)
{
FVector2D MovementVector = Value.Get<FVector2D>();
float InputSizeSquared = MovementVector.SquaredLength();
float MovementVectorSize = 1.0f;
float MovementVectorSizeSquared = MovementVector.SquaredLength();
if (MovementVectorSizeSquared > 1.0f)
{
MovementVector.Normalize();
MovementVectorSizeSquared = 1.0f;
}
else
{
MovementVectorSize = FMath::Sqrt(MovementVectorSizeSquared);
}
FVector MoveDirection = FVector(MovementVector.X, MovementVector.Y, 0.0f);
GetController()->SetControlRotation(FRotationMatrix::MakeFromX(MoveDirection).Rotator());
AddMovementInput(MoveDirection, MovementVectorSize);
}
위와 같은 방식으로 맵핑되어 있떤 모든 이벤트를 지우고
새로운 데이터에 들어있는 MappingContext를 가져와 추가한다.
그렇게 해서 등록해놨던 ChangeControlAction을 누르면 (IA_ChangeControl) 시점이 변경되는 것을 볼 수 있다.
'Unreal > 이득우의 언리얼 프로그래밍' 카테고리의 다른 글
언리얼5_2_2 : 캐릭터의 애니메이션 설정 (0) | 2024.03.25 |
---|---|
언리얼 5_4_1 게임플레이 어빌리티 시스템 캐릭터 제작 기초 (0) | 2024.03.24 |
언리얼5_1_4 : 언리얼 프로젝트의 에셋 (1) | 2024.03.05 |
언리얼5_1_3 : 언리얼 엔진의 자료구조와 메모리 관리 (1) | 2024.02.16 |
언리얼5_1_2 : 언리얼 C++ 모던객체지향 설계 (0) | 2024.02.16 |