Physics y AI de Enemigos

Curso de Desarrollo de Juegos con Pygame

Felipe A. Moreno-Vera

πŸ€– Enemigos y AI BΓ‘sica

Steering Behaviors

Los steering behaviors son simples cΓ‘lculos de fuerza que, combinados, producen un movimiento de AI que parece complejo.

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

Seek

MuΓ©vete hacia un objetivo a mΓ‘xima velocidad:

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

Lo inverso de seek β€” alΓ©jate del objetivo:

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)

Reduce la velocidad a medida que el enemy se acerca al objetivo, para evitar pasarse de largo:

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)

Reduce la velocidad a medida que el enemy se acerca al objetivo, para evitar pasarse de largo:

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
Comportamiento Arrival β€” speed vs distancia al objetivo
  max_speed=150 px/s   slow_radius=80 px

  Distancia    Velocidad    Comportamiento
  --------------------------------------
  200          150.0        velocidad mΓ‘xima
  150          150.0        velocidad mΓ‘xima
  100          150.0        velocidad mΓ‘xima
  80           150.0        reduciendo
  60           112.5        reduciendo
  40           75.0         reduciendo
  20           37.5         reduciendo
  5            9.4          detenido
  0            0.0          detenido

Line of Sight (LOS)

LOS verifica si existe un camino despejado entre dos puntos. Usa una intersecciΓ³n 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 si un objetivo cae dentro del arco frontal del 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)

Una FSM es un conjunto de estados y transiciones entre ellos. Un enemy siempre estΓ‘ en exactamente un 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

Cuando el jugador sale del field of view, el enemy se mueve hacia la ΓΊltima posiciΓ³n conocida en lugar de rendirse de inmediato:

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

SimulaciΓ³n de Enemy FSM
================================================
  Tiempo  State         HP  Dist. jugador
  ----------------------------------------
    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 β€” Resumen

  • Steering = desired_velocity - current_velocity β†’ aplicado como fuerza
  • Seek: dirigirse hacia el objetivo a mΓ‘xima velocidad
  • Flee: alejarse (seek inverso)
  • Arrival: seek con la velocidad escalada segΓΊn la proximidad β†’ sin pasarse de largo
  • Wander: ruido aleatorio sobre un cΓ­rculo proyectado β†’ movimiento idle natural
  • LOS: prueba paramΓ©trica ray–AABB β†’ True si ninguna wall bloquea el camino
  • FOV: diferencia angular entre hacia dΓ³nde mira y la direcciΓ³n del objetivo
  • FSM: un solo estado activo a la vez; transiciones explΓ­citas con hooks enter/exit
  • Memory: guarda la ΓΊltima posiciΓ³n vista; la persigue antes de rendirse