8. 게임플레이 프레임워크

JUSTICE_DER·2023년 7월 23일
0

🌵UNREAL

목록 보기
39/42
post-thumbnail

언리얼엔진에서는 PlayerController에서
위젯의 인스턴스를 가져AddToViewport로 추가를 했다.
Character에 해도 다른 엑터에 해도 똑같은 기능을 할텐데..
왜 위젯을 모두 PlayerController에 생성하는 걸까?

나름의 암묵적인 규칙이 존재하는 걸까??

PlayerController의 코드,
위젯을 생성하고 AddToViewport를 한다.

그리고 UI에 접근하기 위해서 Character에서 PlayerController를 참고해야만 했다.

예외도 존재하긴 한다. 바로 Character의 Component로 부착되는 것이다.
이 경우, 부착되어있기 때문에 Character와 같이 움직인다.


1. 게임플레이 프레임워크

아래의 글들을 참고하였다.

게임을 구성하는 게임의 프레임워크에 있어서
크게 4가지로 나누어볼 수 있다.

1-1. Pawn

  • 게임캐릭터
  • Controller에 의해 조종당할 수 있는 Actor
  • 캐릭터의 입력을 구현할 수 있다

1-2. Controller

  • 게임캐릭터에 빙의되어 조종하는 역할
  • Pawn에 대한 지시를 수행한다
  • 빙의된 캐릭터의 입력을 구현할 수 있다
  • HUD / Input / PlayerCameraManager를 포함한다.

    PlayerController에 관련 함수와 멤버변수가 이미 구현되어있다.

1-3. HUD / PlayerCameraManager

  • 게임화면의 출력을 담당
  • HUD는 PlayerController에 하나정도 적용된다
    • 체력, 경험치 등을 다루는 UI
    • 쉽게말해 게임중에 같이 볼 수 있는 UI를 의미
  • Camera도 PlayerController에 하나정도 적용된다

1-4. (GameMode / GameState) / PlayerState

  • 게임의 규칙 설정 및 기록

    A. GameMode

    • 기존 UE 4.0의 GameMode를 단순화 시킨것이 GameModeBase이고,
    • GameMode가GameModeBase를 상속받도록 만들어졌다
    • GameMode가 멀티플레이어에 특화된 세부기능을 구현해놓아서
      멀티/슈팅 게임에는 GameMode를 사용하는게 적합

      (그러니까 멀티기능이 필요없다면 GameMode를 사용하도록 세분화)
    • 서버에만 존재

    B. GameState

    • 접속된 모든 클라이언트(플레이어)가 알아야 하는 정보를 다룸.
      플레이어 개인이 아닌, 모두에게 공유되는 정보를 관리해야한다.
      (그렇게 하지 않아도 되지만, 그렇게 하라고 만들어 놓은 구조)
    • ex) 각 팀의 점수, 접속된 플레이어의 목록
    • 서버와 모든 클라이언트에 존재

    C. PlayerState

    • 플레이어 혹은 NPC의 정보를 다룸.
    • 개인적인 정보가 들어감
      ex) 플레이어 이름, 개인 점수, 레벨, 인벤토리

2. GameInstance

게임플레이 프레임워크 설명에 없지만
자주 쓰였던 클래스들을 가져와보았다.

정확히 어떤 기능을 구현하기 위해 제작된 클래스인지 본다.

아래의 글들을 참고하였다.

  • 게임이 시작되고, 게임이 끝날 때까지 생존.
    (게임을 시작하면 GameInstance가 가장 먼저 실행되고,
    게임을 종료하면 GameInstance가 가장 마지막에 소멸된다)
  • 따라서 레벨이나 모드의 전환에서도 지속되어야할 데이터를 저장하는데 쓰임.
  • ex) 게임 설정, 플레이어 통계 등 게임모드에서 엑세스해야하는 데이터
  • GameInstance에 CSV파일 테이블을 저장하기도 함
  • 따라서, 특정 레벨이나 특정 모드에서만 쓰일 데이터를 저장하는건 지양
    (해당 목적을 위해선 GameState나 레벨블루프린트가 존재)
  • 단 하나만 생성 가능한 특수 클래스.
    (언리얼의 singleton 클래스와는 별개)
  • 에디터의 맵&모드에서 수정가능
  • 참고로 GameInstance는 UObject화 시켜서 사용하지만,
    CDO를 위한 생성자는 굳이 생성하지 않는다.
    왜냐하면 단 1개만 존재하기 때문에 복사하고 생성하고 이럴 필요가 없기 때문이다.
    대신, Init()함수로 GameInstance의 초기화를 진행한다.

3. 프레임워크 실제 사용

3-1. 예시

  • 각 프레임워크에 어떤 데이터가 들어가야하는지
    그 종류를 살펴보았고, 실제로 어떤 데이터가 들어가있는지 확인해본다.

A. Character

  • 생성자 - CDO - 런타임초기 실행
    • Camera / UIWidget 을 부착하였다.
    • 사실 2개 다 PlayerController에 구현해도 되는 기능이지만,
      Character에 부착하여 같이 따라다니도록 구현한 것으로 보인다.
  • BeginPlay - 사실상 생성자, 실제 플레이 가능한 상태인 경우
    • 이득우 예제의 Character라는 클래스는
      플레이어 캐릭터도 되지만, NPC도 되기 때문에,
      IsPlayerControlled()라는 미리 정의된 메서드로 확인한다.
    • 해당 코드의 값에 따라 각각 Cast하여 Controller를 가져온다.
  • Tick
    • 캐릭터의 이동 구현
    • PlayerController에서 구현하는게 맞지만,
      Character에서 구현해도 되긴 한다..
  • SetupPlayerInputComponent
    • Character의 PlayerInputComponent에 Bind Axis/Action 델리게이트
  • PostInitializeComponents - 생성자 이후 실행
    • AnimInstance 설정
  • 콤보 플래그 생성 및 구현
  • 공격 당함 / 공격함을 구현
  • DrawDebug
  • 무기 설정
  • PossessebBy
    • 빙의되는 시점에 호출되는 함수

( 생성자 - PostInitializeComponents - BeginPlay - PossessedBy 순으로 실행)

B. Controller

  • PlayerController
  • 생성자
    • Widget 인스턴스 생성
  • BeginPlay
    • PlayerState라는 예약어로 바로 PlayerState에 접근가능
  • 생성자에서 생성한 위젯을 기능에 따라 AddToViewport
  • GetGameState로 바로 GameState 정보를 가져옴.
  • AIController
  • AIController는 GameMode, GameState에 바로 접근할 수는 없다.
  • BTTree/BlackBoard와 연결

C. PlayerController에 적용된 HUD의 기능

1) 가장먼저 생성자에서 에셋으로부터
Class의 정보를 담는다.

2) Class를 바탕으로 위젯 인스턴스를 생성한다.

3) 그리고 해당 인스턴스를 뷰포트에 띄운다.

  • 해당 HUD위젯은 PlayerState와 CharacterStatComponent 정보를 받아온다.
  • 각각에 이미 선언된 델리게이트에 위젯의 함수들을 연결한다.
  • 그러면 PlayerState와 CharacterStatComponent에서
    원할 때 위젯의 기능을 실행할 수 있다.
  • 여기서 위젯의 기능이란 Bar와 같은 위젯의 구성원을 업데이트하는 내용.

D. GameMode - 규칙을 정의 및 수주맡김

  • PostLogin
    • PlayerState 예약어로 직접 접근하여 객체를 생성
  • PostInitializeComponents
    • GameState 예약어로 직접 접근하여 객체를 생성
  • GameState를 시켜 점수를 기록 및 관리
  • PlayerController를 시켜 간접적으로 PlayerState가 점수를 기록
  • 클리어 목표점수를 지정
  • 목표점수에 도달하면 PlayerState를 시켜 UI를 띄움

E. GameState - 점수관리

  • 점수를 관리
    • AddScore
    • SetTotalScore
  • 게임 클리어를 관리
    • 게임클리어시 bGameCleard라는 변수를 true로 만듦
    • IsGameCleared라는 getter함수를 통해 값을 반환

F. PlayerState - 캐릭터 데이터 및 점수

  • Player관련 멤버변수를 구현해 둠.
  • Save 값을 받아서 멤버변수를 초기화. (Load기능)
  • 받은 값을 바탕으로 Ratio값을 미리 계산하고 위젯에 넘겨줌.
  • 대부분의 UObject에선 GetGameInstance()를 사용하여 접근할 수 있다.
    • GameInstance의 GetNewCharacterData를 통해
      해당 레벨에 맞는 캐릭터 데이터값만 가져옴
  • 점수 계산 및 저장
    캐릭터 최고 점수 저장
    (값이 변하면 지속적으로 저장)
  • 레벨업 관리
  • 현재 데이터를 Save 값으로 저장. (Save기능)
  • PlayerState에서도 UI관리 / 값이 변할때마다 호출

G. GameInstance - 게임에 필요한 데이터 저장

  • GameDataTable을 받아옴.
  • 레벨을 받으면 해당 DataTable의 레벨에 맞는 정보만 반환

F. CharacterStatComponent

  • ActorComponent를 상속하여 Character의 Stat을 다루기 위한 컴포넌트.
  • 하지만 그냥 PlayerState를 사용하면 되는게 아닐까 의문이 들었다.
  • PlayerController마다 PlayerState를 가지고 있을 것이고,
    PlayerState값을 그대로 가져와서 사용하는데,
    어짜피 GameInstance에 데이터테이블이 존재하고, 접근하면 되기 때문이다.

		if (bIsPlayer)
		{
			DisableInput(NewPlayerController);

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

			//언리얼 5는 Pawn이 아니라 Pawn의 Controller에서 PlayerState를 가져와야만 한다.
			auto NewPlayerState = Cast<ANewPlayerState>(NewPlayerController->PlayerState);
			//auto NewPlayerState = Cast<ANewPlayerState>(PlayerState);

			ABCHECK(nullptr != NewPlayerState);
			CharacterStat->SetNewLevel(NewPlayerState->GetCharacterLevel());

			// 추가한 코드
			auto GameState = GetWorld()->GetGameState();
			auto NewGameStateBase = Cast<ANewGameStateBase>(GameState);
			
			NewGameStateBase->SetTotalScore(NewPlayerState->GetGameScore());
		}
		else
		{
			auto NewGameMode = Cast<ANewGameMode>(GetWorld()->GetAuthGameMode());
			ABCHECK(nullptr != NewGameMode);

			int32 TargetLevel = FMath::CeilToInt(((float)NewGameMode->GetScore() * 0.08f));
			YU_LOG_FORMAT(Error, TEXT("*** TargetLevel  : %f"), ((float)NewGameMode->GetScore() * 0.08f));


			int32 FinalLevel = FMath::Clamp<int32>(TargetLevel, 1, 20);
			YU_LOG_FORMAT(Error, TEXT("*** New NPC Level : %d"), FinalLevel);

			CharacterStat->SetNewLevel(FinalLevel);
		}

이유를 알아냈다..
Character를 NPC와 플레이어가 공동으로 사용하기 떄문에,
AIPlayerController는 PlayerState에 접근할 수 없고,
따라서 Player는 PlayerState를 참고하는 방식으로
차별화하여 구현한 것으로 보인다.

CharacterStatComponent는 위젯을 관리한다.
SetNewLevel에 레벨을 설정하면
GameInstance로부터 받아와 해당 값으로 데이터가 지정이되고,
때릴때의 공격력 스탯과 맞을때의 HP변환을 진행하고,
HP값이 변경되면, SetHP를 호출하여 위젯을 수정한다.
(데이터중에 CurrentHP나 MaxHP같은 HP값을 위주로 사용한다)

PlayerState는 현재 플레이어의 정보를 Save하고 Load할 수 있다.
(hp를 제외한 데이터 모든 멤버변수를 사용한다)
(사실 사용해야하는게 맞다 (로드하려면 현재 체력도 알아야하므로))
따라서 PlayerState의 값들은 모두 Save값을 바탕으로 만들어진 것이고,
CharacterStateComponent는 위의 코드를 통해,
PlayerState의 Level으로 SetNewLevel을 진행한다.
(레벨업도 관리한다.)
이렇게 하여 캐릭터 생성시,
PlayerState의 값과 CharacterStateComponent의 값을 동기화한다.

그렇다면 게임 중간에 레벨업을 한 경우에 HP값을 어떻게 동기화할까?

AddExp가 호출되고 정상적으로 위젯을 갱신하는 함수가 broadcast될 것이다.
레벨업을 한다면, SetCharacterLevel을 사용하여 레벨을 설정한다.

SetCharacterLevel은 위처럼 GameInstance에서
해당 레벨의 테이블 Data를 받아오고,
해당 값으로 CharacterLevel을 지정한다.

그러면 PlayerState의 레벨정보는 수정이 될텐데..
CharacterStateComponent는..?
흠.. 아무리봐도 갱신할 수 있는 코드가 보이지 않는다. update되지 않을 것이다.

한 번 테스트 해본다.

역시 6렙으로 레벨업 했음에도 HP는 동기화되지 않아 차지않는 모습.
하지만 공격력은 증가했을 것이다.

맞으면 죽는다.
UI는 제대로 초기화 되는 중이었다.

결론
예제가 문제가 있었지만 찾아내었고,
PlayerState는 Save파일을 받아오고,
CharacterStatComponent를 초기화하는 형태로 쓰이고
PlayerState에서 레벨업시에, CharacterStatComponent의 델리게이트를 호출한다면
해결될 것으로 보인다.
길게 적을 글이 아니었는데.. 문제가 보였고, 단지 더 잘 알고 싶었다

3-2. 프레임워크 상호작용 함수

  • Character에서 PlayerController = GetOwner()
  • PlayerController에서 Character = GetPawn()
  • PlayerController에서 GameState = GetGameState()
  • GameMode에서 GameInstance = GetGameInstance()
  • 대부분의 UObject에서 GetWorld()를 사용하여 접근가능
    • 관련없는 다른 엑터와 접근해야할 때 world를 매개체로 사용
      따라서 어떤 UObject든 아래의 기능을 수행할 수 있게 된다.
      • World에서 GameMode = GetAuthGameMode()
      • World에서 특정 캐릭터 찾기 =
        TActorIterator<ACharacter>
        UWorld* World = GetWorld();
        if (World)
        {
            for (TActorIterator<ACharacter> It(World); It; ++It)
            {
                ACharacter* Character = *It;
                if (Character->IsA(CharacterClass))
                {
                    OutCharacters.Add(Character);
                }
            }
        }
        //TSubclassOf<ACharacter> CharacterClass 매개변수에 
        //가져오고자 하는 캐릭터 클래스를 전달
      • World에서 특정 PlayerController 찾기 =
        TActorIterator<APlayerController>
        위의 character를 찾고 getowner도 가능
         for (TActorIterator<APlayerController> It(World); It; ++It)
      • World에서 특정 Actor 찾기 =
        World->FindObjectByClass(ActorClassToFind())
        UWorld* World = GetWorld();
        if (World)
        {
            // AYourActor 클래스로부터 생성된 액터를 검색합니다.
            TSubclassOf<AYourActor> ActorClassToFind;
            AYourActor* SpecificActor 
            = Cast<AYourActor>(World->FindObjectByClass(ActorClassToFind));
            //
            return SpecificActor;
        }
      • Character에서 특정 Actor찾기
        • GetOverlappingActors
        • GetAllActorsNearby
// 1. GetOverlappingActors 
//TArray<AActor*> OverlappingActors;
//GetOverlappingActors(OverlappingActors, AYourActor::StaticClass());
//
// 2. GetAllActorsNearby 
TArray<AActor*> NearbyActors;
UGameplayStatics::GetAllActorsNearLocation(GetWorld(), CenterLocation, Radius, TArray<TSubclassOf<AYourActor>>(), NearbyActors);
for (AActor* Actor : NearbyActors)
    {
        AYourActor* SpecificActor = Cast<AYourActor>(Actor);
        if (SpecificActor)
        {
            // 원하는 조건을 만족하는 특정 액터를 찾았습니다.
            // 이제 SpecificActor를 사용하여 원하는 작업을 수행할 수 있습니다.
        }
    }
  • 대부분의 UObject에서 GetGameInstance()를 사용하여 접근가능
profile
Time Waits for No One

1개의 댓글

comment-user-thumbnail
2023년 7월 23일

좋은 정보 얻어갑니다, 감사합니다.

답글 달기