์ํฝ ๋ชฌ์คํฐ 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 ๋ ์ด์์ ์ฌ์ ๋ ฌ ์๋
์ํฝ ๋ชฌ์คํฐ 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 ...
}
์ด ๊ตฌ์กฐ๋ ์ธ ๊ฐ์ง ๋ฌธ์ ๋ฅผ ๋ง๋ ๋ค:
"๊ณตํต ๋ฐํ์ ์ฝ๋๋ ๋ฐ์ดํฐ๋ง ์ฝ๋๋ค"๋ ์์น ์๋ ๋ค์ ๊ตฌ์กฐ๋ก ๋ณ๊ฒฝํ๋ค.
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๊ฐ ์์ ํ๊ณ , ์ฝ๋๋ ๊ทธ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ธฐ๋ง ํด์ผ ํ๋ค.
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 ์๋๊ฐ ์ผ์ด๋๊ณ ์คํจํ๋ ๋ถํ์ํ ์ฐ์ฐ์ด ์๊ธด๋ค.
// 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 ํจ์๋ ๊ธฐ์กด ์๋ธํด๋์ค์ ๋์์ ์ ํ ๊ฑด๋๋ฆฌ์ง ์์ผ๋ฏ๋ก ํญ์ ์์ ํ๋ค.
๋น๋๋ฅผ ์คํํ์ ๋ค์ ์๋ฌ๊ฐ ์์๋๋ก ๋ฐ์ํ๋ค:
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์กฐ๊ฑด์ผ๋ก ๊ฐ์ธ์ผ ํจํค์ง ๋น๋๊ฐ ๊นจ์ง์ง ์๋๋ค.
์์
๋์ค FMonsterPatternRow ํ๋๋ช
์ด ๋ค์์ฒ๋ผ rename๋์ด ์์๋ค:
| ๊ธฐ์กด ์ด๋ฆ (develop ๊ธฐ์ค) | ์์ ์ค ๋ณ๊ฒฝ๋ ์ด๋ฆ |
|---|---|
AttackSequence | SequenceAnimation |
HitboxWindowStartTime | HitboxStartTime |
HitboxWindowDuration | HitboxDuration |
์ด 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
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:ํด๋นํ์ผ๋ก ์๊ฒฉ ๋ฒ์ ์ ๋จผ์ ํ์ธํด์ผ ํ๋ค.
feature/epic-monster-0616์ develop์ ๋จธ์งํ์ ์ถฉ๋์ด ๋ ์ข
๋ฅ๋ก ๋ฐ์ํ๋ค.
๋ฐ์ด๋๋ฆฌ ์ถฉ๋ (์ค ๋จ์ ๋ณํฉ ๋ถ๊ฐ):
Content/Retrieve/Data/DT_MonsterData.uassetContent/Retrieve/Data/DT_MonsterPattern.uassetContent/Retrieve/AI/Test/EnemyTestMap.umap์์ค ์ฝ๋ ์ถฉ๋:
Source/Retrieve/Data/RetrieveDataTableTypes.hSource/Retrieve/GameplayTags/RetrieveGameplayTags.h/.cppSource/Retrieve/Character/RetrieveEnemyCharacter.hSource/Retrieve/AbilitySystem/Enemy/GAS/GA_ShootProjectiles.h/.cppSource/Retrieve/AbilitySystem/Enemy/EnemyProjectile.h/.cppSource/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:
์์ชฝ ํ๊ทธ๋ฅผ ๋ชจ๋ ์ด๋ฆฐ๋ค.
GameplayEvent_Enemy_SpecialAttack_Acceleration, GameplayEvent_Enemy_SpecialAttack_DashAttackGameplayEvent_Enemy_SpecialAttack_ProjectileSpreadRetrieveEnemyCharacter.h:
์์ชฝ์ ํฉ์น๋ค.
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))
{
// ๋ฐ๋ฏธ์ง ์ ์ฉ
}
}
๋จธ์ง ์๋ฃ ํ ๋น๋๋ฅผ ๋๋ฆฌ๋ ์๋ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค:
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 ํฌํผ๋ ํ์ผ ๊ฐ์ ์ด๋ฆ์ด ๊ฒน์น๋ฉด ์ถฉ๋ํ๋ค โ ํ์ผ ์ด๋ฆ์ ์ ๋ฏธ์ฌ๋ก ๋ถ์ด๋ ์ต๊ด์ด ํ์ํ๋ค.
์ํฝ ๋ชฌ์คํฐ๊ฐ ๊ธฐ๋ณธ ๊ณต๊ฒฉ์ ์ ํ ํ์ง ์๋ ๊ฒ์ฒ๋ผ ๋ณด์.
์ผ๋ฐ ๋ชฌ์คํฐ์ ๊ธฐ๋ณธ ๊ณต๊ฒฉ ํ๋ฆ:
StateTree โ EnemyAttack task โ GA_EnemyBasicAttack ๋ฐ๋ โ UAbilitySystemComponent๊ฐ ๋ชฝํ์ฃผ ์ฌ์
โ GAS๊ฐ ์๋์ผ๋ก State_Enemy_Attack ํ๊ทธ ๋ถ์ฌ
์ํฝ ๋ชฌ์คํฐ์ ๊ธฐ๋ณธ ๊ณต๊ฒฉ ํ๋ฆ (AttackMontage ์์ ๋):
StateTree โ EnemyAttack task โ EnemyCombatComponent::RequestPatternByPriority()
โ AttackSequence๋ก ๋์ ๋ชฝํ์ฃผ ์์ฑ ๋ฐ ์ฌ์
โ GAS ๊ฐ์
์์ โ State_Enemy_Attack ํ๊ทธ ๋ฏธ๋ถ์ฌ
State_Enemy_Attack ํ๊ทธ๊ฐ ์์ผ๋ฉด:
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๋ฅผ ๊ฑฐ์น์ง ์๊ณ ์ง์ ๋ชฝํ์ฃผ๋ฅผ ์ฌ์ํ๊ฑฐ๋ ๋ก์ง์ ์คํํ๋ ๊ฒฝ๋ก์์๋ ๊ด๋ จ ํ๊ทธ๋ ์ง์ ๊ด๋ฆฌํด์ผ ํ๋ค. "๋ด๊ฐ ๋ถ์ธ ํ๊ทธ"์ "๋จ์ด ๋ถ์ธ ํ๊ทธ"๋ ๋ฐ๋์ ๊ตฌ๋ถ ํ๋๊ทธ๋ก ์ถ์ ํด์ผ ์๋ชป๋ ํด์ ๋ฅผ ๋ง์ ์ ์๋ค.
๊ฐ๊ณต๊ฒฉ 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๋ผ๋ ์์๋ฐ๋ ์๊ฐ ์ปดํฌ๋ํธ๊ฐ ๋ณด์ฅ๋๋ค.
๊ฒ์ด์ง๊ฐ ๊ฝ ์ฐจ๋ ๋ชฌ์คํฐ๊ฐ ๊ทธ๋ก๊ธฐ ์ํ๋ก ์ ํ๋์ง ์์.
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 ์ค์ ๊ฐ์ด ๋ฎ์ด์จ์ง๋ ๊ฒ์ ๋ง๊ธฐ ์ํด์๋ค.
๋๋๊ณค์ ํฌ์ฌ์ฒด๋ฅผ ํจ๋ง์ผ๋ก ๋ฐ์ฌ์์ผ ๋๋๊ณค์๊ฒ ๋งํ๋ ๊ทธ๋ก๊ธฐ๊ฐ ๊ฑธ๋ฆฌ์ง ์์.
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 ์ฒด์ธ ์์ด ์ปดํฌ๋ํธ๊ฐ ์ง์ ํ๊ทธ๋ฅผ ๋ถ์ด๋ ๊ฒ์ด ๊ฐ์ฅ ๊ฐ๋จํ๊ณ ์ ๋ขฐ์ฑ์ด ๋๋ค.
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 4๊ฐ ํ์ผ์ ์ญ์ ํ๋ค:
Source/Retrieve/Commandlets/RetrieveConfigureEpicMonstersCommandlet.cppSource/Retrieve/Commandlets/RetrieveConfigureEpicMonstersCommandlet.hSource/Retrieve/Commandlets/RetrieveConfigureEpicStateTreeCommandlet.cppSource/Retrieve/Commandlets/RetrieveConfigureEpicStateTreeCommandlet.h๋จ์ Commandlets ํด๋์ BonfireCommandlet, InteractionCommandlet์ ๊ธฐ์กด ํํธ ํ์ผ์ด๋ผ ๊ฑด๋๋ฆฌ์ง ์์๋ค.
Commandlet์ "ํ ๋ฒ ์คํํ๊ณ ์์ ์ ๊ตฌ์์ง ๋ค์๋ ํ์ ์์ด์ง๋" ์ด๊ธฐํ ๋๊ตฌ๋ค. ํ์ง๋ง ์ฝ๋๋ก ๋จ์ ์์ผ๋ฉด ๋๊ตฌ๋ ์ค์๋ก ๋ค์ ์คํํ ์ ์๊ณ , ๊ทธ ์๊ฐ ์๋์ผ๋ก ์กฐ์ ํ ์์ ์ค์ ์ด ์ ๋ถ ๋ ์๊ฐ๋ค. ์ด๊ธฐ ์๋ํ๊ฐ ์๋ฃ๋๋ฉด ๋ฐ๋์ ์ญ์ ํ๊ฑฐ๋ ์คํ ์กฐ๊ฑด์ ์์ ์ฒดํฌ(์: "์ด๋ฏธ ์ค์ ๋ ๊ฒฝ์ฐ ์คํต")๋ฅผ ๋ฃ์ด์ผ ํ๋ค.
์ค์ ์ฒด๋ ฅ๊ณผ 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 ์ฒดํฌ ํ๋๋ผ ๋ฌด์ ๊ฐ๋ฅํ๋ค.
๊ธฐ์กด 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: TrueSuccess - 0 error(s), 0 warning(s)์ฐธ๊ณ :
git status๊ธฐ์ค์ผ๋กContent/Retrieve/UI/WBP_MonsterHealthBar.uasset์??(Untracked)๋ก ์กํ๋ค. PR์ ํฌํจํ๋ ค๋ฉดgit add์ ์ด ํ์ผ๋ ํจ๊ป stageํด์ผ ํ๋ค.
ResetWidgetTree()ํ ๊ฐ์ ์ด๋ฆ์ผ๋ก ์ ์์ ฏ์ ๋ง๋๋ ๊ฒ์ UE์์ ์์ ํ์ง ์๋ค. ํจํค์ง ์ค๋ธ์ ํธ ์ด๋ฆ์ ํธ๋ฆฌ๋ฅผ ๋น์๋ UPackage์ ๋จ์ ์์ด ์ด๋ฆ ์ถฉ๋์ด ๋ฐ์ํ๋ค. ๊ธฐ์กด ์์ ฏ์ ํ๊ธฐ ์ด๋ฆ์ผ๋ก renameํ ๋ค ์ ๊ตฌ์กฐ๋ฅผ ์ฌ์์ฑํ๊ฑฐ๋, ์ฒ์๋ถํฐ ์์ ํ ์ ์ด๋ฆ์ ์ฐ๋ ๊ฒ์ด ์์ ํ๋ค.UUserWidget*๋ฐ์ธ๋ฉ์ ์ธ๋ถ ํจํค์ง์ ์ปค์คํ ์์ ฏ ํด๋์ค๋ฅผ ์ก์ง ๋ชปํ๋ฏ๋ก, ์ธ๋ถ ํ๋ ์/์ฅ์ ์์ ฏ์UWidget*์ผ๋ก ์ ์ธํด์ผ ํ๋ค.