이전 글 보기
https://mountain-noroo.tistory.com/59
[언리얼] 학원 12일차: 데칼, 몬스터 개선(데이터 테이블)
이전 글 보기 https://mountain-noroo.tistory.com/58 [언리얼] 학원 11일차: 또 다른 싱글톤, 콜리전 트레이스 채널 이전 글 보기 https://mountain-noroo.tistory.com/57 [언리얼] 학원 9일차: 투사체 개선, 충돌체 이전
mountain-noroo.tistory.com
데이터 테이블 행을 가져오기 위해 Enum을 문자열로 변환하기
// 데이터 입력
// 몬스터 데이터 테이블을 가져온다.
UDataTable* DataTable = LoadObject<UDataTable>(nullptr, TEXT("/Script/Engine.DataTable'/Game/BlueprintClass/Monster/Table/DT_Monster.DT_Monster'"));
if (IsValid(DataTable))
{
const UEnum* MonEnum = FindObject<UEnum>(ANY_PACKAGE, TEXT("EMON_TYPE")); // EMON_TYPE 의 enum 정보를 가져온다.
// FString 으로 가져오기, Enum 풀네임으로 나옴
FString fString = MonEnum->GetNameStringByValue((int64)m_MonType);
// FName 으로 가져오기, Enum 타입 이름으로 나옴
FName name = MonEnum->GetNameByValue((int64)m_MonType);
}
enum 타입을 하나하나 if문으로 매핑하는 것보다는 문자열로 변환하는 게 빠르다.
Enum을 문자열로 변환하는 방법은 두 가지가 있는데 먼저 언리얼의 문자열 타입에 대해 알아본다.
FString: 런타임 중에 수정하기 용이. 한 문자열도 선언할 때마다 새로 생성.
FName: 런타임 중에 이미 정해짐. 최적화를 위해 사용. 한 문자열은 하나만 존재. 문자열 비교를 할 때도 키값을 보기 때문에 바로 연산이 됨.
데이터 테이블의 문자열도 이미 생성된 것이기 때문에 FName으로 저장되어 있다.
두 타입 모든 위와 같은 방법으로 변환이 가능하며
FString으로 변환했을 시 풀네임이 나오고
FName으로 변환했을 시 타입 이름이 나온다
데이터 테이블 로우 핸들
// meta: 데이터테이블핸들은 무엇이든 가리킬 수 있기 때문에 해당 타입 데이터테이블만 가리킬 수 있게 한다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Info", meta = (RowType = "MonInfo"))
FDataTableRowHandle m_MonTableRow
행 이름을 디테일 창에 노출시켜 상속받은 블루프린트에서 직접 설정할 수 있다. enum값과 매핑하는 수고를 덜어도 된다.
void AMonster_Base::OnConstruction(const FTransform& transform)
{
// 데이터 테이블 안에서 몬스터 타입에 맞는 행 정보를 가져와서
FMonInfo* pInfo = nullptr;
if (IsValid(m_MonTableRow.DataTable) && !m_MonTableRow.RowName.IsNone())
{
pInfo = m_MonTableRow.DataTable->FindRow<FMonInfo>(m_MonTableRow.RowName, TEXT(""));
// 몬스터의 m_Info 에 값을 넣어준다.
if (nullptr != pInfo)
m_Info = *pInfo;
}
}
OnConstruction(const FTransform& transsform): 에디터 상에서 속성, 위치값이 변경될 때 호출되는 함수.
카메라 셰이크
카메라가 흔들리는 효과를 주는 기능
CameraShakeBase를 상속받아 블루프린트 클래스를 만든다.
// 카메라 쉐이크
TSubclassOf<UCameraShakeBase> pShakeClass = LoadClass<UCameraShakeBase>(nullptr, TEXT("/Script/Engine.Blueprint'/Game/BlueprintClass/Effect/BPC_ShakeDefalt.BPC_ShakeDefalt_C'"));
if (IsValid(pShakeClass))
{
GetWorld()->GetFirstPlayerController()->ClientStartCameraShake(pShakeClass);
}
이렇게 만든 셰이크를 가져와서 원하는 곳(ex 폭발)에서 실행하면 된다.
AI Controller
플레이어한테는 PlayerController을 사용하고
몬스터는 AIController를 사용한다.
둘 다 Pawn을 상속받은 클래스이기 때문에 Controller로 빙의하는 것이다.
PlayerController도 AIController도 Controller를 상속받는다.
UCLASS()
class UNREAL_3TH_API AAIController_Melee : public AAIController
{
GENERATED_BODY()
private:
// 사용할 행동트리 모듈
UBehaviorTree* m_BehaviorTree;
// 사용할 블랙보드
UBlackboardData* m_Blackboard;
public:
virtual void OnPossess(APawn* _Owner) override;
virtual void OnUnPossess() override;
};
OnPossess는 빙의를 할 때
OnUnPossess는 빙의를 해제할 때 사용한다.
BehaviorTree와 BlackBoard
Player는 InputComponent를 통해 조작하는 것처럼 몬스터는 행동트리(BehaviorTree)를 통해 조작한다.
행동트리는 블랙보드(BlackBoard)를 참조하며 값이 생기면 기록하고 사용한다.
둘 다 우클릭->인공지능으로 만들 수 있다.
생성 뒤 참조할 블랙보드 에셋을 비헤이비어트리의 디테일 창에서 설정할 수 있다.
void AAIController_Melee::OnPossess(APawn* _Owner)
{
Super::OnPossess(_Owner);
// 빙의한 Pawn 이 Monster_Base 파생클래스인지 확인
AMonster_Base* pMonster = Cast<AMonster_Base>(_Owner);
if (IsValid(pMonster))
{
LOG(AI, Error, TEXT("컨트롤러 빙의 대상 오류 : 몬스터가 아님"))
return;
}
// 빙의 대상(몬스터) 로 부터, 사용할 행동트리를 가져온다.
m_BehaviorTree = pMonster->GetBehaviorTree();
m_Blackboard = pMonster->GetBlackboard();
if (IsValid(m_Blackboard))
{
// AIController 가 처음부터 Blackboard 컴포넌트를 가지고 있는 것은 아님
// UseBlackboard 함수에 blackboard 를 전달하면, 그때 내부에서 BlackboardComponent를 만듬
UBlackboardComponent* pCom = nullptr;
UseBlackboard(m_Blackboard, pCom);
if (IsValid(m_BehaviorTree))
{
// BehaviorTree가 참조하는 Blackboard 와 Useblackboard 함수에 전달한 Blackboard가 서로 다르면
// BehaviorTree에서 참조하는 Blackboard의 우선순위가 더 높다
// RunBehaviorTree함수 안에서 BehaviorTreeComponent가 생성된다.
RunBehaviorTree(m_BehaviorTree);
}
}
}
빙의 대상한테서 사용할 행동트리와 블랙보드를 가져왔다.
주의할 점은 행동트리와 블랙보드는 리소스이며, 행동트리 컴포넌트와 블랙보드 컴포넌트를 필요로 하는데, 이는 각각 UseBlackboard 함수에 블랙보드를 전달했을 때, RunBehaviorTree 함수에 행동트리를 전달했을 때 만들어진다.
서비스
셀렉터를 생성하여 노드를 추가할 수 있다.
비헤이비어 트리는 전위순회 방식으로 작동된다.
셀렉터에는 데코레이터와 서비스를 추가할 수 있다.
데코레이터: 노드의 분기를 만든다. 흐름 제어
서비스: 주기적으로 해야 할 일을 실행한다.
필요한 서비스는 BTService 부모 클래스를 상속받아 직접 만들 수 있다.
UBTSDetectPlayer::UBTSDetectPlayer()
{
// 서비스 설명
NodeName = TEXT("Detect Player");
// 서비스 호출 간격
Interval = 0.5f;
}
기본적으로 서비스가 가지고 있는 인자로
서비스 설명, 서비스 호출 간격을 생성자에서 설정해 준다.
void UBTSDetectPlayer::TickNode(UBehaviorTreeComponent& _OwnCom, uint8* NodeMemory, float _DT)
{
Super::TickNode(_OwnCom, NodeMemory, _DT);
// 빙의한 몬스터에 대한 참조를 얻는다.
AAIController* Controller = _OwnCom.GetAIOwner();
if (!IsValid(Controller))
return;
AMonster_Base* pMonster = Cast<AMonster_Base>(Controller->GetPawn());
if (!IsValid(pMonster))
return;
// 빙의한 몬스터의 탐지번위 수치를 가져온다.
// 탐지범위 내에 플레이어가 있는지 확인한다.
ACharacter* pPlayer = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);
float Distance = FVector::Distance(pMonster->GetActorLocation(), pPlayer->GetActorLocation());
// 탐지범위 내에 플레이어가 있었다.
if (Distance < pMonster->GetMonsterInfo().DetectRange)
{
// 플레이어가 확인되었으면 타겟(플레이어)을 블랙보드에 기록한다.
Controller->GetBlackboardComponent()->SetValueAsObject(TEXT("Target"), pPlayer);
}
// 탐지범위 내에 플레이어가 없었다.
else
{
Controller->GetBlackboardComponent()->SetValueAsObject(TEXT("Target"), nullptr);
}
}
몬스터가 탐색 반경 내에 있으면 쫓아가는 간단한 서비스이다.
참고로 플레이어컨트롤러는 딱 한 명 있기 때문에, 플레이어를 가져오는 함수는 언리얼 엔진에서 지원해주고 있다.
위의 "Target"처럼 값을 저장할 키를 블랙보드에서 만들 수 있다.
키 타입을 설정하고 베이스 클래스를 선택하여 해당 베이스 클래스를 상속받는 오브젝트만 참조 가능하도록 설정한다.
기타
UPROPERTY에서 meta = (AllowPrivateAccess = "true") 라는 인자를 설정해 줄 수 있는데
이는 private 멤버변수일지라도 상속받은 블루프린트 클래스의 디테일 창에 노출시켜 준다는 뜻.
'언리얼 > Assortrack UE5' 카테고리의 다른 글
[언리얼] 학원 15일차: AI Controller 플레이어 추적 (0) | 2023.09.15 |
---|---|
[언리얼] 학원 14일차: AI Controller 몬스터의 플레이어 탐색 (Decorater, Task) (0) | 2023.09.15 |
[언리얼] 학원 12일차: 데칼, 몬스터 개선(데이터 테이블) (0) | 2023.09.12 |
[언리얼] 학원 11일차: 또 다른 싱글톤, 콜리전 트레이스 채널 (0) | 2023.09.11 |
[언리얼] 학원 9일차: 투사체 개선, 충돌체 (0) | 2023.09.07 |