ML-Agents로 학습시키는 보스 몬스터 AI

ML-Agents로 학습시키는 보스 몬스터 AI

강화학습의 기본 구조부터 Unity ML-Agents로 보스 몬스터를 학습시키는 실전 설계를 단계별로 정리합니다. 관측·행동·보상 설계와 학습 실패를 줄이는 디버깅 방법까지 다룹니다.

보스 AI에 강화학습을 적용할 때

보스 몬스터 AI는 단순히 플레이어를 추적하는 수준을 넘어 거리 조절, 공격 선택, 위험 회피, 패턴 전환을 함께 판단해야 합니다. 상태 머신과 행동 트리는 의도를 명확하게 구현하기 좋지만 수치 조정과 예외 처리의 양이 빠르게 늘어날 수 있습니다.

강화학습은 보스가 관측한 게임 상태에서 행동을 선택하고 그 결과에 따른 보상을 통해 정책을 개선하는 방식입니다. 다만 재미있는 보스를 자동으로 만들어 주는 도구는 아닙니다. 어떤 행동을 관찰하게 할지 무엇을 보상할지 어느 범위까지 학습에 맡길지를 게임 디자이너와 개발자가 명확히 정해야 합니다.

이 글에서는 Unity ML-Agents를 기준으로 근접 공격과 돌진 공격을 가진 간단한 보스 AI를 구성합니다. 학습 결과는 공격 타이밍을 전부 대체하는 용도보다 전투 중 위치 선정과 공격 선택에 활용하는 편이 관리하기 좋습니다.

문제를 작은 전투 단위로 나누기

처음부터 여러 페이즈와 복잡한 스킬을 모두 학습시키면 보상이 희소해지고 원인을 추적하기 어려워집니다. 먼저 한 개의 작은 경기장 안에서 다음 목표만 달성하도록 만드세요.

  • 보스는 플레이어에게 일정 거리까지 접근한다.
  • 공격 사거리 안에서는 근접 공격 또는 돌진 공격을 선택한다.
  • 공격이 빗나가거나 너무 오래 멀어지면 손해를 본다.
  • 플레이어 또는 보스의 체력이 0이 되면 에피소드가 끝난다.
flowchart LR
    A[환경 초기화] --> B[보스가 상태 관측]
    B --> C[정책이 행동 선택]
    C --> D[이동·공격 실행]
    D --> E[피해·거리·시간으로 보상 계산]
    E --> F{에피소드 종료?}
    F -- 아니오 --> B
    F -- 예 --> A

이 구조에서 환경은 경기장, 플레이어, 보스, 체력, 판정 규칙을 포함합니다. 에이전트는 보스이고 정책은 관측값을 행동으로 바꾸는 신경망입니다. 학습은 같은 환경을 여러 개 동시에 실행할수록 일반적으로 더 많은 경험을 빠르게 모을 수 있습니다.

관측값: 보스가 알아야 할 정보만 제공하기

관측값은 보스가 판단에 쓰는 입력입니다. 플레이어와 보스의 절대 좌표를 그대로 넣을 수도 있지만 상대 위치와 현재 방향처럼 행동에 직접 연결되는 정보가 학습하기 쉽습니다. 모든 연속 값은 가능한 한 비슷한 범위로 정규화하세요.

예를 들어 XZ 평면 전투에서는 다음 정보로 시작할 수 있습니다.

  • 보스 기준 플레이어의 상대 위치와 거리
  • 보스의 전방 방향에 대한 플레이어 방향
  • 보스와 플레이어의 현재 속도
  • 각자의 체력 비율
  • 현재 공격의 재사용 대기시간 비율
  • 최근 공격 중인지 여부

CollectObservations에서는 VectorSensor에 값을 추가합니다. 회전과 속도는 월드 좌표보다 보스의 로컬 좌표로 바꾸면 방향에 덜 민감한 정책을 만들 수 있습니다.

using Unity.MLAgents;
using Unity.MLAgents.Sensors;
using UnityEngine;

public class BossAgent : Agent
{
    [SerializeField] private Transform player;
    [SerializeField] private Rigidbody body;
    [SerializeField] private float arenaRadius = 12f;
    [SerializeField] private float maxSpeed = 5f;

    public float BossHealth01 { get; private set; } = 1f;
    public float PlayerHealth01 { get; private set; } = 1f;
    public float DashCooldown01 { get; private set; }

    public override void CollectObservations(VectorSensor sensor)
    {
        Vector3 localOffset = transform.InverseTransformDirection(
            player.position - transform.position);
        Vector3 localVelocity = transform.InverseTransformDirection(body.linearVelocity);

        sensor.AddObservation(localOffset / arenaRadius);
        sensor.AddObservation(localVelocity / maxSpeed);
        sensor.AddObservation(BossHealth01);
        sensor.AddObservation(PlayerHealth01);
        sensor.AddObservation(DashCooldown01);
    }
}

관측값에 플레이어의 정확한 의도나 미래 위치 같은 정보를 넣으면 학습 중에는 성능이 높아 보여도 실제 게임에서는 사용할 수 없는 정책이 됩니다. 특권 정보는 디버깅용 비교 기준으로는 쓸 수 있어도 배포할 관측값과 분리하는 편이 안전합니다.

보스 기준 로컬 좌표계에서 플레이어 상대 위치와 이동 벡터를 관측하는 예시

행동 공간: 이동과 기술 선택을 분리하기

행동은 연속 행동과 이산 행동으로 나눌 수 있습니다. 이동 방향처럼 값이 이어지는 제어에는 연속 행동이 적합하고 공격 종류처럼 선택지가 명확한 경우에는 이산 행동이 어울립니다.

이 예제에서는 연속 행동 두 개로 전후·좌우 이동을 받고 이산 행동 하나로 대기·근접 공격·돌진 공격을 선택합니다. Behavior Parameters 컴포넌트에서 연속 행동 수를 2로 설정하고 이산 브랜치 하나의 크기를 3으로 설정합니다.

using Unity.MLAgents.Actuators;

public float moveForce = 18f;
public float attackRange = 2.2f;
public float dashRange = 6f;

public override void OnActionReceived(ActionBuffers actions)
{
    Vector2 moveInput = new(
        Mathf.Clamp(actions.ContinuousActions[0], -1f, 1f),
        Mathf.Clamp(actions.ContinuousActions[1], -1f, 1f));

    Vector3 move = transform.TransformDirection(new Vector3(moveInput.x, 0f, moveInput.y));
    body.AddForce(move * moveForce, ForceMode.Acceleration);

    int combatAction = actions.DiscreteActions[0];
    float distance = Vector3.Distance(transform.position, player.position);

    if (combatAction == 1 && distance <= attackRange)
    {
        TryMeleeAttack();
    }
    else if (combatAction == 2 && distance <= dashRange && DashCooldown01 <= 0f)
    {
        TryDashAttack();
    }
}

공격은 한 번 선택했다고 매 물리 프레임마다 실행되면 안 됩니다. 애니메이션, 공격 판정, 재사용 대기시간은 일반적인 게임 로직으로 제어하고 에이전트는 지금 어떤 의도를 선택할지만 결정하게 두는 편이 좋습니다. 이렇게 하면 학습 시간 단위와 실제 전투 연출을 분리할 수 있습니다.

보상 설계: 이기기보다 원하는 전투를 정의하기

보상은 정책이 최적화하는 기준입니다. 보스가 플레이어를 처치하면 큰 보상을 주는 것만으로는 충분하지 않습니다. 공격을 맞히기 직전에 체력을 잃어도 괜찮다고 판단하거나 시간 제한이 없다면 안전한 위치에서 아무 행동도 하지 않을 수 있습니다.

가장 먼저 목표를 측정 가능한 항목으로 바꾸고 보상은 작고 해석 가능하게 구성하세요. 한 번의 스텝에서 받는 보상은 대체로 작은 값으로 유지하는 것이 좋습니다. 다음은 시작점으로 쓸 수 있는 예시입니다.

상황예시 보상의도
보스 공격이 플레이어에게 적중+0.15유효한 공격 유도
보스가 피해를 받음-0.12무모한 접근 억제
적절한 공격 거리 유지+0.002지나친 도주 방지
경기장 밖으로 나감-0.02전투 영역 유지
플레이어 처치+1.0에피소드 목표
보스 처치-1.0생존과 방어 유도

거리 보상은 가까울수록 무조건 높게 주기보다 원하는 사거리 주변에서 가장 높도록 만드는 편이 낫습니다. 목표 거리를 targetDistance, 실제 거리를 distance라고 하면 다음처럼 설계할 수 있습니다.

rdistance=max(0,1distancetargetDistancetolerance)×cr_{distance}=\max\left(0,1-\frac{|distance-targetDistance|}{tolerance}\right)\times c

여기서 tolerance는 허용 거리 폭이고 c는 매우 작은 보상 계수입니다. 이 보상이 공격 적중 보상보다 커지면 보스는 공격 대신 거리만 유지하려 할 수 있으므로 로그를 보며 크기를 조정해야 합니다.

public void OnAttackHitPlayer()
{
    AddReward(0.15f);
}

public void OnDamaged(float normalizedDamage)
{
    AddReward(-0.12f * normalizedDamage);
}

private void RewardCombatDistance()
{
    float distance = Vector3.Distance(transform.position, player.position);
    const float targetDistance = 2.0f;
    const float tolerance = 3.0f;

    float score = Mathf.Max(0f, 1f - Mathf.Abs(distance - targetDistance) / tolerance);
    AddReward(score * 0.002f);
}

public void WinEpisode()
{
    AddReward(1f);
    EndEpisode();
}

public void LoseEpisode()
{
    AddReward(-1f);
    EndEpisode();
}

에피소드 초기화와 상대 AI

OnEpisodeBegin에서는 이전 전투의 상태가 남지 않도록 두 캐릭터의 위치, 속도, 체력, 쿨다운, 진행 중인 공격을 모두 초기화합니다. 시작 위치까지 항상 같으면 특정 배치에만 적응할 수 있으므로 충돌하지 않는 범위에서 위치와 방향을 무작위로 바꾸세요.

public override void OnEpisodeBegin()
{
    ResetCombatant(transform, body);
    ResetPlayer();
    BossHealth01 = 1f;
    PlayerHealth01 = 1f;
    DashCooldown01 = 0f;
}

private void ResetCombatant(Transform actor, Rigidbody actorBody)
{
    Vector2 point = Random.insideUnitCircle.normalized * Random.Range(3f, 6f);
    actor.position = new Vector3(point.x, 0.5f, point.y);
    actor.rotation = Quaternion.Euler(0f, Random.Range(0f, 360f), 0f);
    actorBody.linearVelocity = Vector3.zero;
    actorBody.angularVelocity = Vector3.zero;
}

상대 플레이어는 처음에는 규칙 기반 봇으로 두는 것이 좋습니다. 예를 들어 일정 거리 안으로 접근해 공격하고 체력이 낮으면 잠시 물러나는 정도면 충분합니다. 학습 중 상대 정책까지 계속 바꾸면 보스의 성능 변화가 보상 설계 때문인지 상대 변화 때문인지 구분하기 어렵습니다.

상대가 너무 약해지면 보스가 특정 행동 하나만 반복할 수 있습니다. 기본 정책이 안정된 뒤에는 서로 다른 이동 속도, 회피 성향, 공격 주기를 가진 여러 규칙 기반 상대를 무작위로 섞어 일반화 성능을 확인하세요.

무작위 시작 위치와 서로 다른 규칙 기반 플레이어 봇을 사용하는 보스 전투 학습 환경

학습 설정과 실행

PPO는 ML-Agents에서 널리 쓰이는 정책 최적화 방식입니다. 아래 YAML은 작은 전투 환경을 위한 출발점입니다. 프로젝트와 패키지 버전에 따라 정확한 필드명과 지원 범위가 달라질 수 있으므로 설치한 ML-Agents 릴리스의 설정 문서를 먼저 확인하세요.

behaviors:
  Boss:
    trainer_type: ppo
    hyperparameters:
      batch_size: 1024
      buffer_size: 10240
      learning_rate: 3.0e-4
      beta: 5.0e-3
      epsilon: 0.2
      lambd: 0.95
      num_epoch: 3
    network_settings:
      normalize: true
      hidden_units: 256
      num_layers: 2
    reward_signals:
      extrinsic:
        gamma: 0.99
        strength: 1.0
    max_steps: 1000000
    time_horizon: 128
    summary_freq: 10000

설정 파일을 config/boss_ppo.yaml로 저장했다면 Python 환경에서 다음처럼 학습을 시작할 수 있습니다.

mlagents-learn config/boss_ppo.yaml --run-id=boss-ppo-01

명령이 대기 상태가 되면 Unity 에디터에서 학습 씬을 실행합니다. 학습 속도가 부족하면 에디터 창을 여러 개 여는 대신 가능한 경우 빌드한 환경을 여러 인스턴스로 실행하는 방식을 검토하세요. 물리 프레임과 의사결정 주기를 지나치게 높이면 학습이 빨라 보이지만 공격 판정이 불안정해질 수 있습니다.

학습 결과를 검증하는 방법

누적 보상이 올랐다고 해서 보스가 재미있어졌다고 단정할 수는 없습니다. 같은 맵과 같은 플레이어 봇뿐 아니라 시작 위치와 상대 성향을 바꾼 평가 환경에서 행동을 관찰해야 합니다.

다음 항목을 별도로 기록하면 문제를 빠르게 찾을 수 있습니다.

  • 에피소드 승률과 평균 전투 시간
  • 근접 공격과 돌진 공격의 선택 비율 및 적중률
  • 보스와 플레이어의 평균 거리
  • 보상이 발생한 원인별 합계
  • 학습에 쓰지 않은 상대 봇에 대한 승률

예를 들어 승률은 높지만 돌진만 반복한다면 돌진의 위험이나 쿨다운 비용이 충분히 반영되지 않았을 가능성이 큽니다. 반대로 이동만 하고 공격하지 않는다면 거리 유지 보상이 너무 크거나 공격 성공 보상이 너무 희소한지 확인하세요. 문제를 해결할 때는 보상 항목을 한꺼번에 바꾸지 말고 한 항목씩 수정한 뒤 같은 조건에서 비교하는 편이 원인을 파악하기 쉽습니다.

배포 단계에서는 안전장치를 남기기

학습된 모델은 모든 상황에서 의도대로 행동한다고 보장할 수 없습니다. 특히 지형이 바뀌거나 플레이어가 예상 밖의 이동 기술을 쓰면 훈련에서 보지 못한 상태가 발생합니다. 따라서 배포 시에는 강화학습 정책 위에 일반 게임 로직의 안전장치를 둡니다.

  • 벽이나 낭떠러지 근처에서는 내비게이션과 충돌 회피가 이동을 보정한다.
  • 연출상 반드시 나와야 하는 페이즈 전환과 무적 시간은 상태 머신이 제어한다.
  • 쿨다운, 사거리, 시전 가능 여부는 행동을 실행하기 전에 다시 검사한다.
  • 일정 시간 동안 유효 행동이 없으면 추적이나 귀환 같은 폴백 로직으로 전환한다.

이 구분은 강화학습을 포기하는 것이 아니라 역할을 나누는 방법입니다. 전투의 규칙과 연출은 예측 가능한 코드로 유지하고 그 안에서 거리 조절과 행동 우선순위처럼 조합이 많은 판단을 정책에 맡기면 디버깅과 밸런싱이 훨씬 수월해집니다.

마무리

ML-Agents로 보스 AI를 만들 때 가장 중요한 작업은 신경망 구조를 복잡하게 만드는 일이 아니라 전투 목표를 관측, 행동, 보상으로 정확하게 번역하는 일입니다. 작은 1대1 전투부터 시작해 보상 로그와 행동 비율을 확인하고 안정된 정책에만 점진적으로 스킬과 페이즈를 더하세요.

학습 모델은 보스 설계의 출발점이 될 수 있지만 최종 난이도와 재미는 여전히 사람이 검증해야 합니다. 플레이 테스트에서 발견한 문제를 다시 환경과 보상 설계에 반영하는 반복 과정이 좋은 보스 AI를 만듭니다.

#Unity#ML-Agents#강화학습#게임 AI#C#

계속 읽어보기

이런 글은 어떠세요?

< Back to Logs