대부분의 AAA 게임 전투 시스템은 여전히 수백 개의 하드코딩된 트랜지션으로 얽힌 수동 제작 상태 머신(Finite State Machine)에 의존하고 있으며, 그 한계는 명확합니다. <갓 오브 워>(2018)의 전투 디렉터 미하일 이레스트(Mihail Interest)는 드라우그(Draugr)라는 적 유형 하나를 처리하는 데만 300개가 넘는 행동 노드를 수동으로 튜닝해야 했다고 밝힌 바 있습니다. <갓 오브 워 라그나로크> 수준의 방대한 적 로스터로 이를 확장하는 것은 행동 제작 방식에 근본적인 변화 없이는 불가능했을 것입니다.
ML 기반 전투 AI가 실용화되는 이유
게임 산업은 현재 변곡점에 서 있습니다. NVIDIA의 DLSS 연산 파이프라인, AMD의 FSR 통합 훅, 그리고 PS5의 전용 ML 코어 등을 통한 신경망 추론 하드웨어 가속 덕분에, 60Hz 환경에서 프레임당 AI 의사결정에 필요한 4ms 미만으로 추론 지연 시간을 낮출 수 있게 되었습니다. 2018년만 해도 이는 불가능한 일이었습니다.
더 중요한 점은 ONNXRuntime-GPU나 소니의 내부 ML SDK(유니티의 Sentis나 언리얼의 NNE 플러그인과 유사한 기능)와 같은 라이브러리를 통해 학습된 모델을 게임 빌드에 직접 포함할 수 있게 되었다는 것입니다. 이제 장벽은 하드웨어가 아니라, ML이 결정론적 제어 흐름(deterministic control flow)을 대체하는 것이 아니라 보완하도록 시스템을 올바르게 설계하는 방법입니다.
전투 상태를 강화 학습 환경으로 모델링하기
첫 번째 설계 결정은 관측 공간(observation space)을 정직하게 정의하는 것입니다. <갓 오브 워>의 전투에는 상대적 위치, 적의 체력 비율, 쿨다운 상태, 플레이어 스탠스, 주변 위험 요소 등이 포함됩니다. 이 모든 데이터를 하나의 평면 벡터(flat vector)에 몰아넣는 것은 프로토타이핑 단계에서는 효과적일지 몰라도, 학습 효율을 급격히 떨어뜨립니다.
더 깔끔한 접근 방식은 상태를 타입이 지정된 서브 텐서(sub-tensor)로 구조화하고, 정책 네트워크(policy network)가 별도의 인코딩을 학습하도록 하는 것입니다.
import numpy as np
from dataclasses import dataclass
@dataclass
class CombatObservation:
# 공간 정보: 아레나 경계 [-1, 1]로 정규화된 상대적 위치
enemy_relative_pos: np.ndarray # N명의 적에 대해 shape (N, 2)
player_velocity: np.ndarray # shape (2,)
# 시간 정보: [0, 1] 범위의 쿨다운 비율
ability_cooldowns: np.ndarray # shape (6,) — 능력당 하나
# 체력 / 위협 수준
player_health_ratio: float
enemy_health_ratios: np.ndarray # shape (N,)
def to_policy_input(self) -> dict[str, np.ndarray]:
return {
"spatial": np.concatenate([
self.enemy_relative_pos.flatten(),
self.player_velocity
]),
"state": np.concatenate([
self.ability_cooldowns,
[self.player_health_ratio],
self.enemy_health_ratios
])
}
이러한 분리가 중요한 이유는 네트워크가 상태 브랜치에는 작은 MLP를 사용하고, 위치 브랜치에는 공간 어텐션 인코더(spatial attention encoder)를 사용할 수 있기 때문입니다. 이를 하나의 벡터로 섞어버리면 네트워크가 이 분리 과정을 암시적으로 학습해야 하므로, 제 경험상 학습 단계가 보통 40~60% 더 많이 소요됩니다.
전투의 '맛'을 위한 보상 설계(Reward Shaping)
단순한 승리/패배 보상만으로는 기술적으로는 뛰어나지만 플레이하기엔 짜증 나는 적을 만듭니다. AI는 캠핑을 하거나, 끝없이 카이팅(kiting)을 하거나, 물리 엔진의 허점을 이용하는 법을 배울 것입니다. 수치상으로는 우수해 보일지 몰라도 플레이어에게는 버그처럼 느껴질 뿐입니다.
핵심은 단순한 결과가 아니라 기획자의 의도를 인코딩하도록 보상을 설계하는 것입니다.
def compute_reward(
prev_state: CombatObservation,
curr_state: CombatObservation,
action_taken: int,
hit_confirmed: bool,
player_hit_received: bool
) -> float:
reward = 0.0
# 핵심 목표 보상
reward += 1.5 if hit_confirmed else 0.0
reward -= 2.0 if player_hit_received else 0.0
# 기획 의도: 체력 우위 시 거리 좁히기에 대한 보상
health_delta = (
curr_state.player_health_ratio - prev_state.player_health_ratio
)
if health_delta < -0.05: # 플레이어가 상당한 피해를 입음
closest_enemy_dist = np.min(
np.linalg.norm(curr_state.enemy_relative_pos, axis=1)
)
# 적이 체력 우위에 있을 때 과도한 카이팅 방지
reward -= 0.3 * closest_enemy_dist
# 스킬 난사 방지 — 다양한 액션 시퀀스 유도
# 슬라이딩 윈도우 카운터 등으로 외부에서 추적
if action_taken == prev_state.last_action:
reward -= 0.4 # 동일 동작 반복 억제
return float(np.clip(reward, -5.0, 5.0))
±5.0에서 클리핑(clipping)하는 것은 단순히 미관상의 이유가 아닙니다. 이는 정책이 무작위인 학습 초기 단계에서 그래디언트 폭주(gradient explosion)를 방지합니다. 클리핑이 없을 경우, 적의 수가 4명을 초과할 때 1만 스텝 이내에 학습이 발산해버리는 것을 자주 목격했습니다.
학습된 정책을 결정론적 게임 로직과 통합하기
적의 행동을 직접 제어하는 순수 RL 정책은 학습 분포의 경계에서 예측 불가능한 결과를 초래할 수 있습니다. 더 안전하고 실제 제작 환경에서 주로 쓰이는 패턴은, 정책을 사용하여 후보 액션들의 점수를 매기고 결정론적 시스템이 게임플레이 유효성에 따라 이를 필터링하는 방식입니다.
import onnxruntime as ort
class EnemyCombatController:
def __init__(self, model_path: str):
self.session = ort.InferenceSession(
model_path,
providers=["CUDAExecutionProvider", "CPUExecutionProvider"]
)
self.action_space = [
"light_attack", "heavy_attack", "dodge_left",
"dodge_right", "block", "special_ability"
]
def select_action(
self,
obs: CombatObservation,
valid_actions: list[str] # 게임 로직에서 주입 (예: 쿨다운 제한)
) -> str:
policy_input = obs.to_policy_input()
# 추론 실행 — RTX 3060 이상에서 2ms 미만 목표
logits = self.session.run(
output_names=["action_logits"],
input_feed={
"spatial": policy_input["spatial"].reshape(1, -1).astype(np.float32),
"state": policy_input["state"].reshape(1, -1).astype(np.float32)
}
)[0].flatten()
# 유효하지 않은 액션 마스킹 — 스킬 쿨다운 처리에 필수적
action_mask = np.array([
1.0 if a in valid_actions else -np.inf
for a in self.action_space
])
masked_logits = logits + action_mask
# 소프트맥스에서 샘플링 — 결정론적 argmax는 로봇처럼 느껴짐
probs = np.exp(masked_logits) / np.sum(np.exp(masked_logits))
chosen_idx = np.random.choice(len(self.action_space), p=probs)
return self.action_space[chosen_idx]
액션 마스킹 단계는 많은 구현체들이 간과하는 부분입니다. 이를 생략하면 정책이 가끔 마스킹된 액션을 출력하게 되고, 플레이어는 쿨다운 중인 스킬을 쓰려고 시도하는 적을 보게 됩니다. 이는 보이지 않는 실패(액션이 조용히 무시됨)나 QA에서 재현하기 매우 힘든 물리적 글리치를 유발합니다.
학습 인프라: 대규모 병렬 셀프 플레이
단일 학습 환경은 초당 약 800스텝 정도를 제공합니다. <갓 오브 워> 급의 전투 복잡도에서 합리적인 반복 주기 내에 모든 적 유형을 처리하는 정책으로 수렴하려면 초당 50,000스텝에 가까운 속도가 필요합니다. 즉, Ray RLlib이나 SB3의 VecEnv 래퍼를 사용한 병렬화된 환경이 필수적입니다.
from stable_baselines3 import PPO
from stable_baselines3.common.vec_env import SubprocVecEnv
from stable_baselines3.common.callbacks import EvalCallback
import gymnasium as gym
def make_env(env_id: str, rank: int, seed: int = 42):
"""팩토리 클로저 — 각 워커는 고유한 시드가 부여된 환경을 가짐"""
def _init():
env = gym.make(env_id)
env.reset(seed=seed + rank)
return env
return _init
if __name__ == "__main__":
N_ENVS = 16 # 16개 병렬 워커 — 8코어 CPU에서 약 40k steps/sec까지 확장
vec_env = SubprocVecEnv([
make_env("GodOfWarCombat-v1", rank=i)
for i in range(N_ENVS)
])
model = PPO(
policy="MultiInputPolicy", # 딕셔너리 관측 공간 처리
env=vec_env,
n_steps=2048,
batch_size=512,
n_epochs=10,
learning_rate=3e-4,
clip_range=0.2, # 표준 PPO 클리핑
ent_coef=0.01, # 엔트로피 보너스로 조기 수렴 방지
verbose=1,
tensorboard_log="./tb_logs/"
)
eval_callback = EvalCallback(
gym.make("GodOfWarCombat-v1"),
eval_freq=10_000,
n_eval_episodes=20,
best_model_save_path="./models/best/"
)
model.learn(total_timesteps=10_000_000, callback=eval_callback)
model.save("combat_policy_final")
엔트로피 계수(ent_coef=0.01)에 주목하십시오. 이 값을 너무 낮게 설정하면 정책이 학습 초기에 좁은 액션 분포에 고착되어 버립니다. 그러면 특정 패턴에는 매우 능숙하지만 다른 상황에서는 취약한 정책이 만들어집니다. 예를 들어, 항상 왼쪽으로만 회피하는 적이 만들어져 플레이어에게 즉시 공략당하게 됩니다.
적 난이도 조절을 위한 커리큘럼 학습(Curriculum Learning)
처음부터 최고 난이도의 플레이어 에이전트를 상대로 학습하면 수렴 속도가 매우 느립니다. 더 나은 접근 방식은 커리큘럼 학습입니다. 처음에는 수동적인 플레이어로 시작하여, 적 정책이 향상됨에 따라 플레이어의 능력을 점진적으로 높이는 것입니다. 적 정책의 승률을 추적하여 현재 플레이어 수준에서 60%를 초과하면 다음 커리큘럼 단계로 넘어갑니다. 이 방식은 고정 난이도 학습에 비해 수렴 시간을 보통 35~45% 단축시킵니다.
실무 권장 사항
중급 규모 이상의 액션 게임을 위한 전투 AI를 구축한다면, 현재 바로 적용 가능한 아키텍처는 다음과 같습니다. PPO로 학습된 정책을 ONNX로 내보내고, ONNXRuntime-GPU를 통해 로드하여 매 프레임이 아닌 3~5 게임 프레임마다 추론을 실행합니다. 이때 결정론적 게임 로직이 쿨다운 제어와 애니메이션 잠금(locking)을 처리하도록 합니다. SB3의 SubprocVecEnv를 사용하여 8~16개의 병렬 환경에서 학습시키고, 보상은 단순한 결과보다는 기획 의도에 맞춰 명시적으로 설계하며, 첫날부터 마스킹된 액션 공간으로 검증하십시오. ML 레이어는 전술적 의사결정을 담당하고, 기존 FSM은 애니메이션 및 물리적 보장(guarantee)을 담당합니다. 결정론적 시스템을 ML로 교체하려 하지 말고, 확장하십시오. 그 경계선이야말로 실무에 적합한 전투 AI가 존재하는 곳입니다.