6 minute read

몬스터 생명력 숫자로 표시

이미지

EnemyHPBar 위젯에 텍스트를 추가함.

이미지

텍스트에 바인딩하여 함수를 2개(현재 체력 표시, 최대 체력 표시) 만듬.

타겟팅되었을 때만 나와야하므로 플레이어의 전투 타겟일 때만 표시됨.



동굴맵 추가

이미지

캐릭터 선택 화면에서 캐릭터를 선택하면 페이드아웃 후 동굴맵이 나오도록 함.

에디터 처음 시작 시에 맵 오픈이 너무 오래 걸리는 문제가 있는데 나중에 해결해야 함.


이미지

npc들과 플레이어의 지팡이, 던전 포탈을 배치해놨다.

던전 포탈 쪽은 너무 어둡길래 조명 추가함.

이미지

반대편에는 돌문(?)과 아이템 ‘디비눔 프레시디움’을 배치해놨다.

이 돌문은 던전을 클리어하고 열릴 예정.

이미지

플레이어가 주으면 이 돌만 사라지게 해야지.

이미지

동굴맵 레벨 블루프린트.

시네마틱 모드로 설정해서 플레이어와 카메라(마우스 회전)를 못 움직이게 함. 페이드인아웃 위젯을 뷰포트에 추가하여 화면을 검게 해두고, 플레이어를 스폰 위치(npc들 앞)에 스폰함.

이미지

그리고 대화를 시작함. 플레이어는 전투몽타주 안에 있는 누워있는 애니메이션을 플레이하고, 대화에서 플레이어의 응답 부분이 나올 때까지 1초 간격으로 대화를 자동 진행함.

이미지

이 때 플레이어는 계속 누워있는 상태여야 하므로 몽타주 Laying 섹션의 다음 섹션을 본인으로 링크함->따라서 Laying 무한 반복됨. 참고로 이 때의 플레이어는 지팡이를 들고 있지 않은 상태이므로 기존에 누워있는 애니메이션에서 손만 다시 편 상태로 만들어서 새롭게 저장함.


Main.cpp
void AMain::Jump()
{
	if (MovementStatus != EMovementStatus::EMS_Dead && !MainPlayerController->bDialogueUIVisible) // 죽거나 대화 중일 때는 점프 불가
	{
		Super::Jump();
	}
}

점프는 대화 도중에 불가.

void AMain::CameraZoom(const float Value)
{
	if (Value == 0.f || !Controller) return;

	if (MainPlayerController->DialogueNum == 0) return;

	const float NewTargetArmLength = CameraBoom->TargetArmLength + Value * ZoomStep;
	CameraBoom->TargetArmLength = FMath::Clamp(NewTargetArmLength, MinZoomLength, MaxZoomLength);
}

카메라 줌은 첫 대화 시에만 못하게 함. 이후 대화에서는 가능.


이미지

그리고 모모를 제외한 npc 모두에게 standing_idle 애니메이션을 추가해줬다.

이미지

블렌드 스페이스에서 스피드가 0일 때 이렇게 서있도록 했다. 기존 idle은 여기서 제거했다.

모모는 기존 idle로 있는 게 더 맘에 들어서 놔둠.



npc 이동 시키는 방법 변경

Dialogue.cpp

in DialogueEvents()

case 10: // npc go
    Main->FollowCamera->SetRelativeRotation(FRotator(0.f, 0.f, 0.f));
    Main->CameraBoom->TargetArmLength = 500.f;

    // npc move except luko              
    Main->Momo->AIController->MoveToLocation(FVector(5200.f, 35.f, 100.f));
    Main->Vovo->AIController->MoveToLocation(FVector(5200.f, 35.f, 100.f));
    Main->Vivi->AIController->MoveToLocation(FVector(5200.f, 35.f, 100.f));
    Main->Zizi->AIController->MoveToLocation(FVector(5200.f, 35.f, 100.f));
    break;    
case 11: 
    CurrentState = 0;
    MainPlayerController->RemoveDialogueUI();
    FTimerHandle Timer;
    GetWorld()->GetTimerManager().SetTimer(Timer, FTimerDelegate::CreateLambda([&]()
    {
        MainPlayerController->DisplayDialogueUI();
    }), 1.7f, false); // 1.7초 뒤 루코 대화
    return;
	break;

기존에는 Main의 NPCList를 for문으로 돌려서 이름 조건으로 구분해서 이동시켰는데, 아예 그냥 Main에 npc별로 변수를 만들어서 이동시켰다.


Main.h
UPROPERTY(VisibleAnywhere)
class AYaroCharacter* Momo;

UPROPERTY(VisibleAnywhere)
class AYaroCharacter* Luko;

UPROPERTY(VisibleAnywhere)
class AYaroCharacter* Vovo;

UPROPERTY(VisibleAnywhere)
class AYaroCharacter* Vivi;

UPROPERTY(VisibleAnywhere)
class AYaroCharacter* Zizi;

덕분에 쓸모도 없던 test 변수를 지울 수 있었음.


YaroCharacter.cpp

in Tick()

if (!Player)
{
    ACharacter* p = UGameplayStatics::GetPlayerCharacter(this, 0);
    Player = Cast<AMain>(p);

    if (Player)
    {
        if (this->GetName().Contains("Momo"))
        {
            Player->Momo = this;
        }
        if (this->GetName().Contains("Luko"))
        {
            Player->Luko = this;
        }
        if (this->GetName().Contains("Vovo"))
        {
            Player->Vovo = this;
        }
        if (this->GetName().Contains("Vivi"))
        {
            Player->Vivi = this;
        }
        if (this->GetName().Contains("Zizi"))
        {
            Player->Zizi = this;
        }
        Player->NPCList.Add(this);
    }
}

그리고 npc 코드의 틱 함수에서 각자 할당함. 물론 NPCList에도 할당. NPCList는 모두 동일한 행동을 할 때 유용할 거라 생각해서 놔둠.



던전으로 이동

이미지

포탈 블루프린트에 오버랩박스라는 이름의 박스 콜리전을 생성.

이미지

이 박스에 오버랩된 것이 플레이어 캐릭터가 아니면 오브젝트 이름에 Yaro가 포함되어 있는지 확인 후 포함되어 있으면 액터 파괴. 즉 npc들은 오버랩되면 파괴됨. 원래 그냥 히든이었는데 그러면 보이지 않는 투명인간이 되고 플레이어가 부딪힐 수 있기 때문에 방해됨. 따라서 그냥 파괴하기로 함.

플레이어 같은 경우 지팡이를 장비했는지를 검사함.

이미지

장비하지 않았으면 아무 일도 일어나지 않음.

장비했으면 히든시키고 시네마틱 모드로 전환해서 못 움직이게 함. 그리고 페이드아웃하며 던전맵 오픈.


이미지

첫번째 던전 레벨 블루프린트.

시네마틱 모드로 설정하고 페이드아웃 위젯 뷰포트에 추가시킴. (페이드인 효과를 줄 것이기 때문)

이미지

플레이어 스폰 후 상태능력치창을 히든 상태로 만든 뒤 뷰포트에 추가시킴.

그리고 완드를 플레이어와 동일한 위치에 스폰시키고 Equip함수 실행해서 플레이어에게 장비시킴.

새 게임이든 이어하기이든 지팡이를 들고 있을 것이므로 여기까지 동일.

이미지

이어하기인 경우 LoadGame() 실행 후 페이드인 0.5초 뒤에 시네마틱 모드 해제 후 상태능력치창 보이게 함.

새 게임인 경우 페이드인을 하며 1.5초 뒤에 DialogueNum을 2로 만들고 대화 시작. 3초 뒤 상태능력치창 보이게 함.


Enemy.cpp
void AEnemy::DeathEnd()
{
	GetMesh()->bPauseAnims = true;
	GetMesh()->bNoSkeletonUpdate = true;
	GetWorldTimerManager().SetTimer(DeathTimer, this, &AEnemy::Disappear, DeathDelay);

	if (this->GetName().Contains("Golem")) // 골렘 쓰러뜨린 뒤 대화
	{
		Main->MainPlayerController->DisplayDialogueUI();
	}
}

골렘 처치 후 대화 시작하게 함.


MainPlayerController.cpp

in DisplayDialogueUI()

case 3:   
    if (!bFadeOn)
    {
        FadeAndDialogue();
        return;
    }              
    DialogueUI->InitializeDialogue(DungeonDialogue2);
    bFadeOn = false;
    break;

골렘 처치 후 대화 시엔 페이드아웃하고 캐릭터들 위치 세팅 후 대화 시작함.

자세한 건 이전 포스트에 있음.


배 타고 이동하기

이게 배가 원래 스태틱 메시인데 오버랩 이벤트가 필요해서 블루프린트 만듬.

이미지

이 플러그인을 설치해서 스태틱 메시를 편집할 수 있다는 걸 알아냈다.

(배에서 꼭 삭제하고 싶은 부분이 있었어서..)

이미지

아무튼 플러그인을 설치하니 툴바 옆에 메시 편집 탭이 나타났고 편집모드를 클릭해서 맘에 안 드는 부분을 지우고 깔끔한 바닥으로 만듬^^

그리고 플레이어랑 npc들이 떨어지거나 딴데로 새지 않게 배 뒷편 제외 다 콜리전으로 막아둠. 배는 무조건 뒤에서만 탈 수 있음.

이미지

이게 이제 블루프린트인데, 박스 콜리전을 3개 추가함. 사진에서 선택되어 있는 박스는 플레이어 인식용.

오버랩박스1은 npc인식용. 그리고 배 앞머리에 기다란 거는 문 인식용.

이 세 박스 모두 오버랩 이벤트가 있음.

참고로 캐릭터 인식용 박스는 worldstatic 타입은 무시해야함. 안 그러면 캐릭터들이 가지고 있는 범위도 인식하기 때문.

이미지

먼저 플레이어 인식 박스 오버랩 이벤트.

Main 형변환에 성공한 뒤 NPCGetOn이 참이면 오버랩 이벤트 해제하고 시네마틱 모드 설정.

거짓이면 PlayerGetOn을 참으로 만듬.

NPCGetOn이 거짓이라는 것은 npc들이 아직 전부 타지 않았다는 의미. npc와 플레이어 전부 배에 올라타야 배가 이동한다.

이미지

아무튼 NPCGetOn이 참이면 대화가 시작되며 배의 루트 컴포넌트인 스태틱 메시 컴포넌트를 조금씩 앞으로 이동시킴. 0.01초마다 반복. 그래야 자연스럽게 이동함.

이미지

플레이어가 오버랩박스를 나가면 PlayerGetOn을 거짓으로 만듬.

이미지

npc 인식 박스 오버랩 이벤트.

보보가 가장 마지막에 탑승하므로 보보가 오버랩되었다는 것은 npc 모두가 배에 있다는 의미.

따라서 오버랩된 오브젝트의 이름에 vovo가 포함되었는지 확인 후 포함되었으면 0.5초 뒤

이미지

Main의 NPCList를 가져와서 for each루프를 돌려서 npc들이 더 이상 움직이지 않도록 한다.

반복문을 다 돌면 NPCGetOn을 참으로 만들고 PlayerGetOn도 참인지 확인.

둘 다 참일 경우(=6명이 전부 배에 탑승) (플레이어 인식용)오버랩박스의 Set GenerateOverlapEvents 노드로 이동.

이미지

문 인식용 박스 오버랩 이벤트.

오버랩된 액터 이름에 Door가 포함되어 있으면 페이드아웃 시작.

2초 뒤 두번째 던전 오픈.



시스템 메세지 표시

이미지

시스템 메시지 위젯.

이미지

텍스트는 때에 따라 바뀌어야 하므로 코드에서 변경해주도록 한다.

메인플레이어컨트롤러의 SystemText를 텍스트로 설정한다.


MainPlayerController.h

UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
FText SystemText;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Widgets")
TSubclassOf<UUserWidget> WSystemMessage;

UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Widgets")
UUserWidget* SystemMessage;

void DisplaySystemMessage();
void RemoveSystemMessage();

bool SystemMessageOn = false;

bool bSystemMessageVisible;

헤더 파일에 필요한 변수와 함수를 선언한다.

MainPlayerController.cpp

in BeginPlay()

 if (WSystemMessage)
    {
        SystemMessage = CreateWidget<UUserWidget>(this, WSystemMessage);
        if (SystemMessage)
        {
            SystemMessage->AddToViewport();
            SystemMessage->SetVisibility(ESlateVisibility::Hidden);
        }
    }

메뉴와 대화창보다 먼저 뷰포트에 추가 해놓기. 안그러면 메뉴 버튼이랑 대화창의 플레이어 응답을 클릭할 수 없게 될 지도…


void AMainPlayerController::DisplaySystemMessage()
{
    if (SystemMessage)
    {
        FString text; //문자열 변수 선언
        if (!bDialogueUIVisible)
        {
            switch (DialogueNum)
            {
            case 2:
                text = FString(TEXT("지팡이 가까이에서 마우스 왼쪽 버튼을 클릭하여\n지팡이를 장비하세요.")); 
                SystemMessageOn = true; 
                break;
            case 3:
                break;
            }
        }
       
        if (bPauseMenuVisible && DialogueNum < 3) // 동굴 안에선 저장 안 됨
        {
            text = FString(TEXT("이 곳에선 저장되지 않습니다."));

        }
        if (bPauseMenuVisible && bDialogueUIVisible) // 메뉴&대화창 올라와있는 상태
        {
            text = FString(TEXT("대화 중엔 저장되지 않습니다."));
        }

        SystemText = FText::FromString(text); // 시스템 텍스트에 할당

        bSystemMessageVisible = true;

        SystemMessage->SetVisibility(ESlateVisibility::Visible); // 위젯 보이게

    }
}

void AMainPlayerController::RemoveSystemMessage()
{
    if (SystemMessage)
    {
        SystemMessage->SetVisibility(ESlateVisibility::Hidden);
        SystemMessageOn = false;
        bSystemMessageVisible = false;

    }
}

분명 코드에 틀린 것이 없는데 계속 오류가 났다. 왜 그런가 했더니 TEXT(“”) 여기에 한글을 써서 그런 거였음.

블루프린트에서는 한글이 아주 잘 적용되는데 코드에서는 오류가 난다. 한글 로그도 마찬가지.

해결방법은 해당 파일에서 다른 이름으로 저장하기를 누른 뒤

그 저장 오른쪽에 아래방향 화살표 눌러서 ‘인코딩하여 저장’을 선택하고

이미지

65001 이걸로 선택해서 저장하면 됨.

그러면 컴파일이 문제 없이 잘 된다.


메뉴 관련 함수도 시스템 메세지 관련해서 추가함.

void AMainPlayerController::DisplayPauseMenu()
{
    if (PauseMenu)
    {
        bPauseMenuVisible = true;

        // 다이얼로그 넘버가 3보다 작고 대화 중일 때 시스템 메시지 같이 표시
        if (DialogueNum < 3 || bDialogueUIVisible) DisplaySystemMessage();

        PauseMenu->SetVisibility(ESlateVisibility::Visible);

        FInputModeGameAndUI InputMode;
        SetInputMode(InputMode);
        bShowMouseCursor = true;
    }
}

void AMainPlayerController::RemovePauseMenu()
{
    if (PauseMenu)
    {      
        bPauseMenuVisible = false;
        // 메뉴 뜨기 전에 이미 시스템 메시지가 있는 상태였으면
        if (SystemMessageOn) DisplaySystemMessage(); // 이전 시스템 메시지 표시
        else if (bSystemMessageVisible) RemoveSystemMessage(); // 시스템 메세지가 없던 상태에서 현재 시스템 메세지가 표시된 상태면

        PauseMenu->SetVisibility(ESlateVisibility::Hidden);

        if (!bDialogueUIVisible) // 대화 중이 아닐 때만 입력 모드 변경
        {
            FInputModeGameOnly InputModeGameOnly;
            SetInputMode(InputModeGameOnly);
            bShowMouseCursor = false;
        }
    }
}



메뉴 위젯 블루프린트

이미지

저장 전에 현재 맵 이름에 던전이 포함되었는지 확인 후 포함되었으면 대화 중인지 아닌지도 검사하여 던전맵&대화 중이 아닐 때만 SaveGame() 실행. 맵 이름에 던전이 없거나 대화 중이면 저장 안 하고 타이틀로 이동 혹은 종료.




두번째 던전 로드까지 쭉


시스템 메시지 까먹어서 하나 더.. + 지팡이 메세지 안 나오는 거 컴파일 후 고침



다음 할 것 : 저장 타이머, npc 전투 개선, 게임 패키징, 레벨/경험치 시스템, 메인화면 및 캐릭터 선택 화면 바꾸기, 몬스터 저장 기능

Updated: