· 9 min readOOP

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

#pokemon-oop

image

Trigger

지난 글에서는 Number를 봤다. 어떤 변화가 일어나는지뿐 아니라, 그 변화가 얼마나 큰지도 객체로 나눠 볼 수 있다는 이야기였다.

그런데 포켓몬 배틀은 여기서 또 멈추지 않는다.

어떤 일이 일어났다면, 그걸 보고 누가 반응하는가?

이 질문이 바로 6편의 주제다.

예를 들어 피카츄가 상대를 몸통박치기 같은 접촉 기술로 때렸다고 해보자. 기술 자체의 Effect는 이미 끝났을 수 있다. 데미지도 들어갔고, Event도 남았다. 그런데 상대 특성이 정전기라면 이야기가 거기서 끝나지 않는다. "접촉이 일어났다"는 사실을 보고 추가로 마비를 걸 수도 있기 때문이다.

생명의구슬도 비슷하다. 기술 효과가 먼저 끝나고, 공격이 성공했다는 사실이 남는다. 그 다음에야 도구가 반응해서 사용자에게 반동 데미지를 준다. 기합의띠는 반대로 빈사 직전 상황을 보고, 체력이 가득 찬 상태였다면 HP 1로 버티게 만든다.

지난 글들의 표현으로 줄이면 이렇다.

Effect = 상태를 바꾼다
Event  = 무슨 일이 일어났는지 남긴다
Trigger = 그 Event를 보고 반응한다

왜 Trigger가 필요한가

처음에는 이런 반응도 그냥 기술 로직 안에 넣고 싶어진다.

예를 들어 "접촉 기술이면 정전기 체크", "공격 성공이면 생명의구슬 반동", "HP가 0 이하가 되려 하면 기합의띠 체크" 같은 걸 전부 공격 처리 함수 안에 몰아넣는 식이다.

그런데 그렇게 가면 곧바로 이상해진다. 기술 하나를 처리하는 함수가 기술 자체 효과만 아는 게 아니라, 상대 특성, 사용자 도구, 날씨, 필드, 교체 규칙, 생존 판정까지 전부 알아야 하기 때문이다.

예를 들어 플레어드라이브를 생각해보면 더 분명하다. 이 기술은 원래도 복잡하다. 상대에게 데미지를 주고, 일정 확률로 화상을 입히고, 마지막에 반동 데미지를 받는다. 여기에 상대 특성이 까칠한피부라면 접촉 반사 데미지가 추가될 수 있고, 사용자 도구가 생명의구슬이라면 또 다른 반동이 붙을 수 있다. 상대가 기합의띠를 들고 있었다면 생존 판정도 중간에 끼어든다.

이걸 전부 기술 하나의 Effect 안에 집어넣으면, 그 순간 기술 구현은 더 이상 기술 구현이 아니게 된다.

그래서 이런 반응도 기술 본체와 분리해서 보는 편이 훨씬 낫다.

기술은 자기 Effect를 실행한다
배틀은 Event를 남긴다
다른 규칙은 그 Event를 보고 각자 반응한다

이 분리가 있어야 기술, 특성, 아이템, 필드 효과가 서로 덜 엉킨다.

Trigger

화이트보드에 간단히 그리면 이런 구조를 떠올릴 수 있다.

classDiagram
    class ITrigger {
        +CanHandle(IEvent event, Battle battle) bool
        +React(IEvent event, Battle battle) void
    }

    class ContactTrigger
    class DamageTrigger
    class FaintTrigger
    class SurvivalTrigger

    ITrigger <|-- ContactTrigger
    ITrigger <|-- DamageTrigger
    ITrigger <|-- FaintTrigger
    ITrigger <|-- SurvivalTrigger

이름은 조금씩 달라질 수 있지만, 핵심은 대체로 비슷하다.

어떤 Trigger는 DamageDealt를 보고 반응하고, 어떤 Trigger는 ContactHappened를 보고 반응하고, 어떤 Trigger는 BattlerFainted나 "빈사 직전" 같은 상황을 보고 반응한다.

2편의 Condition이 "지금 실행 가능한가?"를 묻는 역할이었다면, Trigger는 "이 Event가 생겼을 때 내가 반응할 차례인가?"를 가려내는 역할이라고 볼 수 있다.

Event를 보고 반응하기

4편에서 Event를 남겼던 이유가 여기서 더 분명해진다.

단순히 HP가 줄었다는 사실만으로는 부족하다. 누가 누구를 때렸는지, 접촉이었는지, 데미지가 실제로 들어갔는지, 누가 쓰러졌는지 같은 정보가 있어야 다른 규칙이 올바르게 반응할 수 있기 때문이다.

이 흐름을 아주 단순하게 그리면 이렇다.

flowchart LR
    A["기술 Effect 실행"] --> B["Event 발생"]
    B --> C["Trigger 후보들 확인"]
    C --> D["반응할 Trigger만 선택"]
    D --> E["추가 Effect 실행"]

이제 예시를 붙이면 감이 빨리 온다.

정전기는 "접촉이 있었는가?"를 보고 반응한다.

접촉 Event 발생
= 정전기 Trigger가 반응
= 공격자에게 마비 Effect 적용 가능

생명의구슬은 "공격이 성공해서 실제 데미지가 들어갔는가?"를 보고 반응한다.

DamageDealt Event 발생
= 생명의구슬 Trigger가 반응
= 사용자에게 반동 데미지 적용

기합의띠는 조금 다르다. 공격이 성공한 뒤, 체력이 가득 찬 대상이 그대로 쓰러지려는 순간을 가로채는 타이밍에 작동한다.

빈사 직전 상황 발생
= 기합의띠 Trigger가 반응
= 풀피였다면 HP를 1로 남기고 생존

Trigger는 새로운 기술 종류라기보다, 이미 일어난 일 뒤에 붙는 후속 반응이라고 보면 된다.

누가 Trigger를 가지는가

실제 배틀에서는 Trigger가 한곳에만 붙어 있지 않다.

어떤 것은 Battler가 가진다. 특성과 도구가 여기에 해당한다. 정전기, 까칠한피부, 기합의띠, 생명의구슬은 특정 포켓몬이 들고 있거나 가진 능력이므로, 각 Battler 주변에 붙는 규칙처럼 보는 편이 자연스럽다.

어떤 것은 Battle이 가진다. 날씨, 필드, 룸 같은 전장 전체 규칙이 그렇다. 예를 들어 특정 날씨에서 기술 위력이 달라지거나, 턴 끝에 추가 효과가 일어나는 것은 개별 포켓몬 하나의 반응이라기보다 배틀 전체의 반응으로 처리된다.

그래서 구조를 그리면 보통 이렇게 나뉜다.

flowchart TB
    A["Trigger"] --> B["Battler에 붙는 Trigger"]
    A --> C["Battle에 붙는 Trigger"]

    B --> D["특성"]
    B --> E["도구"]

    C --> F["날씨"]
    C --> G["필드 효과"]
    C --> H["턴 규칙"]

이 구분이 중요한 이유는 책임 범위를 나누기 쉽기 때문이다.

정전기는 공격자와 방어자 중 누가 접촉했는지를 알아야 하므로 Battler에 붙는 게 자연스럽다. 반대로 턴 종료 시 날씨 데미지를 주는 규칙은 특정 한 포켓몬의 능력이라기보다 배틀 전체 흐름이 알아야 하는 반응이다.

정전기

정전기는 Trigger를 설명할 때 가장 보기 좋은 예시 중 하나다.

플레이어 입장에서는 "접촉하면 가끔 마비 걸리는 특성" 한 줄이다. 그런데 시스템 입장에서는 그 한 줄 안에 꽤 많은 조건이 숨어 있다.

접촉 기술이었는가? 실제로 공격이 성립했는가? 상대가 이미 다른 상태이상은 아닌가? 전기 타입이나 특정 면역 규칙은 없는가?

정전기는 그냥 "맞으면 마비"가 아니라, 접촉 관련 Event를 받아 조건을 다시 확인한 뒤 반응하는 Trigger다.

flowchart LR
    A["접촉 Event"] --> B["정전기 Trigger"]
    B --> C["추가 조건 확인"]
    C --> D["마비 Effect"]

짧게 쓰면 이런 식이다.

접촉 Event
+ 정전기 소유자
+ 마비 가능 조건 통과
= 공격자에게 Paralyze

여기서 좋은 점은 정전기가 기술 이름을 몰라도 된다는 점이다. 전광석화플레어드라이브든, 심지어 다른 세대에서 새로운 접촉 기술이 추가되든, 중요한 건 "접촉 Event가 있었는가"다.

즉 Trigger는 기술 이름보다 "무슨 일이 일어났는가"를 더 중요하게 본다.

까칠한피부와 생명의구슬

이 둘을 같이 보면 Trigger가 더 선명해진다.

까칠한피부는 상대가 나를 접촉으로 때렸다는 사실을 보고 반응한다. 반면 생명의구슬은 내가 공격에 성공했다는 사실을 보고 반응한다.

겉으로 보면 둘 다 "추가 데미지"지만, 기준 Event가 다르다.

flowchart LR
    A["접촉 Event"] --> B["까칠한피부"]
    B --> C["공격자에게 반사 데미지"]

    D["DamageDealt Event"] --> E["생명의구슬"]
    E --> F["사용자에게 반동 데미지"]

이 차이를 분리해두면 구조가 아주 깨끗해진다.

까칠한피부는 접촉 여부를 보면 되고, 생명의구슬은 실제 데미지 성공 여부를 보면 된다. 둘 다 "추가 데미지"를 만든다는 점은 같지만, 왜 그 데미지가 발생했는지는 서로 다른 Event가 설명해준다.

기합의띠

기합의띠는 앞의 예시들보다 조금 더 흥미롭다. 이 경우는 공격이 끝난 뒤에 단순히 추가 데미지를 더하는 게 아니라, 쓰러짐 자체를 막아버리기 때문이다.

그래서 이 경우에는 "누가 누구를 때렸는가"보다 "지금 이 포켓몬이 쓰러지려 하는가"가 더 중요하다.

flowchart LR
    A["큰 데미지 발생"] --> B["HP 0 이하 예정"]
    B --> C["기합의띠 Trigger"]
    C --> D["HP 1로 보정"]
    D --> E["생존"]

이런 규칙을 기술 쪽에 넣어두면 거의 모든 공격 기술이 기합의띠를 알아야 한다. 하지만 Trigger로 빼두면 공격 기술은 그냥 데미지를 만들기만 하면 되고, 생존 반응은 도구가 책임질 수 있다.

이게 Trigger 분리의 가장 큰 장점 중 하나다.

기술은 자기 역할만 하고, 반응은 반응하는 규칙이 맡는다.

처리 순서

여기까지 오면 자연스럽게 또 하나의 문제가 나온다.

Trigger가 여러 개면 어떤 순서로 처리할까?

실제 포켓몬 배틀은 이 순서가 꽤 중요하다. 누가 먼저 반응하느냐에 따라 생존 여부가 바뀌기도 하고, 로그 순서도 달라지고, 뒤에 이어질 Trigger가 달라지기도 한다.

예를 들어 접촉 공격 하나에 이런 것들이 한꺼번에 걸릴 수 있다.

기술 자체의 추가 효과
까칠한피부
정전기
생명의구슬
기절 판정

그래서 Trigger 시스템은 보통 "반응할 것들을 모은 다음, 정해진 순서대로 처리한다"는 구조를 가진다. 다만 실제 포켓몬은 세대별 규칙과 세부 판정에 따라 순서가 더 복잡할 수 있으니, 여기서는 개념적인 흐름을 단순화해서 본다고 생각하면 된다.

flowchart TB
    A["Event 발생"] --> B["반응 가능한 Trigger 수집"]
    B --> C["우선순위 정렬"]
    C --> D["하나씩 실행"]
    D --> E["새 Event 발생 가능"]
    E --> F["다음 Trigger 확인"]

이 구조가 필요한 이유는 Trigger도 또 다른 Event를 만들 수 있기 때문이다.

예를 들어 생명의구슬 반동으로 사용자가 쓰러질 수도 있다. 그러면 거기서 다시 쓰러짐 관련 Trigger나 후속 처리로 이어질 수 있다. 즉 Event가 Trigger를 부르고, Trigger가 다시 Effect를 실행하고, 그 Effect가 또 Event를 남기는 흐름이 생긴다.

그래서 배틀 시스템은 "한 번의 거대한 함수 호출"보다는 "작은 단계가 연쇄적으로 이어지는 흐름"에 더 가까워 보인다.

이번 글 정리

지금까지 본 예시를 짧게 다시 묶으면 이렇다.

정전기
= Contact Event를 보고
+ 조건이 맞으면 공격자에게 Paralyze

까칠한피부
= Contact Event를 보고
+ 공격자에게 Damage

생명의구슬
= DamageDealt Event를 보고
+ 사용자에게 Damage

기합의띠
= 빈사 직전 상황을 보고
+ 풀피였다면 HP를 1로 보정

이쯤 되면 시리즈의 흐름도 다시 이어진다.

1편에서는 누가 무엇을 시도하는지를 나눴고, 2편에서는 실행 가능한지를 판단했고, 3편에서는 실제 상태 변화를 만들었고, 4편에서는 그 결과를 기록했고, 5편에서는 그 변화량을 숫자로 만들었다. 그리고 이번 6편에서는 그 결과를 본 다른 규칙들이 어떻게 끼어드는지를 봤다.

결국 포켓몬 배틀처럼 규칙이 많고 예외가 많은 도메인에서는 "기술이 모든 걸 안다"는 모델이 오래 버티기 어렵다. 그보다 기술은 자기 Effect를 실행하고, 배틀은 Event를 남기고, 특성이나 도구 같은 다른 규칙은 Trigger로 반응하는 구조가 훨씬 자연스럽다.

Share:

Comments