언리얼엔진 학습 및 과제

팀 프로젝트 슈터 게임 (7)

park94128 2025. 11. 12. 20:00

● 상호 작용 액터의 기능 확장

A. 엘리베이터 버튼에 좀비 웨이브 넣기

내가 만든 것이 아니라 팀원이 만든 블루프린트

제일 처음에 팀원들 끼리 게임 내용을 구성할 때 특정 구간에 좀비떼가 몰려오는 상황을 구현하기로 했었다. 그리고 적 기능을 담당하는 팀원이 구현했다. 일단 블루프린트로 만들었고, 엘리베이터 앞에 콜리전을 만들어서 플레이어와 겹치면 좀비떼가 몰려오도록 만들었다. 이제 이것을 내가 만든 액터와 합쳐야 한다.

 

private:
	//문열림 연출용 함수
	UFUNCTION()
	virtual void DoorOpenTimeLineFunc(float Output);
	
	//문 액터
	UPROPERTY(EditInstanceOnly, Category = "Door Action", meta = (AllowPrivateAccess = true))
	AActor* DoorActor = nullptr;
	
	//문열림 연출 실행
	FOnTimelineFloat UpdateFunctionFloat;

	//문열림 연출용 CurveFloat
	UPROPERTY(EditDefaultsOnly, meta = (AllowPrivateAccess = true))
	UCurveFloat* DoorTimelineCurveFloat;

	//문열림 연출용 Timeline
	UPROPERTY(EditDefaultsOnly, Category = "Door Action", meta = (AllowPrivateAccess = true))
	UTimelineComponent* DoorTimeline;

 그 전에, 엘리베이터 버튼에 대해서 문열림 연출을 구현하지 않았기 때문에 지금 구현햐야 한다. 일단 병원 문 액터에서 타임라인 관련 내용을 가져왔다.

 

protected:
	//수많은 좀비가 몰려오다.
	UFUNCTION(BlueprintImplementableEvent, Category = "Door Action")
	void StartWave(AActor* PlayerActor);

나는 엘리베이터 버튼 액터를 C++코드로 만들었지만 팀원의 좀비웨이브는 블루프린트로 만들었다. 그래서 이 사이를 이어주기 위해 블루프린트로 내용을 구현하는 함수를 선언했다.

 

A-1. 특정 좀비가 생성되지 않을 조건

너무 어두워서 언릿으로 맞춰놓고 스크린샷을 찍었다.

내가 만든 스테이지2의 구성중 하나는 터널 사이를 이어주는 작은 통로인데 양 끝에 문이 있다. 스테이지 시작 방향의 문은 닫힌 상태고 스테이지 끝 방향의 문은 열린 상태다. 그리고 두개의 문의 스위치는 각각의 방향에 있는 터널에 있다. 그래서 지나가려면 스테이지 시작 방향의 문은 반드시 열어야 하지만 스테이지 끝쪽 방향의 문은 닫을 수 있다. 그리고 이 문 닫음 여부는 마지막 좀비 웨이브의 좀비 수에 영향을 준다.

 

//GateSwitch.h

public:
	//이 스위치가 다루는 문의 열림 여부
	bool GetGateOpen() const;

문 스위치 액터의 코드에서 문열림 여부를 확인할 수 있도록 public 함수를 만든다.

 

//ElevatorButton.h

private:
    //추가 좀비 생성 여부를 결정하는 문 스위치
    UPROPERTY(EditInstanceOnly, Category = "Enemy Wave", meta = (AllowPrivateAccess = true))
    TArray<class AGateSwitch*> GateSwitches;

엘리베이터 버튼 액터에서 여러 문 스위치 액터를 등록할 수 있도록 TArray를 만든다.

 

//ElevatorButton.h

//특정 상황에 따라 추가로 좀비를 생성할지 결정
UFUNCTION(BlueprintPure, Category = "Door Action")
bool CanMakeOtherEnemies();


//ElevatorButton.cpp
bool AElevatorButton::CanMakeOtherEnemies()
{
	if (GateSwitches.IsEmpty())
		return true;

	//지정한 문 중 하나라도 닫힌 상태라면 추가 생성 불가
	for (const AGateSwitch* GateSwitch : GateSwitches)
	{
		if (::IsValid(GateSwitch) && !GateSwitch->GetGateOpen())
			return false;
	}
	
	return true;
}

등록한 문 스위치중 하나라도 닫힌 상태면 추가 생성을 못하게 한다. 이 함수는 블루프린트에서 호출 할 수 있도록 BlueprintPure속키워드를 적는다. (입력 없이 반환만 필요한 경우 BlueprintCallable이 아니라 BlueprintPure를 쓰는 쪽이 편하다.)

 

private:
	//반드시 실행하는 좀비 웨이브
	UPROPERTY(EditInstanceOnly, BlueprintReadOnly, Category = "Enemy Wave", meta = (AllowPrivateAccess = true))
	TArray<AWaveManager*> DefaultWaveMangers;

	//조건 만족시 추가로 실행하는 좀비 웨이브
	UPROPERTY(EditInstanceOnly, BlueprintReadOnly, Category = "Enemy Wave", meta = (AllowPrivateAccess = true))
	TArray<AWaveManager*> AdditionalWaveMangers;

팀원이 만든 좀비를 대량으로 만드는 클래스가 있는데 이것도 반드시 생성할 것과 조건하에 생성할 것을 나눈다.

 

A-2. 좀비 웨이브 기능을 엘리베이터 버튼에 붙여넣기

팀원이 만든 기능을 내가 만든 코드와 결합한다. 일단 좀비 생성 기능을 붙여놓기는 하나 내가 선언한 변수를 사용할 것이므로 적절히 변형시킨다.

- 블루프린트 이벤트 시작은 내가 선언한 함수 호출이다.

- 기존 트리거에서 사용한 매개변수 중 사용하는 것을 내가 선언한 함수의 매개변수로 만들고 그것을 사용하게 했다.

- 팀원은 좀비를 대량으로 만드는 클래스를 그냥 맵에 배치된 모든 것을 가져오게 만들었다. 나는 반드시 실행할 것과 조건 하에 실행할 것으로 나누었고 그게 액터에 등록된 상태니 그 변수를 사용하도록 만든다.

- 기존에는 TArray 순회를 1번만 하지만 나는 조건에 따라 2번 할 수도 있다.

 

좀비가 몰려오는 기능이 완성됐다.

 

A-3. 잠시후 열리는 문

엘리베이터 버튼을 누르면 바로 열리는 것이 아니라 잠시 후에 열린다. 그 사이에 몰려오는 좀비 무리와 싸우는 것이 의도한 설계다.

엘리베이터 버튼이 사용하는 타임라인 그래프는 다른 것과 이질적인 모습이다. 시작시 -1이고 30초에 걸쳐서 서서히 0이 되다가 30초 부터 값이 급등한다. 이 30초부터 문이 열린다.

 

void AElevatorButton::DoorOpenTimeLineFunc(float Output)
{
	//음의 값은 무시한다.
	if (Output < 0)
	{
		return;
	}
	
	//세로 방향으로 이동
	const FVector Value = FVector(0, 0, Output);
	DoorActor->SetActorLocation(FirstDoorLocation + Value);
}

30초 이전에는 음의 값이니 무시하고 30초부터 들어오는 값을 사용하여 문을 이동시키도록 했다.

 

B. 카드 아이템을 획득해야 작동하는 문 스위치

이전 글에서 적지 않은 내용이 있는데, 문 스위치 액터는 하나의 코드지만 2개의 블루프린트에서 사용하고 있었다. 작은 통로를 막는 문을 담당하는 스위치 말고 출구로 향하는 길을 막는 큰 문을 담당하는 스위치가 있다.

 

두 액터는 할당된 메시, 타임라인, 그리고 배정된 문 액터는 다르지만 그 외에는 다를 것이 없어서 같은 코드를 쓰고 있었다. 그런데 카드 아이템의 존재 목적이 필요하기에 큰 문을 담당하는 액터를 바꿀 것이다. 스테이지 내에 1개만 존재하며 출구를 막고 있다는 의미가 있기 때문이다.

 

//GateSwitch.h

protected:
	//이 스위치를 사용할 조건을 만족하는가
	virtual bool UsingSwitchCondition() const;
    

//GateSwitch.cpp
bool AGateSwitch::UsingSwitchCondition() const
{
	//원본 함수는 무조건 true
	return true;
}

부모 클래스가 될 문 스위치 액터에 bool형 가상 함수를 만들었다. 원본 함수는 항상 true를 반환할 것이다.

 

void AGateSwitch::Interaction(ABaseCharacter* Character)
{
	//스위치 사용이 가능한가
	if (!UsingSwitchCondition())
		return;
	
	//문열림 여부 변경
	bGateOpen = !bGateOpen;

	if (bGateOpen)//열기
	{
		SwitchTimeline->Play();
		GateTimeline->Play();
	}
	else//닫기
	{
		SwitchTimeline->Reverse();
		GateTimeline->Reverse();
	}
}

상호작용 함수내 최상단에 이 함수를 호출하여 false면 작동하지 않게 만든다. 부모 클래스에서는 true만 반환하는 함수라서 항상 작동할 것이지만 이 클래스를 상속받는 자식 클래스에서는 다른 결과가 나올 것이다.

 

//LargeGateSwitch.h
UCLASS()
class ALargeGateSwitch : public AGateSwitch
{
	GENERATED_BODY()

public:
	ALargeGateSwitch() : AGateSwitch() {}

protected:
	//이 스위치를 사용할 조건을 만족하는가
	virtual bool UsingSwitchCondition() const override;
};


//LargeGateSwitch.cpp
bool ALargeGateSwitch::UsingSwitchCondition() const
{
	//플레이어의 카드 획득 여부로 결정
	UCH3GameInstance* GameInstance = Cast<UCH3GameInstance>(UGameplayStatics::GetGameInstance(GetWorld()));
	return GameInstance->Level2KeyCard;
}

그러면 이제 기존 문 스위치 액터 클래스를 상속받아 새로운 액터를 만들고, 선언했던 가상 함수에서는 카트 아이템 획득 여부를 반환하도록 한다.

큰 문을 담당하는 액터인 블루프린트에서 클래스 세팅에 들어가면 상속받을 클래스를 변경할 수 있는데 이것을 새로 만든 코드인 클래스를 지정하면 된다.

 

C. 철장 문 관리 액터와 UI 연동

팀원이 퍼즐 정답 입력 UI를 완성했다고 한다. 그래서 액터와 UI 서로 연동되도록 만들어야 한다.

void ACageLocker::Interaction(ABaseCharacter* Character)
{
	if (bDoorOpen)
		return;

	//번호 입력 UI 열기
	AChoiceHubPlayerController* PlayerController = Cast<AChoiceHubPlayerController>(Character->GetController());
	if (!PlayerController)
	{
		UE_LOG(LogTemp, Warning, TEXT("UI 열기 불가"));
		return;
	}

	PlayerController->ShowPasswordKeypad();
}

처음에는 상호작용 즉시 문이 열리도록 했지만 상호작용 함수에 그 기능이 빠졌다. 대신 UI 여는 기능을 실행한다.

 

private:
	static ACageLocker* Instance;
public:
	static ACageLocker* GetInstance();
    
    
    //문열기
	void OpenDoor();
	//문닫기
	void CloseDoor();

그리고 이 액터에 static instance를 만든다. 또한 문을 여는 기능을 public함수로 따로 만든다. 여기에 더하여 반대 기능인 문닫기 함수도 만들었는데 이는 내가 사용하려고 만들었다.

 

void ACageLocker::OpenDoor()
{
	if (bDoorOpen)
		return;
	
	DoorTimeline->Play();
	bDoorOpen = true;
}

void ACageLocker::CloseDoor()
{
	if (!bDoorOpen)
		return;

	DoorTimeline->ReverseFromEnd();
	bDoorOpen = false;
}

타임 라인 기능을 도입하여 문을 열고 닫는다. UI를 담당한 팀원은 올바른 정답 입력시 문을 여는 함수를 실행하기만 하면 될 것이다. 퍼즐을 담당하는 액터도 static instance라서 팀원이 UI 기능을 완성하는 데에는 짧은 시간이 소요됐다.