게임을 시작하면 무슨 일이 벌어지는가?

 

1. 월드가 생성된다.

2. 월드에 레벨을 로딩한다.

3. 지정한 규칙에 따라 로컬 플레이어를 입장시킨다.

4. 플레이어에 관련된 다양한 물체가 생성된다.

 


 

프레임워크 클래스 관계

이 흐름도는 이러한 코어 게임플레이 클래스끼리의 상관 관계를 나타냅니다.
게임은 GameMode 와 GameState 로 이루어집니다.

게임에 참여하는 사람 플레이어는 PlayerController 에 연관됩니다. 이러한 PlayerController 는 플레이어가 레벨 내 물리적 존재를 가질 수 있도록 Pawn 에 빙의됩니다.
PlayerController 는 플레이어에게 입력 컨트롤, 헤드업 디스플레이 (HUD), 카메라 시야 처리를 위한 PlayerCameraManager 등을 줄 수 있습니다.

 

게임 프레임워크의 바탕은 GameMode 입니다. 
GameMode 에서는 게임의 규칙을 설정, 예제에선 결승선을 먼저 통과하는 플레이어가 이기는 규칙입니다.
플레이어 스폰 처리도 여기서 합니다.

플레이어 셋업이 이루어지는 곳은 PlayerController 로, 이것이 Pawn 을 빙의(possess)합니다. 
Pawn 은 게임 내 플레이어의 물리적 표현인 반면, Controller  Pawn 에 빙의되어 그 동작에 대한 규칙을 설정할 수 있습니다.
우리 예제에서는 Pawn 이 둘 있는데, 하나는 달팽이이고 하나는 토끼입니다. 토끼는 사실상 Character 가 되는데, 이는 Pawn 의 특수한 서브클래스로 달리기나 점프같은 이동 함수성이 내장되어 있습니다. 반면 달팽이는 이동 방식이 다르기 때문에 Pawn 클래스를 직접 확장하면 됩니다.

Pawn 자체에 이동이나 기타 게임 로직용 규칙을 포함시킬 수는 있지만, 그러한 함수성은 Controller 에 포함시킬 수도 있습니다. Controller 는 사람 플레이어의 입력을 받는 PlayerController아니면 컴퓨터의 자동 제어를 받는 AIController 가 될 수도 있습니다.
이 예제에서 플레이어는 달팽이를 제어할 것이기에, 달팽이 Pawn  PlayerController 에 빙의됩니다. 토끼는 AI 의 제어를 받는데, 언제 멈추고 달리고 낮잠을 잘 것인지 하는 규칙은 모두 AIController 에 설정되어 있으며,
이 컨트롤러가 토끼 Character 에 빙의됩니다. Camera 에 제공되는 시야는 사람 플레이어에게만 의미가 있으므로, 달팽이 Pawn 에 있는 CameraComponent 하나만 PlayerCamera 에 사용될 것입니다.

게임플레이 도중 플레이어에게 받은 Input 으로 맵 위의 달팽이를 이리저리 움직이면서, Camera 로 제공된 뷰 위에 겹쳐놓인 HUD 에 누가 처음으로 들어왔는지, 경과된 경주 시간은 얼마나 되는지가 표시됩니다.

 

 


월드에 플레이어, 아군, 적군 표시

Pawn

 이란 월드의 대리인 역할을 하는 Actor 입니다. Pawn 은 Controller 에 의해 possess(빙의) 가능하며, 입력을 쉽게 받을 수 있도록 구성 가능하고, 여러가지 다양한 플레이어같은 동작을 할 수 있습니다. 참고로 Pawn 은 인간형이라 가정되지 않습니다.

Character

캐릭터 는 인간형 Pawn 입니다. 기본적으로 콜리전에 쓸 CapsuleComponent 와 CharacterMovementComponent 가 들어있습니다. 기본적인 인간형 동작을 할 수 있으며, 네트워크를 통해 부드러운 이동 리플리케이션이 가능하고, 애니메이션 관련 함수성도 약간 있습니다.

 


플레이어 인풋 또는 AI 로직으로 폰 제어하기

Controller

컨트롤러 는 Pawn 에 대한 지시를 담당하는 Actor 입니다. 보통 두 가지 형태로 등장하는데, AIController 와 PlayerController 입니다. 컨트롤러는 Pawn 에 "possess"(빙의)되어 그 폰을 제어할 수 있습니다.

PlayerController

플레이어 컨트롤러 는 Pawn 과 그것을 제어하는 사람 플레이어를 잇는 인터페이스입니다. PlayerController 는 본질적으로 사람 플레이어의 의지를 나타냅니다.

AIController

AIController 는 말 그대로 Pawn 을 제어하는 의지를 시뮬레이션으로 재현한 것입니다.

 


플레이어에게 정보 표시하기

HUD

HUD 는 "Heads-Up Display" 의 준말이며, 여러 게임에서 볼 수 있는 머리 위에 뜨는 화면 표시기같은 것으로, 생명력, 탄환 수, 총 조준선 등이 표시됩니다. 각 PlayerController 에는 보통 이와 같은 것이 하나씩 있습니다.

Camera

PlayerCameraManager 는 플레이어의 "눈"을 나타내며, 그 동작을 관리합니다. 각 PlayerController 에는 보통 하나씩 있습니다. 자세한 것은 카메라 문서를 참고하세요.

 

 


게임의 규칙 설정 및 기록하기

GameMode

"게임" 이라는 것의 개념은 두 개의 클래스로 나뉩니다. 게임 모드와 게임 스테이트 는 게임의 규칙이나 승리 조건같은 것이 포함된 게임의 정의로, 서버에만 존재합니다. 보통은 플레이 도중 바뀌는 데이터는 많이 없어야 하며, 클라이언트에서 알아야 하는 트랜션트(휘발성) 데이터는 반드시 없어야 할 것입니다.

GameState

GameState 에는 접속된 플레이어 목록, 점수, 체크 게임에서 말들의 위치, 오픈 월드 게임에서 완료한 퀘스트 목록 등과 같은 것이 포함될 수 있는 게임 상태가 포함됩니다. GameState 는 서버와 모든 클라이언트에 존재하며, 최신 상태 유지를 위해 자유롭게 리플리케이트 가능합니다.

PlayerState

인간 플레이어 또는 플레이어인 척 하는 봇과 같은 게임 참여자의 상태를 말합니다. 게임의 일부로써 존재하는 플레이어가 아닌 AI 에는 PlayerState 가 없습니다. PlayerState 에 적합한 예제 데이터라면, 플레이어 이름, 점수, MOBA 게임류에서의 대전상대 레벨, CTF 게임에서 플레이어가 현재 깃발을 운반중인지 여부 등입니다. 모든 플레이어에 대한 PlayerState 는 (PlayerController 와는 달리) 모든 머신에 존재하며, 동기화 상태 유지를 위해 자유로이 리플리케이트 가능합니다.

'Unreal > Concept' 카테고리의 다른 글

게임모드, 스테이트, 인스턴스  (0) 2019.05.14
프로퍼티 지정자  (0) 2019.05.12
언리얼 게임실행 프로세스  (0) 2019.05.12
각종 포인터 얻어오기  (0) 2019.05.12
UClass  (0) 2019.05.09

엔진 시작 및 게임 실행 프로세스에 대한 설명 문서입니다.
여기서 보여드리는 방법은 크게 두 가지, 에디터 방법과 독립형 방법입니다

일반적인 이벤트 순서는, 엔진을 초기화시키고, GameInstance를 생성 및 초기화한 뒤, 레벨을 로드하고, 마지막으로 플레이를 시작합니다. 

하지만 독립형 모드와 에디터 모드 사이의 차이점이 있는데, 일부 함수가 호출되는 정확한 순서와, 어떤 함수가 호출되는지가 다릅니다. 아래 플로우 차트는 두 방법으로 병렬 실행했을 때 게임 시작 전 수렴할 때까지의 흐름을 보여줍니다.

 

게임 흐름

 

 

독립형

독립형 모드는, 에디터 외부에서 플레이하는 게임 모드로, 시작시 엔진 초기화에 이어 바로 게임 플레이에 필요한 오브젝트가 생성 및 초기화됩니다. 
GameInstance 와 같은 오브젝트는 (Engine 생성 및 초기화와 별개로) 엔진 시작 전 생성 및 초기화되며, 엔진의 시작 함수 호출 직후 시작 맵이 로드됩니다.
공식적으로 게임플레이가 시작되는 시점은, 레벨이 적합한 게임 모드와 게임 스테이트 에 이어 다른 액터 를 생성한 이후입니다.

 

에디터

에디터 모드는 에디터에서 플레이  에디터에서 시뮬레이트 에서 사용되며, 다른 흐름으로 실행됩니다.
엔진은 에디터 실행에 필요하므로 즉시 초기화 및 시작되나, GameInstance 와 같은 오브젝트의 생성 및 초기화는 사용자가 버튼을 눌러 PIE 또는 SIE 세션을 실행할 때까지 연기됩니다.
추가로 레벨의 액터 를 복제하여 에디터의 레벨에 영향을 끼치도록 하고, GameInstance 를 포함한 모든 오브젝트는 각 PIE 인스턴스마다 별도의 사본을 갖습니다. UWorld 클래스의 게임플레이 시작과 함께 에디터 방법과 독립형 방법의 재회가 이루어집니다.

'Unreal > Concept' 카테고리의 다른 글

프로퍼티 지정자  (0) 2019.05.12
게임 프레임워크  (0) 2019.05.12
각종 포인터 얻어오기  (0) 2019.05.12
UClass  (0) 2019.05.09
TSubclassOf  (0) 2019.05.08

* 타이틀 제작을 위해서 빈공백의 레벨을 새로 만든 후 Title 로 저장한다.

* 해당 레벨은 아무 기능 없이 UI 화면만 띄우는 역활을 수행할 것이다.

* 사용할 게임 모드와 UI를 띄울 플레이어 컨트롤러를 제작한다.

* PlayerController 상속받아 ABUIPlayerController 클래스 생성


* 새로운 플레이어 컨트롤러 클래스를 생성하면, 이를 상속받은 블루프린트에서 앞으로 띄울UI 클래스 값을 에디터에서 설정할 수 있도록 위젯 클래스 속성을 추가하고 EditDefaultOnly 키워드를 지정한다.

* 플레이어 컨트롤러의 로직은 게임을 시작하면 해당 클래스로부터 UI 인스턴스를 생성하고, 이를 뷰포트에 띄운 후에 입력은 UI에만 전달되도록 제작한다.

//ABUIPlayerController.h
#include "ArenaBattle.h"
#include "GameFramework/PlayerController.h"
#include "ABUIPlayerController.generated.h"

UCLASS()
class ARENABATTLE_API AABUIPlayerController : public APlayerController
{
	GENERATED_BODY()
	
protected:
	virtual void BeginPlay() override;

	UPROPERTY(EditDefaultsOnly, BlueprintReadwrite, Category = UI)
	TSubclassOf<class UUserWidget> UIWidgetClass;

	UPROPERTY()
	class UUSerWidget* UIWidgetInstance;
};

//ABUIPlayerController.cpp
#include "ABUIPlayerController.h"
#include "Blueprint/UserWidget.h"

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

	ABCHECK(nullptr != UIWidgetClass);

	UIWidgetInstance = CreateWidget<UUserWidget>(this, UIWidgetClass);
	ABCHECK(nullptr != UIWidgetInstance);

	UIWidgetInstance->AddToViewport();

	FInputModeUIOnly Mode;
	Mode.SetWidgetToFocus(UIWidgetInstance->GetCachedWidget());

	SetInputMode(Mode);
	bShowMouseCursor = true;
}

* C++ 플레이어 컨트롤러의 기본 로직을 완성하면 이를 기반으로 한 블루프린트 클래스를 생성한다. 

* 새롭게 생성된 블루 프린트 애셋에는 BP_TitleUIPlayerController 로 정함

* 생성된 플레이어 컨트롤러 블루 프린트를 더블 클릭한 후 오른쪽 상단의 UIWidget Class 속성을 클릭해 복사해 넣은 UI 애셋인 UI_Title을 지정한다.

.

* 컨트롤러 세팅이 끝나면, BP를 이용해 게임모드를 생성해 본다. Game Mode Base를 상속한 BP_TitleGameMode 생성

* Default Pawn Class의 정보를 아무 기능이 없는 Pawn으로 지정하고
* Player Controller class의 정보를 방금 생성한 BP_TitleGameMode로 설정한다

 

* 이젠 월드세팅에서 게임모드를 쉽게 변경 할 수 있다.


타이틀 UI가 뜬 결과 화면


* UI 에서 새로시작하기 버튼을 누르면 캐릭터 선택을 위한 Select 레벨로 이동하고
이어하기 버튼을 누르면 GamePlay 레벨로 이동한다. 또한 Player1이라는 슬롯의 게임 데이터가 없으면, 이어하기 버튼은 누를 수 없게 비활성화 된다.
* 로직은 다음과 같음

* 아직 Select 라는 레벨이 없어서 새로 시작하기 버튼을 누르면 오류가 발생한다.

* 그래서 캐릭터 선택을 담당하는 레벨인 Select 레벨을 제작해본다.
* 위와 동일한 방법으로 ABUIPlayerController을 상속받은 블루프린트 BP_SelectUIPlayerController 을 생성한다
* 그리고 BP_SelectGameMode라는 게임 모드를 생성하고, PlayerController 클래스와 DefaultPawnClass를 각각 BP_SelectUIPlayerContoller와 Pawn으로 지정한다

 

* 이제 월드세팅에서 BP_SelectGame를 기본 게임모드로 지정하면 캐릭터 양옆으로 버튼이 보인다.

 


* 이제 C++로 각 버튼에 대한 로직을 구현해본다
* UserWidget을 기반으로 하는 ABCharacterSelectWidget 클래스를 생성한다
* 현재 레벨에 있는 스켈레톤 메시 액터의 목록을 가져오고, 버튼을 누를 떄마다 스켈레톤 메시 컴포넌트에 지정한 스켈레톤 메시를 변경한 ㄴ기능을 구현한다.
* 현재월드에 있는 특정 타입을 상속 받은 액터의 목록은 TActorIterator<액터타입> 구문을 사용해 가져올 수 있다

//ABCharacterSelectWidget.h
#pragma once

#include "ArenaBattle.h"
#include "Blueprint/UserWidget.h"
#include "ABCharacterSelectWidget.generated.h"

UCLASS()
class ARENABATTLE_API UABCharacterSelectWidget : public UUserWidget
{
	GENERATED_BODY()
	

protected:
	UFUNCTION(BlueprintCallable)
		void NextCharacter(bool bForward = true);

	virtual void NativeConstruct() override;
protected:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Character)
	int32 CurrentIndex;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Character)
	int32 MaxIndex;

	TWeakObjectPtr<USkeletalMeshComponent> TargetComponent;
};

//ABCharacterSelectWidget.cpp
#include "ABCharacterSelectWidget.h"
#include "ABCharacterSetting.h"
#include "ABGameInstance.h"
#include "EngineUtils.h"
#include "Animation/SkeletalMeshActor.h"


void UABCharacterSelectWidget::NextCharacter(bool bForward)
{
	bForward ? CurrentIndex++ : CurrentIndex--;

	if (CurrentIndex == -1) CurrentIndex = MaxIndex = 1;
	if (CurrentIndex == MaxIndex) CurrentIndex = 0;

	auto CharacterSetting = GetDefault<UABCharacterSetting>();
	auto AssetRef = CharacterSetting->CharacterAssets[CurrentIndex];

	auto ABGameInstance = GetWorld()->GetGameInstance<UABGameInstance>();
	ABCHECK(nullptr != ABGameInstance);
	ABCHECK(TargetComponent.IsValid());

	USkeletalMesh* Asset = ABGameInstance->StreamableManager.LoadSynchronous<USkeletalMesh>(AssetRef);
	if (nullptr != Asset)
	{
		TargetComponent->SetSkeletalMesh(Asset);
	}

}

void UABCharacterSelectWidget::NativeConstruct()
{
	Super::NativeConstruct();

	CurrentIndex = 0;
	auto CharacterSetting = GetDefault<UABCharacterSetting>();
	MaxIndex = CharacterSetting->CharacterAssets.Num();

	for (TActorIterator<ASkeletalMeshActor> It(GetWorld()); It; ++It)
	{
		TargetComponent = It->GetSkeletalMeshComponent();
		break;
	}
}

* NextCharacter 함수는 블루프린트에서 사용할 수 있도록 함수에 UFUNCTION 매크로를 추가하고 BlueprintCallable 키워드를 지어했다. 

* 이번 C++코드에서 버튼 컨트롤과 NextCharacter 함수를 직접바인딩 했지만, 해당 함수는 블루프린트 로직에서도 호출할 수 있다.

//ABCharacterSelectWidget.h
{
protected:
...
	UPROPERTY()
		class UButton* PrevButton;

	UPROPERTY()
		class UButton* NextButton;

	UPROPERTY()
		class UEditableTextBox* TextBox;

	UPROPERTY()
		class UButton* ConfirmButton;

private:
	UFUNCTION()
		void OnPrevClicked();

	UFUNCTION()
		void OnNextClicked();

	UFUNCTION()
		void OnConfirmClicked();
}        

//ABCharacterSelectWidget.cpp
#include "Components/Button.h"
#include "Components/EditableTextBox.h"
#include "ABSaveGame.h"
#include "ABPlayerState.h"

void UABCharacterSelectWidget::NativeConstruct()
{
.....
	PrevButton = Cast<UButton>(GetWidgetFromName(TEXT("btnPrev")));
	ABCHECK(nullptr != PrevButton);

	NextButton = Cast<UButton>(GetWidgetFromName(TEXT("btnNext")));
	ABCHECK(nullptr != NextButton);

	TextBox = Cast<UEditableTextBox>(GetWidgetFromName(TEXT("edtPlayerName")));
	ABCHECK(nullptr != TextBox);

	ConfirmButton = Cast<UButton>(GetWidgetFromName(TEXT("btnConfirm")));
	ABCHECK(nullptr != ConfirmButton);

	PrevButton->OnClicked.AddDynamic(this, &UABCharacterSelectWidget::OnPrevClicked);
	NextButton->OnClicked.AddDynamic(this, &UABCharacterSelectWidget::OnNextClicked);
	ConfirmButton->OnClicked.AddDynamic(this, &UABCharacterSelectWidget::OnConfirmClicked);
}


void UABCharacterSelectWidget::OnPrevClicked()
{
	NextCharacter(false);
}

void UABCharacterSelectWidget::OnNextClicked()
{
	NextCharacter(true);
}

void UABCharacterSelectWidget::OnConfirmClicked()
{
	FString CharacterName = TextBox->GetText().ToString();
	if (CharacterName.Len() <= 0 || CharacterName.Len() > 10) return;

	UABSaveGame* NewPlayerData = NewObject<UABSaveGame>();
	NewPlayerData->PlayerName = CharacterName;
	NewPlayerData->Level = 1;
	NewPlayerData->Exp = 0;
	NewPlayerData->HighScore = 0;

	auto ABPlayerState = GetDefault<AABPlayerState>();
	if (UGameplayStatics::SaveGameToSlot(NewPlayerData, ABPlayerState->SaveSlotName, 0))
	{
		UGameplayStatics::OpenLevel(GetWorld(), TEXT("GamePlay"));
	}
	else
	{
		ABLOG(Error, TEXT("SaveGame Error!"));
	}
}

 


* 컴파일에 문제가 없다면 UI_Select 애셋의 부모클래스를 ABCharacterSelectWidget으로 변경한다

 

* 이제 진행을 하면 버튼을 누르 때 마다 캐릭터 애셋이 변경이 된다.

 

* 그리고 캐릭터를 선택한 수 하단의 텍스트 박스에 캐릭터가 사용할 이름을 입력하고 캐릭터 생성 버튼을 누른다. 그러면 입력한 플레이어 이름으로 게임이 시작되지만, 선택한 캐릭터와 다른 기본 캐릭터가 나온다.

* 현재 선택한 캐릭터가 게임플레이 에서도 동일하게 나오도록 하려면, 현재 선택한 캐릭터 정보를 저장하고 게임플레이 레벨에서 이를  로딩하는 기능을 만들어야 한다

* 캐릭터 생성 버튼을 누르면 현재 선택한 캐릭터 정보를 세이브데이터에 저장하고 이를 읽어들이는 기능을 구현한다

 

//ABSaveGame.h
{
public:
...
	UPROPERTY()
		int32 CharaterIndex;
};


//ABSaveGame.cpp
UABSaveGame::UABSaveGame()
{
...
	CharaterIndex = 0;
}
//ABCharacterSelectWidget.cpp
void UABCharacterSelectWidget::OnConfirmClicked()
{
	FString CharacterName = TextBox->GetText().ToString();
	if (CharacterName.Len() <= 0 || CharacterName.Len() > 10) return;

	UABSaveGame* NewPlayerData = NewObject<UABSaveGame>();
	NewPlayerData->PlayerName = CharacterName;
	NewPlayerData->Level = 1;
	NewPlayerData->Exp = 0;
	NewPlayerData->HighScore = 0;
    //추가
	NewPlayerData->CharaterIndex = CurrentIndex;

.......
}
//ABPlayerState.h
{
public:
	int32 GetCharacterInex() const;

protected:
	UPROPERTY(Transient)
	int32 CharacterIndex;
}

//ABPlayerState.cpp
AABPlayerState::AABPlayerState()
{
...
	CharacterIndex = 0;
}

int32 AABPlayerState::GetCharacterInex() const
{
	return CharacterIndex;
}

void AABPlayerState::InitPlayerData()
{
	auto ABSaveGame = Cast<UABSaveGame>(UGameplayStatics::LoadGameFromSlot(SaveSlotName, 0));
	if (nullptr == ABSaveGame)
	{
		ABSaveGame = GetMutableDefault<UABSaveGame>();
	}

	SetPlayerName(ABSaveGame->PlayerName);
	SetCharacterLevel(ABSaveGame->Level);
	GameScore = 0;
	GameHighScore = ABSaveGame->HighScore;
	Exp = ABSaveGame->Exp;
	CharacterIndex = ABSaveGame->CharaterIndex; //추가
	SavePlayerData();
}

void AABPlayerState::SavePlayerData()
{
	UABSaveGame* NewPlayerData = NewObject<UABSaveGame>();
	NewPlayerData->PlayerName = GetPlayerName();
	NewPlayerData->Level = CharacterLevel;
	NewPlayerData->Exp = Exp;
	NewPlayerData->HighScore = GameHighScore;
	NewPlayerData->CharaterIndex = CharacterIndex; //추가

	if (!UGameplayStatics::SaveGameToSlot(NewPlayerData, SaveSlotName, 0))
	{
		ABLOG(Error, TEXT("SaveGame Error!"));
	}

}
//ABCharacter.cpp

void AABCharacter::BeginPlay()
{
.....

	auto DefaultSetting = GetDefault<UABCharacterSetting>();

	if (bIsPlayer)
	{
		//AssetIndex = 4;
        //추가함
		auto ABPlayerState = Cast<AABPlayerState>(GetPlayerState());
		ABCHECK(nullptr != ABPlayerState);
		AssetIndex = ABPlayerState->GetCharacterIndex();
	}
.....
}

 

※ GetCharacterInex() -> GetCharacterIndex() 수정

* 이제 시작 UI 기능을 모두 완성했으므로 프로젝트 세팅으로 가서  Title 레벨을 기본 레벨로 설정한다.

▶ GameMode

* 게임 실행 중에 게임 모드의 포인터를 가져올 때는 월드의 GetAuthGameMode라는 함수를 사용한다.
* 멀티 플레이 게임에서 게임 모드는 게임을 관리하는 방장 역황을 하며 게임에서 중요한 데이터를 인증하는 권한을 가진다.

auto ABGameMode = Cast<AABGameMode>(GetWorld()->GetAuthGameMode());

 

▶ GameState

auto ABGameState = Cast<AABGameState>(UGameplayStatics::GetGameState(this));

 

▶ PlayerState

정의 : APlayerState* GetPlayerState() const { return PlayerState; }
    
auto ABPlayerState = Cast<AABPlayerState>(GetPlayerState());

 

▶ AnimInstance

ABAnim = Cast<UABAnimInstance>(GetMesh()->GetAnimInstance());

 

▶ APlayerController (AController)

FORCEINLINE AController* APawn::GetController() const
{
	return Controller;
}
float AABCharacter::TakeDamage(float DamageAmount, FDamageEvent const & DamageEvent, AController * EventInstigator, AActor * DamageCauser)
{
	if (CurrentState == ECharacterState::DEAD)
	{
		if (EventInstigator->IsPlayerController())
		{
       
			auto ABPlayerController = Cast<AABPlayerController>(EventInstigator);  // AController
            
		}
	}
	return FinalDamage;
}

* UI는 GetOwningPlayer 함수를 사용해 현재 자신을 생성하고 관리하는 플레이어 컨트롤러의 정보를 가져올 수 있다.

auto ABPlayerController = Cast<AABPlayerController>(GetOwningPlayer());
ABPlayerController->ChangeInputMode(true);
ABPlayerController->SetPause(false);

 

 

▶  GameInstance

auto ABGameInstance = Cast<UABGameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));

 

▶ UProgressBar

HPProgressBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("PB_HPBar")));

 

▶ UTextBlock

PlayerName = Cast<UTextBlock>(GetWidgetFromName(TEXT("txtPlayerName")));

 

▶ UBehaviorTreeComponent

auto BehaviorTreeComponent = Cast<UBehaviorTreeComponent>(BrainComponent);

 

▶ PAWN

* 충돌판정에서도 가져 올수 있고, AI에서도 가져 올 수 있고, 등등 얻어 올수 있는 방법이 매우 많음
* 자세히 알아보기 (예정)

 

▶ 특정 타입을 상속 받은 액터의 목록

* 현재월드에 있는 특정 타입을 상속 받은 액터의 목록은 TActorIterator<액터타입> 구문을 사용해 가져올 수 있다

'Unreal > Concept' 카테고리의 다른 글

게임 프레임워크  (0) 2019.05.12
언리얼 게임실행 프로세스  (0) 2019.05.12
UClass  (0) 2019.05.09
TSubclassOf  (0) 2019.05.08
UFUNCTION 지정 매크로  (0) 2019.05.01

* 전투와 관련된 부가 요소를 추가해본다.

1. 플레이너는 게임 진행 중에 HP를 회복할 수 없고 오직 레벨업을 할 떄만 회복된다.
2. 캐릭터는 무기를 들 떄 더 긴 공격 범위를 가진다.
3. 무기에는 공격력 증가치가 랜덤으로 부여되며, 운이 없으면 오히려 무기에 의해 공격력이 저하될 수 있다.
4. 현재 게임 스코어가 높을수록 생성되는 NPC의 레벨도 증가한다.

* 1번은 이미 구현했으므로 이번에는 2번 기능을 구현해본다.

* 무기 액터인 ABWeapon에 AttackRange라는 속성을 추가한다.
* 캐릭터에 무기가 없으면 캐릭터의 AttackRange 속성을 사용하고, 캐릭터가 무기를 들면 무기의 AttackRange 속성을 사용하도록 로직을 수정한다.

* 캐릭터의 AttackRange 속성의 기본값을 80으로 낮춘다. 그리고 무기의 AttackRange 속성의 기본값을 150으로 지정하되, 해당 속성 키워드에 EditAnywgere, BlueprintReadWrite를 지정해 앞으로 ABweapon 클래스를 상속받은 무기 블루프린트에서도 공격범위 값을 다르게 설정할 수 있도록 기능을 부여한다.

* 또한 이번에는 캐릭터가 무기를 들고 있더라고 무기를 변경할 수 있도록 CanSetWeapon의 값을 무조건 true로 설정하고, 기존에 무기가 있는 경우 이를 없애고 새로운 무기를 습득하도록 로직을 변경한다.

//ABWeapon.h
class ARENABATTLE_API AABWeapon : public AActor
{
public:	
	float GetAttackRange() const;

protected:
	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category = Attack)
	float AttackRange;
}

//ABWeapon.cpp
AABWeapon::AABWeapon()
{
.....
	AttackRange = 150.0f;
}


float AABWeapon::GetAttackRange() const
{
	return AttackRange;
}

//ABCharacter.h
class ARENABATTLE_API AABCharacter : public ACharacter
{
public:
	float GetFinalAttackRange() const;
}

AABCharacter::AABCharacter()
{
	AttackRange = 80.0f;
}

float AABCharacter::GetFinalAttackRange() const
{
	return (nullptr != CurrentWeapon) ? CurrentWeapon->GetAttackRange() : AttackRange;
}

void AABCharacter::SetWeapon(AABWeapon * NewWeapon)
{
	ABCHECK(nullptr != NewWeapon);
	if (nullptr != CurrentWeapon)
	{
		CurrentWeapon->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
		CurrentWeapon->Destroy();
		CurrentWeapon = nullptr;
	}

	FName WeaponSocket(TEXT("hand_rSocket"));
	if (nullptr != NewWeapon)
	{
		NewWeapon->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, WeaponSocket);
		NewWeapon->SetOwner(this);
		CurrentWeapon = NewWeapon;
	}
}

void AABCharacter::AttackCheck()
{
	float FinalAttackRange = GetFinalAttackRange(); // AttackRange -> FinalAttackRange
	
	FHitResult HitResult;
	FCollisionQueryParams Params(NAME_None, false, this);
	bool bResult = GetWorld()->SweepSingleByChannel(
		HitResult,
		GetActorLocation(),
		GetActorLocation() + GetActorForwardVector() * FinalAttackRange,
		FQuat::Identity,
		ECollisionChannel::ECC_GameTraceChannel2,
		FCollisionShape::MakeSphere(AttackRadius),
		Params);

#if ENABLE_DRAW_DEBUG

	FVector TraceVec = GetActorForwardVector() * FinalAttackRange;
	FVector Center = GetActorLocation() + TraceVec * 0.5f;
	float HalfHeight = FinalAttackRange * 0.5f + AttackRadius;
	FQuat CapsuleRot = FRotationMatrix::MakeFromZ(TraceVec).ToQuat();
	FColor DrawColor = bResult ? FColor::Green : FColor::Red;
	float DebugLifeTime = 5.0f;
    .....
}

//BTDecorator_IsInAttackRange.cpp
bool UBTDecorator_IsInAttackRange::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
	bool bResult = Super::CalculateRawConditionValue(OwnerComp, NodeMemory);

	auto ControllingPawn = Cast<AABCharacter>(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) <= ControllingPawn->GetFinalAttackRange());//변경
	return bResult;
}

 

* 이제 캐릭터는 무기를 들 떄 무기가 없는 NPC보다 더 긴 공격 범위를 가진다.


* 이번에는 3번의 기획 요소를 구현해본다. 공격할 떄 무기가 있는 경우 기존 공격력을 증폭시키는 기능을 구현한다.
* 무기에 랜덤한 순수 공격력과 효과치 속성을 설정하고 최종 대미지를 산출할 떄 이 데이터를 활용하도록 로직을 구현해 본다.

//AABWeapon.h
class ARENABATTLE_API AABWeapon : public AActor
{
public:	
	float GetAttackRange() const;
	float GetAttackDamage() const;
	float GetAttackModifier() const;

protected:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Attack)
		float AttackDamageMin;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Attack)
		float AttackDamageMax;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Attack)
		float AttackModifierMin;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Attack)
		float AttackModifierMax;

	UPROPERTY(Transient, VisibleInstanceOnly, BlueprintReadWrite, Category = Attack)
		float AttackDamage;

	UPROPERTY(Transient, VisibleInstanceOnly, BlueprintReadWrite, Category = Attack)
		float AttackModifier;
}

//AABWeapon.cpp
AABWeapon::AABWeapon()
{
	AttackRange = 150.0f;

	AttackDamageMin = -2.5f;
	AttackDamageMax = 10.0f;
	AttackModifierMin = 0.85f;
	AttackModifierMax = 1.25f;
}

// Called when the game starts or when spawned
void AABWeapon::BeginPlay()
{
	Super::BeginPlay();
	AttackDamage = FMath::RandRange(AttackDamageMin, AttackDamageMax);
	AttackModifier = FMath::RandRange(AttackModifierMin, AttackModifierMax);
}

float AABWeapon::GetAttackDamage() const
{
	return AttackDamage;
}

float AABWeapon::GetAttackModifier() const
{
	return AttackModifier;
}

//AABCharacter.h
public:
float GetFinalAttackDamage() const;

//AABCharacter.cpp
float AABCharacter::GetFinalAttackDamage() const
{
	float AttackDamage = (nullptr != CurrentWeapon) ? (CharacterStat->GetAttack() + CurrentWeapon->GetAttackDamage()) : (CharacterStat->GetAttack());
	float AttackModifier = (nullptr != CurrentWeapon) ? (CurrentWeapon->GetAttackModifier()) : (1.0f);

	return AttackDamage * AttackModifier;
}

void AABCharacter::AttackCheck()
{
.....
	if (bResult)
	{
		if (HitResult.Actor.IsValid())
		{
			FDamageEvent DamageEvent;
			HitResult.Actor->TakeDamage(GetFinalAttackDamage(), DamageEvent, GetController(), this);
		}
	}
}

 

* 로직을 반영하면, 무기를 습득할 때 해당 무기에 의해 플레이어의 공격 대미지가 증가하거나 감소되는 것을 확인할 수 있다.


* 이번에는 마지막 기획요소인 NPC의 레벨을 조정하는 기능을 구현해본다.

* NPC 캐릭터의 LOADING 스테이트에서 현제 게임 스코어를 게임모드에게 질의하고, 이를 기반으로 캐릭터의 레벨값을 설정한다.

//AABGameMode.h
public:
	int32 GetScore() const;

//AABGameMode.cpp
int32 AABGameMode::GetScore() const
{
	return ABGameState->GetTotalGameScore();
}

//ABCharacter.cpp
#include "ABGameMode.h"

void AABCharacter::SetCharacterState(ECharacterState NewState)
{
	ABCHECK(CurrentState != NewState);
	CurrentState = NewState;

	switch (NewState)
	{
	case ECharacterState::LOADING :
	{
		if (bIsPlayer)
		{
			DisableInput(ABPlayerController);

			ABPlayerController->GetHUDWidget()->BindCharacterStat(CharacterStat);

			auto ABPlayerState = Cast<AABPlayerState>(GetPlayerState());
			ABCHECK(nullptr != ABPlayerState);
			CharacterStat->SetNewLevel(ABPlayerState->GetCharacterLevel());
		}
		else //추가
		{
			auto ABGameMode = Cast<AABGameMode>(GetWorld()->GetAuthGameMode());
			ABCHECK(nullptr != ABGameMode);
			int32 TargetLevel = FMath::CeilToInt(((float)ABGameMode->GetScore() * 0.8f));
			int32 FinalLevel  = FMath::Clamp<int32>(TargetLevel, 1, 20);
			ABLOG(Warning, TEXT("New NPC Level : %d"), FinalLevel);
			CharacterStat->SetNewLevel(FinalLevel);
		}

		SetActorHiddenInGame(true);
		HPBarWidget->SetHiddenInGame(true);
		bCanBeDamaged = false;
		break;
	}
    .....

※ 게임 실행 중에 게임 모드의 포인터를 가져올 때는 월드의 GetAuthGameMode라는 함수를 사용한다.
※ 멀티 플레이 게임에서 게임 모드는 게임을 관리하는 방장 역황을 하며 게임에서 중요한 데이터를 인증하는 권한을 가진다.

NPC가 생성된결과 화면

 

+ Recent posts