회사 및 과거 프로젝트가 유추될 수 있는 사항은 제외했습니다.

 


언리얼 리플렉션 시스템 (https://www.unrealengine.com/ko/blog/unreal-property-system-reflection)

  • 프로그램이 실행시간에 자기 자신을 조사할 수 있는 기능. (본인이 어떤 클래스인지 등을 알 수 있음)
  • 언리얼 엔진에서 C++ 클래스, 구조체, 함수, 멤버 변수, 열거형 관련 정보를 수집, 질의, 조작하는 별도의 시스템
  • 리플렉션 시스템에 등록시키고자 하는 변수 등에 UProperty 키워드 등을 부착시 Unreal Header Tool (UHT) 에 의해 리플렉션 시스템에 등록됨
    • UProperty 사용시, 언리얼 GC에 포함이 됨.
    • 각종 메타데이터들을 추가하여 해당 변수가 어떤 속성을 가졌는지를 알 수 있음.

 

 

 

언리얼 최적화 관련

  • 언리얼에서는 자체적인 GC를 사용함.
  • GC는 다음과 같이 작동함 :: 
    Unreachable 플래그를 모든 UObject에 부착 -> RootSet에 걸려있는 UObject에서 플래그 회수 -> 플래그 남은 UObject 기록 -> 리스트업 된 UObject Destroy (Begin/Finish) -> UObject의 소멸자 호출
  • GC의 타겟이 되지 않게 하기 위해서는 관련 오브젝트에 "AddToRoot" 혹은 "SetFlags(RF_MarkAsRootSet)" 플래그를 붙이면 됨.
  • 주로, 인게임에서 이미 소멸된 액터거나 Replicate 범위 밖 (NetCullDistance)에 존재하는 경우 타겟이 될 수 있음.

 

 

 

언리얼 리플리케이션 관련

  • 기본 개념 : 서버-클라간 동기화를 위한 개념
    • 서버->클라로는 호출한 / 소지한 (Autonomous) 클라이언트에게, 혹은 모든 클라이언트에게 전달 (Multicast) 가능
    • 클라->서버로는 클라이언트가 소지한 액터가 Autonomous일때만 서버로 전송가능 (이것도 RPC만 가능)
    • 변수 리플리케이션은 "원칙적으로는" 서버->클라만 가능함.
  • Role
    • Role은 Local Role / Remote Role 2개가 있음.
    • 보통 서버 입장에서 Local Role은 Authority인 경우가 대부분
    • 클라에서는 Remote는 Authority, Local이 다를 수 있음.
    • 비교
      • 클라 입장에서 Autonomous - "현재 클라이언트가 컨트롤 중인 플레이어"
      • 클라 입장에서 Simulated - "현재 클라이언트의 영향 외의 동기화 액터"
  • 기타
    • RPC는 단발성 리플리케이션에 주로 사용, 변수 리플리케이션은 영구적 리플리케이션에서 주로 사용함.

 

 

언리얼 포인터 관련


기본적으로 언리얼 스마트 포인터는 Object 포인터와 일반 포인터 (비 Object 포인터 = C++ 노멀 포인터)가 있음

  • 오브젝트 포인터 (Actor 클래스 등의 언리얼 오브젝트에서 사용. 언리얼 GC 타겟이 된다.)
    • TWeakObjectPtr - 순환참조 이슈 등을 대응하기 위한 오브젝트 포인터. 참조 대상 파괴시 nullptr로 세팅됨.
    • TObjectPtr - 64bit 기반 신규 포인터.
      원시포인터 (Normal Pointer) 와 유사하나, 다이나믹 해상도 (액터에 대한 동적 해상도 전환), 엑세스 트래킹 등의 신규 기능 추가.
      • 5.0 되면서 추가된 신규 포인터임. 원시 포인터를 TObjectPtr로 바꾸는걸 언리얼에서는 권장함.

  • 노멀 포인터
    • TUniquePtr - 일반적인 Unique Pointer와 동일. (하나의 포인터로 단 하나의 메모리만을 참조)
    • TSharedPtr - 일반적인 Shared Pointer와 동일
    • TWeakPtr - 일반적인 Weak Pointer와 동일 (Shared Pointer의 보조)
      • TWeakPtr은 TSharedPtr를 통해서만 복사 생성, 대입 연산이 가능하다.
      • TWeakPtr은 TSharedPtr를 참조하되, 레퍼런스 카운트를 증가시키지 않는다.
      • TWeakPtr을 통해 객체를 참조하려면 반드시 TSharedPtr로 변환하여 사용해야 한다.

 

 

 

* 주의 *

1. 해당 작업은 StandAlone에서만 테스트되었습니다.

2. 해당 작업은 임시 연동을 위해 작업되었으며, 타 환경 (네트워크 환경 등)에서는 정상 작동을 보장하지 않습니다.

3. 필요시, 코드를 적당히 수정해서 사용해주세요!

 

 

(아마 기억이 맞다면) 언리얼 5.3에 들어오면서 큰 변화가 몇가지 생겼다.

그중에 프로그래머 입장에서 유의미하게 볼것이 바로 EnhancedInput의 정식 사용 시작 및 AbilitySystem의 공식 플러그인 화 일것이다.

 

근데 웃긴게, 이 두가지가 하나는 정식기능, 다른 하나는 플러그인이라서 그런가 공식적으로 지원하는 연동 코드가 없다.....

 

물론, 진짜 하나도 없진 않다. 대신, LyraStarterGame에 포함되어있을뿐...

 

LyraStarterGame에서는 해당 기능을 HeroComponent라는 연동 컴포넌트와, EnhancedInput을 위한 LyraInputComponent를 만들어서 사용하는데, 해당 컴포넌트 및 Lyra 식 사용에 대한 분석은 다음에 하기로 하고, 우선은 조금 빠른 사용법을 익혀보도록 하자.

 

 

* 해당 코드의 일부는 EpicGames에서 제공하는 Lyra Starter Game에서 발췌했습니다.


우선, 이전 글을 참고하여 AbilitySystemComponent를 정상적으로 사용할 수 있는 상태를 기준으로 한다.

 

https://locketgoma.tistory.com/80

 

짧막 팁 : AbilitySystem 관련 헤더를 불러오지 못할때

대충 이런 상황이다. AbilitySystemComponent 및 기타 관련 헤더를 사용해야하는데 못불러오는경우... 해당 문제는 모듈이 빠져있는 상황으로, 프로젝트 명칭으로 된 폴더에 있는 "(ProjectName).Build.cs"

locketgoma.tistory.com

 

 

두가지를 연동하려면 우선, InputAction과 GameplayTag를 연동시켜둔 데이터파일이 필요하다.

해당 파일 타입을 Lyra에서는 "FLyraInputAction" 라는 struct를 사용하는 ULyraInputConfig라는 DataAsset를 만들어 사용한다.

 

일단, 해당 방법을 그대로 사용하자.

 

UDataAsset를 상속받는 신규 클래스를 만들고, 해당 파일에

 

USTRUCT(BlueprintType)
struct FInputAction		//이름은 적당히 바꿔서 쓰면 됩니다.
{
	GENERATED_BODY()

public:

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	TObjectPtr<const UInputAction> InputAction = nullptr;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Meta = (Categories = "InputTag"))
	FGameplayTag InputTag;
};

해당 구조체를 추가, uproperty로 추가해주면 된다.

Lyra에서는 (EditDefaultsOnly, BlueprintReadOnly) 조건으로 사용중이나, 적당히 필요한대로 쓰시면 될것같다.

 

 

 

그 다음부터는 선택지가 갈리는데, EnhancedInputComponent 및 PanwComponent를 상속받아서 Lyra와 같이 InputComponent + HeroComponent를 만들어서 사용하거나, 간략하게 AbilitySystemComponent를 상속하여 구현해주는 방법 등이 있다.

 

여기서는 AbilitySystemComponent 을 상속하여 추가 함수를 만들어주는 방식으로 구현하고자 한다.

 

Lyra 방식은... 공부도 하실겸 Lyra 코드를 열어보시는걸 추천드립니다.

 

 

 

첫번째. UAbilitySystemComponent를 상속받은 컴포넌트를 추가한다.

두번째. 위에서 만든 DataAsset Class를 헤더에 추가한다.
(단순히 전방선언으로 사용하면 구조체의 사이즈를 알 수 없어 오류가 발생한다)

세번째. 다음과 같이 (혹은 적당히 수정하여) 헤더를 작성한다.

UCLASS()
class PROJECTCLOUD_API UCLAbilitySystemComponent : public UAbilitySystemComponent
{
	GENERATED_BODY()

public:
	//인풋 액션을 세팅하는 함수 (return bool인 이유는 오류 검출 용도)
	bool BindInputActions(const UCLAbilityInputConfig* InputConfig, UEnhancedInputComponent* EnhancedInputComponent);
	
	//인풋 액션을 호출하는 바인딩 함수
	void TryActiveAbilityFromInputAction(const FInputActionInstance& Value);

private:
	//인풋 액션 리스트
	TMap<TObjectPtr<const UInputAction>, FGameplayTag> InputactionList;
};

네번째. cpp 파일에 필요한 헤더들을 추가한다.
EnhancedInput관련, GameplayTag 관련 헤더를 추가해주면 된다.

다섯번째. 바인딩 코드 작성.

bool UCLAbilitySystemComponent::BindInputActions(const UCLAbilityInputConfig* InputConfig, UEnhancedInputComponent* EnhancedInputComponent)
{
	if (!ensure(EnhancedInputComponent))
	{
		UE_LOG(LogTemp, Error, TEXT("UCLAbilitySystemComponent :: EnhancedInputComponent is Null."));
		return false;
	}

	for (const FCLInputAction Action : InputConfig->AbilityInputActions)
	{
		InputactionList.Add(Action.InputAction, Action.InputTag);

		EnhancedInputComponent->BindAction(Action.InputAction, ETriggerEvent::Triggered, this, &UCLAbilitySystemComponent::TryActiveAbilityFromInputAction);
	}
	return true;
}

 

Input으로 받은 DataAsset에서 리스트를 가져와서 EnhancedInput에 바인드하고, 로직 검사를 위해 ASC에 추가한 Map에 등록해주는 로직이다.

 

여섯번째. 바인드 될 호출함수 작성

void UCLAbilitySystemComponent::TryActiveAbilityFromInputAction(const FInputActionInstance& Value)
{
	//1. 이벤트로 들어온 InputAction과 매치되는 Tag가 있는지 검사
	FGameplayTag* InputTag = InputactionList.Find(Value.GetSourceAction());
	
    //2. 매치되는 Tag가 있다면
	if (InputTag != nullptr)
	{
    	//3. Pay로드 작성 후
		FGameplayEventData TempPayload;
		TempPayload.EventTag = *InputTag;
		TempPayload.Instigator = GetOwner();
        
        //4. ASC에 포함된 HandleGameplaytEvent를 호출한다.
        HandleGameplayEvent(TempPayload.EventTag, &TempPayload);
	}
}

Payload값은 임의로 수정해도 된다. (단, EventTag 제외)

 

 

다음과 같이 작성하면 함수 준비는 끝났다.

 

이를 어떻게 사용하느냐...

 

사실 생각보다 별거 없다. EnhancedInput과 ASC를 사용하고자 한다면, 아마 여러가지 검색을 하면서 Player Pawn에다가 연동하는 작업도 수행했을것이다.

아니었다구요? 그렇다면..

 

	UEnhancedInputComponent* EnhancedInputComponent = GetComponentByClass<UEnhancedInputComponent>();

	if (ACLPlayerState * PS = Cast<ACLPlayerState>(GetPlayerState()))
	{
		PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
		PS->SetAbilitiesFromActionSet(AbilitySet);
		PS->GetAbilitySystemComponent()->BindInputActions(InputConfig, EnhancedInputComponent);
	}

다음과 같은 로직을 Player Pawn의 Beginplay 등의 Init 단계에서 호출하도록 추가해주면 된다.

 

당연한 얘기겠지만, 이미 EnhancedInput을 Player Pawn에 등록했어야 하고,
해당 로직의 경우에는 PlayerState에 ASC를 등록해서 사용했기에, 해당 과정도 거친 상태여야 한다.

(스텐드 얼론이어서 Pawn에 직접 ASC를 붙여서 사용한다면, 코드를 적당히 수정하면 된다.)

 

다음과 같이 등록하고, Player Pawn에 "InputConfig"  변수를 추가해주면 끝.

 

정상적으로 잘 따라왔다면,

 다음과 같은 InputConfig를 만들 수 있고,

해당 InputConfig를 추가할 수 있는 공간이 Player Pawn에 추가된다.

 

이제 자유롭게 InputAction과 InputTag를 부착하면, Player의 ASC에 부착된 GameplayAbility 중 Trigger Event로 호출되는 Ability를 사용할 수 있게 된다.

 

 


ASC 사용법은 저 말고도 다른 분들이 잘 설명해준 문서들이 많으니 참고 바랍니다...
EnhancedInput과 ASC 연동하는 방법은 어디에도 없길래 Lyra 코드 분석해가면서 간략화해서 만들어봤어요

 

틀린 내용에 대한 지적은 언제나 받습니다.

대충 이런 상황이다.

 

AbilitySystemComponent 및 기타 관련 헤더를 사용해야하는데 못불러오는경우...

 

해당 문제는 모듈이 빠져있는 상황으로, 프로젝트 명칭으로 된 폴더에 있는 "(ProjectName).Build.cs" 파일에 모듈을 추가해주면 된다.

 

	PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay" });

Build.cs 파일의 해당 위치에

 

 

	PublicDependencyModuleNames.AddRange(new string[] { "GameplayAbilities", "GameplayTags", "GameplayTasks", "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay" });

다음과 같이 어빌리티 시스템 관련을 추가해주면 됨.

 

 

어빌리티 시스템 관련을 모듈로 추가해준 뒤, 프로젝트 파일을 Re-Generate 해주면 완료.

 

 

 

 

참고 자료 : https://dev.epicgames.com/documentation/en-us/unreal-engine/gameplay-ability-system-for-unreal-engine?application_version=5.0

1. 스프라이트 셋에서 스프라이트 추출

스프라이트 셋을 불러온 뒤, 불러와진 텍스쳐파일에서 우클릭 -> 스프라이트 추출

 

추출 옵션은 적당히 선택하면 되나, 대체로 Grid를 사용하게 될것임..

정확한 너비를 모르겠다면, 셀 수 X,Y를 입력한 뒤 셀 너비 및 높이 옆의 돌아가기 버튼을 누르면 알아서 지정해준다.

https://youtu.be/W4oZq4tn57w?si=SxHKHf56uecrfZk1

 

어쩌다 찾은 자료.

블리자드 오버워치팀에서 17년도에 발표한 자료인데,

네트워크 동기화, 하이라이트, 킬캠 등의 자료를 담고 있다.

 

 

주말중에 여러번 돌려보면서 정리해볼 예정

최근에 다들 이름만 들어도 알 회사에 면접을 볼 일이 있었다.

그 전, 입사 테스트로 단방향 링크드 리스트 뒤집기라는 과제를 받았었는데, 손으로 짜려다가 머리가 고장나버리는 바람에 제대로 완성을 못하고, 머릿속으로만 복기를 했었다가 면접때 해당 문제 다시풀기 라는 초유의 사태를 만나 그대로 면접에서 대형 사고를 치고 말았다..

 

물론 떨어졌고.

 

너무 아쉽긴 하지만 실수해서 망한건 이미 망한거고...

이왕 이렇게 된거, 복기도 좀 하고 다시는 같은 실수를 안하도록 코드 정리를 해서 올려보고자 한다.

 


 

우선, 단방향 리스트는 대충 다음과 같은 느낌으로 구현된다.

 

struct List
{
	List* next;
	int data;

	List(int newData)
	{
		data = newData;
		next = nullptr;
	}
};

 

그리고 이걸 뒤집으려면, 단순히 생각하면 전체를 순회하면서 한바퀴 돌면 그만.

 

 

 

그래서 처음 생각했던 (그리고 면접때까지 결국 제대로 못 구현했던) 방식은 다음과 같다.

 

우선, 링크드리스트가 "1,2,3,4,5"가 순서대로 있다는 가정하에 설명을 이어가도록 하겠다.

List* ReverseList(List* list)
{
	List* Result;
	List* InputList = list;

	//끝부분 인식
	while (true)
	{
		if (InputList->next->next == nullptr)
		{
			Result = InputList->next;
			InputList->next = nullptr;
			Result->next = InputList;

			break;
		}
		InputList = InputList->next;
	}

	//순서 뒤집기
	List* ResultNow = Result->next;
	InputList = list;
	while (InputList->next != nullptr)
	{
		
		while (true)
		{		
			std::cout << InputList->data << std::endl;
			if (InputList->next->next == nullptr)
			{
				ResultNow->next = InputList;
				InputList->next = nullptr;
				ResultNow = ResultNow->next;

				InputList = list;
				break;
			}
			InputList = InputList->next;
		}
	}
	return Result;
}

 

굉장히... 길다.

 

로직을 간단히 설명하면, 먼저 링크드 리스트의 맨 마지막인 5를 찾고,

그 다음부터는 "뒤가 존재하지 않는 노드" 를 뒤집을 리스트의 끝에 집어 넣는 방식이다.

 

즉,

 

처음 While문 종료시

원본 리스트 :  1->2->3->4->5

뒤집은 리스트 : 5->4

뒤집힐 리스트 : 1->2->3->4

 

해당 상태가 되는데,

 

두번째 While문에서부터는 본인 노드의 다음 다음 노드가 비어있을때 본인을 집어넣는 방식으로 이루어진다.

 

그래서, 위와같이 4가 마지막인것이 2개가 존재하는것이 의도적인 작업인데, 이를 면접때 제대로 대답을 하지 못한게 좀 치명적인 실수였다고 생각이 든다...

 

여튼, 두번째 While문에서는 My->Next->Next == nullptr 일때, My를 집어넣는식으로 이루어진다.

 

물론, 당연히 결과는 잘 나온다.

 

하지만, 시간 복잡도 상으로는 O(n+n²) 라는... 좀 느린 수준의 코드고, 좀 어거지로 짠 느낌이 없잖아 있다.

 

 

우선, while 순회를 1개 블럭으로 줄여보자.

 

별 거 없다, 두개 합치면 그만이다.

List* ReverseList(List* list)
{
	List* Result = nullptr;
	List* ResultNow = nullptr;
	List* InputList = list;


	//순서 뒤집기
	InputList = list;
	while (InputList->next != nullptr)
	{		
		while (true)
		{		
			//std::cout << InputList->data << std::endl;
			if (InputList->next->next == nullptr)
			{
				if (Result == nullptr)
				{
					Result = InputList->next;
					InputList->next = nullptr;
					Result->next = InputList;
					ResultNow = Result->next;
				}
				else
				{
					ResultNow->next = InputList;
					InputList->next = nullptr;
					ResultNow = ResultNow->next;

				}
				InputList = list;
				break;
			}
			InputList = InputList->next;
		}
	}
	return Result;
}

어차피 if의 조건은 List->next->next == nullptr 로 동일하기 때문에 두개의 While을 하나로 합칠 수 있다.

 

하지만 이래도 O(n²) 의 시간복잡도가 나온다.

더 줄여보자.

 

: 해당 로직은 다음 블로그를 참고하였습니다 : 

https://seongonion.tistory.com/78

 

단방향 링크드 리스트 뒤집기 - 파이썬 (Python)

링크드 리스트 정리 https://seongonion.tistory.com/20?category=867075 링크드 리스트의 구현 및 연산 - 파이썬(Python) 링크드 리스트의 구현 (Node, __init__, __str__) 링크드 리스트를 구현하기 위해선, 각각의 데

seongonion.tistory.com

 

 

해당 블로그의 내용을 요약하면...

이전 위치, 현재 위치, 다음 위치를 기억하고 뒤집어가면서 진행하기 정도로 요약가능하다.

 

다음 위치를 진행 시키면서, 현재 위치의 다음 방향을 이전 위치로 기록하면서 진행하면 OK.

 

List* ReverseList(List* list)
{
	List* Result = nullptr;
	//순서 뒤집기
	List* prev = nullptr;
	List* temp = list->next;
	List* now = list;

	while (now != nullptr)
	{
		now->next = prev;
		if (temp == nullptr)
		{
			Result = now;
			break;
		}
		prev = now;
		now = temp;
		temp = temp->next;		
	}
	return Result;
}

 

이번에도 잘 나온다.

 

코드도 훨씬 깔끔하고 간결해져서, 가독성 측면에선 이쪽이 훨씬 나아보임.

 

 

 

 

..이라는 복기를 좀만 일찍했으면 좋았을걸~ 싶은 생각이 든다.

'프로그래밍 > C/C++' 카테고리의 다른 글

C++ 스마트 포인터  (0) 2024.09.04
std::sort()  (0) 2024.09.03
"개체에 일치를 방해하는 형식 한정자가 있음" 오류 해결법  (0) 2022.03.31
기록용 : 메모리 누수 찾기  (0) 2021.07.18
짧은 기록  (0) 2021.06.18

재료 :
냉동 칵테일 새우 26/30 300g / 너무 많이 넣었음. 반으로 줄이고, 잘라서 넣는게 나을듯

마늘 한줌 (대충 150g) - 반은 편썰고, 반은 반만 썰어서 넣음

양파 2개 - 하나는 원형, 하나는 깍뚝썰기 / 1개만 넣어도 충분함

양송이 5개 - 4등분

면-데세코 스파게티나 150g 정도

바질페스토 시판 크게 한숟가락 - 너무 많이넣음 찻숟가락 1개 정도면 충분할듯

페퍼론치노 약간 - 취향차이... 

 

 

 

새우는 꼬리 껍질 및 내장 다 빼서 레몬즙 담군 물에 넣고 대충 30분

 

 

올리브유 적당히 잠길만큼만 넣고 마늘 -> 새우 순으로 넣고 만들었는데..

 

 

 

비주얼은 나쁘지 않으나 내 기준에선 새우향이 너무 쌔서 다음에는 새우 자르고, 새우를 더 익혀서 만들거같음

면이 생각보다 빨리 익어서 감바스 알 하이요가 완성될때쯤부터 면 삶는게 맞을듯함

'신변잡기 > 먹부림' 카테고리의 다른 글

오늘자 밥반찬  (0) 2024.11.19

1. 꾸준히 개인 프로젝트 (언리얼 5 / 최신 따라가기) 진행하기

2. 운동 계속하기

3. DX 11 / 12 공부 계속 하기

4. 기회가 된다면 강연같은거 나가보기

+ Recent posts