KoreanFoodie's Study
이득우 언리얼 C++ 12 : AI 컨트롤러와 비헤이비어 트리 본문
이득우님의 "이득우의 언리얼 C++ 게임 개발의 정석" 책을 따라가며 실습한 내용을 정리한 포스팅입니다. 실습에 필요한 자료들은 이 링크에서, 제가 작업한 예제 소스 완성본은 여기에서 찾아보실 수 있습니다. (저는 언리얼 4.27.2 버전 기준으로 작업하였습니다)
AIController 와 내비게이션 시스템
NPC(Non Player Character) 에 인공지능을 추가해 플레이어를 쫓아가서 공격하도록 만들어 보자. 먼저 AIController 를 부모 클래스로 하는 ABAIController 클래스를 생성하는 것부터 시작해 보자.
ABCharacter.cpp
...
#include "ABAIController.h"
...
AABCharacter::AABCharacter()
{
...
AIControllerClass = AABAIController::StaticClass();
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}
클래스 속성을 AIController 클래스 속성을 AABAIController 로 정하고 AI 의 생성 옵션을 PlacedInWorldOrSpawned 로 설정한다. 그러면 앞으로 레벨에 배치하거나 새롭게 생성되는 ABCharacter 마다 ABAIController 액터가 생성되며, 플레이어가 조종하는 캐릭터를 제외한 모든 캐릭터는 ABAIController 의 지배를 받는다.
AIController 에 플레이어를 따라가도록 만들기 위해 네비게이션 메시(Navigation Mesh) 기반의 길 찾기 시스템을 이용한다.
내비게이션 시스템을 이용해 이동 가능한 목적지를 랜덤으로 가져오는 GetRandomPointInNavigableRadius 함수와 목적지로 폰을 이동시키는 SimpleMoveToLocation 함수를 이용한다.
ABAIController.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "ArenaBattle.h"
#include "AIController.h"
#include "ABAIController.generated.h"
/**
*
*/
UCLASS()
class ARENABATTLE_API AABAIController : public AAIController
{
GENERATED_BODY()
public:
AABAIController();
virtual void OnPossess(APawn* InPawn) override;
virtual void OnUnPossess() override;
private:
void OnRepeatTimer();
FTimerHandle RepeatTimerHandle;
float RepeatInterval;
};
ABAIController.cpp
#include "ABAIController.h"
AABAIController::AABAIController()
{
RepeatInterval = 3.0f;
}
void AABAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
GetWorld()->GetTimerManager().SetTimer(RepeatTimerHandle, this, &AABAIController::OnRepeatTimer, RepeatInterval, true);
}
void AABAIController::OnUnPossess()
{
Super::OnUnPossess();
GetWorld()->GetTimerManager().ClearTimer(RepeatTimerHandle);
}
void AABAIController::OnRepeatTimer()
{
auto CurrentPawn = GetPawn();
ABCHECK(nullptr != CurrentPawn);
UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());
if (nullptr == NavSystem) {
ABLOG(Warning, TEXT("NavSystem is nullptr!"));
return;
}
FNavLocation NextLocation;
if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.0f, NextLocation))
{
//UNavigationSystemV1::SimpleMoveToLocation(this, NextLocation.Location);
UAIBlueprintHelperLibrary::SimpleMoveToLocation(this, NextLocation.Location);
ABLOG(Warning, TEXT("Next Location : %s"), *NextLocation.Location.ToString());
}
}
비헤이비어 트리 시스템
비헤이비어 트리를 이용해 우선순위가 높은 생동부터 NPC 가 실행할 수 있도록 트리 구조로 설계해 보자. 인공지능 메뉴에서 BB_ABCharacter 이름으로 블랙보드 애셋을, BT_ABCharacter 라는 이름으로 비헤이비어 트리 애셋을 생성한다.
- 블랙보드 : 인공지능의 판단에 사용하는 데이터 집합
- 비헤이비어 트리 : 블랙보드의 데이터에 기반해 설계한 트리 정보를 저장한 애셋
블랙보드에서는 우리가 사용할 키들을 추가한다. HomPos 와 PatrolPos 는 Vector 로, Target 은 Base Class 를 ABCharacter 로 갖는 Object Type 으로 만든다.
이제 비헤이비어 트리를 보자.
태스크는 독립적으로 실행될 수 없고, 반드시 컴포짓(Composite) 노드를 거쳐 실행돼야 한다. 컴포짓에는 Selector, Sequence, Simple Parallel 이 있다.
- Sequence : 연결된 Task 들이 False 결과가 나올 때까지 왼쪽에서 오른쪽으로 태스크를 계속 실행함
- Selector : 지정된 키값이나 조건을 기준으로 조건에 맞는 노드를 실행
- Simple Parallel : 자식으로 붙은 태스크들이 동시에 실행됨. Finish Mode 설정을 통해서 메인 테스크가 완료되면, 서브 트리를 중단시키고 즉시(Immediate) 노드를 완료시킬수도 있고. 서브 트리를 완료할 때까지 지연(Delayed) 시킬지를 설정할 수 있음.
비헤이비어 트리는 5개의 요소로 구성된다. Root, Composite(Sequence, Selector, Simple Parallel), Decorator, Service, Task. 컴포짓은 위에서 설명했으니, 나머지 4개의 기능을 정리해보면 :
- Root : 비헤이비어 트리의 시작점. 데코레이터나 서비스를 덧붙일 수 없음. 블랙보드 애셋을 설정할 수 있음.
- Composite : Sequence, Selector, Simple Parallel (위에서 설명)
- Decorator : 조걸절. 컴포짓이나 태스크에 붙여 분기나 노드가 실행될 것인지를 정의한다. 종류는 16 가지가 있다.
- Service : 서비스는 컴포짓 노드에 분기가 실행되는 동안 정해진 빈도에 맞춰서 실행된다. 보통 검사를 하고 그 검사를 바탕으로 블랙보드의 내용을 업데이트하는데 사용된다.
- Task : AI 의 이동이나 블랙보드의 값 조정 등의 작업을 한다. 태스크 노드의 종류에는 7 가지가 있다.
비헤이비어 트리를 사용하려면 ArenaBattle.Build.cs 에 AIModule 모듈을 추가해야 한다.
ArenaBattle.Build.cs
PublicDependencyModuleNames.AddRange(new string[] { "Core","CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "UMG",
"NavigationSystem", "AIModule", "GameplayTasks" });
NavigationSystem 은 이전의 내비게이션 시스템을 사용하기 위해, GameplayTasks 모듈은 추후 Task 를 이용하는데 필요하므로 추가해 놓는다.
마찬가지로, ABAIController 에도 아까 생성한 블랙보드/비헤이비어 트리 애셋을 추가한다.
ABAIController.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "ArenaBattle.h"
#include "AIController.h"
#include "ABAIController.generated.h"
/**
*
*/
UCLASS()
class ARENABATTLE_API AABAIController : public AAIController
{
GENERATED_BODY()
public:
AABAIController();
virtual void OnPossess(APawn* InPawn) override;
static const FName HomePosKey;
static const FName PatrolPosKey;
static const FName TargetKey;
private:
UPROPERTY()
class UBehaviorTree* BTAsset;
UPROPERTY()
class UBlackboardData* BBAsset;
};
NPC 가 자동으로 랜덤한 위치로 정찰을 하도록 만들고, Target 을 찾으면 해당 Target 의 위치로 이동하도록 만들 것이다.
ABAIController.cpp
#include "ABAIController.h"
#include "NavigationSystem.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
const FName AABAIController::HomePosKey(TEXT("HomePos"));
const FName AABAIController::PatrolPosKey(TEXT("PatrolPos"));
const FName AABAIController::TargetKey(TEXT("Target"));
AABAIController::AABAIController()
{
//RepeatInterval = 3.0f;
static ConstructorHelpers::FObjectFinder<UBlackboardData> BBObject(TEXT("/Game/Book/AI/BB_ABCharacter.BB_ABCharacter"));
if (BBObject.Succeeded())
{
BBAsset = BBObject.Object;
}
static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTObject(TEXT("/Game/Book/AI/BT_ABCharacter.BT_ABCharacter"));
if (BTObject.Succeeded())
{
BTAsset = BTObject.Object;
}
}
void AABAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (UseBlackboard(BBAsset, Blackboard))
{
Blackboard->SetValueAsVector(HomePosKey, InPawn->GetActorLocation());
if (!RunBehaviorTree(BTAsset))
{
ABLOG(Error, TEXT("AIController couldn't run behavior tree!"));
}
}
}
HomePosKey, PatrolPosKey, TargetKey 를 각각 HomePos. PatrolPos, Target 으로 하드코딩한다.
Detect 를 이용해 주변에 플레이어가 있는지를 검출할 것이다. 검출될 경우, 공격 범위 안에 있으면 Simple Parallel 컴포짓을 이용해 공격과 플레이어 방향으로의 회전을 동시에 진행한다. 공격 범위에 없을 경우, Move To 태스크를 이용해 플레이어의 위치(타깃)으로 NPC 를 이동시킨다. 공격 범위에 있는지 아닌지는 IsInAttackRange 라는 태스크를 생성할 것이다.
만약 주변에 플레이어가 없을 경우, 1초 동안 Wait 을 실행하고 FindPatrolPos 를 이용해 랜덤한 위치를 잡은 다음, 해당 위치(PatrolPos) 로 이동한다.
비헤이비어 트리는 태스크를 실행할 때 태스크 클래스의 ExecuteTask 라는 멤버 함수를 실행한다. ExecuteTask 함수는 다음의 넷 중 하나의 값을 반환해야 한다.
- Aborted : 태스크 실행 중에 중단 (결과적으로 실패)
- Failed : 태스크를 수행했지만 실패
- Succeeded : 태스크를 성공적으로 수행
- InProgress : 태스크를 계속 수행하고 있음. 태스크의 실행 결과는 향후 알려줄 예정
Execute 함수의 실행 결과에 따라 컴포짓 내에 있는 다음 태스크를 계속 수행할지, 중단할지가 결정된다. 현재 사용중인 시퀀스 컴포짓은 자신이 속한 태스크를 실패할 때까지 계속 실행하는 성질을 가진다. NodeName 속성을 바꾸면 태스크의 이름이 바뀐다.
각 태스크들의 코드를 보자.
BTTask_FindPatrolPos.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_FindPatrolPos.generated.h"
/**
*
*/
UCLASS()
class ARENABATTLE_API UBTTask_FindPatrolPos : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_FindPatrolPos();
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
BTTask_FindPatrolPos.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "BTTask_FindPatrolPos.h"
#include "ABAIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "NavigationSystem.h"
UBTTask_FindPatrolPos::UBTTask_FindPatrolPos()
{
NodeName = TEXT("FindPatrolPos");
}
EBTNodeResult::Type UBTTask_FindPatrolPos::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);
auto ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
if (nullptr == ControllingPawn)
return EBTNodeResult::Failed;
UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(ControllingPawn->GetWorld());
if (nullptr == NavSystem)
return EBTNodeResult::Failed;
FVector Origin = OwnerComp.GetBlackboardComponent()->GetValueAsVector(AABAIController::HomePosKey);
FNavLocation NextPatrol;
if (NavSystem->GetRandomPointInNavigableRadius(Origin, 500.0f, NextPatrol))
{
OwnerComp.GetBlackboardComponent()->SetValueAsVector(AABAIController::PatrolPosKey, NextPatrol.Location);
return EBTNodeResult::Succeeded;
}
return EBTNodeResult::Failed;
}
BTTask_Attack.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "ArenaBattle.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_Attack.generated.h"
/**
*
*/
UCLASS()
class ARENABATTLE_API UBTTask_Attack : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_Attack();
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
protected:
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSecondes) override;
private:
bool IsAttacking = false;
};
BTTask_Attack.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "BTTask_Attack.h"
#include "ABAIController.h"
#include "ABCharacter.h"
UBTTask_Attack::UBTTask_Attack()
{
bNotifyTick = true;
IsAttacking = false;
}
EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);
auto ABCharacter = Cast<AABCharacter>(OwnerComp.GetAIOwner()->GetPawn());
if (nullptr == ABCharacter)
return EBTNodeResult::Failed;
ABCharacter->Attack();
IsAttacking = true;
ABCharacter->OnAttackEnd.AddLambda([this]() -> void {
IsAttacking = false;
});
return EBTNodeResult::InProgress;
}
void UBTTask_Attack::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);
if (!IsAttacking)
{
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}
}
BTTask_Attack 은 공격 애니메이션이 끝날 때까지 대기해야 하는 지연 태스크이므로 ExecuteTask 의 결과 값을 InProgress 로 일단 반환하고 공격이 끝났을 때 태스크가 끝났다고 알려줘야 한다. 이를 알려주는 함수가 FinishLatentTask 이다. 태스크에서 이 함수를 나중에 호출해주지 않으면 비헤이비어 트리 시스템은 현재 태스크에 계속 머물게 된다.차후에 FinishLatentTask 를 호출할 수 있도록 노드의 Tick 기능을 활성화하고 Tick 에서 조건을 파악한 후 태스크 종료 명령을 내려줘야 한다.
ABCharacter 클래스의 Attack 함수의 접근 권한을 public 으로 변경하고, 델리게이트를 만들어 공격이 종료될 때 FinishLatentTask 함수를 호출하도록 만들어야 한다.
ABCharacter.h
...
DECLARE_MULTICAST_DELEGATE(FOnAttackEndDelegate);
...
UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
...
void Attack();
FOnAttackEndDelegate OnAttackEnd;
}
ABCharacter.cpp
void AABCharacter::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
...
OnAttackEnd.Broadcast();
}
BTTask_TurnToTarget.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "ArenaBattle.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_TurnToTarget.generated.h"
/**
*
*/
UCLASS()
class ARENABATTLE_API UBTTask_TurnToTarget : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_TurnToTarget();
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
BTTask_TurnToTarget.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "BTTask_TurnToTarget.h"
#include "ABAIController.h"
#include "ABCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"
UBTTask_TurnToTarget::UBTTask_TurnToTarget()
{
NodeName = TEXT("Turn");
}
EBTNodeResult::Type UBTTask_TurnToTarget::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);
auto ABCharacter = Cast<AABCharacter>(OwnerComp.GetAIOwner()->GetPawn());
if (nullptr == ABCharacter)
return EBTNodeResult::Failed;
auto Target = Cast<AABCharacter>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(AABAIController::TargetKey));
if (nullptr == Target)
return EBTNodeResult::Failed;
FVector LookVector = Target->GetActorLocation() - ABCharacter->GetActorLocation();
LookVector.Z = 0.0f;
FRotator TargetRot = FRotationMatrix::MakeFromX(LookVector).Rotator();
ABCharacter->SetActorRotation(FMath::RInterpTo(ABCharacter->GetActorRotation(), TargetRot, GetWorld()->GetDeltaSeconds(), 2.0f));
return EBTNodeResult::Succeeded;
}
맨 왼쪽 아래에는 Simple Parallel 을 넣어 동작을 동시에 실행한다.
NPC 추격 기능을 위한 Detect 구현
이제 Detect 부터 시작해서 실제 코드를 보자. BTService_Detect 는 BTService 를 부모로 하여 생성한다.
비헤이비어 트리의 서비스 노드는 자신이 속한 컴포짓 노드가 활성화될 경우 주기적으로 TickNode 함수를 호출한다. 호출하는 주기는 서비스 노드 내부에 설정된 Interval 속성 값으로 지정할 수 있다.
TickNode 함수에는 반경 6미터 내에 캐릭터가 있는지 감지하는 기능을 넣고(OverlapMultiByChannel), 캐릭터 정보는 TArray 로 전달한다.
BTService_Detect.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "ArenaBattle.h"
#include "BehaviorTree/BTService.h"
#include "BTService_Detect.generated.h"
/**
*
*/
UCLASS()
class ARENABATTLE_API UBTService_Detect : public UBTService
{
GENERATED_BODY()
public:
UBTService_Detect();
protected:
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};
BTService_Detect.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "BTService_Detect.h"
#include "ABAIController.h"
#include "ABCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "DrawDebugHelpers.h"
UBTService_Detect::UBTService_Detect()
{
NodeName = TEXT("Detect");
Interval = 1.0f;
}
void UBTService_Detect::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
APawn* ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
if (nullptr == ControllingPawn) return;
UWorld* World = ControllingPawn->GetWorld();
FVector Center = ControllingPawn->GetActorLocation();
float DetectRadius = 600.0f;
if (nullptr == World) return;
TArray<FOverlapResult> OverlapResults;
FCollisionQueryParams CollisionQueryParam(NAME_None, false, ControllingPawn);
bool bResult = World->OverlapMultiByChannel(
OverlapResults,
Center,
FQuat::Identity,
ECollisionChannel::ECC_GameTraceChannel2,
FCollisionShape::MakeSphere(DetectRadius),
CollisionQueryParam
);
if (bResult)
{
for (auto const& OverlapResult : OverlapResults)
{
AABCharacter* ABCharacter = Cast<AABCharacter>(OverlapResult.GetActor());
if (ABCharacter && ABCharacter->GetController()->IsPlayerController())
{
OwnerComp.GetBlackboardComponent()->SetValueAsObject(AABAIController::TargetKey, ABCharacter);
DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Green, false, 0.2f);
DrawDebugPoint(World, ABCharacter->GetActorLocation(), 10.0f, FColor::Blue, false, 0.2f);
DrawDebugLine(World, ControllingPawn->GetActorLocation(), ABCharacter->GetActorLocation(), FColor::Blue, false, 0.27f);
return;
}
}
}
OwnerComp.GetBlackboardComponent()->SetValueAsObject(AABAIController::TargetKey, nullptr);
DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Red, false, 0.2f);
}
비헤이비어 트리에서는 다음과 같이 Detect 서비스를 부착할 수 있다.
디텍터 오른쪽은 위와 같이 설정한다. Notify Observer 에 On Value Change 를, 관찰자 중단에 Self 를 넣는다. Target 은 Is Not Set 이다.
왼쪽은 Target 을 Is Set 으로 넣어준다. 나머지는 마찬가지로 비슷하게 설정한다.
마지막으로, 비헤이비어 트리 셀렉터에 IsInAttackRange 데코레이터를 추가한다. 해당 데코레이터의 조건이 참이 되면 공격을 수행하고, 아니면 플레이어를 따라잡도록 만들 것이다.
데코레이터 클래스는 CalculateRawConditionValue 함수를 상속받아 원하는 조건을 체크한다. 이 함수는 const 로 선언돼 데코레이터 클래스의 멤버 변수 값은 변경할 수 없다.
BTDecorator_IsInAttackRange.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "ArenaBattle.h"
#include "BehaviorTree/BTDecorator.h"
#include "BTDecorator_IsInAttackRange.generated.h"
/**
*
*/
UCLASS()
class ARENABATTLE_API UBTDecorator_IsInAttackRange : public UBTDecorator
{
GENERATED_BODY()
public:
UBTDecorator_IsInAttackRange();
protected:
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
};
BTDecorator_IsInAttackRange.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "BTDecorator_IsInAttackRange.h"
#include "ABAIController.h"
#include "ABCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"
UBTDecorator_IsInAttackRange::UBTDecorator_IsInAttackRange()
{
NodeName = TEXT("CanAttack");
}
bool UBTDecorator_IsInAttackRange::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
bool bResult = Super::CalculateRawConditionValue(OwnerComp, NodeMemory);
auto ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
if (nullptr == ControllingPawn)
return false;
auto Target = Cast<AABCharacter>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(AABAIController::TargetKey));
if (nullptr == Target)
return false;
bResult = (Target->GetDistanceTo(ControllingPawn) <= 200.0f);
return bResult;
}
IsInAttackRange 의 오른쪽은 관찰자 중단을 Self 로, InverseCondition 을 true 로 체크해주면 된다.
NPC 가 플레이어를 공격하고, 데미지가 다는 것도 확인해 볼 수 있다!
'Game Dev > Unreal C++ : Tutorial' 카테고리의 다른 글
이득우 언리얼 C++ 14 : 게임플레이의 제작 (9) | 2022.03.23 |
---|---|
이득우 언리얼 C++ 13 : 프로젝트의 설정과 무한 맵의 제작 (4) | 2022.03.23 |
이득우의 언리얼 C++ 11 : 게임 데이터와 UI 위젯 (3) | 2022.03.19 |
이득우의 언리얼 C++ 10 : 아이템 상자와 무기 제작 (2) | 2022.03.10 |
이득우의 언리얼 C++ 9 : 콜리전(오브젝트 채널, 트레이스 채널)과 대미지 프레임워크 (2) | 2022.03.06 |