로그인게임하기
구조 읽기

[100턴 구조 읽기 2] 문자열로 조립하는 전투 스킬

2026.10.10

졸업까지 100턴 구조 읽기 · 2편

졸업까지 100턴은 전투 스킬마다 클래스를 만들지 않습니다. 표에 적힌 문자열로 발동 조건 8종과 효과 23종을 골라 조합합니다. 새 스킬을 만들 때 코드를 거의 고치지 않는 구조와, 웹 빌드에서 만난 함정을 함께 보겠습니다.

PROBLEM

스킬마다 클래스를 만들면 조합이 폭발합니다

왼쪽에는 스킬마다 쌓인 책 더미, 오른쪽에는 로라가 조건 카드와 효과 카드를 골라 합치는 그림
스킬마다 책을 쓰면 더미가 되고, 조건과 효과를 나누면 카드 두 줄로 조합합니다.

"적이 가까이 오면 광역 피해", "아군이 다치면 회복", "길게 누르면 화상" 같은 스킬은 결국 두 질문의 조합입니다. 언제 쓰는가, 그리고 무엇을 하는가.

스킬마다 클래스

  • 조건 8개와 효과 23개를 섞으면 수백 가지가 됩니다.
  • 비슷한 코드가 클래스마다 반복됩니다.
  • 기획자가 조합을 바꾸려면 프로그래머를 기다려야 합니다.

조건과 효과를 따로

  • 조건 8개와 효과 23개만 만들면 됩니다.
  • 조합은 표의 문자열로 정합니다.
  • 새 조합에는 코드가 필요 없습니다.
STRUCTURE

구조

ActiveSkill 표ConditionType, SkillType, SecondarySkillType 문자열과 수치
TDActiveSkill문자열로 클래스를 찾아 조건 하나와 효과 둘을 조립합니다
TDSkillCondition언제 발동하는지 판단하고, 대상을 함께 돌려줍니다
TDSkillRunner대상 배열을 받아 효과를 적용하고, 대상 배열을 다시 반환합니다

조건의 계약은 짧습니다. 발동 여부와 대상을 함께 돌려주므로 효과 쪽은 대상 선정을 신경 쓰지 않아도 됩니다.

TDSkillCondition.csTDClient/Assets/Sources/Content/Skill/TDSkillCondition.csr5406 · 3–19줄
public abstract class TDSkillCondition
{
    protected int conditionValue1;
    protected int conditionValue2;
    protected TDField field = null;
    protected TDCharacter owner = null;

    public void Initialize(TDField field, TDCharacter owner, int conditionValue1, int conditionValue2)
    {
        this.field = field;
        this.owner = owner;
        this.conditionValue1 = conditionValue1;
        this.conditionValue2 = conditionValue2;
    }

    public abstract TDCharacter[] CheckCondition(ElStateTypes currentType, out bool check);
}

효과의 계약도 짧습니다. 대상을 받아 대상을 반환합니다. 이 모양이 뒤에서 효과를 이어 붙이는 열쇠가 됩니다.

TDSkillRunner.csTDClient/Assets/Sources/Content/Skill/TDSkillRunner.csr5406 · 62–63줄
public abstract TDField.TeamTypes GetTargetTeamType();
public abstract TDCharacter[] RunSkill(TDCharacter[] targetCharacters);
FACTORY

문자열에서 클래스로

표에 EnemyNear 라고 적혀 있으면 TDSkillCondition_EnemyNear 클래스를 찾아 인스턴스를 만듭니다. 이름 규칙이 곧 등록 절차입니다. switch 문은 어디에도 없습니다.

TDActiveSkill.csTDClient/Assets/Sources/Content/Skill/TDActiveSkill.csr5406 · 38–59줄
string conditionClassName = "TDSkillCondition_" + skillInfo.ConditionType;

Type type = Type.GetType(conditionClassName);

if (type != null)
{
    object instance = Activator.CreateInstance(type);

    if (instance is TDSkillCondition myClassInstance)
    {
        condition = (TDSkillCondition)instance;
    }
}

if (condition == null)
    condition = new TDSkillCondition_Whenever();

condition.Initialize(field, owner, skillInfo.ConditionValue1, skillInfo.ConditionValue2);


CreateRunner(skillInfo.SkillType, 1, ref runner);
CreateRunner(skillInfo.SecondarySkillType, 2, ref runner2);

새 효과를 추가할 때 할 일은 두 가지입니다. TDSkillRunner_ 로 시작하는 클래스를 만들고, 표의 칸에 그 이름을 적습니다.

PIPELINE

효과를 이어 붙입니다

효과가 대상 배열을 반환하기 때문에, 첫 효과가 맞힌 대상에게 두 번째 효과를 그대로 넘길 수 있습니다. "광역 피해를 주고, 맞은 적에게 화상을 남긴다" 같은 스킬이 표 두 칸으로 만들어집니다.

TDActiveSkill.csTDClient/Assets/Sources/Content/Skill/TDActiveSkill.csr5406 · 213–235줄
if (runner.CanChangeTarget)
{
    skillDelaying = true;
    DoAction(delegate
    {
        targetCharacters = runner.RunSkill(targetCharacters);

        if (!string.IsNullOrEmpty(skillInfo.TargetEffectPath) && skillInfo.TargetEffectPath != "-" && targetCharacters != null && targetCharacters.Length > 0)
        {
            foreach (var target in targetCharacters)
            {
                var eff = CreateEffect(skillInfo.TargetEffectPath, target.transform);
                OnCreateTargetEffect(eff, target, runner);
            }
        }

        if (runner2 != null)
        {
            targetCharacters = runner2.RunSkill(targetCharacters);
        }

        skillDelaying = false;
    });
  1. 조건적이 가까이 왔다
  2. 효과 1광역 피해, 맞은 대상 반환
  3. 효과 2같은 대상에게 화상
INHERIT

비슷한 효과는 상속으로 줄입니다

화상, 공포, 빙결, 중독, 기절은 모두 상태 이상을 거는 효과입니다. 공통 동작은 부모 클래스가 맡고, 자식은 어떤 상태 이상인지만 말합니다.

TDSkillRunner_Burn.csTDClient/Assets/Sources/Content/Skill/Runner/TDSkillRunner_Burn.csr5406 · 4–11줄
public class TDSkillRunner_Burn : TDSkillRunner_AbnormalStatus
{
    public TDSkillRunner_Burn() { }

    public override TDCharacter.AbnormalStatusTypes GetAbnormalStatus()
    {
        return TDCharacter.AbnormalStatusTypes.Burn;
    }

화상

Burn

공포

Fear

빙결

Freezing

중독

Poisoning

기절

Stun

WEBGL

웹 빌드에서 클래스가 사라지는 함정

브라우저 창 모양 포장 기계를 지난 카드 가운데 이름으로 불리지 않은 두 장이 빈 카드가 되어 나와 에드윈이 고개를 갸웃하는 그림
이름으로 불리지 않은 클래스는 웹 빌드에서 걷혀 나갑니다.

Unity 의 웹 빌드는 IL2CPP 로 컴파일하면서 쓰이지 않는 코드를 걷어 냅니다. 그런데 이 스킬 클래스들은 코드 어디에서도 이름으로 불리지 않습니다. 문자열로만 찾기 때문이죠.

빌드가 지운 클래스는 조용히 사라집니다

에디터에서는 잘 동작하던 스킬이 웹 빌드에서만 기본 효과로 바뀝니다. 오류도 나지 않습니다.

그래서 link.xml 에 조건 8개와 효과 쪽 24개(상태 이상 부모 클래스 포함), 모두 32개 타입을 지우지 말라고 적어 둡니다.

link.xmlTDClient/Assets/link.xmlr5406 · 9–20줄
<assembly fullname="Assembly-CSharp">
    <type fullname="TDSkillCondition_EnemyBack" preserve="all"/>
    <type fullname="TDSkillCondition_EnemyFar" preserve="all"/>
    <type fullname="TDSkillCondition_EnemyNear" preserve="all"/>
    <type fullname="TDSkillCondition_EnemyStraight" preserve="all"/>
    <type fullname="TDSkillCondition_FriendlyNear" preserve="all"/>
    <type fullname="TDSkillCondition_LongPressEnemy" preserve="all"/>
    <type fullname="TDSkillCondition_LongPressFriendly" preserve="all"/>
    <type fullname="TDSkillCondition_Whenever" preserve="all"/>
    <type fullname="TDSkillRunner_AbnormalStatus" preserve="all"/>
    <type fullname="TDSkillRunner_AccuracyPointBuff" preserve="all"/>
    <type fullname="TDSkillRunner_AreaDamage" preserve="all"/>
RETRO

돌아보며

잘된 점

  • 새 효과는 클래스를 만들고 표에 이름을 적어 추가합니다.
  • 조건과 효과를 따로 두어 조합을 데이터로 정합니다.
  • 효과가 대상을 반환해 두 효과를 이어 붙일 수 있습니다.

아쉬운 점

  • 이름을 잘못 적으면 조용히 기본값으로 바뀝니다. 조건은 항상 발동, 효과는 광역 피해가 되어 오타가 "항상 쓰는 광역기" 로 보입니다. 개발 중에는 오류로 알려야 했습니다.
  • link.xml 에 손으로 등록해야 해서, 빠뜨리면 웹 빌드에서만 문제가 납니다.
  • 피해량 계산이 효과 클래스마다 반복되어 있습니다.

정리

  1. 조건과 효과를 나눕니다. 스킬은 "언제" 와 "무엇을" 의 조합입니다.
  2. 이름 규칙으로 등록합니다. 문자열 접두사와 리플렉션으로 switch 문 없이 클래스를 찾습니다.
  3. 리플렉션은 빌드 설정과 짝입니다. IL2CPP 가 지우지 않도록 link.xml 에 남깁니다.

다음 편에서는 보드판의 주사위를 서버가 굴리고, 클라이언트가 그 결과를 연출하는 구조를 보겠습니다.

← 개발 노트 목록