Carrot
본문 바로가기
Unity/멋쟁이사자처럼 부트캠프

[멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(57일차) - 게임 디자인 패턴 (2), ScriptableObject, 데이터 파싱

by 독기품은토끼 2025. 8. 7.
✅ 오늘의 학습 목표
1. 디자인 패턴 (2)
2. ScriptableObejct 실습
3. 데이터 파싱 실습

1. 팩토리 패턴 (Factory Pattern)

1. Pizza

팩토리 패턴은 부모클래스에서 객체를 생성하기 위한 인터페이스를 제공하지만 서브클래스가 생성될 객체의 유형을 변경할 수 있도록 허용하는 패턴이다.

using UnityEngine;

public abstract class Pizza
{
    public string Name { get; }
    public string Sauce { get; }

}

using UnityEngine;

public abstract class PizzaStore : MonoBehaviour
{
    public Pizza OrderPizza(string type)
    {
        Pizza pizza = CreatePizza(type);

        return pizza;
    }

    protected abstract Pizza CreatePizza(string type);
}

 

각 피자의 기본 구조를 정의한 추상 클래스이다.

모든 피자는 이름(Name)과 소스(Sauce)를 가지고,

어떤 피자를 만들지는 CreatePizza라는 추상 메서드에 맡긴다.

 

using UnityEngine;

public class LegacyPizzaStore : PizzaStore
{
    protected override Pizza CreatePizza(string type)
    {
        if (type.Equals("Nomal"))
        {
            return new CheesePizza();
        }

        if (type.Equals("Special"))
        {
            return new PotatoPizza();
        }

        return null;
    }
}

using UnityEngine;

public class NewPizzaStore : PizzaStore
{
    protected override Pizza CreatePizza(string type)
    {
        if (type.Equals("Nomal"))
        {
            return new CheesePizza();
        }

        if (type.Equals("Special"))
        {
            return new BulgogiPizza();
        }

        return null;
    }
}

 

두 개의 피자 가게가 각각 자신만의 방식으로 피자를 만든다.

어떤 피자를 만들지 결정하는 CreatePizza를 오버라이드해서 구체적인 피자를 생성한다.

 

using UnityEngine;

public class CheesePizza : Pizza
{
    public string Name => "Cheese Pizza";
    public string Sauce => "Tomato Sauce";
    //public string Sauce { get => "Tomato Sauce"; }
    //public string Sauce
    //{
    //    get
    //    {
    //        return "Tomato Sauce";
    //    }
    //}
}

using UnityEngine;

public class BulgogiPizza : Pizza
{
    public string Name => "Bulgogi Pizza";
    public string Sauce => "Bulgogi Sauce";
}

using UnityEngine;

public class PotatoPizza : Pizza
{
    public string Name => "Potato Pizza";
    public string Sauce => "Tomato Sauce and Mayonnaise";
}

 

각각의 피자들이 Pizza 클래스를 상속받아 자신만의 이름과 소스를 정의한다.

 

using System.Collections;
using UnityEngine;

public class PizzaController : MonoBehaviour
{
    IEnumerator Start()
    {
        PizzaStore pizzaStore = null;
        Pizza pizza = null;

        pizzaStore = new LegacyPizzaStore();
        pizza = pizzaStore.OrderPizza("Nomal");
        Debug.Log($"주문한 {pizza} 나왔습니다.");

        yield return new WaitForSeconds(1f);
        pizza = pizzaStore.OrderPizza("Special");
        Debug.Log($"주문한 {pizza} 나왔습니다.");

        yield return new WaitForSeconds(1f);

        pizzaStore = new NewPizzaStore();
        pizza = pizzaStore.OrderPizza("Nomal");
        Debug.Log($"주문한 {pizza} 나왔습니다.");

        yield return new WaitForSeconds(1f);
        pizza = pizzaStore.OrderPizza("Special");
        Debug.Log($"주문한 {pizza} 나왔습니다.");
    }
}

 

이렇게 피자를 어떤 방식으로 만들지 몰라도 OrderPizza만 호출하면 된다.


이게 바로 팩토리 패턴의 핵심이다.

 

  • 확장성: 새로운 피자를 추가하더라도 PizzaStore를 바꾸지 않아도 됨
  • 유지보수 용이: 어떤 피자가 어떻게 만들어지는지는 PizzaStore가 아닌 각각의 서브 클래스에서 관리
  • 의존성 분리: 객체 생성과 사용이 분리됨

 

2. Monster

게임 개발에서는 몬스터 생성, 무기 생성, 스킬 생성 등 다양한 곳에 팩토리 패턴이 활용될 수 있다.
특히 이런 구조는 유닛 종류가 많거나 조건에 따라 다른 인스턴스를 생성해야 할 때 진가를 발휘한다.

 

 

2. 데코레이터 패턴 (Decorator Pattern)

객체에 기능을 추가하고 싶을 때 상속 없이도 동적으로 기능을 추가할 수 있도록 해주는 디자인 패턴

 

게임을 만들다 보면 어떤 객체에 기능을 유동적으로 추가하거나 변경하고 싶을 때가 있다.
예를 들어 커피에 우유를 넣거나, 모카를 추가하는 상황처럼 말이다.
이런 상황에서 유용하게 쓰이는 패턴이 바로 데코레이터 패턴이다.

 

1. Coffe

using UnityEngine;

public interface ICoffee
{
    string Name();
    int Cost();
}

 

모든 커피는 Name()과 Cost() 메서드를 가진다.

인터페이스를 통해 공통 구조를 정의한다.

 

using UnityEngine;

public class Espresso : ICoffee
{
    public string Name()
    {
        return "Espresso";
    }

    public int Cost()
    {
        return 4000;
    }
}

using UnityEngine;

public class Latte : ICoffee
{
    public string Name()
    {
        return "Latte";
    }

    public int Cost()
    {
        return 5500;
    }
}

 

기본 커피 메뉴에 해당하는 클래스들이다.

ICoffee를 구현해서 이름과 가격을 리턴한다.

 

using UnityEngine;

public class CoffeeDecorator : ICoffee
{
    protected ICoffee coffee;

    public CoffeeDecorator(ICoffee coffee)
    {
        this.coffee = coffee;
    }

    public virtual string Name()
    {
        return coffee.Name();
    }

    public virtual int Cost()
    {
        return coffee.Cost();
    }
}

 

기능을 덧붙이기 위한 기반 클래스다.
모든 데코레이터는 기본적으로 커피 객체를 포함하고 있으며 ICoffee를 구현한다.

 

using UnityEngine;

public class MilkDecorator : CoffeeDecorator
{
    public MilkDecorator(ICoffee coffee) : base(coffee)
    {

    }

    public override string Name()
    {
        return coffee.Name() + "Milk";
    }

    public override int Cost()
    {
        return coffee.Cost() + 500;
    }
}

using UnityEngine;

public class MochaDecorator : CoffeeDecorator
{
    public MochaDecorator(ICoffee coffee) : base(coffee)
    {

    }

    public override string Name()
    {
        return coffee.Name() + "Mocha";
    }

    public override int Cost()
    {
        return coffee.Cost() + 1000;
    }
}

 

이처럼 데코레이터를 겹겹이 감싸면서 기능을 동적으로 추가할 수 있다.

 

  • 조합 가능: 우유 + 모카, 모카 + 우유 등 자유롭게 기능을 조합할 수 있음
  • OCP(Open/Closed Principle) 만족: 기존 코드를 수정하지 않고 확장 가능
  • 유연함: 필요할 때만 기능을 추가할 수 있음

 

2. Attack

게임 개발에서는 공격 시스템에 적용할 수 있다.

기본 공격에 여러 스킬 (불 속성, 얼음 속성)을 추가하여 겹겹이 붙이는 방식으로 구현이 가능하다.

 

 

3. 어댑터 패턴 (Adapter Pattern)

서로 인터페이스가 다른 클래스들이 함께 작동할 수 있도록 해주는 패턴

 

게임 개발을 하다 보면 기존 코드(레거시 코드)는 그대로 두면서 새롭게 만든 시스템에 통합해야 하는 상황이 발생할 수 있다.

이럴 때 사용하는 패턴이 바로 어댑터 패턴이다.

 

using UnityEngine;

public interface ICharacter
{
    void Move(Vector3 dir);
    void Attack();
}

 

새로운 시스템에서 사용할 공통 인터페이스
모든 캐릭터는 Move()와 Attack() 메서드를 구현해야 한다.

 

using UnityEngine;

public class LegacyPlayer
{
    public void LegacyMove(float x, float y, float z)
    {
        Debug.Log($"Legacy Player 이동 : {x}, {y}, {z}");
    }

    public void LegacyAttack()
    {
        Debug.Log($"Legacy Attack");
    }
}

 

기존 코드에서 사용하는 클래스

메서드 이름이 다르고 벡터가 아닌 x, y, z 각각 값을 따로 받는 방식

 

using UnityEngine;

public class LegacyPlayerAdapter : MonoBehaviour, ICharacter
{
    private LegacyPlayer legacyPlayer;

    void Awake()
    {
        legacyPlayer = new LegacyPlayer();
    }

    public void Move(Vector3 dir)
    {
        legacyPlayer.LegacyMove(dir.x, dir.y, dir.z);
    }

    public void Attack()
    {
        legacyPlayer.LegacyAttack();
    }
}

 

LegacyPlayer를 감싸서 ICharacter처럼 보이게 해주는 클래스.
이 어댑터 덕분에 기존 코드를 수정하지 않고도 새 시스템과 연결할 수 있다.

 

using UnityEngine;

namespace Pattern.Adapter
{
    public class PlayerController : MonoBehaviour
    {
        public GameObject player;

        private ICharacter character;

        void Start()
        {
            character = player.GetComponent<ICharacter>();

            character.Move(Vector3.forward);
            character.Attack();

            LegacyPlayer legacyPlayer = new LegacyPlayer();
            legacyPlayer.LegacyMove(0, 0, 1);
        }
    }
}

 

GameObject에서 ICharacter를 가져와서 통일된 방식으로 Move()와 Attack() 호출한다.

위처럼 ICharacter 인터페이스로 동작하지만

내부에서는 LegacyPlayerAdapter가 LegacyPlayer를 감싸고 있기 때문에 결국 레거시 방식으로 동작하게 된다.

 

  • 레거시 코드 보호: 기존 코드를 건드리지 않고 그대로 유지
  • 새 시스템과 통합: 새로운 인터페이스를 기반으로 일관된 방식으로 동작 가능
  • 유지보수 용이: 어댑터만 수정하면 호환성 해결

 

4. 퍼싸드 패턴 (Facade Pattern)

여러 개의 복잡한 클래스나 시스템을 감싸서 하나의 통합된 인터페이스를 제공하는 패턴

 

게임 시스템이 복잡해질수록 여러 시스템을 한꺼번에 조작해야 하는 일이 발생할 수 있다.

이럴 때 유용하게 쓸 수 있는 패턴이 바로 퍼사드 패턴이다.

using UnityEngine;

public class InventorySystem : MonoBehaviour
{
    public void AddItem(string itemName)
    {
        Debug.Log($"{itemName} 획득");
    }

    public void HasItem(string itemName)
    {
        Debug.Log($"{itemName} 유무");
    }

    public void RemoveItem(string itemName)
    {
        Debug.Log($"{itemName} 버림");
    }
}

using UnityEngine;

public class QuestSystem : MonoBehaviour
{
    public void StartQuest(string questName)
    {
        Debug.Log($"{questName} 획득");
    }

    public void HasQuest(string questName)
    {
        Debug.Log($"{questName} 유무");
    }

    public void CompleteQuest(string questName)
    {
        Debug.Log($"{questName} 포기");
    }
}

using UnityEngine;

public class SoundSystem : MonoBehaviour
{
    public void StartSound(string soundName)
    {
        Debug.Log($"{soundName} 재생");
    }
    public void PauseSound(string soundName)
    {
        Debug.Log($"{soundName} 일시정지");
    }
    public void StopSound(string soundName)
    {
        Debug.Log($"{soundName} 종료");
    }
}

 

아이템 / 퀘스트 / 사운드를 관리하는 단순한 클래스

각 메서드는 Facade를 통해 호출 받는다.

 

using UnityEngine;

public class GameFacade : Singleton<GameFacade>
{
    private InventorySystem inventorySystem;
    private QuestSystem questSystem;
    private SoundSystem soundSystem;

    void Awake()
    {
        inventorySystem = GetComponent<InventorySystem>();
        questSystem = GetComponent<QuestSystem>();
        soundSystem = GetComponent<SoundSystem>();

        if (inventorySystem != null)
            inventorySystem = gameObject.AddComponent<InventorySystem>();

        if (questSystem != null)
            questSystem = gameObject.AddComponent<QuestSystem>();

        if (soundSystem != null)
            soundSystem = gameObject.AddComponent<SoundSystem>();
    }

    public void ItemEvent(int index, string itemName)
    {
        if (index == 0)
        {
            inventorySystem.AddItem(itemName);
        }
        else if (index == 1)
        {
            inventorySystem.AddItem(itemName);
        }
        else if (index == 2)
        {
            inventorySystem.AddItem(itemName);
        }
    }

    public void QuestEvent(int index, string questName)
    {
        if (index == 0)
        {
            questSystem.StartQuest(questName);
        }
        else if (index == 1)
        {
            questSystem.HasQuest(questName);
        }
        else if (index == 2)
        {
            questSystem.CompleteQuest(questName);
        }
    }

    public void SoundEvent(int index, string soundName)
    {
        if (index == 0)
        {
            soundSystem.StartSound(soundName);
        }
        else if (index == 1)
        {
            soundSystem.PauseSound(soundName);
        }
        else if (index == 2)
        {
            soundSystem.StopSound(soundName);
        }
    }
}

 

 

퍼싸드 클래스는 여러 가지 시스템을 다룰 수 있는 중앙 관리 클래스이다.

내부 로직은 서브 시스템에 위임하면서 외부에서는 간단한 메서드로 제어가 가능하다.

 

  • 코드 간결화: 복잡한 시스템을 단순하게 다룰 수 있음
  • 결합도 감소: 다른 시스템과 독립적으로 관리 가능
  • 확장성 좋음: 내부 시스템이 바뀌어도 퍼사드만 수정하면 됨

 

5. 서비스 로케이터 (Service Locator)

객체들을 전역적으로 찾아서 가져올 수 있는 공통 접근 지점을 제공해 주는 패턴
결합도는 낮추되 서비스에 대한 등록/탐색 로직을 하나의 클래스로 묶을 수 있는 패턴이다.

 

using UnityEngine;

public interface IAudioService
{
    void PlaySound();
    void StopSound();
}

public interface ISaveService
{
    void SaveData();
    void LoadData();
}

 

 

오디오 기능과 저장 기능을 추상화한 인터페이스

어떤 오디오/저장 구현체든 이 인터페이스만 맞추면 사용 가능하다.

 

using UnityEngine;

public class AudioService : MonoBehaviour, IAudioService
{
    public void PlaySound()
    {
        Debug.Log("Play Sound");
    }

    public void StopSound()
    {
        Debug.Log("Stop Sound");
    }
}

using UnityEngine;

public class SaveService : MonoBehaviour, ISaveService
{
    public void SaveData()
    {
        Debug.Log("Save Data");
    }

    public void LoadData()
    {
        Debug.Log("Load Data");
    }
}

 

 

 

실제로 사운드를 재생하는 컴포넌트와 데이터를 저장하는 컴포넌트

IAudioService / ISaveService를 구현하고 있으니 서비스로 등록 가능하다.

 

using System;
using System.Collections.Generic;
using Pattern;
using UnityEngine;

public class ServiceLocator : MonoBehaviour
{
    private Dictionary<Type, object> services = new Dictionary<Type, object>();

    public T GetService<T>() where T : class
    {
        if (services.TryGetValue(typeof(T), out var service))
        {
            return service as T;
        }

        return null;
    }

    public void RegisterService<T>(T service)
    {
        services[typeof(T)] = service;
    }

    public void UnregisterService<T>()
    {
        services.Remove(typeof(T));
    }
}

 

 

서비스를 등록하고, 찾고, 제거하는 핵심 클래스

Dictionary<Type, object> 구조

 

  • 여기서 Type은 IAudioService, ISaveService 같은 인터페이스 타입
  • object는 그 인터페이스를 구현한 실제 인스턴스 (AudioService, SaveService 등)

RegisterService<T>(T service)

 

  • typeof(T)로 타입을 키로 삼고
  • service 객체를 값으로 저장함
  • IAudioService 키로 AudioService 인스턴스를 등록하게 됨

GetService<T>()

 

  • typeof(T)로 해당 타입의 서비스가 등록되어 있는지 확인
  • 있으면 object를 T 타입으로 캐스팅해서 반환

 

🚨 딕셔너리 사용 이유

  • 하나의 클래스 안에서 다양한 타입의 서비스를 저장하려면 각 타입마다 따로 변수를 만들 수 없음
  • 그래서 타입 자체를 키로 쓰는 딕셔너리가 가장 유연하고 깔끔한 방식임
    → 타입에 따라 어떤 객체든 보관 가능함
    → 꺼낼 땐 원하는 타입으로 꺼낼 수 있음

 

6. ScriptableObject

 

MonoBehaviour 클래스의 경우 런타임 중 에디터에서 값을 바꾸면 런타임 종료 시 값이 원래대로 돌아온다.

그런데 런타임 상황에서도 데이터를 수정하거나 추적하고 싶거나 공통으로 사용하는 데이터가 있을 경우 변환된 값이 유지되었으면 하는 경우가 있을 것이다.

 

그럴 때 사용하는 것이 ScriptableObejct 클래스이다.

참고로 해당 클래스는 MonoBehaviour 클래스를 사용하기에 쓸 수 있었던 Coroutine이나 Componet 등 여러 기능을 사용하지 못하고 Start문과 Update문을 사용할 수 없다.

구분 MonoBehaviour ScriptableObject
GameObject 필요 필요함 필요 없음
런타임 값 변경 유지되지 않음 유지됨
사용 목적 씬 안에서 동작하는 로직 공유 가능한 데이터 저장
대표 메서드 Start(), Update() 등 없음 (직접 만들어야 함)
인스턴스 생성 AddComponent CreateInstance 또는 에셋 생성

 

1. 기본 구조

using UnityEngine;

[CreateAssetMenu(fileName = "WeaponData", menuName = "Scriptable Objects/WeaponData")]
public class WeaponData : ScriptableObject
{
    public string weaponName;
    public int attackDamage;
    public int attackRange;
}

 

무기 이름, 공격력, 공격 범위를 ScriptableObject로 정의하였다.

 

ScriptableObject로 정의된 오브젝트는 유니티 에디터에서 Create - Scriptable Object - WeaponData로 생성할 수 있다.

이렇게 에셋처럼 관리할 수 있다는 장점이 있다.

 

using UnityEngine;

public class WeaponController : MonoBehaviour
{
    public GameObject[] weaponObjs;
    public WeaponData[] weaponDatas;

    public string currWeaponName;
    public int currWeaponDamage;
    public int currWeaponRange;

    void Start()
    {
        foreach (var data in weaponDatas)
        {
            Debug.Log($"{data.weaponName} / {data.attackDamage} / {data.attackRange}");
        }
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            SwapWeapon(0);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            SwapWeapon(1);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha3))
        {
            SwapWeapon(2);
        }
    }

    private void SwapWeapon(int index)
    {
        foreach (var weapon in weaponObjs)
            weapon.SetActive(false);

        weaponObjs[index].SetActive(true);

        currWeaponName = weaponDatas[index].weaponName;
        currWeaponDamage = weaponDatas[index].attackDamage;
        currWeaponRange = weaponDatas[index].attackRange;
    }
}

 

무기 오브젝트와 데이터를 연결해서 번호 키(1, 2, 3)로 무기를 스왑하는 방식을 구현하였다.

이렇게 하면 런타임에서 에디터의 값을 바꾸면 바로 적용되는 것을 확인할 수 있고

무기 데이터를 따로 에셋으로 관리할 수 있어 유지보수가 쉬워진다.

 

2. 그룹

그런데 만약 무기 데이터에 들어가는 속성이 많아진다고 해보자.

예를 들어 공격력도 최소/최대, 성공률, 크리티컬 확률까지 관리하고 추가로 가격, 강화 레벨 같은 디테일 정보도 포함하려면?

 

이런 경우에는 ScriptableObject 안에 클래스를 중첩시켜서 더 복잡한 데이터 구조를 만들 수 있다.

 

using UnityEngine;

[CreateAssetMenu(fileName = "WeaponDataGroup", menuName = "Scriptable Objects/WeaponDataGroup")]
public class WeaponDataGroup : ScriptableObject
{
    public WData[] wData;
}

[System.Serializable]
public class WData
{
    public string name;
    public DamageSystem damageSystem;
    public int range;
    public DetailData detailData;
}

[System.Serializable]
public class DetailData
{
    public int cost;
    public int upgradeLevel;
}

[System.Serializable]
public class DamageSystem
{
    public int minDamage;
    public int maxDamage;
    public int successPercent;
    public int criticalChance;
}

 

WeaponDataGroup이라는 ScriptableObject 안에 여러 무기 데이터를 배열로 넣고 각 무기는 WData라는 클래스 형태로 구성했다.

여기서 WData에는 DamageSystem, DetailData 등 하위 클래스를 넣어서 공격 관련 정보나 무기 업그레이드 관련 데이터를 따로 관리할 수 있게 했다.

 

그룹형 구조를 쓰면 에셋 하나로 여러 무기를 묶어서 관리할 수 있고

무기 수가 많아지거나 구조가 복잡할 때 더 보기 편하고 유지보수도 쉬워진다.

 

 

  • 무기 수가 적고 구조가 단순하다면 → WeaponData처럼 개별 ScriptableObject로 만들면 관리하기 쉽다.
  • 무기 수가 많거나 속성이 복잡하다면 → WeaponDataGroup처럼 그룹 구조로 만들어서 계층적으로 데이터를 나누는 게 좋다.
  • ScriptableObject를 사용하면 게임 밸런스 조정이 쉬워지고
  • 코드랑 데이터가 분리돼서 유지보수도 편하고
  • 인스펙터에서 바로 수정 가능해서 테스트도 빠르다.

 

7. 데이터 파싱 (Data Parsing)

문자열로 된 데이터를 구조화된 형태로 바꾸는 작업

 

이번에는 Unity에서 CSV 또는 TSV 파일을 읽어와서 캐릭터 데이터를 파싱하는 작업을 해보려고 한다.

[CSV 파일]
CharID,Name,HP,Attack
C01,Player,100,15
C02,Goblin,30,5
C03,Dragon,500,50
C04,Wizard,70,20

[TSV 파일]
CharID	Name	HP	Attack 
C01	Player	100	15
C02	Goblin	30	5
C03	Dragon	500	50
C04	Wizard	70	20

 

CSV 파일은 구분자를 쉼표(,)

TSV 파일은 구분자를 탭(\t) 기준으로 데이터를 나눈다.

 

using System.Collections.Generic;
using UnityEngine;

public class CsvTsvParser : MonoBehaviour
{
    [System.Serializable]
    public class CharacterData
    {
        public string charID;
        public string name;
        public int hp;
        public int attack;

        public CharacterData(string charID, string name, int hp, int attack)
        {
            this.charID = charID;
            this.name = name;
            this.hp = hp;
            this.attack = attack;
        }
    }

    public List<CharacterData> characters = new List<CharacterData>();

    void Start()
    {
        // var dataFile = Resources.Load<TextAsset>("CsvData");
        var dataFile = Resources.Load<TextAsset>("TsvData");

        string data = dataFile.text;

        ParsingCharacterData(data);
    }

    // 데이터를 데이터 클래스에 맞게 파싱하는 기능
    private void ParsingCharacterData(string data)
    {
        Debug.Log(data);

        string[] rows = data.Split('\n'); // lines

        for (int i = 1; i < rows.Length; i++)
        {
            // string[] cols = rows[i].Split(',');
            string[] cols = rows[i].Split('\t');

            CharacterData characterData = new CharacterData(cols[0], cols[1], int.Parse(cols[2]), int.Parse(cols[3]));

            characters.Add(characterData);
        }
    }
}

 

 

CharacterData 클래스 정의
캐릭터의 속성들을 담는 데이터 클래스다.

charID, name, hp, attack 필드를 갖고 있고 파싱 한 값을 이 클래스 객체로 만들어 리스트에 저장한다.

 

Start()에서 텍스트 파일 로드
Resources.Load<TextAsset>("TsvData")로 텍스트 파일을 불러온다.
Resources 폴더에 파일이 있어야 하며, 확장자는 .csv, .tsv지만 코드에서 쓸 땐 이름만 넣으면 된다.

 

파싱 함수 ParsingCharacterData()

먼저 텍스트 전체를 줄 단위로 Split('\n') 해서 각 행으로 나눈다.

첫 줄은 헤더라서 생략하고, 1번 인덱스부터 반복문을 돌린다.

각 행은 다시 Split('\t')로 나눠서 칼럼 값을 가져온다 (TSV 기준).

그 값을 CharacterData 객체로 만들어 리스트에 추가한다.

 

이번 실습으로 외부 텍스트 데이터를 게임 안으로 가져오는 기본 구조를 익혔다.
게임 기획 단계에서 캐릭터 수치나 이름을 엑셀로 관리하고 이를 TSV로 저장해서 Unity에서 바로 불러오는 방식으로 확장할 수 있다.

나중에 Excel → TSV 자동 변환까지 연동하면 디자이너-개발자 협업도 훨씬 편해질 거라고 말씀하셨다.