Physics y AI de Enemigos

Curso de Desarrollo de Juegos con Pygame

Felipe A. Moreno-Vera

Nota

En estos módulos aprenderás:

  • Gravity, velocidad, aceleración, fricción, altura de salto variable, coyote time, jump buffering, plataformas de una sola dirección, plataformas móviles, y wall jumping
  • Steering behaviors (seek, flee, patrol, wander), line of sight, vision cones, y una Finite State Machine completa para enemigos

⚙️ Physics Básica

El Physics Update Loop

La physics de plataformas se basa en dos ecuaciones fundamentales que se aplican en cada frame:

velocity += acceleration × dt      # Euler integration
position += velocity   × dt

El Physics Update Loop

La physics de plataformas se basa en dos ecuaciones fundamentales que se aplican en cada frame:

velocity += acceleration × dt      # Euler integration
position += velocity   × dt

dt (delta time) es el tiempo transcurrido desde el último frame, en segundos. Usar dt hace que la simulación sea independiente del frame rate: el mismo comportamiento a 30 FPS y a 120 FPS.

# Every frame:
vy    += GRAVITY * dt       # Gravity accelerates the fall
y     += vy * dt            # Velocity moves the position

El Physics Update Loop

La physics de plataformas se basa en dos ecuaciones fundamentales que se aplican en cada frame:

velocity += acceleration × dt      # Euler integration
position += velocity   × dt

dt (delta time) es el tiempo transcurrido desde el último frame, en segundos. Usar dt hace que la simulación sea independiente del frame rate: el mismo comportamiento a 30 FPS y a 120 FPS.

# Every frame:
vy    += GRAVITY * dt       # Gravity accelerates the fall
y     += vy * dt            # Velocity moves the position

Importante

Multiplica siempre las fuerzas y velocidades por dt. Sin esto, tu juego se ejecutará a distintas velocidades según el hardware.

Gravity

Gravity es simplemente una aceleración constante hacia abajo aplicada en cada frame:

GRAVITY      = 900.0    # px/s²  — feel free to tune
TERMINAL_VEL = 850.0    # cap: prevents infinite acceleration

vy = min(vy + GRAVITY * dt, TERMINAL_VEL)
y  += vy * dt

Gravity

Gravity es simplemente una aceleración constante hacia abajo aplicada en cada frame:

GRAVITY      = 900.0    # px/s²  — feel free to tune
TERMINAL_VEL = 850.0    # cap: prevents infinite acceleration

vy = min(vy + GRAVITY * dt, TERMINAL_VEL)
y  += vy * dt
Simulación de caída libre (60 FPS, gravity=900 px/s²)
  frame    vy (px/s)      y (px)       notas
  ----------------------------------------------------
  0        15.0           0.2          
  10       165.0          16.5         
  20       315.0          57.8         
  30       465.0          124.0        
  40       615.0          215.2        
  50       765.0          331.5        
  60       850.0          469.8        terminal velocity alcanzada

Jumping

Un salto es simplemente una velocidad instantánea hacia arriba aplicada cuando el jugador está en el suelo:

JUMP_VY = -460.0    # Negative = upward (Y grows downward in Pygame)

if player_pressed_jump and on_ground:
    vy = JUMP_VY

Variable Jump Height

Mantén presionado el botón de salto para un salto más alto; suéltalo antes para un salto corto. El truco: reducir la gravity mientras el botón está presionado y el jugador todavía está subiendo.

JUMP_FRAMES_MAX = 20   # Max frames of reduced gravity
JUMP_FRAMES_MIN = 6    # Must hold at least this long

if jumping:
    jump_frames += 1
    if not keys[K_SPACE] and jump_frames > JUMP_FRAMES_MIN:
        jumping = False          # Cut jump short
    if jump_frames >= JUMP_FRAMES_MAX:
        jumping = False

grav_mult = 0.4 if jumping else 1.0   # Reduced gravity = higher jump
vy += GRAVITY * grav_mult * dt

Coyote Time

El período de gracia de coyote time permite que el jugador salte durante algunos frames después de caminar fuera de una plataforma — se siente justo y responsive:

COYOTE_FRAMES = 8

if on_ground:
    coyote_timer = COYOTE_FRAMES
elif coyote_timer > 0:
    coyote_timer -= 1

can_jump = on_ground or coyote_timer > 0

Jump Buffering

Si el jugador presiona saltar justo antes de aterrizar, guarda el input en un buffer y ejecútalo al hacer contacto:

BUFFER_FRAMES = 10

# On KEYDOWN event:
jump_buffer = BUFFER_FRAMES

# In update():
if jump_buffer > 0:
    jump_buffer -= 1
if jump_buffer > 0 and can_jump:
    vy = JUMP_VY
    jump_buffer = 0

Jump Buffering

Valores de Coyote time y jump buffer
=============================================
  Coyote frames  : 8 frames = 133 ms
  Buffer frames  : 10 frames = 167 ms

Por qué importa:
  Sin coyote: caminar fuera del borde → no puedes saltar (se siente injusto)
  Con coyote:     8 frames de gracia → se siente responsivo

  Sin buffer: presionar salto 1 frame antes → no pasa nada
  Con buffer:     input mantenido 10 frames → se dispara automáticamente al aterrizar

Collision Resolution

El patrón más confiable: muévete en un eje, resuelve la collision, y luego muévete en el otro.

# Step 1: move horizontally
x += vx * dt
for platform in platforms:
    if player_rect.colliderect(platform.rect):
        if vx > 0: x = platform.rect.left  - player_w   # Pushed left
        if vx < 0: x = platform.rect.right               # Pushed right
        vx = 0

# Step 2: move vertically
y += vy * dt
on_ground = False
for platform in platforms:
    if player_rect.colliderect(platform.rect):
        if vy > 0:                                        # Falling
            y        = platform.rect.top - player_h
            vy       = 0
            on_ground = True
        elif vy < 0:                                      # Rising
            y  = platform.rect.bottom
            vy = 0

Collision Resolution

El patrón más confiable: muévete en un eje, resuelve la collision, y luego muévete en el otro.

# Step 1: move horizontally
x += vx * dt
for platform in platforms:
    if player_rect.colliderect(platform.rect):
        if vx > 0: x = platform.rect.left  - player_w   # Pushed left
        if vx < 0: x = platform.rect.right               # Pushed right
        vx = 0

# Step 2: move vertically
y += vy * dt
on_ground = False
for platform in platforms:
    if player_rect.colliderect(platform.rect):
        if vy > 0:                                        # Falling
            y        = platform.rect.top - player_h
            vy       = 0
            on_ground = True
        elif vy < 0:                                      # Rising
            y  = platform.rect.bottom
            vy = 0

Tip

Resuelve siempre X e Y por separado. Resolverlos juntos hace que el jugador atraviese esquinas y se quede pegado a las paredes de forma incorrecta.

One-Way Platforms

Sólidas solo desde arriba — el jugador puede atravesarlas saltando desde abajo:

for platform in one_way_platforms:
    if player_rect.colliderect(platform.rect):
        # Only resolve if falling AND was above the platform top last frame
        if vy > 0 and (prev_y + player_h) <= platform.rect.top + 4:
            y        = platform.rect.top - player_h
            vy       = 0
            on_ground = True
        # If rising: ignore (pass through)

Moving Platforms — Velocity Inheritance

Cuando el jugador está parado sobre una plataforma en movimiento, súmale la velocidad de la plataforma al jugador:

class MovingPlatform:
    def update(self, dt):
        prev_x     = self.rect.x
        self.rect.x += self.direction * self.speed * dt
        self.vel_x  = (self.rect.x - prev_x) / dt   # Derived velocity

# In player update:
if on_ground and standing_on:
    x += standing_on.vel_x * dt   # Inherit platform movement

Acceleration-Based Movement

La velocidad instantánea se siente rígida. La aceleración le da peso y momentum al movimiento:

MOVE_ACCEL   = 1800.0   # px/s² — ground acceleration
AIR_ACCEL    = 900.0    # Less control in the air
MAX_VX       = 240.0

accel = MOVE_ACCEL if on_ground else AIR_ACCEL

if pressing_right:
    vx += (MAX_VX - vx) * (accel / MAX_VX) * dt   # Accelerate toward target
else:
    vx -= math.copysign(MOVE_ACCEL * dt, vx)       # Decelerate

Acceleration-Based Movement

La velocidad instantánea se siente rígida. La aceleración le da peso y momentum al movimiento:

MOVE_ACCEL   = 1800.0   # px/s² — ground acceleration
AIR_ACCEL    = 900.0    # Less control in the air
MAX_VX       = 240.0

accel = MOVE_ACCEL if on_ground else AIR_ACCEL

if pressing_right:
    vx += (MAX_VX - vx) * (accel / MAX_VX) * dt   # Accelerate toward target
else:
    vx -= math.copysign(MOVE_ACCEL * dt, vx)       # Decelerate
Velocity profile en 30 frames
  Frame    Instantánea  Accelerated
  ----------------------------------
  0        240.0        30.0
  5        240.0        132.3
  10       240.0        184.8
  15       240.0        211.7
  20       240.0        225.5
  25       240.0        232.5

El movimiento Accelerated alcanza max speed de forma gradual → se siente weighted

Physics — Resumen

  • velocity += acceleration × dt y luego position += velocity × dt — usa siempre dt
  • Gravity = aceleración constante hacia abajo + límite de terminal velocity
  • Variable jump = reducir la gravity mientras se mantiene presionado el botón de salto
  • Coyote time = período de gracia de salto (8 frames) después de caminar fuera de una plataforma
  • Jump buffering = recordar el input de salto (10 frames) antes de aterrizar
  • Resolución separada de X e Y para evitar que el jugador atraviese esquinas
  • One-way platforms: solo resolver la collision al caer y estar por encima del borde superior
  • Moving platforms: derivar vel_x a partir del delta de posición, heredarla en el jugador
  • Acceleration-based movement: vx += (target - vx) * accel_factor * dt