Dev18 옐로 스톤 관련, 바닥 올라오게 하기, 플레이어 스폰 기능
옐로 스톤 관련
비비가 돌을 던지고 플레이어가 그 돌을 집어서 특정 위치에 놓으면 바닥이 올라오는 그런 시스템
먼저 그 발판으로 점프하면 짧은 대화가 나옴.
트리거박스를 이쪽에 놔두고
오버랩 이벤트를 만들었다.
오버랩된 게 플레이어면 대화 넘버가 6인지 7인지 검사하고 6이면(트리거박스1 안 다녀옴) 7로 바꾸고, 7이면 그냥 진행.
마우스와 키보드 입력을 막고 대화를 시작한다. (플레이어가 대화 진행 불가)
0.4초 뒤 캐릭터 움직임 막고 0.6초 뒤 자동으로 Interact() 실행하여 다음 대사로 넘어감.
대사 2개라 딜레이 후 Interact 한 번 더 실행.
그 후 한 번 더 점프하여 반대편으로 잘 건너가게 되면 또 대화 시작.
여기에도 트리거박스를 놔뒀다.
오버랩 이벤트도 당연히 만듬. 여기가 상당히 좀 복잡하다.
오버랩된 게 npc이고 대화넘버가 10이면 대화를 시작한다.
Do Once 노드로 대화가 한 번만 실행되게 한다. 따라서 첫 npc가 오버랩되었을 때만 대화 시작.
오버랩된 게 플레이어면 대화넘버가 8이 맞는지 검사하고 맞으면 시퀀스 노드를 통해 두 가지 동작을 같이 실행한다.
하나는 Plane1(점프했던 작은 발판)이 유효하면 AddRelativeLocation을 통해 아래로 떨어뜨리는 것.
나머지 하나는 0.3초 뒤 대화를 시작하고 플레이어의 움직임을 막는다.
작은 발판 떨어뜨리는 과정은 AddRelativeLocationd을 0.01초마다 실행해주는 것.
0.01초마다 (Plane1이 유효한 지 검사 후) Z값을 5씩 감소시킨다. 참, 이렇게 이동시키려면 이동할 개체의 모빌리티가 무버블이어야한다.
이렇게 아래로 떨어뜨리면서 4초 뒤 파괴한다. 시퀀스 노드를 이용해서 떨어뜨리는 것과 동시 실행하지만 파괴는 딱 한 번만 실행.
작은 발판 떨구기로 이걸로 끝이고 대화쪽으로 넘어가자.
플레이어 움직임을 막고 2초 뒤 비비가 NormalMontage를 재생하고 있는지 검사한다.
던지는 행위를 해야하기 때문에 비비에게 노말몽타주를 만들어줬다.
(해당 대화가 끝나면 비비가 노말몽타주의 던지는 애니메이션을 재생한다.)
비비가 노말몽타주를 재생하고 있다면 몽타주의 특정 위치를 넘어가면 옐로 스톤의 위치를 비비의 LeftHandSocket의 위치로 설정한다.
비비의 노말몽타주.
스켈레톤에 LeftHandSocket도 추가해줬다.
오른손엔 지팡이를 들고 있으니 돌은 왼손에 있어야하기 때문. (플레이어도 마찬가지)
옐로 스톤 블루프린트.
아이템 클래스를 이용해 블루프린트를 만들고 돌 스태틱 메시를 넣었다.
이렇게 월드에 배치해뒀다. 메시의 Hidden in Game이 켜져있기 때문에 게임 시작하면 안 보인다.
다시 그래프로 넘어가자면 비비의 왼쪽 손 소켓으로 위치하게 한 뒤에는 메시의 Hidden in Game을 꺼서 눈에 보이도록 한다.
그리고 MoveComponentTo 노드를 통해 1초동안 특정 위치로 이동하게 한다. (비비가 던져서 날아가는 것처럼 보임)
시스템메세지 넘버를 10으로 설정하고 돌에 관한 시스템 메시지를 보이도록 한 뒤 플레이어가 움직일 수 있도록 한다.
돌이 검은색으로 빛나서 잘 안 보이지만 플레이어 앞에 돌이 있음.
마우스 왼쪽 버튼으로 주울 수 있음.
Main.cpp
in LMBDown()
if (ActiveOverlappingItem && MainPlayerController->DialogueNum == 9) // pick up the yellow stone
{
if (MainAnimInstance && NormalMontage)
{
if (MainAnimInstance->Montage_IsPlaying(NormalMontage) == true) return;
bCanMove = false;
FRotator LookAtYaw = GetLookAtRotationYaw(ActiveOverlappingItem->GetActorLocation());
SetActorRotation(LookAtYaw);
MainAnimInstance->Montage_Play(NormalMontage);
MainAnimInstance->Montage_JumpToSection(FName("PickItem"), NormalMontage);
}
}
아이템 클래스라서 ActiveOverlappingItem에 잡힘.
노말 몽타주의 PickItem섹션에서 시작하는데
PickItem이라는 노티파이를 추가해둠.
대화 넘버 검사 후 오버랩된 아이템을 옐로 스톤으로 변환한 후 PickUp 함수에 매개변수로 플레이어 넣어서 실행.
5초 뒤 플레이어 이동 가능.
PickUp함수는 원래 옐로 스톤 블루프린트에 만들어뒀었는데 아이템 클래스가 전부 쓸 수 있도록 코드에 넣음.
Item.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Main.h" // generated.h 위에 써야함
#include "Item.generated.h"
UFUNCTION(BlueprintCallable)
void PickUp(AMain* Char);
Item.cpp
void AItem::PickUp(AMain* Char)
{
// 카메라랑 폰 콜리전 무시
Mesh->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
Mesh->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore);
Mesh->SetSimulatePhysics(false);
Mesh->bHiddenInGame = false;
const USkeletalMeshSocket* LeftHandSocket = Char->GetMesh()->GetSocketByName("LeftHandSocket"); // 플레이어의 LeftHandSocket 가져오기
if (LeftHandSocket)
{
LeftHandSocket->AttachActor(this, Char->GetMesh());
bRotate = false;
Char->ItemInHand = this; // ItemInHand에 할당
Char->SetActiveOverlappingItem(nullptr); // ActiveOverlappingItem 제거
}
}
Weapon의 Equip함수를 참고했다.
이렇게 해서 플레이어의 왼쪽 손에 돌이 들려있는 걸 볼 수 있다.
이제 문제는 돌을 끼우는 것(놓는 것)
고민하다가 결국 플레이어에게 새로운 범위를 주기로 했다.
Main.h
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Items)
class AItem* ItemInHand; // 손에 있는 아이템
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Items)
class USphereComponent* ItemSphere; // 새 범위, 액터 추출
UFUNCTION()
virtual void ItemSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
UFUNCTION()
virtual void ItemSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = Items)
class AActor* CurrentOverlappedActor; // 새 범위에 오버랩된 액터
Main.cpp
in 생성자
ItemSphere = CreateDefaultSubobject<USphereComponent>(TEXT("ItemSphere"));
ItemSphere->SetupAttachment(GetRootComponent());
ItemSphere->InitSphereRadius(130.f);
ItemSphere->SetCollisionObjectType(ECollisionChannel::ECC_WorldStatic);
in BeginPlay()
ItemSphere->OnComponentBeginOverlap.AddDynamic(this, &AMain::ItemSphereOnOverlapBegin);
ItemSphere->OnComponentEndOverlap.AddDynamic(this, &AMain::ItemSphereOnOverlapEnd);
void AMain::ItemSphereOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor)
{
if (auto actor = Cast<AItem>(OtherActor)) return; // 오버랩된 게 아이템이면 실행X
CurrentOverlappedActor = OtherActor;
}
}
void AMain::ItemSphereOnOverlapEnd(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
if (OtherActor == CurrentOverlappedActor)
{
CurrentOverlappedActor = nullptr;
}
}
요정도 범위이다.
그리고 손에 돌은 든 채로 왼쪽 마우스 버튼을 클릭하면
Main.cpp
in LMBDown()
if (CurrentOverlappedActor && ItemInHand)
{
if (CurrentOverlappedActor->GetName().Contains("MovingStone") && ItemInHand->GetName().Contains("Yellow")) // MovingStone이 가깝고 손에 옐로스톤이 있으면
{
if (MainPlayerController->DialogueNum == 9)
{
if (MainAnimInstance && NormalMontage)
{
if (MainAnimInstance->Montage_IsPlaying(NormalMontage) == true) return;
bCanMove = false;
FRotator LookAtYaw = GetLookAtRotationYaw(CurrentOverlappedActor->GetActorLocation());
SetActorRotation(LookAtYaw);
MainAnimInstance->Montage_Play(NormalMontage);
MainAnimInstance->Montage_JumpToSection(FName("PutStone"), NormalMontage); // 돌 놓기
}
}
}
}
PutStone이라는 섹션으로 점프하는데
참고로 이건 나중에 엔딩에서 돌 가져올 때 쓸 애니메이션을 거꾸로 재생한 것.
디테일창 보면 재생속도가 -1로 되어있다. 이렇게 하면 거꾸로 재생이 가능함.
PutStone이라는 노티파이도 추가함.
플레이어의 손에 든 아이템(돌)을 파괴한 뒤 ItemInHand를 null로 만들고 CurrentOverlappingActor(가까운 액터, MovingStone)의 루트 컴포넌트의 자식 컴포넌트 중 이름에 09d가 들어간 컴포넌트(돌)의 HiddenInGame을 꺼버려서 보이게 한다. 즉, 플레이어 손에 있던 돌은 없어지고 MovingStone에 안 보이던 돌이 보이게 됨. (플레이어가 돌을 놓은 것처럼 보임)
0.5초 뒤에 Main의 PlaneUp을 호출한다.
바닥 올라오게 하기
이제 또 문제. 플레이어가 이 돌을 놓았을 때 바닥이 올라오게 해야함.
바닥은 레벨에 위치한 스태틱 메시인데..
플레이어의 행동->레벨의 스태틱 메시 이동. 레벨 블루프린트에서 플레이어가 돌을 놓았는지 계속 검사하는 것도 별로고 코드에서 스태틱 메시를 이동시키는 건 더더욱 불가.
이것을 어떻게 연결시켜야할지 연구하다가 이벤트 디스패처라는 것을 알게 됨. 블루프린트에서 만드는 건데 이걸 가지고 레벨 블루프린트에서 불러서 바닥을 올라오게 하는 노드랑 연결시키면 되긴 할 거 같음.
하지만 플레이어 캐릭터는 둘이라 블루프린트도 두 개임. 여캐인지 남캐인지 구분해서 블루프린트 가져오는 것도 번거로울 것 같음. Main에서 이벤트 디스패처를 만들 순 없을까하다가 델리게이트라는 걸 보게 됨.
Main.h
// 블루프린트에서 쓰려면 다이나믹 멀티캐스트여야함
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FDele_Dynamic);
UPROPERTY(BlueprintAssignable, BlueprintCallable) // 레벨 블루프린트에서 바인딩함
FDele_Dynamic PlaneUp;
여러 번의 시도 끝에 컴파일에 성공함.
델리게이트가 뭔지 잘은 모르겠지만 얘로 만들면 블루프린트에서 바인딩할 수 있다고 함.
그래서 cpp에 함수 정의 안 씀.
커스텀이벤트를 만들어서 Main의 PlaneUp 함수에 바인딩함.
이렇게 하면 남캐 여캐 구분해서 블루프린트 불러올 필요가 없음. 남캐든 여캐든 전부 Main클래스이기 때문에.
Main에 선언했으니 호출도 아주 쉬움. (위에 노티파이 이벤트에서 PlaneUp호출)
아무튼 호출되면 시스템 메세지를 없앤 뒤 효과음을 재생하고 Plane2를 1.9초동안 특정 위치로 이동시킨다. 그리고 대화 시작.
이제 npc들이 건너올 수 있겠지 했지만 애들이 안 옴.
내비게이션을 확인해보니 안 생김.
보니까 기본 설정이 내비게이션 한 번만 딱 생성하고 마는 거임.
Runtime Generation을 다이나믹으로 해주면 실시간으로 내비게이션을 생성함.
기본값은 스태틱임.
이제 애들이 건너오는데 살짝 막히길래 난간 스태틱 메시들 콜리전을 수정하고 조금씩 이동시켜서 길을 조금 뚫어줌.
플레이어 스폰
플레이어가 맵 밖으로 떨어졌거나 다시 돌아오기 힘든 곳에 갔을 때 자동으로 이전 위치로 스폰
일단 플레이어가 가면 안되는 공간에 전부 트리거 박스를 놓기로 함.
근데 하다가 너무 귀찮아서 놔둔 곳도 있음. 물론 맵 밖으로 떨어질 수 있는 곳은 전부 함.
이 모든 트리거박스가 하는 일이 동일한데… 얘네 오버랩 이벤트를 전부 하나로 할 수 없나?
ㅋㅋ 안 될리가. 커스텀 이벤트 만들어서 오버랩이벤트에 바인딩함.
근데 그냥 커스텀이벤트면 안 되고 오버랩 이벤트처럼 Overlapped Actor랑 Other Actor 매개변수 있어야함.
아무튼 이 트리거박스들에 플레이어가 오버랩되면 MainPlayerController의 bFallenPlayer true하고 대화창 보이는 상태면 대화창 없애고 플레이어 움직임 막은 뒤 FadeAndDialogue 실행.
MainPlayerController.cpp
void AMainPlayerController::FadeAndDialogue()
{
if (WFadeInOut)
{
FadeInOut = CreateWidget<UUserWidget>(this, WFadeInOut);
if (FadeInOut)
{
bFadeOn = true;
if (bFallenPlayer) FallingCount += 1; // 떨어진 횟수 증가
if (DialogueNum == 3) // 이건 던전1 골렘 이후만
{
SetCinematicMode(true, true, true);
SetControlRotation(FRotator(0.f, 57.f, 0.f));
}
FadeOut();
FadeInOut->AddToViewport();
}
}
}
MainPlayerController 블루프린트.
두번째 던전 입장한 다음(대화 넘버 6이상)이고 bFallenPlayer가 참이면 플레이어의 위치를 마지막으로 저장된 위치로 설정함.
페이드인 시작하고 대화넘버가 9가 아니면 npc들이 다시 플레이어 따라가게 하고 처음 떨어졌거나 5번 떨어졌으면 대화 시작.
그 외에는 그냥 1초 뒤에 플레이어 이동 가능하고 bFallenPlayer false함.
스폰 대화를 따로 만들어서 MainPlayerController에 넣음.
세세한 건 귀찮아서 생략.