· 10 min readOOP

OOP로 이해하는 포켓몬 배틀 시스템(7)

#pokemon-oop

image

Battle Loop

지난 글에서는 Trigger를 봤다. 어떤 일이 일어났을 때, 그 사실을 보고 특성이나 도구, 전장 규칙이 어떻게 반응하는지를 살펴봤다.

그런데 여기까지 오면 자연스럽게 마지막 질문이 남는다.

이 모든 것들이 실제 한 턴 안에서 어떤 순서로 돌아갈까?

지금까지 시리즈에서는 Condition, Effect, Event, Number, Trigger를 따로따로 떼어 봤다. 각각은 따로 놓고 보면 충분히 납득된다. 문제는 실제 배틀에서는 이 요소들이 따로 떨어져 움직이지 않는다는 점이다. 한 턴이 시작되면 입력을 받고, 행동 순서를 정하고, 실행 가능 여부를 확인하고, 상태를 바꾸고, 이벤트를 남기고, 반응을 처리한 뒤, 턴 종료 효과까지 이어진다.

즉, 마지막으로 남는 건 "조각들의 설명"이 아니라 "조각들이 실제로 이어지는 흐름"이다.

이 글에서는 그 흐름을 정리해 본다.

짧게 줄이면 이렇다.

Turn 시작
= 입력 수집
= 행동 순서 결정
= Attempt 실행
= Event와 Trigger 처리
= 턴 종료 처리
= 다음 Turn

왜 Battle Loop가 필요한가

처음 구현할 때는 공격 하나를 함수로 만들고, 그 안에서 일어나는 일을 차례대로 적고 싶어진다.

예를 들어 "기술 선택", "명중 판정", "데미지 계산", "상태이상", "특성 반응", "도구 반응", "기절 판정", "턴 종료 처리"를 한 함수 안에 전부 밀어 넣는 식이다.

그런데 그렇게 가면 곧바로 함수 하나가 너무 많은 책임을 갖게 된다. 기술 하나를 처리하는 로직이 턴 전체 흐름, 반응 순서, 로그 타이밍, 교체 규칙까지 전부 알아야 하기 때문이다.

반대로 배틀을 루프로 보면 각 조각이 들어갈 자리가 또렷해진다.

Condition은 실행 전에, Effect는 실행 중에 개입하고, Event는 그 결과를 남기며, Trigger는 그 이벤트를 보고 반응한다. Number는 그 사이사이에서 필요한 값을 계산해 준다.

Battle Loop는 새로운 규칙이 아니라, 지금까지 본 규칙들이 실제로 어떤 순서로 돌아가는지 보여주는 흐름이다.

한 턴의 큰 흐름

먼저 전체 그림부터 보면 이렇다.

flowchart TB
    A["Turn 시작"] --> B["플레이어 입력 수집"]
    B --> C["Attempt 만들기"]
    C --> D["행동 순서 결정"]
    D --> E["Attempt 하나 실행"]
    E --> F["Condition 확인"]
    F --> G["Effect 실행"]
    G --> H["Event 기록"]
    H --> I["Trigger 처리"]
    I --> J["다음 Attempt 또는 턴 종료"]
    J --> K["턴 종료 효과"]
    K --> L["다음 Turn"]

이 그림이 중요한 이유는, 지금까지 나온 객체들이 어디에 들어가는지가 한 번에 보이기 때문이다.

2편의 Condition은 실행 직전에 들어간다. 3편의 Effect는 실제 상태를 바꾼다. 4편의 Event는 무슨 일이 일어났는지 남긴다. 5편의 Number는 그 변화량을 계산해 준다. 6편의 Trigger는 그 이벤트를 보고 다시 반응한다.

즉 배틀 시스템은 "행동 하나를 호출하면 끝"이 아니라, 작은 단계들이 순서대로 이어지는 흐름으로 보이기 시작한다.

입력은 Attempt가 된다

배틀은 보통 플레이어 입력부터 시작한다. 플레이어 입장에서는 그저 기술을 선택하거나 교체를 고를 뿐이지만, 시스템 입장에서는 그 선택을 곧바로 실행하지 않는다. 먼저 "이번 턴에 시도할 행동"으로 바꿔 놓는다.

이 시점에서 1편의 MoveAttempt가 다시 등장한다.

flowchart LR
    A["플레이어 입력"] --> B["Move 선택"]
    B --> C["Attempt 생성"]
    C --> D["실행 대기열에 추가"]

이 구조가 필요한 이유는, 배틀이 입력을 받자마자 바로 실행되는 게임이 아니기 때문이다. 두 포켓몬의 선택을 모은 뒤, 누가 먼저 행동하는지 정하고, 그 순서에 따라 하나씩 처리해야 한다.

그래서 Attempt는 "지금 당장 실행된 행동"이라기보다 "곧 실행될 행동"을 의미한다.

행동 순서 정하기

그 다음은 순서다.

포켓몬 배틀은 같은 턴 안에서도 누가 먼저 움직이느냐가 중요하다. 우선도, 스피드, 특정 규칙이 모두 여기에 얽힌다. 이 편에서는 세부 공식까지 깊게 들어가지는 않겠지만, 최소한 배틀 루프 안에서 순서 결정이 어디에 들어가는지는 보는 게 좋다.

flowchart LR
    A["Attempt 목록"] --> B["우선도 비교"]
    B --> C["스피드 비교"]
    C --> D["실행 순서 확정"]

이걸 따로 떼어 두는 이유는 분명하다. 그래야 기술 효과와 순서 결정을 섞지 않을 수 있다.

예를 들어 전광석화가 먼저 나가는 이유는 데미지 공식 때문이 아니라 우선도 규칙 때문이다. 즉 어떤 기술이 얼마나 아픈지와, 어떤 행동이 먼저 처리되는지는 같은 문제가 아니다.

Attempt 하나는 어떻게 실행되나

순서가 정해지면 이제 그 턴의 첫 번째 Attempt를 실행한다.

여기서부터 지금까지 시리즈가 가장 직접적으로 이어진다.

flowchart LR
    A["Attempt 실행 시작"] --> B["Condition 확인"]
    B --> C["실행 가능"]
    C --> D["필요한 Number 계산"]
    D --> E["Effect 실행"]
    E --> F["상태 변화"]
    F --> G["Event 기록"]
    G --> H["Trigger 처리"]

글로 풀면 이렇다.

먼저 이 행동이 정말 실행 가능한지 본다. 마비 때문에 행동 불능은 아닌지, PP가 남았는지, 대상이 면역은 아닌지 같은 것들이 여기서 걸린다. 이 단계는 2편의 Condition이 맡는다.

통과했다면 실제 상태를 바꾼다. HP를 깎거나, 상태이상을 걸거나, 랭크를 바꾸거나, 날씨를 바꾼다. 이건 3편의 Effect다.

그 과정에서 숫자가 필요하면 5편의 Number가 계산을 맡는다. 단순 고정값일 수도 있고, 위력과 공격/방어 비율, 상성, 랜덤 보정이 섞인 공식일 수도 있다.

그리고 실행이 끝나면 4편의 Event가 남는다. 누가 누구를 때렸는지, 실제 데미지가 들어갔는지, 누가 쓰러졌는지 같은 사실들이 여기 기록된다.

마지막으로 6편의 Trigger가 그 이벤트를 보고 반응한다. 특성, 도구, 날씨, 필드 효과가 여기서 움직인다.

결국 한 번의 행동은 작은 단계들이 이어진 한 묶음이라고 볼 수 있다.

가장 단순한 예시: 10만 볼트

먼저 비교적 단순한 턴부터 보자.

피카츄가 10만 볼트를 쓰고, 상대는 별다른 반응 특성이나 도구가 없다고 해보자.

그러면 흐름은 대략 이렇게 보인다.

1. 피카츄가 10만 볼트를 선택한다
2. Attempt가 만들어진다
3. 행동 순서가 정해진다
4. Condition을 확인한다
5. 데미지 Number를 계산한다
6. Effect가 상대 HP를 깎는다
7. DamageDealt Event가 남는다
8. 추가 효과가 붙으면 StatusInflicted Event가 남는다
9. 반응할 Trigger가 없으면 다음 단계로 간다

플레이어 입장에서는 "10만 볼트 맞았네" 정도로 지나갈 수 있지만, 시스템 입장에서는 그 한 줄이 이미 꽤 많은 단계로 쪼개진다.

하지만 각 단계의 책임이 분리되어 있으면, 복잡성도 의외로 잘 관리된다.

조금 복잡한 예시: 플레어드라이브

이제 정말 포켓몬다운 장면을 생각해보자.

리자몽이 플레어드라이브를 썼고, 상대 특성은 까칠한피부이며, 리자몽은 생명의구슬을 들고 있다고 해보자. 여기에 상대가 기합의띠까지 들고 있다면 더 재미있다.

플레이어 입장에서는 그냥 "세게 때렸는데 이것저것 많이 터졌다" 정도로 보인다. 하지만 시스템 입장에서는 그 순서가 굉장히 중요하다.

아래 흐름은 개념을 설명하기 위해 단순화한 예시다. 실제 포켓몬의 세대별 처리 순서는 더 세밀하게 갈릴 수 있지만, 여기서는 "기술 효과 뒤에 후속 반응이 연쇄적으로 붙는다"는 구조를 보는 데 집중한다.

flowchart TB
    A["플레어드라이브 Attempt"] --> B["Condition 통과"]
    B --> C["데미지 Number 계산"]
    C --> D["상대에게 Damage Effect"]
    D --> E["DamageDealt Event"]
    E --> F["기합의띠 확인(상대가 풀피였다면)"]
    F --> G["상대 생존 여부 확정"]
    G --> H["까칠한피부 Trigger"]
    H --> I["공격자 반사 데미지"]
    I --> J["생명의구슬 Trigger"]
    J --> K["공격자 반동 데미지"]

이걸 문장으로 풀면 훨씬 또렷하다.

먼저 플레어드라이브 자체가 상대에게 데미지를 준다. 이때 5편에서 본 데미지 공식이 돌아간다. 그러고 나면 DamageDealt 같은 이벤트가 남는다.

그 다음 상대가 기합의띠를 들고 있었고, 공격을 맞기 전 체력이 가득 찬 상태였다면, 쓰러질 상황을 보고 생존 Trigger가 끼어들 수 있다. 즉 상대는 HP 1로 버틸 수 있다.

그 다음 접촉 공격이었다는 사실을 보고 까칠한피부가 반응한다. 그러면 이번엔 공격자 쪽 HP가 깎인다.

그리고 실제 공격이 성공했다는 사실을 보고 생명의구슬이 반응한다. 그러면 공격자는 또 한 번 반동 데미지를 받는다.

여기까지 오면 한 번의 공격이 사실상 "기술 자체 효과 + 여러 후속 반응"의 연쇄라는 점이 또렷해진다.

중요한 건 이 모든 걸 플레어드라이브라는 기술 하나가 전부 알고 있을 필요는 없다는 점이다.

턴 종료 처리

한 턴은 행동이 다 끝났다고 바로 끝나지 않는다.

포켓몬 배틀에는 턴 종료에 따로 처리되는 것들이 많다. 날씨 데미지, 상태이상 데미지, 지속 효과, 남은 카운트 감소 같은 것들이 이 단계에서 처리된다.

그래서 배틀 루프는 보통 "모든 행동 실행"에서 끝나지 않고, 마지막에 턴 종료 단계가 한 번 더 붙는다.

flowchart LR
    A["모든 Attempt 실행 완료"] --> B["턴 종료 Effect"]
    B --> C["턴 종료 Event"]
    C --> D["턴 종료 Trigger"]
    D --> E["카운트 감소 및 다음 Turn 준비"]

이 단계를 따로 두는 이유는, 턴 도중 반응과 턴 종료 반응이 같은 종류가 아니기 때문이다.

예를 들어 화상 데미지는 "공격 성공 직후"가 아니라 "턴이 끝날 때" 들어가는 편이 맞다. 모래바람 데미지도 마찬가지다. 이런 것들을 전부 기술 실행 중간에 섞어 넣으면 흐름이 금방 뒤틀린다.

그래서 배틀 루프는 보통 이렇게 두 층을 가진다.

행동 중에 일어나는 처리
턴이 끝날 때 일어나는 처리

이 구분 하나만 있어도 엔진이 훨씬 덜 꼬인다.

결국 Battle은 오케스트라 지휘자 역할을 한다

여기까지 오면 1편에서 봤던 Battle의 역할이 다시 또렷해진다.

Battle은 직접 모든 규칙을 계산하는 거대한 객체라기보다, 여러 규칙이 순서대로 흘러가도록 만드는 조율자에 가깝다.

flowchart TB
    A["Battle"] --> B["입력 받기"]
    A --> C["Attempt 순서 정하기"]
    A --> D["Condition 실행"]
    A --> E["Effect 실행"]
    A --> F["Event 기록"]
    A --> G["Trigger 처리"]
    A --> H["턴 종료 처리"]

Battle이 중요한 이유는 "모든 걸 혼자 안다"가 아니라, "각 조각이 어느 타이밍에 움직여야 하는지 조율한다"는 점이다.

그래서 앞편에서 조각들을 잘 쪼개 둘수록 마지막 루프는 오히려 단순해진다. Condition은 질문을 맡고, Effect는 변화를 만들고, Number는 값을 계산하고, Event는 사실을 기록하고, Trigger는 반응을 맡는다. Battle은 그 전부를 순서대로 이어 붙인다.

마무리

처음 영상을 보고 이 시리즈를 써 보고 싶다고 생각했을 때만 해도, 그냥 "포켓몬 배틀 시스템을 OOP로 보면 재밌겠다" 정도였다. 막상 쪼개어 보니 그보다 훨씬 더 흥미로웠다. 플레이어에게는 한 턴이 짧은 애니메이션처럼 보이지만, 시스템 입장에서는 질문하고, 바꾸고, 기록하고, 반응하고, 정리하는 작은 단계들의 연쇄로 이루어져 있다.

그래서 결국 포켓몬 배틀 시스템을 OOP로 본다는 건 기술 하나를 거대한 함수로 구현하는 일이 아니라, 배틀을 이루는 요소들을 작은 객체와 단계로 나눠 읽어내는 과정이다.

시리즈 마지막을 한 줄로 줄이면 이런 느낌일 것 같다.

배틀은 복잡한 규칙의 집합이 아니라,
작은 객체들이 순서대로 대화하는 흐름으로 볼 수 있다
Share:

Comments