Physics e AI de Inimigos

Curso de Desenvolvimento de Jogos com Pygame

Felipe A. Moreno-Vera

🤖 Inimigos e AI Básica

Steering Behaviors

Os steering behaviors são simples cálculos de força que, combinados, produzem um movimento de AI que parece complexo.

Desired Velocity = direction × max_speed
Steering Force   = Desired Velocity - Current Velocity
New Velocity     = Current Velocity + Steering Force × dt

Seek

Mova-se em direção a um alvo na velocidade máxima:

def seek(ex, ey, evx, evy, tx, ty, max_speed, max_force):
    dx, dy   = tx - ex, ty - ey
    dist     = math.hypot(dx, dy)
    if dist < 0.1: return 0, 0
    desired_vx = (dx / dist) * max_speed
    desired_vy = (dy / dist) * max_speed
    fx = desired_vx - evx   # Steering = desired - current
    fy = desired_vy - evy
    # Clamp to max_force
    fmag = math.hypot(fx, fy)
    if fmag > max_force:
        fx, fy = fx / fmag * max_force, fy / fmag * max_force
    return fx, fy

Flee

O inverso de seek — afaste-se do alvo:

def flee(ex, ey, evx, evy, tx, ty, max_speed, max_force):
    # Seek toward a point on the opposite side
    return seek(ex, ey, evx, evy,
                ex - (tx - ex),   # Mirrored target X
                ey - (ty - ey),   # Mirrored target Y
                max_speed, max_force)

Arrival (Seek with deceleration)

Reduza a velocidade conforme o enemy se aproxima do alvo, para evitar passar do ponto:

def seek_with_arrival(ex, ey, evx, evy, tx, ty,
                      max_speed, max_force, slow_radius=80):
    dx, dy   = tx - ex, ty - ey
    dist     = math.hypot(dx, dy)
    speed    = max_speed if dist > slow_radius else max_speed * (dist / slow_radius)
    desired_vx = (dx / dist) * speed if dist > 0.1 else 0
    desired_vy = (dy / dist) * speed if dist > 0.1 else 0
    fx = desired_vx - evx
    fy = desired_vy - evy
    fmag = math.hypot(fx, fy)
    if fmag > max_force:
        fx, fy = fx / fmag * max_force, fy / fmag * max_force
    return fx, fy

Arrival (Seek with deceleration)

Reduza a velocidade conforme o enemy se aproxima do alvo, para evitar passar do ponto:

def seek_with_arrival(ex, ey, evx, evy, tx, ty,
                      max_speed, max_force, slow_radius=80):
    dx, dy   = tx - ex, ty - ey
    dist     = math.hypot(dx, dy)
    speed    = max_speed if dist > slow_radius else max_speed * (dist / slow_radius)
    desired_vx = (dx / dist) * speed if dist > 0.1 else 0
    desired_vy = (dy / dist) * speed if dist > 0.1 else 0
    fx = desired_vx - evx
    fy = desired_vy - evy
    fmag = math.hypot(fx, fy)
    if fmag > max_force:
        fx, fy = fx / fmag * max_force, fy / fmag * max_force
    return fx, fy
Comportamento Arrival — speed vs distância até o alvo
  max_speed=150 px/s   slow_radius=80 px

  Distância    Velocidade   Comportamento
  --------------------------------------
  200          150.0        velocidade máxima
  150          150.0        velocidade máxima
  100          150.0        velocidade máxima
  80           150.0        reduzindo
  60           112.5        reduzindo
  40           75.0         reduzindo
  20           37.5         reduzindo
  5            9.4          parado
  0            0.0          parado

Line of Sight (LOS)

LOS verifica se existe um caminho livre entre dois pontos. Usa uma interseção paramétrica line-AABB:

def line_of_sight(ax, ay, bx, by, wall_rects):
    """Returns True if A can see B without hitting any wall."""
    dx, dy = bx - ax, by - ay
    for wall in wall_rects:
        inv_dx = 1 / dx if dx != 0 else float('inf')
        inv_dy = 1 / dy if dy != 0 else float('inf')
        tx1 = (wall.left   - ax) * inv_dx
        tx2 = (wall.right  - ax) * inv_dx
        ty1 = (wall.top    - ay) * inv_dy
        ty2 = (wall.bottom - ay) * inv_dy
        tmin = max(min(tx1, tx2), min(ty1, ty2))
        tmax = min(max(tx1, tx2), max(ty1, ty2))
        if tmax >= 0 and tmin <= tmax and tmin <= 1:
            return False   # Blocked
    return True

Vision Cone (Field of View)

Verifica se um alvo cai dentro do arco frontal do enemy:

def in_fov(ex, ey, facing_angle, fov_degrees, tx, ty):
    dx, dy       = tx - ex, ty - ey
    target_angle = math.atan2(dy, dx)
    # Shortest angular distance (handles wraparound)
    delta = abs(math.atan2(
        math.sin(target_angle - facing_angle),
        math.cos(target_angle - facing_angle)
    ))
    return delta <= math.radians(fov_degrees / 2)

The Finite State Machine (FSM)

Uma FSM é um conjunto de estados e transições entre eles. Um enemy está sempre em exatamente um estado.

┌──────────────────────────────────────────────────────┐
│                   ENEMY FSM                          │
│                                                      │
│  IDLE ──timer──► PATROL ──detect──► ALERT            │
│   ▲                 ▲                  │             │
│   │                 │             timer / lost       │
│   │                 │                  ▼             │
│   │                 └───lost──── CHASE ◄─── far      │
│   │                                   │              │
│   │                              close│              │
│   │                                   ▼              │
│   │                               ATTACK ──low HP──► │
│   │                                                  │
│   │                  FLEE ──safe / hp──► PATROL      │
│   └────────────────────────────────────────────────  │
└──────────────────────────────────────────────────────┘

State base class pattern

class State:
    def enter(self, enemy): pass
    def update(self, enemy, dt, target): pass
    def exit(self, enemy):  pass

class IdleState(State):
    def enter(self, enemy):
        enemy.vx = enemy.vy = 0

    def update(self, enemy, dt, target):
        dist = math.hypot(target.x - enemy.x, target.y - enemy.y)
        if dist < DETECT_RADIUS:
            enemy.transition_to("ALERT")
        elif enemy.state_timer > IDLE_DURATION:
            enemy.transition_to("PATROL")

class ChaseState(State):
    def update(self, enemy, dt, target):
        if math.hypot(target.x - enemy.x, target.y - enemy.y) < ATTACK_RADIUS:
            enemy.transition_to("ATTACK")
        # Steer toward target
        fx, fy = seek(enemy.x, enemy.y, enemy.vx, enemy.vy,
                      target.x, target.y, CHASE_SPEED, MAX_FORCE)
        enemy.vx += fx * dt
        enemy.vy += fy * dt

Memory — last seen position

Quando o jogador sai do field of view, o enemy se move para a última posição conhecida em vez de desistir imediatamente:

MEMORY_DURATION = 3.0   # seconds

# When player is visible:
last_seen_x   = player.x
last_seen_y   = player.y
memory_timer  = MEMORY_DURATION

# When player is not visible:
memory_timer -= dt
if memory_timer > 0:
    steer_toward(last_seen_x, last_seen_y)   # Move to last known pos
else:
    transition_to("PATROL")                  # Give up

Memory — last seen position

Simulação de Enemy FSM
================================================
   Tempo  State         HP  Dist. jogador
  ----------------------------------------
    2.99  PATROL       100           400
    3.98  PATROL       100           400
    4.48  ALERT        100           180
    4.98  ALERT        100           180
    6.98  CHASE        100           120
    7.47  ATTACK       100            30
    7.97  ATTACK     80.15999999999998            30
    8.46  ATTACK     60.319999999999965            30
   10.46  CHASE      60.319999999999965           350

Transitions:
  IDLE → PATROL
  PATROL → ALERT
  ALERT → CHASE
  CHASE → ATTACK
  ATTACK → CHASE

Enemy AI — Resumo

  • Steering = desired_velocity - current_velocity → aplicado como força
  • Seek: dirigir-se ao alvo na velocidade máxima
  • Flee: afastar-se (seek inverso)
  • Arrival: seek com a velocidade escalada pela proximidade → sem passar do ponto
  • Wander: ruído aleatório sobre um círculo projetado → movimento idle natural
  • LOS: teste paramétrico ray–AABB → True se nenhuma wall bloquear o caminho
  • FOV: diferença angular entre para onde se olha e a direção do alvo
  • FSM: um único estado ativo por vez; transições explícitas com hooks enter/exit
  • Memory: guarda a última posição vista; persegue-a antes de desistir