← Volver al listado de tecnologías

Desarrollo de Juegos

Por: SiempreListo
kivyjuegosgamedevcolisiones

Game Loop en Kivy

El game loop básico usa Clock.schedule_interval.

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.clock import Clock

class JuegoWidget(Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        Clock.schedule_interval(self.update, 1/60)  # 60 FPS

    def update(self, dt):
        # Lógica del juego
        # dt = tiempo desde última actualización
        pass

Movimiento Básico

from kivy.uix.widget import Widget
from kivy.properties import NumericProperty
from kivy.vector import Vector

class Pelota(Widget):
    velocidad_x = NumericProperty(0)
    velocidad_y = NumericProperty(0)

    def mover(self):
        self.pos = Vector(self.velocidad_x, self.velocidad_y) + self.pos
<Pelota>:
    size: 50, 50
    canvas:
        Ellipse:
            pos: self.pos
            size: self.size

Detección de Colisiones

Colisión Rectangular

def colisiona(self, otro):
    return self.collide_widget(otro)

Colisión por Punto

def on_touch_down(self, touch):
    if self.collide_point(*touch.pos):
        print('Tocado!')
        return True

Colisión Circular

from kivy.vector import Vector

def colision_circular(self, otro):
    distancia = Vector(self.center).distance(otro.center)
    radio_suma = (self.width + otro.width) / 2
    return distancia < radio_suma

Colisión con Bordes

def verificar_bordes(self, ventana_size):
    # Rebote horizontal
    if self.x < 0 or self.right > ventana_size[0]:
        self.velocidad_x *= -1

    # Rebote vertical
    if self.y < 0 or self.top > ventana_size[1]:
        self.velocidad_y *= -1

Ejemplo: Pong Simplificado

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import NumericProperty, ReferenceListProperty, ObjectProperty
from kivy.vector import Vector
from kivy.clock import Clock

class Pelota(Widget):
    velocidad_x = NumericProperty(0)
    velocidad_y = NumericProperty(0)
    velocidad = ReferenceListProperty(velocidad_x, velocidad_y)

    def mover(self):
        self.pos = Vector(*self.velocidad) + self.pos

class Paleta(Widget):
    puntaje = NumericProperty(0)

    def rebotar_pelota(self, pelota):
        if self.collide_widget(pelota):
            pelota.velocidad_x *= -1.1

class PongGame(Widget):
    pelota = ObjectProperty(None)
    jugador1 = ObjectProperty(None)
    jugador2 = ObjectProperty(None)

    def servir(self):
        self.pelota.center = self.center
        self.pelota.velocidad = Vector(4, 0).rotate(30)

    def update(self, dt):
        self.pelota.mover()

        # Rebote en paletas
        self.jugador1.rebotar_pelota(self.pelota)
        self.jugador2.rebotar_pelota(self.pelota)

        # Rebote superior/inferior
        if self.pelota.y < 0 or self.pelota.top > self.height:
            self.pelota.velocidad_y *= -1

        # Punto
        if self.pelota.x < 0:
            self.jugador2.puntaje += 1
            self.servir()
        if self.pelota.right > self.width:
            self.jugador1.puntaje += 1
            self.servir()

    def on_touch_move(self, touch):
        if touch.x < self.width / 3:
            self.jugador1.center_y = touch.y
        if touch.x > self.width * 2 / 3:
            self.jugador2.center_y = touch.y

class PongApp(App):
    def build(self):
        juego = PongGame()
        juego.servir()
        Clock.schedule_interval(juego.update, 1/60)
        return juego
<Pelota>:
    size: 50, 50
    canvas:
        Ellipse:
            pos: self.pos
            size: self.size

<Paleta>:
    size: 25, 200
    canvas:
        Rectangle:
            pos: self.pos
            size: self.size

<PongGame>:
    pelota: pelota
    jugador1: jugador1
    jugador2: jugador2

    Pelota:
        id: pelota
        center: root.center

    Paleta:
        id: jugador1
        x: root.width * 0.05
        center_y: root.center_y

    Paleta:
        id: jugador2
        x: root.width * 0.95
        center_y: root.center_y

    Label:
        text: str(jugador1.puntaje)
        pos: root.width * 0.25, root.height * 0.9

    Label:
        text: str(jugador2.puntaje)
        pos: root.width * 0.75, root.height * 0.9

Input para Juegos

Teclado

from kivy.core.window import Window

class JuegoWidget(Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._keyboard = Window.request_keyboard(self._on_keyboard_closed, self)
        self._keyboard.bind(on_key_down=self._on_key_down)
        self._keyboard.bind(on_key_up=self._on_key_up)
        self.teclas_presionadas = set()

    def _on_keyboard_closed(self):
        self._keyboard.unbind(on_key_down=self._on_key_down)
        self._keyboard = None

    def _on_key_down(self, keyboard, keycode, text, modifiers):
        self.teclas_presionadas.add(keycode[1])
        return True

    def _on_key_up(self, keyboard, keycode):
        self.teclas_presionadas.discard(keycode[1])
        return True

    def update(self, dt):
        if 'left' in self.teclas_presionadas:
            self.jugador.x -= 5
        if 'right' in self.teclas_presionadas:
            self.jugador.x += 5

Touch Multitouch

def on_touch_down(self, touch):
    touch.grab(self)
    touch.ud['inicial'] = touch.pos
    return True

def on_touch_move(self, touch):
    if touch.grab_current is self:
        dx = touch.x - touch.ud['inicial'][0]
        dy = touch.y - touch.ud['inicial'][1]
        # Usar dx, dy para control

Sprites y Animación

Sprite Básico

from kivy.uix.image import Image

class Sprite(Image):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.source = 'sprite.png'
        self.allow_stretch = True

Animación de Sprites

from kivy.clock import Clock
from kivy.uix.image import Image

class SpriteAnimado(Image):
    def __init__(self, frames, fps=12, **kwargs):
        super().__init__(**kwargs)
        self.frames = frames  # Lista de rutas de imagen
        self.frame_actual = 0
        Clock.schedule_interval(self.siguiente_frame, 1/fps)

    def siguiente_frame(self, dt):
        self.frame_actual = (self.frame_actual + 1) % len(self.frames)
        self.source = self.frames[self.frame_actual]

Con Atlas

# Crear atlas
# python -m kivy.atlas mi_atlas 256x256 sprite*.png

from kivy.uix.image import Image

sprite = Image(source='atlas://mi_atlas/sprite1')

Sistema de Partículas Simple

from kivy.uix.widget import Widget
from kivy.graphics import Color, Ellipse
from kivy.clock import Clock
from random import uniform

class Particula:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.vx = uniform(-2, 2)
        self.vy = uniform(2, 5)
        self.vida = 1.0
        self.size = uniform(5, 15)

class SistemaParticulas(Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.particulas = []
        Clock.schedule_interval(self.update, 1/60)

    def emitir(self, x, y, cantidad=10):
        for _ in range(cantidad):
            self.particulas.append(Particula(x, y))

    def update(self, dt):
        self.canvas.clear()
        particulas_vivas = []

        with self.canvas:
            for p in self.particulas:
                p.x += p.vx
                p.y += p.vy
                p.vy -= 0.1  # Gravedad
                p.vida -= dt

                if p.vida > 0:
                    Color(1, 0.5, 0, p.vida)
                    Ellipse(pos=(p.x, p.y), size=(p.size, p.size))
                    particulas_vivas.append(p)

        self.particulas = particulas_vivas

Sonidos

from kivy.core.audio import SoundLoader

class JuegoWidget(Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.sonido_golpe = SoundLoader.load('golpe.wav')
        self.musica = SoundLoader.load('musica.mp3')

    def reproducir_golpe(self):
        if self.sonido_golpe:
            self.sonido_golpe.play()

    def iniciar_musica(self):
        if self.musica:
            self.musica.loop = True
            self.musica.volume = 0.5
            self.musica.play()

KivEnt - Game Engine

Para juegos más complejos, considera KivEnt.

pip install kivent_core
from kivent_core.gameworld import GameWorld
from kivent_core.managers.resource_managers import texture_manager

# KivEnt usa sistema de entidades/componentes

Optimización

Object Pooling

class PoolBalas:
    def __init__(self, cantidad):
        self.disponibles = [Bala() for _ in range(cantidad)]
        self.activas = []

    def obtener(self):
        if self.disponibles:
            bala = self.disponibles.pop()
            self.activas.append(bala)
            return bala
        return None

    def devolver(self, bala):
        self.activas.remove(bala)
        self.disponibles.append(bala)

Usar NumericProperty

# Más eficiente para valores que cambian frecuentemente
velocidad = NumericProperty(0)

Testing de Juegos

# test_juegos.py
import unittest
from kivy.vector import Vector
from kivy.uix.widget import Widget

class TestVector(unittest.TestCase):
    def test_suma_vectores(self):
        v1 = Vector(1, 0)
        v2 = Vector(0, 1)
        resultado = v1 + v2
        self.assertEqual(resultado.x, 1)
        self.assertEqual(resultado.y, 1)

    def test_normalizar(self):
        v = Vector(3, 4)
        n = v.normalize()
        self.assertAlmostEqual(n.length(), 1.0)

    def test_angulo(self):
        v = Vector(1, 0)
        self.assertEqual(v.angle(Vector(0, 1)), 90)

class TestColisiones(unittest.TestCase):
    def test_collide_widget(self):
        w1 = Widget(pos=(0, 0), size=(100, 100))
        w2 = Widget(pos=(50, 50), size=(100, 100))
        self.assertTrue(w1.collide_widget(w2))

    def test_no_collision(self):
        w1 = Widget(pos=(0, 0), size=(50, 50))
        w2 = Widget(pos=(100, 100), size=(50, 50))
        self.assertFalse(w1.collide_widget(w2))

    def test_collide_point(self):
        w = Widget(pos=(0, 0), size=(100, 100))
        self.assertTrue(w.collide_point(50, 50))
        self.assertFalse(w.collide_point(150, 150))

if __name__ == '__main__':
    unittest.main()

Probar en Dispositivo con ADB

# Monitorear FPS
adb shell dumpsys gfxinfo com.ejemplo.miapp reset
# Jugar por 10 segundos
adb shell dumpsys gfxinfo com.ejemplo.miapp | grep "Total frames"

# Ver uso de CPU (importante para juegos)
adb shell top -n 1 | grep miapp

# Monitorear memoria durante juego
adb shell dumpsys meminfo com.ejemplo.miapp | grep "TOTAL"

# Probar input de juego
adb shell input swipe 200 500 400 500 100  # Swipe rápido
adb shell input tap 300 400  # Tap para disparar

# Grabar gameplay
adb shell screenrecord --bit-rate 6000000 /sdcard/gameplay.mp4

Resumen