Online Subsystem

Unreal 이론|2024. 11. 25. 20:12

IP Address

  • Local IP Address는 로컬 네트워크 라우터에서 컴퓨터에 할당한 주소 (192, 168등으로 시작함)
  • 로컬 네트워크 내의 다른 컴퓨터만이 볼 수 있음. 외부의 컴퓨터는 로컬 IP를 이용해 컴퓨터에 연결할 수 없음
  • 컴퓨터는 이더넷이나 와이파이로 ISP(Internet Service Provider) 와 연결되있음
  • 외부와의 연결을 위해 ISP는 라우터에 공용 IP주소를 할당함
  • 따라서 네트워크 흐름은 공개or외부IP주소 -> 로컬or내부IP주소로 이동함
  • 그렇다면 로컬 네트워크 외부의 다른 컴퓨터와 연결되어 게임을 하려면?
    • 다른 유저의 외부 IP주소를 알고있다면 연결할 수 있다. -> 하지만 비현실적, 비효율적
    • Dedicated Server는 연결 가능한 IP주소 중 하나를 할당하고, 해당 IP주소를 다른 플레이어들에게 공유해 서로 연결이 가능하게 함 -> 게임에 플레이어가 많아질수록 더 많은 서버, 더 많은 저장공간, 더 많은 자원이 필요
    • Listen Server는 플레이어중 하나가 Listen Server로 참여, 다른 플레이어들이 여기에 붙는다 -> 서버는 Listen Server 플레이어의 호스팅만 담당한다면 Dedicated Server에 비해 코스트가 줄어든다.

Online Subsystem이란?

  • 유저들이 IP Address 없이도 게임에 접속할 수 있도록 만들어진 Code Base
  • 플랫폼의 라이브 서비스에 맞춘 코드를 작성할 필요 없도록 크로스플랫폼의 세부사항을 언리얼의 Abstraction layer에서 자체적으로 처리했음
  • 즉 Onlinesubsystem은 다양한 온라인 서비스에 연결되도록 설계된 네트워킹 추상화 계층
  • 이러한 온라인 서브시스템을 사용한 플러그인을 패키징해 IP 주소의 입력 없이 간단히 Play 버튼 하나만으로 멀티플레이 세션에 접속할 수 있도록 디자인해보자.

Online Subsystem

  • 온라인 플랫폼 서비스에 접근하는 기능 제공
  • Xbox, Steam과 같은 온라인 플랫폼 서비스는 친구, 업적, 세션 등의 기능을 제공하는 자체 서비스셋이 있고, 온라인 서브시스템은 이런 다양한 서비스를 처리하도록 설계된 일련의 인터페이스가 있음
  • 그래서 온라인 서브시스템을 사용하려면 타겟 플랫폼을 설정하고 프로젝트를 구성해야함
  • Engine.ini 파일에서 다음과 같은 블럭을 추가해 기본 플랫폼 서비스를 지정한다.
[OnlineSubsystem]
DefaultPlatformService=<Platform>
  • I 타입 클래스로, IOnlineSubsystem::Get() static 함수를 통해 클래스를 얻어 사용할 수 있다.

Session Interface

  • 게임 세션의 생성, 관리, 파괴 로직을 담당하는 인터페이스
  • 세션 검색과 매치메이킹 기능 또한 담당
  • 세션 = 게임 서버에서 돌아가는 인스턴스
    • Lifetime of Session
      • Create Session -> 유저 대기 -> 유저 등록 -> 세션 시작 -> 게임 플레이
      • 게임 종료 후 세션 종료 -> 등록된 유저 해제 -> (필요시) 세션 업데이트 or 매치 설정 변경 or 세션 Destory
    • Session Interface Functions
      • CreateSession()
      • FindSessions()
      • JoinSession()
      • StartSession()
      • DestroySession()
      • 이 함수들만 기억해도 작동하는 게임을 만들 수 있다.

세션 생성 및 참가 로직

  • 이렇게 소개했을 땐 내가 짠 로직과 다를게 없다.. 뭐가 잘못됬는지 알아보자.
  • 이러한 로직을 다루는 Class를 생성

'Unreal 이론' 카테고리의 다른 글

Multiplayer Games Network model  (0) 2024.11.24
게임의 완성  (0) 2024.11.23
게임 플로우 다듬기  (1) 2024.11.21
헤드업디스플레이의 구현  (0) 2024.11.14
인공지능 - 행동트리 모델의 구현  (0) 2024.11.11

댓글()

Multiplayer Games Network model

Unreal 이론|2024. 11. 24. 17:29

Single Games

  • 싱글 게임은 단일 컴퓨터에서 단일 게임 세션으로 구성됨
  • 네트워크를 통해 다른 컴퓨터에서 실행되는 게임의 다른 인스턴스로 정보를 전송할 필요 없음

Multiplayer Games

  • 두개 이상의 인스턴스가 서로 다른 컴퓨터에서 실행되는 형태
  • 서로 다른 인스턴스에서의 변경사항 (ex: 캐릭터 입력)은 게임의 다른 인스턴스로 전송되야함
  • 그러한 정보를 어떻게 옮기는가? 
    • Peer to Peer
      • 정보 전송의 가장 간단한 방법
      • 변경사항이 다른 플레이어의 캐릭터로 "직접" 전송된다
      • 플레이어가 많아질수록 많은 양의 데이터가 네트워크를 통해 전송되야 하므로 부하 증가
      • Authoritative 버전의 게임이 없다 -> 로컬 게임의 정보가 변경될 때 마다 인스턴스가 변경되므로 어떤 플레이어의 게임 인스턴스가 올바른가? -> 어떤것도 올바른 버전의 게임이 아님 -> 클라이언트 변조 문제로도 연결 가능
    • Client-Server model
      • 단일 시스템이 서버로 지정되고, 다른 모든 시스템은 클라이언트로 지정
      • 모든 클라이언트는 서버하고만 연결되어 통신하며, 다른 클라이언트에게 정보를 전송하지 않는다.
      • 서버와 정보를 송수신하기 위한 대역폭 요구사항만 충족하면 된다
      • 서버가 올바른 버전으로 정립된 게임 버전을 실행한다는게 핵심
      • 각 클라이언트가 서버에 요청을 보낼 때 마다(ex: 캐릭터를 움직이는 요청) 서버는 적절한 요청인지 확인
      • 서버가 적절한 요청인지 확인 후 다른 클라이언트들에게 정보를 전달 -> 이 과정을 복제(replication)라고 함

클라이언트-서버 모델의 종류

  •  Listen Server
    • 한명의 플레이어가 서버를 맡고, 나머지 플레이어가 클라이언트
    • 서버를 맡은 플레이어가 게임을 플레이하며 그래픽을 렌더링함
    • 서버를 맡은 플레이어가 응답속도에서 좀 더 유리함
  • Dedicated Server
    • 게임을 플레이 하지 않는 서버컴퓨터 존재
    • 그래픽을 서버컴퓨터가 렌더링하진 않는다
    • 서버가 Authoritative 버전 게임의 시뮬레이션만 담당하며 클라이언트들에게 데이터를 복제해 전송한다
    • 특히 대규모 MMO나 많은 유저가 빠른 응답속도를 필요로 하는 게임에 적합

What Unreal Engine uses?

  • Authoritative Client-Server model
  • 한 컴퓨터는 항상 서버, 다른컴퓨터는 클라이언트로 서버에 연결된다.
  • 서버 버전만이 항상 Authoritative 버전
  • 싱글 플레이어 게임도 클라이언트-서버 모델(클라이언트와 서버가 같은 컴퓨터)

Testing Multiplayer - Lan & PIE

  • 멀티플레이 테스트 -> PIE의 Multiplayer Options 확인
    • Play As Listen Server -> 에디터 인스턴스가 Server와 Client 권한을 동시에 가짐. 다른 인스턴스는 Client로 에디터 인스턴스에 연결된것.
    • Standalone이나 Listen Server는 쉽게 생각할 수 있지만, Play as Client는 좀 다르다
    • 백그라운드에 Dedicated Server가 올라가고, 서로 다른 클라이언트가 설정한 수 만큼 인스턴스로 생성되어 윈도우에 표시된다.
    • 3rd person 템플릿에 배치된 기본 캐릭터에 possess 되지 않고, 클라이언트 수만큼 캐릭터가 스폰된다.

Setting up LAN Connection

  • 하나의 라우터에 여러 플레이어가 붙어서 플레이하는 게임 형태
  • 각각의 플레이어는 고유한 local IP 주소를 가지고 있다.
  • 동일한 네트워크 환경에 Local IP로 접속하는것

Open Level(), ClientTravel()

  • Open Level 함수도 그냥 썻었는데, 짚고 넘어갈 부분이 있는것 같다.
  • 첫번째 파라미터가 월드 컨텍스트 오브젝트. 간단하게 this 라는 키워드를 사용했는데, 월드에 존재하여 해당 월드의 컨텍스트를 전달할 수 있는 오브젝트이다.
  • ClientTravel은 APlayerController 클래스의 함수.
  • GetGameInstance()->Get***PlayerController() 함수를 통해 PlayerController를 가져와 ClientTravel을 호출

'Unreal 이론' 카테고리의 다른 글

Online Subsystem  (0) 2024.11.25
게임의 완성  (0) 2024.11.23
게임 플로우 다듬기  (1) 2024.11.21
헤드업디스플레이의 구현  (0) 2024.11.14
인공지능 - 행동트리 모델의 구현  (0) 2024.11.11

댓글()

게임의 완성

Unreal 이론|2024. 11. 23. 23:02

[이득우의 언리얼 프로그래밍 Part2 수업의 정리]

 

게임 모드

  • 멀티플레이를 포함해 게임에서 유일하게 존재하는 게임의 심판 오브젝트
  • 최상단에서 게임의 진행을 관리하며, 게임 판정에 관련된 중요한 행동을 주관하는데 적합
  • 다양한 게임 규칙을 적용할 수 있도록 핵심 기능과 분리해 설계하는것이 바람직
  • 게임의 상태와 플레이어의 상태를 별도로 저장할 수 있는 프레임웍 제공
  • 지금의 프로젝트는 싱글 게임이기 때문에 GameState와 PlayerState는 사용하지 않음

 

구현을 위해 GameMode 클래스에 코드 작성

  • 현재 진행되고 있는 게임의 점수와 게임 클리어를 위한 점수 두가지 설정
  • CurrentScore 같은 경우는 개별 플레이어의 점수이므로, 게임의 규모가 커진다면 PlayerState에 보관하는것이 좋다
  • StageGimmick 클래스의 OnOpponentDestroyed 함수가 적을 처치했을 때 호출되므로, 여기서 점수를 추가하도록 구성하는게 좋다
  • 클리어했을 땐 보상상자가 나오는 로직 대신 return시켜 게임이 그대로 끝나도록 구성
  • 플레이어가 죽는 경우엔 ABCharacterPlayer 클래스의 SetDead 함수에 로직 작성
  • 이렇게 중요한 정보를 GameMode를 호출해 전달하면 GameMode 클래스에선 사용자가 어떠한 액션을 취하도록 유도
  • 가장 적합한 방법은 UI를 띄워주는것. 하지만 GameMode는 UI 컨트롤에 적합하지 않으므로 UI를 띄워달라고 별도로 명령을 내린다
  • PlayerController에 GameScoreChanged, GameClear, GameOver 세 함수를 선언하고 GameMode에 불러온다.
void AABGameMode::OnPlayerScoreChanged(int32 NewPlayerScore)
{
	CurrentScore = NewPlayerScore;

	AABPlayerController* ABPlayerController = Cast<AABPlayerController>(GetWorld()->GetFirstPlayerController());
	if (ABPlayerController) {
		ABPlayerController->GameScoreChanged(CurrentScore);
	}

	if (CurrentScore >= ClearScore) {
		bIsCleared = true;
		if (ABPlayerController) {
			ABPlayerController->GameClear();
		}
	}
}

void AABGameMode::OnPlayerDead()
{
	AABPlayerController* ABPlayerController = Cast<AABPlayerController>(GetWorld()->GetFirstPlayerController());
	if (ABPlayerController) {
		ABPlayerController->GameOver();
	}
}
  • 이후 같은 기능을 하는 K2_OnGameState 함수들을 선언해주고 블루프린트에서 구현

게임의 저장 기능

  • 언리얼의 SaveGame 클래스를 상속받아 ABGameSave 클래스를 Player 폴더에 생성
  • RetryCount라는 간단한 변수 추가 후 PlayerController에 SaveGame 객체 가져옴
  • 게임이 시작되면 저장된 세이브 데이터가 있는지 확인 후 이것을 로딩하는 코드 작성
  • UGameplayStatics에서 제공하는 LoadGameFromSlot(TEXT("Player0"), 0) 함수 활용해 저장
    • Player0 -> 세이브 파일명, 0-> 플레이어 아이디. 싱글플레이의 경우 항상 0번 지정

게임 패키징

  • 에디터 상단 툴바의 Platforms->Windows->Shipping(디버깅에 관련된 모든 부분 제외된 가장 가벼운 선택지)
  • Project Setting->Package->Additional builds for this Project 아래의 advanced 항목-> Additional Asset Directories to Cook : 우리가 사용한 에셋들을 플랫폼에 맞춰서 변환하는 작업
  • 기본적으로 추가된 항목 외에도 아이템 등 코드에서 불러들이는 에셋들을 수동으로 추가해줘야함

'Unreal 이론' 카테고리의 다른 글

Online Subsystem  (0) 2024.11.25
Multiplayer Games Network model  (0) 2024.11.24
게임 플로우 다듬기  (1) 2024.11.21
헤드업디스플레이의 구현  (0) 2024.11.14
인공지능 - 행동트리 모델의 구현  (0) 2024.11.11

댓글()

게임 플로우 다듬기

Unreal 이론|2024. 11. 21. 14:55

[이득우의 언리얼 프로그래밍 Part2 수업의 정리]

 

게임 구성 요소의 분류

  • 세개의 레이어를 기준으로 게임을 구성하는 다양한 기능의 구현

 

게임플로우를 위해 보강할 내용

  • 죽었을 때의 NPC와 플레이어의 처리
  • 이동속도의 적용
  • 포션/스크롤 아이템의 추가 구현과 캐릭터의 적용
  • 스탯 기능 및 UI의 기능의 보강

죽었을 때 NPC의 인공지능 로직이 더이상 작동하지 않도록 처리

  • NonPlayer 클래스의 SetDead 함수에 StopAI 실행되도록 추가

죽었을 때 플레이어가 더이상 입력을 할 수 없도록 처리

  • SetDead 구현을 하지 않았기 때문에, 플레이어 클래스에 SetDead 구현
  • SetDead 함수에서 PlayerController를 가져오고, DisableInput으로 입력 끄기
  • 이렇게 구현했다면 BeginPlay 함수에서 Enable 함수 추가
void AABCharacterPlayer::BeginPlay()
{
	Super::BeginPlay();

	APlayerController* PlayerController = Cast<APlayerController>(GetController());
	if (PlayerController) {
		EnableInput(PlayerController);
	}

	SetCharacterControl(CurrentCharacterControlType);

}

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

	// Stop Input
	APlayerController* PlayerController = Cast<APlayerController>(GetController());
	if (PlayerController) {
		DisableInput(PlayerController);
	}
}

 

스탯이 변경됐을 때 이동속도의 변화 적용

  • MovementSpeed 참고해서 적용
  • CharacterBase 클래스에 스탯 변경시 반영 함수 추가해 델리게이트에 연결
  • 초기화 단계에서 수행되는 PostInitializeComponents 함수에서 등록

 

아이템 추가로 HP회복 및 스탯 변경 구현

  • ABItemData를 상속받은 Potion 및 Scroll 클래스 Item 폴더에 생성
  • ItemData의 생성자 코드와 GetPrimaryAsset코드를 의무적으로 모든 아이템들이 구현하도록 설정
UCLASS()
class ARENABATTLE_API UABPotionItemData : public UABItemData
{
	GENERATED_BODY()
	
public:
	UABPotionItemData();

	FPrimaryAssetId GetPrimaryAssetId() const override {
		// 생성될 에셋의 아이디 직접 지정
		return FPrimaryAssetId("ABItemData", GetFName());
	}
	
public:
	UPROPERTY(EditAnywhere, Category = Hp)
	float HealAmount;
};
  • 빌드 후 에디터에서 포션의 회복량에 따른 DataAsset 추가

  • 위와 비슷하게 스크롤, 무기도 여러 종류를 스탯을 달리해 추가
  • ABCharacterStatComponent 클래스에 아이템 습득에 따른 스탯과 HP의 변화를 반영할 수 있도록 인라인 함수 추가, 값이 변경됬으니 BroadCast로 델리게이트 연결
  • CharacterBase 클래스에 포션과 스크롤 습득 기능 구현

HpBar 기능 확장

  • Hp바가 직관적이지 않으니 Hp바 위의 레이어에 숫자 표기 추가
  • Vertical Box에 overlay 블록 추가, hierarchy 수정
  • 텍스트 블록을 가져와서 Hp값에 따른 변화 적용

기본적인 게임 플로우를 다듬다 보면 기본 데이터들의 지속적인 수정이 불가피

  • 아이템은 Data Asset 우클릭->Bulk Edit Via Property Maxtrix
    • Grid 기능을 사용해 엑셀 셀 단위로 전체 아이템 값들을 바꿀수 있도록 인터페이스 제공
  • GameData의 엑셀 데이터
    • 엑셀에서 수정 후 Reimport하면 변경된 값 바로 적용가능

'Unreal 이론' 카테고리의 다른 글

Multiplayer Games Network model  (0) 2024.11.24
게임의 완성  (0) 2024.11.23
헤드업디스플레이의 구현  (0) 2024.11.14
인공지능 - 행동트리 모델의 구현  (0) 2024.11.11
인공지능 - 행동트리 모델의 이해  (2) 2024.11.10

댓글()

헤드업디스플레이의 구현

Unreal 이론|2024. 11. 14. 18:19

[이득우의 언리얼 프로그래밍 Part2 수업의 정리]

 

[플레이어의 HP와 스탯을 표시하는 헤드업 디스플레이 UI의 구현]\

 

헤드업 디스플레이의 생성 과정

  • 헤드업 디스플레이 (HUD)는 플레이어 컨트롤러에 의해 제작되고 관리되는 UI객체
  • HUD의 구현은 위젯을 생성하고 이를 플레이어 뷰포트에 띄우는 과정으로 생성된다.
  • 이렇게 만들어진 위젯은 자신을 소유한 플레이어 컨트롤러에 접근할 수 있다.

HUD는 모니터 화면 전체를 다 쓰기 때문에 Canvas Panel 사용

  • 클래스 추가-> UserWidget 선택-> 클래스 생성 후 WBP_ABHUD의 클래스 세팅으로 가 해당 클래스를 Parent Class로 설정해 생성한 클래스를 상속받도록 설정
  • ConstructorHelper를 사용해 생성자에서 WBP_ABHUD를 가져오고, BeginPlay에서 CreateWidget->AddtoViewport를 사용해 화면에 띄워준다.
  • 블루프린트에서 쓰던거랑 똑같은 구조인걸 알 수 있다.
#include "Player/ABPlayerController.h"
#include "UI/ABHUDWidget.h"

AABPlayerController::AABPlayerController()
{
	static ConstructorHelpers::FClassFinder<UABHUDWidget> ABHUDWidgetRef(TEXT("/Game/ArenaBattle/UI/WBP_ABHUD.WBP_ABHUD_C"));
	if (ABHUDWidgetRef.Class)
	{
		ABHUDWidgetClass = ABHUDWidgetRef.Class;
	}
}

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

	FInputModeGameOnly GameOnlyInputMode;
	SetInputMode(GameOnlyInputMode);

	ABHUDWidget = CreateWidget<UABHUDWidget>(this, ABHUDWidgetClass);
	if (ABHUDWidget) {
		ABHUDWidget->AddToViewport();
	}
}

 

초기화 프로세스의 정리

  • 컴포넌트, 액터, UI 위젯의 초기화 과정
  • 컴포넌트는 BeginPlay전에 컴포넌트가 가장 먼저 초기화되어 데이터가 확정되는것이 중요
  • Initalize Component로 스텟에 관한 데이터 먼저 초기화
  • 액터는 그 후 PostInitialize Components 함수로 안전하게 확정된 데이터를 쓴다
  • BeginPlay 이후 PlayerController는 CreateWidget으로 UI 위젯 초기화
  • UI위젯은 이때 NativeOnInitialized 함수 호출, 위젯이 생성되는것이고 눈에 보이진 않음
  • 눈에 보이려면 AddtoViewport 호출되야함. 이때 UI위젯에서 NativeConstruct로 위젯 확정지음



'Unreal 이론' 카테고리의 다른 글

게임의 완성  (0) 2024.11.23
게임 플로우 다듬기  (1) 2024.11.21
인공지능 - 행동트리 모델의 구현  (0) 2024.11.11
인공지능 - 행동트리 모델의 이해  (2) 2024.11.10
게임데이터 관리  (0) 2024.11.06

댓글()

인공지능 - 행동트리 모델의 구현

Unreal 이론|2024. 11. 11. 15:52

[이득우의 언리얼 프로그래밍 Part2 수업의 정리]

 

패트롤 기능 구현

  • Blackboard에서 Vector Key인 PatrolPos 추가
  • 새롭게 스폰한 NPC가 새로운 섹션에서 랜덤으로 정찰할 포지션을 지정하기 위해 네비게이션 메쉬 볼륨 추가
  • Place Actor->Volume->NavMeshBoundsVolume 추가

NavMesh 구축 후 뷰포트에서 P키를 누르면 초록색으로 길찾기를 수행가능한 영역이 표시된다
동적으로 맵이 추가되기 때문에 NavMesh의 설정 중 Runtime Generation 옵션을 Dynamic으로 변경

  • Dynamic으로 설정을 변경하면 스폰 액터를 통해 생성된 섹션도 NavMesh를 사용가능
  • 행동 트리 설계를 위해 Build.cs에 NavigationSystem, AIModule, GameplayTasks 세개의 모듈 추가
// BTTask_FindPatrolPos.cpp

EBTNodeResult::Type UBTTask_FindPatrolPos::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);

	APawn* 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(BBKEY_HOMEPOS);
	FNavLocation NextPatrolPos;

	if (NavSystem->GetRandomPointInNavigableRadius(Origin, 500.0f, NextPatrolPos)) {
		OwnerComp.GetBlackboardComponent()->SetValueAsVector(BBKEY_PATROLPOS, NextPatrolPos.Location);
		return EBTNodeResult::Succeeded;
	}

	return EBTNodeResult::Failed;
}
  • 다음 정찰 위치를 찾는 태스크 추가

위와 같이 하드코딩을 통해 구현한 내용을 빌드 후 확인해보면 NPC가 제대로 움직인다.

이제 해야할건 인터페이스에 NPC가 가져야 할 내용을 구현한 후 뿌려주는것.

  • NPC 헤더를 직접 참조하지 않고 인터페이스를 통해 간접적으로 필요한 값을 얻어오도록 구조 수정
  • AIInterface 클래스에 필요한 값을 가져오는 가상함수를 추가 후 NonPlayer클래스에서 그 함수를 구현, PatrolPos 클래스에서 인터페이스를 캐스팅 해 그 함수를 가져와 간접적으로 값을 얻는 구조

  • BTService 클래스를 상속받은 유저감지 Detect 노드 추가
  • 서비스를 부착한 컴포짓 노드가 활성화 되면 TickNode가 지정한 Interval로 계속해서 호출된다.
  • 내용물을 구현하려면 TickNode에 구현해서 Interval마다 구현한 작업을 수행하도록 구성

구현한 Detect 서비스를 Sequence 노드에 부착

  • BlackBoard에 Target 변수 생성, 타입 Pawn으로 지정
  • 여기까지 한 후 실행을 눌러보면, NPC의 감지 반경과 감지 성공시 플레이어와의 거리가 뷰포트에 출력되는걸 확인 가능

  • 플레이어를 쫓아가서 공격하도록 Selector 추가
  • 언리얼 엔진이 기본으로 제공하는 Blackboard 데코레이터 부여, Key값을 Target으로 지정해 타겟이 설정되있을때만 셀렉터 컴포짓이 수행되도록 설정
  • 이후 Attack, AttackInRange 함수 구현.
  • 델리게이트로 NonPlayer에서 공격하는 로직이 호출되도록 구성
  • NPC도 플레이어처럼 PressCombo 함수를 호출해주고, 콤보가 끝나는 타이밍을 계산하기 위한 함수 CharacterBase에 추가
  • 과정이 꽤 길고 다양한 클래스들을 수정해 코드를 넣진 못했지만, CharacterBase에 공격이 끝난것을 알려주는 가상 함수를 선언하고 ComboActionEnd 함수 마지막에 호출되도록 추가 -> 가상함수는 NonPlayer 클래스에서 받아서 구현한 후 Attack 클래스에서 델리게이트를 묶어줘서 흐름이 굴러가게끔 설계
  • 빌드 후 실행해보면 캐릭터를 잘 쫓아와서 공격하지만, 캐릭터가 쓰러진 후에도 계속해서 공격 -> 셀렉터에 Detect 서비스 추가하면 캐릭터가 쓰러진 후 타겟이 없어지므로 해결

  • 캐릭터가 NPC 공격 중 회피를 했지만 NPC가 몸을 돌리지 않고 처음 공격 방향으로만 공격하는 현상 발생
  • BTTask 노드 추가 - TurnToTarget 클래스 생성해 이를 해결

  • Simple Parallel 컴포짓으로 Selector를 대체하고, Turn to Target 노드 추가
  • 이후 실행해보면 공격하며 동시에 타겟으로 회전하는 NPC를 확인할 수 있다.

'Unreal 이론' 카테고리의 다른 글

게임 플로우 다듬기  (1) 2024.11.21
헤드업디스플레이의 구현  (0) 2024.11.14
인공지능 - 행동트리 모델의 이해  (2) 2024.11.10
게임데이터 관리  (0) 2024.11.06
무한 맵의 제작  (0) 2024.10.31

댓글()

인공지능 - 행동트리 모델의 이해

Unreal 이론|2024. 11. 10. 15:32

[이득우의 언리얼 프로그래밍 Part2 수업의 정리]

 

행동 트리(Behavior Tree)의 역사

  • 2004년 개발사 번지의 헤일로2에서 인공지능을 설계하는데 사용됨
  • 2005년 GDC에서 발표 : "Handling Complexity in the Halo 2 AI"
  • 우선순위와 트리구조를 사용해 인공지능을 설계하는 기법
  • 단순한 행동에서 복잡한 행동으로 이어지도록 설계
  • 게임산업에서 개량해 널리 사용하고 있음

행동 트리 모델의 장점

  • FSM이 가지고 있던 문제를 해결하는 새로운 게임 인공지능 모델의 수립
  • 행동 트리 모델의 장점
    • 모듈화 잘 되어있어 확장 자유로움
    • 트리를 기반으로 계층화 잘 되어있어, 복잡한 인공지능 모델을 쉽게 설계할 수 있음
    • 다이어그램으로 인공지능 모델을 효과적으로 표현할 수 있음
    • 제공되는 여러 편리한 부가 기능을 활용해 다양한 상황에 대해 손쉽게 제어 가능

행동 트리 모델의 구성 요소

  • 트리에서 항상 왼쪽에 있는 노드에 우선 순위 부여
  • 시작 상태에서 설정할 필요 없이 왼쪽에서부터 깊이 우선 탐색(DFS)을 시작한다

Root에 의해 의사결정이 이루어진다

 

행동 트리 모델의 구성 요소

  • 행동을 중심으로 설계
  • 단, 부모 노드에서 다수의 행동을 컨트롤. 이를 컴포짓(Composite)이라 함
    • 셀렉터 (여러 행동 중 하나의 행동을 지정)
    • 시퀀스 (여러 행동을 모두 수행)
    • 패러렐 (여러 행동을 함께 수행)
  • ex) 밥먹기, 영화보기 두 행동이 있다면
    • 셀렉터: 밥과 영화 중 하나를 선택
    • 시퀀스: 밥을 먹은 후 영화를 봄
    • 패러렐: 밥을 먹으며 영화를 봄
  • 행동에 대한 다양한 결과
    • 성공 (Succeeded): 행동의 성공
    • 실패 (Failed): 행동의 실패
    • 중지 (Aborted): 외부 요인으로 인한 행동의 실패
    • 진행 중 (InProgress): 행동 결과를 홀딩
  • 컴포짓 노드마다 다른 행동 결과 처리
    • 셀렉터: 성공한 노드가 나오면 종료 (밥이 안나오면 대신 영화 시청)
    • 시퀀스: 실패한 노드가 나올때까지 진행 (밥을 안먹었다면 영화도 안봄)
  • 컴포짓 노드에 부착하는 다양한 추가 기능
    • 데코레이터(Decorator): 컴포짓 노드가 실행되는 조건 지정, 만족할때만 노드 실행
    • 서비스(Service): 컴포짓 노드가 활성화될 때 주기적으로 실행하는 부가 명령
    • 관찰자 중단(Abort): 데코레이터 조건에 부합되면 컴포짓 내 활동을 모두 중단

행동 트리 모델의 예시

  • 퇴근하고 집에 가는 행동 트리 모델의 설정
    • 지하철 역까지 이동 -> 지하철 탑승 -> 버스 환승 -> 문앞 도착
    • 시퀀스 컴포짓을 사용해 이들을 묶는다.

 

  • 트리가 가진 깊이를 활용해 단계별로 세부적인 행동 설계
  • 퇴근하고 집에가기 
    • 1단계: 지하철 역까지 이동한 후, 지하철 탑승, 버스환승, 문앞 도착
    • 2단계: 셀렉터 컴포짓을 사용해 지하철 역까지 걷거나 버스를 타거나 둘 중 하나 선택

 

컴포짓의 데코레이터 설정

  • 컴포짓에 조건을 걸어 선택의 폭 넓힐 수 있도록 확장
  • 우선 순위가 높은 컴포짓을 왼쪽에 배치, 데코레이터 설정

 

컴포짓에 관찰자 중단(Abort) 설정

  • 비상상황에 대한 처리. ex) 친구로부터 함께 놀자는 연락 왔다.
  • 트리 맨 왼쪽에 친구에게 간다는 컴포짓 추가
  • 집에 가는 모든 컴포짓에 친구와 약속이라는 Abort 추가
  • 친구에게 연락이 오면 집에 가던 모든 행동 중단 후 Root에서 다시 판단

행동 트리 모델의 구현 예시

  • 전형적인 RPG 게임 NPC에 대한 행동 트리 모델 예시
  • 쉬기(IDLE)와 정찰(PATROL)에 상태에 대한 행동 트리

  • 전형적인 RPG 게임 플레이어에 대한 행동 트리 모델 예시
  • 추가적으로 추격(CHASE)과 공격(ATTACK) 상태에 대한 행동 트리

 

그렇다면 실제 적용은 어떻게 하는가?

  • 월드에 배치된 캐릭터, SpawnActor 함수를 사용해 스폰시킨 캐릭터 둘 중 어느쪽에 AI를 적용할지 선택하는 Auto Possess AI 옵션. 둘다 적용하도록 선택
  • NPC 액터에 빙의할 AI 컨트롤러 클래스 선택하는 옵션 - 행동 트리 모델 적용한 클래스 새로 생성
// ABCharacterNonPlayer.cpp

#include "AI/ABAIController.h"

AABCharacterNonPlayer::AABCharacterNonPlayer()
{
	GetMesh()->SetHiddenInGame(true);

	AIControllerClass = AABAIController::StaticClass();
	AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}
  • NPC 생성자 코드에 AIControllerClass 할당

Behavior Tree, Blackboard 두가지 에셋을 추가

  • Blackboard: 인공지능 모델에서 의사결정을 하기 위한 기본 데이터 제공하는 데이터 저장소
// ABAIController.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "AIController.h"
#include "ABAIController.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API AABAIController : public AAIController
{
	GENERATED_BODY()

public:
	AABAIController();

	void RunAI();
	void StopAI();

protected:
	// 어떤 컨트롤러가 폰에 빙의해서 조종할때 발생되는 이벤트 함수
	virtual void OnPossess(APawn* InPawn) override;
	
private:
	UPROPERTY()
	TObjectPtr<class UBlackboardData> BBAsset;

	UPROPERTY()
	TObjectPtr<class UBehaviorTree> BTAsset;

};

// ABAIController.cpp
// Fill out your copyright notice in the Description page of Project Settings.


#include "AI/ABAIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BlackboardComponent.h"

AABAIController::AABAIController()
{
	static ConstructorHelpers::FObjectFinder<UBlackboardData> BBAssetRef(TEXT("/Script/AIModule.BlackboardData'/Game/ArenaBattle/AI/BB_ABCharacter.BB_ABCharacter'"));
	if (nullptr != BBAssetRef.Object)
	{
		BBAsset = BBAssetRef.Object;
	}

	static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTAssetRef(TEXT("/Script/AIModule.BehaviorTree'/Game/ArenaBattle/AI/BT_ABCharacter.BT_ABCharacter'"));
	if (nullptr != BTAssetRef.Object)
	{
		BTAsset = BTAssetRef.Object;
	}
}

void AABAIController::RunAI()
{
	UBlackboardComponent* BlackboardPtr = Blackboard.Get();
	if (UseBlackboard(BBAsset, BlackboardPtr)) {
		bool RunResult = RunBehaviorTree(BTAsset);
		ensure(RunResult);
	}
}

void AABAIController::StopAI()
{
	UBehaviorTreeComponent* BTComponent = Cast<UBehaviorTreeComponent>(BrainComponent);
	if (BTComponent) {
		BTComponent->StopTree();
	}
}

void AABAIController::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);

	RunAI();
}
  • 이후 ABAIController에서 Blackboard, BehaviorTree 에셋을 가져오고, 간단히 동작+정지 함수 구현

'Unreal 이론' 카테고리의 다른 글

헤드업디스플레이의 구현  (0) 2024.11.14
인공지능 - 행동트리 모델의 구현  (0) 2024.11.11
게임데이터 관리  (0) 2024.11.06
무한 맵의 제작  (0) 2024.10.31
아이템 시스템  (1) 2024.10.30

댓글()

게임데이터 관리

Unreal 이론|2024. 11. 6. 13:10

[이득우의 언리얼 프로그래밍 Part2 수업의 정리]

 

 

사전 준비

CSV 파일로 게임의 설정 데이터를 저장해 언리얼 엔진에서 Import 한다.

프로젝트 폴더에 GameData 폴더를 만들고, 엑셀 파일을 UTF-8의 csv 파일로 저장

위의 CSV 파일을 사용하기 위해 인덱스 이름과 동일한 이름을 가진 구조체를 생성

-> 소스파일 폴더에서 구조체 헤더파일 작성 -> Generated project file 실행해 헤더파일을 프로젝트에 추가

 

엑셀 데이터의 임포트

  • DataAsset과 유사하게 FTableRowBase를 상속받은 구조체를 선언
  • 엑셀의 Name 컬럼 (얘가 key값이 된다)을 제외한 컬럼과 동일하게 UPROPERTY 속성 선언
  • 엑셀 데이터를 csv로 익스포트 한 후 언리얼 엔진에 임포트

기타->DataTable로 생성 후 Reimport를 클릭해 위에서 만든 Csv 파일을 가져온다

 

데이터를 관리할 싱글톤 클래스의 설정

  • 언리얼 엔진에서 제공하는 싱글톤 클래스
    • 게임 인스턴스
    • 에셋 매니저
    • 게임 플레이 관련 액터 (GameMode, GameState)
    • 프로젝트에 싱글톤으로 등록한 언리얼 오브젝트
  • 언리얼 오브젝트 생성자에서 사용하지 않도록 주의

  • 위의 사진에 싱글톤 클래스를 설정하면 엔진이 초기화 될 때 자동으로 GEngine이라는 전역변수에 싱글톤을 만들어줌
// ABGameSingleton.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "ABCharacterStat.h"
#include "ABGameSingleton.generated.h"

DECLARE_LOG_CATEGORY_EXTERN(LogABGameSingleton, Error, All);

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABGameSingleton : public UObject
{
	GENERATED_BODY()

public:
	UABGameSingleton();
	static UABGameSingleton& Get();

	// Character Stat Data Section
public:
	FORCEINLINE FABCharacterStat GetCharacterStat(int32 InLevel) const { return CharacterStatTable.IsValidIndex(InLevel) ? CharacterStatTable[InLevel] : FABCharacterStat(); }

	UPROPERTY()
	int32 CharacterMaxLevel;


private:
	// Stat Data 내부 보관용 배열 - 추후 필요한 객체들에게 이 배열로 뿌림
	TArray<FABCharacterStat> CharacterStatTable;
	
};


// ABGameSingleton.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "GameData/ABGameSingleton.h"

DEFINE_LOG_CATEGORY(LogABGameSingleton);

UABGameSingleton::UABGameSingleton()
{
	// UDataTable -> (Key, Value)의 한쌍으로 이루어짐
	static ConstructorHelpers::FObjectFinder<UDataTable> DataTableRef(TEXT("/Script/Engine.DataTable'/Game/ArenaBattle/GameData/ABCharacterStatTable.ABCharacterStatTable'"));
	if (nullptr != DataTableRef.Object)
	{
		const UDataTable* DataTable = DataTableRef.Object;
		check(DataTable->GetRowMap().Num() > 0);

		TArray<uint8*> ValueArray;
		DataTable->GetRowMap().GenerateValueArray(ValueArray);
		// Algo의 Transform 알고리즘으로 value값만 가져오도록 설계
		Algo::Transform(ValueArray, CharacterStatTable,
			[](uint8* Value)
			{
				return *reinterpret_cast<FABCharacterStat*>(Value);
			}
		);
	}

	CharacterMaxLevel = CharacterStatTable.Num();
	ensure(CharacterMaxLevel > 0);
}

UABGameSingleton& UABGameSingleton::Get()
{
	// 엔진 초기화시 바로 활성화 되기 때문에 Get 함수로 받을 수 없다면 잘못된것이므로 CastCheck로 형변환 됬는지 강력히 검사
	UABGameSingleton* Singleton = CastChecked<UABGameSingleton>(GEngine->GameSingleton);
	if (Singleton) {
		return *Singleton;
	}
	UE_LOG(LogABGameSingleton, Error, TEXT("Invalid Game Singleton"));

	// 코드 흐름을 위해 작성한것일 뿐 실제 여기까지 가는 일이 일어나지 않도록 앞에서 철저히 검사
	return *NewObject<UABGameSingleton>();
}
  • 싱글톤 클래스의 로직. 싱글톤 클래스가 엔진 초기화시 제대로 생성되었는지 검사하며, 요청한 객체가 잘 쓸수 있도록 생성자에서 DataTable의 value들을 가져온다.

 

프로젝트의 주요 레이어

  • 게임 레이어: 기믹과 NPC
  • 미들웨어 레이어: 캐릭터의 스탯 컴포넌트, 아이템 박스
  • 데이터 레이어: 스탯 데이터, 데이터 관리를 위한 싱글톤 클래스

위에서 아래로만 참조하도록

 

캐릭터 스탯 시스템

  • 위의 구조대로 Stat Component가 Base Stat과 무기의 능력치에 따른 modifier Stat을 합산하여 반영하도록 ABCharacterStatComponent의 구조를 변경, 아래의 함수와 변수를 추가
void SetLevelStat(int32 InNewLevel);
FORCEINLINE float GetCurrentLevel() const { return CurrentLevel; }
FORCEINLINE void SetModifierStat(const FABCharacterStat& InModifierStat) { ModifierStat = InModifierStat; }
FORCEINLINE FABCharacterStat GetTotalStat() const { return BaseStat + ModifierStat; }

UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat)
float CurrentLevel;

UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
FABCharacterStat BaseStat;

UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
FABCharacterStat ModifierStat;
  • 위의 구조 변경에 따라 CharacterBase 파일도 Stat의 값을 참조하도록 적절하게 변경

  • 기믹 클래스에서 스테이지가 상승할때 마다 NPC 레벨이 증가해 더 강해진 NPC를 스폰하도록 설정
  • 이를 위해 스테이지 번호를 지정하고, 이 번호의 증가에 NPC의 레벨을 연동. 이를 위해 CharacterBase에 SetLevel 함수 선언
  • 근데 이렇게 해서 테스트 하다보면 2스테이지부터 MaxHp와 CurrentHp가 안맞는 현상 발생

액터의 생성과 지연 생성의 프로세스

  • 액터의 BeginPlay에서 CurrentHp값과 MaxHp값이 세팅이 되지만, 기믹 함수에서는 이미 SpawnActor를 실행하고 SetLevel 함수가 실행되기 때문에 CurrentHp값이 반영안되는 문제 발생
  • SpawnActor 대신 SpawnActorDeferred 함수를 사용하면 FinishSpawning 함수를 마지막에 호출해 액터의 BeginPlay가 실행된 후 최종적으로 스폰처리가 완료된다
  • 스테이지 스폰과 NPC 스폰, 상자 스폰을 모두 Deferred로 변경해준다.

  • 캐릭터가 무기를 습득했을 때 무기스탯에 따라 최종 스탯을 얻는 구조 추가
  • Weapon 클래스에 ModifierStat 변수를 추가해서, CharacterBase의 equip weapon 함수에서 Stat이 WeaponItemData의 ModifierStat을 얻도록 로직 추가

  • NPC 캐릭터가 스폰될 때 각기 다른 캐릭터로 스폰되도록 설정 추가
  • 프로젝트 폴더의 Config->DefaultArenaBattle.ini 파일 추가.

NPCMeshes라는 TArray가 ABCharacterNonPlayer에 선언되어 있다면 이것의 값을 지정하는 ini 파일

  • AABCharacterNonPlayer에 Config파일을 가져와 메쉬를 로딩하도록 로직 추가
// ABCharacterNonPlayer.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Character/ABCharacterBase.h"
#include "Engine/StreamableManager.h"
#include "ABCharacterNonPlayer.generated.h"

/**
 * 
 */
UCLASS(config = ArenaBattle)
class ARENABATTLE_API AABCharacterNonPlayer : public AABCharacterBase
{
	GENERATED_BODY()

public:
	AABCharacterNonPlayer();

protected:
	virtual void PostInitializeComponents() override;

protected:
	void SetDead() override;
	void NPCMeshLoadCompleted();
	
	// 비동기 방식으로 로드
	UPROPERTY(config)
	TArray<FSoftObjectPath> NPCMeshes;

	TSharedPtr<FStreamableHandle> NPCMeshHandle;
};

// ABCharacterNonPlayer.cpp

#include "Character/ABCharacterNonPlayer.h"
#include "Engine/AssetManager.h"

AABCharacterNonPlayer::AABCharacterNonPlayer()
{
	GetMesh()->SetHiddenInGame(true);
}

void AABCharacterNonPlayer::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	ensure(NPCMeshes.Num() > 0);
	int32 RandIndex = FMath::RandRange(0, NPCMeshes.Num() - 1);
	NPCMeshHandle = UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(NPCMeshes[RandIndex], FStreamableDelegate::CreateUObject(this, &AABCharacterNonPlayer::NPCMeshLoadCompleted));
}

void AABCharacterNonPlayer::NPCMeshLoadCompleted()
{
	if (NPCMeshHandle.IsValid())
	{
		USkeletalMesh* NPCMesh = Cast<USkeletalMesh>(NPCMeshHandle->GetLoadedAsset());
		if (NPCMesh)
		{
			GetMesh()->SetSkeletalMesh(NPCMesh);
			GetMesh()->SetHiddenInGame(false);
		}
	}

	NPCMeshHandle->ReleaseHandle();
}
  • 이후 실행해보면 메쉬 로딩까지 NPC가 숨겨져있다가 랜덤한 캐릭터 메쉬로 잘 로딩되는것 확인 가능

'Unreal 이론' 카테고리의 다른 글

인공지능 - 행동트리 모델의 구현  (0) 2024.11.11
인공지능 - 행동트리 모델의 이해  (2) 2024.11.10
무한 맵의 제작  (0) 2024.10.31
아이템 시스템  (1) 2024.10.30
캐릭터 스탯과 위젯  (1) 2024.10.27

댓글()