10 minute read

대화 시스템

DialogueUI.h

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Engine/DataTable.h" // generated.h 위에 써야함

#include "DialogueUI.generated.h"

USTRUCT(BlueprintType)
struct FPlayerReplies // 플레이어 응답 구조체
{
	GENERATED_USTRUCT_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	FText ReplyText; 

	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	int32 AnswerIndex; 

};

USTRUCT(BlueprintType)
struct FNPCDialogue : public FTableRowBase // 대화 데이터테이블 만들 때 행 구조 이거 선택해야함
{
	GENERATED_USTRUCT_BODY()

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FName CharacterName;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	TArray<FText> Messages; // npc 대사

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	TArray<FPlayerReplies> PlayerReplies;
};


UCLASS()
class YARO_API UDialogueUI : public UUserWidget
{
	GENERATED_BODY()


protected:

	UPROPERTY(meta = (BindWidget))
	class UTextBlock* NPCText;

	UPROPERTY(meta = (BindWidget))
	class UTextBlock* CharacterNameText;
	
	UPROPERTY(EditAnywhere, Category = "Dialogue")
	float DelayBetweenLetters = 0.06f; // 다음 글자가 표시되는 텀

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	class USoundBase* SoundCueMessage; // npc 대사 나올 때 소리

public:

	UFUNCTION(BlueprintImplementableEvent, Category = "Animation Events")
	void OnAnimationShowMessageUI(); // 대화창 나타남 - 블루프린트에서 설정

	UFUNCTION(BlueprintImplementableEvent, Category = "Animation Events")
	void OnAnimationHideMessageUI(); // 대화창 사라짐 - 블루프린트에서 설정

	UFUNCTION(BlueprintImplementableEvent, Category = "Animation Events")
	void OnResetOptions(); // 플레이어 응답 리셋 후 안 보이게 - 블루프린트에서 설정

	UFUNCTION(BlueprintImplementableEvent, Category = "Animation Events")
	void OnSetOption(int32 Option, const FText& OptionText); // 플레이어 응답 보이게 - 블루프린트 설정


	void SetMessage(const FString& Text); // 표시될 텍스트 설정

	void SetCharacterName(const FString& Text); // 표시될 npc 이름 설정

	void AnimateMessage(const FString& Text); // 텍스트 표시 시작

	void InitializeDialogue(class UDataTable* DialogueTable); // 대화 테이블 초기화

	UFUNCTION(BlueprintCallable)
    void Interact(); // 다음 대사로 넘어가게 함
   
	void DialogueEvents(); // 이벤트가 필요한 대사에 이용

private:
	
	FString InitialMessage;

	FString OutputMessage;

	int32 iLetter;

	TArray<FNPCDialogue*> Dialogue; // 대화의 행 배열

	UFUNCTION()
	void OnAnimationTimerCompleted(); // 글자 하나씩 표시함


public:
    UPROPERTY(BlueprintReadWrite)
	int32 CurrentState; // 0 = None, 1 = Animating, 2 = Text Completed, 3 = Dialogue is waiting for replies
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 SelectedReply; // 플레이어가 선택한 응답 번호

    UPROPERTY(BlueprintReadWrite)
    int32 RowIndex;

    UPROPERTY(BlueprintReadWrite)
    int32 MessageIndex;

	//플레이어 대답 버튼들 Visibility 때문에 어쩔 수 없이 만든 변수
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int NumOfReply; 

	UPROPERTY(EditAnywhere)
	class AMain* Main;

	UPROPERTY(EditAnywhere)
	class AMainPlayerController* MainPlayerController;

	// 대화 끝나고 바로 대화 또 못하게끔(애니메이션 실행할 시간이 필요)
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	bool bCanStartDialogue = true;

	FTimerHandle TimerHandle;
};


DialogueUI.cpp

#include "DialogueUI.h"
#include "Components/TextBlock.h"
#include "Main.h"
#include "MainPlayerController.h"
#include "Kismet/GameplayStatics.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "YaroCharacter.h"
#include "AIController.h"

void UDialogueUI::SetMessage(const FString& Text)
{
    if (NPCText == nullptr) return;
    NPCText->SetText(FText::FromString(Text)); // FString을 FText로 변환해서 SetText의 매개변수로 넣음
}

void UDialogueUI::SetCharacterName(const FString& Text)
{
    if (CharacterNameText == nullptr) return;
    CharacterNameText->SetText(FText::FromString(Text));
}

void UDialogueUI::AnimateMessage(const FString& Text)
{
    CurrentState = 1; // 텍스트 표시되는 중

    InitialMessage = Text;
    
    OutputMessage = "";

    iLetter = 0;

    NPCText->SetText(FText::FromString(""));
    
    // 대화 데이터 테이블의 행에서 캐릭터 이름 가져오기
    CharacterNameText->SetText(FText::FromString(Dialogue[RowIndex]->CharacterName.ToString()));
	
    GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &UDialogueUI::OnAnimationTimerCompleted, 0.2f, false);
}

void UDialogueUI::OnAnimationTimerCompleted()
{
    GetWorld()->GetTimerManager().ClearTimer(TimerHandle);

    OutputMessage.AppendChar(InitialMessage[iLetter]); // 한 글자씩 뒤에 더함

    NPCText->SetText(FText::FromString(OutputMessage)); // 대사 업데이트 -> 한글자씩 늘어나는 것처럼 보이게 됨

    if (SoundCueMessage != nullptr) // 한글자씩 업데이트될 때마다 텍스트 소리 나옴
        UAudioComponent* AudioComponent = UGameplayStatics::SpawnSound2D(this, SoundCueMessage);

    if ((iLetter + 1) < InitialMessage.Len()) // 표시할 글자가 남았으면
    {
        iLetter += 1;
        GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &UDialogueUI::OnAnimationTimerCompleted, DelayBetweenLetters, false);
    }
    else
    {
        CurrentState = 2; // 현재 대사 다 표시함
    }
}


void UDialogueUI::InitializeDialogue(UDataTable* DialogueTable) 
{
    if (Main == nullptr)
        Main = Cast<AMain>(UGameplayStatics::GetPlayerCharacter(this, 0));

    if (MainPlayerController == nullptr)
        MainPlayerController = Cast<AMainPlayerController>(Main->GetController());
    
    CurrentState = 0; // 상태 초기화, 아무 상태도 아님

    CharacterNameText->SetText(FText::FromString(""));
    NPCText->SetText(FText::FromString(""));

    OnResetOptions(); // 플레이어 응답 안 보이게

    Dialogue.Empty(); // 원래 있던 (이전)대사 행 배열 비움

    for (auto it : DialogueTable->GetRowMap())
    {
        FNPCDialogue* Row = (FNPCDialogue*)it.Value;

        Dialogue.Add(Row); // 새로운 대사 행들 추가
    }


    if (Dialogue.Num() > 0)
    {
        RowIndex = 0;

        if (Dialogue[RowIndex]->Messages.Num() > 0) // 대사 행의 메세지가 있으면
        {
            MessageIndex = 0;
            OnAnimationShowMessageUI(); // 대화창 나타남
            AnimateMessage(Dialogue[RowIndex]->Messages[MessageIndex].ToString()); // 대사 나타나기
        }
    }
}

void UDialogueUI::Interact() // 다음 대사로 넘어가기
{
    if (CurrentState == 1) // The text is being animated, skip
    {
        GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
        NPCText->SetText(FText::FromString(InitialMessage)); // 현재 대사 바로 표시
        CurrentState = 2;
    }
    else if (CurrentState == 2) // Text completed
    {
        // Get next message
        if ((MessageIndex + 1) < Dialogue[RowIndex]->Messages.Num()) // 같은 npc의 다음 대사
        {
            MessageIndex += 1;
            DialogueEvents(); // 대사 이벤트 확인 후 대사 표시
        }
        else // npc 대사 끝남
        {
            NPCText->SetText(FText::FromString(""));

            if (Dialogue[RowIndex]->PlayerReplies.Num() > 0) // 플레이어 응답 있으면
            {
                OnResetOptions();
                NumOfReply = Dialogue[RowIndex]->PlayerReplies.Num(); // 응답 개수
			   SelectedReply = 0; // 플레이어가 선택한 응답 초기화
                for (int i = 0; i < Dialogue[RowIndex]->PlayerReplies.Num(); i++)
                {
                    OnSetOption(i, Dialogue[RowIndex]->PlayerReplies[i].ReplyText); //응답 보이게 함
                }
                
                CurrentState = 3; // 플레이어 응답 기다림
            }
            else // 플레이어의 응답이 존재하지 않으면
            {
                RowIndex += 1; // 다음 대사 행

                if ((RowIndex >= 0) && (RowIndex < Dialogue.Num())) // 다음 npc 대사
                {
                    MessageIndex = 0;
                    DialogueEvents();
                }
                else // 플레이어 응답 없고 다음 npc 대사도 없으면 대화 종료
                {
                    bCanStartDialogue = false; // (수동으로) 대화 시작 못함
                    MainPlayerController->RemoveDialogueUI(); // 대화창 없어짐
                    CurrentState = 0; // 상태 초기화
                }
            }
        }
    }
    else if (CurrentState == 3) // 플레이어 응답 선택한 상태
    {
        // 플레이어 응답에 따라 RowIndex 바뀜
        RowIndex = Dialogue[RowIndex]->PlayerReplies[SelectedReply].AnswerIndex;
        OnResetOptions(); // 응답 리셋

        if ((RowIndex >= 0) && (RowIndex < Dialogue.Num())) // npc 대사 있으면
        {
            NPCText->SetText(FText::FromString(""));
            MessageIndex = 0;
            DialogueEvents(); // 이벤트 확인 후 대사 표시
        }
        else // npc 대사 없으면 대화 종료
        {
            bCanStartDialogue = false;
            CurrentState = 0;
            MainPlayerController->RemoveDialogueUI();
        }
    }
}

void UDialogueUI::DialogueEvents() // 대사 이벤트 확인
{
    int DNum = MainPlayerController->DialogueNum; 
    if (DNum == 0) // First Dialogue (cave)
    {
        if (RowIndex < 10 && Main->CameraBoom->TargetArmLength > 0) // 1인칭시점이 아닐 때
        {
            AnimateMessage(Dialogue[RowIndex]->Messages[MessageIndex].ToString());
            return;
        }

        switch (RowIndex) // 1인칭 시점일 때 카메라 회전
        {
            case 1: // Momo, Set FollowCamera's Z value of Rotation
            case 7: 
                Main->FollowCamera->SetRelativeRotation(FRotator(0.f, -15.f, 0.f));
                break;
            case 2: // Vivi
            case 6:
                Main->FollowCamera->SetRelativeRotation(FRotator(0.f, 0.f, 0.f));
                break;
            case 3: // Luko
            case 8:
                Main->FollowCamera->SetRelativeRotation(FRotator(0.f, -25.f, 0.f));
                break;
            case 4: // Zizi
                Main->FollowCamera->SetRelativeRotation(FRotator(0.f, 13.f, 0.f));
                break;
            case 5: // Vovo
            case 9:
                Main->FollowCamera->SetRelativeRotation(FRotator(0.f, 30.f, 0.f));
                if(RowIndex == 5 && MessageIndex == 2)
                    Main->FollowCamera->SetRelativeRotation(FRotator(0.f, 5.f, 0.f));
                break;
            case 10: // npc go
                Main->FollowCamera->SetRelativeRotation(FRotator(0.f, 0.f, 0.f));
                Main->CameraBoom->TargetArmLength = 500.f; // 3인칭 시점
                for(int i = 0; i < Main->NPCList.Num(); i++) // 플레이어에게 NPCList라는 배열을 만들고 npc들을 배열에 추가했음
                {
                    if (!Main->NPCList[i]->GetName().Contains("Luko")) // npc move except luko
                    {
                        Main->NPCList[i]->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.5f, false); // 1.5초 뒤 루코 대화
                return;
                break;
        }
    }

    if (DNum == 3) // Third Dialogue (first dungeon, after golem battle)
    {
        switch (RowIndex)
        {
            case 0:
                Main->CameraBoom->TargetArmLength = 200.f; // 카메라 당김
                break;
            case 2:
                if (MessageIndex == 1)
                {
                    for (int i = 0; i < Main->NPCList.Num(); i++)
                    {
                        if (!Main->NPCList[i]->GetName().Contains("Vovo")) // npc move to the boat except vovo
                        {
                            Main->NPCList[i]->AIController->MoveToLocation(FVector(628.f, 946.f, 1840.f));
                        }
                    }
                }
            	break;
        case 4:
            for (int i = 0; i < Main->NPCList.Num(); i++)
            {
                if (Main->NPCList[i]->GetName().Contains("Vovo")) // vovo moves to the boat
                {
                    Main->NPCList[i]->AIController->MoveToLocation(FVector(628.f, 885.f, 1840.f));

                }
            }
            CurrentState = 0;
            MainPlayerController->RemoveDialogueUI();
            return;
            break;     
        }
    }
    AnimateMessage(Dialogue[RowIndex]->Messages[MessageIndex].ToString()); // 대사 표시
}



DialogueUI 블루프린트

이미지

디테일 패널의 카테고리 DialogueUI 안에서 SoundCueMessage에 TextSound(사운드웨이브) 넣음.

이미지

DialogueShow라는 이름의 애니메이션을 만듬. 2초동안 스케일 0에서 1이 되도록 함. (가운데서 뿅 나오게 함)

이미지

모든 텍스트를 한글 폰트로 바꿈. (앨리스폰트)

이미지

OnAnimationShowMessageUI() - DialogueShow애니메이션을 앞에서부터 실행(대화창이 나타남)

OnAnimationHideMessageUI - DialogueShow애니메이션을 뒤에서부터 실행(대화창이 사라짐), 애니메이션이 끝나고 0.3초 뒤 Visibility 히든으로 바꾸고 2초 뒤 불 변수 true(=수동으로 대화 시작 가능)

이미지

플레이어 응답 텍스트들을 빈 텍스트로 만들고, 응답 버튼들을 숨김.

이미지

플레이어 응답 텍스트들을 보여주는 이벤트. 응답 텍스트 내용들 세팅하고 일단 첫번째 응답은 무조건 존재할 것이므로 이름창 안 보이게 한 다음 첫번째 응답 버튼을 보이게 함. 그 후에는 NumOfReply의 값에 따라 두세번째 응답버튼을 보이게 할지 말지가 결정됨.

이미지

응답버튼을 눌렀을 때 SelectedReply에 값이 들어가고 Interact함수를 실행해서 다음 대사로 넘어감. 플레이어 응답 후에는 무조건 npc 대사이므로 이름창을 다시 보이게 한 뒤 RowIndex가 1이고 DialogueNum이 0일 때만

이미지

페이드인되게 함. 그리고 플레이어의 전투몽타주에서 일어나는 애니메이션을 실행하고 5초 뒤에 카메라붐의 길이를 -60까지 당김 = 1인칭 시점이 되게 함.


MainPlayercontroller.h
UFUNCTION(BlueprintCallable)
void DisplayDialogueUI();

void RemoveDialogueUI();

UPROPERTY(VisibleAnywhere)
bool bDialogueUIVisible; // 대화창이 보이는 상태면 참, 아니면 거짓


UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dialogue")
class UDialogueUI* DialogueUI;

UPROPERTY(VisibleAnywhere, Category = "Dialogue")
TSubclassOf<class UUserWidget> DialogueUIClass;

UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Dialogue")
int DialogueNum; // 0 - intro

void DialogueEvents(); // 대화 종료 후의 이벤트

//페이드인아웃
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Widgets")
TSubclassOf<UUserWidget> WFadeInOut;

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

UFUNCTION(BlueprintImplementableEvent, Category = "Fade Events")
void FadeOut();

void FadeAndDialogue();

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Fade Events")
bool bFadeOn = false;

UFUNCTION(BlueprintCallable)
void SetPositions(); // npc&플레이어 위치 세팅

protected:
// Dialogue data tables
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dialogue")
class UDataTable* IntroDialogue;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dialogue")
class UDataTable* DungeonDialogue1;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Dialogue")
class UDataTable* DungeonDialogue2;
MainPlayerController.cp

in BeginPlay()

if (DialogueUIClass != nullptr)
{
    DialogueUI = CreateWidget<UDialogueUI>(this, DialogueUIClass); //위젯 생성해서 할당
}

if (DialogueUI != nullptr)
{
    DialogueUI->AddToViewport(); //뷰포트 추가한 뒤
    DialogueUI->SetVisibility(ESlateVisibility::Hidden); // 안 보이게

}
void AMainPlayerController::DisplayDialogueUI()
{
    if (DialogueUI)
    {
        if (!DialogueUI->bCanStartDialogue) return; // (수동으로)대화 시작할 수 없는 상태면 리턴

        bDialogueUIVisible = true; // 대화창이 보이는 상태

        switch (DialogueNum) 
        {
            case 0:
                DialogueUI->InitializeDialogue(IntroDialogue);
                break;
            case 1: // onlu luko dialogue
            case 4: // the boat move
                DialogueUI->OnAnimationShowMessageUI();
                GetWorld()->GetTimerManager().SetTimer(DialogueUI->TimerHandle, DialogueUI, &UDialogueUI::OnTimerCompleted, 0.1f, false);
                break;
            case 2:
                DialogueUI->InitializeDialogue(DungeonDialogue1);
                break;
            case 3:   
                if (!bFadeOn)
                {
                    FadeAndDialogue();
                    return;
                }              
                DialogueUI->InitializeDialogue(DungeonDialogue2);
                bFadeOn = false;
                break;
        }

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


        FInputModeGameAndUI InputMode;  
        SetInputMode(InputMode); //입력 모드 바꿈
        bShowMouseCursor = true; // 마우스 커서 보이게

    }
}
void AMainPlayerController::RemoveDialogueUI()
{
    if (DialogueUI)
    {
        DialogueNum++; 
       
        DialogueEvents(); // 이벤트 확인

        bDialogueUIVisible = false; // 대화창이 안 보이는 상태

        bShowMouseCursor = false; // 마우스 커서 안 보이게

        FInputModeGameOnly InputModeGameOnly;
        SetInputMode(InputModeGameOnly); // 입력모드 바꿈

        DialogueUI->OnAnimationHideMessageUI(); // 대화창 사라지는 애니메이션 실행

    }
}
void AMainPlayerController::DialogueEvents()
{
    switch (DialogueNum)
    {
        case 1: // luko moves to player
            for (AYaroCharacter* npc : Main->NPCList) 
            {
                if (npc->GetName().Contains("Luko"))
                {
                    npc->MoveToPlayer();
                }
            }
            break;
        case 2:
            SetCinematicMode(false, true, true); // 시네마틱 모드 해제
            for (AYaroCharacter* npc : Main->NPCList)
            {
                if (npc->GetName().Contains("Luko"))
                {
                    GetWorldTimerManager().ClearTimer(npc->MoveTimer); // 플레이어를 따라가지 않도록 타이머 제거
                    npc->AIController->MoveToLocation(FVector(5200.f, 35.f, 100.f));
                }
            }
            break;
        case 3:
        case 4:
            SetCinematicMode(false, true, true);
            break;
    }
    

}
void AMainPlayerController::FadeAndDialogue()
{
    if (WFadeInOut) 
    {
        FadeInOut = CreateWidget<UUserWidget>(this, WFadeInOut); // 위젯 생성 후 변수에 할당
        if (FadeInOut)
        {
            bFadeOn = true; // 현재 페이드되는 상태

            SetCinematicMode(true, true, true); // 시네마틱 모드로 설정, 플레이어 움직이기 불가능, 카메라 회전 불가능
            SetControlRotation(FRotator(0.f, 57.f, 0.f)); // 시야 조정

            FadeOut(); // 페이드아웃
            FadeInOut->AddToViewport(); // 뷰포트에 위젯 추가
            
        }
    }
}
void AMainPlayerController::SetPositions()
{
    if (DialogueNum == 3) // 골렘 전투 후
    {
        Main->SetActorLocation(FVector(646.f, -1747.f, 2578.f)); //플레이어의 위치와 회전값 설정
        Main->SetActorRotation(FRotator(0.f, 57.f, 0.f)); // y(pitch), z(yaw), x(roll)

        for (AYaroCharacter* npc : Main->NPCList)
        {
            npc->AIController->StopMovement(); // npc들 움직임 멈추고
            GetWorldTimerManager().ClearTimer(npc->MoveTimer); // 타이머들 해제
            GetWorldTimerManager().ClearTimer(npc->TeamMoveTimer);

            // 각자 맞는 위치에 세팅
            if (npc->GetName().Contains("Momo"))
            {
                npc->SetActorLocation(FVector(594.f, -1543.f, 2531.f));
                npc->SetActorRotation(FRotator(0.f, 280.f, 0.f));
            }
            else if (npc->GetName().Contains("Luko"))
            {
                npc->SetActorLocation(FVector(494.f, -1629.f, 2561.f));
                npc->SetActorRotation(FRotator(0.f, 6.f, 0.f));
            }
            else if (npc->GetName().Contains("Vovo"))
            {
                npc->SetActorLocation(FVector(903.f, -1767.f, 2574.f));
                npc->SetActorRotation(FRotator(0.f, 165.f, 0.f));
            }
            else if (npc->GetName().Contains("Vivi"))
            {

                npc->SetActorLocation(FVector(790.f, -1636.f, 2566.f));
                npc->SetActorRotation(FRotator(00.f, 180.f, 0.f));
            }
            else if (npc->GetName().Contains("Zizi"))
            {
                npc->SetActorLocation(FVector(978.f, -1650.f, 2553.f));
                npc->SetActorRotation(FRotator(0.f, 187.f, 0.f));
            }
        }
    }
}


데이터 테이블들

이미지

이미지

이런 식임. 행을 추가해서 캐릭터 이름과 대사를 작성하고, 플레이어 응답도 추가하고 싶으면 추가.


메인플레이어컨트롤러 블루프린트

이미지

디테일 패널에서 대화 데이터테이블 확인 가능


이미지

페이드인아웃 위젯의 페이드아웃이벤트 실행하고 2초 뒤 캐릭터들 위치 세팅한 뒤 페이드인이벤트 실행.

1.5초 뒤 대화 시작.


Main.h
UPROPERTY(VisibleAnywhere)
class AYaroCharacter* test; // 이거 없으면 밑에 배열 오류남.

UPROPERTY(VisibleAnywhere)
TArray<AYaroCharacter*> NPCList;

어떤 클래스로 배열을 만드려면 해당 클래스형의 변수가 있어야 하나 봄.. 확신은 없지만 아무튼..


YaroCharacter.cpp

in Tick()

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

    if(Player) Player->NPCList.Add(this);
}

NPCList에 npc 본인 추가.


Fade_InOut 블루프린트(위젯)

이미지

FadeEffect라는 이름의 애니메이션을 만듬. 2초동안 알파값이 0에서 1로 증가.


이미지

페이드인 - 애니메이션을 처음부터 실행

페이드아웃 - 애니메이션을 뒤에서부터 실행(알파값1에서 0으로 감소)

둘 다 4초 뒤 위젯(본인) 삭제



Enemy코드 리팩토링

공격 멈춤 버그 때문에 보다가 너무 복잡한 거 같아서 쭉 살펴보고 정리함.

특히 공격이랑 인식 부분.

bHasValidTarget 변수 없애고 필요없는 코드 조금 정리함 + 공격 버그 고침

뭐 때문이었는지 이제 기억이 안 남.. 고친 지 좀 되어가지고…


파우스 메뉴를 이용해 타이틀로 돌아간 뒤 바로 이어하기 하면 강종되는 문제

분명히 플레이어 변수 관련한 문제인데 정확하게 뭐 때문이다! 라고 하기는 어려움. 나도 잘 모르겠음.

일단 모모/루코의 MoveToPlayer()에서 계속 오류가 나서 오류가 안 나는 비비/지지/보보네 조건문 안으로 같이 들어감.

YaroCharacter.cpp

in Tick()

if (!canGo && Player && Player->NpcGo)
{
    canGo = true;
    if (this->GetName().Contains("Momo") || this->GetName().Contains("Luko"))
    {
        GetWorldTimerManager().SetTimer(MoveTimer, this, &AYaroCharacter::MoveToPlayer, 1.f);
    }
    else // 비비, 지지, 보보
    {
        MoveToLocation();
    }
}

그리고 함수 내의 타이머를 없애고 헤더에서 그냥 타이머핸들 만든 뒤 함수에서 SetTimer해서 돌림.

비비/지지/보보의 경우 아직 함수 내에서 계속 돌림. 타이머핸들만 헤더에서 선언한 거 씀.

in MoveToPlayer()

if (Player == nullptr) return;

함수 안에서 제일 먼저 검사함. 플레이어 없으면 리턴.

GetWorldTimerManager().SetTimer(MoveTimer, this, &AYaroCharacter::MoveToPlayer, 0.5f);	

함수 맨 끝에서 타이머 설정함. 시간도 1초에서 0.5초로 감소함. 인식이 좀 느린 것 같길래..



영상은 다음 개발로그에서…

Updated: