반응형

언리얼 에서 델리게이트는 C++ 객체에만 사용할 수 있는 델리게이트와 C++, 블루프린트 객체가 모두 사용할 수 있는 델리게이트로 나뉜다. 블루프린트 오브젝트는 멤버 함수에 대한 정보를 저장하고 로딩하는 직렬화 매커니즘이 들어있기 때문에 일반 C++ 언어가 관리하는 방법으로 멤버 함수를 관리 할수 없다. 

그래서 블루프린트와 관련된 C++함수는 모두 UFUNCTION 매크로를 사용해야 한다.
이렇게 블루프린트 객체와도 연동하는 델리게이트를 언리얼 엔진에서는 다이내믹 델리게이트라고 한다.

 

 

 

 


Delegate란?

함수를 바인딩하는 형태로 등록시켜 CallBack함수와 같이 활용 할 수 있습니다.

 

언리얼 C++에서 충돌감지 컴포넌트 개열에서 AddDynamic 메크로를 통해 출돌시 등록한 CallBack함수를 호출하는 것이 구현 되어 있습니다.

 

언리얼 C++에는 총 4가지 종류의 Delegate가 있습니다.

델리게이트(싱글 케스트) 가장 기본적인 Delegate로 함수 1개를 바인드하여 사용합니다.
멀티 케스트 싱글 케스트와 동일하지만 여러 함수를 바인드 할 수 있습니다.
이벤트 멀티 케스트와 동일하지만 전역으로 설정할 수 없어 외부 클래스에서 추가 델리게이트 선언이 불가능합니다.
다이나믹 멀티케스트(다이나믹) 다이나믹은 싱글과, 멀티 두개다 존재하며 다이나믹 델리게이트는 직렬화(Serialize)화 되어 블루프린트에서 사용 가능합니다.

 

바인딩(Bind)란 Delegate에 특정 바인드 함수를 통해 콜백함수를 등록하는 것을 의미합니다.

공식문서에는 여러 형태의 바인드함수가 있지만 이 포스팅에서는 함수 바인딩을 중심적으로 설명하겠습니다.

 

 

 


샘플 프로젝트 소개

샘플 프로젝트는 포스팅 상단에 업로드했습니다.

 

샘플 프로젝트는 폭탄 오브젝트가 하나 있으며 키보드 "B"키는 누르면 점화되어 3초 뒤에 폭발합니다.

프로젝트의 구현 형태는 폭탄 클래스에다 함수들을 바인드시켜놓고 폭탄이 터지면 바인드한 함수들을 호출하여

 

출력 로그에 해당 함수 내에서 로그 코드가 발동되게 구현했습니다.

 

 

 


기본적인 Delegate 세팅법은 먼저 싱글케스트를 예시로 먼저 설명하겠습니다.

 

헤더 파일 설명

//! ABoom.h

#include "Boom.generated.h"

//! SingleCast
DECLARE_DELEGATE(FDele_Single); 
DECLARE_DELEGATE_OneParam(FDele_Single_OneParam, int32);

UCLASS()
class DELEGATETEST_API ABoom : public AActor
{
	GENERATED_BODY()

........

public :
	FDele_Single Fuc_DeleSingle;
	FDele_Single_OneParam Fuc_DeleSingle_OneParam;
};

 

헤더 파일에서 Delegate함수를 만들고 DECLARE_DELEGATE()메크로를 통해 델리게이트화 시킵니다.

 

DECLARE_DELEGATE는 인자 값 없는 함수에 사용하고 DECLARE_DELEGATE_OneParam는 1개의 인자값이 있는 함수에 사용합니다, 만약 인자 값이 2개면 TwoParam을 사용합니다.

 

델리게이트는 자료형 이름 앞에 "F"를 붙여야합니다.

안 그러면 위 에러가 뜨게 됩니다.

 

 

Cpp 파일 설명

//! ABoom.cpp

void ABoom::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason);
	//! 델리게이트 해제
	Fuc_DeleSingle.Unbind();
	Fuc_DeleSingle_OneParam.Unbind();
}

void ABoom::Tick_Boom(float DeltaTime)
{
		//! Delegate호출하는 부분
		if(Fuc_DeleSingle.IsBound()==true)	Fuc_DeleSingle.Execute();
		if(Fuc_DeleSingle_OneParam.IsBound() == true) Fuc_DeleSingle_OneParam.Execute(123);
}

 

EndPlay함수에 Unbind()함수는 일종의 메모리 해제 함수입니다, 해당 델리게이트에 바인드된 함수를 제거합니다.

 

실제 델리게이트에 등록된 Callback 함수를 호출하는 부분입니다.

IsBound()를 통해 바인드가 되어있는지 확인하고 리턴값이 True이면 Execute()함수를 통해 호출합니다.

인자 값이 있는 함수는 Execute함수의 인자 값으로 입력합니다.

 

 

 


Delegate에 함수 등록 & 싱글케스트(Single cast)

이 챕터는 Delegate가 세팅된 ABoom 클래스에 ATestPlayer클래스의 함수를 등록하는 과정입니다.

 

//! ATestPlayer.h

UCLASS()
class DELEGATETEST_API ATestPlayer : public AActor
{
	GENERATED_BODY()
public:	
	//! Delegate에 의해 호출될 함수

	UFUNCTION()
		void CallDeleFunc_Single();
	UFUNCTION()
		void CallDeleFunc_Single_OneParam(int32 nValue);

먼저 ABoom클래스에 등록할 함수는 인자값에 맞춰 함수를 만듭니다.

Delegate에 등록할 함수는 UFUNCTION()메크로를 붙여야 합니다.

 

//! ATestPlayer.cpp

//! Delegate에 의해 호출될 함수
void ATestPlayer::CallDeleFunc_Single()
{
	UE_LOG(LogTemp, Warning, TEXT("CallDeleFunc_Single"));
}

void ATestPlayer::CallDeleFunc_Single_OneParam(int32 nValue)
{
	UE_LOG(LogTemp, Warning, TEXT("CallDeleFunc_Single_OneParam / %d"), nValue);
}

해당 함수에 단순하게 로그를 출력하는 코드를 작성했습니다.

 

//! ATestPlayer.cpp

void ATestPlayer::BeginPlay()
{
	m_pBoom->Fuc_DeleSingle.BindUFunction(this, FName("CallDeleFunc_Single"));
	m_pBoom->Fuc_DeleSingle_OneParam.BindUFunction(this, FName("CallDeleFunc_Single_OneParam"));
}

함수를 바인드하는 코드입니다, ABoom.h에서 만든 Delegate 변수에 접근하여 함수를 바인드하는 함수를 통해 함수를 등록합니다.

 

 

이로서 Delegate의 기본 형태인 싱글케스트 구현이 완료됬으며, 폭탄이 터지면 로그가 출력될 것입니다.

 

 


멀티 케스트(Multi cast)

멀티케스트가 델리게이트(싱글케스트)와 차이점은 함수를 여러개 바인드 할 수 있습니다.

 

//! ABoom.h

//! MultiCast
DECLARE_MULTICAST_DELEGATE(FDele_Multi);
DECLARE_MULTICAST_DELEGATE_OneParam(FDele_Multi_OneParam, int32);

UCLASS()
class DELEGATETEST_API ABoom : public AActor
{
	GENERATED_BODY()
public :
	
	FDele_Multi Fuc_DeleMulti;
	FDele_Multi_OneParam Fuc_DeleMulti_OneParam;
};

멀티 케스트는 싱글케스트와 비슷하게 헤더파일에서 작성됩니다.

 

//! ABoom.cpp

void ABoom::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason);
	//! 델리게이트 해제
	Fuc_DeleMulti.Clear();
	Fuc_DeleMulti_OneParam.Clear();
}

void ABoom::Tick_Boom(float DeltaTime)
{
		if(Fuc_DeleMulti.IsBound() == true) Fuc_DeleMulti.Broadcast();
		if(Fuc_DeleMulti_OneParam.IsBound() == true) Fuc_DeleMulti_OneParam.Broadcast(456);
}

cpp파일입니다.

 

 

싱글케스트와 차이점은 Unbind가 Clear로 되고 Execute가 Broadcast로 바뀌었습니다.

 

 

//! ATestPlayer.cpp

void ATestPlayer::BeginPlay()
{
	Super::BeginPlay();

		//! SingleCast
		m_pBoom->Fuc_DeleSingle.BindUFunction(this, FName("CallDeleFunc_Single"));
		m_pBoom->Fuc_DeleSingle_OneParam.BindUFunction(this, FName("CallDeleFunc_Single_OneParam"));

		//! MultiCast
		m_pBoom->Fuc_DeleMulti.AddUFunction(this, FName("CallDeleFunc_Multi_1"));
		m_pBoom->Fuc_DeleMulti.AddUFunction(this, FName("CallDeleFunc_Multi_2"));
        m_pBoom->Fuc_DeleMulti_OneParam.AddUFunction(this, FName("CallDeleFunc_Multi_OneParam_1"));
		m_pBoom->Fuc_DeleMulti_OneParam.AddUFunction(this, FName("CallDeleFunc_Multi_OneParam_2"));

함수를 등록하는 부분입니다, 싱글케스트와 쓰임세는 비슷하지만 함수는 BindUFunction->AddUFunction으로 변경되었습니다.

 

결과를 확인하면 폭탄이 터질때 해당 델리게이트에 .BroadCast()함수를 한번 호출하면 안에 등록된 모든 함수가 호출이 되는 것을 확인 할 수 있습니다.

 

없으면 아무것도 실행 안됨


이벤트(Event)

이벤트는 멀티 케스트와 유사하지만 전역으로 설정할 수 없어 외부 클래스에서 추가 델리게이트 선언이 불가능합니다.

캡슐화 개념으로 사용하면 될것 같습니다.

 

//! ABoom.h

UCLASS()
class DELEGATETEST_API ABoom : public AActor
{
	GENERATED_BODY()
public :
	//! Event
	DECLARE_EVENT(ABoom, FDele_Event);
	DECLARE_EVENT_OneParam(ABoom, FDele_Event_OneParam, int32);
    
public :
	FDele_Event Fuc_DeleEvent;
	FDele_Event_OneParam Fuc_DeleEvent_OneParam;

전역에 작성했던 이전 Delegate와 달리 Event는 클래스 내부에 작성하고 인자값으로 주인클래스의 클래스를 입력합니다.

 

이외에는 이전 멀티케스트와 사용법이 동일합니다.

 

 

 


다이나믹 멀티캐스트 (다이나믹(Dynamic))

다이나믹 델리게이트는 직렬화(Serialize)화 되어 블루프린트에서 사용 가능합니다.

 

//! ABoom.h

//Dynamic
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FDele_Dynamic);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDele_Dynamic_OneParam, int32, SomeParameter);

UCLASS()
class DELEGATETEST_API ABoom : public AActor
{
	GENERATED_BODY()
public :
	UPROPERTY(BlueprintAssignable, VisibleAnywhere, BlueprintCallable, Category = "Event")
		FDele_Dynamic Fuc_Dynamic;

	UPROPERTY(BlueprintAssignable, VisibleAnywhere, BlueprintCallable, Category = "Event")
		FDele_Dynamic_OneParam Fuc_Dynamic_OneParam;

이전 델리게이트와 비슷하지만 변수 작성부분에서 BlueprintAssignable 등 UPROPERTY 메크로를 작성합니다.

 

해당 포스팅에서는 DYNAMIC_MULTICAST만 사용했습니다,

 

 

다이나믹 델리게이트는 직렬화(Serialize)화가 가능하고 UPROPERTY 메크로를 통해서 블루프린트에서 델리게이트 바인드가 가능합니다.

이 기능은 블루프린트 디스패처랑 비슷하게 사용이 가능합니다.

 

그리고 Cpp에서는 이전 멀티케스트와 동일하게 사용합니다.

 

 

//! ATestPlayer.cpp

void ATestPlayer::BeginPlay()
{
	Super::BeginPlay();
    
	m_pBoom->Fuc_Dynamic.AddDynamic(this, &ATestPlayer::CallDeleFunc_Dynamic);
	m_pBoom->Fuc_Dynamic_OneParam.AddDynamic(this, &ATestPlayer::CallDeleFunc_Dynamic_OneParam);

다이나믹 델리게이트는 이전과는 다르게 위 .AddDynamic 메크로함수로 바인드를 합니다.

 

 

폭탄이 터지고 로그를 확인하면 블루프린트에서 바인드한 Custom함수와, C++에서 바인드한 함수가 동시에 호출되는 것을 확인 할 수 있습니다.

 

 

 


핵심 코드

 

1. 델리게이트 세팅

//! SingleCast
DECLARE_DELEGATE(FDele_Single);
DECLARE_DELEGATE_OneParam(FDele_Single_OneParam, int32);

//! MultiCast
DECLARE_MULTICAST_DELEGATE(FDele_Multi);
DECLARE_MULTICAST_DELEGATE_OneParam(FDele_Multi_OneParam, int32);

//Dynamic
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FDele_Dynamic);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDele_Dynamic_OneParam, int32, SomeParameter);

UCLASS()
class DELEGATETEST_API ABoom : public AActor
{
	GENERATED_BODY()
public :
	//! Event
	DECLARE_EVENT(ABoom, FDele_Event);
	DECLARE_EVENT_OneParam(ABoom, FDele_Event_OneParam, int32);
    
    
 //---------------------------------------------------------------------
 
 public :
	FDele_Single Fuc_DeleSingle;
	FDele_Single_OneParam Fuc_DeleSingle_OneParam;

	FDele_Multi Fuc_DeleMulti;
	FDele_Multi_OneParam Fuc_DeleMulti_OneParam;

	FDele_Event Fuc_DeleEvent;
	FDele_Event_OneParam Fuc_DeleEvent_OneParam;

	UPROPERTY(BlueprintAssignable, VisibleAnywhere, BlueprintCallable, Category = "Event")
		FDele_Dynamic Fuc_Dynamic;

	UPROPERTY(BlueprintAssignable, VisibleAnywhere, BlueprintCallable, Category = "Event")
		FDele_Dynamic_OneParam Fuc_Dynamic_OneParam;

 

2. 델리게이트에 바인드된 함수 호출 & 해제

void ABoom::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	Super::EndPlay(EndPlayReason);
	//! 델리게이트 해제

	Fuc_DeleSingle.Unbind();
	Fuc_DeleSingle_OneParam.Unbind();

	Fuc_DeleMulti.Clear();
	Fuc_DeleMulti_OneParam.Clear();

	Fuc_DeleEvent.Clear();
	Fuc_DeleEvent_OneParam.Clear();

	Fuc_Dynamic.Clear();
	Fuc_Dynamic_OneParam.Clear();
}

void ABoom::Tick_Boom(float DeltaTime)
{
		//! Delegate호출하는 부분
		if(Fuc_DeleSingle.IsBound()==true)	Fuc_DeleSingle.Execute();
		if(Fuc_DeleSingle_OneParam.IsBound() == true) Fuc_DeleSingle_OneParam.Execute(123);
		if(Fuc_DeleMulti.IsBound() == true) Fuc_DeleMulti.Broadcast();
		if(Fuc_DeleMulti_OneParam.IsBound() == true) Fuc_DeleMulti_OneParam.Broadcast(456);
		if(Fuc_DeleEvent.IsBound() == true) Fuc_DeleEvent.Broadcast();
		if(Fuc_DeleEvent_OneParam.IsBound() == true) Fuc_DeleEvent_OneParam.Broadcast(789);
		if (Fuc_Dynamic.IsBound() == true) Fuc_Dynamic.Broadcast();
		if (Fuc_Dynamic_OneParam.IsBound() == true) Fuc_Dynamic_OneParam.Broadcast(999);
}

 

3. 델리게이트 함수 바인드

void ATestPlayer::BeginPlay()
{
	Super::BeginPlay();

	//! Delegate 등록하기
	if (m_pBoom != nullptr)
	{
		m_pBoom->Fuc_DeleSingle.BindUFunction(this, FName("CallDeleFunc_Single"));
		m_pBoom->Fuc_DeleSingle_OneParam.BindUFunction(this, FName("CallDeleFunc_Single_OneParam"));

		m_pBoom->Fuc_DeleMulti.AddUFunction(this, FName("CallDeleFunc_Multi_1"));
		m_pBoom->Fuc_DeleMulti.AddUFunction(this, FName("CallDeleFunc_Multi_2"));

		m_pBoom->Fuc_DeleMulti_OneParam.AddUFunction(this, FName("CallDeleFunc_Multi_OneParam_1"));
		m_pBoom->Fuc_DeleMulti_OneParam.AddUFunction(this, FName("CallDeleFunc_Multi_OneParam_2"));
		
		m_pBoom->Fuc_DeleEvent.AddUFunction(this, FName("CallDeleFunc_Event"));
		m_pBoom->Fuc_DeleEvent_OneParam.AddUFunction(this, FName("CallDeleFunc_Event_OneParam"));

		m_pBoom->Fuc_Dynamic.AddDynamic(this, &ATestPlayer::CallDeleFunc_Dynamic);
		m_pBoom->Fuc_Dynamic_OneParam.AddDynamic(this, &ATestPlayer::CallDeleFunc_Dynamic_OneParam);
	}
}

 

 

https://darkcatgame.tistory.com/66

반응형
반응형

언리얼엔진하면은 다룬다고 하면 알아야할 몇가지들이 있다. 그 중에서 언리얼엔진에서 베이스가 되는 프로퍼티 시스템인 리플렉션 이 있다.

 

 리플렉션이라는 것은 자바나 C#등에선 지원하지만, 언리얼엔진에서 사용되는 C++ 언어에서는 지원하지 않아 언리얼엔진에서 구현되어 있는 시스템이라고 했다.

 

하지만 난 리플렉션이라는게 뭐길래 엔진에서 구현해서 지원해주는것일까??  처음들어봐서 자바에서 어떻게 사용되는지 먼저 찾아보았다.

 

 

 

 

자바에서의 리플렉션 개념

자바에서의 리플렉션이란 객체를 통해 클래스의 정보를 분석해 내는 프로그램 기법을 말한다. 
(투영, 반사 라는 사전적인 의미를 지니고 있다.)

자바의 Reflection은 JVM에서 실행되는 애플리케이션의 런타임 동작을 검사하거나 수정할 수 있는 기능이 필요한 프로그램에서 사용됩니다. 쉽게 말하자면, 클래스의 구조를 개발자가 확인할 수 있고, 값을 가져오거나 메소드를 호출하는데 사용됩니다.

 

간단히 요약하자면, 컴파일 시간이 아니라 런타임시간에 동적으로 특정 클래스의 정보 객체화를 통해 분석 및 추출해낼수 있는 프로그래밍 기법이라고 표현할수 있다. (개발 과정에서 개발자에게 조금 더 도움이 되는 시스템이라고 다가왔다.)

 

 

 

 

 

언리얼 엔진에서의 리플렉션 

이런 좋은 시스템이 C++에는 지원하지 않아, 언리얼 엔진에서 구현되어 있는데, 우선  관련 설명을 찾아보면

리플렉션(Reflection)은 프로그램이 실행시간에 자기 자신을 조사하는 기능입니다.

이는 엄청나게 유용한 데다 언리얼 엔진 테크놀로지의 근간을 이루는 것으로, 에디터의 디테일 패널, 시리얼라이제이션, 가비지 콜렉션, 네트워크 리플리케이션, 블루프린트/C++ 커뮤니케이션 등 다수의 시스템에 탑재된 것입니다.


언리얼엔진 홈페이지 중...

위에서 '자기 자신'은 클래스, 구조체, 함수, 멤버 변수, 열거형 등을 의미하고 있다.

수집 된 정보는 UClass에 보관된다. 런타임에는 GetClass() 함수를 통해 접근하고, 컴파일 타임에서는 StaticClass()를 사용해 접근한다. 해당 함수들은 언리얼 헤더 툴에 의해 자동으로 생성된다.

 

 

 

 

 

잠깐, 왜 언리얼에서는 프로퍼티 시스템 이라고도 하는가?

전형적으로 이러한 리플렉션은 '프로퍼티 시스템'이라고 부르는데,그 이유로는 아마도 '리플렉션(Reflection/반사)'은 그래픽 용어라서 혼돈을 빚을 수 있기 때문인것 같다.(개인적인 추측)

 

 

리플렉션을 알려주는 방법?

 Unreal Header Tool (UHT)가 그 프로젝트를 컴파일할 때 해당 정보를 수집한다.

헤더(.h)에 리플렉션이 있는 유형으로 알려주려면, 헤더 파일 상단에 특수한 include 를 추가해 줘야 하야한다.

 그러면 리플렉션이 있는 유형은 이 파일을 고려해야 한다는것 그리고 시스템 구현에도 필요함을 UHT 에 알려줍니다.

그 #include는 아래와 같다.

#include "FileName.generated.h"

 

위와 같은 헤더가 추가 된다면, 이제 열겨형/UENUM(), 클래스/UCLASS(), 구조체/USTRUCT(), 함수/UFUNCTION(), 멤버변수/UPROPERTY() 를 사용하여 헤더의 다양한 유형과 멤버 변수 주석을 달 수 있다.

이 매크로 각각은 유형 및 멤버 선언 전에 오며, 추가적인 지정자 키워드를 담을 수 있습니다. 

 

 

 

 

 

리플렉션작동 원리

UBT(Unreal Build Tool) 는 그 역할을 위해 헤더를 스캔한 다음 리플렉션된 유형이 최소 하나 있는 헤더가 들어있는 모듈을 기억.

그 헤더 중 어떤 것이든 지난 번 컴파일 이후 변경되었다면, UHT 를 실행하여 리플렉션 데이터를 수집하고 업데이트.

UHT 는 헤더를 파싱하고, 리플렉션 데이터 세트를 빌드한 다음, (모듈별.generated.inl 에 기여하는) 리플렉션 데이터가 들어있는 C++ 코드를 생성할 뿐만 아니라, (헤더별 .generated.h 인) 다양한 헬퍼 및 thunk 함수도 생성.

 

요약 : 

1. UBT가 리플렉션 키워드 탐색

2. UHT가 해당 .cpp를 파싱

3. 리플렉션 데이터 정보를 수집

4. 수집된 정보는 별개의 C++ 코드 .generated.h / .cpp로 저장

5. 빌드 시 기존 코드에 .generated.h 코드를 추가해 컴파일

 

 

이렇게 UHT에 수집된 리플렉션 데이터는 별개의 코드로 저장되고 클래스이름.generated가 붙는다.

언리얼 실행환경은 이를 사용해 언리얼 오브젝트를 관리하고 에디터에서는 에디터에서 편집할 수 있는 인터페이스를 제공한다.

해당 파일들을 프로젝트 폴더에서 \Intermediate 폴더에 저장 된다. 클래스.h의 내용이 변경될때마다 자동 생성, 기존 파일을 덮어 쓴다.

 

 

 

generated 함수에는 StaticClass() / StaticStruct() 같은 것이 포함되어 있어, 유형에 대한 리플렉션 데이터를 구하는 것이 쉬워질 뿐만 아니라, 블루프린트나 네트워크 리플리케이션에서 C++ 함수를 호출하는 데 사용되는 thunk 를 구하는 것도 쉬워집니다.  이는 클래스나 구조체의 일부로 선언되어야 하며, GENERATED_UCLASS_BODY() 또는 GENERATED_USTRUCT_BODY() 매크로가 리플렉션된 유형에 포함되어야 하는지에 대한 이유가 됩니다.

이 매크로를 정의하는 #include 'TypeName.generated.h' 는 물론입니다.

 

 

 

ref : https://hyo-ue4study.tistory.com/182

 

반응형
반응형

 

 

Damage is a common concept in video games. Because of this, one of the default features in Unreal Engine is a framework for dealing and receiving damage.

This tutorial is the first in a series about damage, and aims to give you an introduction to the Damage system in blueprint and C++.

Damage in UnrealPermalink

To borrow a phrase from Alex Forsythe: With Unreal, damage comes standard. It’s a feature that is part of the Actor class, meaning that every actor class that you’ve created so far already natively supports dealing & receiving damage!

Let’s start this tutorial with a quick introduction to the concept of instigator. After that, we’ll look into how damage should be dealt and received, both in blueprint and C++.

InstigatorPermalink

If you’ve been working in Unreal, you might already have seen the term “instigator” a couple of times. Every actor has an Instigator property, it is a reference to a Pawn and represents the pawn/character responsible for any actions the spawned actor will do, like dealing damage. A very practical example is a cannonball, the instigator would be the pawn firing the cannon.

The instigator is usually set when you spawn the actor. In blueprints, this is done like this:

In C++, you can set the instigator pawn in the FActorSpawnParameters struct:

void AYourPawn::SpawnProjectile(const TSubclassOf<AActor> ProjectileClass)
{
    // FP_MuzzleLocation must be a valid component.
    check(FP_MuzzleLocation != nullptr);

    // Spawn a projectile at a specified transform, with instigator set to this pawn.
    const FTransform& SpawnTransform = FP_MuzzleLocation->GetComponentTransform();
    FActorSpawnParameters SpawnParams;
    SpawnParams.Instigator = this;
    World->SpawnActor<AActor>(ProjectileClass, SpawnTransform, SpawnParams);
}

It is also possible to change the instigator after spawning by simply calling SetInstigator.

The concept of instigators is incredibly useful for damage: It allows a damage receiver to act depending on who was responsible for dealing the damage. The classroom example for this is friendly fire: If you receive damage and the instigator of the damage is someone on your own team, you might decide to ignore the damage if friendly fire is disabled.

Dealing damagePermalink

To inflict damage on an actor with blueprints, simply call the ApplyDamage function:

Let’s briefly talk about some of the parameters:

  • Damaged Actor: The actor you’re dealing damage to.
  • Event Instigator: The controller that is responsible for dealing this damage. You can obtain the instigator controller using the GetInstigatorController node.
  • Damage Causer: The actor that actually dealt this damage (e.g. a projectile or grenade).
  • Damage Type Class: An object that can influence damage calculations. This deserves a tutorial on it’s own so we will just keep it empty for now.

In addition to ApplyDamage, there are also separate, specialized functions for dealing point and radial damage. Point damage is for damage at a particular point, coming from a particular direction (like being hit with a bullet). Radial damage is for damage in a wider area, possibily affecting multiple actors (like a grenade blast). More about point and radial damage in a future tutorial.

The C++ equivalent for dealing damage is AActor::TakeDamage. While the blueprint equivalent has different functions for each damage variant (i.e. point, radius, etc.), the TakeDamage function combines all of these into one with a FDamageEvent struct to differentiate between the variants.

Dealing damage in C++ is pretty simple though, here is the equivalent for the above blueprint example:

void AYourActor::DealDamageTo(class AActor* OtherActor)
{
    // Much like the blueprint example, we're not using DamageType yet.
    OtherActor->TakeDamage(4.2f, FDamageEvent(), GetInstigatorController(), this);
}

For dealing point and radial damage, you will need to supply a FPointDamageEvent or FRadialDamageEvent with the proper arguments, instead of the FDamageEvent. For applying radial damage, I recommend using the UGameplayStatics::ApplyRadialDamage helper function instead.

Responding to damagePermalink

Responding to damage in an actor is as simple as overriding the AnyDamage function in blueprint. For C++ things are a bit more tricky, we’ll get to that after the blueprint stuff.

In addition to the any damage function, there are also separate functions for receiving point and radial damage. Keep in mind that when you receive point or radial damage, AnyDamage will still get called!

The following is an example of responding to damage blueprint, simply override the AnyDamage function:

Most of the function arguments here are the same as the values you provided to the ApplyDamage function. The only exception is Damage Type, which is now an object reference rather than a class reference, more info on this in a future tutorial.

In addition to AnyDamage, there is also the OnTakeAnyDamage event dispatcher. This event is particularly useful if you have a component that takes care of dealing with damage, rather than the actor itself:

Unfortunately, the C++ function ReceiveAnyDamage in AActor was not made virtual by Epic, so it cannot be overridden. The next best way is to override the TakeDamage function instead (InternalTakeRadialDamage and InternalTakePointDamage exist for radial and point damage respectively):

float AYourActor::TakeDamage(const float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
    // If you need the DamageType object like in blueprint, this is how you do it:
    UDamageType const* const DamageTypeCDO = DamageEvent.DamageTypeClass ? DamageEvent.DamageTypeClass->GetDefaultObject<UDamageType>() : GetDefault<UDamageType>();

    // Make sure to call Super so blueprint & event dispatchers still fire.
    return Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
}

Fortunately, the OnTakeAnyDamage delegate can simply be bound to in C++, just like with blueprint:

void UYourActorComponent::BeginPlay()
{
    Super::BeginPlay();

    // Subscribe to the owner receiving damage event.
    GetOwner()->OnTakeAnyDamage.AddDynamic(this, &UYourActorComponent::OnTakeRadialDamage);
}

void UYourActorComponent::OnTakeRadialDamage(AActor* DamagedActor, float Damage, const UDamageType* DamageType, AController* InstigatedBy, AActor* DamageCauser)
{
    // Your damage handling code here..
}

Conclusion

This tutorial has covered the basics of damage: It introduced the concept instigators, how damage should be dealt and how it should be received, both in blueprint and C++.

 

 

 

 

 

 

https://jasperdelaat.com/unreal-engine/damage-1/

반응형

'게임엔진(GameEngine) > Unreal4' 카테고리의 다른 글

[UE] 델리게이트 종류와 차이  (0) 2023.03.08
리플렉션  (0) 2023.03.07
애니메이션 디스턴스매칭  (0) 2023.02.20
TSharedPtr<.... , ESPMode::ThreadSafe> 스레드 안정성  (0) 2023.01.20
Widget for painting  (0) 2022.11.23
반응형

UE5 기능이긴한데 카테고리 만들기가 귀찮음으로..

 

로코모션 : 한 지점에서 다른 지점으로 이동하는 행위 또는 움직이는 방법의 정의다

로코모션의 퀄리티는 발이 미끄러지지 않게 처리하는 것이 핵심이다

게임 액터와 애니메이션 재생이 다르거나 블랜딩 할때 차이로 인해 미끄러지는 현상이 발생

 

루트모션이란 : 애니메이션 시퀀스의 루트본 움직임을 게임 앤터의 움직임으로 전환하는 기능이다

 

루트모션의 경우 애니메이션 블랜딩 할때 루트본도 같이 블랜딩 되기 때문에 미끄러짐 현상이 나타난다

 

이를 해결 하기 위해 대스턴스매칭 또는 포즈워핑이 사용된다

 

디스턴스매칭 : 거리를 기반으로 애니메이션 재생타이밍을 조절한다

 

 

디스턴스 매칭 미적용시 (속도가 감속 하는 경우인 경우)

5미터의 거리를 감속하면서 애니메이션을 멈출때 속도가 줄어들어도 3미터 거리 이동으로 이동하면서 정지한다고 하면 

상단 처럼 5미터 이동 발이 하단의 3미터 거리에서도 그대로 적용되어 발이 밀리게 된다

 

 

디스턴스 매칭 => 거리기반 디스턴스 매칭

거리기반 디스턴스 매칭의 경우

 

  1. 거리에 맞도록 애니메이션 시작 타이밍을 변경한다
  2. 5미터의 애니메이션 재생 중에 남은 거리가 3미터라면 3미터를 이동하는 애니메이션으로 재생을 하도록 하여 발이 밀리지 않도록 한다

좌 : 미적용, 우 : 적용

 

 

 

 

 

반복되는 루핑 애니메이션의 경우 거리를 이용할수 없기 때문에 프레임당 이동 거리를 기준으로 애니메이션 재생 속도를 조절하게 된다

 

속도기반 디스턴스 매칭

동일한 애니를 속도에 따라 재생 속도를 달리 한것

왼쪽은 초당 속도 200 미터, 오른쪽은 초당 속도 1000 미터 인 속도기반 디스턴스 매칭

 

 

  1. 디스턴스매칭을 위해 필요한 것은 특정 시점에서 캐릭터가 있을 위치를 예측하는것이 필요한데
    캐릭터가 정지할 위치를 알아야 현재 위치와 정지 위치 사이에 거리를 구하고 거리를 구해야 거리기반 디스턴스 매칭을 작동시킬수 있기 때문 
  2. 각 애니메이션에서 루트본의 이동거리 정보가 필요하다
    디스턴스 매칭에서 이 거리 정보를 기반으로 애니메이션의 시작지점을 결정하거나 애니메이션 재생속도를 결정하기 때문이다

 

 

[사용법 구현] 

우선 현재 캐릭터의 가속 여부를 구현다 HasAcceleration 변수에 매 프레임마다 저장

 

 

HasVelocity : 현재 캐릭터의 속도 여부도 저장한다

 

 

 

 

 

단방향용 로코모션용 애님 스테이트 머신 구성

 

캐릭터가 정지할 위치를 예측하기 위해 사용 되는 노드

입력들은 위에서 구한 것들을 입력해준다

위 출력을 아래 처럼 Vector LengthXY 를 통해 DistanceToTarget 변수에 저장

 

애니메이션이 끝나기 전에 캐릭터가 정지 위치에 도착하게 되면 애니메이션도 멈춰 버리기 때문에 자연스러운 정지를 위해서 DistanceToTarget 값이 0 보다 큰지 봐서 만약 크다면 아직 거리가 남아 있는 것이기 때문에 디스턴스 매칭을 작동시켜야 한다 아래처럼..

그런데 0 보다 작은 경우, 즉 이미 위치에 도달한 경우라면 AdvanceTime 을 이용해서 애니메이션을 원래의 속도로 플레이 시키도록 한다

 

 

 

 

 

스트라이드 워핑과 오리엔테이션 워핑을 조합하면 더 자연스러운 애니를 루트모션에서 만들 수 있다

 

스트라이드 워핑 : 이동 속도에 따라 포폭 조절, 디스턴스 매칭으로 우선 느려질때 애니메이션이 느린 걸로 교체되기 때문에 이때 스트라이드 워핑과 더 잘맞아 떨어짐

 

오리엔테이션 워핑  : 방향에 따라 하체 회전 , 만약 애니가 전,후 , 좌, 우 4방향 밖에 없다면 대각선일때 애니가 이상해지는데   

 

 적용 전 앞-우측 방향 진행시


적용 후 앞-우측 방향 진행

하체가 회전 된걸 볼 수 있다

 

 

ref : https://youtu.be/RFNGB4ZTusQ

ref : https://docs.unrealengine.com/5.0/ko/root-motion-in-unreal-engine/

반응형
반응형

스레드 안정성

기본적으로 스마트 포인터는 싱글 스레드가 접근하는 것이 안전합니다. 멀티 스레드가 접근해야 한다면 스마트 포인터 클래스의 스레드 세이프 버전을 사용하세요:

  • TSharedPtr<T, ESPMode::ThreadSafe>
  • TSharedRef<T, ESPMode::ThreadSafe>
  • TWeakPtr<T, ESPMode::ThreadSafe>
  • TSharedFromThis<T, ESPMode::ThreadSafe>

이러한 스레드 세이프 버전은 원자적(atomic) 참조 카운팅으로 인해 디폴트보다 다소 느리지만 그 비헤이비어는 일반 C++ 포인터와 같습니다:

  • 읽기와 복사본은 항상 스레드 세이프입니다.
  • 안전성을 위해 쓰기와 초기화는 반드시 동기화되어야 합니다.

하나 이상의 스레드가 포인터에 접근하지 않는다는 것이 확실하다면, 스레드 세이프 버전을 사용하지 않음으로써 퍼포먼스를 향상시킬 수 있습니다.

팁 및 제한사항

  • 가급적이면 함수에 데이터를 TSharedRef 또는 TSharedPtr 매개변수로 넣지 않는 것을 권장합니다. 이러한 데이터의 해제와 참조 카운팅으로 인해 오버헤드가 발생하게 됩니다. 그 대안으로, 레퍼런스된 오브젝트를 ‘const &'로 넣으세요. (즉 읽기로만 사용 할때를 말하는 것)
  • 쉐어드 포인터를 불완전한 타입/형식으로 미리 선언할 수 있습니
  • 쉐어드 포인터는 언리얼 오브젝트(UObject 와 이로부터 파생된 클래스)와 호환되지 않습니다. 언리얼 엔진은 ‘UObject' 관리를 위한 별도의 메모리 관리 시스템이 있으며 (언리얼 오브젝트 처리 문서를 참고하세요) 두 시스템은 완전히 다른 시스템입니다

 

 

ref : https://docs.unrealengine.com/4.27/ko/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/SmartPointerLibrary/

반응형
반응형

구문

Tags { "TagName1" = "Value1" "TagName2" = "Value2" }

TagName1 Value1을, TagName2 Value2를 지정합니다. 태그를 원하는 수만큼 지정할 수 있습니다.

세부 정보

태그는 기본적으로 키-값 페어입니다. 서브셰이더 내에서 태그는 서브셰이더의 다른 파라미터 및 렌더링 순서를 결정하기 위해 사용합니다. Unity가 인식하는 다음 태그는 반드시 패스가 아닌 서브셰이더 섹션 내에 위치해야 합니다.

Unity가 인식하는 빌트인 태그 외에도 Material.GetTag 함수를 사용하여 자신의 태그를 사용하고 쿼리할 수 있습니다.

렌더링 순서 - Queue 태그

Queue 태그를 사용하여 오브젝트를 드로우 처리할 순서를 결정할 수 있습니다. 셰이더는 자신의 오브젝트가 어떤 렌더 대기열에 속하는지를 결정하며 이 방식으로 모든 불투명 오브젝트가 그려진 후에 모든 투명 셰이더가 그려지도록 할 수 있습니다.

사전 정의된 렌더 대기열은 4개가 있으며 사전 정의된 대기열 사이에 추가로 대기열을 넣을 수 있습니다. 사전 정의된 대기열은 다음과 같습니다.

  • Background- 이 렌더 대기열은 다른 대기열보다 먼저 렌더링됩니다. 보통 배경에 배치해야 할 항목에 사용합니다.
  • Geometry (디폴트) - 대부분 오브젝트에 사용합니다. 불투명 지오메트리가 이 대기열을 사용합니다.
  • AlphaTest - 알파 테스트된 지오메트리가 이 대기열을 사용합니다. 알파 테스트된 오브젝트는 모든 솔리드 오브젝트를 드로우한 후에 렌더링하는 것이 훨씬 효율적이므로 이 대기열은 Geometry 대기열과는 별도로 존재합니다.
  • Transparent - 이 렌더 대기열은 Geometry  AlphaTest 후에 뒤에서 앞의 순서로 렌더링됩니다. 알파 블렌드된 모든 항목(즉, 뎁스 버퍼에 기록하지 않는 셰이더)은 여기 속해야 합니다(유리, 파티클 효과 등).
  • Overlay - 이 렌더 대기열은 오버레이 효과용입니다. 마지막에 렌더링되는 모든 항목이 속해야 합니다(예: 렌즈 플레어 등).
Shader "Transparent Queue Example"
{
     SubShader
     {
        Tags { "Queue" = "Transparent" }
        Pass
        {
            // rest of the shader body...
        }
    }
}

투명 대기열에서 렌더링하는 방법을 보여주는 예제

특수 용도를 위해 대기열 사이에 대기열을 둘 수 있습니다. 내부적으로 각 대기열은 정수 인덱스로 대표됩니다. 예를 들어, Background는 1000, Geometry는 2000, AlphaTest는 2450, Transparent는 3000, Overlay는 4000입니다. 셰이더가 대기열을 다음과 같이 사용한다면

Tags { "Queue" = "Geometry+1" }

렌더 대기열 인덱스가 2001이 되므로(Geometry+1), 이 오브젝트는 모든 불투명 오브젝트가 렌더링된 후에 렌더링되고 투명 오브젝트보다는 먼저 렌더링됩니다. 이 방법은 일부 오브젝트가 항상 다른 오브젝트 세트 사이에 그려지도록 하고 싶을 때 유용합니다. 예를 들어, 투명한 물은 불투명한 오브젝트보다는 나중에 그려야 하지만 투명한 오브젝트보다 먼저 그려야 합니다.

2500 이하의 대기열(“Geometry+500”)은 “불투명”으로 간주되며 최상의 성능을 위해 오브젝트의 드로우 순서를 최적화합니다. 더 높은 인덱스의 렌더링 대기열은 “투명 오브젝트”로 간주되며 거리에 따라 오브젝트를 정렬합니다. 즉, 가장 먼 오브젝트부터 렌더링하여 가장 가까운 오브젝트를 맨 마지막에 렌더링합니다. 스카이박스는 모든 불투명 오브젝트와 투명 오브젝트 사이에 그려집니다.

 

 

 

ref : https://docs.unity3d.com/kr/530/Manual/SL-SubShaderTags.html

반응형
반응형

C#은 상당히 좋은 언어다. 가장 많이 알려진 C#의 특징 중 하나는 메모리 관리에 부담이 없다는 점이다.

So Cool~ C# 메모리
C/C++를 사용하면서 포인터 때문에 괴로워 해본 적이 있는가? 그렇다면 C#에 관심을 가져보는 것이 좋다. C#은 다음과 같은 특징들을 제공하기 때문이다.

- 메모리 해제에 신경 쓰지 않아도 된다.
- 이미 삭제된 메모리에 접근하는 실수를 방지해준다.
- 잘못된 캐스팅으로 엉뚱한 메모리에 접근하지 않게 한다.
- 배열 크기보다 큰 메모리에 접근하지 못한다.
- 메모리 단편화에 대해 신경 쓰지 않아도 된다.

편한 C#, 마구잡이로 사용하면 낭패
골치 아픈 메모리 관리를 신경 쓰지 않아도 된다는 점은 사용자들에게 무척 편리하게 다가온다. 하지만 C#에서도 메모리를 다루기 위해서는 세심한 주의가 필요하다. 마음 놓고 개발하다 당황했던 과거 필자의 경험을 살펴보도록 하자. 

개 발 초창기, 게임 플레이 중에 주기적으로 랙이 발생했다. 로직을 확인해 봤지만 특별히 로딩 중이거나 초기화된 부분이 없어 의아했다. 유니티 엔진에서 제공하는 프로파일러로 한 프레임에 걸리는 시간을 측정해봤다. 측정 결과, System.GC.Collect() 호출에 오랜 시간이 걸린다는 점이 발견됐다. 플레이 중에 프레임마다 소모되는 시간을 그래프로 보여주는 <그림 1>을 보면 System.GC.Collect() 호출 시 그래프가 크게 튀어 오른 모습이 확인된다. C#에서 사용하지 않는 메모리를 정리하면서 가비지 컬렉션(Garbage collection) 랙이 발생한 것이다.

 


<그림 1> 프로파일러 이미지

이때는 가비지 컬렉션이 동작하는 횟수를 줄여서 랙 발생을 줄이면 된다. 가비지 발생을 줄이면 가비지 컬렉션이 호출되는 시간을 늦출 수 있어 동작 횟수가 줄어든다. 가비지란 프로그램이 실행되면서 어디에서든 더 이상 참조되지 않는 메모리를 의미하므로 가능한 한 메모리를 할당했다 금방 버려지는 상황을 만들지 않는 것이 좋다. 몇 가지 사례들을 살펴보자.

‘+’ operator를 통한 문자열 조합 
C#은 문자열 조합이 쉽다. <리스트 1>에 보이는 것처럼 ‘+’로 연결하면 간단히 문자열 조합이 이뤄진다. 모든 객체가 ToString()을 지원하기 때문에 문자열끼리만 조합되는 게 아니라 int, float 등의 값도 알아서 문자열로 변환·조합된다.

<리스트 1> ‘+’로 연결한 문자열 조합
class Names
{
    public string[] name = new string[100];
    public void Print()
    {
        for (int index = 0; index < name.Length; index++)
        {
            string output = "[" + index + "]" + name;
            Console.WriteLine(output);
        }
    }
}

문제는 <리스트 1>에서 가비지가 많이 발생한다는 점이다. ‘+’ 연산자로 두 값을 연결할 때마다 새로운 string 인스턴스가 생성된다. 연이어 ‘+’ 연산자가 나오기 때문에 다시금 새로운 string 인스턴스가 생성되고, 이전에 만들어진 string 인스턴스는 가비지가 된다. string 조합을 위해 ‘+’ operator 호출이 많아질수록 많은 가비지가 만들어지는 것이다.

그래서 문자열을 조합하는 동안 새로운 객체를 생성하지 않는 System.Text.StringBuilder 객체를 소개한다. ‘+’ operator가 아닌 Append() 메소드를 통해 문자열을 추가하며, string 객체를 만들어내는 게 아니라 이미 잡아놓은 메모리 공간에 문자열만 복사해 뒀다가 한번에 ToString()으로 string 객체를 생성해낸다.

<리스트 2> System.Text.StringBuilder 객체 사용
class NewNames
{
    public string[] name = new string[100];
    private StringBuilder sb = new StringBuilder();

    public void Print()
    {
        sb.Clear();     // sb.Length = 0;
        for (int index = 0; index < name.Length; index++)
        {
            sb.Append("[");
            sb.Append(index);
            sb.Append("] ");
            sb.Append(name);
            sb.AppendLine();
        }
        Console.WriteLine(sb.ToString());
    }
}

과다한 Append() 메소드 호출이 필요해 ‘+’ 코드보다 깔끔하지 못하다고 생각된다면 AppendFormat()을 사용하는 것도 좋다.

 <리스트 3> AppendFormat() 활용
class NewNames
{
    public string[] name = new string[100];
    private StringBuilder sb = new StringBuilder();

    public void Print()
    {
        sb.Clear();     // sb.Length = 0;
        for (int index = 0; index < name.Length; index++)
        {
            sb.AppendFormat("[{0}] {1}", index, name.ToString());
        }
        Console.WriteLine(sb.ToString());
    }
}

string처럼 Immutable pattern을 사용한 객체들의 값에 접근할 때는 기존 메모리를 수정하지 않고 새로운 메모리를 만들어 반환하거나 입력받으므로 사용 시 주의가 필요하다.

메소드 안에서 생성한 객체
C#은 C++과 달리 클래스를 인스턴싱하려면 반드시 new를 해줘야 한다. 이때 heap에서 메모리 할당이 일어난다. 

<리스트 4>와 같이 메소드 안에서 new로 생성된 인스턴스는 메소드를 빠져나오면 더 이상 사용하지 않게 돼 가비지로 처리된다. 이런 패턴의 메소드가 자주 호출될수록 가비지도 많이 발생한다.

<리스트 4> new로 생성된 인스턴스
public class MyVector
{
    public float x, y;
    public MyVector(float x, float y) { this.x = x; this.y = y; }
    public double Length() { return System.Math.Sqrt(x * x + y * y); }
}

static class TestMyVector
{
    public static void PrintVectorLength(float x, float y)
    {
        MyVector v = new MyVector(x, y);
        Console.WriteLine("Vector=({0},{1}), lenght={2}", x, y, v.Length());
    }
}

Vector 클래스를 구조체로 바꿔보면, new 연산자로 인스턴스를 만들어도 heap 영역에 메모리가 할당되지 않는다. 구조체 역시 Value type이기 때문에 stack 영역에 메모리가 할당되며, 메소드를 빠져나갈 경우 자동으로 삭제된다. 물론 heap 영역에 생성된 메모리가 아니기 때문에 가비지 컬렉션의 대상이 되지도 않는다. 

<리스트 5> Vector 클래스를 구조체로 변환
public struct MyVector
{
    public float x, y;
    public MyVector(float x, float y) { this.x = x; this.y = y; }
    public double Length() { return System.Math.Sqrt(x * x + y * y); }
}

static class TestMyVector
{
    public static void PrintVectorLength(float x, float y)
    {
        MyVector v = new MyVector(x, y);
        Console.WriteLine("Vector=({0},{1}), lenght={2}", x, y, v.Length());
    }
}

구조체로 바꿀 수 없다면, <리스트 6>처럼 멤버변수 사용을 권장한다.

<리스트 6> 멤버변수 사용
public class MyVector
{
    public float x, y;
    public MyVector() { x = .0f; y = .0f; }
    public MyVector(float x, float y) { this.x = x; this.y = y; }
    public double Length() { return System.Math.Sqrt(x * x + y * y); }
}

static class TestMyVector
{
    private static MyVector m_cachedVector = new MyVector();
    public static void PrintVectorLength(float x, float y)
    {
        m_cachedVector.x = x;
        m_cachedVector.y = y;

        Console.WriteLine("Vector=({0},{1}), lenght={2}",
x, y, m_cachedVector.Length());
    }
}

속도 저하가 큰 Boxing
Boxing이란 Value type 객체를 Reference type 객체로 포장하는 과정을 뜻한다. C#의 모든 객체는 object로부터 상속되는데, 심지어 상속받지 못하는 int, float 등의 Value type 조차도 object로부터 상속된 것처럼 사용할 수 있다. 하지만 가비지 컬렉션에 의한 부하 못지않게 boxing으로 인한 부하도 크다. 무심코 만든 코드에서 boxing 과정이 일어나는 경우가 있으니 잘 이해하고 사용해야 한다.

<리스트 7>을 보면 리스트에 서로 다른 type의 값을 추가했지만, loop 안에서 추가 값을 object type으로 받아 하나의 코드로 처리할 수 있음을 알 수 있다.

<리스트 7> 서로 다른 type 값 추가
class MyClass
{
    public override string ToString() { return "다섯"; }

    static public void Sample()
    {
        ArrayList list = new ArrayList();
        list.Add(1);
        list.Add(1.5f);
        list.Add(‘3’);
        list.Add("four");
        list.Add(new MyClass());

        foreach (object item in list)
            Console.WriteLine(item.ToString());
    }
}

 


<그림 2> <리스트 7>의 실행 결과

매력적인 C#이지만 Value type의 값을 Reference type인 object로 바꿔주는 과정에는 많은 시간이 걸리며 변환 시에는 System.Object 내부에 랩핑하고 관리되는 heap에 저장된다. 즉 새로운 객체가 만들어지는 셈이다. MSDN에서 발췌한 <그림 3>을 참조하길 바란다.

 

 

 

https://msdn.microsoft.com/ko-kr/library/yz2be5wk.aspx

http://www.gamedevforever.com/322


<그림 3> boxing과 unboxing의 비교

따라서 한 번에 다양한 type을 처리하는 경우가 아니라면 collection에 사용된 값의 type을 명시해주는 Generic collection 사용을 권한다. Generic은 C++의 template와 비슷하다. 그래서 Generic collection들은 C++의 STL container들과 비슷하게 생겼다. <리스트 8>을 참고하자.

<리스트 8> Generic collection
class Example
{
    static public void BadCase()
    {
        ArrayList list = new ArrayList();
        int evenSum = 0;
        int oddSum = 0;

        for (int i = 0; i < 1000000; i++)
            list.Add(i);

        foreach (object item in list)
        {
            if (item is int)
            {
                int num = (int)item;
                if(num % 2 ==0) evenSum += num;
                else oddSum += num;
            }
        }
           
        Console.WriteLine("EvenSum={0}, OddSum={1}", evenSum, oddSum);
    }

    static public void GoodCase()
    {
        List<int> list = new List<int>();
        int evenSum = 0;
        int oddSum = 0;

        for (int i = 0; i < 1000000; i++)
            list.Add(i);

        foreach (int num in list)
        {
            if (num % 2 == 0) evenSum += num;
            else oddSum += num;
        }
           
        Console.WriteLine("EvenSum={0}, OddSum={1}", evenSum, oddSum);
    }
}

메모리가 계속 늘어나는 또 다른 문제의 발생!
이 글을 시작하며 C#에서 사용자는 메모리 해제에 신경 쓸 필요가 없다고 했지만 의도하지 않게 메모리가 늘어나기도 한다. C#에는 delete 같은 메모리 해제 명령이 없기에 메모리 릭(memory leak) 현상이 발생하면 당혹스러울 수 있다. 여기서 C# 메모리의 특징을 다시 한 번 떠올려보자.

시스템에서 더 이상 참조가 없는 메모리를 알아서 해제하는 것을 우리는 가비지 컬렉션이라 부른다. 가비지는 더 이상 참조가 없는 메모리다. C# 애플리케이션이 메모리가 해제되지 않고 계속 증가되고 있다면 어디선가 의도하지 않는 참조가 일어나고 있다고 보면 된다. 그렇다면 어디에서 의도하지 않은 참조가 일어나는 것일까? 예를 통해 확인해 보자.

 


<그림 4> #1 - 케릭터 매니저에서 케릭터를 생성한다

 


<그림 5> #2 - 누군가 디버깅을 위해 '캐릭터 위치 표시' 객체를 만들고 캐릭터 매니저에 접근해 등록된 캐릭터를 모두 참조한다

 


<그림 6> #3 - 캐릭터 매니저에서 필요없는 캐릭터를 삭제한다

 


<그림 7> #4 - 캐릭터 매니저에서 삭제됏지만 '캐릭터 위치 표시' 객체에서는 여전히 참조 중이다. 가비지가 아니기 때문에 메모리에 계속 남아있으며, 구현에 따라서는 의도하지 않게 화면에 남을 수도 있다.

WeakReference로 의도하지 않은 참조를 없애보자
System.WeakReference 는 가비지 컬렉션에 의한 객체 회수를 허용하면서 객체를 참조한다. 인스턴스를 참조하려면 Weak Reference.Target으로 접근해야 하는데 원본 인스턴스가 가비지 컬렉터에 의해 회수되면 WeakReference.Target은 null이 반환된다.

<리스트 9> WeakReference.Target
public class Sample
{
    private class Fruit
    {
        public Fruit(string name) { this.Name = name; }
        public string Name { private set; get; }
    }

    public static void TestWeakRef()
    {
        Fruit apple = new Fruit("Apple");
        Fruit orange = new Fruit("Orange");
           
        Fruit fruit1 = apple;   // strong reference
        WeakReference fruit2 = new WeakReference(orange);
        Fruit target;
           
        target = fruit2.Target as Fruit;
        Console.WriteLine(" (1) Fruit1 = \"{0}\", Fruit2 = \"{1}\"",
            fruit1.Name, target == null ? "" : target.Name);

        apple = null;
        orange = null;

        System.GC.Collect(0, GCCollectionMode.Forced);
        System.GC.WaitForFullGCComplete();

        // fruit1과 fruit2의 값을 바꾼 적은 없지만, fruit2의 결과가 달라진다.
        target = fruit2.Target as Fruit;
        Console.WriteLine(" (2) Fruit1 = \"{0}\", Fruit2 = \"{1}\"",
            fruit1==null ? "" : fruit1.Name,
            target == null ? "" : target.Name);
    }
}

<리스트 9>의 실행으로 <그림 8>을 확인할 수 있다. Fruit2가 참조하고 있던 orange 인스턴스는 가비지 컬렉터에 의해 회수돼 null이 됐다.

 


<그림 8> <리스트 9>의 실행 결과

‘캐릭터 매니저’처럼 객체의 생성·삭제를 직접 관여하는 모듈이 아닌 곳에서는 가능한 WeakRefernce를 사용하는 것이 좋다. ‘객체 위치 표시 객체’처럼 인스턴스를 참조하는 모듈에서 WeakReference를 사용하면, 의도하지 않은 참조로 메모리가 해제되지 않는 실수를 방지할 수 있다. 주의할 점은 Weak Reference.Target 값을 보관하면 안 된다는 것이다. 만약 그대로 보관하고 있으면 강한 참조(strong reference)가 일어나 이를 인식한 가비지 컬렉터는 회수를 실행하지 않게 된다.

C/C++처럼 원하는 시점에 객체를 삭제하고 싶다면
C#에서 는 할당된 메모리를 임의로 해제할 수 없다. 컬렉션에 보관된 인스턴스를 제거하거나 인스턴스를 담고 있던 변수에 null을 넣어 더 이상 참조하지 않는 방법이 있지만 실제 인스턴스가 삭제되는 시점은 가비지 컬렉션 동작 이후가 되므로, 언제가 될 지 정확히 알 수 없다. 의도한 시점에 맞춰 정확히 삭제할 수 없다는 점이 그렇게 중요하지는 않다. 하지만 캐릭터 매니저에서 캐릭터를 제거했는데도 여전히 캐릭터 인스턴스가 남아서 화면에 한동안 계속 나타나는 경우가 발생할 수 있다.

Dispose pattern 소개C#에서는 관리되지 않는 메모리(리소스)를 해제하는 용도로 System.IDisposable이라는 인터페이스를 제공한다. IDisposable 인터페이스를 상속받은 클래스라면 용도에 맞게 Dispose()를 구현해줘야 하는데 이는 FileStream 관련 객체들에서 많이 볼 수 있다.

리소스를 강제로 해제시키려면 직접 Release(), Delete(), Destroy(), Close() 등의 메소드를 만들어 사용하면 되는데 굳이 IDisposable을 사용할 필요가 있을까? 서로 다른 type의 객체여도 IDisposable 인터페이스를 상속받고 있다면, 하나의 코드로 다양한 type의 메모리를 정리할 수 있기 때문에 IDisposable을 사용할 필요가 있다. 또한 Dispose() 메소드만 보고도 “아, 이 클래스는 사용이 끝나면 Dispose()를 호출해서 메모리를 정리해야 하는구나” 라고 금방 알 수 있다.

캐릭터 객체에서 IDisposable 인터페이스를 구현해보자. 업데이트 목록에서도 제외시키고 렌더링 정보도 다 지우자. 캐릭터의 Dipsose()를 호출한 이후에 캐릭터는 어떠한 동작도 하지 못하게 된다. 물론 Dispose()를 호출한다고 캐릭터가 가비지 컬렉터에 의해 메모리 해제되는 것은 아니다.

WeakReference과 IDisosalbe의 조합원하는 시점에 메모리를 해제하려면 앞서 설명한 Weak Reference와 IDisposable을 개별적으로 사용하는 것으로는 부족하다. 둘을 함께 사용하는 것이 좋다. <리스트 10>을 보자.

<리스트 10> Disposable 인터페이스를 상속받아 구현된 캐릭터 클래스
namespace MyApp
{
    public class SampleChar : IDisposable
    {
        private IRenderObject m_Render = Renderer.CreateRenderObject();

        public void Dispose()
        {
            SampleCharManager.Remove(this);
            m_Render = null;
        }

        public bool isRemoved { get { return m_Render == null; } }
        public void Render()
        {
            if (m_Render == null) return;
            // 렌더링
        }

        public void Update() { }
    }
}

예제로 만들어 본 캐릭터 클래스는 Disposable 인터페이스를 상속받아 구현된다. Dispose 후에는 더 이상 업데이트가 되지 않도록 SampleCharManager에서 제거되며, 렌더링 객체를 null로 만들어 화면에 그려지지 않도록 했다.

IRenderObject 인터페이스는 <리스트 11>과 같이 구현된다.

<리스트 11> IRenderObject 인터페이스
namespace MyApp
{
    public interface IRenderObject
    {
        void Render();
    }

    public static class Renderer
    {
        public static IRenderObject CreateRenderObject()
{
return new DumyRenderObject(); // IRenderObject를 상속받은 더미 객체
}
    }
}

<리스트 12>의 캐릭터 매니저 클래스는 등록된 캐릭터들을 일괄적으로 업데이트시키고 렌더링한다.

<리스트 12> 등록 캐릭터 일괄 업데이트 및 렌더링
namespace MyApp 
{
    static class SampleCharManager
    {
        private static List<SampleChar> m_list = new List<SampleChar>();

        public static void Update()
        {
            foreach (SampleChar obj in m_list) 
                obj.Update();
        }

        public static void Render()
        {
            foreach (SampleChar obj in m_list)
                obj.Render();
        }

        public static void Add(SampleChar obj)
        {
            m_list.Add(obj); 
        }

        public static void Remove(SampleChar obj)
        {
            m_list.Remove(obj);
        }
    }
}

<리스트 13>의 디버깅을 위한 ‘캐릭터 위치 표시 객체’는 WeakReference를 통해 SampleChar 객체를 참조하도록 구현돼 있고, SampleCharManager에서 캐릭터를 삭제하더라도 안전하게 가비지가 회수된다. 업데이트 시 DisplayCharInfo는 삭제된 캐릭터를 스스로 판단해 목록에서 제거한다.

<리스트 13> 디버깅을 위한 캐릭터 위치 표시 객체
namespace MyDebug
{
    static class DisplayCharInfo
    {
        private static List<WeakReference> m_list = new List<WeakReference>();
        private static Queue<WeakReference> m_removeQueue =
new Queue<WeakReference>();

        public static void Update()
        {
            foreach (WeakReference item in m_list)
            {
                MyApp.SampleChar obj = (item.Target != null) ?
 item.Target as MyApp.SampleChar : null;

                if (obj == null || obj.isRemoved)
                {
                    m_removeQueue.Enqueue(item);
                }
                else 
                { 
                    /* 캐릭터 정보 표시 */ 
                }
            }

            while(m_removeQueue.Count > 0)
            {
                WeakReference item = m_removeQueue.Dequeue();
                m_list.Remove(item);
            }
        }

        public static void Add(MyApp.SampleChar obj)
        {
            m_list.Add(new WeakReference(obj));
        }
    }
}

C#에서 메모리를 관리하는 데 도움되길 바라며, 지금까지 설명한 내용을 요약하면 다음과 같다.

- string 조합이 많다면, StringBuilder 활용
- Immutable 객체의 값 접근 시 매번 메모리가 생성될 수 있으므로 주의
- 매번 호출되는 메소드 안에서 반복해서 일회성 인스턴스가 생성되지 않도록 주의
- Boxing / unboxing이 가능한 일어나지 않도록 주의
- WeakReference를 사용해서 의도하지 않은 참조 줄이기
- IDisposable 인터페이스를 사용해 사용자가 원하는 시점에 객체 삭제하기

Value type과 Reference type 비교

Value type은 stack 영역에 할당되며 값이 통째로 복사된다.

 

유니티 3D엔진에서의 메모리 관리
유니티 3D엔진으로 개발하면서 주의할 내용을 알아보자. 유니티 3D엔진은 크게 모노 메모리와 엔진에서 관리하는 메모리로 나뉜다. 둘 다 메모리가 부족하면 내부에 관리하는 heap 영역을 늘려 메모리를 할당한다. 이렇게 한 번 늘어난 heap은 줄어들지 않는 특징을 가진다. 물론 늘어난 heap 안에서 메모리가 재사용되므로, 무턱대고 늘어나진 않는다. 하지만 가비지를 너무 많이 생성시키면 GC.Collect()로 인한 성능저하와 더불어 최대 메모리가 늘어날 수도 있으니 주의해야 한다. 가능한 가비지가 덜 생성되도록 코드를 구현하는 게 좋다. 메모리는 한 번에 잡는 것이 좋고, caching이나 memory pool을 사용하는 것도 도움이 된다.
<리스트 14> Value typepublic static class Sample
{
    public static void TestValueType()
    {
        int a = 100;
        int b = a;
       
        a = 200;
        Console.WriteLine(" a={0}, b={1}", a, b);
    }
}

<리스트 14>를 실행하면 <그림 9>와 같은 결과를 확인할 수 있다. a와 b는 서로 다른 메모리 공간을 가지고 있다.





<그림 9> <리스트 14>의 실행 결과

Reference Type은 heap 영역에 할당되며, C/C++의 포인터나 레퍼런스처럼 new로 생성한 인스턴스를 참조한다.
<리스트 15> Reference type
public class MyInt
{
    public int Value { get; set; }
    public MyInt(int val) { this.Value = val; }

    public static void TestReferenceType()
    {
        MyInt a = new MyInt(100);
        MyInt b = a;

        a.Value = 200;
        Console.WriteLine(" a={0}, b={1}", a.Value, b.Value);
    }
}

<리스트 15>의 실행 결과로 <그림 10>을 확인할 수 있다. a와 b는 같은 메모리를 참조한다.





<그림 10> <리스트 15>의 실행 결과

 

 

*** 출처: 마이크로소프트웨어

*** 링크: 효과적인 C#메모리 기법

ref : https://semoa.tistory.com/988

반응형
반응형

소스 코드를 완벽하게 파악하고 있는 Accelerate Solutions 팀은 Unity 엔진을 최대한 활용할 수 있도록 수많은 고객을 지원합니다. 팀은 크리에이터 프로젝트를 심도 있게 분석하여 속도, 안정성, 효율성 등을 향상시키기 위해 최적화할 부분을 파악합니다. Unity에서 가장 경력이 많은 소프트웨어 엔지니어로 이루어진 이 팀과 함께 모바일 게임 최적화에 관한 전문적인 지식을 공유하는 자리를 마련했습니다. 

모바일 게임 최적화에 관한 인사이트를 공유하기 시작하면서, 원래 계획한 하나의 블로그 포스팅에 담기에는 너무나 방대한 정보가 있다는 사실을 알게 되었습니다. 따라서 이 방대한 지식을 한 권의 전자책(여기에서 다운로드 가능)과 75가지 이상의 실용적인 팁을 담은 블로그 포스팅 시리즈를 통해 제공하기로 했습니다.

이번 시리즈의 첫 게시물에서는 프로파일링, 메모리, 코드 아키텍처를 통해 게임 성능을 개선하는 방법을 자세히 들여다 봅니다. 이후 몇 주 이내에 두 개의 포스팅이 추가로 업로드될 예정입니다. 첫 번째는 UI 물리에 대해, 두 번째는 오디오와 에셋, 프로젝트 설정, 그래픽스에 대해 다룹니다.

시리즈 전체 내용을 지금 바로 확인하고 싶다면 무료로 전자책을 다운로드하세요.

그럼 시작하겠습니다.

프로파일링

모바일 성능 데이터의 프로파일링과 수집 및 활용 과정은 진정한 모바일 성능 최적화가 시작되는 지점입니다.

개발 초기부터 타겟 기기에서 자주 프로파일링 실행하기

Unity 프로파일러는 사용 시 애플리케이션에 대한 필수 성능 정보를 제공합니다. 출시가 멀지 않은 시점이 아닌 개발 초기에 프로젝트를 프로파일링하세요. 여러 오류나 성능 문제를 발생 즉시 조사하세요. 프로젝트의 '성능 시그니처'를 개발하면서 새로운 문제를 보다 쉽게 발견할 수 있습니다.

에디터에서 프로파일링을 수행하면 다양한 시스템의 상대적인 게임 성능에 관한 정보를 얻을 수 있는 반면, 각 기기를 대상으로 프로파일링하면 보다 정확한 인사이트를 확보할 수 있습니다. 가능하면 타겟 기기에서 개발 빌드를 프로파일링하세요. 지원할 기기 중 최고 사양의 기기와 최저 사양의 기기 모두를 프로파일링하고 모두 최적화해야 합니다.

Unity 프로파일러와 더불어 다음과 같은 iOS 및 Android의 네이티브 툴을 사용하면 각 엔진에서 추가로 성능 테스트를 수행할 수 있습니다.

특정 하드웨어는 Arm Mobile Studio, Intel VTune, 및 Snapdragon Profiler 등 추가 프로파일링 툴을 활용할 수 있습니다. 자세한 내용은 Profiling Applications Made with Unity 학습 자료를 참고하세요.

올바른 영역 최적화하기

게임 성능을 저하시키는 요인을 추정하거나 가정하지 않도록 합니다. Unity 프로파일러 및 플랫폼별 툴을 사용하여 성능 저하의 정확한 원인을 찾으세요. 

물론 여기에서 설명하는 최적화가 모두 애플리케이션에 적용되지는 않습니다. 다른 프로젝트에는 적합했던 최적화라도 현재 프로젝트에는 적용되지 않을 수 있습니다. 실제 병목 지점을 파악하고 실질적으로 최적화가 필요한 부분에 집중하세요.

Unity 프로파일러의 작동 방식 이해하기

Unity 프로파일러는 런타임 시 성능 저하 또는 중단의 원인을 감지하고 특정 프레임 또는 시점에 발생하는 상황을 보다 정확하게 이해하는 데 도움이 될 수 있습니다. CPU 및 메모리 트랙을 기본적으로 활성화하세요. 물리를 많이 사용하는 게임이나 음악에 기반한 게임플레이를 개발 중이라면 필요에 따라 렌더러, 오디오, 물리와 같은 보조 프로파일러 모듈을 모니터링할 수 있습니다.

 
Unity 프로파일러를 사용하여 애플리케이션의 성능 및 리소스 할당 테스트확장

Development Build  Autoconnect Profiler를 선택하여 기기에 애플리케이션을 빌드하거나 수동으로 연결하여 앱 시작 시간을 단축하세요.

 
확장

프로파일링을 실행할 타겟 플랫폼을 선택하세요. 녹화 버튼은 애플리케이션 재생을 몇 초 동안 추적합니다(기본 300 프레임). 더 오래 캡처해야 한다면 Unity > Preferences > Analysis > Profiler > Frame Count로 이동하여 이 값을 2000까지 늘릴 수 있습니다. 이렇게 하면 Unity 에디터가 더 많은 CPU 작업을 수행하고 더 많은 메모리를 사용하지만, 상황에 따라 유용할 수 있습니다.

이는 계측 기반 프로파일러로 ProfileMarkers에 명시적으로 래핑된 코드 타이밍을 프로파일링합니다(예: MonoBehaviour의 Start나 Update 메서드 또는 특정 API 호출). 또한 Deep Profiling 설정을 사용하면 Unity는 스크립트 코드에 있는 모든 함수 호출의 시작과 끝을 프로파일링하여 정확히 애플리케이션의 어느 부분에서 성능 저하가 발생하는지 제시합니다.

 
타임라인 뷰를 사용하여 CPU 바운드 또는 GPU 바운드 여부 판단확장

게임을 프로파일링할 때는 성능 불안정과 평균 프레임의 비용을 모두 살펴보는 것이 좋습니다. 각 프레임에서 발생하는 높은 비용의 작업을 파악하고 최적화하면 타겟 프레임 속도 이하에서 실행되는 애플리케이션에 더 유용할 수 있습니다. 스파이크 현상을 살펴볼 때는 물리, AI, 애니메이션 등의 고비용 작업을 먼저 검토한 다음 가비지 컬렉션을 살펴봅니다.

창을 클릭하여 특정 프레임을 분석하세요. 그리고 나서 타임라인 또는 계층 구조 뷰를 사용합니다. 

  • 타임라인: 특정 프레임의 타이밍을 시각적으로 요약하여 제시합니다. 이를 통해 각 활동이 다양한 스레드 전반에서 서로 어떤 관계를 맺고 있는지 시각화할 수 있습니다. 이 옵션을 사용하여 CPU 바운드 또는 GPU 바운드 여부를 판단하세요.
  • 계층 구조: 그룹화된 ProfileMarkers의 계층 구조를 제시합니다. 이를 통해 밀리초 단위(Time ms  Self ms)의 시간 비용을 기준으로 샘플을 정렬할 수 있고, 함수에 대한 호출 수와 프레임의 관리되는 힙 메모리(GC Alloc)의 양도 알 수 있습니다.
 
계층 구조 뷰를 통해 시간 비용에 따라 ProfileMarkers 정렬확장

여기에서 Unity 프로파일러의 전체 개요를 읽어보세요. 프로파일링이 생소하다면 Unity 프로파일링 소개 영상을 시청해 보세요.

프로젝트에서 최적화를 수행하기 전에 먼저 Profiler .data 파일을 저장하세요. 변경 사항을 구현하고 수정 이전  이후에 저장된 .data 파일을 비교해 보세요. 반복적인 프로파일링, 최적화, 비교를 통해 성능을 향상할 수 있습니다. 이후에도 이 과정을 반복합니다.

Profile Analyzer 사용하기

이 툴을 사용하면 Profiler 데이터의 여러 프레임을 집계한 다음 관심 있는 프레임을 찾을 수 있습니다. 프로젝트를 변경하면 Profiler에 어떤 영향을 주는지 확인하고 싶으신가요? Compare 뷰를 사용하면 두 데이터 세트를 로드해서 차이점을 확인할 수 있으므로 변경 사항을 테스트하고 결과를 개선할 수 있습니다. Profile Analyzer는 Unity의 패키지 관리자를 통해 사용할 수 있습니다.

 
기존 Profiler를 보완하는 Profile Analyzer로 프레임과 마커 데이터 확인확장

프레임당 정해진 시간 예산으로 작업하기 

각 프레임은 목표로 하는 초당 프레임 수(fps)를 기반으로 시간 예산을 가집니다. 30fps로 실행되는 애플리케이션의 경우 프레임당 약 33.33ms(1000ms/30fps)를 허용하는 것이 이상적입니다. 마찬가지로 60fps의 경우 목표 시간 예산은 프레임당 16.66ms입니다.

컷씬이나 로딩 시퀀스 같은 짧은 시간 동안 기기는 예산을 초과할 수도 있지만, 긴 시간 동안 초과할 수는 없습니다.

기기 온도 고려하기

모바일 기기에서는 기기가 과열되거나 OS가 CPU 및 GPU를 서멀 스로틀링할 수 있으므로 최대 시간을 지속적으로 사용하지 않는 것이 좋습니다. 프레임 간 쿨다운을 위해 가용 시간의 약 65%만 사용할 것을 권장합니다. 일반적인 프레임 예산은 30fps에서는 프레임당 약 22ms, 60fps에서는 11ms입니다. 

대부분의 모바일 기기에는 데스크톱과 달리 냉각 장치가 없습니다. 물리적인 열기가 성능에 직접적인 영향을 미칠 수 있습니다.

기기가 가열되면 장기적인 문제로 이어지지 않는다 해도 Profiler에서 성능 저하를 인식하고 이를 보고할 수 있습니다. 프로파일링 과열을 방지하기 위해 짧은 간격으로 프로파일링하세요. 이렇게 하면 기기가 냉각되고 실제 사용 조건에서 시뮬레이션됩니다. 일반적으로 다시 프로파일링하기 전에 10~15분 간 기기를 냉각하는 것이 바람직합니다.

GPU 바운드 또는 CPU 바운드 여부 판단하기

Profiler를 통해 CPU가 프레임 예산을 할당된 양보다 오래 사용 중이거나 GPU에 문제가 있는지 파악할 수 있습니다. 다음과 같이 Gfx가 접두어인 마커를 방출하는 방식을 사용합니다.

  • Gfx.WaitForCommands 마커가 표시되면 렌더 스레드가 준비되었으나 메인 스레드에 병목 현상이 일어날 가능성을 의미합니다.
  • Gfx.WaitForPresent가 빈번하게 발생하면 메인 스레드는 준비되었으나 GPU가 프레임을 표시하기를 기다리는 중이었음을 의미합니다.
메모리

Unity는 사용자 생성 코드 및 스크립트에 자동 메모리 관리를 사용합니다. 값이 입력된 로컬 변수처럼 작은 데이터는 스택에 할당됩니다. 더 큰 데이터와 장기 스토리지는 관리되는 힙에 할당됩니다.

가비지 컬렉터는 사용되지 않은 힙 메모리를 주기적으로 파악하여 할당을 해제합니다. 이 작업은 자동으로 실행되지만, 힙의 모든 개체를 검사하는 과정에서 게임이 끊기거나 느리게 실행될 수 있습니다.

메모리 사용량을 최적화하려면 힙 메모리의 할당 및 할당 해제 시점뿐 아니라 가비지 컬렉션의 영향을 최소화하는 방법을 알아야 합니다. 자세한 내용은 관리되는 힙의 이해를 참조하세요.

 
Memory Profiler에서 스냅샷 캡처, 검토 및 비교확장

Memory Profiler 사용하기 

Memory Profiler는 별도 애드온(패키지 관리자에서 실험 패키지 또는 프리뷰 패키지로 제공)으로 관리되는 힙 메모리의 스냅샷을 캡처하여 단편화 및 메모리 누출 같은 문제를 식별할 수 있습니다.

메모리에 보관된 네이티브 오브젝트에 대한 변수를 추적하려면 Tree Map 뷰를 클릭하세요. 여기에서는 과도하게 큰 텍스처 또는 중복 에셋과 같은 일반적인 메모리 사용량 문제를 확인할 수 있습니다.

Unity의 Memory Profiler를 활용하여 메모리 사용량을 개선하는 방법을 알아보세요. 공식 Memory Profiler 기술 자료도 확인할 수 있습니다.

가비지 컬렉션의 영향 줄이기

Unity는 Boehm-Demers-Weiser 가비지 컬렉터를 사용하며, 이 컬렉터는 프로그램 코드의 실행을 중단하고 작업이 완료될 때만 일반 실행을 재개합니다. 

힙의 불필요한 할당은 GC(가비지 컬렉션) 스파이크를 유발할 수 있으므로 유의해야 합니다. 

  • 문자열: C#에서 문자열은 값 유형이 아닌 참조 유형입니다. 불필요한 문자열의 생성 또는 조작을 줄이세요. JSON, XML과 같은 문자열 기반 데이터 파일은 구문 분석하지 않는 것이 좋습니다. 데이터를 ScriptableObjects 또는 MessagePack이나 Protobuf 같은 형식으로 대신 저장하세요. 런타임에서 문자열을 빌드해야 하는 경우 StringBuilder 클래스를 사용합니다.
  • Unity 함수 호출: 일부 함수는 힙 할당을 생성합니다. 참조를 루프 도중에 할당하지 말고 배열에 저장하세요. 아울러 가비지 생성을 방지하는 함수들을 활용하세요. 예를 들어 문자열을 GameObject.tag와 직접 비교하는 대신 GameObject.CompareTag를 사용하는 방법이 있습니다(새로운 문자열 반환은 가비지를 생성합니다).
  • 박싱: 참조 유형 변수 대신 값 유형 변수를 전달하지 않도록 합니다. 이렇게 하면 임시 오브젝트가 생성되며 동반되는 잠재적 가비지가 값 유형을 암묵적으로 타입 오브젝트로 변환합니다(예: int i = 123, object o = i). 대신 전달하려는 값 유형에 구체적인 오버라이드를 제공해 보세요. 이러한 오버라이드에는 제네릭이 사용될 수도 있습니다.
  • 코루틴: yield는 가비지를 생성하지 않지만 새로운 WaitForSeconds 오브젝트를 만들면 가비지가 생성됩니다. yield 라인에서 생성하는 대신 WaitForSeconds 오브젝트를 캐시하고 재사용하세요.
  • LINQ 및 정규식: 두 가지 모두 박싱에서 가비지를 생성합니다. 성능이 문제라면 LINQ와 정규식을 사용하지 마세요. 새로운 배열을 만드는 대신 for 루프와 리스트를 사용하세요.

가능한 경우 가비지 컬렉션 측정하기

가비지 컬렉션 멈춤 현상이 게임의 특정 지점에 영향을 주지 않는다면 System.GC.Collect로 가비지 컬렉션을 트리거할 수 있습니다.

자동 메모리 관리의 이해에서 이 방식을 활용하는 방법을 참고하세요.

점진적 가비지 컬렉터를 활용하여 GC 워크로드 분할하기 

점진적 가비지 컬렉션을 사용하면 프로그램 실행 중에 한 번 길게 중단되는 것이 아니라 훨씬 짧은 중단이 여러 프레임에 걸쳐 여러 번 나타납니다. 가비지 컬렉션이 성능에 영향을 미친다면 이 옵션을 사용하여 GC 스파이크를 줄일 수 있는지 확인해 보세요. Profile Analyzer를 사용하여 이 방식이 애플리케이션에 어떤 이점으로 작용하는지 확인하세요.

 
점진적 가비지 컬렉터를 사용하여 GC 스파이크 줄이기확장
프로그래밍 및 코드 아키텍처

Unity PlayerLoop에는 게임 엔진의 코어와 상호작용하기 위한 함수가 포함되어 있습니다. 이 구조는 초기화와 프레임별 업데이트를 처리하는 다양한 시스템을 포함합니다. 모든 스크립트가 이 PlayerLoop를 활용하여 게임플레이를 생성하게 됩니다.

프로파일링 시에는 PlayerLoop 아래에 프로젝트의 사용자 코드가 표시됩니다(EditorLoop 아래에는 에디터 컴포넌트).

 
전체 엔진 실행의 맥락에서 커스텀 스크립트, 설정, 그래픽스를 제시하는 Profiler확장
 
확장

PlayerLoop 및 스크립트의 수명 주기를 파악하세요.

다음과 같은 유용한 팁으로 스크립트를 최적화할 수 있습니다.

Unity PlayerLoop 이해하기 

Unity 프레임 루프의 실행 순서를 이해해야 합니다. 모든 Unity 스크립트는 사전에 정해진 순서대로 여러 이벤트 함수를 실행합니다. Awake, Start, Update 및 스크립트의 수명 주기를 생성하는 다른 함수들 사이의 차이점을 이해해야 합니다. 

이벤트 함수의 구체적인 실행 순서는 스크립트 수명 주기 플로우 차트를 참고하세요.

매 프레임에 실행되는 코드 최소화하기 

코드를 반드시 모든 프레임에 실행해야 하는지 확인하세요. 불필요한 로직을 Update, LateUpdate, FixedUpdate에서 제외하세요. 이러한 이벤트 함수에는 프레임마다 업데이트해야 하는 코드를 편리하게 배치할 수 있으며, 같은 빈도로 업데이트할 필요가 없는 로직은 추출됩니다. 가능하다면 상황이 바뀌는 경우에만 로직을 실행하세요.

반드시 Update를 사용해야 한다면 n개 프레임마다 코드를 실행하는 방안을 검토해 보세요. 이는 여러 프레임에 대규모 워크로드를 분산하는 일반적인 기법인 타임 슬라이싱의 적용 방식 중 하나이기도 합니다. 이 예에서는 3개 프레임마다 한 번씩 ExampleExpensiveFunction을 실행합니다.

 

Start/Awake에서 대규모 로직 사용 방지 

첫 번째 씬을 로드하면 다음과 같은 함수가 각 오브젝트에 대해 호출됩니다.

  • Awake
  • OnEnable
  • Start

애플리케이션이 첫 번째 프레임을 렌더링하기 전까지는 이러한 함수에서 고비용 로직의 사용을 피하세요. 그렇게 하지 않으면 필요 이상으로 로딩 시간이 길어질 수 있습니다.

첫 번째 씬 로드에 대한 자세한 내용은 이벤트 함수의 실행 순서를 참조하세요.

빈 Unity 이벤트 방지 

빈 MonoBehaviours도 리소스를 필요로 하므로 비어 있는 Update 또는 LateUpdate 메서드는 제거해야 합니다.

테스트에 이러한 메서드를 사용하고 있다면 다음과 같은 프리 프로세서 지시문을 사용하세요.

 

빌드에 불필요한 오버헤드가 유입되는 일 없이 에디터 내에서 Update를 자유롭게 사용하여 테스트할 수 있습니다.

Debug Log 구문 제거하기 

Log 구문, 특히 Update, LateUpdate 또는 FixedUpdate에 있는 Log 구문은 성능을 낮출 수 있습니다. 빌드를 만들기 전에 Log 구문을 비활성화하세요.

이 작업을 보다 쉽게 하려면 프리 프로세서 지시문과 함께 조건부 속성을 만드는 것이 좋습니다. 예를 들어 다음과 같은 커스텀 클래스를 만듭니다.

 
 
커스텀 프리 프로세서 지시문을 추가하여 스크립트 분리확장

커스텀 클래스로 로그 메시지를 생성합니다. Player Settings에서 ENABLE_LOG 프리 프로세서를 비활성화하면 모든 Log 구문이 동시에 사라집니다.

문자열 파라미터 대신 해시 값 사용하기 

Unity는 내부적으로 애니메이터나 머티리얼, 셰이더 프로퍼티를 식별할 때 문자열 이름을 사용하지 않습니다. 빠른 처리를 위해 모든 프로퍼티 이름이 프로퍼티 ID에 해시되어 있으며, 해당 ID가 실제로 프로퍼티를 식별하는 데 사용됩니다.

애니메이터, 머티리얼 또는 셰이더에서 Set 또는 Get 메서드를 사용하는 경우에는 문자열 값 메서드 대신 정수 값 메서드를 사용하세요. 문자열 메서드 역시 문자열 해싱을 수행한 다음 해시된 ID를 정수 값 메서드로 전달하기 때문입니다.

애니메이터 프로퍼티 이름에는 Animator.StringToHash를, 머티리얼 및 셰이더 프로퍼티 이름에는 Shader.PropertyToID를 사용하세요.

올바른 데이터 구조 선택

한 번 선택한 데이터 구조는 프레임당 수천 번씩 반복되므로 효율성에 영향을 줍니다. 컬렉션에 리스트, 배열 또는 딕셔너리 중 무엇을 사용해야 할지 고민하고 계신가요? 올바른 자료 구조를 선택하기 위한 일반적인 가이드인 C#의 데이터 구조 MSDN 가이드를 참고하세요.

런타임 시 컴포넌트 추가 방지 

런타임에 AddComponent를 호출하면 비용이 발생합니다. Unity는 런타임에 컴포넌트가 추가될 때마다 중복 또는 기타 필요한 컴포넌트를 확인해야 합니다. 

이미 설정된 원하는 컴포넌트로 프리팹을 인스턴트화하는 것이 일반적으로 더 효과적입니다.

게임 오브젝트 및 컴포넌트 캐시하기 

GameObject.Find, GameObject.GetComponent, Camera.main(2020.2 이전 버전)은 비용이 많이 들 수 있으므로 Update 메서드에서는 호출하지 않는 것이 가장 좋습니다. 대신 Start에서 호출하고 결과를 캐시하세요.

다음 예는 반복적인 GetComponent 호출의 비효율성을 보여줍니다.

 

함수의 결과가 캐시되므로 GetComponent는 한 번만 호출하세요. Update에서 GetComponent를 추가로 호출하지 않아도 캐시된 결과를 재사용할 수 있습니다.

 

오브젝트 풀 사용하기 

Instantiate 함수  Destroy 함수는 가비지와 가비지 컬렉션 스파이크를 야기하며, 일반적으로 속도가 느린 프로세스입니다. 게임 오브젝트를 계속 인스턴트화하고 삭제하기보다는(예: 총알 발사) 재사용 및 재활용할 수 있는 사전에 할당된 오브젝트 을 사용해 보세요.

 
재사용 가능한 PlayerLaser 인스턴스 20개를 생성하는 ObjectPool의 예확장

CPU 스파이크가 적을 때 게임 내 한 지점(예: 메뉴 화면)에서 재사용 가능한 인스턴스를 생성합니다. 컬렉션으로 이러한 오브젝트 '풀'을 추적합니다. 게임플레이 중에는 필요할 때 다음으로 사용 가능한 인스턴스를 사용 설정하고, 오브젝트를 삭제하는 대신 사용 중지한 다음 풀로 돌려 보내면 됩니다.

 
비활성 및 발사 준비 상태의 PlayerLaser 오브젝트 풀확장

이렇게 하면 프로젝트에서 관리되는 할당의 수가 줄어들기 때문에 가비지 컬렉션 문제를 방지할 수 있습니다.

Unity에서 간단한 오브젝트 풀링 시스템을 만드는 방법은 여기에서 알아보세요.

스크립터블 오브젝트 사용하기 

변하지 않는 값 또는 설정은 MonoBehaviour가 아닌 스크립터블 오브젝트에 저장하세요. 스크립터블 오브젝트는 한 번만 설정하면 되는 프로젝트 내부의 에셋으로 게임 오브젝트에 직접 연결할 수 없습니다.

스크립터블 오브젝트에서 필드를 생성하여 값 또는 설정을 저장한 다음 MonoBehaviours에서 스크립터블 오브젝트를 참조하세요.

 
다양한 게임 오브젝트의 설정을 보관하는 Inventory 스크립터블 오브젝트확장

스크립터블 오브젝트의 필드를 사용하면 MonoBehaviour로 오브젝트를 인스턴스화할 때마다 데이터의 불필요한 중복을 방지할 수 있습니다.

스크립터블 오브젝트 소개 튜토리얼에서 스크립터블 오브젝트의 장점을 알아보세요. 관련 기술 자료는 여기에서도 찾을 수 있습니다.

모바일 성능 팁 전체 목록 다운로드

다음 블로그 포스팅에서는 그래픽스 및 GPU 최적화에 대해 자세히 알아보겠습니다. 팀에서 제공하는 유용한 팁 목록을 모두 보고 싶다면 여기에서 전자책으로 참고하실 수 있습니다.

 

 

ref : https://blog.unity.com/kr/technology/optimize-your-mobile-game-performance-tips-on-profiling-memory-and-code-architecture

반응형

'게임엔진(GameEngine) > Unity3D' 카테고리의 다른 글

렌더링 순서 - Queue 태그  (0) 2022.12.24
효과적인 C# 메모리 관리 기법  (0) 2022.12.04
C#은 stackless 코루틴  (0) 2022.12.04
Unity vertex / fragment Shader01 -빨강 쉐이더  (0) 2022.12.02
Keyword Boolean  (0) 2022.11.27
반응형

C#은 stackless한 코루틴을 지원합니다

  • stackless 코루틴은 stackful 코루틴처럼 사기군같은 컨텍스트 스위칭 없이 자체 상태머신을 이용한 멀쩡해보이는 방법으로 구현한다.

Stackful Coroutines vs Stackless Coroutines

Stackful Coroutines

먼저 스택풀 코루틴(Stackful Coroutine)은 함수 호출시에 사용되는 분리된 스택을 가지고 있다. 코루틴이 어떻게 동작하는지 정확하게 이해하기 위해서 로우레벨 관점에서 간단히 함수 프레임과 함수 호출에 대해서 살펴볼 것이다. 하지만 먼저 스택풀 코루틴의 특성에 대해서 알아보자.

  • 스택풀 코루틴은 자신만의 스택을 가지고 있다.
  • 스택풀 코루틴의 수명은 호출한 코드로부터 독립적이다.

스택이란 것은 메모리 관점에서 볼 때 연속된 메모리 블럭이며, 스택은 지역변수 및 함수 인자등을 저장하기 위해 필요하다. 하지만 더 중요한 것은 각 함수 호출 후에 추가적인 정보가 스택에 배치되어 호출된 함수가 피호출자(callee)에게 반환하고 프로세서 레지스터를 복원하는 방법을 알려주게 된다.

몇가지 레지스터들은 특정 목적을 가지고 있으며 함수 호출시 스택에 저장된다. 이러한 레지스터는 다음과 같다.

  • SP – Stack Pointer
  • LR – Link Register
  • PC – Program Counter

스택포인터 (Stack Pointer)는 현재 함수 호출에 속하는 스택의 시작주소를 가지고 있는 레지스터다. 이 값 덕분에 스택에 저장된 인수와 지역 변수를 쉽게 참조할 수 있다.

링크 레지스터(Link Register) 함수 호출에서 매우 중요하다. 현재 함수 실행이 끝난 후 실행할 코드가있는 반환 주소(수신자 주소)를 저장한다. 함수가 호출되면 PC가 LR에 저장되고, 함수가 반환되면 LR을 사용하여 PC가 복원된다.

프로그램 카운터(Program counter)는 현재 실행중인 명령어의 주소다.

함수가 호출 될 때마다 링크 레지스터가 저장되므로 함수가 완료된 후 반환 할 위치를 알 수 있다.

함수의 호출과 반환에서 PC 및 LR 레지스터의 동작

스택풀 코루틴이 실행되면 호출 된 함수는 이전에 할당 된 스택을 사용하여 인수와 지역 변수를 저장한다. 함수 호출은 스택풀 코루틴에 대한 모든 정보를 스택에 저장하기 때문에 코루틴에서 호출되는 모든 함수들의 실행을 중단 할 수 있다.

Stackless Coroutines

스택리스 코루틴(Stackless Coroutine)은 스택풀 코루틴과 비교했을 때 약간의 차이점이 있다. 그러나 주요 특징은 여전히 시작(started)되고, 스스로 중단(suspended) 된 후에 다시 재개(resumed)할 수 있다는 점이다.

스택리스 코루틴의 특징은 다음과 같다.

  • 코루틴은 호출자(caller)와 강하게 연결되어 있다. 코루틴에 대한 호출은 실행을 코루틴으로 전송하고 코루틴에서 양보(yield)하는 것은 호출자에게 돌아온다.
  • 스택풀 코루틴은 스택의 수명만큼 유지 되지만, 스택리스 코루틴은 객체의 수명만큼 유지된다.

그러나 스택리스 코루틴의 경우 전체 스택을 할당할 필요가 없기 때문에 훨씬적은 메모리를 요구하지만, 그 때문에 몇가지 제약사항이 생긴다.

스택이 없다면 메모리에서 스택을 할당하지 못하면 어떻게 동작하는거지? 스택에 저장될 모든 데이터는 어디로 가는걸까?라는 의문이 생길 수 있다.  정답은 바로 호출자의 스택을 사용하는 것이다.

스택리스 코루틴은 최상위 레벨(top-level) 함수에서만 스스로를 중단(suspend) 할수 있다. 일반적인 함수의 경우, 데이터가 호출 수신자 스택에 할당되기 때문에 코루틴에서 호출 된 모든 함수는 코루틴이 중단되기 전에 완료되어야 한다. 코루틴이 상태를 유지하는 데 필요한 모든 데이터는 Heap 메모리에 동적으로 할당된다. 이것은 일반적으로 미리 할당 된 전체 스택보다 크기가 훨씬 작은 두 개의 지역 변수와 인수를 사용한다.

코루틴 중단 및 재개에서 기억해야 할 데이터가 훨씬 적지만 코루틴은 최상위 레벨 함수에서만 일시 중단하고 자신을 반환 할 수 있다. 모든 함수 및 코루틴 호출은 동일한 방식으로 발생하지만 코루틴의 경우 일부 추가 데이터를 호출에서 보존해야 중단 지점으로 이동하고 지역 변수의 상태를 복원하는 방법을 알 수 있다. 게다가 함수 프레임과 코루틴 프레임 사이에는 차이가 없다.

코루틴은 다른 코루틴을 호출 할 수도 있다. 스택리스 코루틴의 경우, 하나에 대한 각 호출이 새로운 코루틴 데이터를 위한 새로운 공간을 할당하게 된다. (코루틴에 대한 여러 호출은 다양한 동적 메모리 할당을 유발할 수 있음).

코루틴을 사용하기 위해서는 코루틴을 지원하는 언어가 필요한 이유는 컴파일러가 코루틴의 상태를 설명하는 변수를 결정하고 중단 지점으로의 점프를 위한 보일러 플레이트 코드를 생성해야 하기 때문이다.

 

 

ref : https://pjc0247.tistory.com/100

ref : https://www.charlezz.com/?p=44635 

반응형
반응형

금일은 요즘 유니티를 사용하면서 문제점에 부디친 URP 때문에
유니티 쉐이더를 다시 들여다 보려고 합니다.(역시 기본을 아는 것이 가장 중요한 것 같습니다.)

기존의 유니티 쉐이더는 3가지 방식으로 작성될 수 있었습니다.

ShaderLab, Surface Shader 그리고
전통적인 방식인 vertex, fragment 쉐이더 방식으로 작성되어오고 있습니다.

작성법도 편하고 빛계산도 비교적 수월한 Surface 쉐이더를 많이 사용하고 있는 것 같은데요 최근의 URP 파이프라인으로 넘어오면서는 전통적인 HLSL
(vertex, fragment )방식으로 쉐이더가 작성되고 있습니다. (물론 쉐이더그래프를 지원하며 더 직관적인것 같긴 하지만…. 그 원리를 모른다면 매번 문제에 직면 하는 것 같습니다.)
그래서 이전의 vertex, fragment 쉐이더방식을 처음부터 차근차근 되짚어 보려고 합니다.

그 중에서 모든 쉐이더 공부의 첫 시작은 빨갱이 쉐이더인 것 같아서
버텍스쉐이더와 프래그먼트쉐이더를 사용해서 빨강 쉐이더를 만들어 보려고 합니다. (URP이전의 쉐이더 입니다.)

정말 간결하고 아름답습니다.
왜 아름다운지 그 구조부터 들여다 보겠습니다. 유니티 쉐이더의 구조는

좌측 그림처럼 Shader 안에 Properties(속성) 안에 SubShader (여러개가 존재할수 있습니다.) 그리고 마지막에 fallback 이라는 구조로 구성되어 있습니다.(참조 https://jinhomang.tistory.com/43)

그런데 왜 서브쉐이더라는 것이 존재하며 여러개 존재할 수 있을까요?
바로 하드웨어의 성능 때문이라고 합니다.
하드웨어 마다 다양한 성능을 가지고 있기때문에 거기에 적합한 쉐이더들을 각각의 서브쉐이더에 작성을 해둔다고 합니다.

유니티가 렌더링을 할때 서브쉐이더를 들여다 보면서 가장 위의 서브쉐이더(가장 최상품질)부터 찾아본후 그에 적합한 서브쉐이더를 적용하게 되며
마지막까지 적합한 스펙이 없으때 fallBack 이라는 부분에 적혀 있는 쉐이더를 사용하게 된다고 합니다.

https://jinhomang.tistory.com/43 흑기사님의 글이 정말 주옥같습니다.!

그리고 서브쉐이더 안에는 여러개의 pass가 존재하는데요 오브젝트의 렌더링되는 단위라고 합니다. 즉 한번만 그리는 것이 아니라 pass를 통해 여러번 단계로 그릴수 있다는 것을 의미합니다.

자 이제 빨강쉐이더로 돌아가 보겠습니다.

Shader “CustomRout/CustomShader”{
// 쉐이더의 이름과 경로를 정해주게 됩니다. 쉐이더 선택창에 CustomRout 안에 CustomShader로 보여집니다.
Properties{
_Color(“MainColor”, Color) = (1,1,1,1)
}
// 속성을 적용할수 있는 부분이며 메터리얼 창에 나타나게 됩니다. 메터리얼 창에 MainColoe 로 보여지며 디폴트값은 1,1,1,1 즉 화이트로 보여지게 됩니다.

//서브쉐이더가 시작됩니다.
SubShader
{
Tags { “RenderType”=”Opaque” } // 렌더타입을 적어줍니다. 이후에 알아봅니다.

LOD 100

// 패스가 시작됩니다.
Pass
{

// 실직적인 쉐이더 프로그래밍은 CGPROGRAM ~ ENDCG 안에서 이루어 집니다.
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

// 버텍스 함수는 vert라는 함수가 사용될것이다
// 프래그먼트 함수는 frag 라는 함수가 사용될것이다, 즉 vert 와 frag는 사용자가 이름을 바꾸어줄 수도 있다는 뜻입니다.

struct vertexInput
{
float4 vertex : POSITION;
};

//버텍스 입력을 받는 구조체입니다.
float4 형식의 vertex 가 POSITION 값을 가지고 있습니다.

struct vertexOutput
{
float4 pos : SV_POSITION;
};

//버텍스 출력을 하는 구조체입니다. float4 형식의 pos 가 SV_ POSITION 값을 가지고 있습니다. SV_POSITION 은 버텍스의 위치를 출력해준다라고 생각하시면 됩니다.

float4 _Color;

//프라퍼티에 적용하였던 값이며 꼭 한번은 선언을 해주어야 작동하게 됩니다.

vertexOutput vert (vertexInput v)

{
vertexOutput o;
o.pos = UnityObjectToClipPos(v.vertex);

// 버텍스함수입니다.

버텍스 인풋구조체 v의 값을 인자로 가집니다.
o의 오브젝트 공간의 위치를 v의 버텍스위치에서 클립공간으로 바꾸어줍니다.

오브젝트 공간의 한 점을 동질적인 좌표에 있는 카메라의 클립 공간으로 변환합니다. mul(UNITY_MATRIX_MVP, float4(pos, 1.0))과 같으며, 이를 대신하여 사용해야 합니다.
로컬 공간(오브젝트 공간) 의 버텍스를 클립공간으로 변환합니다. 기본적으로 공간변환은 로컬 — 월드 — 뷰 — 프러스텀인데, 이걸 모델-뷰-프러스텀이라고 부르면서 MVP라고 부릅니다. 그런데 프러스텀은 니어와 파로 잘라지죠? 그러면 절두체 모양이 됩니다. 일종의 찌그러진 큐브죠. 이 큐브 영역을 0~ 1사이로 변환한 것이 클립 포지션이라 합니다. 즉 최종 영역까지 한 번에 변환하는 함수입니다.

출처: https://chulin28ho.tistory.com/539 [대충 살아가는 게임개발자]

return o; // o구조체를 반환홥니다.
}

//프래그먼트 함수입니다.

fixed4 frag (vertexOutput i) : SV_Target

{

float4 col = float4(1.0,0.0,0.0,1.0);

return col;

//레드컬러를 반환합니다.

}

ENDCG

}

}

}

이상으로 기본적인 짧은 unlit 빨강 쉐이더를 알아보았습니다.
(참고 unlit 은 unlighting 즉 빛이 적용되지않는다! 라는 뜻입니다.

 

 

ref : https://medium.com/@p5js/unity-vertex-fragment-shader01-%EB%B9%A8%EA%B0%95-%EC%89%90%EC%9D%B4%EB%8D%94-a3fdebbc0c8f

반응형
반응형

 

간단한 기능인데 : on, off 기능으로 에디터에서 default 값을 어떻게 선택하느냐에 따라 on, off 중 하나만 out 으로 내보낸다

 

 


 

여기서 맨 밑의 Keyword는 약간 다른 종류인데, 이 Keyword 하위에 있는 요소들(Boolean, Enum)은 셰이더의 정적 분기를 위한 노드라고 한다.

경우에 따라서 출력되는 게 다른 상황에서, 셰이더의 Variant를 만들어내기 위해서 나온 기능이라고 하는데,

최적화에는 별로 좋지 않다고 하고..

나뉘는 경우의 수마다 다른 셰이더로 쳐 진다고 한다.

 

직접 사용해보자. 우선은 Keyword 아래의 Boolean부터 꺼낸다.

 

이 Boolean은 인스펙터 창에서 체크박스 형태로 나타나게 되는데,

그 체크박스의 체크 여부에 따라서 최종 결과물이 달라지는 형태이다.

즉, 체크박스에 체크가 되어있다면 위에서 On의 결과물이 Out으로 나가게 될 것이고,

반대로 체크가 되어있지 않다면 Off의 결과물이 Out으로 나가게 될 것이다.

 

간단하게 예제를 만들어보자.

아까 위에서 만들었던 곱하기의 결과물이 On으로, 그리고 곱하기를 하지 않은 Sample Texture 2D 상태가 Off로 들어오도록 노드를 연결해준다.

그런 다음 Boolean을 Base Color에 연결해준다.

그러면 이런 모습이 되고,

직접 씬에서 본다면

 

이렇게 Boolean의 On/Off 여부에 따라 결과물이 달라지는 것을 볼 수 있다.

 

 

 

 


Boolean Keywords

Boolean Keywords are either on or off. This results in two shader variants. Shader Graph uses the value in the Reference name field for the on state, and automatically defines the off state as an underscore ( _ ).

To expose a Boolean Keyword in the Material Inspector, its Reference name must include the _ON suffix. For example, BOOLEAN_A506A032_ON.

 

 

ref : https://docs.unity3d.com/Packages/com.unity.shadergraph@8.1/manual/Keywords.html

ref : https://rusalgames.tistory.com/34

반응형

'게임엔진(GameEngine) > Unity3D' 카테고리의 다른 글

C#은 stackless 코루틴  (0) 2022.12.04
Unity vertex / fragment Shader01 -빨강 쉐이더  (0) 2022.12.02
Unity Audio  (0) 2022.11.23
UnityEngine.Random  (0) 2022.11.16
UI 버튼 바인딩  (1) 2022.11.06
반응형

 

 

In Unreal Engine blueprint widget, you can create different shapes through an array of points. There is no line drawing in the user widget with an array of points in C++, so we will use UWidgetBlueprintLibrary. In order to draw the shape, we must use UWidgetBlueprintLibrary::DrawLines. And then another trouble awaits us — we cann’t override OnPaint.

All animals are equal, but some animals are more equal than others.

So, we’ll just override NativePaint.

int32 UPaintWidget::NativePaint(const FPaintArgs &Args, 
const FGeometry &AllottedGeometry, const FSlateRect &MyCullingRect, FSlateWindowElementList &OutDrawElements, int32 LayerId, const FWidgetStyle &InWidgetStyle, bool bParentEnabled) const
{
    Super::NativePaint(Args, AllottedGeometry, MyCullingRect,  OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);    FPaintContext Context(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);    UWidgetBlueprintLibrary::DrawLines(Context, Points, FLinearColor::Blue, true, 10.0f);    return LayerId + 1;
}

We simply created a variable of the array of points in the class and by updating this array, our shape is also updated.

The complete code of this project is available on GitHub.

 

 

https://medium.com/@qerrant/gesture-recognizer-for-unreal-engine-24422d5868ba

반응형
반응형

사운드 구현

- 사운드를 구현하려면 소리를 내는 파트(Audio Source)와 소리를 듣는 파트(Audio Listener) 그리고 음원 파일(Audio Clip)이 필요하다

- 유니티로 import된 음원 파일을 오디오 클립이라고 부른다

- 오디오는 일반적으로 Singleton 방식을 이용해서 하나의 게임오브젝트가 사운드 출력을 총괄하도록 관리한다

AudioListener 컴포넌트

- 이 컴포넌트는 Main Camera에 기본 컴포넌트로 미리 추가되어 있다. 이 컴포넌트에 대해서는 별도로 속성을 이용해 제어할 수 없다

- 이 컴포넌트는 모든 게임오브젝트에 붙일 수 있지만 일반적으로 게임세계를 바라보는 유저의 눈에 해당하는 카메라 게임오브젝트에 포함되는 게 좋으며 조작하고 있는 캐릭터에 붙이기도 한다

- Audio Listener는 씬 내에 하나만 존재해야 한다(카메라가 여러 대인 경우 Audio Listener가 모든 카메라에서 활성화되어 있지는 않은지 꼭 확인하자). 반대로 Audio Source는 씬 내에 여러 개가 있을 수 있다

Audio Source 컴포넌트 1

Audio Source 컴포넌트 2

 

Audio Source 컴포넌트

- 씬 내에서 음원을 재생하는 컴포넌트

AudioClip: 재생할 음원 파일

Output: 재생되는 음원의 출력을 특정 Audio Listener 또는 Audio Mixer로 설정(?)

Mute: 음원이 재생되고 있는 상태로 음소거시킨다

Bypass Effects: 모든 Filter effects를 즉시 켜거나 끈다

Bypass Listener Effects: 모든 Listener effects를 즉시 켜거나 끈다

Bypass Reverb Zones: 모든 Reverb Zones를 즉시 켜거나 끈다

Play On Awake: 컴포넌트가 활성화 상태일 때 씬 실행과 동시에 음원을자동재생한다. 체크해제할 경우 스크립트에서 Play 함수로 재생할 수 있다

Loop: 반복재생 여부 설정

Priority: 씬 내의 여러 음원들 중에서 재생 우선순위 설정. default는 128이며 0이 최우선 순위, 256이 마지막 순위이다

Volume: Audio Listener로부터 1유닛(=1미터) 만큼 떨어지 거리에 있을 때를 기준으로 볼륨 설정

Pitch

Stereo Pan

Spatial Blend: 3D엔진이 Audio Source에 영향을 주는 정도를 설정

Reverb Zone Mix

3D Sound Settings: Spatial Blend에 값에 따라 비례적으로 영향을 받는다

Doppler Level: doppler effect의 적용 정도를 설정

Spread: 3D 스테레오나 멀티채널 사운드일 경우 스피커 공간에서의 spread angle을 조절

Volume Rolloff: Audio Source로부터 거리가 멀어질수록 볼륨이 감소하는 세 가지 Preset 옵션을 제공. 하단의 Curved Editor에서 감소 효과를 세부적으로 조절 가능

Min Distance: 볼륨을 100%로 들을 수 있는 범위. 3D공간에서 소리를 '더 크게' 만드려면 값을 높인다. 이 범위를 벗어나면 소리가 점점 줄어든다

Max Distance: 볼륨을 50% 이상으로 들을 수 있는 최대 범위. 즉, Max Distance 를 벗어나면 볼륨이 더 이상 감소하지 않고 50%로 들린다

Audio Clip의 인스펙터 뷰

Audio Clip

Force To Mono: 멀티채널(스테레오 포함) 음원을 싱글채널(모노)로 변환한다(down-mixing). 변환 후에는 기존 음원보다 소리가 작아지므로 데이터를 peak-

normalize 한다

Normalize: 변환 과정에서 normalize 여부를 설정

Load In Background: 체크하면 오디오 클립의 로드를 메인 쓰레드가 아닌 별개의 쓰레드에서 delayed time에 진행하므로 메인 쓰레드를 block하지 않는다. 유니티는 씬이 플레이되기 전에 씬 내에서 사용할 모든 오디오 클립을 로드해놓도록 기본설정되어 있으므로 이 속성은 default로 체크해제 되어 있다. Audio Clip 컴포넌트가 Background에서 음원을 로드하는 중이면 음원 로드를 완료할 때까지 컴포넌트는 작동이 지연된다

Ambisonic: 음원파일에 Ambisonic-encoded audio가 포함된 경우 체크. Listener의 방향orientation에 따라 회전하는 soundfield를 지원하는 포맷으로 음원을 저장

한다. XR이나 360도 영상에서 유용하게 사용된다

Load Type: 유니티가 실시간으로 음원을 불러올 때 사용할 방식을 설정

Decompress On Load: 오디오 파일을 압축되지 않은 상태로 메모리에 올리는 방식. 용량이 큰 오디오 파일은 성능상 오버헤드를 발생시키므로 용량이 작은 오디오 파일에

적합하다. 메모리는 많이 사용하지만, CPU 자원은 덜 소모한다

Compressed In Memory: 오디오 파일을 압축된 상태로 메모리에 올리는 방식. 실행할 때 압축을 해제하기 때문에 약간의 성능상 오버헤드를 발생시킨다. 큰 사이즈의

오디오 파일에 적합하다. 품질을 떨어뜨리면 파일크기를 줄일 수 있으므로 음질손상이 없는 범위 내에서 적절하게 Quality 값 설정

Streaming: Persistent Memory(HDD, Flash Driver)에 저장된 오디오 파일을 스트리밍 방식으로 재생한다. 따라서 오디오 파일을 저장하기 위한 메모리가 필요 없다

Preload Audio Data: 씬이 로드될 때 씬에서 사용할 모든 오디오 클립도 미리 로드하도록 설정. default로 체크되어 있다. 체크해제할 경우 AudioSource.Play/PlayOneShot 함수가 처음 호출되는 시점에 음원이 로드된다. 또는 AudioSource.LoadAudioData 함수로 원하는 시점에 로드하고 AudioSource.UnloadAudioData 함수로 언로드할 수 있다. 오디오 클립을 처음 재생하는 시점에 음원이 로드될 경우 렉이 발생할 수 있다는 점을 유의하자

Compression Format: 압축 포맷은 오디오 파일의 용도에 따라 다르게 설정해야 한다. 유니티에서 지원하는 오디오 압축 포맷의 종류는 다음과 같다

PCM: 음질이 좋은 대신 파일의 크기가 비교적 크기 때문에 효과음처럼 재생시간이 짧은 오디오 파일에 적합하다(비압축 포맷)

ADPCM: 압축률이 PCM 대비 3.5배이기에 파일의 크기가 작아서 메모리는 덜 쓸 수 있는 반면 CPU 자원은 좀 더 사용한다. 노이즈가 발생하기에 노이즈가 약간 있더라도

크게 상관 없는 총 소리나 발걸음 소리 같은 음원에 적합하다

Vorbis / MP3: 중간길이 정도의 효과음 또는 배경음악에 적합하다. PCM에 비해 음질은 떨어지지만 압축률(Quality 설정)을 조절할 수 있다. 압축률이 100%이면 PCM

과 동일해진다. 보통 70% 정도로 설정

*Vorbis의 Quality 값을 낮추게 되면 압축이 일어나므로 값을 다시 올리더라도 원래 상태로 돌아오지 않는다(다시 로드해야함)

HEVAG: PS Vista 고유의 압축 포맷으로 ADPCM과 유사하다

Quality: 압축되는 오디오 클립에 대한 압축률 설정. Vorbis 포맷에만 적용된다

Sample Rate Setting

Preserve Sample Rate: default 설정. 기존 음원파일의 sample rate를 유지

Optimize Sample Rate: 음원파일을 분석해서 가장 높은 주파수에 맞게 sample rate를 최적화

Override Sample Rate: sample rate를 수동으로 override 한다

오디오 설정 팁

- import한 오디오 클립은 2개의 채널로 이뤄져 있다(스테레오 방식. 각 채널은 좌,우 스피커로 구분해서 소리를 따로 보낸다). 그러나 음향 효과를 극대화한 게임이 아니라면 모노(소리의 방향구분 X)로 변환하는 걸 권장한다. 특히 모바일 게임의 경우 스테레오 사운드는 용량 부족과 성능 저하의 원인이 될 수 있다

*유니티는 스테레오, 모노 방식 외에도 최대 8개까지의 멀티채널을 지원한다

- raw 파일 음원은 용량이 크다. wav 파일은 용량이 크지만 바로 출력할 수 있기에 실행속도가 빠르다. mp3 파일은 용량이 작지만 압축 해제한 후 출력되기에 실행속도가 상대적으로 느리다(딜레이 발생) *유니티는 aif, wav, mp3, ogg 포맷의 음원을 지원한다

1. wav 파일: 즉시 출력돼야 하는 효과음(칼이나 총알의 충돌 등)에 사용

2. mp3 파일: 약간의 딜레이가 허용되는 인물의 대사나 배경음악에 사용

- 배경음악이나 환경음은 대부분 용량이 크기에 실행시 미리 압축을 풀어서 메모리에 보관할 경우 가용 메모리가 줄어든다. 따라서 용량이 큰 음원은 다음과 같은 import 설정을 권장한다

1. Streaming+Vorbis

2. Compressed in Memory+Vorbis+Quality(70%)

- 오디오의 발생 빈도나 오디오 파일의 크기에 따라서는 다음과 같은 import 설정을 권장한다

1. 자주 발생하며 파일크기가 작은 경우: Decompress On Load+PCM(or ADPCM)

2. 자주 발생하며 파일크기가 중간인 경우: Compressed in Memory+ADPCM

3. 가끔 발생하며 파일크기가 작은 경우: Compressed in Memory+ADPCM

4. 가끔 발생하며 파일크기가 중간인 경우: Compressed in Memory+Vorbis

- 음원은 import될 때 설정된 플랫폼(build target)에 적합한 타입(Load Type)으로 변환(re-encode)된다(default는 Decompress On Load+Vorbis). Load Type은 원본을 크게 손상시키지 않는 PCM이나 Vorbis/MP3 타입이 주로 쓰인다. PCM은 비압축포맷이기에 메모리에서 바로 읽어들일 수 있으므로 CPU의 부담이 덜하다. Vorbis/MP3 포맷은 불필요한 소리 정보를 잘라내는 방식으로 압축한다. ADPCM은 메모리 성능과 CPU 성능 사이에서 타협한 방식인데, PCM에 비해 CPU를 살짝 더 사용하지만 꽤 많이 압축되기에 메모리 부담을 줄인다.


Scripting API

AudioSource

public AudioClip clip: 재생할 default 오디오 클립. 다음에 재생할 오디오 클립을 clip에 담는 방법은 아래 코드 참고

public class ExampleClass : MonoBehaviour { public AudioClip otherClip; IEnumerator Start() { AudioSource audio = GetComponent<AudioSource>(); audio.Play(); yield return new WaitForSeconds(audio.clip.length); audio.clip = otherClip; audio.Play(); } }

public void PlayOneShot(AudioClip clip, float volumeScale = 1.0F)

- 오디오 클립(첫 번째 인수)을 특정 볼륨값(두 번째 인수)으로 재생한다. 볼륨값은 0에서 1사이의 실수로 설정

public void Play(ulong delay = 0)

- clip속성에 저장된 오디오 클립을 재생한다

- 인수는 재생하기까지 딜레이를 부여하는 값인데, 현재는 잘 사용되지 않고 PlayDelayed 함수로 대체해서 사용한다

public void Stop()

- 오디오 클립 재생을 멈춘다. 다시 Play할 경우 처음부터 재생한다

public void Pause()

- 오디오 클립 재생을 일시정지한다

public void UnPause()

- 일시정지된 오디오 클립을 정지된 시점부터 다시 재생한다

- Play 함수도 일시정지된 오디오 클립을 재생시킬 수 있다. 단, Play 함수는 어떤 상태의 오디오 클립에 대해서도 재생하도록 만들지만(create a new playback voice),

UnPause 함수는 일시정지된 상태가 아닌 오디오 클립을 재생할 수는 없다


*진폭은 소리의 크기, 진동수는 소리의 높낮이를 의미한다. 짧은 시간 동안 진동수가 많은 것은 고음을 의미한다

*압축된 음원파일은 가청영역대 이외의 부분을 잘라낸 후 아날로그 정보를 비슷한 주파수끼리 묶어서 디지털화한 것

 

 

ref : https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=dj3630&logNo=221461311485

<참고자료>

- 이재현, <절대강좌! 유니티>, 2018

- https://docs.unity3d.com/Manual/class-AudioSource.html

- https://docs.unity3d.com/Manual/class-AudioListener.html

- https://docs.unity3d.com/Manual/class-AudioClip.html

반응형

'게임엔진(GameEngine) > Unity3D' 카테고리의 다른 글

Unity vertex / fragment Shader01 -빨강 쉐이더  (0) 2022.12.02
Keyword Boolean  (0) 2022.11.27
UnityEngine.Random  (0) 2022.11.16
UI 버튼 바인딩  (1) 2022.11.06
앵커  (0) 2022.11.06
반응형

Description

Easily generate random data for games.

This static class provides several easy game-oriented ways of generating pseudorandom numbers.

The generator is an Xorshift 128 algorithm, based on the paper Xorshift RNGs by George Marsaglia. It is statically initialized with a high-entropy seed from the operating system, and stored in native memory where it will survive domain reloads. This means that the generator is seeded exactly once on process start, and after that is left entirely under script control.

For more details on the seed, including how to manage it yourself, see InitState. To learn how to save and restore the state of Random, see state.

Versus System.Random

This class has the same name as the .NET Framework class System.Random and serves a similar purpose, but differs in some key ways:

Static vs instanced
UnityEngine.Random is a static class, and so its state is globally shared. Getting random numbers is easy, because there is no need to new an instance and manage its storage. However, static state is problematic when working with threads or jobs (the generator will error if used outside the main thread), or if multiple independent random number generators are required. In those cases, managing instances of System.Random would be a better option.

Float upper bounds are inclusive
All properties and methods in UnityEngine.Random that work with or derive work from float-based randomness (for example value or ColorHSV) will use an inclusive upper bound. This means that it is possible, though as rare as any other given value, for the max to be randomly returned. In contrast, System.Random.NextDouble() has an exclusive maximum, and will never return the maximum value, but only a number slightly below it.

Performance
Methods in UnityEngine.Random have been measured to be between 20% and 40% faster than their equivalents in System.Random.

Name resolution ambiguity
Because the classes share the name Random, it can be easy to get a CS0104 "ambiguous reference" compiler error if the System and UnityEngine namespaces are both brought in via using. To disambiguate, either use an alias using Random = UnityEngine.Random;, fully-qualify the typename e.g. UnityEngine.Random.InitState(123);, or eliminate the using System and fully-qualify or alias types from that namespace instead.

Static Properties

insideUnitCircle Returns a random point inside or on a circle with radius 1.0 (Read Only).
insideUnitSphere Returns a random point inside or on a sphere with radius 1.0 (Read Only).
onUnitSphere Returns a random point on the surface of a sphere with radius 1.0 (Read Only).
rotation Returns a random rotation (Read Only).
rotationUniform Returns a random rotation with uniform distribution (Read Only).
state Gets or sets the full internal state of the random number generator.
value Returns a random float within [0.0..1.0] (range is inclusive) (Read Only).

Static Methods

ColorHSV Generates a random color from HSV and alpha ranges.
InitState Initializes the random number generator state with a seed.
Range Returns a random float within [minInclusive..maxInclusive] (range is inclusive).
 

 

ref : https://docs.unity3d.com/ScriptReference/Random.html

반응형

'게임엔진(GameEngine) > Unity3D' 카테고리의 다른 글

Keyword Boolean  (0) 2022.11.27
Unity Audio  (0) 2022.11.23
UI 버튼 바인딩  (1) 2022.11.06
앵커  (0) 2022.11.06
Has Exit Time  (0) 2022.11.03
반응형

이름과 동일한 컴포넌트를 gameObject 에서 찾아 _objects 에 기록해 놓는다 (코드에서 UI 찾아 접근하기 위해서)

찾을땐 enum 으로 찾으면 되는데 for( int i 에서 names 가 enum 에 대한 string 순서를 따라 감으로)

 

 

그다음 Bind 까지 해서 미리 objects 에 담아 놓는다

 

 

 

담아 놓은 것을 찾는 경우..

 

GameObject 는GetComponent로 부터 찾을 수 없는데 이 함수는 찾으려는 대상은 Monobehaviour 또는 컴포넌트로부터 상속 받은 것이어야 하거나 인터페이스여야 한다

그래서 위 구문으론 GameObject를 찾을 수 없고 별도로 마들어 줘야한다

 

 

 

 

 

사용법 예시

 

 

 

enum 을 제외한 UI_Button 을 베이스 클래스로보고 이것을 상속받아 필요한 자식 클래스에서 Bind 하여 바로 사용 할수 있도록 확장 할 수도 있다

반응형

'게임엔진(GameEngine) > Unity3D' 카테고리의 다른 글

Unity Audio  (0) 2022.11.23
UnityEngine.Random  (0) 2022.11.16
앵커  (0) 2022.11.06
Has Exit Time  (0) 2022.11.03
픽킹, 카메라에서 Raycast로 구해보기  (0) 2022.11.01
반응형

 

4개의 앵커가 있는데 주황색의 거리

전체 화면의 좌상단과 앵커 좌상단까지의 비율적인 거리를 애커가 나타내고 (주황색)

 

좌상단의 앵커와 button 의 좌상단 까지의 위치까지는 고정거리로 표시가 됩니다 (노란색)

즉 노란색 공간이 작아지면 버튼 자체도 작아지게 됩니다, 절대적 거리이므로

 

이런 작업은 4번 반복하여 버튼 사각형 위치 포인트들이 만들어집니다

 

 

위 상태에서 아래 처럼 앵커를 변경하면 화면과 앵커까지의 비율은 고정이 되고 

앵커와 버튼까지의 거리는 고정이 됨으로 회색부분의 화면을 줄이면

 

버튼과 앵커까지의 고정거리 (노란색 위치)가 줄어들게 되면서 버튼 자체가 축소 되는것을 볼 수 있습니다

 

 

 

 

 

 

앵커와 버튼 위치가 같을땐 

 

 

 

회색 부분 화면을 줄이면 주황색 부분 비율은 유지 하면서 버튼이 사이즈가 주는 것을 알 수있습니다

즉 화면 사이즈의 비율대로 줄이고 싶을때 이렇게 처리 하면 됩니다

 

 

 

 

 

앵커가 이렇게 중앙에 배치되게 되면 화면 크기가 줄어도 버튼 크기는 그대로가 된다

 

 

반응형

'게임엔진(GameEngine) > Unity3D' 카테고리의 다른 글

UnityEngine.Random  (0) 2022.11.16
UI 버튼 바인딩  (1) 2022.11.06
Has Exit Time  (0) 2022.11.03
픽킹, 카메라에서 Raycast로 구해보기  (0) 2022.11.01
충돌 액션 매트릭스  (0) 2022.10.30
반응형

 

Has Exit Time

 

어떤 한 애니 상태에서 Run 인상태에서 Has Exit Time 체크 되어 있다면 Run 애니가 끝나면다른 상태로 탈출한다는 뜻을 말하니다

 

Has Exit Time 아 체크 해제 되어 있다면 Run 상태에서 계속 반복적인 애니만 재생 되게 됩니다

 

만약 Run -> wait 으로 has Exit Time 에 체크되어 있고 반대의 상황에서도 체크 되어 있다면

 

Run 애니가 끝날때 Wait 애니가 재생 되고 Wait 애니가 끝나면 다시 Run 애니가 재생되는 상태가 됩니다

 

 

 

Fixed Duration 이 켜져 있다면 0.6875 초 이후에 다른 애니도 탈출한다는 것이고

Fixed Duration 이 꺼져 있다면 0.6875퍼센트 애니에 도달(완료) 했을때 다른 애니도 탈출한다는 것입니다

 

 

Transition Duration 은 겹치는 상태가 몇초가 될것인지를 결정

 

 

여러 트렌지션이 있을때의 순서를 정할수 도 있다

 

 

하단 파란색 시간 간격은 애니메이션 Blending 되는 간격을 말한다

 

즉 이 간격을 조절하면 Transition Duration  시간과  Exit Time 또한 같이 변하게 된다

반응형

'게임엔진(GameEngine) > Unity3D' 카테고리의 다른 글

UI 버튼 바인딩  (1) 2022.11.06
앵커  (0) 2022.11.06
픽킹, 카메라에서 Raycast로 구해보기  (0) 2022.11.01
충돌 액션 매트릭스  (0) 2022.10.30
Unity 코루틴(Coroutine) 이해하기: 동작원리 및 구현  (0) 2022.10.10
반응형

ScreenToWorldPoint 가 스크린 좌표에서 월드 좌표로 변환 시켜주는 코드

반응형
반응형

플레이어와 큐브의 충돌 조건시

OnCollisionEvent 가 호출 되는 조건

 

플레이어 : rigidbody 있고, isKinematic off 상태,  collider가 있고, IsTrigger 가 off 인 상태 => 리지드바디 콜라이더

큐브 오브젝트 : rigidbody는 없고 ,  collider가 있는 상태

 

간단하게 정리 하자면 플레이어는 컴포넌트가 다 있는데 모두다 끈 상태이고

양쪽다 collider가 있다는 가정하에 어느 한쪽이든 (Rigidbody) 가 있다면 OnCollisionEvent 는 호출 됩니다

 

하지만 isKinematic 이 켜져 있으면 충돌이벤트가 호출 될 수도 안될 수도 있으니 경우의 수를 봐야합니다

 

큐브는 콜라이더만 있는 상태라면 OnCollisionEvent 가 호출 됩니다

 

 

 

OnTriggerEnter 발동조건은

둘다 콜라이더가 있어야 하고 , 둘중 하나는 IsTrigger : on 이어야 한다, 둘중 하나는 Rigidbody 여야 한다

 

 

https://docs.unity3d.com/Manual/CollidersOverview.html

 

 

 

 

 

반응형
반응형

Unity 코루틴이란?

무의식적으로 코루틴은 쓰레드가 생성되는 멀티스레드 방식으로 느껴질 수 있다.

하지만 코루틴은 싱글 스레드로 비동기 방식을 구현한다.

따라서 실제로 병렬 처리가 아니다. 순차 처리로부터 태스크를 분할 처리한다.

 

🚧 2022년 7월 수정 사항
코루틴은 싱글 스레드로 구현되기 때문에 비동기 방식이 아닙니다.
멀티 스레딩 모델의 비동기 방식은 함수 A의 완료와 함수 B의 실행 시점이 일치하지 않습니다.
왜냐하면 병렬로 처리되기 때문인데 그에 반해 코루틴은 순차적으로 처리합니다.
코루틴의 작업 처리가 늦을 수록 다음 작업에 딜레이가 생기는 이유입니다.
그러므로 코루틴은 함수 실행과 완료 시점이 일치하지 않더라도 동기 방식입니다.

MSDN에서는 '순차적으로 작업을 분할해서 처리'하는 것도 비동기로 분류하고 있습니다.
이런 점에서 코루틴도 비동기 방식이라 할 수 있습니다.
하지만 MSDN이 설명하는 것은 엄밀히 따지면 싱글스레드-비동기가 아닌 멀티스레드-비동기입니다.
사용자 코드만 놓고 봐서는 싱글스레드이지만 실제로 시스템 스레드풀에 위임해버리기 때문입니다.
즉, 사용자는 싱글스레드를 쥐어주고 시스템에선 작업자 스레드를 운용하는 것입니다.
이런 방식을 싱글스레드-비동기라고 합니다.

반면 코루틴은 사용자는 싱글스레드, 시스템은 싱글스레드 또는 멀티스레드 일 수 있습니다. 그래서 싱글 스레드를 yield return 하는 경우엔 동기방식이 될테고 멀티 스레드를 운용하는 API를 yield return 하는 경우 비동기방식이라 할 수 있습니다. 하지만 대부분 싱글 스레드로 운영될 것입니다. 함수를 아주 잘게 테스크로 쪼개어서 여러 프레임에 걸쳐 분산 처리 하는 것입니다.

자세한 내용은 여기를 참고해주세요.
cf. 자바스크립트는 싱글 스레드인데 왜 비동기가 가능할까?
📖 레퍼런스
코루틴은 LateUpdate 주기에 실행된다.
In-depth analysis of the Unity Coroutine principle
그 근거는 위 아티클의 테스트 코드를 통해 얻을 수 있다.
e.g. 게임오브젝트의 enabled가 false로 바뀌면 실행 중인 코루틴이 모두 중단 됨.
e.g. yield return 이후 코드는 LateUpdate 호출 시점에 실행됨.

 

그 동안 궁금했던 동작원리를 알아보자.

IEnumerator

코루틴의 반환 타입은 IEnumerator이다. 왜 IEnumerator를 반환해야 할까?

먼저 IEnumerator에 대해 알아보자. 

IEnumerator는 클래스가 IEnumerable이 되려면 반드시 구현해야 할 인터페이스다.

// IEnumerator
class Enumerator : IEnumerator
{
    int _idx = 0;
    int _current;
    int[] _pmArr = new int[] { 2, 3, 5, 7 };

    public object Current => _current;
    // 다음 데이터 있는지 체크 & current 갱신
    public bool MoveNext()
    {
        if (_pmArr.Length - 1 < _idx)
        {
            return false;
        }
        _current = pmArr[_idx];
        _idx++;
        return true;
    }
    public void Reset()
    {
        _idx = 0;
    }
}

IEnumerator은 배열의 Length를 체크하고 다음 데이터가 있는지 여부 Current값을 저장한다.

// IEnumerable
class PrimeNums : IEnumerable
{
    // IEnumerator
    class Enumerator : IEnumerator
    {
        // 중략
    }
    public IEnumerator GetEnumerator()
    {
        return new Enumerator();
    }
}

클래스가 IEnumerable이 되면

// foreach
private static void DoForeach()
{
    PrimeNums primeNums = new PrimeNums();

    foreach (var item in primeNums)
    {
        Console.WriteLine(item);
    }
}

 foreach문 사용이 가능하다.

✏️ 더 알아보기
IEnumerable<T>를 구현하면 IEnumerable<T>를 통해 System.Linq도 쓸 수 있다.
e.g. Selcet, Where, Orderby 등

✏️ IEnumerable 캡슐화 하는 데 도움이 된다.
IEnumerable은 단순히 컬렉션을 순회하는 데에만 쓰이므로 컬렉션의 데이터 변경을 막는 데 사용할 수 있다. List, Array 등 데이터 변경이 가능한 컬렉션 대신 IEnumerable을 참조하도록 하는 것이다

 

foreach를 사용하는 데에 IEnumerator가 필요한 이유는

결국 내부적으로 필요하기 때문일 것이다

// foreach - pseudo native C# code
private static void DoEnumerate()
{
    PrimeNums primeNums = new PrimeNums();

    var enumerator = primeNums.GetEnumerator();
    while (enumerator.MoveNext())
    {
        var item = enumerator.Current;
        Console.WriteLine(item);
    }
}

foreach문의 IL코드를 들여다보면 위와 유사한 형태의 로직이 나타난다.

 

foreach에서 쓰인 IEnumerator는 '다음 데이터가 있는지 여부'를 체크하고 요소를 Current에 저장했다.

즉, 코루틴에서 반환하는 IEnumerator도 이런 역할을 하기 위해서 존재한다.

 IEnumerator : 컬렉션 vs 코루틴
컬렉션(List, Array 등)의 IEnumerator Current에 순회할 요소를 담지만,
코루틴은 지연 함수 또는 비동기 함수의 IEnumerator를 담는다.

 

yield return

foreach가 내부적으로 IEnumerator를 사용하듯

IEnumerator를 사용하는 예약어가 또 있는데, 그 것은 바로 yield return이다.

다음 그림은 yield return 예약어가 생성한 MoveNext의 IL코드이다.

yield return foreach와 다르게 switch 분기문으로 로직이 구성됐다.

Console.WriteLine("DoEnumerate 1")
Console.WriteLine("DoEnumerate 2")
Console.WriteLine("DoEnumerate 3")

각 구문을 스위치를 통해 분기했다.

<>1__state는 스위치 분기문의 인덱스로 활용된다.

즉, 컬렉션 순회를 위해 _idx가 있었던 것처럼 스위치 분기를 위해 <>1__state가 있는 것이다.

  foreach vs yield return
IL코드를 들여다보면 컴파일 전에는 볼 수 없었던 예약어의 native code를 조금 엿볼 수 있다.

foreach는 IEnumerable.GetEnumerator() IEnumerator.MoveNext() 등 우리가 구현한 클래스를 참조한다. 그러려면 IEnumerable IEnumerator의 구현 코드를 우리가 직접 작성 해야만 한다.

반면 yield return IEnumerator 클래스 구현도 컴파일러가 대신 작성해주고 있다. 
즉, IEnumerator를 구현해야만 하는 수고로운 일이 사라진 것이다.

cf. C# 라이브러리의 컬렉션(List, Array 등)은 대부분 이미 IEnumerable이 구현된 상태이다. 그래서 곧바로 System.Linq나 foreach 등 순회문을 사용할 수 있는 것이다.

 

코루틴 구현

코루틴도 IEnumerator.MoveNext 함수를 호출하는 것을 볼 수 있다.

using System.Collections;
using UnityEngine;

public class CoroutineStudy : MonoBehaviour
{
    private void Start()
    {
        StartCoroutine(DoCoroutine());
        Debug.Log("Start");
    }
    
    IEnumerator DoCoroutine()
    {
        Debug.Log("DoCoroutine WaitForSeconds");
        yield return new WaitForSeconds(1.0f);
        Debug.Log("DoCoroutine Null");
        yield return null;
        Debug.Log("DoCoroutine end");
    }
}

IEnumerator의 동작원리를 이용해 위 코루틴을 직접 구현해보자.

using System.Collections;
using UnityEngine;

public class CoroutineStudy : MonoBehaviour
{
    // native code written by compiler service
    class PseudoCoroutineEnumerator : IEnumerator
    {
        int _state = 0;
        object _current = null;
        public object Current => _current;

        public bool MoveNext()
        {
            switch (_state)
            {
                case -1:
                    return false;
                case 0:
                    Debug.Log("DoPseudoCoroutine WaitForSeconds");
                    _state++;
                    _current = new WaitForSeconds(1.0f);
                    return true;
                case 1:
                    Debug.Log("DoPseudoCoroutine Null");
                    _state++;
                    _current = null;
                    return true;
                case 2:
                    _state = -1;
                    Debug.Log("DoPseudoCoroutine end");
                    return true;
            }
            return false;
        }

        public void Reset()
        {
            _state = 0;
        }
    }

    IEnumerator enumerator;                         // native code of UnityEngine
    private void Start()
    {
        StartPseudoCoroutine(DoPseudoCoroutine());  // native code of UnityEngine
        Debug.Log("Start");

    }

    float seconds = 1.0f;    // = WaitForSeconds.m_seconds
    float elapsed = .0f;     // native code of UnityEngine
    private void LateUpdate()
    {
        // native code of UnityEngine
        if (enumerator.Current != null)
        {
            if (enumerator.Current is WaitForSeconds)
            {
                if (seconds < elapsed)
                {
                    enumerator.MoveNext();
                }
                elapsed += Time.deltaTime;
            }
        }
        else
        {
            enumerator.MoveNext();
        }
    }

    // native code of UnityEngine
    void StartPseudoCoroutine(IEnumerator enumerator)
    {
        this.enumerator = enumerator;
        enumerator.MoveNext();
    }

    IEnumerator DoPseudoCoroutine()
    {
        // native code written by compiler service
        return new PseudoCoroutineEnumerator();
    }
}

native code(기계어로 컴파일되는 코드)는 IL코드에 의해 생성될 것을 가정하고 임의로 작성했다.

결과는 코루틴과 같았다. 

싱글 스레드만으로 마치 병렬처리인 것 처럼 구현이 가능한 것이다.

🚧 
"이렇겠다"라는 추측에서 나온 결과물이다. 실제로는 코루틴을 관리하는 클래스가 존재한다. 그리고 이 것보다 복잡할 것이다. 하지만 기본적인 아이디어는 이 것과 크게 다르지 않을 거라 생각된다.

 

결론

1. 코루틴은 싱글스레드로 구현 가능하다.

2. C# 예약어(foreach, yield return 등)는 IEnumerator 클래스를 참조 또는 작성한다.

3. yield return이 작성한 IEnumerator.MoveNext()에 의해 코루틴 구문들이 Switch로 분기된다.

4. yield return구문으로 반환되는 값은 Current에 저장된다(주로 AsyncOperation이나 Wait~개체).

5. MonoBehaviour MoveNext() Current를 참조해 코루틴을 구현한다.

"코루틴은 싱글스레드 비동기이다."

 

ref : https://planek.tistory.com/36

반응형
반응형

 

javah 툴이라는 것이 자바의 코드중 cpp 에서 사용 할수 있도록 헤드를 만들어 주는 도구 역할을 한다

 

그리고 몇몇 네이밍 규칙들을 맞춰줘야 한다 _ 같은 것들

 

규칙들은 자바 코드 쪽에서

JNIEXPORT jintArray JNICALL Java_org_in_jnitest_JavaJNITest_OverloadedMethod___3II
   (JNIEnv *, jobject, jintArray, jint);


JNIEXPORT jdouble JNICALL Java_org_in_jnitest_JavaJNITest_OverloadedMethod__I_3Ljava_lang_String_2Ljava_lang_String_2
   (JNIEnv *, jobject, jint, jobjectArray, jstring);

 

JNIEnv *, jobject

이렇게 두개의 인자가 앞에 들어간다는 것이다, 그 이후엔 원하는 타입

 

 

 

 

The concept of function and data type is the basic building block of any programming language. When working with heterogeneous languages’ features, bridging their differences is crucial. JNI (Java Native Interface) provides a native interface through which Java communicates with other languages. In JNI, the first hurdle of establishing communication with languages other than Java is the distinctive feature of data type and function mapping between two unmatched programming languages. This communication difference begins with its data type and function representation because they are the basic structure of any code. Here we shall use C/C++ as the native language support for Java while working with JNI. The article tries to explicate function and data type mapping between Java and C/C++ with some appropriate examples.

Native Method Naming Conventions

Declaring a native method inside a Java code is as simple as adding just the native keyword. The complexity creeps in as soon as mapping occurs with the native language. The code looks mangled beyond recognition. Let’s get into the details and try to identify some of its common pattern.

As we know, C/C++ header files for the native method declared in the Java code are auto generated by the javah tool. The function naming scheme in the header file is mapped based on the following conventions:

  • the native method name always begins with ‘Java’ as the first word.
  • The package/sub package separator for the fully qualified name is separated by an underscore character.
  • For an overloaded method name, it begins with ‘Java’ as the first word, then fully qualified package/sub package name separated by underscore characters, and at the end, two underscore characters with the mangled method’s signature.

Undoubtedly, the native method signature is clumsy in appearance, yet the scheme has a logical pattern. Because they are auto generated with the javah tool, programmers need not worry about the naming convention. What we can do while implementing the methods in the C/C++ file is simply copy-paste the function prototype from the header file and concentrate on defining our logic rather than brooding over complexity. However, to get a glimpse of native method naming convention, observe the following Java file and its corresponding C/C++ header file generated through the javah tool.

 

 

package org.in.jnitest;

public class JavaJNITest {
   private native void method1();
   private native void method2(String str);
   private native int[] OverloadedMethod(int[] intArray,
      int intValue);
   private native double OverloadedMethod(int intValue,
      String strArray[], String str);
   private native double OverloadedMethod(double
      doubleValue, String str);
   private native double OverloadedMethod(short
      shortArray, String str);
}




/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class org_in_jnitest_JavaJNITest */

#ifndef _Included_org_in_jnitest_JavaJNITest
#define _Included_org_in_jnitest_JavaJNITest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     org_in_jnitest_JavaJNITest
 * Method:    method1
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_in_jnitest_JavaJNITest_method1
   (JNIEnv *, jobject);

/*
 * Class:      org_in_jnitest_JavaJNITest
 * Method:     method2
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_org_in_jnitest_JavaJNITest_method2
   (JNIEnv *, jobject, jstring);

/*
 * Class:     org_in_jnitest_JavaJNITest
 * Method:    OverloadedMethod
 * Signature: ([II)[I
 */
JNIEXPORT jintArray JNICALL Java_org_in_jnitest_JavaJNITest_OverloadedMethod___3II
   (JNIEnv *, jobject, jintArray, jint);

/*
 * Class:     org_in_jnitest_JavaJNITest
 * Method:    OverloadedMethod
 * Signature: (I[Ljava/lang/String;Ljava/lang/String;)D
 */
JNIEXPORT jdouble JNICALL Java_org_in_jnitest_JavaJNITest_OverloadedMethod__I_3Ljava_lang_String_2Ljava_lang_String_2
   (JNIEnv *, jobject, jint, jobjectArray, jstring);

/*
 * Class:     org_in_jnitest_JavaJNITest
 * Method:    OverloadedMethod
 * Signature: (DLjava/lang/String;)D
 */
JNIEXPORT jdouble JNICALL Java_org_in_jnitest_JavaJNITest_OverloadedMethod__DLjava_lang_String_2
   (JNIEnv *, jobject, jdouble, jstring);

/*
 * Class:     org_in_jnitest_JavaJNITest
 * Method:    OverloadedMethod
 * Signature: (SLjava/lang/String;)D
 */
JNIEXPORT jdouble JNICALL Java_org_in_jnitest_JavaJNITest_OverloadedMethod__SLjava_lang_String_2
   (JNIEnv *, jobject, jshort, jstring);

#ifdef __cplusplus
}
#endif
#endif

 

 

 

그리고 아래 데로 타입들을 맞춰 주면 된다

native 기본 데이터 타입은 j 로 시작하고 그 뒤를 잊는 소문자는 java 타입 명과 동일하다

 

Data Type Mapping

The mapping between Java data types and data types used in native code is pretty straightforward. The naming scheme remains the same: the native data type name is preceded by the character ‘j’, followed by all lowercase data type name equivalent to Java. JNI also includes another data type named jsize, which stores the length or an array or string.

Java primitive data type Native primitive data type Description
void void None
byte jbyte 8-bit signed. Range is -27 to 27 – 1
int jint 32-bit signed. Range is -231 to 231 – 1
float jfloat 32 bits. Represent a real number as small as 1.4 x 10-45 and as big as 3.4 x 1038 (approx.), positive or negative
double jdouble 64 bits. Represent a real number as small as 4.9 x 10-324 and as big as 1.7 x 10308 (approx.), positive or negative
char jchar 16-bit unsigned. Range is 0 to 65535
long jlong 64-bit signed. Range -263 to 263 – 1
short jshort 16-bit signed. Range is -215 to 215 – 1
boolean jboolean Unsigned 8 bits. true and false

In case of reference types, JNI defines a few of the most common references, such as string or class, and so forth. Other references are mapped to the JNI jobject. String, Class, and Throwable are just exceptional reference types that otherwise can be handled by jobject.

Java Reference type JNI type Description
java.lang.object jobject Any Java object
java.lang.String jstring String representation
java.lang.Class jclass Java class object
java.lang.Throwable jthrowable Java throwable object

JNI uses jarray for a generic array type that represents the Java array type. Another data type, named jvalue, is nothing but a union type defined in C/C++.

Conclusion

These are some of the primary yet very vital concepts to begin coding in JNI. Although data types of most languages are more or less similar, when it comes to mapping between two different languages, the question always arises about how they are mapped. Observe that the classic C/C++ data type naming is not followed in JNI. The representation may be the same, yet the naming scheme differs. Here, we have refreshed the concept, thus paving way for more hands on work in the next set of articles.

 

 

 

 

ref : https://www.developer.com/microsoft/c-sharp/jni-data-type-mapping-to-c-c/

 

 

 

반응형
반응형

자바에서 AAA.java 에서 

아래 함수를 선언만 해놓는다


public class AAA{
@Keep
public static native void onImageReady(byte[] buffer, int width, int height);

 

그리고 cpp 에서 onImageReady 아래 처럼 만들어 놓으면
java 에서 cpp 호출 할때 이벤트 처럼 호출이 가능해진다

언리얼에서 자바를 호출 할수 있게 가능하게 만들어줌

 

w자바에서 onImageReady 호출

_activity.runOnUiThread(new Runnable() {
				@Override
				public void run() {
					Bitmap bm = BitmapFactory.decodeFile(path);
					Bitmap bitmap = rotate(bm, path);

					onImageReady(getBitmapBytes(bitmap), bitmap.getWidth(), bitmap.getHeight());

 




UE4 C++ 코드

#if PLATFORM_ANDROID

JNI_METHOD void Java_com_회사명_androidgoodies_AAA_onImageReady(JNIEnv* env, jclass clazz, jbyteArray buffer, int width, int height)
{
UE_LOG(LogTemp, Warning, TEXT("C++!"));

JNIEnv* Env = FAndroidApplication::GetJavaEnv();
bufferRef = static_cast<jbyteArray>(Env->NewGlobalRef(buffer));

Env->DeleteLocalRef(buffer);
AsyncTask(ENamedThreads::GameThread, [=]() {
TArray<uint8> byteArray = AGArrayConvertor::ConvertToByteArray(bufferRef);
UTexture2D* result = AGMediaUtils::TextureFromByteArray(byteArray, width, height);

UAGPickersBPL::OnImageReady(result);

byteArray.Empty();
});
}

 

 

본 코드는 AndroidGoodies 플러그인이 필요하다

 

 

https://www.unrealengine.com/marketplace/ko/product/android-native-goodies

반응형
반응형

 

RPC (Remote Procedure Call) 는 로컬에서 호출되지만 (호출하는 머신과는) 다른 머신에서 원격 실행되는 함수를 말합니다.

RPC 함수는 매우 유용하게 사용될 수 있으며, 네트워크 연결을 통해 클라이언트와 서버 사이에 메시지를 전송할 수 있습니다.

이 기능의 주요 용도는 속성상 장식이나 휘발성인 비신뢰성 게임플레이 이벤트를 위한 것입니다. 이는 사운드 재생, 파티클 스폰, 액터의 핵심적인 기능과는 무관한 일시적 효과와 같은 작업을 하는 이벤트를 포함합니다. 기존에 이러한 유형의 이벤트는 종종 액터 프로퍼티를 통해 리플리케이트되고는 했습니다.

RPC 사용시 오너십 작동 방식 을 이해해 두는 것이 중요합니다. 대부분의 RPC 실행 장소를 결정하기 때문입니다.

RPC 사용하기

함수를 RPC 로 선언하려면 UFUNCTION 선언에 Server, Client, NetMulticast 키워드를 붙여주기만 하면 됩니다.

예를 들어 함수를 서버에서 호출되지만 클라이언트에서 실행되는 RPC 로 선언하려면, 이렇게 합니다:

UFUNCTION( Client )
void ClientRPCFunction();

함수를 클라이언트에서 호출되지만 서버에서 실행되는 RPC 로 선언하는 것은 Server 키워드를 사용한다는 것 빼고는 매우 비슷합니다:

UFUNCTION( Server )
void ServerRPCFunction();

Multicast 라 불리는 특수 유형 RPC 함수가 하나 더 있습니다. Multicast RPC 는 서버에서 호출된 다음 서버는 물론 현재 연결된 모든 클라이언트에서도 실행되도록 고안된 것입니다. 멀티캐스트 함수를 선언하려면 그냥 NetMulticast 키워드를 사용하면 됩니다:

UFUNCTION( NetMulticast )
void MulticastRPCFunction();

멀티캐스트 RPC 는 클라이언트에서도 호출 가능하지만, 이 경우 로컬에서만 실행됩니다.

간단 팁

함수 초반에 Client, Server, Multicast 키워드를 앞쪽에 어떻게 붙였는지 보세요. 저희 내부적으로 합의한 규칙으로, 이 함수를 사용하는 프로그래머들에게 이 함수가 각각 클라이언트, 서버, 모든 클라이언트에서 호출된다는 것을 알리기 위한 것입니다.

멀티플레이어 세션 도중 이 함수가 어느 머신에서 호출되는지 한 눈에 알아볼 수 있기에 매우 유용합니다.

요건 및 주의사항

RPC 의 정상 작동을 위해 충족시켜야 하는 요건이 몇 가지 있습니다:

  1. Actor 에서 호출되어야 합니다.
  2. Actor 는 빈드시 replicated 여야 합니다.
  3. 서버에서 호출되고 클라이언트에서 실행되는 RPC 의 경우, 해당 Actor 를 실제 소유하고 있는 클라이언트에서만 함수가 실행됩니다. (client)
  4. 클라이언트에서 호출되고 서버에서 실행되는 RPC 의 경우, 클라이언트는 RPC 가 호출되는 Actor 를 소유해야 합니다.
  5. Multicast RPC 는 예외입니다:
    • 서버에서 호출되는 경우, 서버에서는 로컬에서 실행될 뿐만 아니라 현재 연결된 모든 클라이언트에서도 실행됩니다.
    • 클라이언트에서 호출되는 경우, 로컬에서만 실행되며, 서버에서는 실행되지 않습니다.
    • 현재 멀티캐스트 이벤트에 대해 단순한 스로틀 조절 메카니즘이 있습니다. 멀티캐스트 함수는 주어진 액터의 네트워크 업데이트 기간동안 두 번 이상 리플리케이트되지 않습니다. 장기적으로 크로스 채널 트래픽 관리 및 스로틀 조절 지원을 개선시킬 계획입니다.

 

Autority

Role_Autority : 서버상에 존재하는 복제된 액터, 서버만이 액터의 변수와 상태에 대한 권한을 갖고 있음,
Role_AutonomousProxy : 소유한 클라이언트에 존재하는 액터의 버전으로 간주함, 클라와 서버간의 직접적인 RPC 송수신 등의 작업을 수행함
Role_SimulatedProsy : 다른 모든 클라이언트에 있는 액터 버전으로 간주한다, 순전히 시뮬레이터이며 액터 상태에 대한 아무런 권한이 없음(조정할수 없는 다른 캐릭터들)의 역할

 

(멀티 플레이 접속시) 서버에선 플레이어 컨트롤러 2개(플레이어 수만큼), 클라이언트에선 플레이어 컨트롤러가 1개이다

 

아래는 리슨 서버로 실행하여 서버 pc 에서 본 화면

 

  • 플레이어 컨트롤러는 소유 클라이언트에서 생성되며 플레이어 컨트롤러는 서버와의 모든 통신이 이뤄지는 채널이다
    (이것이 비 소유 클라이언트와의 차이임)
  • (멀티 플레이 접속시) 서버에선 플레이어 컨트롤러 2개(플레이어 수만큼)
    클라이언트에선 플레이어 컨트롤러가 1개이다

    각 컨트롤러는 서버와 소유 클라이언트 간에 복제된다, 같은 맥락으로 게임 모드는 네트워크 권한에서만 생성된다(이경우는 대게 서버임),  클라이언트와 서버 간의 게임 플레이어 정보를 전달하는 방법은 GameState 와 PlayerState 오브젝트를 통해 이뤄지는데 이 두 오브젝트도 연결된 모든 클라이언트에 복제된다

 

 

서버와 클라이언트 통신 할때 GameState 와 PlayerState 오브젝트를 통해서 정보 전달이 이뤄진다

그리고 이 두 오브젝트는 연결된 모든 클라이언트에 복제된다

 

Player state : 플레이어 상태 : 모든 플레이어는 서버(혹은 스탠드 얼론 게임)에 각자의 PlayerState를 가집니다. 

PlayerState는 모든 클라이언트에게 리플리케이트 되며

플레이어의 네트워크와 관련 정보를 지니게 된다, 플레이어 이름, 점수 등등

 

 

다음 표는 호출 액터의 소유권(가장 왼쪽 열)에 따라 주어진 유형의 RPC 실행 위치를 나타냅니다.

서버에서 호출된 RPC

액터 소유권리플리케이트 안됨NetMulticast서버클라이언트

 

RPC 는 호출 PC 에서 액터의 소유권을 보고 호출되는 대상이 1차적으로 정해지고, 그다음 프래그를 보고 어떻게 호출할지 2차적으로 결정 된다

 

Multicast 는 소유권과 관계 없이 동작하는 특성을 갖음

리플리케이트 안됨 = 리플리케이트 없는 상태일때를 말함

서버소유 액터 일때 NetMulticast 도 서버와 모든 클라이언트에서 실행이다

 

 

 

 

클라이언트에서 호출된 RPC

액터 소유권리플리케이트 안됨NetMulticast서버클라이언트

 

 


위 표를 기준으로 Multicast 예를 들자면

[호출 PC 가 서버에서 호출인 경우 ]

Multicast RPC 는 서버에서 호출되는 경우서버와 현재 연결된 모든 클라이언트에서 실행된다

 

[호출 PC 가 클라이언트에서 호출하는 경우 ]

클라이언트에서 호출되는 경우 소유권에 관계 없이 로컬에서만 실행되며 서버에서 실행되지 않는다

 

 

 

신뢰성

기본적으로 RPC 는 비신뢰성입니다. RPC 호출이 원격 머신에서 확실히 실행되도록 하기 위해서는 Reliable 키워드를 붙이면 됩니다:

UFUNCTION( Client, Reliable )
void ClientRPCFunction();

블루프린트

RPC 로 마킹된 함수는 블루프린트에서 호출해도 리플리케이트됩니다. 이 경우 마치 C++ 에서 호출된 것과 같은 규칙을 따릅니다. 현재 블루프린트에서 동적으로 함수를 RPC 마킹하는 것은 가능하지 않습니다.

하지만 Custom event 는 블루프린트 에디터 안에서 replicated 마킹 가능합니다.

이 기능을 사용하기 위해서는, 이벤트 그래프에서 custom event 를 새로 만듭니다. custom event 에 클릭한 다음 디테일 뷰에서 Replication 세팅을 편집합니다:

인증

최근, 악성 데이터/입력 감지를 위한 관문 역할을 위해 RPC 에 인증(validation) 함수를 추가하는 기능이 생겼습니다. RPC 에 대한 인증 함수가 악성 파라미터를 감지한 경우, 해당 RPC 를 호출한 클라이언트/서버 연결을 끊도록 시스템에 알린다는 개념입니다.

RPC 에 대해 인증 함수를 선언하려면, UFUNCTION 선언문에 WithValidation 키워드를 추가해 주기만 하면 됩니다:

UFUNCTION( Server, WithValidation )
void SomeRPCFunction( int32 AddHealth );

그런 다음 Implementation 함수 옆 어딘가에 Validate 함수를 넣어주면 됩니다.

bool SomeRPCFunction_Validate( int32 AddHealth )
{
    if ( AddHealth > MAX_ADD_HEALTH )
    {
        return false;                       // This will disconnect the caller
    }
    return true;                              // This will allow the RPC to be called
}

void SomeRPCFunction_Implementation( int32 AddHealth )
{
    Health += AddHealth;
}

좀 더 최근에, 클라이언트->서버 RPC 의 경우 _Validation 함수를 포함하도록 UHT 를 변경했습니다. 서버 RPC 함수의 안전성 확보를 위해, 알려진 모든 입력 제한에 대해 각각의 모든 파라미터의 유효성 검사를 위한 코드를 쉽게 추가할 수 있도록 하기 위한 것입니다.

 

 

ref : https://docs.unrealengine.com/4.27/ko/InteractiveExperiences/Networking/Actors/RPCs/

반응형
반응형

com.google.games:gpgs-plugin-support:0.11.01 --> com.google.games:gpgs-plugin-support:+

 

 

 

I use GPGS v0.11.01 and was same error java.lang.ClassNotFoundException: com.google.android.gms.games.PlayGames.
Android Force Resolve failed with an error Failed to fetch the following dependencies: com.google.games:gpgs-plugin-support:+.
How did I fix it:

  • open the file Assets/GooglePlayGames/com.google.play.games/Editor/GooglePlayGamesPluginDependencies.xml
  • change line Packages/com.google.play.games/Editor/m2repository to Assets/GooglePlayGames/com.google.play.games/Editor/m2repository
  • run Android Force Resolve

 

 

 

https://github.com/playgameservices/play-games-plugin-for-unity/releases

 

 

ref : https://github.com/playgameservices/play-games-plugin-for-unity/issues/2796#issuecomment-1166347984

ref : https://github.com/playgameservices/play-games-plugin-for-unity/issues/2796

반응형
반응형

 

 

I have data endpoint which returns a json like [{"Name": "Alex", "Age": 24}]

now if i Deserialize this as an JsonObject It fails as it's an array with no name how can I get the data inside the array?

 

 

Here is an snippet from my code. Should help.

TSharedRef<TJsonReader<>> reader = TJsonReaderFactory<>::Create(data);

TArray< TSharedPtr<FJsonValue> > ue4ObjectArray;

successful = FJsonSerializer::Deserialize(reader, ue4ObjectArray);

 

 

TArray<TSharedPtr<FJsonValue>> objArray = JsonObject->GetArrayField(TEXT("AwesomeStructs"));

for (int32 i = 0; i < objArray.Num(); i++)
	{
		TSharedPtr<FJsonValue> value = objArray[i];
		TSharedPtr<FJsonObject> json = value->AsObject();

		FVector  AwesomeVector  = ParseAsVector(json, FString("AwesomeVector"));
		bool     AwesomeBoolean = json->GetBoolField(TEXT("AwesomeBoolean"));
		float    AwesomeFloat   = json->GetNumberField(TEXT("AwesomeFloat"));
		int32    AwesomeInteger = json->GetNumberField(TEXT("AwesomeInteger"));
		FRotator AwesomeRotator = ParseAsRotator(json, FString("AwesomeRotator"));
		
		FAwesomeStruct AwesomeStruct = FAwesomeStruct::BuildAwesomeStruct(
			AwesomeVector,AwesomeBoolean,AwesomeFloat,AwesomeInteger,AwesomeRotator
		);

		AwesomeStructs.Push(AwesomeStruct);
	}
}

 

 

Array 읽기

TArray<TSharedPtr<FJsonValue>> Array = JsonObject->GetArrayField(TEXT("FieldName"));

또는

const TArray<TSharedPtr<FJsonValue>>* Value;
if(JsonObject->TryGetArrayField(TEXT("FieldName"), Value)) {
	// When Parsing Success.
} else {
	// When Parsing Failed.
}

 

ref : https://ballbot.tistory.com/45

ref : https://www.reddit.com/r/unrealengine/comments/shpgbw/json_array_to_unreal_engine_c_or_varest/

ref : https://gist.github.com/hanzochang/07eba255ce0d1695582a92e98e973200

반응형
반응형
2. Abstract
 
해당 클래스가 추상 클래스임을 나타내는 지정자이다.
당연하게도 인스턴스를 생성하지 못하며, 에디터 상에 그 자체로 추가될 수 없다.
 
엔진에 미리 정의된 클래스 중에선, 대표적으로 AActor 클래스가 추상 클래스이다.
 
  1. UCLASS(abstract)
  2. class ENGINE_API AActor : public UObject
  3. {
  4.     ...
  5. }

 

 

Hi, I’m wondering why if writing pure virtual methods in my abstract base classes is supported? Currently I’ve got a class that inherits from actor and I’ve marked it as UCLASS(abstract) with a pure virtual method.

I then have a class that inherits from this class, and implements the pure virtual method. When I compile this I get the following error.

Only functions in the second interface class can be declared abstract.

Does that mean if I want to use pure virtual methods in my abstract class that I have to use a separate interface? If that’s the case, then what is the purpose of declaring my class abstract? Aren’t pure virtual methods what make classes abstract?

UCLASS(abstract)
class ASpawnVolume : public AActor
{
	GENERATED_UCLASS_BODY()

public:
	UFUNCTION(BlueprintCallable, Category = "Spawning")
	virtual void SpawnUnitWithTransform(TSubclassOf<AActor> unitType, FTransform trans);

	UFUNCTION(BlueprintCallable, Category = "Spawning")
	virtual void SpawnUnit(TSubclassOf<AActor> unitType);

	UFUNCTION(BlueprintCallable, Category = "Spawning")
	virtual void SpawnUnits(TSubclassOf<AActor> unitType, uint32 count);

	UFUNCTION(BlueprintCallable, Category = "Spawning")
	virtual FTransform GetRandomTransformInVolume() = 0;
};

 

 

 

UClasses cannot really be abstract in the C++ sense, because the UObject sub-system requires that each class can be instantiated (it creates at least one instance of each class as a so called Class Default Object [CDO] that holds the default properties of that class). Therefore, every class method must have an implementation, even if it does nothing.

That being said, you can get similar behavior by decorating your inline method implementations with the PURE_VIRTUAL macro. This will tell the UObject sub-system that your intent is to declare a pure virtual method. So even though the method is not pure virtual in the C++ sense - it has a (possibly empty) function body - the compiler can still ensure that all child classes do supply an actual implementation.

For example, you could do:

virtual void SpawnUnit(TSubclassOf<AActor> unitType) PURE_VIRTUAL(ASpawnVolume::CanRedo,);

You can find many more examples in the code base, including methods with return values by searching for PURE_VIRTUAL.

 

 

 

I’d like to add to this that the definition of the PURE_VIRTUAL macro is defined as

#define PURE_VIRTUAL(func,extra) { LowLevelFatalError(TEXT("Pure virtual not implemented (%s)"), TEXT(#func)); extra }.

 

 

In case you want a pure virtual with return type, the return must be part of the PURE_VIRTUAL macro. as it must be a complete function in the CPP sense.

	virtual bool IsValid() PURE_VIRTUAL(MyClass::IsValid, return false;);

 

ref : http://egloos.zum.com/sweeper/v/3205742

ref : https://forums.unrealengine.com/t/how-do-i-implement-pure-virtual-methods/280323

반응형
반응형

유니티 코루틴 호출하는 방법 2가지가 있다.

 

[ A ] 직접 호출

StartCoroutine(Method());
StartCoroutine(Method(object value)); // 매개변수

[ B ] String 호출

StartCoroutine("Method");
StartCoroutine("Method", value);

 

B는 스트링이라 호출 오버헤드가 발생. 이후의 성능 차이는 없다고 한다.

하지만 그렇다고 A를 쓰기엔 주의해야 될 점이 있는데,

 

A방식으로 호출한 코루틴은 StopCoroutine을 사용할 때 string으로 호출해도 중지시킬 수 없다.

StartCoroutine( Method());
StopCoroutine( Method()); // 중지되지않음 : 잘못된 사용법


StartCoroutine( Method() );
StopCoroutine( "Method" ); // 중지되지않음


StartCoroutine( "Method" );
StopCoroutine( "Method" ); // 중지됨

 

아래가 올바른 사용법.

Coroutine runningCoroutine = null;  //코루틴 변수. 1개의 루틴만 돌리기 위해 저장한다.

//만약 이미 돌고 있는 코루틴이 있다면, 정지시킨 후 코루틴을 실행한다.
if(runningCoroutine != null)
{
    StopCoroutine(runningCoroutine);
}
runningCoroutine = StartCoroutine(ScaleUp()); //코루틴을 시작하며, 동시에 저장한다.

 

 

정리하자면,

1. String으로 호출한 Coroutine은 String으로 멈출 것.

2. 직접 호출한 코루틴은 반환 값을 저장한 다음에 저장된 반환 값을 사용하여 멈출 것.

 

# 참고로 코루틴을 여러 개 돌리고 싶다면 변수 방식으로 해야 한다. string방식은 이름이 같은 모든 코루틴을 조작한다.

 

 

ref : https://mentum.tistory.com/248

반응형
반응형

This Repository shows a Code Example of how to use UE4 Online Subsystem Sessions in C++.

If you need some additional information, consider heading over to the matching Blog Post: https://cedric-neukirchen.net/2021/06/27/ue4-multiplayer-sessions-in-c/

Project was created in Unreal Engine 4.26.2.

Please report any Issues with GitHubs Issues page.

Please utilize Pull Requests if you wish to suggest changes, improvements or updates.

 

 

ref : https://github.com/eXifreXi/CppSessions

 

GitHub - eXifreXi/CppSessions: Repository showing how to use UE4 OnlineSubsystem Sessions in C++.

Repository showing how to use UE4 OnlineSubsystem Sessions in C++. - GitHub - eXifreXi/CppSessions: Repository showing how to use UE4 OnlineSubsystem Sessions in C++.

github.com

반응형
반응형

 

Q :

Hello all!

I feel like this is a pretty simple one, but I can’t wrap my head around it.

In a multiplayer game with a dedicated server and multiple players in the level, what will GetPlayerController(0) return?

What if it’s not a dedicated server game?

Is the answer different when the code runs on the server or on each of the clients?

My first guess is that the first player spawned into the level has the index 0. My second guess is that the index 0 is always with the “local” player for clients; not sure what it would be for the server. I’m edging towards guess 1 at the moment.

 

 

A :

Player 0 is the first player created on the machine that is running the code.

So if you have 2 players on the Server and 2 players on a client and 2 players on another client for a total of 6 players in the game over 3 computers,

then the server’s 2 players will be at index 0 and 1,
and client A’s 2 players will be at index 0 and 1 on their machine. they will also be on the server’s machine but who knows what index they’ll be at there, and they’ll have IDs of -1 because they dont belong to the server machine.

client B, exact same situation.

So server will see 6 player controllers but only its own will have IDs >= 0 the others have IDs == -1

Each client will only see its own playercontrollers.

 

 

ref : https://forums.unrealengine.com/t/what-does-get-player-controller-player-index-0-return-in-multiplayer/431805

반응형
반응형
FString UMobileUtility::GetIPAddress()
{
	FString IpAddr("NONE");
	bool canBind = false;
	TSharedRef<FInternetAddr>LocalIp = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->GetLocalHostAddr(*GLog, canBind);
	if (LocalIp->IsValid())
	{
		//If you want to add a port, write ture
		IpAddr = LocalIp->ToString(false); 
	}

	return IpAddr;
}
반응형

+ Recent posts