๐Ÿ“˜TIL - ์—ํ”ฝ ๋ชฌ์Šคํ„ฐ ๊ตฌํ˜„ ๋งˆ๋ฌด๋ฆฌ & ๋ฆฌํŒฉํ† ๋ง

HappyCircleยท2026๋…„ 6์›” 19์ผ

Unreal ๊ฐœ๋ฐœ

๋ชฉ๋ก ๋ณด๊ธฐ
149/163

๋ชฉ์ฐจ

  1. ์˜ค๋Š˜ ์ž‘์—… ๊ฐœ์š”
  2. ๊ณตํ†ต ์ฝ”๋“œ ํ•˜๋“œ์ฝ”๋”ฉ ์ œ๊ฑฐ (๋ฆฌํŒฉํ† ๋ง ์„ค๊ณ„)
  3. ๊ณตํ†ต StateTree Task์—์„œ EpicMonsterCharacter ์ง์ ‘ ์˜์กด ์ œ๊ฑฐ
  4. Build.cs ์—๋””ํ„ฐ ๋ชจ๋“ˆ ์˜์กด์„ฑ ๋ˆ„๋ฝ ์ˆ˜์ •
  5. DataTable ํ•„๋“œ๋ช… ํ˜ธํ™˜์„ฑ ๋ณต๊ตฌ ๋ฐ develop ๊ตฌ์กฐ ์ •๋ ฌ
  6. develop ๋จธ์ง€ ์ถฉ๋Œ ํ•ด๊ฒฐ ์ „๋žต
  7. ์—ํ”ฝ ๊ธฐ๋ณธ ๊ณต๊ฒฉ ํƒœ๊ทธ ๋ˆ„๋ฝ ๋ฒ„๊ทธ
  8. ๊ทธ๋กœ๊ธฐ ์‹œ์Šคํ…œ 3์ข… ๋ฒ„๊ทธ ์ˆ˜์ •
  9. Treant ํŠน์ˆ˜ ์Šคํ‚ฌ ํˆฌ์‚ฌ์ฒด/VFX ์˜ค๋ฅ˜ ์ˆ˜์ •
  10. HP Bar ๋™๊ธฐํ™” ๋ฌธ์ œ ์ˆ˜์ •
  11. WBP_MonsterHealthBar ๋ ˆ์ด์•„์›ƒ ์žฌ์ •๋ ฌ
  12. ์˜ค๋Š˜ ๋ณ€๊ฒฝ ํŒŒ์ผ ์š”์•ฝ
  13. ๋‚ด์ผ ํ•  ์ผ

์˜ค๋Š˜ ์ž‘์—… ๊ฐœ์š”

์—ํ”ฝ ๋ชฌ์Šคํ„ฐ 3์ข…(Dragon, Magma, Treant) ๊ตฌํ˜„์„ ์™„๋ฃŒํ•œ ๋’ค, ๊ณตํ†ต ์ฝ”๋“œ์— ์„ž์ธ ์—ํ”ฝ ์ „์šฉ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜๋Š” ๋ฆฌํŒฉํ† ๋ง์„ ์ง„ํ–‰ํ–ˆ๋‹ค. ์ดํ›„ origin/develop ์ตœ์‹  ์‚ฌํ•ญ๊ณผ ๋จธ์ง€ํ•˜๋Š” ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•œ ์ถฉ๋Œ ํ•ด๊ฒฐ, ๊ธฐ๋ณธ ๊ณต๊ฒฉ ๋ฒ„๊ทธ ์ˆ˜์ •, ๊ทธ๋กœ๊ธฐ ์‹œ์Šคํ…œ ์ „๋ฐ˜ ์ˆ˜์ •, Treant ํˆฌ์‚ฌ์ฒด/VFX ์˜ค๋ฅ˜ ์ˆ˜์ •๊นŒ์ง€ ์ด์–ด์„œ ์ง„ํ–‰ํ–ˆ๋‹ค.

์ „์ฒด ํ๋ฆ„์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค:
1. ๊ณตํ†ต ์ฝ”๋“œ ํ•˜๋“œ์ฝ”๋”ฉ ์ œ๊ฑฐ ๋ฐ virtual ํ›… ๊ตฌ์กฐ ์„ค๊ณ„
2. Build.cs ์—๋””ํ„ฐ ๋ชจ๋“ˆ ์˜์กด์„ฑ ๋ˆ„๋ฝ ์ˆ˜์ •
3. DataTable ํ•„๋“œ๋ช… ํ˜ธํ™˜์„ฑ ๋ณต๊ตฌ ๋ฐ develop ๊ตฌ์กฐ ์ •๋ ฌ
4. develop ๋จธ์ง€ ๋ฐ ์ถฉ๋Œ ํ•ด๊ฒฐ
5. ์—ํ”ฝ ๊ธฐ๋ณธ ๊ณต๊ฒฉ ํƒœ๊ทธ ๋ˆ„๋ฝ ๋ฒ„๊ทธ ์ˆ˜์ •
6. ๊ทธ๋กœ๊ธฐ ์‹œ์Šคํ…œ 3์ข… ๋ฒ„๊ทธ ์ˆ˜์ •
7. Treant ํˆฌ์‚ฌ์ฒด VFX ์˜ค๋ฅ˜ ์ˆ˜์ •
8. HP Bar ๋™๊ธฐํ™” ๋ฌธ์ œ ์ˆ˜์ •
9. WBP_MonsterHealthBar ๋ ˆ์ด์•„์›ƒ ์žฌ์ •๋ ฌ ์‹œ๋„


1. ๊ณตํ†ต ์ฝ”๋“œ ํ•˜๋“œ์ฝ”๋”ฉ ์ œ๊ฑฐ (๋ฆฌํŒฉํ† ๋ง ์„ค๊ณ„)

๋ฐฐ๊ฒฝ

์—ํ”ฝ ๋ชฌ์Šคํ„ฐ 3์ข… ๊ตฌํ˜„์„ ์™„๋ฃŒํ•œ ์งํ›„, ๋กœ์ปฌ ์ฒด์ธ์ง€์—๋Š” ๊ณตํ†ต ํŒŒ์ผ(EnemyCombatComponent, GA_ShootProjectiles, StateTreeTask_*)์ด ๋Œ€๊ฑฐ ์ˆ˜์ •๋˜์–ด ์žˆ์—ˆ๋‹ค. ๋ฌธ์ œ๋Š” ์ด ํŒŒ์ผ๋“ค์ด ์ผ๋ฐ˜ ๋ชฌ์Šคํ„ฐยท๋ณด์Šค ๋ชฌ์Šคํ„ฐ์—์„œ๋„ ์‚ฌ์šฉํ•˜๋Š” ๊ณตํ†ต ํŒŒ์ผ์ด๋ผ๋Š” ์ ์ด์—ˆ๋‹ค.

๊ตฌ์ฒด์ ์œผ๋กœ EnemyCombatComponent.cpp์—๋Š” ์•„๋ž˜ ๊ฐ™์€ ์ฝ”๋“œ๊ฐ€ ๋“ค์–ด๊ฐ€ ์žˆ์—ˆ๋‹ค:

// ์ž˜๋ชป๋œ ์˜ˆ - ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ์— ์—ํ”ฝ row ์ด๋ฆ„ ํ•˜๋“œ์ฝ”๋”ฉ
if (PatternRow->RowName == TEXT("Dragon_Bite") || PatternRow->RowName == TEXT("Dragon_AerialFireball"))
{
    Priority = 100;
}
if (PatternRow->RowName == TEXT("Treant_FloodBarrage"))
{
    MaxRange = 2500.f;
}

GA_ShootProjectiles.cpp์—๋„ ์—์…‹ ๊ฒฝ๋กœ๊ฐ€ ๋ฐ•ํ˜€ ์žˆ์—ˆ๋‹ค:

// ์ž˜๋ชป๋œ ์˜ˆ - ๊ณตํ†ต GA์— ์—ํ”ฝ ์ „์šฉ ์—์…‹ ๊ฒฝ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ
if (PatternRow->RowName.ToString().Contains("Dragon"))
{
    ProjectileClass = StaticLoadClass(AEnemyProjectile::StaticClass(), nullptr,
        TEXT("/Game/Retrieve/AI/Boss/Projectiles/BP_LavaProjectile.BP_LavaProjectile_C"));
    StatusEffectClass = StaticLoadClass(UGameplayEffect::StaticClass(), nullptr,
        TEXT("/Game/Retrieve/AbilitySystem/Enemy/GE_Burn.GE_Burn_C"));
}
else if (PatternRow->RowName.ToString().Contains("Magma"))
{
    // GE_Cold ...
}

์ด ๊ตฌ์กฐ๋Š” ์„ธ ๊ฐ€์ง€ ๋ฌธ์ œ๋ฅผ ๋งŒ๋“ ๋‹ค:

  • ์ผ๋ฐ˜/๋ณด์Šค ๋ชฌ์Šคํ„ฐ๊ฐ€ ์ด ๊ณตํ†ต ์ฝ”๋“œ๋ฅผ ํƒ€๋Š” ์ˆœ๊ฐ„, ์˜๋„์น˜ ์•Š๊ฒŒ ์—ํ”ฝ ๋ถ„๊ธฐ์— ๊ฑธ๋ฆด ์ˆ˜ ์žˆ๋‹ค.
  • ํŒ€์›๋“ค์ด ์ผ๋ฐ˜/๋ณด์Šค ๋ชฌ์Šคํ„ฐ ์ž‘์—… ์ค‘ ์ด ํŒŒ์ผ์„ ๊ฑด๋“œ๋ฆด ๋•Œ ๋ณ‘ํ•ฉ ์ถฉ๋Œ์ด ํฌ๊ฒŒ ๋ฐœ์ƒํ•œ๋‹ค.
  • row ์ด๋ฆ„์„ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ์ƒˆ ์—ํ”ฝ ๋ชฌ์Šคํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ๋งˆ๋‹ค ๊ณตํ†ต ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•ด์•ผ ํ•œ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉํ–ฅ

"๊ณตํ†ต ๋Ÿฐํƒ€์ž„ ์ฝ”๋“œ๋Š” ๋ฐ์ดํ„ฐ๋งŒ ์ฝ๋Š”๋‹ค"๋Š” ์›์น™ ์•„๋ž˜ ๋‹ค์Œ ๊ตฌ์กฐ๋กœ ๋ณ€๊ฒฝํ–ˆ๋‹ค.

FMonsterPatternRow์— ์‹ ๊ทœ ํ•„๋“œ ์ถ”๊ฐ€:

// RetrieveDataTableTypes.h
USTRUCT(BlueprintType)
struct FMonsterPatternRow : public FTableRowBase
{
    // ... ๊ธฐ์กด ํ•„๋“œ ...

    // ์—ํ”ฝ ๋ชฌ์Šคํ„ฐ ์ „์šฉ ํ™•์žฅ ํ•„๋“œ (์ผ๋ฐ˜/๋ณด์Šค๋Š” ๊ธฐ๋ณธ๊ฐ’ nullptr/None)
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TSubclassOf<AEnemyProjectile> ProjectileClass;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TSubclassOf<UGameplayEffect> StatusEffectClass;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    EProjectileSpawnPattern SpawnPattern = EProjectileSpawnPattern::Single;

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    FMonsterProjectilePatternConfig ProjectileConfig;
};

FMonsterProjectilePatternConfig์—๋Š” ํˆฌ์‚ฌ์ฒด ํŒจํ„ด ์„ธ๋ถ€ ์„ค์ •์„ ๋ฐ์ดํ„ฐํ™”ํ–ˆ๋‹ค:

USTRUCT(BlueprintType)
struct FMonsterProjectilePatternConfig
{
    UPROPERTY(EditAnywhere)
    int32 RadialProjectileCount = 8;       // RadialSpread ๋ฐœ์ˆ˜

    UPROPERTY(EditAnywhere)
    float SpreadSpeedMultiplierMin = 0.9f;

    UPROPERTY(EditAnywhere)
    float SpreadSpeedMultiplierMax = 1.2f;

    UPROPERTY(EditAnywhere)
    float GroundPillarSpawnDepth = -80.f;  // GroundPillar ์ง€๋ฉด ์˜คํ”„์…‹

    UPROPERTY(EditAnywhere)
    bool bRainFromAbove = false;

    UPROPERTY(EditAnywhere)
    bool bReflectable = false;
};

GA_ShootProjectiles.cpp ์ •๋ฆฌ ํ›„:

// ์ •๋ฆฌ ํ›„ - row์—์„œ ๋ฐ์ดํ„ฐ๋งŒ ์ฝ์Œ
void UGA_ShootProjectiles::ScheduleProjectiles(const FMonsterPatternRow* PatternRow)
{
    // C++ ๋ถ„๊ธฐ ์—†์Œ, ๋ฐ์ดํ„ฐ ํ…Œ์ด๋ธ”์ด ๊ฒฐ์ •
    TSubclassOf<AEnemyProjectile> ResolvedClass = PatternRow->ProjectileClass
        ? PatternRow->ProjectileClass
        : DefaultProjectileClass;

    switch (PatternRow->SpawnPattern)
    {
        case EProjectileSpawnPattern::RadialSpread:
            SpawnRadialVolley(ResolvedClass, PatternRow->ProjectileConfig.RadialProjectileCount,
                              PatternRow->ProjectileConfig.SpreadSpeedMultiplierMin,
                              PatternRow->ProjectileConfig.SpreadSpeedMultiplierMax);
            break;
        case EProjectileSpawnPattern::GroundPillar:
            SpawnGroundPillar(ResolvedClass, PatternRow->ProjectileConfig.GroundPillarSpawnDepth);
            break;
        default:
            SpawnSingle(ResolvedClass);
    }
}

EnemyCombatComponent.cpp ์ •๋ฆฌ ํ›„:

// ์ •๋ฆฌ ํ›„ - row ์ด๋ฆ„ ๋ถ„๊ธฐ ์—†์ด DT์˜ Priority/MaxActivationRange/Cooldown๋งŒ ์ฝ์Œ
bool UEnemyCombatComponent::CanActivatePattern(const FMonsterPatternRow* Row) const
{
    if (DistanceToTarget > Row->MaxActivationRange) return false;
    if (PatternCooldowns.Contains(Row->RowName) && PatternCooldowns[Row->RowName] > 0.f) return false;
    return true;
}

๋ฐฐ์šด ์ 

๊ณตํ†ต ๋Ÿฐํƒ€์ž„ ์ฝ”๋“œ์— ํŠน์ • ๋ชฌ์Šคํ„ฐ row ์ด๋ฆ„์ด ๋‹จ ํ•˜๋‚˜๋ผ๋„ ๋“ค์–ด๊ฐ€๋Š” ์ˆœ๊ฐ„, ๊ทธ ํŒŒ์ผ์€ "ํŠน์ • ๋ชฌ์Šคํ„ฐ ์ „์šฉ ํŒŒ์ผ"๊ณผ ๋‹ค๋ฆ„์—†์–ด์ง„๋‹ค. ๊ธฐํš์ด ๋ฐ”๋€Œ๊ฑฐ๋‚˜ ์ƒˆ ๋ชฌ์Šคํ„ฐ๊ฐ€ ์ถ”๊ฐ€๋  ๋•Œ๋งˆ๋‹ค ๊ณตํ†ต ์ฝ”๋“œ๋ฅผ ๊ณ ์ณ์•ผ ํ•˜๋Š” ๊ตฌ์กฐ๊ฐ€ ๋œ๋‹ค. ๋ชจ๋“  ๋ชฌ์Šคํ„ฐ๋ณ„ ํŠน์„ฑ์€ DataTable row๊ฐ€ ์†Œ์œ ํ•˜๊ณ , ์ฝ”๋“œ๋Š” ๊ทธ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๊ธฐ๋งŒ ํ•ด์•ผ ํ•œ๋‹ค.


2. ๊ณตํ†ต StateTree Task์—์„œ EpicMonsterCharacter ์ง์ ‘ ์˜์กด ์ œ๊ฑฐ

๋ฐฐ๊ฒฝ

StateTreeTask_EnemyAttack.cpp, StateTreeTask_EnemyPatternAttack.cpp, StateTreeTask_ShiftOrbitSlot.cpp ์•ˆ์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝ”๋“œ๊ฐ€ ์žˆ์—ˆ๋‹ค:

#include "Character/RetrieveEpicMonsterCharacter.h"

// ...

ARetrieveEpicMonsterCharacter* EpicChar = Cast<ARetrieveEpicMonsterCharacter>(EnemyChar);
if (EpicChar && EpicChar->UsesForwardLocomotion())
{
    EpicChar->UpdateGroundTurnAnimation(TurnRate);
}

๊ณตํ†ต StateTree task๊ฐ€ ํŒŒ์ƒ ํด๋ž˜์Šค์ธ ARetrieveEpicMonsterCharacter๋ฅผ ์ง์ ‘ includeํ•œ๋‹ค๋Š” ๊ฒƒ์€ ์˜์กด ๋ฐฉํ–ฅ์ด ์—ญ์ „๋œ ์ƒํƒœ๋‹ค. base โ†’ derived ์ฐธ์กฐ๋Š” ๊ณง "base๊ฐ€ derived ์—†์ด๋Š” ๋นŒ๋“œ๊ฐ€ ์•ˆ ๋œ๋‹ค"๋Š” ๋œป์ด ๋œ๋‹ค.

๋˜ํ•œ ์ผ๋ฐ˜/๋ณด์Šค ๋ชฌ์Šคํ„ฐ์—์„œ ์ด task๊ฐ€ ์‹คํ–‰๋  ๋•Œ๋„ Cast ์‹œ๋„๊ฐ€ ์ผ์–ด๋‚˜๊ณ  ์‹คํŒจํ•˜๋Š” ๋ถˆํ•„์š”ํ•œ ์—ฐ์‚ฐ์ด ์ƒ๊ธด๋‹ค.

ํ•ด๊ฒฐ โ€” Base์— no-op virtual ํ›… ์ถ”๊ฐ€

// RetrieveEnemyCharacter.h
UCLASS()
class RETRIEVE_API ARetrieveEnemyCharacter : public ACharacter
{
    // ... ๊ธฐ์กด ๋ฉค๋ฒ„ ...

    // ์—ํ”ฝ ๋ชฌ์Šคํ„ฐ๋งŒ override, ์ผ๋ฐ˜/๋ณด์Šค๋Š” ๊ธฐ๋ณธ no-op
    virtual bool UsesForwardLocomotion() const { return false; }
    virtual void UpdateGroundTurnAnimation(float TurnRate) {}
    virtual void StopGroundTurnAnimation() {}
    virtual bool HasAerialPhase() const { return false; }
};
// RetrieveEpicMonsterCharacter.h โ€” override
virtual bool UsesForwardLocomotion() const override { return true; }
virtual void UpdateGroundTurnAnimation(float TurnRate) override;
virtual void StopGroundTurnAnimation() override;
virtual bool HasAerialPhase() const override { return true; }

StateTree task ์ •๋ฆฌ ํ›„:

// #include "Character/RetrieveEpicMonsterCharacter.h" ์ œ๊ฑฐ
// ARetrieveEpicMonsterCharacter* EpicChar = Cast<...> ์ œ๊ฑฐ

// base ํ›…๋งŒ ํ˜ธ์ถœ โ€” Cast ์—†์ด ๋™์ž‘
if (EnemyChar->UsesForwardLocomotion())
{
    EnemyChar->UpdateGroundTurnAnimation(TurnRate);
}

์ด ๋ณ€๊ฒฝ์œผ๋กœ ๊ณตํ†ต StateTree task์—์„œ ์—ํ”ฝ ํ—ค๋” include๊ฐ€ ์™„์ „ํžˆ ์‚ฌ๋ผ์ง€๊ณ , ์ผ๋ฐ˜/๋ณด์Šค ๋ชฌ์Šคํ„ฐ๋Š” no-op ๊ธฐ๋ณธ๊ฐ’์„ ํƒ€์„œ ๋™์ž‘ ๋ณ€ํ™”๊ฐ€ ์—†๋‹ค.

๋ฐฐ์šด ์ 

ํŒŒ์ƒ ํด๋ž˜์Šค๋งŒ์˜ ๋™์ž‘์ด ๊ณตํ†ต ์ฝ”๋“œ์— ํ•„์š”ํ•  ๋•Œ๋Š” ๋ฐ˜๋“œ์‹œ base์— virtual ํ›…์„ ๋‘๊ณ  ํŒŒ์ƒ ํด๋ž˜์Šค์—์„œ overrideํ•˜๋Š” ํŒจํ„ด์œผ๋กœ ๊ฐ€์•ผ ํ•œ๋‹ค. ๊ณตํ†ต ์ฝ”๋“œ์—์„œ ํŒŒ์ƒ ํด๋ž˜์Šค๋กœ Castํ•˜๋Š” ์ˆœ๊ฐ„ ์˜์กด ๋ฐฉํ–ฅ์ด ์—ญ์ „๋˜์–ด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•ด์ง„๋‹ค. no-op ๊ธฐ๋ณธ ๊ตฌํ˜„์ด ์žˆ๋Š” virtual ํ•จ์ˆ˜๋Š” ๊ธฐ์กด ์„œ๋ธŒํด๋ž˜์Šค์˜ ๋™์ž‘์„ ์ „ํ˜€ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์œผ๋ฏ€๋กœ ํ•ญ์ƒ ์•ˆ์ „ํ•˜๋‹ค.


3. Build.cs ์—๋””ํ„ฐ ๋ชจ๋“ˆ ์˜์กด์„ฑ ๋ˆ„๋ฝ ์ˆ˜์ •

๋ฐฐ๊ฒฝ ๋ฐ ์ฆ์ƒ

๋นŒ๋“œ๋ฅผ ์‹คํ–‰ํ•˜์ž ๋‹ค์Œ ์—๋Ÿฌ๊ฐ€ ์ˆœ์„œ๋Œ€๋กœ ๋ฐœ์ƒํ–ˆ๋‹ค:

error C2027: use of undefined type 'FStateTreeCompilerLog'
error LNK2019: unresolved external symbol UAnimGraphNode_SequencePlayer
error LNK2019: unresolved external symbol FPropertyBindingPath

RetrieveConfigureEpicStateTreeCommandlet.cpp๊ฐ€ StateTree๋ฅผ ์—๋””ํ„ฐ์—์„œ ์ปดํŒŒ์ผํ•˜๋Š” API๋ฅผ ์“ฐ๊ณ  ์žˆ์—ˆ๋Š”๋ฐ, ํ•ด๋‹น API๊ฐ€ ์„ ์–ธ๋œ ํ—ค๋”๊ฐ€ StateTreeEditorModule์˜ Public ๊ฒฝ๋กœ์— ์žˆ์—ˆ๋‹ค.

// RetrieveConfigureEpicStateTreeCommandlet.cpp
#include "StateTreeCompilerLog.h"  // StateTreeEditorModule/Public/ ์•„๋ž˜ ์žˆ์Œ

๊ทธ๋Ÿฐ๋ฐ Retrieve.Build.cs์— ์ด ๋ชจ๋“ˆ์ด ์—†์—ˆ๋‹ค:

// ์ˆ˜์ • ์ „ โ€” ์—๋””ํ„ฐ ๋ชจ๋“ˆ ์˜์กด์„ฑ ๋ˆ„๋ฝ
PrivateDependencyModuleNames.AddRange(new string[]
{
    "GameplayAbilities",
    "StateTreeModule",
    // StateTreeEditorModule ์—†์Œ
});

ํ•ด๊ฒฐ

// ์ˆ˜์ • ํ›„ โ€” Editor ๋นŒ๋“œ์—์„œ๋งŒ ์ถ”๊ฐ€
if (Target.Type == TargetRules.TargetType.Editor)
{
    PrivateDependencyModuleNames.AddRange(new string[]
    {
        "StateTreeEditorModule",   // FStateTreeCompilerLog, CompileStateTree API
        "AnimGraph",               // UAnimGraphNode_SequencePlayer
        "PropertyBindingUtils",    // FPropertyBindingPath
    });
}

AnimGraph์™€ PropertyBindingUtils๋Š” ์ฒซ ๋ฒˆ์งธ ๋นŒ๋“œ๊ฐ€ ๋งํฌ ๋‹จ๊ณ„๊นŒ์ง€ ๋„˜์–ด๊ฐ„ ๋’ค ์—ฐ์‡„์ ์œผ๋กœ ๋ฐœ๊ฒฌ๋˜์—ˆ๋‹ค. ์ปดํŒŒ์ผ ๋‹จ๊ณ„ ์—๋Ÿฌ(ํ—ค๋” ๋ฏธํฌํ•จ)์™€ ๋งํฌ ๋‹จ๊ณ„ ์—๋Ÿฌ(์‹ฌ๋ณผ ๋ฏธ์ •์˜)๋Š” ์›์ธ์ด ๋‹ค๋ฅด๋ฏ€๋กœ, ํ•˜๋‚˜์”ฉ ์ˆœ์„œ๋Œ€๋กœ ์žก์•„์•ผ ํ•œ๋‹ค.

๋ฐฐ์šด ์ 

UE์—์„œ ์—๋””ํ„ฐ ์ „์šฉ ํƒ€์ž…/API๋ฅผ ์“ธ ๋•Œ๋Š” ๋‘ ๊ฐ€์ง€๋ฅผ ๋ฐ˜๋“œ์‹œ ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค: (1) ํ•ด๋‹น ํ—ค๋”๊ฐ€ ์–ด๋А ๋ชจ๋“ˆ์˜ Public ๋””๋ ‰ํ† ๋ฆฌ์— ์žˆ๋Š”์ง€, (2) ๊ทธ ๋ชจ๋“ˆ์ด Build.cs์˜ ์—๋””ํ„ฐ ์กฐ๊ฑด ๋ธ”๋ก์— ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€. ์ปดํŒŒ์ผ ์—๋Ÿฌ๋Š” ํ—ค๋” ๊ฒฝ๋กœ ๋ฌธ์ œ, ๋งํฌ ์—๋Ÿฌ๋Š” ์‹ฌ๋ณผ ์†Œ์œ  ๋ชจ๋“ˆ ๋ฌธ์ œ๋‹ค. editor-only ์˜์กด์„ฑ์€ ๋ฐ˜๋“œ์‹œ Target.Type == Editor ์กฐ๊ฑด์œผ๋กœ ๊ฐ์‹ธ์•ผ ํŒจํ‚ค์ง• ๋นŒ๋“œ๊ฐ€ ๊นจ์ง€์ง€ ์•Š๋Š”๋‹ค.


4. DataTable ํ•„๋“œ๋ช… ํ˜ธํ™˜์„ฑ ๋ณต๊ตฌ ๋ฐ develop ๊ตฌ์กฐ ์ •๋ ฌ

๋ฐฐ๊ฒฝ

์ž‘์—… ๋„์ค‘ FMonsterPatternRow ํ•„๋“œ๋ช…์ด ๋‹ค์Œ์ฒ˜๋Ÿผ rename๋˜์–ด ์žˆ์—ˆ๋‹ค:

๊ธฐ์กด ์ด๋ฆ„ (develop ๊ธฐ์ค€)์ž‘์—… ์ค‘ ๋ณ€๊ฒฝ๋œ ์ด๋ฆ„
AttackSequenceSequenceAnimation
HitboxWindowStartTimeHitboxStartTime
HitboxWindowDurationHitboxDuration

์ด rename์€ DataTable ๋ฐ”์ด๋„ˆ๋ฆฌ(.uasset)์— ์ €์žฅ๋œ ๊ธฐ์กด ๊ฐ’๊ณผ C++ struct ํ•„๋“œ๋ช…์ด ๋ถˆ์ผ์น˜ํ•ด์„œ, ์—๋””ํ„ฐ์—์„œ ์—ด ๋•Œ ๊ธฐ์กด ์ผ๋ฐ˜/๋ณด์Šค ๋ชฌ์Šคํ„ฐ row์˜ ๊ฐ’์ด ์ „๋ถ€ ๋‚ ์•„๊ฐ€๋Š” ๊ฒฐ๊ณผ๋ฅผ ์ดˆ๋ž˜ํ•œ๋‹ค.

origin/develop์„ ํ™•์ธํ•ด๋ณด๋‹ˆ ์ด ํ•„๋“œ๋“ค์€ ์ด๋ฏธ develop์—๋„ ๊ธฐ์กด ์ด๋ฆ„ ๊ทธ๋Œ€๋กœ ์žˆ์—ˆ๊ณ , rename์ด ํ•„์š”ํ•œ ์ด์œ ๊ฐ€ ์—†๋Š” ์ƒํƒœ์˜€๋‹ค.

๋ณต๊ตฌ

// ๊ธฐ์กด ํ•„๋“œ๋ช…์œผ๋กœ ๋ณต๊ตฌ
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UAnimSequence* AttackSequence = nullptr;       // SequenceAnimation X

UPROPERTY(EditAnywhere, BlueprintReadWrite)
float HitboxWindowStartTime = 0.f;             // HitboxStartTime X

UPROPERTY(EditAnywhere, BlueprintReadWrite)
float HitboxWindowDuration = 0.3f;             // HitboxDuration X

DropRow ๊ตฌ์กฐ ์ •๋ ฌ ๋ฌธ์ œ

origin/develop์„ ํ™•์ธํ•˜๋Š” ๊ณผ์ •์—์„œ FMonsterDataRow::DropRow ๋‹จ์ˆ˜๊ฐ€ DropRows TArray๋กœ ๋ฐ”๋€Œ์–ด ์žˆ์Œ์„ ๋ฐœ๊ฒฌํ–ˆ๋‹ค. ํ˜„์žฌ ์ž‘์—… ๋ธŒ๋žœ์น˜๋Š” ๋‹จ์ˆ˜ ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•˜๊ณ  ์žˆ์–ด์„œ DropComponent์™€ ํƒ€์ž… ๋ถˆ์ผ์น˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์ด ๋ณ€๊ฒฝ์€ ์—ํ”ฝ ๋ชฌ์Šคํ„ฐ ๊ตฌํ˜„ ๋ฒ”์œ„์™€ ๋ฌด๊ด€ํ•˜๊ณ , ํ•จ๋ถ€๋กœ develop ๊ตฌ์กฐ๋กœ ๋งž์ถ”๋ฉด DropComponent ์ „์ฒด๊ฐ€ ์—ฐ์‡„์ ์œผ๋กœ ๋ฐ”๋€Œ์–ด์•ผ ํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ๋‹ค. ๋”ฐ๋ผ์„œ Drop ์ชฝ์€ ์—ํ”ฝ ์ž‘์—… ์ปค๋ฐ‹์—์„œ ์ œ์™ธํ•˜๊ณ  ๊ธฐ์กด ๋‹จ์ˆ˜ ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค.

// ์—ํ”ฝ ์ปค๋ฐ‹ ๋ฒ”์œ„์—์„œ๋Š” ๊ธฐ์กด ๋‹จ์ˆ˜ ๊ตฌ์กฐ ์œ ์ง€
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FName DropRow;  // DropRows TArray๋Š” ๋ณ„๋„ PR์—์„œ ์ฒ˜๋ฆฌ

๋ฐฐ์šด ์ 

DataTable row ํ•„๋“œ๋ช…์€ C++ struct์™€ .uasset ๋ฐ”์ด๋„ˆ๋ฆฌ๊ฐ€ ๋™์‹œ์— ๋ฐ”๋€Œ์–ด์•ผ ํ•œ๋‹ค. C++๋งŒ renameํ•˜๋ฉด ์—๋””ํ„ฐ์—์„œ ๊ธฐ์กด row ๊ฐ’ ์ „์ฒด๊ฐ€ ์†Œ๋ฉธ๋œ๋‹ค. develop์— ์ด๋ฏธ ์žˆ๋Š” ํ•„๋“œ๋ฅผ "์ƒˆ๋กœ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค"๊ณ  ์ฐฉ๊ฐํ•˜์ง€ ์•Š์œผ๋ ค๋ฉด ๋จธ์ง€/์ปค๋ฐ‹ ์ „์— ํ•ญ์ƒ git show origin/develop:ํ•ด๋‹นํŒŒ์ผ๋กœ ์›๊ฒฉ ๋ฒ„์ „์„ ๋จผ์ € ํ™•์ธํ•ด์•ผ ํ•œ๋‹ค.


5. develop ๋จธ์ง€ ์ถฉ๋Œ ํ•ด๊ฒฐ ์ „๋žต

์ถฉ๋Œ ํŒŒ์ผ ๋ถ„๋ฅ˜

feature/epic-monster-0616์— develop์„ ๋จธ์ง€ํ•˜์ž ์ถฉ๋Œ์ด ๋‘ ์ข…๋ฅ˜๋กœ ๋ฐœ์ƒํ–ˆ๋‹ค.

๋ฐ”์ด๋„ˆ๋ฆฌ ์ถฉ๋Œ (์ค„ ๋‹จ์œ„ ๋ณ‘ํ•ฉ ๋ถˆ๊ฐ€):

  • Content/Retrieve/Data/DT_MonsterData.uasset
  • Content/Retrieve/Data/DT_MonsterPattern.uasset
  • Content/Retrieve/AI/Test/EnemyTestMap.umap

์†Œ์Šค ์ฝ”๋“œ ์ถฉ๋Œ:

  • Source/Retrieve/Data/RetrieveDataTableTypes.h
  • Source/Retrieve/GameplayTags/RetrieveGameplayTags.h/.cpp
  • Source/Retrieve/Character/RetrieveEnemyCharacter.h
  • Source/Retrieve/AbilitySystem/Enemy/GAS/GA_ShootProjectiles.h/.cpp
  • Source/Retrieve/AbilitySystem/Enemy/EnemyProjectile.h/.cpp
  • Source/Retrieve/Components/Enemy/EnemyCombatComponent.cpp

๋ฐ”์ด๋„ˆ๋ฆฌ ์ถฉ๋Œ ์ฒ˜๋ฆฌ ์›์น™

๋ฐ”์ด๋„ˆ๋ฆฌ ์—์…‹์€ ์ค„ ๋‹จ์œ„ ๋ณ‘ํ•ฉ์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค. "ours"๋ฅผ ์„ ํƒํ•˜๋ฉด develop์˜ ์ผ๋ฐ˜/๋ณด์Šค ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์ด ๋‚ ์•„๊ฐ€๊ณ , "theirs"๋ฅผ ์„ ํƒํ•˜๋ฉด ์—ํ”ฝ row๊ฐ€ ์‚ฌ๋ผ์ง„๋‹ค.

์ฑ„ํƒํ•œ ๋ฐฉ์‹: develop ๋ฒ„์ „ ์„ ํƒ โ†’ ์—ํ”ฝ row๋Š” ํ›„์ฒ˜๋ฆฌ๋กœ ์žฌ์ ์šฉ

git checkout --theirs -- "Content/Retrieve/AI/Test/EnemyTestMap.umap"
git checkout --theirs -- "Content/Retrieve/Data/DT_MonsterData.uasset"
git checkout --theirs -- "Content/Retrieve/Data/DT_MonsterPattern.uasset"

์—ํ”ฝ row๋Š” ์ดํ›„ Commandlet ๋˜๋Š” ์—๋””ํ„ฐ์—์„œ ์ˆ˜๋™์œผ๋กœ ์žฌ์ž…๋ ฅํ•œ๋‹ค.

์†Œ์Šค ์ฝ”๋“œ ์ถฉ๋Œ ์ฒ˜๋ฆฌ ์›์น™

๊ฐ ํŒŒ์ผ๋ณ„๋กœ "develop ๊ธฐ์กด ๊ธฐ๋Šฅ ์œ ์ง€ + ์—ํ”ฝ ํ™•์žฅ๋งŒ ์ถ”๊ฐ€"๊ฐ€ ์›์น™์ด๋‹ค.

RetrieveGameplayTags.h/.cpp:
์–‘์ชฝ ํƒœ๊ทธ๋ฅผ ๋ชจ๋‘ ์‚ด๋ฆฐ๋‹ค.

  • develop ์ถ”๊ฐ€๋ถ„: GameplayEvent_Enemy_SpecialAttack_Acceleration, GameplayEvent_Enemy_SpecialAttack_DashAttack
  • ์—ํ”ฝ ์ถ”๊ฐ€๋ถ„: GameplayEvent_Enemy_SpecialAttack_ProjectileSpread

RetrieveEnemyCharacter.h:
์–‘์ชฝ์„ ํ•ฉ์นœ๋‹ค.

  • develop ์ถ”๊ฐ€๋ถ„: RefreshMoveSpeedFromAttribute()
  • ์—ํ”ฝ ์ถ”๊ฐ€๋ถ„: HasAerialPhase(), UsesForwardLocomotion(), UpdateGroundTurnAnimation(), StopGroundTurnAnimation()

GA_ShootProjectiles.h/.cpp:
develop์˜ ๊ณต๊ฒฉ ์†๋„ ๊ด€๋ จ ๋กœ์ง์„ ์œ ์ง€ํ•˜๊ณ  ์—ํ”ฝ ํ™•์žฅ์„ ์–น๋Š”๋‹ค.

// develop์—์„œ ๋“ค์–ด์˜จ ๊ฒƒ โ€” ์œ ์ง€
float PlayRate = GetAttackMontagePlayRate(1.f);
float FireDelay = 0.f;
Combat->GetAttackDelay(PatternRow, FireDelay);

// ์—ํ”ฝ์—์„œ ์ถ”๊ฐ€ํ•œ ๊ฒƒ โ€” ์œ ์ง€
TSubclassOf<AEnemyProjectile> Resolved = PatternRow->ProjectileClass
    ? PatternRow->ProjectileClass : DefaultProjectileClass;

EnemyProjectile.h/.cpp:
develop์˜ ShouldApplyDamageTo()(ํŒ€ ํŒ์ •) + ์—ํ”ฝ์˜ HandleReflectedOverlap(), TryReflectOnHit() ๋‘˜ ๋‹ค ์œ ์ง€.

// ์ถฉ๋Œ ์ฒ˜๋ฆฌ ์ˆœ์„œ โ€” ๋ฐ˜์‚ฌ๊ฐ€ ๋จผ์ €, ํŒ€ ํŒ์ •์ด ๋‚˜์ค‘
void AEnemyProjectile::OnProjectileHit(...)
{
    if (HandleReflectedOverlap(OtherActor, SweepResult)) return;
    if (HasAuthority() && TryReflectOnHit(OtherActor, TargetASC)) return;
    if (HasAuthority() && DamageEffectClass && ShouldApplyDamageTo(OtherActor))
    {
        // ๋ฐ๋ฏธ์ง€ ์ ์šฉ
    }
}

Unity Build ์ด๋ฆ„ ์ถฉ๋Œ ๋ฌธ์ œ

๋จธ์ง€ ์™„๋ฃŒ ํ›„ ๋นŒ๋“œ๋ฅผ ๋Œ๋ฆฌ๋‹ˆ ์•„๋ž˜ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค:

error C2086: 'UAnimInstance *GetEnemyAnimInstance(...)': redefinition

์ต๋ช… namespace ์•ˆ์˜ ํ—ฌํผ ํ•จ์ˆ˜ ์ด๋ฆ„์ด ์—ฌ๋Ÿฌ ํŒŒ์ผ์—์„œ ๋™์ผํ•˜๊ฒŒ ์„ ์–ธ๋˜์–ด ์žˆ์—ˆ๋Š”๋ฐ, Unity Build์—์„œ ๊ฐ™์€ ๋ฒˆ์—ญ ๋‹จ์œ„๋กœ ๋ฌถ์ด๋ฉด์„œ ์ด๋ฆ„์ด ์ถฉ๋Œํ•œ ๊ฒƒ์ด์—ˆ๋‹ค.

ํ•ด๊ฒฐ: ํŒŒ์ผ๋ณ„๋กœ ๊ณ ์œ ํ•œ ์ด๋ฆ„์œผ๋กœ rename

// StateTreeTask_EnemyAerialPhase.cpp
namespace { UAnimInstance* GetEnemyAnimInstanceAerial(ACharacter* Char) { ... } }

// StateTreeTask_ShiftOrbitSlot.cpp
namespace { UAnimInstance* GetEnemyAnimInstanceOrbit(ACharacter* Char) { ... } }

// RetrieveConfigureEpicStateTreeCommandlet.cpp
namespace { const FString EpicCommandletStateTreePath = TEXT("..."); }

๋ฐฐ์šด ์ 

๋ฐ”์ด๋„ˆ๋ฆฌ DataTable ๋จธ์ง€์—์„œ๋Š” ์š•์‹ฌ์„ ๋ฒ„๋ ค์•ผ ํ•œ๋‹ค. "ours + theirs" ๋‘ ๋ฒ„์ „์„ ํ•ฉ์น˜๋ ค ํ•˜๋ฉด ๋ฐ˜๋“œ์‹œ ์—์…‹์ด ๊นจ์ง„๋‹ค. develop ๋ฒ„์ „์„ ๊ธฐ์ค€์œผ๋กœ ๋ฐ›๊ณ  ์—ํ”ฝ row๋งŒ ์žฌ์ฃผ์ž…ํ•˜๋Š” 2๋‹จ๊ณ„ ๋ฐฉ์‹์ด ํ•ญ์ƒ ์•ˆ์ „ํ•˜๋‹ค. Unity Build์—์„œ๋Š” ์ต๋ช… namespace ํ—ฌํผ๋„ ํŒŒ์ผ ๊ฐ„์— ์ด๋ฆ„์ด ๊ฒน์น˜๋ฉด ์ถฉ๋Œํ•œ๋‹ค โ€” ํŒŒ์ผ ์ด๋ฆ„์„ ์ ‘๋ฏธ์‚ฌ๋กœ ๋ถ™์ด๋Š” ์Šต๊ด€์ด ํ•„์š”ํ•˜๋‹ค.


6. ์—ํ”ฝ ๊ธฐ๋ณธ ๊ณต๊ฒฉ ํƒœ๊ทธ ๋ˆ„๋ฝ ๋ฒ„๊ทธ

์ฆ์ƒ

์—ํ”ฝ ๋ชฌ์Šคํ„ฐ๊ฐ€ ๊ธฐ๋ณธ ๊ณต๊ฒฉ์„ ์ „ํ˜€ ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ž„.

์›์ธ ๋ถ„์„

์ผ๋ฐ˜ ๋ชฌ์Šคํ„ฐ์˜ ๊ธฐ๋ณธ ๊ณต๊ฒฉ ํ๋ฆ„:

StateTree โ†’ EnemyAttack task โ†’ GA_EnemyBasicAttack ๋ฐœ๋™ โ†’ UAbilitySystemComponent๊ฐ€ ๋ชฝํƒ€์ฃผ ์žฌ์ƒ
                                                          โ†’ GAS๊ฐ€ ์ž๋™์œผ๋กœ State_Enemy_Attack ํƒœ๊ทธ ๋ถ€์—ฌ

์—ํ”ฝ ๋ชฌ์Šคํ„ฐ์˜ ๊ธฐ๋ณธ ๊ณต๊ฒฉ ํ๋ฆ„ (AttackMontage ์—†์„ ๋•Œ):

StateTree โ†’ EnemyAttack task โ†’ EnemyCombatComponent::RequestPatternByPriority()
                              โ†’ AttackSequence๋กœ ๋™์  ๋ชฝํƒ€์ฃผ ์ƒ์„ฑ ๋ฐ ์žฌ์ƒ
                              โ†’ GAS ๊ฐœ์ž… ์—†์Œ โ†’ State_Enemy_Attack ํƒœ๊ทธ ๋ฏธ๋ถ€์—ฌ

State_Enemy_Attack ํƒœ๊ทธ๊ฐ€ ์—†์œผ๋ฉด:

  • ABP๊ฐ€ "๊ณต๊ฒฉ ์ค‘"์œผ๋กœ ์ธ์‹ํ•˜์ง€ ๋ชปํ•ด ๋กœ์ฝ”๋ชจ์…˜ ๋ชฝํƒ€์ฃผ๋ฅผ ์ •์ƒ์ ์œผ๋กœ ์žฌ์ƒ
  • ๋กœ์ฝ”๋ชจ์…˜ ๋ชฝํƒ€์ฃผ๊ฐ€ ๊ธฐ๋ณธ ๊ณต๊ฒฉ ๋ชฝํƒ€์ฃผ๋ฅผ ๋ฎ์–ด์”€
  • ๊ฒฐ๊ณผ์ ์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ทจ์†Œ๋˜๋ฉด์„œ ๊ณต๊ฒฉ์ด ์•ˆ ๋˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ž„

ํ•ด๊ฒฐ

EnemyCombatComponent.cpp์—์„œ ์‹œํ€€์Šค ๊ธฐ๋ฐ˜ ๊ธฐ๋ณธ ๊ณต๊ฒฉ ๊ฒฝ๋กœ์— ํƒœ๊ทธ ์ˆ˜๋ช… ๊ด€๋ฆฌ๋ฅผ ์ง์ ‘ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

// EnemyCombatComponent.h
private:
    bool bSequenceAttackTagApplied = false;

// EnemyCombatComponent.cpp
bool UEnemyCombatComponent::PlayAttackSequence(UAnimSequence* Sequence)
{
    UAnimMontage* DynamicMontage = CreateDynamicMontageFromSequence(Sequence);
    if (!DynamicMontage) return false;

    // GA๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ˆ˜๋™์œผ๋กœ ๊ณต๊ฒฉ ํƒœ๊ทธ ๋ถ€์—ฌ
    if (AvatarASC && !bSequenceAttackTagApplied)
    {
        AvatarASC->AddLooseGameplayTag(RetrieveGameplayTags::State_Enemy_Attack);
        bSequenceAttackTagApplied = true;
    }

    FOnMontageEnded EndDelegate;
    EndDelegate.BindUObject(this, &UEnemyCombatComponent::OnSequenceAttackMontageEnded);
    AvatarAnimInstance->Montage_Play(DynamicMontage);
    AvatarAnimInstance->Montage_SetEndDelegate(EndDelegate, DynamicMontage);

    return true;
}

void UEnemyCombatComponent::OnSequenceAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
    // ๋‚ด๊ฐ€ ๋ถ™์ธ ํƒœ๊ทธ๋งŒ ์ œ๊ฑฐ (GA๊ฐ€ ๋ถ™์ธ ํƒœ๊ทธ๋Š” ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์Œ)
    if (AvatarASC && bSequenceAttackTagApplied)
    {
        AvatarASC->RemoveLooseGameplayTag(RetrieveGameplayTags::State_Enemy_Attack);
        bSequenceAttackTagApplied = false;
    }
}

bSequenceAttackTagApplied ํ”Œ๋ž˜๊ทธ๋ฅผ ์‚ฌ์šฉํ•ด์„œ, "๋‚ด๊ฐ€ ๋ถ™์ธ ํƒœ๊ทธ"์™€ "GA๊ฐ€ ๋ถ™์ธ ํƒœ๊ทธ"๋ฅผ ๊ตฌ๋ถ„ํ•œ๋‹ค. ์‹œํ€€์Šค ๊ณต๊ฒฉ ๋„์ค‘ GA ๊ธฐ๋ฐ˜ ๊ณต๊ฒฉ์ด ๋ฐœ๋™๋˜์–ด ํƒœ๊ทธ๊ฐ€ ์ด๋ฏธ ์žˆ๋”๋ผ๋„ ์ž˜๋ชป ์ œ๊ฑฐํ•˜์ง€ ์•Š๋„๋ก ํ•œ๋‹ค.

๋ชฝํƒ€์ฃผ ์žฌ์ƒ ์ž์ฒด๊ฐ€ ์‹คํŒจํ•  ๊ฒฝ์šฐ์—๋„ ํƒœ๊ทธ๊ฐ€ ๋‚จ์ง€ ์•Š๋„๋ก:

bool UEnemyCombatComponent::PlayAttackSequence(UAnimSequence* Sequence)
{
    // ...
    bool bPlayed = (AvatarAnimInstance->Montage_Play(DynamicMontage) > 0.f);
    if (!bPlayed)
    {
        // ์žฌ์ƒ ์‹คํŒจ โ€” ๋ถ™์ธ ํƒœ๊ทธ ์ฆ‰์‹œ ํšŒ์ˆ˜
        if (bSequenceAttackTagApplied)
        {
            AvatarASC->RemoveLooseGameplayTag(RetrieveGameplayTags::State_Enemy_Attack);
            bSequenceAttackTagApplied = false;
        }
        return false;
    }
    return true;
}

๋ฐฐ์šด ์ 

UE GAS์—์„œ ํƒœ๊ทธ ๋ถ€์—ฌยทํ•ด์ œ๋Š” GA(GameplayAbility)๋ฅผ ํ†ตํ•  ๋•Œ๋งŒ ์ž๋™์œผ๋กœ ๋ณด์žฅ๋œ๋‹ค. GA๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ์ง์ ‘ ๋ชฝํƒ€์ฃผ๋ฅผ ์žฌ์ƒํ•˜๊ฑฐ๋‚˜ ๋กœ์ง์„ ์‹คํ–‰ํ•˜๋Š” ๊ฒฝ๋กœ์—์„œ๋Š” ๊ด€๋ จ ํƒœ๊ทธ๋„ ์ง์ ‘ ๊ด€๋ฆฌํ•ด์•ผ ํ•œ๋‹ค. "๋‚ด๊ฐ€ ๋ถ™์ธ ํƒœ๊ทธ"์™€ "๋‚จ์ด ๋ถ™์ธ ํƒœ๊ทธ"๋Š” ๋ฐ˜๋“œ์‹œ ๊ตฌ๋ถ„ ํ”Œ๋ž˜๊ทธ๋กœ ์ถ”์ ํ•ด์•ผ ์ž˜๋ชป๋œ ํ•ด์ œ๋ฅผ ๋ง‰์„ ์ˆ˜ ์žˆ๋‹ค.


7. ๊ทธ๋กœ๊ธฐ ์‹œ์Šคํ…œ 3์ข… ๋ฒ„๊ทธ ์ˆ˜์ •

๋ฒ„๊ทธ 1 โ€” EpicMonsterGroggyComponent ๋ฏธ๋ถ€์ฐฉ ๋ฌธ์ œ

์ฆ์ƒ

๊ฐ•๊ณต๊ฒฉ 3ํšŒ๋ฅผ ๋งžํ˜€๋„ ๊ทธ๋กœ๊ธฐ๊ฐ€ ๊ฑธ๋ฆฌ์ง€ ์•Š๊ณ , ๊ทธ๋กœ๊ธฐ ๊ฒŒ์ด์ง€ UI๋„ ์ „ํ˜€ ๋ณด์ด์ง€ ์•Š์Œ.

์›์ธ

ARetrieveEpicMonsterCharacter C++ ์ƒ์„ฑ์ž์—์„œ EpicMonsterGroggyComponent๋ฅผ CreateDefaultSubobject๋กœ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ  ์žˆ์—ˆ๋‹ค. ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ๋ธ”๋ฃจํ”„๋ฆฐํŠธ(BP_Dragon, BP_Magma, BP_Treant)์—์„œ ์ˆ˜๋™์œผ๋กœ ๋ถ™์ด๋Š” ๊ตฌ์กฐ์˜€๋Š”๋ฐ, ๋จธ์ง€ ๊ณผ์ •์—์„œ BP ์—์…‹์ด develop ๋ฒ„์ „์œผ๋กœ ์„ ํƒ๋˜๋ฉด์„œ ์ปดํฌ๋„ŒํŠธ ์„ค์ •์ด ๋น ์กŒ๋‹ค.

ํ•ด๊ฒฐ

// RetrieveEpicMonsterCharacter.cpp ์ƒ์„ฑ์ž
ARetrieveEpicMonsterCharacter::ARetrieveEpicMonsterCharacter()
{
    // ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ๋“ค ...

    // C++ ๊ธฐ๋ณธ ์ปดํฌ๋„ŒํŠธ๋กœ ๋“ฑ๋ก โ€” BP์— ์˜์กดํ•˜์ง€ ์•Š์Œ
    EpicGroggyComponent = CreateDefaultSubobject<UEpicMonsterGroggyComponent>(TEXT("EpicGroggyComponent"));
}

BP์—์„œ ๋ถ™์ด๋Š” ๋ฐฉ์‹์€ ํ•ด๋‹น BP ์—์…‹์ด ์—†๊ฑฐ๋‚˜ ๋ฎ์–ด์จ์ง€๋ฉด ๋ฐ”๋กœ ๋™์ž‘์ด ๊นจ์ง„๋‹ค. C++์—์„œ CreateDefaultSubobject๋กœ ๋“ฑ๋กํ•˜๋ฉด ์–ด๋–ค BP๋ผ๋„ ์ƒ์†๋ฐ›๋Š” ์ˆœ๊ฐ„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ณด์žฅ๋œ๋‹ค.


๋ฒ„๊ทธ 2 โ€” ๊ทธ๋กœ๊ธฐ ํƒœ๊ทธ ๋ฏธ์ ์šฉ ๋ฌธ์ œ

์ฆ์ƒ

๊ฒŒ์ด์ง€๊ฐ€ ๊ฝ‰ ์ฐจ๋„ ๋ชฌ์Šคํ„ฐ๊ฐ€ ๊ทธ๋กœ๊ธฐ ์ƒํƒœ๋กœ ์ „ํ™˜๋˜์ง€ ์•Š์Œ.

์›์ธ

EpicMonsterGroggyComponent๊ฐ€ ๊ฒŒ์ด์ง€๊ฐ€ ์ฐจ๋ฉด GameplayEvent_GroggyTrigger ์ด๋ฒคํŠธ๋งŒ ๋ณด๋‚ด๊ณ  ์žˆ์—ˆ๋Š”๋ฐ, ์ด ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•ด์„œ ์‹ค์ œ๋กœ State_Enemy_Groggy ํƒœ๊ทธ๋ฅผ ๋ถ™์ด๋Š” GA/GE๊ฐ€ ์–ด๋””์—๋„ ์—†์—ˆ๋‹ค.

// ๊ธฐ์กด ์ฝ”๋“œ โ€” ์ด๋ฒคํŠธ๋งŒ ๋ณด๋‚ด๊ณ  ๋
void UEpicMonsterGroggyComponent::OnGaugeMaxReached()
{
    FGameplayEventData Payload;
    UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(
        GetOwner(), RetrieveGameplayTags::GameplayEvent_GroggyTrigger, Payload);
    // ์ด ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›๋Š” GA๊ฐ€ ์—†์—ˆ์Œ
}

ํ•ด๊ฒฐ

์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ง์ ‘ State_Enemy_Groggy loose tag๋ฅผ ๋ถ€์—ฌํ•˜๊ณ , duration ๋™์•ˆ Tick์œผ๋กœ ๋‚จ์€ ์‹œ๊ฐ„์„ UI์— ์ „๋‹ฌํ•œ ๋’ค ์ œ๊ฑฐํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ๋‹ค.

// EpicMonsterGroggyComponent.h
private:
    float GroggyDuration = 3.f;        // ๊ธฐ๋ณธ ๊ทธ๋กœ๊ธฐ ์ง€์†์‹œ๊ฐ„
    float ActiveGroggyDuration = 0.f;  // ํ˜„์žฌ ๊ทธ๋กœ๊ธฐ์˜ ์‹ค์ œ ์ง€์†์‹œ๊ฐ„ (์™ธ๋ถ€์—์„œ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ)
    float GroggyEndTime = 0.f;
    bool bGroggyActive = false;

public:
    void ApplyGroggyState(float OverrideDuration = -1.f);

// EpicMonsterGroggyComponent.cpp
void UEpicMonsterGroggyComponent::ApplyGroggyState(float OverrideDuration)
{
    if (bGroggyActive) return;

    // ์™ธ๋ถ€(๋ฐ˜์‚ฌ ํˆฌ์‚ฌ์ฒด ๋“ฑ)์—์„œ duration์„ ์ง€์ •ํ•œ ๊ฒฝ์šฐ์™€ ๊ธฐ๋ณธ๊ฐ’์„ ๊ตฌ๋ถ„
    ActiveGroggyDuration = (OverrideDuration > 0.f) ? OverrideDuration : GroggyDuration;
    GroggyEndTime = GetWorld()->GetTimeSeconds() + ActiveGroggyDuration;
    bGroggyActive = true;

    // GA/GE ์—†์ด ์ง์ ‘ ํƒœ๊ทธ ๋ถ€์—ฌ
    if (AvatarASC)
    {
        AvatarASC->AddLooseGameplayTag(RetrieveGameplayTags::State_Enemy_Groggy);
    }

    SetComponentTickEnabled(true);  // UI ๊ฐฑ์‹  Tick ํ™œ์„ฑํ™”
    GroggyCounter = 0;              // ๊ฒŒ์ด์ง€ ์ดˆ๊ธฐํ™”
}

void UEpicMonsterGroggyComponent::TickComponent(float DeltaTime, ...)
{
    if (!bGroggyActive) return;

    float Remaining = GroggyEndTime - GetWorld()->GetTimeSeconds();
    if (Remaining <= 0.f)
    {
        // ๊ทธ๋กœ๊ธฐ ์ข…๋ฃŒ
        if (AvatarASC)
        {
            AvatarASC->RemoveLooseGameplayTag(RetrieveGameplayTags::State_Enemy_Groggy);
        }
        bGroggyActive = false;
        SetComponentTickEnabled(false);
        OnGroggyEnded.Broadcast();
        return;
    }

    // UI์— ๋‚จ์€ ์‹œ๊ฐ„ ๋น„์œจ ์ „๋‹ฌ (0.0 ~ 1.0)
    float Ratio = Remaining / ActiveGroggyDuration;
    OnGroggyGaugeUpdated.Broadcast(Ratio);
}

ActiveGroggyDuration์„ ๋ณ„๋„ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌํ•˜๋Š” ์ด์œ : ApplyGroggyState(5.f)์ฒ˜๋Ÿผ ์™ธ๋ถ€์—์„œ override duration์„ ๋„ฃ์„ ๋•Œ ์ปดํฌ๋„ŒํŠธ์˜ ๊ธฐ๋ณธ GroggyDuration ์„ค์ •๊ฐ’์ด ๋ฎ์–ด์จ์ง€๋Š” ๊ฒƒ์„ ๋ง‰๊ธฐ ์œ„ํ•ด์„œ๋‹ค.


๋ฒ„๊ทธ 3 โ€” ๋“œ๋ž˜๊ณค ๋ฐ˜์‚ฌ ํˆฌ์‚ฌ์ฒด ๊ทธ๋กœ๊ธฐ ๋ฏธ๋ฐœ๋™

์ฆ์ƒ

๋“œ๋ž˜๊ณค์˜ ํˆฌ์‚ฌ์ฒด๋ฅผ ํŒจ๋ง์œผ๋กœ ๋ฐ˜์‚ฌ์‹œ์ผœ ๋“œ๋ž˜๊ณค์—๊ฒŒ ๋งžํ˜€๋„ ๊ทธ๋กœ๊ธฐ๊ฐ€ ๊ฑธ๋ฆฌ์ง€ ์•Š์Œ.

์›์ธ

EpicEnemyProjectile.cpp์˜ ๋ฐ˜์‚ฌ ์ ์ค‘ ์ฒ˜๋ฆฌ๊ฐ€ PatternCounterComponent์˜ ํ˜„์žฌ ํŒจํ„ด ๋ฐ์ดํ„ฐ์— ์˜์กดํ•˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ, ๋ฐ˜์‚ฌ๋œ ํˆฌ์‚ฌ์ฒด๊ฐ€ ๋“œ๋ž˜๊ณค ์ž์‹ ์—๊ฒŒ ๋งž์„ ๋•Œ PatternCounterComponent์— ํ•ด๋‹น ํŒจํ„ด ์ •๋ณด๊ฐ€ ์—†๋Š” ์ƒํƒœ์˜€๋‹ค.

ํ•ด๊ฒฐ

PatternCounterComponent ๋ฐ์ดํ„ฐ์— ์˜์กดํ•˜์ง€ ์•Š๊ณ  EpicMonsterGroggyComponent๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋„๋ก ๋ณ€๊ฒฝ.

// EpicEnemyProjectile.cpp
void AEpicEnemyProjectile::HandleReflectedHit(AActor* HitTarget)
{
    // ๋ฐ˜์‚ฌ ํˆฌ์‚ฌ์ฒด๊ฐ€ ์—ํ”ฝ ๋ชฌ์Šคํ„ฐ์— ์ ์ค‘
    ARetrieveEpicMonsterCharacter* EpicMonster = Cast<ARetrieveEpicMonsterCharacter>(HitTarget);
    if (!EpicMonster) return;

    UEpicMonsterGroggyComponent* GroggyComp =
        EpicMonster->FindComponentByClass<UEpicMonsterGroggyComponent>();
    if (GroggyComp)
    {
        // ๋ฐ˜์‚ฌ ์ ์ค‘์€ ์ฆ‰์‹œ ๊ทธ๋กœ๊ธฐ, 5์ดˆ ์ง€์†
        GroggyComp->ApplyGroggyState(5.f);
    }
}

FindComponentByClass๋ฅผ ์“ฐ๋Š” ์ด์œ : C++ ์ƒ์„ฑ์ž์—์„œ CreateDefaultSubobject๋กœ ๋“ฑ๋กํ–ˆ์œผ๋ฏ€๋กœ ํ•ญ์ƒ ์กด์žฌ๊ฐ€ ๋ณด์žฅ๋˜์ง€๋งŒ, ๋ฐฉ์–ด์  ์ฝ”๋“œ๋กœ null ์ฒดํฌ๋ฅผ ์œ ์ง€ํ•œ๋‹ค.

๋ฐฐ์šด ์ 

C++ CreateDefaultSubobject๋Š” "์ด ์ปดํฌ๋„ŒํŠธ๋Š” ์ด ํด๋ž˜์Šค์˜ ๋ถˆ๊ฐ€๋ถ„ํ•œ ์ผ๋ถ€"๋ผ๋Š” ์„ ์–ธ์ด๋‹ค. BP์—์„œ๋งŒ ๋ถ™์ด๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” ์—์…‹ ์ƒํƒœ์— ๋”ฐ๋ผ ์—†์„ ์ˆ˜๋„ ์žˆ๋‹ค. ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฐ˜๋“œ์‹œ C++ ์ƒ์„ฑ์ž์—์„œ ๋“ฑ๋กํ•ด์•ผ ํ•œ๋‹ค. ๊ทธ๋กœ๊ธฐ์ฒ˜๋Ÿผ "์กฐ๊ฑด ๋งŒ์กฑ โ†’ ์ฆ‰์‹œ ๋ฐœ๋™"์ด ํ•„์š”ํ•œ ๋ฉ”์ปค๋‹ˆ์ฆ˜์€ GA/GE ์ฒด์ธ ์—†์ด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ง์ ‘ ํƒœ๊ทธ๋ฅผ ๋ถ™์ด๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ๊ฐ„๋‹จํ•˜๊ณ  ์‹ ๋ขฐ์„ฑ์ด ๋†’๋‹ค.


8. Treant ํŠน์ˆ˜ ์Šคํ‚ฌ ํˆฌ์‚ฌ์ฒด/VFX ์˜ค๋ฅ˜ ์ˆ˜์ •

์ฆ์ƒ

  • Treant์˜ ํŠน์ˆ˜ ์Šคํ‚ฌ(Treant_FloodBarrage)์„ ์‚ฌ์šฉํ•˜๋ฉด ์˜๋„์™€ ์ „ํ˜€ ๋‹ค๋ฅธ VFX๊ฐ€ ์žฌ์ƒ๋จ.
  • BP_TreantWaterPillarProjectile์ด ์žˆ์Œ์—๋„ ๊ณต์šฉ BP_WaterProjectile์ด ๋ฐœ์‚ฌ๋จ.

์›์ธ

๋‘ ๋‹จ๊ณ„์˜ ๋ฌธ์ œ๊ฐ€ ๊ฒน์ณ ์žˆ์—ˆ๋‹ค.

๋ฌธ์ œ 1 โ€” Commandlet์ด Treant row๋ฅผ ๊ณต์šฉ ํˆฌ์‚ฌ์ฒด๋กœ ์„ค์ •

RetrieveConfigureEpicMonstersCommandlet.cpp์—์„œ Magma์™€ Treant ๋ชจ๋‘ ๊ฐ™์€ ํˆฌ์‚ฌ์ฒด ๊ฒฝ๋กœ๋ฅผ ์“ฐ๊ณ  ์žˆ์—ˆ๋‹ค:

// ์ž˜๋ชป๋œ ์ฝ”๋“œ
static const FString WaterProjectileBlueprintPath =
    TEXT("/Game/Retrieve/AI/Boss/Projectiles/BP_WaterProjectile.BP_WaterProjectile_C");

// Treant ์„ค์ • ์‹œ
PatternRow.ProjectileClass = LoadClass<AEnemyProjectile>(nullptr, *WaterProjectileBlueprintPath);

๋ฌธ์ œ 2 โ€” Commandlet์ด ํˆฌ์‚ฌ์ฒด VFX๋ฅผ ์™ธ๋ถ€ ์—์…‹์œผ๋กœ ๋ฎ์–ด์”€

Commandlet์ด BP_TreantWaterPillarProjectile์˜ FlightVFX/ImpactVFX๋ฅผ ์„ค์ •ํ•˜๋Š” ๊ณผ์ •์—์„œ ์™ธ๋ถ€ ํŒจํ‚ค์ง€์˜ Waterfall Niagara ์‹œ์Šคํ…œ์œผ๋กœ ๋ฎ์–ด์“ฐ๊ณ  ์žˆ์—ˆ๋‹ค.

ํ•ด๊ฒฐ

Commandlet์—์„œ Treant ์ „์šฉ ํˆฌ์‚ฌ์ฒด์™€ VFX๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋ถ„๋ฆฌํ–ˆ๋‹ค:

// ์ˆ˜์ • ํ›„
static const FString TreantWaterPillarProjectilePath =
    TEXT("/Game/Retrieve/AI/Boss/Projectiles/BP_TreantWaterPillarProjectile.BP_TreantWaterPillarProjectile_C");

static const FString TreantWaterPillarVFXPath =
    TEXT("/Game/Retrieve/AI/Boss/Projectiles/VFX/NS_Hanna_WaterPillar.NS_Hanna_WaterPillar");

// Treant FloodBarrage ํŒจํ„ด ์„ค์ • ์‹œ
PatternRow.ProjectileClass = LoadClass<AEnemyProjectile>(nullptr, *TreantWaterPillarProjectilePath);
PatternRow.SpawnPattern = EProjectileSpawnPattern::GroundPillar;

// ํˆฌ์‚ฌ์ฒด VFX ์„ค์ • โ€” Treant ์ „์šฉ ์œ ์ง€
ConfigureProjectileBlueprintVFX(
    TreantWaterPillarProjectilePath,
    TreantWaterPillarVFXPath,   // FlightVFX
    TreantWaterPillarVFXPath    // ImpactVFX
);

Commandlet ์ˆ˜์ • ํ›„ ์‹ค์ œ ์—์…‹์— ๋ฐ˜์˜ํ•˜๊ธฐ ์œ„ํ•ด ์‹คํ–‰:

attrib -R "Content\Retrieve\AI\Boss\Projectiles\BP_TreantWaterPillarProjectile.uasset"
attrib -R "Content\Retrieve\Data\DT_MonsterPattern.uasset"
& 'C:\Program Files\Epic Games\UE_5.7\Engine\Binaries\Win64\UnrealEditor-Cmd.exe' `
  'Retrieve.uproject' -run=RetrieveConfigureEpicMonsters -unattended -nop4

Commandlet ์‹คํ–‰ ๋กœ๊ทธ์—์„œ VFX๊ฐ€ ์˜๋„ํ•œ ๊ฒฝ๋กœ๋กœ ์ €์žฅ๋œ ๊ฒƒ ํ™•์ธ:

[ConfigureEpicMonsters] BP_TreantWaterPillarProjectile: FlightVFX = NS_Hanna_WaterPillar
[ConfigureEpicMonsters] BP_TreantWaterPillarProjectile: ImpactVFX = NS_Hanna_WaterPillar

Commandlet ์ตœ์ข… ์‚ญ์ œ

์ดํ›„์—๋„ Commandlet์„ ๋‹ค์‹œ ์‹คํ–‰ํ•˜๋ฉด ์ˆ˜๋™์œผ๋กœ ์—๋””ํ„ฐ์—์„œ ์กฐ์ •ํ•œ ์„ค์ •์„ ์›๋ณต์‹œํ‚ค๋Š” ๋ฌธ์ œ๊ฐ€ ๊ณ„์† ๋ฐœ์ƒํ–ˆ๋‹ค. ์—ํ”ฝ ๋ชฌ์Šคํ„ฐ ์ดˆ๊ธฐ ์„ค์ • ์ž๋™ํ™” ๋ชฉ์ ์€ ์ด๋ฏธ ๋‹ฌ์„ฑ๋œ ์ƒํƒœ์˜€๊ธฐ ๋•Œ๋ฌธ์— Commandlet 4๊ฐœ ํŒŒ์ผ์„ ์‚ญ์ œํ–ˆ๋‹ค:

  • Source/Retrieve/Commandlets/RetrieveConfigureEpicMonstersCommandlet.cpp
  • Source/Retrieve/Commandlets/RetrieveConfigureEpicMonstersCommandlet.h
  • Source/Retrieve/Commandlets/RetrieveConfigureEpicStateTreeCommandlet.cpp
  • Source/Retrieve/Commandlets/RetrieveConfigureEpicStateTreeCommandlet.h

๋‚จ์€ Commandlets ํด๋”์˜ BonfireCommandlet, InteractionCommandlet์€ ๊ธฐ์กด ํŒŒํŠธ ํŒŒ์ผ์ด๋ผ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š์•˜๋‹ค.

๋ฐฐ์šด ์ 

Commandlet์€ "ํ•œ ๋ฒˆ ์‹คํ–‰ํ•˜๊ณ  ์—์…‹์— ๊ตฌ์›Œ์ง„ ๋’ค์—๋Š” ํ•„์š” ์—†์–ด์ง€๋Š”" ์ดˆ๊ธฐํ™” ๋„๊ตฌ๋‹ค. ํ•˜์ง€๋งŒ ์ฝ”๋“œ๋กœ ๋‚จ์•„ ์žˆ์œผ๋ฉด ๋ˆ„๊ตฌ๋“  ์‹ค์ˆ˜๋กœ ๋‹ค์‹œ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ณ , ๊ทธ ์ˆœ๊ฐ„ ์ˆ˜๋™์œผ๋กœ ์กฐ์ •ํ•œ ์—์…‹ ์„ค์ •์ด ์ „๋ถ€ ๋‚ ์•„๊ฐ„๋‹ค. ์ดˆ๊ธฐ ์ž๋™ํ™”๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ๋ฐ˜๋“œ์‹œ ์‚ญ์ œํ•˜๊ฑฐ๋‚˜ ์‹คํ–‰ ์กฐ๊ฑด์— ์•ˆ์ „ ์ฒดํฌ(์˜ˆ: "์ด๋ฏธ ์„ค์ •๋œ ๊ฒฝ์šฐ ์Šคํ‚ต")๋ฅผ ๋„ฃ์–ด์•ผ ํ•œ๋‹ค.


9. HP Bar ๋™๊ธฐํ™” ๋ฌธ์ œ ์ˆ˜์ •

์ฆ์ƒ

์‹ค์ œ ์ฒด๋ ฅ๊ณผ HP Bar๊ฐ€ ์—ฐ๋™์ด ์•ˆ ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๊ฐ„ํ—์ ์œผ๋กœ ๋ฐœ์ƒ.

์›์ธ

NormalMonsterHealthBarComponent๊ฐ€ BeginPlay์—์„œ ๋‹จ ํ•œ ๋ฒˆ๋งŒ ๋ฐ”์ธ๋”ฉ์„ ์‹œ๋„ํ–ˆ๋‹ค. ์—ํ”ฝ ๋ชฌ์Šคํ„ฐ์ฒ˜๋Ÿผ AbilitySystemComponent ์ดˆ๊ธฐํ™”๊ฐ€ BeginPlay ์ดํ›„์— ์™„๋ฃŒ๋˜๋Š” ๊ฒฝ์šฐ, ์ฒซ ๋ฒˆ์งธ ๋ฐ”์ธ๋”ฉ ์‹œ๋„ ๋•Œ ASC๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•„ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ์—ฐ๊ฒฐ์ด ์‹คํŒจํ•œ๋‹ค. ์ดํ›„์— ASC๊ฐ€ ์ค€๋น„๋˜์–ด๋„ ์žฌ๋ฐ”์ธ๋”ฉ ์ฝ”๋“œ๊ฐ€ ์—†์–ด์„œ ์ฒด๋ ฅ์ด ์˜์˜ ์—ฐ๋™๋˜์ง€ ์•Š๋Š”๋‹ค.

// ๊ธฐ์กด ์ฝ”๋“œ โ€” ํ•œ ๋ฒˆ๋งŒ ์‹œ๋„
void UNormalMonsterHealthBarComponent::BeginPlay()
{
    Super::BeginPlay();
    InitializeWithASC();  // ASC๊ฐ€ ์—†์œผ๋ฉด ๊ทธ๋ƒฅ ์‹คํŒจํ•˜๊ณ  ๋
}

ํ•ด๊ฒฐ

๋ฐ”์ธ๋”ฉ ์„ฑ๊ณต ์—ฌ๋ถ€๋ฅผ ์ถ”์ ํ•˜๊ณ , ์‹คํŒจํ•œ ๊ฒฝ์šฐ Tick์—์„œ ๊ณ„์† ์žฌ์‹œ๋„ํ•œ๋‹ค. ์„ฑ๊ณตํ•˜๋ฉด Tick์„ ๋น„ํ™œ์„ฑํ™”ํ•ด ๋ถˆํ•„์š”ํ•œ ์—ฐ์‚ฐ์„ ์ค„์ธ๋‹ค.

// NormalMonsterHealthBarComponent.h
private:
    bool bHealthBound = false;

// NormalMonsterHealthBarComponent.cpp
void UNormalMonsterHealthBarComponent::BeginPlay()
{
    Super::BeginPlay();
    SetComponentTickEnabled(true);  // ๋ฐ”์ธ๋”ฉ ์„ฑ๊ณต๊นŒ์ง€ Tick ์œ ์ง€
    TryBindToHealthComponent();
}

void UNormalMonsterHealthBarComponent::TickComponent(float DeltaTime, ...)
{
    if (bHealthBound) return;  // ์ด๋ฏธ ๋ฐ”์ธ๋”ฉ๋์œผ๋ฉด ์ฆ‰์‹œ ๋ฆฌํ„ด
    TryBindToHealthComponent();
}

void UNormalMonsterHealthBarComponent::TryBindToHealthComponent()
{
    URetrieveHealthComponent* HealthComp =
        GetOwner()->FindComponentByClass<URetrieveHealthComponent>();
    if (!HealthComp) return;

    UAbilitySystemComponent* ASC = HealthComp->GetAbilitySystemComponent();
    if (!ASC) return;

    // ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฐ”์ธ๋”ฉ
    ASC->GetGameplayAttributeValueChangeDelegate(
        URetrieveHealthAttributeSet::GetHealthAttribute())
        .AddUObject(this, &UNormalMonsterHealthBarComponent::OnHealthChanged);

    // ์ดˆ๊ธฐ๊ฐ’ ๋™๊ธฐํ™”
    float CurrentHP = HealthComp->GetHealth();
    float MaxHP = HealthComp->GetMaxHealth();
    RefreshHealthPercent(CurrentHP, MaxHP);

    bHealthBound = true;
    SetComponentTickEnabled(false);  // ๋ฐ”์ธ๋”ฉ ์„ฑ๊ณต โ€” Tick ๋น„ํ™œ์„ฑํ™”
}

๋ฐฐ์šด ์ 

UE์—์„œ ์ปดํฌ๋„ŒํŠธ ์ดˆ๊ธฐํ™” ์ˆœ์„œ๋Š” BeginPlay ์‹œ์ ์—๋„ ๋ณด์žฅ๋˜์ง€ ์•Š๋Š”๋‹ค. ํŠนํžˆ GAS์˜ AbilitySystemComponent๋Š” BeginPlay ์ดํ›„์— ์„ค์ •๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ํ”ํ•˜๋‹ค. ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฐ”์ธ๋”ฉ์„ "BeginPlay 1ํšŒ ์‹œ๋„"๋กœ๋งŒ ๊ตฌํ˜„ํ•˜๋ฉด ํ•ญ์ƒ ํƒ€์ด๋ฐ ์ด์Šˆ๊ฐ€ ์ƒ๊ธด๋‹ค. "์„ฑ๊ณตํ•  ๋•Œ๊นŒ์ง€ ๋งค ํ‹ฑ ์žฌ์‹œ๋„ โ†’ ์„ฑ๊ณต ์‹œ Tick ๋น„ํ™œ์„ฑํ™”" ํŒจํ„ด์ด ์•ˆ์ •์ ์ด๋‹ค. Tick ๋น„์šฉ์€ ํ”„๋ ˆ์ž„๋‹น bool ์ฒดํฌ ํ•˜๋‚˜๋ผ ๋ฌด์‹œ ๊ฐ€๋Šฅํ•˜๋‹ค.


10. WBP_MonsterHealthBar ๋ ˆ์ด์•„์›ƒ ์žฌ์ •๋ ฌ

๋ชฉํ‘œ

  • ๊ทธ๋กœ๊ธฐ ๊ฒŒ์ด์ง€๊ฐ€ ํ™”๋ฉด์— ์ž˜ ๋ณด์ด๋„๋ก ์œ„์ ฏ ๊ฐ€์‹œ์„ฑ ์„ค์ •
  • ์—ํ”ฝ ๋ชฌ์Šคํ„ฐ์ผ ๋•Œ HP๋ฐ”/๊ทธ๋กœ๊ธฐ ์ง„ํ–‰๋ฐ”๊ฐ€ ํ”„๋ ˆ์ž„ ์•ˆ์— ํฌํ•จ๋˜๋Š” ๋ ˆ์ด์•„์›ƒ

์ ‘๊ทผ ๋ฐฉ์‹

๊ธฐ์กด Monolith ์—๋””ํ„ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํŒจํ„ด(RetrieveUIVFXEditorUtility)์— ์œ„์ ฏ ๋ ˆ์ด์•„์›ƒ ์žฌ๊ตฌ์„ฑ ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ  Python ์Šคํฌ๋ฆฝํŠธ๋กœ ์‹คํ–‰ํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ–ˆ๋‹ค.

// RetrieveUIVFXEditorUtility.h
UFUNCTION(BlueprintCallable, CallInEditor)
static bool ConfigureMonsterHealthBarLayoutAsset();
# Saved/ConfigureMonsterHealthBarLayout.py
import unreal
result = unreal.RetrieveUIVFXEditorUtility.configure_monster_health_bar_layout()
unreal.EditorAssetLibrary.save_asset('/Game/Retrieve/UI/WBP_MonsterHealthBar', False)

์ค‘๊ฐ„์— ๋ฐœ์ƒํ•œ ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ ๊ณผ์ •

๋ฌธ์ œ 1 โ€” ResetWidgetTree ํ›„ ์ด๋ฆ„ ์ถฉ๋Œ

ResetWidgetTree() ํ›„ ๊ฐ™์€ ์ด๋ฆ„์œผ๋กœ ์œ„์ ฏ์„ ์žฌ์ƒ์„ฑํ•˜๋ฉด ํŒจํ‚ค์ง€ ์˜ค๋ธŒ์ ํŠธ ์ด๋ฆ„์ด UPackage์— ๋‚จ์•„ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ๋‹ค.

Cannot replace object 'FRA_Vignette' โ€” different class

์™ธ๋ถ€ ํŒจํ‚ค์ง€์—์„œ ์ž„ํฌํŠธ๋œ FRA_Vignette(Monolith ํ”„๋ ˆ์ž„ ์œ„์ ฏ)๋Š” ์ปค์Šคํ…€ ํด๋ž˜์Šค๋ผ์„œ, ๊ฐ™์€ ์ด๋ฆ„์œผ๋กœ Border ํƒ€์ž…์˜ ์ƒˆ ์˜ค๋ธŒ์ ํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์—†์—ˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉํ–ฅ: ๊ธฐ์กด ํŠธ๋ฆฌ๋ฅผ ํ†ต์งธ๋กœ ๋น„์šฐ์ง€ ์•Š๊ณ  ๊ธฐ์กด ์œ„์ ฏ์„ ์žฌ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ํ๊ธฐ ์ด๋ฆ„์œผ๋กœ renameํ•œ ๋’ค ์ƒˆ ๊ตฌ์กฐ๋ฅผ ์žฌ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ „ํ™˜.

๋ฌธ์ œ 2 โ€” stale GUID ์ฐธ์กฐ๋กœ ์ธํ•œ Blueprint Compile ensure

๊ธฐ์กด ์œ„์ ฏ์„ ์œ ์ง€ํ•œ ์ฑ„ ์žฌ๋ฐฐ์น˜ํ•˜๋Š” ๋ฐฉ์‹์—์„œ ์™ธ๋ถ€ ํ”„๋ ˆ์ž„ ์œ„์ ฏ์˜ stale GUID ์ฐธ์กฐ๊ฐ€ Blueprint Compile ๋‹จ๊ณ„์—์„œ ensure๋ฅผ ๋ฐœ์ƒ์‹œ์ผฐ๋‹ค. ๊ฐ•์ œ Compile ํ˜ธ์ถœ์„ ์ œ๊ฑฐํ•˜๊ณ  ์ €์žฅ๋งŒ ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ „ํ™˜ํ–ˆ๋”๋‹ˆ ์ €์žฅ ์ž์ฒด๋Š” ์„ฑ๊ณตํ–ˆ๋‹ค(Configure result: True, Save result: True).

์ตœ์ข…์ ์œผ๋กœ ๊ธฐ์กด ์œ„์ ฏ์„ ํ๊ธฐ ์ด๋ฆ„์œผ๋กœ rename ํ›„ ์ƒˆ ๊ตฌ์กฐ๋กœ ์žฌ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ stale GUID ๋ฌธ์ œ๊นŒ์ง€ ํ•ด์†Œํ–ˆ๋‹ค.

์ตœ์ข… ์œ„์ ฏ ๊ตฌ์กฐ

WBP_MonsterHealthBar (UUserWidget)
โ””โ”€โ”€ CanvasPanel (Root)
    โ”œโ”€โ”€ FRA_Frame  (์™ธ๋ถ€ ํ”„๋ ˆ์ž„ โ€” T_FantasyWarrior_Bar_Horizontal08 ํ…์Šค์ฒ˜ ์ ์šฉ)
    โ”‚   โ””โ”€โ”€ FRA_Vignette
    โ”œโ”€โ”€ HPBar (UProgressBar)            โ† ์ฒด๋ ฅ ๋ฐ”์ธ๋”ฉ
    โ”œโ”€โ”€ GroggyProgressBar (UProgressBar) โ† ๊ทธ๋กœ๊ธฐ ๊ฒŒ์ด์ง€ ๋ฐ”์ธ๋”ฉ
    โ”œโ”€โ”€ Text_MonsterName (UTextBlock)
    โ””โ”€โ”€ Text_HPValue (UTextBlock)

ํ”„๋ ˆ์ž„ ํ…์Šค์ฒ˜ ์ ์šฉ:
HP๋ฐ”์™€ ๊ทธ๋กœ๊ธฐ ๊ฒŒ์ด์ง€๋ฅผ ๊ฐ์‹ธ๋Š” ํ”„๋ ˆ์ž„์œผ๋กœ ๊ธฐ์กด UIFantasyWarriorHUD ํŒฉ์˜ ๊ฐ€๋กœํ˜• ๋ฐ” ํ…์Šค์ฒ˜๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

Content/External/UIFantasyWarriorHUD/Textures/FantasyWarrior/T_FantasyWarrior_Bar_Horizontal08.uasset

์ด ํ…์Šค์ฒ˜๋ฅผ FRA_Frame ์œ„์ ฏ์˜ ๋ธŒ๋Ÿฌ์‹œ๋กœ ์„ค์ •ํ•ด์„œ HP๋ฐ”/๊ทธ๋กœ๊ธฐ ๊ฒŒ์ด์ง€๊ฐ€ ํ”„๋ ˆ์ž„ ์•ˆ์— ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ฐฐ์น˜๋˜๋„๋ก ํ–ˆ๋‹ค.

๊ทธ๋กœ๊ธฐ ๊ฒŒ์ด์ง€ ํ‘œ์‹œ ์ฒ˜๋ฆฌ:

๊ธฐ์กด์—๋Š” NormalMonsterHealthBarComponent๊ฐ€ ํ‰์†Œ HiddenInGame ์ƒํƒœ๋ผ์„œ ๊ทธ๋กœ๊ธฐ ์ด๋ฒคํŠธ๊ฐ€ ์™€๋„ ๊ฒŒ์ด์ง€๊ฐ€ ๋ณด์ด์ง€ ์•Š์•˜๋‹ค. ๊ทธ๋กœ๊ธฐ ๊ฒŒ์ด์ง€ ์ถฉ์ „ ๋˜๋Š” ๊ทธ๋กœ๊ธฐ ์ƒํƒœ ํ™œ์„ฑํ™” ์‹œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐ•์ œ๋กœ ํ‘œ์‹œํ•˜๊ณ , ๊ทธ๋กœ๊ธฐ๊ฐ€ ๋๋‚  ๋•Œ ๋‹ค์‹œ ์ˆจ๊ธฐ๋„๋ก ์ˆ˜์ •ํ–ˆ๋‹ค.

void UNormalMonsterHealthBarComponent::HandleGroggyGaugeUpdated(float Ratio)
{
    // ๊ทธ๋กœ๊ธฐ ๊ฒŒ์ด์ง€๊ฐ€ ๊ฐฑ์‹ ๋  ๋•Œ ๋ฐ”๊ฐ€ ๋ณด์ด์ง€ ์•Š์œผ๋ฉด ๊ฐ•์ œ๋กœ ํ‘œ์‹œ
    if (IsHiddenInGame())
    {
        SetHiddenInGame(false);
        // ๊ทธ๋กœ๊ธฐ ์ค‘์—๋Š” ์ˆจ๊น€ ํƒ€์ด๋จธ ์ทจ์†Œ
        GetWorld()->GetTimerManager().ClearTimer(HideTimerHandle);
    }

    if (Widget)
    {
        Widget->UpdateGroggyGauge(Ratio);
    }
}

๋Ÿฐํƒ€์ž„ ์œ„์ ฏ ๋ฐ”์ธ๋”ฉ ํƒ€์ž… ํ™•์žฅ:

๊ธฐ์กด FRA_Frame, FRA_Vignette๋ฅผ UUserWidget*์œผ๋กœ ์„ ์–ธํ•ด์„œ ์™ธ๋ถ€ ํ”„๋ ˆ์ž„ ํด๋ž˜์Šค๋Š” ์žกํžˆ์ง€ ์•Š๋˜ ๋ฌธ์ œ๋ฅผ UWidget*์œผ๋กœ ํƒ€์ž…์„ ํ’€์–ด์„œ ํ•ด๊ฒฐํ–ˆ๋‹ค.

// ์ˆ˜์ • ์ „ โ€” ์ปค์Šคํ…€ ์™ธ๋ถ€ ํ”„๋ ˆ์ž„ ์œ„์ ฏ์„ ์žก์ง€ ๋ชปํ•จ
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional))
UUserWidget* FRA_Frame;

// ์ˆ˜์ • ํ›„ โ€” ์–ด๋–ค ์œ„์ ฏ ํƒ€์ž…์ด๋“  ์žกํž˜
UPROPERTY(BlueprintReadOnly, meta = (BindWidgetOptional))
UWidget* FRA_Frame;

๊ฒ€์ฆ ๊ฒฐ๊ณผ

  • RetrieveEditor Win64 Development ๋นŒ๋“œ ์„ฑ๊ณต
  • ConfigureMonsterHealthBarLayout.py ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰: Configure result: True, Save result: True
  • ๋กœ๊ทธ: Success - 0 error(s), 0 warning(s)
  • ๊ทธ๋กœ๊ธฐ ๊ฒŒ์ด์ง€๊ฐ€ ํ™”๋ฉด์— ์ •์ƒ ํ‘œ์‹œ๋˜๊ณ  ๊ทธ๋กœ๊ธฐ ์ƒํƒœ ๋™์ž‘ ํ™•์ธ
  • ์—ํ”ฝ ๋ชฌ์Šคํ„ฐ HP๋ฐ”/๊ทธ๋กœ๊ธฐ ๊ฒŒ์ด์ง€๊ฐ€ ํ”„๋ ˆ์ž„ ์•ˆ์— ํฌํ•จ๋œ ๋ ˆ์ด์•„์›ƒ ํ™•์ธ

์ฐธ๊ณ : git status ๊ธฐ์ค€์œผ๋กœ Content/Retrieve/UI/WBP_MonsterHealthBar.uasset์€ ??(Untracked)๋กœ ์žกํžŒ๋‹ค. PR์— ํฌํ•จํ•˜๋ ค๋ฉด git add ์‹œ ์ด ํŒŒ์ผ๋„ ํ•จ๊ป˜ stageํ•ด์•ผ ํ•œ๋‹ค.

๋ฐฐ์šด ์ 

ResetWidgetTree() ํ›„ ๊ฐ™์€ ์ด๋ฆ„์œผ๋กœ ์ƒˆ ์œ„์ ฏ์„ ๋งŒ๋“œ๋Š” ๊ฒƒ์€ UE์—์„œ ์•ˆ์ „ํ•˜์ง€ ์•Š๋‹ค. ํŒจํ‚ค์ง€ ์˜ค๋ธŒ์ ํŠธ ์ด๋ฆ„์€ ํŠธ๋ฆฌ๋ฅผ ๋น„์›Œ๋„ UPackage์— ๋‚จ์•„ ์žˆ์–ด ์ด๋ฆ„ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•œ๋‹ค. ๊ธฐ์กด ์œ„์ ฏ์„ ํ๊ธฐ ์ด๋ฆ„์œผ๋กœ renameํ•œ ๋’ค ์ƒˆ ๊ตฌ์กฐ๋ฅผ ์žฌ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜, ์ฒ˜์Œ๋ถ€ํ„ฐ ์™„์ „ํžˆ ์ƒˆ ์ด๋ฆ„์„ ์“ฐ๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•˜๋‹ค. UUserWidget* ๋ฐ”์ธ๋”ฉ์€ ์™ธ๋ถ€ ํŒจํ‚ค์ง€์˜ ์ปค์Šคํ…€ ์œ„์ ฏ ํด๋ž˜์Šค๋ฅผ ์žก์ง€ ๋ชปํ•˜๋ฏ€๋กœ, ์™ธ๋ถ€ ํ”„๋ ˆ์ž„/์žฅ์‹ ์œ„์ ฏ์€ UWidget*์œผ๋กœ ์„ ์–ธํ•ด์•ผ ํ•œ๋‹ค.


profile
๊ฐœ๋ฐœํ•ฉ์‹œ๋‹ค!

0๊ฐœ์˜ ๋Œ“๊ธ€