
같은 동적 머터리얼을 런타임에 공유해서 써도 되는 것들이 있다.
총과 총알은 항상 같은 색으로 동기화 돼야 하기 때문에, 이 둘의 동적 머터리얼을 따로 만들지 않고 공유하는 설계로 바꾸려 한다.
추상 클래스와 인터페이스의 차이점은 그 목적이라고 할 수 있습니다. 추상 클래스는 기본적으로 클래스 이며 이를 상속, 확장하여 사용하기 위한 것입니다. 반면 인터페이스는 해당 인터페이스를 구현한 객체들에 대한 동일한 사용방법과 동작을 보장하기 위해 사용합니다.
출처: https://www.inflearn.com/community/questions/236439/interface와-abstract-class-의-차이점?srsltid=AfmBOoosy4KiblxAqPiQO0Ma8fymZMP7JX-HuErREn3whfBYC8u__E6t
인터페이스 클래스는 (잠재적으로) 무관한 클래스 세트가 공통의 함수 세트를 구현할 수 있도록 하는 데 쓰입니다. 그대로라면 유사성이 없었을 크고 복잡한 클래스들에 어떤 게임 함수 기능을 공유시키고자 하는 경우 매우 좋습니다. 예를 들어 트리거 볼륨에 들어서면 함정이 발동되거나, 적에게 경보가 울리거나, 플레이어에게 점수를 주는 시스템을 가진 게임이 있다 칩시다. 함정, 적, 점수에서 ReactToTrigger (트리거에 반응) 함수를 구현하면 될 것입니다. 하지만 함정은
AActor에서 파생될 수도, 적은 특수APawn또는ACharacter서브클래스일 수도, 점수는UDataAsset일 수도 있습니다. 이 모든 클래스에 공유 함수 기능이 필요하지만,UObject말고는 공통 조상이 없습니다. 이럴 때 인터페이스를 추천합니다.
출처: https://dev.epicgames.com/documentation/ko-kr/unreal-engine/interfaces?application_version=4.27
위와 같은 내용에 따라 인터페이스로 ColorGun과 ProjectileActor 클래스의 동적 머터리얼 기능만을 따로 묶으려 했다. ChangeColor()라는 공유 함수 기능을 넣을 공통 조상이 없었기 때문이다.
이는 잘못된 생각이다. 인터페이스에는 멤버 변수와 멤버 함수를 넣으면 안되기 때문이다.
- 추상 클래스는 순수 가상 함수 외에도 멤버 변수와 멤버 함수가 존재해도 되지만,
- 인터페이스에는 순수 가상 함수만 존재해야한다.
언리얼 UObject와 AActor는 기본적으로 다중 상속이 불가능하다. 다이아몬드 상속 구조를 피하기 위해 이렇게 설계 된 것으로 알고 있다.
ACMColorGun은 이미 ACMWeapon 을 상속받고, 그렇다고 ProjectileActor가 ACMWeapon을 상속받게 하는 설계는 맞지 않다고 생각한다.
분명 Projectile 클래스에는 필요 없는 Weapon만의 공통 함수가 존재하고, 모든 Weapon이 Dynamic Material Instance 변수를 공유할 필요가 없기 때문이다.
Dynamic Material Instance 변수를 공유하는 클래스를 새로 만들고, ColorGun과 Projectile이 이 클래스 하나의 객체를 런타임에서 공유하는 것이다.
나중에 컨텐츠가 확장되어 이 두 개의 클래스와 동시에 색깔이 바뀌는 무언가가 생길 수 있으므로, 그냥 함수 매개변수 등으로 넘겨서 공유하기 보다는 컴포지션이 나을 것이다.
수영님 의견: 사실 색상은 가진다고 표현하는 게 맞으므로, 컴포지션이 맞는 것 같다.
결론: AActorComponent를 상속받은 컴포넌트로 설계하자.
그러하다면 이 클래스의 객체는 플레이어 별로 공유되어야 하기 때문에
서버 측에서는 로컬: Authority, 원격: SimulatedProxy가 되어야 하고,
클라이언트 측에서는 로컬: SimulatedProxy, 원격: Authority가 되어야 한다.
// Fill out your copyright notice in the Description page of Project Settings.
#include "Weapon/CMColorSyncComponent.h"
#include "CMSharedDefinition.h"
// Sets default values for this component's properties
UCMColorSyncComponent::UCMColorSyncComponent()
{
CurrentColor = CM_COLOR_NONE;
}
void UCMColorSyncComponent::CreateDynamicMaterial(UMaterialInterface* InMaterial)
{
if (InMaterial)
{
UMaterialInstanceDynamic* TempDynmaicInstance = Cast<UMaterialInstanceDynamic>(InMaterial);
if (TempDynmaicInstance)
{
SetDynmicInstance(TempDynmaicInstance);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("UCMColorSyncComponent::Failed to CreateDynamicMaterial"));
}
}
void UCMColorSyncComponent::ChangeColor(const FGameplayTag& InColor)
{
CurrentColor = InColor;
const FLinearColor RealColor = CMSharedDefinition::TranslateColor(CurrentColor);
if (DynamicInstance)
{
// 현재 컬러로 곱하기
DynamicInstance->SetVectorParameterValue(FName("Tint"), RealColor);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("UCMColorSyncComponent::Failed to Load DynmaicMaterial"));
}
}
→ ColorSyncComponent를 ColorGun이 먼저 생성자에서 생성하고, BeginPlay에서 ColorGun이 갖는 메시의 첫번째 머터리얼 (미리 세팅해둔 총알과 같이 쓰는 머터리얼) 을 DynamicInstance 생성해 컴포넌트의 변수에 넣는다. CreateDynamicMaterial()
void ACMColorGun::BeginPlay()
{
Super::BeginPlay();
if (ColorSync)
{
// Set Mat by Created Sync Dynamic Mat
ColorSync->CreateDynamicMaterial(SkeletalMesh->CreateAndSetMaterialInstanceDynamic(0));
SkeletalMesh->SetMaterial(0, ColorSync->GetDynamicInstance());
// Set As Default Color
CallChangeColor(ColorSync->GetCurrentColor());
}
}
→ 총알은 ColorGun에서 Projectile을 생성하는 시기(ACMColorGun::Fire())에 ColorSync 변수가 ColorGun의 것과 똑같은 객체를 가르키도록 변수에 넣는다.
void ACMColorGun::Fire()
{
// Skip if it has no bullet
if(GetCurrentBullet() <= 0)
{
return;
}
// Skip if it has no Animation
if (PlayerAnimInstance == nullptr)
//...
// 왼손 슈팅 Animation 정상 처리 됐을 때 총알 쏘기
else if (ProjectileClass && PlayerAnimInstance->PlayShooting(1) == 1)
{
// Projectile 생성 위치 (카메라 기준) 계산 로직 생략 ...
{
ACMProjectileActor* Projectile = GetWorld()->SpawnActor<ACMProjectileActor>(ProjectileClass, MuzzleLocation, MuzzleRotation, SpawnParams);
if (Projectile)
{
FVector LaunchDirection = MuzzleRotation.Vector();
//*******************************************
if (ColorSync)
{
Projectile->SetSyncAndMat(ColorSync);
}
//*******************************************
Projectile->FireInDirection(LaunchDirection);
}
}
SetCurrentBullet(GetCurrentBullet() - 1);
OnBulletChanged.Broadcast(GetCurrentBullet(), GetMaxBullet());
}
if(GetCurrentBullet() == 0)
{
CallChangeColor(CM_COLOR_NONE);
}
}
void ACMProjectileActor::SetSyncAndMat(UCMColorSyncComponent* InColorSync)
{
ColorSync = InColorSync;
if (ColorSync && ColorSync->GetDynamicInstance())
{
// Set Mat by Sync Dynamic Mat
StaticMesh->SetMaterial(0, ColorSync->GetDynamicInstance());
}
}
→ ColorGun과 Projectile의 공통된 CurrentColor변수와 ChangeColor() 함수를 컴포넌트로 빼고, ColorGun::ChangeColor()에서 총알 수와 UI 델리게이트 호출에 관련된 로직은 따로 분리했다.
void ACMColorGun::CallChangeColor(const FGameplayTag& InColor)
{
if(ColorSync)
{
ColorSync->ChangeColor(InColor);
OnColorChanged.Broadcast(InColor);
// 색이 제대로 바뀌었을 때만 장전 완료 처리
if(InColor != CM_COLOR_NONE)
{
// Update Bullet
SetCurrentBullet(GetMaxBullet());
OnBulletChanged.Broadcast(GetCurrentBullet(), GetMaxBullet());
UE_LOG(LogTemp, Warning, TEXT("ColorGun Reload : %s"), *InColor.ToString());
}
}
}

ACMColorGun::Fire() 에서 Projectile 생성과 동시에 동적 머터리얼을 공유한 후, 남은 총알이 1 → 0이 될 때 바로 UI 를 Default 색(NONE)으로 초기화한다.
이러한 로직 때문에 마지막 총알 1발은 Default 색(NONE)으로 초기화되고, 몬스터를 맞출 때도 몬스터와 데칼이 이 색이 전달되는 문제가 발생한다.
void ACMColorGun::Fire()
{
//...
ACMProjectileActor* Projectile = GetWorld()->SpawnActor<ACMProjectileActor>(ProjectileClass, MuzzleLocation, MuzzleRotation, SpawnParams);
if (Projectile)
{
FVector LaunchDirection = MuzzleRotation.Vector();
//*******************************************
if (ColorSync)
{
Projectile->SetSyncAndMat(ColorSync);
}
//*******************************************
Projectile->FireInDirection(LaunchDirection);
}
}
SetCurrentBullet(GetCurrentBullet() - 1);
OnBulletChanged.Broadcast(GetCurrentBullet(), GetMaxBullet());
}
//*******************************************
if(GetCurrentBullet() == 0)
{
CallChangeColor(CM_COLOR_NONE);
}
//*******************************************
}
Fire()의 로직 상 머터리얼만 공유하고, 프로젝타일을 가지지 않은 채 색 데이터를 변경하여 자동으로 머터리얼에 반영되기 때문에, NONE 색으로 바꾸는 시점에 이미 공유된 머터리얼로 발사한 프로젝타일의 머터리얼을 따로 다시 공유되지 않는 머터리얼로 바꿀 수 있을 지 고민되었다.
→ 프로젝타일 변수의 scope 존재하는 곳에서 먼저 Bullet 수를 업데이트 한 후에, 수에 따라서 프로젝타일이 공유된 (ColorSync) Dynamic Material을 사용할 것인지, 공유되지 않고 총알 스스로 만든 Dynamic Material을 사용할 것인지에 대한 분기문으로 해결했다.
→ 몬스터에게 전달되는 Color Tag 값은 총알 클래스에서 공유되는 ColorSync의 Current Color 값으로 하지 않고, 총알 클래스에 한 번 Memorized 된 Local Current Color 변수 값을 넘겨주도록 하여 해결했다.
void ACMColorGun::Fire()
{
//... 생략
ACMProjectileActor* Projectile = GetWorld()->SpawnActor<ACMProjectileActor>(ProjectileClass, MuzzleLocation, MuzzleRotation, SpawnParams);
if (Projectile)
{
// 생략 ...
//*******************************************
// Update Bullet Num
SetCurrentBullet(GetCurrentBullet() - 1);
//*******************************************
if (ColorSync)
{
//*******************************************
// Memorizing Color in Each Projectile
Projectile->SetProjectileCurrentColor(ColorSync->GetCurrentColor());
//*******************************************
// If it is not the Last, Set DynMat As Shared by ColorGun
if (GetCurrentBullet() != 0)
{
Projectile->SetSyncAndMat(ColorSync);
}
else
{
//*******************************************
// Last Projectile has to be Localized DynMat
Projectile->ChangeColorByLast();
//*******************************************
}
}
Projectile->FireInDirection(LaunchDirection);
}
}
// Update Bullet UI
OnBulletChanged.Broadcast(GetCurrentBullet(), GetMaxBullet());
}
if(GetCurrentBullet() == 0)
{
**// 총알 넘긴 이후로는 ColorSync의 머터리얼과 UI 등의 색이 DEFAULT 색으로 변경돼도 괜찮다.**
CallChangeColor(CM_COLOR_NONE);
}
}
void ACMProjectileActor::ChangeColorByLast()
{
UMaterialInstanceDynamic* TempDynamic = StaticMesh->CreateAndSetMaterialInstanceDynamic(0);
if (TempDynamic)
{
const FLinearColor RealColor = CMSharedDefinition::TranslateColor(ProjectileCurrentColor);
TempDynamic->SetVectorParameterValue(FName("Tint"), RealColor);
StaticMesh->SetMaterial(0, TempDynamic);
UE_LOG(LogTemp, Warning, TEXT("ACMProjectileActor::ChangeColorByLast"));
}
}




Alloc/Free Event Count 절댓값이 줄어들었다.
Dynamic Instance를 공유함으로써 메모리 할당 횟수가 줄지 않았을까 추측해본다.
이것이 절대적인 지표라고 할 수는 없다.

물론 이것이 플레이 중 각각 극히 일부 시점이고, LLM Scene Render 의 값이 Material 관련 메모리의 완벽한 지표라고 할 수는 없지만, 종합적으로 봤을 때 아주 미미하게 값을 줄일 수 있지 않았을까 추측해본다.
사실 너무 미미한 차이라서 거의 알아보기 힘들다. 네트워크로 확장해서 다수의 클라이언트가 총알을 계속 쐈을 때는 차이를 알아볼 수 있지 않을까 싶다.
언제나 의견 및 댓글은 환영입니다!
Dynamic Material Instance를 많이 생성하면
런타임 비용이 증가한다.
항상 모든 시점에서 색깔이 동기화 되는 객체 끼리는
같은 Dynamic Material Instance를 공유하도록 설계한다.
멤버변수와 공통 함수를 공유하되,
ColorGun은 이미 Weapon을 상속받으므로
컴포지션 관계로 설계한다.
액터 컴포넌트를 상속받는 ColorSyncComponent 클래스 객체를
Projectile 생성 시 넘겨준다.