6 분 소요

🎮 2D SRPG 만들기 선행학습 6부작 — 파랜드 택틱스 같은 쿼터뷰 SRPG 를 직접 만들기 위한 기초 체력을 쌓습니다. 게임 루프부터 세이브/로드, 맵 에디터, UI, 사운드, 연출까지 편마다 실행되는 코드로 하나씩. 전체 10편.
  1. 게임 루프와 델타타임 — 모든 게임의 심장
  2. 타일맵과 쿼터뷰 좌표계 — 마름모 세상의 수학
  3. 스프라이트와 애니메이션 — 캐릭터에 생명 불어넣기
  4. 이동 범위와 A* 길찾기 — SRPG 의 파란 칸
  5. 턴제 전투와 상태머신 — 턴 큐·데미지·적 AI
  6. 데이터 설계와 세이브/로드 — 게임을 콘텐츠로 채우기지금 글
  7. Tiled 맵 에디터 — 배열 손편집 졸업하기
  8. UI 와 메뉴 — 정보를 보여주는 기술
  9. 사운드 — 타격감은 귀에서 나온다
  10. 연출 — 손맛을 만드는 잔기술

Summary

5편까지로 전투가 돌아가는 게임 골격이 생겼어요. 그런데 지금 유닛 능력치가 전부 make_unit(1, 1, "ally") 같은 코드 속에 박혀 있습니다. 캐릭터 20명과 스킬 50개를 이 방식으로 만들면, 밸런스 수치 하나 바꿀 때마다 코드를 뒤져야 해요.

이번 편은 이 문제를 푸는 데이터 주도 설계(data-driven design) 와, 게임을 껐다 켜도 이어할 수 있게 하는 세이브/로드를 다룹니다. 여기까지가 게임의 뼈대예요. 그리고 뼈대에서 더 나아가려면 무엇을 배우면 되는지 로드맵도 남겨둘게요.

💡 이 글에서 다루는 것

  • 데이터 주도 설계 — 코드는 규칙만, 콘텐츠는 JSON 으로
  • 템플릿과 인스턴스 — “기사 클래스”와 “전장 위의 기사 3호” 구분하기
  • 스킬을 데이터로 정의하고 코드로 해석하기
  • 세이브/로드 — 무엇을 저장하고 무엇을 저장하지 않는가
  • 시리즈 이후의 학습 로드맵 — 맵 에디터, 사운드, 배포, 엔진 이전



1. 코드는 규칙만, 콘텐츠는 데이터로

상용 게임 개발의 대원칙 하나를 소개할게요. 코드는 “어떻게 동작하는가”(규칙)만 담고, “무엇이 있는가”(콘텐츠)는 데이터 파일로 뺀다 — 이걸 데이터 주도 설계라고 합니다.

  코드에 남는 것 (규칙) 데이터로 빠지는 것 (콘텐츠)
유닛 데미지 공식, 이동 알고리즘 이름, HP, 공격력, 이동력
스킬 효과 해석기 (데미지/회복 처리) 스킬 이름, 위력, 사거리, 효과 종류
타일 렌더링, 통행 판정 타일 배열, 배치, 이벤트

이렇게 나누면 밸런스 조정이 게임 코드를 안 건드리고 JSON 편집만으로 끝나요. 지형 데이터를 2편에서 MOVE_COST 딕셔너리로, 데미지 공식을 5편에서 함수로 분리해온 것도 전부 이 원칙의 실천이었습니다. 오늘은 유닛과 스킬까지 마저 빼냅니다.

data/units.json 을 이렇게 만들어요.

{
  "version": 1,
  "units": [
    {"id": "knight",  "name": "기사",   "max_hp": 24, "atk": 12, "dfn": 9, "mov": 3,
     "skills": ["slash"]},
    {"id": "archer",  "name": "궁수",   "max_hp": 18, "atk": 13, "dfn": 5, "mov": 4,
     "skills": ["arrow_shot"]},
    {"id": "goblin",  "name": "고블린", "max_hp": 16, "atk": 10, "dfn": 4, "mov": 4,
     "skills": ["slash"]}
  ]
}



2. 템플릿과 인스턴스 — 기사와 기사 3호

데이터를 읽을 때 꼭 구분해야 하는 개념이 있어요. units.json 의 “기사”는 템플릿(설계도)이고, 전장 위에 서 있는 기사 한 명 한 명은 인스턴스(실체)입니다. 인스턴스는 템플릿에 없는 자기만의 상태 — 현재 위치, 현재 HP, 행동 여부 — 를 따로 가져요.

import json

def load_templates(path="data/units.json"):
    with open(path, encoding="utf-8") as f:
        return {t["id"]: t for t in json.load(f)["units"]}

def spawn(templates, tid, gx, gy, team):
    t = templates[tid]
    return {**t,                          # 템플릿 복사 (이름·능력치)
            "gx": gx, "gy": gy, "team": team,
            "hp": t["max_hp"],            # 인스턴스 상태는 여기서 시작
            "acted": False, "path": []}

templates = load_templates()
u = spawn(templates, "knight", 1, 1, "ally")
print(u["name"], u["hp"], "/", u["max_hp"], "@", (u["gx"], u["gy"]))
기사 24 / 24 @ (1, 1)

{**t, ...} 한 줄이 템플릿을 복사한 뒤 인스턴스 상태를 덧씌웁니다. 5편의 make_unit 을 이 spawn 으로 갈아끼우면, 이제 새 유닛 종류 추가는 JSON 에 한 줄 추가로 끝나요. 코드 변경 없이요.

⚠️ {**t} 는 얕은 복사라서, 템플릿 안에 리스트(예: skills)가 있으면 인스턴스들이 같은 리스트를 공유합니다. 인스턴스가 그 리스트를 수정할 일이 있다면(스킬 습득 등) copy.deepcopy(t) 로 바꾸세요. 읽기만 하면 얕은 복사로 충분해요.



3. 스킬도 데이터로 — 작은 해석기 패턴

스킬은 “종류가 다양하지만 구조는 비슷한” 전형적인 콘텐츠라, 데이터화의 효과가 가장 큰 곳입니다. 요령은 스킬의 효과를 종류(effect) + 수치(power) 조합으로 표준화하는 거예요.

{
  "version": 1,
  "skills": [
    {"id": "slash",      "name": "베기",    "effect": "damage", "power": 1.0, "range": 1},
    {"id": "arrow_shot", "name": "활쏘기",  "effect": "damage", "power": 0.8, "range": 3},
    {"id": "first_aid",  "name": "응급처치", "effect": "heal",   "power": 8,   "range": 1}
  ]
}

코드 쪽엔 이 데이터를 해석하는 작은 해석기 하나만 둡니다.

def apply_skill(skill, user, target):
    if skill["effect"] == "damage":
        dmg, crit = calc_damage(round(user["atk"] * skill["power"]), target["dfn"])
        target["hp"] = max(0, target["hp"] - dmg)
        return f"{skill['name']}! {dmg} 데미지"
    elif skill["effect"] == "heal":
        healed = min(skill["power"], target["max_hp"] - target["hp"])
        target["hp"] += healed
        return f"{skill['name']}! {healed} 회복"

새 데미지 스킬 10개를 만들어도 코드는 그대로고, effect 종류가 늘어날 때만(버프, 상태이상 등) 분기 하나를 추가합니다. 파랜드 택틱스의 그 많은 마법들도 결국 “종류 몇 가지 × 수치 조합”이에요. 사거리(range)는 4편의 movement_range 를 재활용해 “스킬 사거리 내 칸 하이라이트”로 그대로 확장됩니다.



4. 세이브/로드 — 상태만 저장한다

세이브의 제1원칙은 “다시 계산할 수 있는 것은 저장하지 않는다” 입니다. 저장할 것은 오직 되돌릴 수 없는 상태뿐이에요.

저장한다 (상태) 저장하지 않는다 (파생/데이터)
유닛 위치·현재 HP·행동 여부 능력치 템플릿 (units.json 에 있음)
현재 턴 수, 누구 턴인지 이동 범위, 경로 (다시 계산 가능)
진행 플래그 (스토리, 획득 아이템) 화면 좌표, 애니메이션 프레임

이 원칙대로면 세이브 파일이 놀랄 만큼 작아집니다. 인스턴스에서 상태 필드만 골라 JSON 으로 내리면 돼요.

SAVE_FIELDS = ("id", "gx", "gy", "team", "hp", "acted")

def save_game(units, turn, path="save.json"):
    data = {"version": 1, "turn": turn,
            "units": [{k: u[k] for k in SAVE_FIELDS} for u in units]}
    with open(path, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

def load_game(templates, path="save.json"):
    with open(path, encoding="utf-8") as f:
        data = json.load(f)
    units = []
    for s in data["units"]:
        u = spawn(templates, s["id"], s["gx"], s["gy"], s["team"])
        u["hp"], u["acted"] = s["hp"], s["acted"]     # 상태 복원
        units.append(u)
    return units, data["turn"]

units = [spawn(templates, "knight", 1, 1, "ally"),
         spawn(templates, "goblin", 6, 2, "enemy")]
units[1]["hp"] = 9                                     # 전투로 깎였다 치고
save_game(units, turn=3)
loaded, turn = load_game(templates)
print(turn, "턴,", [(u["name"], u["hp"]) for u in loaded])
3 턴, [('기사', 24), ('고블린', 9)]

로드가 spawn 을 다시 거치는 게 포인트예요. 능력치는 항상 최신 units.json 에서 오고, 세이브에는 상태만 있으니 — 밸런스 패치를 해도 옛 세이브가 그대로 호환됩니다. 능력치까지 통째로 저장했다면 패치 전 수치가 세이브에 박제됐겠죠.

💡 "version": 1 필드는 지금은 아무 일도 안 하지만, 나중에 세이브 구조를 바꿀 때 “옛 버전이면 변환해서 읽기” 분기를 걸 수 있게 해주는 보험입니다. 데이터 파일에는 처음부터 버전을 박아두는 습관을 추천드려요.



5. 뼈대 완성 — 다음 로드맵

여섯 편으로 파랜드 택틱스 같은 게임의 뼈대에 필요한 선행지식은 전부 다뤘습니다. 정리하면 이렇게 쌓였어요.

배운 것 게임에서의 역할
1편 게임 루프, 델타타임 심장 박동
2편 타일맵, 쿼터뷰 좌표 변환 전장
3편 스프라이트, 애니메이션 살아있는 캐릭터
4편 다익스트라, A*, 경로 보간 이동 규칙
5편 상태머신, 턴, 데미지, 적 AI 전투
6편 데이터 주도 설계, 세이브 콘텐츠와 지속성

여기서 “내 게임”으로 나아가는 다음 단계들을 남겨둡니다. 급한 순서대로예요.

  1. 맵 에디터 — 배열 손편집은 금방 한계가 와요. Tiled 라는 무료 에디터로 맵을 그리고, pytmx 라이브러리로 읽어오는 조합이 표준입니다. 2편의 배열 자리에 그대로 꽂혀요. 가장 급한 항목이라 바로 다음 7편에서 다룹니다.
  2. UI 와 메뉴 — 유닛 정보창, 행동 메뉴, 전투 예측창. 5편의 상태머신에 상태를 추가하는 연습으로 최적입니다. 8편에서 다룹니다.
  3. 사운드pygame.mixer 로 BGM 과 효과음. 타격음 하나만 넣어도 게임의 체감이 확 달라져요. 9편에서 다룹니다.
  4. 연출 — 데미지 숫자 팝업, 화면 흔들림, 페이드 전환. 전부 델타타임 응용입니다. 10편에서 다룹니다.
  5. 배포pyinstaller 로 실행 파일 묶기. 친구에게 보내보는 순간 동기부여가 다시 차올라요.
  6. 엔진 이전 — 규모가 커지면 Godot 을 추천합니다. 이 시리즈의 개념(씬 트리=상태머신, TileMap 노드=2편, AnimationPlayer=3편)이 이름만 바뀐 채 그대로 나와서, 학습이 아니라 번역에 가까울 거예요.

✅ 가장 중요한 조언은 이거예요 — 처음부터 20챕터짜리 대작을 만들지 마세요. 맵 하나, 아군 셋, 적 다섯, 승리 조건 하나인 “1챕터 게임”을 끝까지 완성해보는 게, 미완성 대작 열 개보다 백 배 많이 가르쳐줍니다.

이 시리즈의 코드 조각들은 전부 이어 붙이면 돌아가도록 짰으니, 꼭 직접 실행하고 숫자를 바꿔보면서 자기 것으로 만드시길 바랍니다.

일단 오늘은 여기까지….. 다음 글에서는 로드맵의 1번, Tiled 맵 에디터를 정리해볼게요.


← 이전 글: (5/10) 2D SRPG 만들기 선행학습 — 턴제 전투와 상태머신다음 글 →: (7/10) 2D SRPG 만들기 선행학습 — Tiled 맵 에디터로 전장 만들기