11.26 - TIL

김혁·2025년 11월 26일

TIL

목록 보기
65/84

오늘의 코드카타

  • 표 병합
    • 병합과 병합 해제 기능을 가진 표를 구현하는 문제
    • 병합을 할 때, 트리 구조를 활용해서 루트 노드를 저장해서 해당 노드에 접근하게 함
    • 병합 해제할 때, 루트 노드에 접근해서 해당 노드를 루트로 가지고 있는 모든 노드를 초기화함
    • https://school.programmers.co.kr/learn/courses/30/lessons/150366

오늘의 공부

팀 프로젝트 진행 내용

1. 플레이어 스태미나 리젠 Effect 구현

  • 플레이어가 자동으로 스태미나를 리젠하는 능력을 구현하는 데에 있어서, Ability와 Effect 중에서 무엇으로 구현하는 것이 좋을까 고민을 했다.
  • 이 능력은 처음에 캐릭터에 부여해놓고, 필요시에만 끄는 것으로 구현하는 것이 좋아 보인다. 따라서 GameplayEffect로 구현했다.
  • 먼저 현재 플레이어에게 Effect를 자동으로 부여하는 코드를 추가했다.
// AO_PlayerCharacter.h

UCLASS()
class AO_API AAO_PlayerCharacter : public ACharacter, public IAbilitySystemInterface, public IAO_FoleyAudioBankInterface
{
	...
    
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "PlayerCharacter|GAS")
	TArray<TSubclassOf<UGameplayEffect>> DefaultEffects;
    
    ...
}

// AO_PlayerCharacter.cpp

void AAO_PlayerCharacter::BeginPlay()
{
	...
    
	for (const auto& DefaultEffect : DefaultEffects)
	{
		FGameplayEffectContextHandle Context = AbilitySystemComponent->MakeEffectContext();
		Context.AddInstigator(this, this);
            
		FGameplayEffectSpecHandle Handle = AbilitySystemComponent->MakeOutgoingSpec(DefaultEffect, 1.f, Context);
            
		if (Handle.IsValid())
		{
			AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*Handle.Data.Get());
		}
	}
    
    ...
}
  • 다음으로 Effect에 부여할 Effect.Regen.Stamina 태그와 해당 Effect를 비활성화시킬 Status.Debuff.NoStaminaRegen 태그를 추가했다.
  • GE_PlayerCharacter_RegenStamina 에셋을 생성해서 아래와 같이 할당을 했다.
  • 0.1초당 2씩 증가해서 1초당 20씩 증가하도록 했고, Target이 Status.Debuff.NoStaminaRegen 태그를 소유할 시에 해당 Modifier가 적용이 되지 않도록 설정했다.

  • GE_PlayerCharacter_StaminaCost_Sprint 에셋에서 Target Actor에 태그를 부여해서 Sprint 동작 시에 스태미나 리젠이 되지 않도록 했다.
  • 이를 통해 Target Actor에 Status.Debuff.NoStaminaRegen를 부여만 해도, 스태미나 리젠이 되지 않도록 설정을 했다.
  • 아래를 보면 정상적으로 태그가 부여되고, Effect가 적용이 되는 것을 확인할 수 있다.



2. Attribute Set의 정보를 보여주는 UI 생성

  • Attribute Set의 Stamina 값을 보여주는 UserWidget 클래스를 생성했다.
  • Attribute Set의 값이 변경되면 알려주는 델리게이트가 있어서 해당 델리게이트에 구독을 하고, 값 변경 시에 OnStaminaChanged() 함수를 호출해서 WBP에서 해당 함수를 통해 위젯의 정보를 변경하는 방식으로 설계를 했다.
// AO_StaminaWidget.h

UCLASS()
class AO_API UAO_StaminaWidget : public UAO_UserWidget
{
	GENERATED_BODY()

public:
	virtual bool Initialize() override;

protected:
	UFUNCTION(BlueprintImplementableEvent, Category = "GAS|UI")
	void OnStaminaChanged(float NewStamina, float NewMaxStamina);

private:
	void BindStaminaDelegates(UAbilitySystemComponent* ASC);

	void StaminaChanged(const FOnAttributeChangeData& Data);
	void MaxStaminaChanged(const FOnAttributeChangeData& Data);

	float CurrentStamina = 0.0f;
	float MaxStamina = 0.0f;
};
  • 처음 Initialize()가 실행되면 Attribute Set을 찾아서 각각의 필요한 Attribute Data 변경 시에 실행되는 델리게이트를 바인딩해주었고, 초기 값을 설정한 후에 처음에 OnStaminaChanged()를 통해 값 변경을 알려주었다.
// AO_StaminaWidget.cpp

bool UAO_StaminaWidget::Initialize()
{
	if (!Super::Initialize())
	{
		return false;
	}

	if (const TObjectPtr<AAO_PlayerCharacter> PlayerCharacter = Cast<AAO_PlayerCharacter>(GetOwningPlayerPawn()))
	{
		if (TObjectPtr<UAbilitySystemComponent> ASC = PlayerCharacter->GetAbilitySystemComponent())
		{
			BindStaminaDelegates(ASC);
			return true;
		}
	}
	
	return false;
}

void UAO_StaminaWidget::BindStaminaDelegates(UAbilitySystemComponent* ASC)
{
	if (!ASC)
	{
		return;
	}

	if (const UAO_PlayerCharacter_AttributeSet* AttributeSet = ASC->GetSet<UAO_PlayerCharacter_AttributeSet>())
	{
		ASC->GetGameplayAttributeValueChangeDelegate(AttributeSet->GetStaminaAttribute())
			.AddUObject(this, &UAO_StaminaWidget::StaminaChanged);
		ASC->GetGameplayAttributeValueChangeDelegate(AttributeSet->GetMaxStaminaAttribute())
			.AddUObject(this, &UAO_StaminaWidget::MaxStaminaChanged);

		CurrentStamina = AttributeSet->GetStamina();
		MaxStamina = AttributeSet->GetMaxStamina();
		OnStaminaChanged(CurrentStamina, MaxStamina);
	}
}

void UAO_StaminaWidget::StaminaChanged(const FOnAttributeChangeData& Data)
{
	CurrentStamina = Data.NewValue;
	OnStaminaChanged(CurrentStamina, MaxStamina);
}

void UAO_StaminaWidget::MaxStaminaChanged(const FOnAttributeChangeData& Data)
{
	MaxStamina = Data.NewValue;
	OnStaminaChanged(CurrentStamina, MaxStamina);
}
  • 처음으로 기능만 제작하는 것이기 때문에 간단한 구조로 WBP를 제작했다.

  • 해당 UI를 PlayerController의 BeginPlay()에서 생성한 후, 뷰포트에 추가했다.
  • 좌측 상단에 간단하게 체력과 스태미나를 표시했고, 테스트 시에 Regen과 Sprint 시에 스태미나가 정상 동작하는 것을 확인했다.

오늘의 CS

메모리 누수를 찾는 방법

  • 동적 메모리 할당한 후에 delete를 통해 메모리 해제를 하지 않은 경우, 메모리 누수가 발생하게 됨
  • 자동화된 분석 도구 사용을 통해 해당 메모리 누수를 확인할 수 있음

Visual Studio 메모리 디버깅 도구 (Window CRT 라이브러리)

  • CRT(C Runtime) 라이브러리를 통해 MSVC 환경에서 제공하는 디버깅 기능을 활용할 수 있다.
  • _CrtSetDbgFlag() : 함수를 통해 할당된 메모리 블록 번호를 추적하여 누수가 발생한 위치를 파악할 수 있음
  • _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF : 보통 내부에 비트 필드를 통해 디버그할 속성들을 설정해서 사용할 수 있다.
  • 참고 사이트 : Microsoft : _CrtSetDbgFlag

소멸자에 virtual 키워드를 붙여야 하는 이유

  • C++의 다형성과 상속 관계에서 발생하는 메모리 누수를 방지하기 위함

  • 상속 관계에서 Base Class의 포인터로 Derived Class의 객체를 가리키는 경우가 많음
Base* ptr = new Derived();
delete ptr;
  • 이때 Base Class의 소멸자에 virtual 키워드가 없으면 문제가 발생함
    • 정적 바인딩 : 컴파일러는 포인터의 타입만 보고 Base 클래스의 소멸자만 호출함
    • 부분 소멸 및 누수 : Derived 클래스의 소멸자는 호출하지 않음. 이로 인해 만약에 Derived 클래스에서 힙 메모리를 할당한 경우, 해제되지 않고 메모리 누수가 발생하게 됨
  • virtual 키워드를 붙이게 되면 실제 객체 타입의 소멸자를 호출하게 됨
    • Derived 클래스의 소멸자가 호출되어 파생 클래스의 자원을 해제함
    • 이후 Base 클래스의 소멸자가 호출되어 기반 클래스의 자원을 해제함
  • 이로 인해 상속 관계에서 발생할 수 있는 메모리 누수를 방지할 수 있음
profile
게임 개발자를 향해..

0개의 댓글