6. UMG 디자인

개발 일지|2024. 11. 9. 22:43

UI를 클릭했을 때 유저의 Focus가 있는 위젯에 노란색 강조 테두리가 생기고, 다른 위젯을 클릭하면 원래 Focus를 가진 위젯의 테두리는 없애고 새로 Focus를 가져간 위젯에 노란색 강조 테두리를 만드는 이 사소한 기능이 이렇게 구현하기 어려운거였나?

 

사소한 기능 하나 구현못해서 오늘 하루가 없어진게 너무 짜증난다.

 

Border -> 이거는 왜 만든거지? 배경색과 테두리 색깔을 왜 각각 컨트롤 할 수 없지?

왜 Brush color를 변경해도 테스트하면 아무런 작동이 일어나지 않지?

왜 백그라운드 컬러만 주구장창 바꾸고 있지?

왜 이미지 컴포넌트의 child는 없지?

왜 Canvas pannel은 크기를 지 맘대로 생성하고 다른 위젯과 겹치게 생성되지?

왜 버튼 디자인은 Normal, Hover, Pressed, Disabled밖에 없지? On Focus는 왜 없지?

 

UI가 싫다.

 

https://forums.unrealengine.com/t/umg-outline-settings-and-parent-borders/385674/2

 

[UMG] Outline Settings and Parent Borders

Hello Absolute Reality, This appears to share the same root cause as a known issue. I have provided a link to the public tracker. Please feel free to use the link provided for future updates. Link: Unreal Engine Issues and Bug Tracker (UE-40691) Make it a

forums.unrealengine.com

내일은 거지같은 블루프린트에 배열을 만들어서 개같은 세션 리스트 Size Box들을 관리해보자^^

'개발 일지' 카테고리의 다른 글

OnlineSubsystem  (0) 2024.11.16
7. 온라인세션? 쉽지않다  (0) 2024.11.12
5. 멀티플레이 개발  (0) 2024.11.08
4. 서버 구축을 위한 삽질  (0) 2024.09.02
3. 플레이어 랜덤 스폰 구현  (0) 2024.08.31

댓글()

5. 멀티플레이 개발

개발 일지|2024. 11. 8. 23:14

지난번에 Dedicated server 개발용 언리얼 엔진을 다운받고 많은 일이 있었다.

우선 깃허브 엔진과 에픽게임즈 런처 엔진이 달라 원래 개발하던 프로젝트가 죽었었다.

여러 방법을 시도해봤지만 안됬었는데, 중간에 비주얼 스튜디오 업데이트를 하면서 더 꼬였었다.

결국 깃허브에서 엔진 재설치를 하고 프로젝트 임시 파일을 싹 날린다음 .uproject 파일에서 Generate project file을 하고 클린 프로젝트 -> Rebuild를 하니 살릴 수 있었다.

엔진의 빌드가 8시간 넘게 걸려 새로 빌드를 해보는 날엔 거의 하루를 날렸었는데, 고치는동안 빌드를 너무 많이 시도해 날린 많은 시간들이 아깝다..


 

그다음 세션 생성 및 참가를 위해 로직을 작성했는데 처음에는 GameMode 클래스에 모든 로직을 작성했었다.

근데 서버 - 클라이언트 동작이 GameMode 클래스에서 동작하지 않아 PlayerController로 로직을 일부 옮겨 멀티플레이의 핵심인 세션 생성 및 세션 검색, 세션 참가를 GameMode 클래스에서 동작하도록 하고, PlayerController 클래스에서 각 플레이어가 서버에 해당하는 동작을 요청하도록 변경하였다.


 

세션 관리는 따로 GameMode 클래스가 서버에 단 하나만 존재한다고 생각해 SessionInfo 구조체에 세션 이름, 맵 이름, 현재인원수, 최대인원수, 세션 UID를 저장하는 방식으로 설계하고 OnlineSubsystem이 세션을 생성할 때 FName 타입으로 UID를 지정해서 추후 세션 참가시 세션 UID를 참고하여 접속할 수 있도록 구성했었다.

 

하지만 PIE 테스트 특성 때문인지 GameMode 클래스에서 TMap 타입 ActiveSession을 UID-SessionInfo 조합으로 선언했는데, 인스턴스 마다 ActiveSession을 새로 생성해서 참조하는 일이 발생했다.

 

이것을 해결하기 위해 여러 방법을 시도해봤지만, 최종적으로 GameInstance 클래스에 ActiveSession TMap을 옮기고 관리하도록 구성한 다음, GameInstance를 싱글톤 형식으로 다른 클래스들에서 참조하도록 하니 ActiveSession의 일관성이 유지되었다.

 


 

그다음 Widget Blueprint로 UI를 구성하고 C++로 구성해놓은 세션 관련 함수들을 바인딩 해서 테스트 해보았다.

Create Session은 현재 처음 엔진을 가동하고 나서 첫 동작시 정상적으로 레벨 이동이 되지 않고 메인 UI로 튕겨져나오는 현상이 발생하고 있다. 그거 제외하고는 잘 동작하고 있다.

 

Join Session이 가장 말썽이 많았다.

SessionList의 각 세션마다 Horizontal Box를 생성해서 Join Session UI의 Scroll Box에 들어가도록 설계했다.

블루프린트도 디자이너쪽에서 사용하기 좋다는 말을 봐서 UI를 모두 Widget Blueprint로 생성하고 블루프린트로 동작을 구성했다.

Event Construct 노드를 사용해 처음 세션 검색을 하도록 하고, Refresh버튼의 클릭 이벤트가 발생하면 세션 리스트를 새로고침 하도록 했다. 세션 리스트 중 하나를 클릭하면 SessionList Widget BP에 담겨있는 Session UID를 참고해 세션에 참가하도록 설계했다.

 

하지만 Scroll Box 내부에 세션 리스트가 겹쳐서 생성되거나, 클릭이 안되거나 하는 여러 문제가 생겼었다.

대부분 해결했지만 현재 해결해야할 문제는 다음과 같다.

 

1. SessionList 내부 UID를 사용해 세션 참가 요청을 보내지만 일치하는 UID가 없다고 세션에 참가하지 못하는 문제

2. 클릭한 UI에 강조표시가 나타나지 않는 문제

3. 세션 리스트 중 하나를 클릭하고, 다른 세션을 클릭했을 때 선택된 세션의 정보로 데이터가 바뀌지 않는 문제

 

빨리 해결해야 한다..

'개발 일지' 카테고리의 다른 글

7. 온라인세션? 쉽지않다  (0) 2024.11.12
6. UMG 디자인  (0) 2024.11.09
4. 서버 구축을 위한 삽질  (0) 2024.09.02
3. 플레이어 랜덤 스폰 구현  (0) 2024.08.31
2. 움직이는 플레이어 캐릭터 만들기  (0) 2024.08.26

댓글()

UE5 컴파일시 Debug console만 뜨는 에러

Error|2024. 11. 7. 22:34

잘 되던 프로젝트 파일이 컴파일을 하면 자동으로 엔진이 실행되어야 하는데 디버그 콘솔만 뜨고 아무일도 일어나지 않는다..

딱히 에러도 안뜨고 코드도 건드린적 없는데 왜이러는걸까?

 

다른 프로젝트 파일의 라이브 코딩을 사용한 적 있는데 이게 아마 원인인듯 하다. 

 

빌드 툴이 UnrealBuildTool로 되어있었는데, AutomaionTool로 변경하니 다음과 같은 에러가 표시된다.

갑자기?? 멀쩡히 잘 되던게??

 

원인을 검색해보니 비주얼 스튜디오를 업데이트 하면서 5.3버전의 언리얼 엔진과 충돌이 발생한것 같다..

그에 따른 해결법은 

https://forums.unrealengine.com/t/error-compiling-the-automation-tool-after-updating-visual-studio-today-unreal-5-3-2/1393088/18

 

Error compiling the Automation Tool after updating Visual Studio today (Unreal 5.3.2)

Here is the fix for people who can’t open the github commits: 2 files to fix BgScriptReader.cs & CheckForHacks.cs Engine/Source/Programs/AutomationTool/BuildGraph/BgScriptReader.cs line 1640, replace report.NotifyUsers.UnionWith(users); with if (users !=

forums.unrealengine.com

이걸 참고했는데, 엔진의 소스코드를 고치는 이해할수 없는 해결 방법이 제시되었고 꽤 많은 사람들이 도움이 됬다고 고마움을 표시하고 있다.

열심히는 아니지만 그래도 몇년간 코딩을 했는데, 엔진의 소스코드를 고치는게 해결법이라니.. 처음보는 일이다.

Engine\Source\Programs\AutomationTool\BuildGraph\BgScriptReader.cs
Engine\Source\Programs\AutomationTool\Scripts\CheckForHacks.cs

당연한거긴 한데 위의 글에서 시키는 대로 경로를 타고 들어가 파일을 열어보면 실제로 문제의 라인에 빨간줄이 쳐져있다.

 


시작 프로젝트를 UE5로 설정했었는데, 이건 저 문제가 발생하고 나서 엔진을 빌드하고 나서 변경했던 사항이다.

그전에도 안됬었는데, 일단 저대로 고친 다음 엔진을 빌드하고 시작프로젝트를 내가 개발하던 프로젝트로 변경하고 컴파일 하니 제대로 실행된다.

 

해결은 됬는데.. 바보가 된 기분이다..

'Error' 카테고리의 다른 글

user32.pdb not loaded  (0) 2024.11.15
C++ LNK2005, LNK2001 에러  (2) 2024.10.07
ModRSsim Key 오류, MSVCR100.dll 오류  (0) 2023.08.28
응용프로그램의 side-by-side  (0) 2023.08.25

댓글()

게임데이터 관리

Unreal 이론|2024. 11. 6. 13:10

[이득우의 언리얼 프로그래밍 Part2 수업의 정리]

 

 

사전 준비

CSV 파일로 게임의 설정 데이터를 저장해 언리얼 엔진에서 Import 한다.

프로젝트 폴더에 GameData 폴더를 만들고, 엑셀 파일을 UTF-8의 csv 파일로 저장

위의 CSV 파일을 사용하기 위해 인덱스 이름과 동일한 이름을 가진 구조체를 생성

-> 소스파일 폴더에서 구조체 헤더파일 작성 -> Generated project file 실행해 헤더파일을 프로젝트에 추가

 

엑셀 데이터의 임포트

  • DataAsset과 유사하게 FTableRowBase를 상속받은 구조체를 선언
  • 엑셀의 Name 컬럼 (얘가 key값이 된다)을 제외한 컬럼과 동일하게 UPROPERTY 속성 선언
  • 엑셀 데이터를 csv로 익스포트 한 후 언리얼 엔진에 임포트

기타->DataTable로 생성 후 Reimport를 클릭해 위에서 만든 Csv 파일을 가져온다

 

데이터를 관리할 싱글톤 클래스의 설정

  • 언리얼 엔진에서 제공하는 싱글톤 클래스
    • 게임 인스턴스
    • 에셋 매니저
    • 게임 플레이 관련 액터 (GameMode, GameState)
    • 프로젝트에 싱글톤으로 등록한 언리얼 오브젝트
  • 언리얼 오브젝트 생성자에서 사용하지 않도록 주의

  • 위의 사진에 싱글톤 클래스를 설정하면 엔진이 초기화 될 때 자동으로 GEngine이라는 전역변수에 싱글톤을 만들어줌
// ABGameSingleton.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "ABCharacterStat.h"
#include "ABGameSingleton.generated.h"

DECLARE_LOG_CATEGORY_EXTERN(LogABGameSingleton, Error, All);

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABGameSingleton : public UObject
{
	GENERATED_BODY()

public:
	UABGameSingleton();
	static UABGameSingleton& Get();

	// Character Stat Data Section
public:
	FORCEINLINE FABCharacterStat GetCharacterStat(int32 InLevel) const { return CharacterStatTable.IsValidIndex(InLevel) ? CharacterStatTable[InLevel] : FABCharacterStat(); }

	UPROPERTY()
	int32 CharacterMaxLevel;


private:
	// Stat Data 내부 보관용 배열 - 추후 필요한 객체들에게 이 배열로 뿌림
	TArray<FABCharacterStat> CharacterStatTable;
	
};


// ABGameSingleton.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "GameData/ABGameSingleton.h"

DEFINE_LOG_CATEGORY(LogABGameSingleton);

UABGameSingleton::UABGameSingleton()
{
	// UDataTable -> (Key, Value)의 한쌍으로 이루어짐
	static ConstructorHelpers::FObjectFinder<UDataTable> DataTableRef(TEXT("/Script/Engine.DataTable'/Game/ArenaBattle/GameData/ABCharacterStatTable.ABCharacterStatTable'"));
	if (nullptr != DataTableRef.Object)
	{
		const UDataTable* DataTable = DataTableRef.Object;
		check(DataTable->GetRowMap().Num() > 0);

		TArray<uint8*> ValueArray;
		DataTable->GetRowMap().GenerateValueArray(ValueArray);
		// Algo의 Transform 알고리즘으로 value값만 가져오도록 설계
		Algo::Transform(ValueArray, CharacterStatTable,
			[](uint8* Value)
			{
				return *reinterpret_cast<FABCharacterStat*>(Value);
			}
		);
	}

	CharacterMaxLevel = CharacterStatTable.Num();
	ensure(CharacterMaxLevel > 0);
}

UABGameSingleton& UABGameSingleton::Get()
{
	// 엔진 초기화시 바로 활성화 되기 때문에 Get 함수로 받을 수 없다면 잘못된것이므로 CastCheck로 형변환 됬는지 강력히 검사
	UABGameSingleton* Singleton = CastChecked<UABGameSingleton>(GEngine->GameSingleton);
	if (Singleton) {
		return *Singleton;
	}
	UE_LOG(LogABGameSingleton, Error, TEXT("Invalid Game Singleton"));

	// 코드 흐름을 위해 작성한것일 뿐 실제 여기까지 가는 일이 일어나지 않도록 앞에서 철저히 검사
	return *NewObject<UABGameSingleton>();
}
  • 싱글톤 클래스의 로직. 싱글톤 클래스가 엔진 초기화시 제대로 생성되었는지 검사하며, 요청한 객체가 잘 쓸수 있도록 생성자에서 DataTable의 value들을 가져온다.

 

프로젝트의 주요 레이어

  • 게임 레이어: 기믹과 NPC
  • 미들웨어 레이어: 캐릭터의 스탯 컴포넌트, 아이템 박스
  • 데이터 레이어: 스탯 데이터, 데이터 관리를 위한 싱글톤 클래스

위에서 아래로만 참조하도록

 

캐릭터 스탯 시스템

  • 위의 구조대로 Stat Component가 Base Stat과 무기의 능력치에 따른 modifier Stat을 합산하여 반영하도록 ABCharacterStatComponent의 구조를 변경, 아래의 함수와 변수를 추가
void SetLevelStat(int32 InNewLevel);
FORCEINLINE float GetCurrentLevel() const { return CurrentLevel; }
FORCEINLINE void SetModifierStat(const FABCharacterStat& InModifierStat) { ModifierStat = InModifierStat; }
FORCEINLINE FABCharacterStat GetTotalStat() const { return BaseStat + ModifierStat; }

UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat)
float CurrentLevel;

UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
FABCharacterStat BaseStat;

UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
FABCharacterStat ModifierStat;
  • 위의 구조 변경에 따라 CharacterBase 파일도 Stat의 값을 참조하도록 적절하게 변경

  • 기믹 클래스에서 스테이지가 상승할때 마다 NPC 레벨이 증가해 더 강해진 NPC를 스폰하도록 설정
  • 이를 위해 스테이지 번호를 지정하고, 이 번호의 증가에 NPC의 레벨을 연동. 이를 위해 CharacterBase에 SetLevel 함수 선언
  • 근데 이렇게 해서 테스트 하다보면 2스테이지부터 MaxHp와 CurrentHp가 안맞는 현상 발생

액터의 생성과 지연 생성의 프로세스

  • 액터의 BeginPlay에서 CurrentHp값과 MaxHp값이 세팅이 되지만, 기믹 함수에서는 이미 SpawnActor를 실행하고 SetLevel 함수가 실행되기 때문에 CurrentHp값이 반영안되는 문제 발생
  • SpawnActor 대신 SpawnActorDeferred 함수를 사용하면 FinishSpawning 함수를 마지막에 호출해 액터의 BeginPlay가 실행된 후 최종적으로 스폰처리가 완료된다
  • 스테이지 스폰과 NPC 스폰, 상자 스폰을 모두 Deferred로 변경해준다.

  • 캐릭터가 무기를 습득했을 때 무기스탯에 따라 최종 스탯을 얻는 구조 추가
  • Weapon 클래스에 ModifierStat 변수를 추가해서, CharacterBase의 equip weapon 함수에서 Stat이 WeaponItemData의 ModifierStat을 얻도록 로직 추가

  • NPC 캐릭터가 스폰될 때 각기 다른 캐릭터로 스폰되도록 설정 추가
  • 프로젝트 폴더의 Config->DefaultArenaBattle.ini 파일 추가.

NPCMeshes라는 TArray가 ABCharacterNonPlayer에 선언되어 있다면 이것의 값을 지정하는 ini 파일

  • AABCharacterNonPlayer에 Config파일을 가져와 메쉬를 로딩하도록 로직 추가
// ABCharacterNonPlayer.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Character/ABCharacterBase.h"
#include "Engine/StreamableManager.h"
#include "ABCharacterNonPlayer.generated.h"

/**
 * 
 */
UCLASS(config = ArenaBattle)
class ARENABATTLE_API AABCharacterNonPlayer : public AABCharacterBase
{
	GENERATED_BODY()

public:
	AABCharacterNonPlayer();

protected:
	virtual void PostInitializeComponents() override;

protected:
	void SetDead() override;
	void NPCMeshLoadCompleted();
	
	// 비동기 방식으로 로드
	UPROPERTY(config)
	TArray<FSoftObjectPath> NPCMeshes;

	TSharedPtr<FStreamableHandle> NPCMeshHandle;
};

// ABCharacterNonPlayer.cpp

#include "Character/ABCharacterNonPlayer.h"
#include "Engine/AssetManager.h"

AABCharacterNonPlayer::AABCharacterNonPlayer()
{
	GetMesh()->SetHiddenInGame(true);
}

void AABCharacterNonPlayer::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	ensure(NPCMeshes.Num() > 0);
	int32 RandIndex = FMath::RandRange(0, NPCMeshes.Num() - 1);
	NPCMeshHandle = UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(NPCMeshes[RandIndex], FStreamableDelegate::CreateUObject(this, &AABCharacterNonPlayer::NPCMeshLoadCompleted));
}

void AABCharacterNonPlayer::NPCMeshLoadCompleted()
{
	if (NPCMeshHandle.IsValid())
	{
		USkeletalMesh* NPCMesh = Cast<USkeletalMesh>(NPCMeshHandle->GetLoadedAsset());
		if (NPCMesh)
		{
			GetMesh()->SetSkeletalMesh(NPCMesh);
			GetMesh()->SetHiddenInGame(false);
		}
	}

	NPCMeshHandle->ReleaseHandle();
}
  • 이후 실행해보면 메쉬 로딩까지 NPC가 숨겨져있다가 랜덤한 캐릭터 메쉬로 잘 로딩되는것 확인 가능

'Unreal 이론' 카테고리의 다른 글

인공지능 - 행동트리 모델의 구현  (0) 2024.11.11
인공지능 - 행동트리 모델의 이해  (2) 2024.11.10
무한 맵의 제작  (0) 2024.10.31
아이템 시스템  (1) 2024.10.30
캐릭터 스탯과 위젯  (1) 2024.10.27

댓글()

무한 맵의 제작

Unreal 이론|2024. 10. 31. 18:14

[이득우의 언리얼 프로그래밍 Part2 수업의 정리]

 

스테이지 기믹 기획

  • 스테이지는 플레이어와 NPC가 1:1로 겨루는 장소
  • 스테이지는 총 4개의 상태를 가지고 있으며 순서대로 진행
    • Ready: 플레이어 입장 처리
    • Fight: 플레이어와 NPC 대전
    • Reward: 플레이어가 보상 선택
    • Next: 다음 스테이지로 이동 처리
  • 무한히 순환하는 구조로 설계

스테이지 준비 단계

  • 스테이지 중앙에 위치한 트리거 볼륨 준비
  • 플레이어가 트리거 볼륨에 진입하면 대전 단계로 이동

스테이지 대전 단계

  • 플레이어가 못 나가게 스테이지의 모든 문 닫고 대전할 NPC 스폰
  • NPC 없어지면 보상 단계로 이동

스테이지 보상 선택 단계

  • 정해진 위치의 4개의 상자에서 아이템 랜덤하게 생성
  • 상자 중 하나를 선택하면 다음 스테이지 단계로 이동

다음 스테이지 선택 단계

  • 스테이지 문 개방
  • 문에 설치된 트리거 볼륨 활용해 통과하는 문에 새로운 스테이지 스폰

스테이지 기믹의 설계와 구현

  • 스테이지에 설치한 트리거 볼륨의 감지 처리
  • 각 문에 설치한 4개의 트리거 볼륨의 감지 처리
  • 상태별로 설정할 문의 회전 설정
  • 대전할 NPC의 스폰 기능
  • 아이템 상자의 스폰 기능
  • NPC의 죽음 감지 기능
  • 아이템 상자의 오버랩 감지

 

에셋 매니저

  • 언리얼 엔진이 제공하는 에셋을 관리하는 싱글톤 클래스
  • 엔진이 초기화될 때 제공되며, 에셋 정보를 요청해 받을 수 있음
  • PrimaryAssetId를 사용해 프로젝트 내 에셋의 주소를 얻어로 수 있음
  • PrimaryAssetId는 태그와 이름의 두가지 키 조합으로 구성되어 있음
  • 특정 태그를 가진 모든 에셋 목록을 가져올 수 있음

 

랜덤 보상 설정

  • 아이템 데이터에 ABItemData라는 에셋 태그 설정
  • 프로젝트 설정에서 해당 에셋들이 담긴 폴더를 지정
  • 전체 에셋 목록 중에서 하나를 랜덤으로 선택하고 이를 로딩해 보상으로 할당

위와 같은 로직의 구현 위해 AActor를 상속받은 ABStageGimmick 클래스 생성

스테이지의 Static Mesh를 열어보면 동서남북 게이트 위치에 소켓 확인 가능

// ABStageGimmick.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ABStageGimmick.generated.h"

UCLASS()
class ARENABATTLE_API AABStageGimmick : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AABStageGimmick();

	// Stage Section
protected:
	UPROPERTY(VisibleAnywhere, Category = Stage, Meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class UStaticMeshComponent> Stage;

	UPROPERTY(VisibleAnywhere, Category = Stage, Meta = (AllowPrivateAccess = "true"))
	TObjectPtr<class UBoxComponent> StageTrigger;

	UFUNCTION()
	void OnStageTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

	// Gate Section
protected:
	UPROPERTY(VisibleAnywhere, Category = Gate, Meta = (AllowPrivateAccess = "true"))
	TMap<FName, TObjectPtr<class UStaticMeshComponent>> Gates;

	UPROPERTY(VisibleAnywhere, Category = Gate, Meta = (AllowPrivateAccess = "true"))
	TArray<TObjectPtr<class UBoxComponent>> GateTriggers;

	UFUNCTION()
	void OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
};



// ABStageGimmick.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "Gimmick/ABStageGimmick.h"
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"
#include "Physics/ABCollision.h"

// Sets default values
AABStageGimmick::AABStageGimmick()
{
 	// Stage Section
    Stage = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Stage"));
    RootComponent = Stage;

    static ConstructorHelpers::FObjectFinder<UStaticMesh> StageMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Stages/SM_SQUARE.SM_SQUARE'"));
    if (StageMeshRef.Object) {
        Stage->SetStaticMesh(StageMeshRef.Object);
    }

    StageTrigger = CreateDefaultSubobject<UBoxComponent>(TEXT("StageTrigger"));
    StageTrigger->SetBoxExtent(FVector(775.0f, 775.0f, 300.0f));
    StageTrigger->SetupAttachment(Stage);
    StageTrigger->SetRelativeLocation(FVector(0.0f, 0.0f, 250.0f));
    StageTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
    StageTrigger->OnComponentBeginOverlap.AddDynamic(this, &AABStageGimmick::OnStageTriggerBeginOverlap);

    // Gate Section
    static FName GateSockets[] = { TEXT("+XGate"), TEXT("-XGate"), TEXT("+YGate"), TEXT("-YGate") };
    static ConstructorHelpers::FObjectFinder<UStaticMesh> GateMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_GATE.SM_GATE'"));
    for (FName GateSocket : GateSockets) {
        UStaticMeshComponent* Gate = CreateDefaultSubobject<UStaticMeshComponent>(GateSocket);
        Gate->SetStaticMesh(GateMeshRef.Object);
        Gate->SetupAttachment(Stage, GateSocket);
        Gate->SetRelativeLocation(FVector(0.0f, -80.5f, 0.0f));
        Gate->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
        Gates.Add(GateSocket, Gate);

        FName TriggerName = *GateSocket.ToString().Append(TEXT("Trigger"));
        UBoxComponent* GateTrigger = CreateDefaultSubobject<UBoxComponent>(TriggerName);
        GateTrigger->SetBoxExtent(FVector(100.0f, 100.0f, 300.0f));
        GateTrigger->SetupAttachment(Stage, GateSocket); // 소켓을 부여해서 좀 더 편리하게 위치 조정
        GateTrigger->SetRelativeLocation(FVector(70.0f, 0.0f, 250.0f));
        GateTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
        GateTrigger->OnComponentBeginOverlap.AddDynamic(this, &AABStageGimmick::OnGateTriggerBeginOverlap);
        GateTrigger->ComponentTags.Add(GateSocket); // 동서남북 중 어느문으로 들어갔는지 알 수 있도록 태그값 부착
    }

}
  • Gate 부분은 4개의 문으로 구성했기에 배열로 이름 지정 및 반복문을 돌면서 동일한 4개의 문 제작

빌드 돌려서 실행한 후, ABStageGimmick 클래스를 상속받은 BP클래스 생성. 4개의 문 트리거와 스테이지 트리거가 잘 생성되있다.

 

스테이지의 상태를 보관하고, 그에따른 동작을 실행할 함수 제작

// ABStageGimmick.h

DECLARE_DELEGATE(FOnStageChangedDelegate);
USTRUCT(BlueprintType)
struct FStageChangedDelegateWrapper {
	GENERATED_BODY()
	FStageChangedDelegateWrapper(){ }
	FStageChangedDelegateWrapper(const FOnStageChangedDelegate& InDelegate) : StageDelegate(InDelegate) {}
	FOnStageChangedDelegate StageDelegate;
};

UENUM(BlueprintType)
enum class EStageState : uint8 {
	READY = 0,
	FIGHT,
	REWARD,
	NEXT
};

	// State Section
protected:
	UPROPERTY(EditAnywhere, Category = Stage, Meta = (AllowPrivateAccess = "true"))
	EStageState CurrentState;

	void SetState(EStageState InNewState); // 상태 변경은 직접 접근 대신 SetState 함수로만 이뤄지도록! (생성자에서만 직접 접근해 초기화)

	UPROPERTY()
	TMap<EStageState, FStageChangedDelegateWrapper> StateChangeActions;

	void SetReady();
	void SetFight();
	void SetChooseReward();
	void SetChooseNext();
    
// ABStageGimmick.cpp
    
AABStageGimmick::AABStageGimmick()
{
    // State Section
    CurrentState = EStageState::READY;
    StateChangeActions.Add(EStageState::READY, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetReady)));
    StateChangeActions.Add(EStageState::FIGHT, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetFight)));
    StateChangeActions.Add(EStageState::REWARD, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetChooseReward)));
    StateChangeActions.Add(EStageState::NEXT, FStageChangedDelegateWrapper(FOnStageChangedDelegate::CreateUObject(this, &AABStageGimmick::SetChooseNext)));
}

void AABStageGimmick::SetState(EStageState InNewState)
{
    // switch문은 state가 늘어나면 복잡해진다.
    // Delegate Array로 함수 포인터 사용해 구현

    CurrentState = InNewState;

    if (StateChangeActions.Contains(InNewState)) {
        StateChangeActions[CurrentState].StageDelegate.ExecuteIfBound();
    }

}
  • State 상태를 지정할 열거형 선언, switch문을 통해 4가지 상태의 분기에 따른 동작을 제어하는 로직을 구성하는 대신, 델리게이트 배열로 함수 포인터를 사용해 가독성을 높인다.
// ABStageGimmick.cpp

void AABStageGimmick::OpenAllGates()
{
    for (auto Gate : Gates) {
        (Gate.Value)->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
    }
}

void AABStageGimmick::CloseAllGates()
{
    for (auto Gate : Gates) {
        (Gate.Value)->SetRelativeRotation(FRotator::ZeroRotator);
    }
}

void AABStageGimmick::SetState(EStageState InNewState)
{
    // switch문은 state가 늘어나면 복잡해진다.
    // Delegate Array로 함수 포인터 사용해 구현

    CurrentState = InNewState;

    if (StateChangeActions.Contains(InNewState)) {
        StateChangeActions[CurrentState].StageDelegate.ExecuteIfBound();
    }

}

void AABStageGimmick::SetReady()
{
    // 스테이지 트리거만 활성화
    StageTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
    for (auto GateTrigger : GateTriggers) {
        GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
    }

    CloseAllGates();
}

void AABStageGimmick::SetFight()
{
    // 모든 트리거 비활성화
    StageTrigger->SetCollisionProfileName(TEXT("NoCollision"));
    for (auto GateTrigger : GateTriggers) {
        GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
    }

    CloseAllGates();
}

void AABStageGimmick::SetChooseReward()
{
    // 모든 트리거 비활성화
    StageTrigger->SetCollisionProfileName(TEXT("NoCollision"));
    for (auto GateTrigger : GateTriggers) {
        GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
    }

    CloseAllGates();
}

void AABStageGimmick::SetChooseNext()
{
    // 문 트리거만 활성화
    StageTrigger->SetCollisionProfileName(TEXT("NoCollision"));
    for (auto GateTrigger : GateTriggers) {
        GateTrigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
    }

    OpenAllGates();
}
  • 문의 열고닫힘을 제어하는 함수 추가, 4가지 상태에 따른 트리거 활성 및 비활성화 로직 추가

 

이렇게 함수를 구현해놓고, 에디터에서 테스트 할 때 AB_StageGimmick의 Detail 패널에서 값을 변경한다고 해서 바로 시뮬레이션 할 수는 없다.

OnConstruction 함수를 상속받아 구현함으로써 State값 변화에 따른 Trigger의 발동을 쉽게 테스트 가능

void AABStageGimmick::OnConstruction(const FTransform& Transform)
{
    // Transform뿐만 아니라 속성이 변경될때 호출됨
    Super::OnConstruction(Transform);

    SetState(CurrentState);
}

문이 열리는 유일한 상태인 NEXT 입력하자 문이 열린다

// ABStageGimmick.h
    
    // Fight Section
protected:
	// 언리얼에서 제공하는 TSubclassOf 템플릿 사용. 지정한 클래스로부터 상속받은 클래스 목록만 표시되도록 한정해서 지정 가능
	// NPC 구현시 블루프린트로 확장해 다양한 캐릭터 만들 때, 모든 액터의 목록 조사하지 않도록 클래스 정보 한정시킨다.
	UPROPERTY(EditAnywhere, Category = Fight, Meta = (AllowPrivateAccess = "true"))
	TSubclassOf<class AABCharacterNonPlayer> OpponentClass;

	UPROPERTY(EditAnywhere, Category = Fight, Meta = (AllowPrivateAccess = "true"))
	float OpponentSpawnTime; // NPC가 바로 스폰되지 않도록 Delay 걸어줄 변수

	// NPC 죽었을 때 보상단계로 진입시키기 위한 함수
	UFUNCTION()
	void OnOpponentDestroyed(AActor* DestroydActor);

	FTimerHandle OpponentTimerHandle;
	void OnOpponentSpawn();


// ABStageGimmick.cpp

void AABStageGimmick::OnStageTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    SetState(EStageState::FIGHT);
}

void AABStageGimmick::OnGateTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    check(OverlappedComponent->ComponentTags.Num() == 1); // GateSockets 이름으로 달아준 OverlappedComponent의 태그 검증
    FName ComponentTag = OverlappedComponent->ComponentTags[0];
    FName SocketName = FName(*ComponentTag.ToString().Left(2)); // 태그를 받아서 왼쪽 두개만 잘라낸다 (+X, +Y 등)
    check(Stage->DoesSocketExist(SocketName));

    FVector NewLocation = Stage->GetSocketLocation(SocketName); // 소켓 위치를 태그 이름을 통해 얻음
    TArray<FOverlapResult> OverlapResults;
    FCollisionQueryParams CollisionQueryParam(SCENE_QUERY_STAT(GateTrigger), false, this); // this -> 자신을 제외한 상태에서 검사
    // OverlapMultiByObjectType 함수를 사용해 해당 위치에 무언가 배치되어 있는지 검사
    bool bResult = GetWorld()->OverlapMultiByObjectType(
        OverlapResults,
        NewLocation,
        FQuat::Identity,
        FCollisionObjectQueryParams::InitType::AllStaticObjects, // 모든 스태틱 오브젝트 타입에 대해 
        FCollisionShape::MakeSphere(775.0f), // 해당 위치에 큰 구체 생성
        CollisionQueryParam
    );

    if (!bResult) { // 검사한 위치에 아무것도 없을 시
        GetWorld()->SpawnActor<AABStageGimmick>(NewLocation, FRotator::ZeroRotator);
    }
}

void AABStageGimmick::OnOpponentDestroyed(AActor* DestroydActor)
{
    SetState(EStageState::REWARD);
}

void AABStageGimmick::OnOpponentSpawn()
{
    const FVector SpawnLocation = GetActorLocation() + FVector::UpVector * 88.0f;
    AActor* OpponentActor = GetWorld()->SpawnActor(OpponentClass, &SpawnLocation, &FRotator::ZeroRotator);
    AABCharacterNonPlayer* ABOpponentCharacter = Cast<AABCharacterNonPlayer>(OpponentActor);
    if (ABOpponentCharacter) {
        ABOpponentCharacter->OnDestroyed.AddDynamic(this, &AABStageGimmick::OnOpponentDestroyed);
    }
}

 

  • 플레이어와 싸울 NPC를 ABCharacterNonPlayer를 상속받아 구현, NPC의 스폰 로직 및 전투 후 State 변경 함수 구현
  • Gate 트리거 함수를 작성. 문을 통과하는 시점에서 전방 구역을 검사해 아무것도 배치되있지 않다면 맵을 생성
// ABStageGimmick.cpp

AABStageGimmick::AABStageGimmick()
{
 	// Fight Section
    OpponentSpawnTime = 2.0f;
    OpponentClass = AABCharacterNonPlayer::StaticClass();
}

void AABStageGimmick::SetFight()
{
    // 모든 트리거 비활성화
    StageTrigger->SetCollisionProfileName(TEXT("NoCollision"));
    for (auto GateTrigger : GateTriggers) {
        GateTrigger->SetCollisionProfileName(TEXT("NoCollision"));
    }

    CloseAllGates();

    GetWorld()->GetTimerManager().SetTimer(OpponentTimerHandle, this, &AABStageGimmick::OnOpponentSpawn, OpponentSpawnTime, false);
}

 

  • Stage에 들어와 Trigger가 발동되면 2초 후 NPC가 스폰되도록 코드 수정
  • 소환된 NPC가 죽고 Destroy 함수가 발동된 후 Gimmick의 State를 보면 REWARD로 변경된 것 확인 가능

 

// ABStageGimmick.h

// Reward Section
protected:
	UPROPERTY(VisibleAnywhere, Category = Reward, Meta = (AllowPrivateAccess = "true"))
	TSubclassOf<class AABItemBox> RewardBoxClass;

	UPROPERTY(VisibleAnywhere, Category = Reward, Meta = (AllowPrivateAccess = "true"))
	TArray<TWeakObjectPtr<class AABItemBox>> RewardBoxes;

	TMap<FName, FVector> RewardBoxLocations;

	UFUNCTION()
	void OnRewardTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

	void SpawnRewardBoxes();
    
// ABStageGimmick.cpp

void AABStageGimmick::OnRewardTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    for (const auto& RewardBox : RewardBoxes) {
        if (RewardBox.IsValid()) {
            AABItemBox* ValidItemBox = RewardBox.Get();
            AActor* OverlappedBox = OverlappedComponent->GetOwner();
            if (OverlappedBox != ValidItemBox) {
                ValidItemBox->Destroy();
            }
        }
    }

    SetState(EStageState::NEXT);
}

void AABStageGimmick::SpawnRewardBoxes()
{
    for (const auto& RewardBoxLocation : RewardBoxLocations) {
        FVector WorldSpawnLocation = GetActorLocation() + RewardBoxLocation.Value + FVector(0.0f, 0.0f, 30.0f);
        AActor* ItemActor = GetWorld()->SpawnActor(RewardBoxClass, &WorldSpawnLocation, &FRotator::ZeroRotator);
        AABItemBox* RewardBoxActor = Cast<AABItemBox>(ItemActor);
        if (RewardBoxActor) {
            RewardBoxActor->Tags.Add(RewardBoxLocation.Key);
            RewardBoxActor->GetTrigger()->OnComponentBeginOverlap.AddDynamic(this, &AABStageGimmick::OnRewardTriggerBeginOverlap);
            RewardBoxes.Add(RewardBoxActor);
        }
    }
}
  • Reward 로직의 함수를 구현
  • TWeakObjectPtr 템플릿 클래스를 사용해 약참조로 RewardBoxes 선언. 액터와 무관하게 내부 로직에서 스폰하거나 파괴될 수 있기 때문에 강참조로 걸게되면 메모리에서 소멸되지 않을 수 있음.
  • 생성된 상자의 Overlap 이벤트 선언, 상자 스폰 함수 선언
// ABItemBox.h

public:
	// Sets default values for this actor's properties
	AABItemBox();

	FORCEINLINE class UBoxComponent* GetTrigger() { return Trigger; }

protected:
	UPROPERTY(VisibleAnywhere, Category = Box)
	TObjectPtr<class UBoxComponent> Trigger; // Loot Component
  • ItemBox의 Trigger는 protected로 선언되있기 때문에 외부에서 접근하도록 Inline 함수 선언

 


상자에 보상 추가

  • 에셋 매니저라는 싱글톤 클래스를 사용해 ABItemData 관리
  • 이 에셋 매니저에서 모든 에셋 불러오도록 설정

 

// ABItemData.h

public:
	FPrimaryAssetId GetPrimaryAssetId() const override {
		// 생성될 에셋의 아이디 직접 지정
		return FPrimaryAssetId("ABItemData", GetFName());
	}
// ABItemBox.h

protected:
	// 아이템 박스가 초기화 됬을 때 에셋 매니저가 제공하고 있는 목록 살펴서 랜덤으로 하나 선택해 할당
	virtual void PostInitializeComponents() override; // 액터 세팅 마무리 시점에 호출되는 함수
    
void AABItemBox::PostInitializeComponents()
{
	// 엔진이 초기화될때 언제나 로딩된다는 보장O
	Super::PostInitializeComponents();

	UAssetManager& Manager = UAssetManager::Get();

	TArray<FPrimaryAssetId> Assets;
	Manager.GetPrimaryAssetIdList(TEXT("ABItemData"), Assets);
	ensure(0 < Assets.Num());

	int32 RandomIndex = FMath::RandRange(0, Assets.Num() - 1); // 전체 개수 중 하나 랜덤
	FSoftObjectPtr AssetPtr(Manager.GetPrimaryAssetPath(Assets[RandomIndex]));
	if (AssetPtr.IsPending()) { // 약참조로 선언했기 때문에 로딩 되있지 않다면 로딩시킨다
		AssetPtr.LoadSynchronous();
	}
	Item = Cast<UABItemData>(AssetPtr.Get());
	ensure(Item);
}
  • ABItemData.h에서 UPrimaryDataAsset의 GetPrimaryAssetId() 상속받아 구현
  • ABItemBox에 에셋 매니저에서 에셋을 가져와 랜덤으로 Item 변수에 형변환으로 집어넣어주는 로직 구현
  • 이후 ItemData들을 Reload 한번 해주고 실행해보면 랜덤하게 보상이 잘 들어가있다.

'Unreal 이론' 카테고리의 다른 글

인공지능 - 행동트리 모델의 이해  (2) 2024.11.10
게임데이터 관리  (0) 2024.11.06
아이템 시스템  (1) 2024.10.30
캐릭터 스탯과 위젯  (1) 2024.10.27
캐릭터 콤보 액션  (0) 2024.10.22

댓글()

아이템 시스템

Unreal 이론|2024. 10. 30. 16:21

[이득우의 언리얼 프로그래밍 Part2 수업의 정리]

 

트리거 박스의 구현

  • 루트에 트리거를 설정하고 자식에 메시 컴포넌트 부착
  • 이펙트는 기본 값으로 비활성화 상태로 두고 오버랩 이벤트 발생시 발동되도록 설정
  • 이펙트 종료시 액터가 제거되도록 설정
// ABItemBox.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ABItemBox.generated.h"

UCLASS()
class ARENABATTLE_API AABItemBox : public AActor
{
	GENERATED_BODY()

public:
	// Sets default values for this actor's properties
	AABItemBox();

protected:
	UPROPERTY(VisibleAnywhere, Category = Box)
	TObjectPtr<class UBoxComponent> Trigger; // Loot Component

	UPROPERTY(VisibleAnywhere, Category = Box)
	TObjectPtr<class UStaticMeshComponent> Mesh;

	UPROPERTY(VisibleAnywhere, Category = Effect)
	TObjectPtr<class UParticleSystemComponent> Effect;

	// 캐릭터가 박스를 밟았을 때 이펙트
	UFUNCTION() // Trigger에 원래 있는 델리게이트에 연결할 함수
	void OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult);

	UFUNCTION()
	void OnEffectFinished(class UParticleSystemComponent* ParticleSystem);
};
  • 헤더에 먼저 Loot Component인 Trigger를 선언하고, Mesh와 Effect도 선언해준다.
  • BoxComponent에 존재하는 델리게이트 OnComponentBeginOverlap과 PatricleSystemComponent의 델리게이트 OnSystemFinished에 연결할 함수를 각각 선언해준다.
// ABItemBox.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "Item/ABItemBox.h"
#include "Components/BoxComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Particles/ParticleSystemComponent.h"
#include "Physics/ABCollision.h"

// Sets default values
AABItemBox::AABItemBox()
{
	// CreateDefaultSubobject 함수로 객체 생성
	Trigger = CreateDefaultSubobject<UBoxComponent>(TEXT("TriggerBox"));
	Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
	Effect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("Effect"));

	// 액터 내부에 Scene Setup
	RootComponent = Trigger;
	Mesh->SetupAttachment(Trigger);
	Effect->SetupAttachment(Trigger);

	Trigger->SetCollisionProfileName(CPROFILE_ABTRIGGER);
	Trigger->SetBoxExtent(FVector(40.0f, 42.0f, 30.0f));
	Trigger->OnComponentBeginOverlap.AddDynamic(this, &AABItemBox::OnOverlapBegin);


	static ConstructorHelpers::FObjectFinder<UStaticMesh> BoxMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/ArenaBattle/Environment/Props/SM_Env_Breakables_Box1.SM_Env_Breakables_Box1'"));
	if (BoxMeshRef.Object) {
		Mesh->SetStaticMesh(BoxMeshRef.Object);
	}
	Mesh->SetRelativeLocation(FVector(0.0f, -3.5f, -30.0f));
	Mesh->SetCollisionProfileName(TEXT("NoCollision"));

	static ConstructorHelpers::FObjectFinder<UParticleSystem> EffectRef(TEXT("/Script/Engine.ParticleSystem'/Game/ArenaBattle/Effect/P_TreasureChest_Open_Mesh.P_TreasureChest_Open_Mesh'"));
	if (EffectRef.Object) {
		Effect->SetTemplate(EffectRef.Object);
		Effect->bAutoActivate = false;
	}

}

void AABItemBox::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
	Effect->Activate(true);
	Mesh->SetHiddenInGame(true);
	SetActorEnableCollision(false);

	// Effect 끝난 뒤 델리게이트 사용
	Effect->OnSystemFinished.AddDynamic(this, &AABItemBox::OnEffectFinished);
}

void AABItemBox::OnEffectFinished(UParticleSystemComponent* ParticleSystem)
{
	Destroy();
}

 

  • 액터의 RootComponent를 Trigger로 설정하고, Trigger에 Mesh와 Effect를 Setup해준다.
  • Trigger의 충돌은 CPROFILE_ABTRIGGER로 ABCollision.h에 선언했던걸 사용
  • OnComponentBeginOverlap 델리게이트는 컴포넌트간에 겹칠때 호출되는 이벤트. 워프포인트, 캐릭터의 히트판정시 데미지 구현 등등 많이 쓴다고 하니 잘 기억해두자.
  • OnComponentBeginOverlap에 바인딩한 OnOverlapBegin 함수에서는 Effect를 true로 설정해 재생하고, 발동한 상자 메시를 지우고 Collision을 없앤다. Effect 끝난 뒤 OnSystemFinished 델리게이트를 다시 바인딩
  • OnSystemFinished 델리게이트가 실행되면 최종적으로 OnEffectFinished 함수에서 이펙트를 없앤다.

이 아이템 상자에 아이템 정보를 넣어 무기를 습득하도록 구현

// ABItemData.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "ABItemData.generated.h"

/**
 * 
 */

UENUM(BlueprintType)
enum class EItemType : uint8 {
	Weapon = 0,
	Potion,
	Scroll
};

UCLASS()
class ARENABATTLE_API UABItemData : public UPrimaryDataAsset
{
	GENERATED_BODY()
	
public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Type)
	EItemType Type;
};
  • 아이템 데이터를 에셋으로 관리할 수 있도록 PrimaryDataAsset 상속받은 ItemData 클래스 생성
// ABWeaponItemData.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Item/ABItemData.h"
#include "ABWeaponItemData.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABWeaponItemData : public UABItemData
{
	GENERATED_BODY()
	
public:
	UPROPERTY(EditAnywhere, Category = Weapon)
	TObjectPtr<USkeletalMesh> WeaponMesh;
};
  • ItemData를 상속받은 WeaponItemData 클래스 생성해 확장 구현
  • 별도로 프로젝트 폴더에 Item 클래스들을 관리하기 위한 폴더 생성, 각각 DataAsset 생성

 

프로젝트에서 사용할 아이템 에셋

  • 총 3가지 종류의 아이템 타입 지정
  • 무기 타입: 캐릭터에 무기 부착(무기에 의한 부가 스탯 강화)
  • 포션 타입: 캐릭터의 HP 회복
  • 스크롤 타입: 캐릭터의 기본 스탯 상승
  • 실제 스탯 구현은 차후 강좌

프로젝트의 주요 레이어

  • 데이터 레이어: 게임을 구성하는 기본 데이터 (스탯 정보, 캐릭터 레벨 테이블 등)
  • 미들웨어 레이어: 게임에 사용되는 미들웨어 모듈 (UI, 아이템, 애니메이션, AI 등)
  • 게임 레이어: 게임 로직을 구체적으로 구현하는데 사용 (캐릭터, 게임모드 등)
  • 위에서 아래로는 직접 참조하되, 아래에서 위로는 인터페이스를 통해 접근하도록 설정
  • ex) 아이템을 먹어 캐릭터가 행동을 수행하도록 아래에서 위로 명령을 내리는 상황 -> 인터페이스 사용

 

  • ABCharacterItemInterface 클래스를 생성해 TakeItem 가상함수 생성
// ABCharacterItemInterface.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "ABCharacterItemInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UABCharacterItemInterface : public UInterface
{
	GENERATED_BODY()
};

/**
 * 
 */
class ARENABATTLE_API IABCharacterItemInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	virtual void TakeItem(class UABItemData* InItemData) = 0;
};
  • ItemBox 클래스에 Item 언리얼 오브젝트 포인터 변수 추가
  • OnOverlapBegin 함수에 아이템 습득 추가
void AABItemBox::OnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
	if (nullptr == Item) {
		Destroy();
		return;
	}

	IABCharacterItemInterface* OverlappingPawn = Cast<IABCharacterItemInterface>(OtherActor);
	if (OverlappingPawn) {
		OverlappingPawn->TakeItem(Item);
	}

	Effect->Activate(true);
	Mesh->SetHiddenInGame(true);
	SetActorEnableCollision(false);

	// Effect 끝난 뒤 델리게이트 사용
	Effect->OnSystemFinished.AddDynamic(this, &AABItemBox::OnEffectFinished);
}
  • CharacterBase 클래스에 TakeItem 가상함수 구현 및 아이템 종류별 행동 구현할 Delegate 및 구조체 선언, Delegate에 바인딩 할 가상함수 3개 선언
DECLARE_DELEGATE_OneParam(FOnTakeItemDelegate, class UABItemData* /*InItemData*/);
USTRUCT(BlueprintType)
struct FTakeItemDelegateWrapper {
	GENERATED_BODY()
	FTakeItemDelegateWrapper() {}
	FTakeItemDelegateWrapper(const FOnTakeItemDelegate& InItemDelegate) : ItemDelegate(InItemDelegate) {}

	FOnTakeItemDelegate ItemDelegate;
};

protected:
	UPROPERTY()
	TArray<FTakeItemDelegateWrapper> TakeItemActions;

	virtual void TakeItem(class UABItemData* InItemData) override;

	virtual void DrinkPotion(class UABItemData* InItemData);
	virtual void EquipWeapon(class UABItemData* InItemData);
	virtual void ReadScroll(class UABItemData* InItemData);
  •  cpp 파일에서 구체적인 동작 구현
AABCharacterBase::AABCharacterBase()
{
	// ItemActions
	TakeItemActions.Add(FTakeItemDelegateWrapper(FOnTakeItemDelegate::CreateUObject(this, &AABCharacterBase::EquipWeapon)));
	TakeItemActions.Add(FTakeItemDelegateWrapper(FOnTakeItemDelegate::CreateUObject(this, &AABCharacterBase::DrinkPotion)));
	TakeItemActions.Add(FTakeItemDelegateWrapper(FOnTakeItemDelegate::CreateUObject(this, &AABCharacterBase::ReadScroll)));
}

void AABCharacterBase::TakeItem(UABItemData* InItemData)
{
	// Item 종류 따라 캐릭터의 다른 동작 구현
	if (InItemData) {
		TakeItemActions[(uint8)InItemData->Type].ItemDelegate.ExecuteIfBound(InItemData);
	}
}

void AABCharacterBase::DrinkPotion(UABItemData* InItemData)
{
	UE_LOG(LogABCharacter, Log, TEXT("Drink Potion"));
}

void AABCharacterBase::EquipWeapon(UABItemData* InItemData)
{
	UE_LOG(LogABCharacter, Log, TEXT("Equip Weapon"));
}

void AABCharacterBase::ReadScroll(UABItemData* InItemData)
{
	UE_LOG(LogABCharacter, Log, TEXT("Read Scroll"));
}
  • 생성자에 무기 스켈레탈 메시 부여, EquipWeapon 함수에서 적용
AABCharacterBase::AABCharacterBase()
{	
    // Weapon Component
	Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Weapon"));
	// 그냥 트랜스폼 부여x, 캐릭터의 특정 본에 무기가 항상 부착되도록 소켓 이름 지정
	// hand_rSocket은 Infinity Warrior Character에 원래 존재하는 소켓 명
	Weapon->SetupAttachment(GetMesh(), TEXT("hand_rSocket"));
}

void AABCharacterBase::EquipWeapon(UABItemData* InItemData)
{
	UABWeaponItemData* WeaponItemData = Cast<UABWeaponItemData>(InItemData);
	if (WeaponItemData) {
		Weapon->SetSkeletalMesh(WeaponItemData->WeaponMesh);
	}
	UE_LOG(LogABCharacter, Log, TEXT("Equip Weapon"));
}

 

소프트 레퍼런싱 vs 하드 레퍼런싱

  • 액터 로딩시 TObjectPtr로 선언한 언리얼 오브젝트도 따라서 메모리에 로딩됨
  • 이를 하드 레퍼런싱이라 함
  • 게임 진행에 필수적인 언리얼 오브젝트는 이렇게 선언 가능. 근데 아이템은?
  • 데이터 라이브러리에 1000종의 아이템 목록이 있으면 이걸 다 로딩할것인지? -> 메모리 부담 1000배
  • 필요한 데이터만 로딩하도록 TSoftObjectPtr로 선언하고 대신 에셋 주소 문자열 지정
  • 필요시 에셋 로딩하도록 구현 변경할 수 있으나 에셋 로딩 시간 소요됨
  • 현재 게임에서 로딩되어 있는 스켈레탈 메시 목록 살펴보기 -> 콘솔 명령어 [Obj List Class=SkeletalMesh]
  • 그럼 WeaponMesh를 SoftObjectPtr로 바꿔서 확인
void AABCharacterBase::EquipWeapon(UABItemData* InItemData)
{
	UABWeaponItemData* WeaponItemData = Cast<UABWeaponItemData>(InItemData);
	if (WeaponItemData) {
		if (WeaponItemData->WeaponMesh.IsPending()) {
			WeaponItemData->WeaponMesh.LoadSynchronous();
		}
		Weapon->SetSkeletalMesh(WeaponItemData->WeaponMesh.Get());
	}
	UE_LOG(LogABCharacter, Log, TEXT("Equip Weapon"));
}
  • 이렇게 바꾸면 처음 엔진 로딩시에는 스켈레탈 메시가 로딩되지 않고, 아이템을 먹었을 때 로딩된다.
  • 언리얼 엔진 최적화의 중요한 요소 [초기의 게임이 로드될 때 메모리 양 최소화]

'Unreal 이론' 카테고리의 다른 글

게임데이터 관리  (0) 2024.11.06
무한 맵의 제작  (0) 2024.10.31
캐릭터 스탯과 위젯  (1) 2024.10.27
캐릭터 콤보 액션  (0) 2024.10.22
캐릭터 애니메이션 설정  (0) 2024.10.21

댓글()

캐릭터 스탯과 위젯

Unreal 이론|2024. 10. 27. 11:17

[이득우의 언리얼 프로그래밍 Part2 수업의 정리]

 

액터 컴포넌트를 활용한 스탯의 설계

  • 액터에 부착할 수 있는 컴포넌트 중 트랜스폼이 없는 컴포넌트
  • 액터의 기능을 확장할 때 컴포넌트로 분리해 모듈화할 수 있음
  • 스탯 데이터를 담당하는 컴포넌트와 UI 위젯을 담당하는 컴포넌트로 분리
  • 액터는 두 컴포넌트가 서로 통신하도록 중개하는 역할로 지정

 

언리얼 델리게이트를 활용한 발행 구독 모델의 구현

  • 푸시(Push) 형태의 알림(Notification)을 구현하는데 적합한 디자인 패턴
  • 스탯이 변경되면 델리게이트에 연결된 컴포넌트에 알림을 보내 데이터를 갱신
  • 스탯 컴포넌트와 UI 컴포넌트 사이의 느슨한 결합 생성
  • 델리게이트를 이용해 서로의 존재를 모른 채 자기일만 하는 컴포넌트끼리 데이터를 주고받게 함

컴포넌트의 느슨한 결합

 

HPBar 생성

  • Widget Blueprint에서 Vertical Box에 Progressbar를 자식으로 추가
  • Fill 옵션 선택해 채우고, 적당히 크기 조절. 어차피 외부에서 크기를 조절한다.
  • UserWidget을 상속받은 ABHpBarWidget C++ 클래스를 만들어 준 후, WBP HpBar의 Parent Class를 C++ 클래스로 바꿔준다

BP 그래프를 쓰지않고 C++ 클래스에서 로직을 구성

  • ABCharacterStatComponent에서 Hp의 변경에 따른 로직들을 구현한다.
// ABCharacterStatComponent.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "ABCharacterStatComponent.generated.h"

DECLARE_MULTICAST_DELEGATE(FOnHpZeroDelegate);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnHpChangedDelegate, float /*CurrentHp*/);

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ARENABATTLE_API UABCharacterStatComponent : public UActorComponent
{
	GENERATED_BODY()

public:	
	// Sets default values for this component's properties
	UABCharacterStatComponent();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

	// Tick 사용하지 않고 필요할때만 함수를 호출해 부하를 최대한 줄인다.
public:
	FOnHpZeroDelegate OnHpZero;
	FOnHpChangedDelegate OnHpChanged;

	FORCEINLINE float GetMaxHp() { return MaxHp; }
	FORCEINLINE float GetCurrentHp() { return CurrentHp; }
	float ApplyDamage(float InDamage);

protected:
	void SetHp(float NewHp);

	UPROPERTY(VisibleInstanceOnly, Category = Stat) // 배치된 캐릭터들 마다 각각 다른값을 사용하게 만듦
	float MaxHp;

	UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat) // 현재 Hp는 게임을 할 때 마다 새롭게 지정이 되므로 Transient 키워드 사용해 디스크에 저장x
	float CurrentHp;

		
};

// ABCharacterStatComponent.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "CharacterStat/ABCharacterStatComponent.h"

// Sets default values for this component's properties
UABCharacterStatComponent::UABCharacterStatComponent()
{
	MaxHp = 200.0f;
	SetHp(MaxHp);
}


// Called when the game starts
void UABCharacterStatComponent::BeginPlay()
{
	Super::BeginPlay();

	SetHp(MaxHp);
	
}

float UABCharacterStatComponent::ApplyDamage(float InDamage)
{
	const float PrevHp = CurrentHp;
	const float ActualDamage = FMath::Clamp<float>(InDamage, 0, InDamage); // 음수가 들어오지 않도록 처리

	SetHp(PrevHp - ActualDamage); 
	if (CurrentHp <= KINDA_SMALL_NUMBER) {
		OnHpZero.Broadcast();
	}

	return ActualDamage;
}

void UABCharacterStatComponent::SetHp(float NewHp)
{
	CurrentHp = FMath::Clamp<float>(NewHp, 0.0f, MaxHp); // 0보다 크고 MaxHp보단 작도록 처리
	
	OnHpChanged.Broadcast(CurrentHp);
}
  • ABHpBarWidget 클래스에서는 HpBar Widget의 동작에 관련된 로직을 추가한다.
  • Components/ProgressBar.h 사용으로 Build.cs에 UMG 모듈을 추가해준다.
// ABHpBarWidget.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ABHpBarWidget.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABHpBarWidget : public UUserWidget
{
	GENERATED_BODY()
	
public:
	UABHpBarWidget(const FObjectInitializer& ObjectInitializer);

protected:
	virtual void NativeConstruct() override; // 위젯 초기화 될 때 HpProgressBar 포인터 가져오는 기능

public:
	FORCEINLINE void SetMaxHp(float NewMaxHp) { MaxHp = NewMaxHp; }
	void UpdateHpBar(float NewCurrentHp);

protected:
	UPROPERTY()
	TObjectPtr<class UProgressBar> HpProgressBar; // ProgressBar 컨트롤에 대한 오브젝트 포인터

	UPROPERTY()
	float MaxHp;
};


// ABHpBarWidget.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "UI/ABHpBarWidget.h"
#include "Components/ProgressBar.h"

UABHpBarWidget::UABHpBarWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
	MaxHp = -1.0f;
}

void UABHpBarWidget::NativeConstruct()
{
	// UI 관련된 기능 거의 다 초기화 된 후 호출된다
	Super::NativeConstruct();

	HpProgressBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("PbHpBar")));
	ensure(HpProgressBar);
}

void UABHpBarWidget::UpdateHpBar(float NewCurrentHp)
{
	ensure(MaxHp > 0.0f);
	if (HpProgressBar) {
		HpProgressBar->SetPercent(NewCurrentHp / MaxHp);
	}
}

 

  • ABCharacterBase 클래스에서 언리얼 엔진이 제공하는 위젯 컴포넌트를 사용해 캐릭터에 부착한다.
// ABCharacterBase.h

	// Stat Section
protected:
	UPROPERTY(VisibleAnyWhere, BlueprintReadOnly, Category = Stat, Meta = (AllowPrivateAcess = "true"))
	TObjectPtr<class UABCharacterStatComponent> Stat;
	// UI widget Section
protected:
	UPROPERTY(VisibleAnyWhere, BlueprintReadOnly, Category = Widget, Meta = (AllowPrivateAcess = "true"))
	TObjectPtr<class UWidgetComponent> HpBar;
    
    

// ABChracterBase.cpp

#include "Engine/DamageEvents.h"
#include "CharacterStat/ABCharacterStatComponent.h"
#include "Components/WidgetComponent.h"

// 생성자에 추가
// Stat Component
Stat = CreateDefaultSubobject<UABCharacterStatComponent>(TEXT("Stat"));

// Widget Component
HpBar = CreateDefaultSubobject<UWidgetComponent>(TEXT("Widget"));
HpBar->SetupAttachment(GetMesh());
HpBar->SetRelativeLocation(FVector(0.0f, 0.0f, 180.0f));
static ConstructorHelpers::FClassFinder<UUserWidget> HpBarWidgetRef(TEXT("/Game/ArenaBattle/UI/WBP_HpBar.WBP_HpBar_C"));
if (HpBarWidgetRef.Class) {
	HpBar->SetWidgetClass(HpBarWidgetRef.Class);
	HpBar->SetWidgetSpace(EWidgetSpace::Screen); // 2D widget Setting
	HpBar->SetDrawSize(FVector2D(150.0f, 15.0f)); // HpBar Size Setting
	HpBar->SetCollisionEnabled(ECollisionEnabled::NoCollision); // UI Collision delete
}

캐릭터 상단에 HpBar가 잘 부착된 모습. 기본적인 세팅이 완료된 형태

 

액터의 라이프 사이클(Life Cycle)

  • 월드에서 액터가 초기화되고 소멸되는 프로세스

좌측: 액터의 로딩, 우측: 액터의 스폰

  • 마지막 BeginPlay직전 PostInitializeComponents 함수 호출 -> 모든 컴포넌트들이 초기화 된 후 실행되는 함수. 이때이후부터 Tick이 발동
  • 액터를 최종적으로 마무리하고싶다 -> PostInitializeComponents 함수 사용
  • 시작할 때 초기화 -> BeginPlay 사용

위젯 컴포넌트와 위젯

  • 위젯 컴포넌트는 액터 위에 UI 위젯을 띄우는 컴포넌트
  • 3차원 모드와 2차원 모드 지원
  • 위젯 컴포넌트는 컨테이너 역할만 할 뿐, 둘은 서로 독립적으로 동작

위젯 컴포넌트의 초기화 과정

  • 발행 구독 모델의 구현을 위해 위젯 컴포넌트들의 초기화 단계를 파악할 필요가 있음
  • UI 관련 컴포넌트는 액터의 BeginPlay 이후에 호출되고 있음
  • 유저 위젯과 스탯 컴포넌트를 연결시켜줄수 있는 적당한 타이밍이 필요
  • 생성시 InitWidget 함수와 NativeConstruct 함수를 호출
  • 차후에 변경될 가능성도 염두

 

위젯 컴포넌트와 위젯의 확장

  • 위젯에 소유한 액터 정보를 보관할 수 있도록 클래스를 확장 (ABUserWidget)
  • 위젯 컴포넌트 초기화 과정에서 이를 설정할 수 있돌고 클래스를 확장 (ABWidgetComponent)
  • 위젯 초기화 단계에서 부모 클래스 정보를 읽고 자신을 등록 (ABCharacterWidgetInterface)

  • 위의 세 클래스를 구현한 후, CharacterBase 클래스에서 느슨한 결합 완성
// CharacterBase.cpp

void AABCharacterBase::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	Stat->OnHpZero.AddUObject(this, &AABCharacterBase::SetDead);
}

void AABCharacterBase::SetupCharacterWidget(UABUserWidget* InUserWidget)
{
	UABHpBarWidget* HpBarWidget = Cast<UABHpBarWidget>(InUserWidget);
	if (HpBarWidget) {
		HpBarWidget->SetMaxHp(Stat->GetMaxHp());
		HpBarWidget->UpdateHpBar(Stat->GetCurrentHp());
		// 두 컴포넌트간 느슨한 결합 완성
		Stat->OnHpChanged.AddUObject(HpBarWidget, &UABHpBarWidget::UpdateHpBar);
	}
}

 

'Unreal 이론' 카테고리의 다른 글

무한 맵의 제작  (0) 2024.10.31
아이템 시스템  (1) 2024.10.30
캐릭터 콤보 액션  (0) 2024.10.22
캐릭터 애니메이션 설정  (0) 2024.10.21
캐릭터 컨트롤 설정  (0) 2024.10.20

댓글()

캐릭터 공격 판정

카테고리 없음|2024. 10. 23. 15:42

[이득우의 언리얼 프로그래밍 Part2 수업의 정리]

 

캐릭터 액션의 충돌 판정

  • 월드가 제공하는 충돌 판정 서비스를 사용
  • 월드는 크게 세 가지의 충돌 판정 서비스를 제공
  • 월드 내 배치된 충돌체와 충돌하는지 파악하고, 충돌한 액터 정보를 얻을 수 있음
    • LineTrace: 지정한 방향으로 선을 투사해 이 선이 어떤 물체와 충돌되는지 파악하는 방법
    • Sweep: LineTrace와 비슷하지만 선 대신 도형을 투사
    • Overlap: 지정한 영역에 큰 범위의 도형을 설정해 해당 볼륨 영역과 물체가 충돌되는지 검사

 

트레이스 채널과 충돌 프로필 생성

  • 액션 판정을 위한 트레이스 채널의 생성: ABAction, 기본 반응은 무시
  • 캐릭터는 두가지 요소 고려
    • 캐릭터 캡슐용 프로필: ABAction 트레이스 채널에 반응, 오브젝트 타입은 Pawn
    • 스켈레탈 메시용 프로필: 랙돌 구현을 위해 주로 활용됨
  • 기믹 트리거용 프로필: 폰 캡슐에만 반응하도록 설정. 오브젝트 타입은 WorldStatic

edit -> project setting -> Collision
Trace Type은 Ignore와 Block만 고려. Object Type은 Overlap까지 세가지 경우 모두 고려
Config -> DefaultEngine.ini에 추가된 모습. 코드 내에선 ECC_GameTraceChannel1 이라는 이름을 사용하는것이 더 편하다
Anim Notify 클래스를 추가하여 몽타주 에셋의 적절한 프레임을 선택, 넣어준다.

 

 

월드 트레이싱 함수의 선택

  • 세가지 카테고리로 원하는 함수 이름을 얻을 수 있음
  • 카테고리 1: 처리 방법
  • 카테고리 2: 대상
    • Test: 무언가 감지되었는지를 테스트
    • Single 또는 AnyTest: 감지된 단일 물체 정보를 반환
    • Multi: 감지된 모든 물체 정보를 배열로 반환
  • 카테고리3: 처리 설정
    • ByChannel: 채널 정보를 사용해 감지
    • ByObjectType: 물체에 지정된 물리 타입 정보를 사용해 감지
    • ByProfile: 프로필 정보를 사용해 감지
  • {처리방법}{대상}{처리설정}

캐릭터 공격 판정의 구현

  • 캐릭터의 위치에서 시선 방향으로 물체가 있는지 감지
  • 작은 구체를 제작하고 시선 방향으로 특정 거리까지만 투사 (Sweep 사용)
  • 하나의 물체만 감지
  • 트레이스 채널을 사용해 감지
  • 그러므로 사용할 함수는 {처리방법}{대상}{처리설정} -> {Sweep}{Single}{ByChannel}

공격 판정 함수를 구현하기 전 Collision 헤더파일 추가

// ABCollision.h

#pragma once

#include "CoreMinimal.h"

#define CPROFILE_ABCAPSULE TEXT("ABCapsule")
#define CPROFILE_ABCAPSULE TEXT("ABTrigger")
#define CCHANNEL_ABACTION ECC_GameTraceChannel1
// ABCharacterBase.cpp

void AABCharacterBase::AttackHitCheck()
{
	FHitResult OutHitResult;
	FCollisionQueryParams Params(SCENE_QUERY_STAT(Attack), false, this);

	const float AttackRange = 40.0f;
	const float AttackRadius = 50.0f;
	const float AttackDamage = 30.0f;
	const FVector Start = GetActorLocation() + GetActorForwardVector() * GetCapsuleComponent()->GetScaledCapsuleRadius();
	const FVector End = Start + GetActorForwardVector() * AttackRange;

	// 결과값 받아오는 FHitResult 구조체 넣어주고, 투사 시작지점, 끝지점, 트레이스 채널 지정, 구체영역 지정, Param
	bool HitDetected = GetWorld()->SweepSingleByChannel(OutHitResult, Start, End, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(AttackRadius), Params);
}

 

물리 충돌 테스트

  • 디버그 드로잉 함수를 사용해 물리 충돌을 시각적으로 테스트
  • 90도로 회전시킨 캡슐 그리기
    • Origin
    • HalfHeight
    • Radius

// ABCharacterBase.cpp - AttackHitCheck 함수의 Debug Draw 함수

#if ENABLE_DRAW_DEBUG
	FVector CapsuleOrigin = Start + (End - Start) * 0.5f;
	float CapsuleHalfHeight = AttackRange * 0.5f;
	FColor DrawColor = HitDetected ? FColor::Green : FColor::Red;

	DrawDebugCapsule(GetWorld(), CapsuleOrigin, CapsuleHalfHeight, AttackRadius, FRotationMatrix::MakeFromZ(GetActorForwardVector()).ToQuat(), DrawColor, false, 5.0f);

#endif

 

이후 NPC클래스를 생성해 ABCharacterBase를 상속받는다.

데드 애니메이션을 할당한 몽타주를 생성하고, Slot을 DeadSlot으로 새로 만들어서 AB_Character에 넣어준다.

그리고 CharacterBase 클래스에 DeadAnimation과 관련된 함수들을 추가해준다

float AABCharacterBase::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

	SetDead();

	return DamageAmount; // 최종적으로 액터가 받은 데미지 값
}

void AABCharacterBase::SetDead()
{
	GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
	PlayDeadAnimation();
	SetActorEnableCollision(false);
}

void AABCharacterBase::PlayDeadAnimation()
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	AnimInstance->StopAllMontages(0.0f);
	AnimInstance->Montage_Play(DeadMontage, 1.0f);
}

AttackHitCheck 함수에도 데미지 발생 이벤트를 마저 작성한다.

if (HitDetected) {
	FDamageEvent DamageEvent;
	OutHitResult.GetActor()->TakeDamage(AttackDamage, DamageEvent, GetController(), this);
}

마지막으로 죽었을 때 액터가 사라지도록 함수를 추가해준다.

// ABCharacterNonPlayer.cpp

void AABCharacterNonPlayer::SetDead()
{
	Super::SetDead();

	FTimerHandle DeadTimerHandle;
	GetWorld()->GetTimerManager().SetTimer(DeadTimerHandle, FTimerDelegate::CreateLambda(
		[&]() {
			Destroy();
		}
	), DeadEventDelayTime, false);
}

이후 빌드해서 확인해보면 히트체크, 애니메이션 재생, 액터 소멸까지 잘 동작하는걸 확인할 수 있다.

댓글()