En este artículo explicamos cómo programar en simpleJ la estructura básica de un videojuego.
Nota: Este artículo asume que ya tienes algunas nociones básicas de cómo se programa una computadora, tales como los conceptos de variables, ciclos, condiciones, procedimientos y funciones. Todos estos conceptos están explicados en el libro "¡Descubre cómo se hace un videojuego!".
Ciclo principal de un videojuego
La estructura básica de un videojuego es muy sencilla. Después de inicializar el estado del juego simplemente ejecuta un ciclo infinito con los tres pasos siguientes:
- Lee los controles
- Ejecuta la lógica del juego
- Redibuja la pantalla
En el primer paso --Lee los Controles-- checa si el jugador presionó algún botón del control.
En el segundo paso --Ejecuta la lógica del juego-- es donde interpreta lo que leyó de los controles para mover el personaje o vehículo del jugador, ejecuta los algoritmos de inteligencia artificial de los oponentes y calcula la física del juego.
En el tercer paso --Redibuja la pantalla-- vuelve a dibujar la pantalla para que todos los objetos del juego (personaje del jugador, oponentes, items, etc.) aparezcan en sus nuevas posiciones.
Al ejecutar estos tres pasos repeditamente y con suficiente velocidad, por lo menos unas decenas de veces por segundo, el programa simula objetos en movimiento que reaccionan a las acciones del jugador.
El "hardware" de simpleJ ya tiene un ciclo implícito
Todo el proyecto simpleJ está inspirado en las computadoras personales de los principios de los ochentas, como la Atari 800 y Commodore 64, que tenian un hardware especializado para hacer videojuegos (similar al hardware del Colecovision y del Nintendo Entertainment System).
La parte "central" de simpleJ es el Integrated Audio and Video Controller (IAVC), un "chip" simulado de audio y video con características muy similares a las de los chips empleados en las computadoras y consolas de videojuegos de esa época.
El IAVC redibuja la pantalla 25 veces por segundo y cada vez que termina de redibujarla manda un Vertical Blank Interrupt (VBI) al CPU indicandole que es un buen momento para modificar el contenido de la pantalla. Un interrupt (interrupción) es una señal que algún cirtuito le manda al CPU para avisarle que ocurrió algo. Al recibir un interrupt, el CPU suspende momentaneamente lo que estaba haciendo para atender la interrupción. En simpleJ, a manera de especificar el código que debe ejecutarse para atender esa interrupción es definiendo un procedimiento llamado vbi().
Por lo tanto, el procedimiento vbi() se ejecuta automáticamente 25 veces por segundo y representa un ciclo implícito, sincronizado con el redibujado de la pantalla
Un "juego" sencillo
Aquí está el fuente completo de un programa en simpleJ que te permite emplear las flechas del teclado para mover una bola en la pantalla:
final BOTON_ARRIBA = 1;
final BOTON_ABAJO = 2;
final BOTON_IZQUIERDA = 4;
final BOTON_DERECHA = 8;
final ANCHO = 256;
final ALTO = 192;
final SPRITE = 0;
final IMG_BOLA = 20;
final DIAMETRO = 8;
var x = 128;
var y = 96;
var vx, vy;
var botones;
setSmallSpriteImage(SPRITE, IMG_BOLA);
leeControl() {
botones = readCtrlOne();
}
fisica() {
vx = 0;
vy = 0;
if (isButtonDown(botones, BOTON_ARRIBA))
vy = -5;
if (isButtonDown(botones, BOTON_ABAJO))
vy = 5;
if (isButtonDown(botones, BOTON_IZQUIERDA))
vx = -5;
if (isButtonDown(botones, BOTON_DERECHA))
vx = 5;
x = clamp(x + vx, 0, ANCHO - DIAMETRO);
y = clamp(y + vy, 0, ALTO - DIAMETRO);
}
clamp(valor, min, max) {
if (valor > max)
return max;
if (valor < min)
return min;
return valor;
}
dibuja() {
putSpriteAt(SPRITE, x, y);
}
vbi() {
leeControl();
fisica();
dibuja();
}
Para ejecutar el programa
Para ejecutar este programa sigue los pasos siguientes:
- Abre el simpleJ devkit. En donde dice "
or type the name of a new project -->", escribe el nombre de un nuevo proyecto (por ejemplo:pruebas) y haz click en el botónOK. - Copia el fuente del programa y pégalo dentro del área de Program del devkit.
- Haz click en el botón
(start) para ejecutar el programa. - Usa las flechas del teclado para mover la bola dentro del área Video del devkit.
Cómo funciona el programa
Veamos ahora el programa parte por parte para entender cómo funciona.
Declaración de constantes
El programa empieza con la declaración de varias constantes. Las primeras cuatro constantes son para los botones de las flechas:
final BOTON_ARRIBA = 1; final BOTON_ABAJO = 2; final BOTON_IZQUIERDA = 4; final BOTON_DERECHA = 8;
En simpleJ se emplea la función predefinida readCtrlOne() para leer que botones están presionados en el "control" del primer jugador (realmente es el teclado de tu computadora). Esta función devuelve un número indicando que botones están presionados. Estas constantes le dan un nombre a cada uno de los números que corresponden a las flechas para que sea más sencillo leer y entender el programa.
Las dos constantes siguientes declaran el ancho y alto de la pantalla de simpleJ, en pixeles:
final ANCHO = 256; final ALTO = 192;
La últimas tres constantes están relacionadas con la bola que vamos a mover en la pantalla:
final SPRITE = 0; final IMG_BOLA = 20; final DIAMETRO = 8;
En simpleJ se emplean sprites para mostrar en la pantalla los personajes o vehículos de tu videojuego. Un sprite es una pequeña imagen que se puede colocar, delante del fondo, en cualquier lugar de la pantalla. El "hardware" de simpleJ soporta 32 sprites, numerados del 0 al 31. En este ejemplo vamos a usar el sprite cero para dibujar la bola. A cada sprite se le puede asignar una imagen de 8x8 pixeles o de 16x16 pixeles. Pueden haber hasta 128 imágenes de 8x8 pixeles y otras 128 imágenes de 16x16 pixeles. Al resetear el "hardware" de simpleJ, las 128 imágenes de 8x8 para los sprites se inicializan automáticamente con ciertos patrones predefinidos. Estas imágenes están numeradas del 0 al 127, y la imagen número 20 corresponde a una bola. Como la imagen del sprite es de 8x8 definimos que el diametro de la bola es de 8 pixeles (en realidad la imágen de la bola es un poco más pequeña, no llena ocupa todo el espacio de 8x8, pero en este ejemplo vamos a ignorar ese detalle).
Las variables
Después, en el programa, vienen las declaraciones de las variables globales:
var x = 128; var y = 96; var vx, vy; var botones;
Las variables x y y son para almacenar la coordenadas de la esquina superior izquierda del sprite de la bola. Al colocar un sprite en la pantalla, el origen del sistema de coordenadas está en la esquina superior izquierda de la pantalla; los valores en el eje x se incrementan hacia la derecha y los valores en el eje y se incrementan hacia abajo:

Las coordenadas se miden en pixeles. Creamos las variables
x y y con los valores iniciales 128 y 96 respectivamente, para que al iniciar la ejecución del programa la bola esté más o menos al centro de la pantalla.
La velocidad de la bola es un vector. Tiene una magnitud --qué tán rápido va la bola-- y una dirección --hacia donde va la bola. Podríamos almacenar esta información en dos variables, una llamada magnitud y otra llamada dirección, pero resulta más práctico para los cálculos de movimiento representar el vector velocidad separado en sus componentes horizontal y vertical dentro de las variables vx y vy respectivamente:

En otras palabras,
vx y vy representan la velocidad horizontal y la velocidad vertical de la bola. Si vx es positivo la bola se está moviendo hacia la derecha, y si es negativo se está moviendo hacia la izquierda. De la misma manera, si vy es positivo la bola se esta moviendo hacia abajo, y si es negativo se está moviendo hacia arriba. El movimiento exacto de la bola es la suma de su movimiento horizontal y su movimiento vertical.
La última variable global que declaramos es botones. Vamos a emplear esta variable para almacenar el número que devuelve la función predefinida readCtrlOne(), indicando qué botones están presionados en ese momento.
El siguiente paso en la inicialización del programa es asignarle a nuestro sprite la imagen de una bola:
setSmallSpriteImage(SPRITE, IMG_BOLA);
El procedimiento predefinido setSmallSpriteImage() recibe dos argumentos: spriteIndex e imageIndex. El primer argumento, spriteIndex, es el número del sprite al que queremos asignar una imagen de 8x8 pixeles. El segundo argumento, imageIndex, es el número de la imágen que le queremos asignar. Lo que estamos haciendo aquí es asignarle al primer sprite (SPRITE es 0) la imagen de una bola (IMG_BOLA es 20, que corresponde a la imagen de una bola.
Leer el control
Después viene la definición del procedimiento leeControl:
leeControl() {
botones = readCtrlOne();
}
Este procedimiento corresponde al paso #1 del ciclo principal de un videojuego (Lee los controles). Nuestro procedimiento simplemente llama a la función predefinida readCtrlOne() y almacena el valor que devuelve en la variable global botones.
La física
En el siguiente procedimiento es donde calculámos la física de nuestro "juego":
fisica() {
vx = 0;
vy = 0;
if (isButtonDown(botones, BOTON_ARRIBA))
vy = -5;
if (isButtonDown(botones, BOTON_ABAJO))
vy = 5;
if (isButtonDown(botones, BOTON_IZQUIERDA))
vx = -5;
if (isButtonDown(botones, BOTON_DERECHA))
vx = 5;
x = clamp(x + vx, 0, ANCHO - DIAMETRO);
y = clamp(y + vy, 0, ALTO - DIAMETRO);
}
El procedimiento fisica() corresponde al paso #2 del ciclo principal del juego: Ejecuta la lógica del juego. Debe calcular la nueva posición de la bola, tomando en cuenta qué botones del control están presionados y cuidando que la bola no se salga de la pantalla.
Empezamos por asumir que la velocidad de la bola, tanto en su componente horizontal como en su componente vertical, es cero. Después checamos que botones del control están presionados y empleamos esa información para modificar el componente correspondiente del vector velocidad.
Una manera sencilla de checar si está presionado el botón que corresponde a la flecha para arriba es viendo si el valor de la variable botones es igual a BOTON_ARRIBA, con esta expresión: botones == BOTON_ARRIBA. Este sistema funciona, pero tiene un pequeño problema. Si el jugador presiona simultáneamente dos botones, por ejemplo la flecha hacia arriba y la flecha hacia la izquierda, entonces el contenido de la variable botones va a ser igual a la suma de BOTON_ARRIBA mas BOTON_IZQUIERDA (es decir, 1 + 4 = 5). Y en este caso nuestro programa no detectaría que está presionado el botón de flecha hacia arriba.
Una mejor manera de detectar si un botón está presionado es empleando la función predefinida isButtonDown(). Esta función espera dos argumentos: buttons y mask. El primer argumento, buttons, es un valor devuelto por la función readCtrlOne() o readCtrlTwo(), el cual puede estar indicando que hay varios botones presionados simultáneamente. El segundo argumento, mask, indica el botón que queremos saber si está apoyado . La función devuelve true (verdadero) si ese botón está apoyado o false (falso) si no lo está, independientemente del estado de los demás botones.
Nota: El segundo argumento que espera la función isButtonDown(), el argumento mask, en realidad indica un conjunto de botones de los cuales nos interesa saber si está apoyado cualquiera de ellos. Pero dejaremos la explicación de ese caso para algún otro artículo, probablemente alguno donde expliquemos algunos detalles interesantes del sistema binario que usan las computadoras para representar los números.
Al emplear la función isButtonDown(), la manera de checar si está presionado el botón que corresponde a la flecha hacia arriba es con la expresión: isButtonDown(botones, BOTON_ARRIBA). Empleamos esta expresión dentro de un if para que cuando esté presionado ese botón modifiquemos el componente vertical del vector velocidad de la bola:
if (isButtonDown(botones, BOTON_ARRIBA)) vy = -5;
Al asignar un -5 a la variable vy, estamos indicando que la bola se tiene que mover de 5 pixeles hacia arriba. Como este procedimiento se va a ejecutar 25 veces por segundo (lo vamos a estar llamando dentro del procedimiento vbi() que se ejecuta cada vez que se termina de redibujar la pantalla) eso implica que la bola se va a mover hacia arriba a una velocidad de 125 pixeles/segundo (5 por 25 da 125). Después, hacemos lo mismo para las otras tres direcciones (abajo, izquierda y derecha).
Ya que actualizamos los componentes horizontal y vertical del vector velocidad, de acuerdo a las teclas que están presionadas, los empleamos para calcular la nueva posición de la bola. Para esto basta con sumar el componente horizontal de la velocidad a la posición horizontal de la bola (x + vx) y el componente vertical de la velocidad a su posición vertical (y + vy). Debemos tener cuidado de que que la bola no se salga de la ventana. Decidimos emplear una función llamada clamp() para asegurar que los valores de las variables x y y se mantengan dentro de los límites válidos. La función clamp() espera tres argumentos: el nuevo valor, el límite inferior y el limite superior. Cuando el nuevo valor está dentro de los límites, la función clamp() simplemente devuelve ese mismo valor. Pero, cuando se sale de esos límites, devuelve el límite inferior si el nuevo valor era menor que él o el límite superior si el nuevo valor era mayor que él. En inglés la palabra clamp significa abrazadera y también existe el verbo to clamp que significa asegurar: fijar sólidamente. Estamos fijando los valores dentro de un rango válido, en el caso horizontal el valor mínimo de x es cero (cuando la bola está en el extremo izquierdo de la ventana) y el valor máximo es ANCHO - DIAMETRO (cuando la bola está en el extremo derecho de la ventana).
Después viene la definición del procedimiento clamp():
clamp(valor, min, max) {
if (valor > max)
return max;
if (valor < min)
return min;
return valor;
}
Este procedimiento es tan sencillo que no requiere ninguna explicación.
La parte gráfica
La parte gráfica, hacer que la bola se dibuje en la posición correcta de la pantalla (que corresponde al paso #3 Redibuja la pantalla), es trivial con simpleJ:
dibuja() {
putSpriteAt(SPRITE, x, y);
}
Lo único que hace es emplear el procedimiento predefinido putSpriteAt() para colocar el sprite de la bola en la posición de la pantalla indicada por las variables x y y.
El procedimiento putSpriteAt() espera tres argumentos: spriteIndex, x y y. El argumento spriteIndex indica cual es el sprite que hay que mover, mientras que los argumentos x y y indican la posición, en pixeles, donde debe quedar su esquina superior izquierda.
El "ciclo" principal del juego
Como mencionamos anteriormente, en simpleJ el procedimiento vbi() funciona automáticamente como un ciclo implícito que se ejecuta 25 veces por segundo. Aquí lo aprovechamos para que se ejecuten los tres pasos de nuestro "juego":
vbi() {
leeControl();
fisica();
dibuja();
}
En diagonal, la bola se mueve más rápido
Ejecuta el programa y prueba lo que pasa cuando presionas simulatáneamente la fecha derecha y la flecha abajo. Al combinar el movimiento horizontal con el movimiento vertical, la bola se mueve en diagonal. Y no sólo eso, prueba mover la bola sólo horizontalmente o verticalmente y después vuelvela a mover en diagonal. ¿Viste lo que pasa? ¡La bola va más rápido cuando se mueve en diagonal! Esto se debe a que la velocidad en diagonal es la suma de sus componentes horizontal y vertical. Si la bola se mueve de 5 pixeles hacia la derecha y, al mismo tiempo, se mueve de 5 pixeles hacia abajo, entonces la distancia que recorrio fue de más de 5 pixeles. Pero no fue de 10 pixeles, porque los desplazamientos de 5 pixeles fueron en direcciones diferentes.
Para calcular cual es la velocidad de la bola cuando se mueve en diagonal, podemos emplear el Teorema de Pitágoras: en un triángulo rectángulo la suma de los cuadrados de los catetos es igual al cuadrado de la hipotenusa. En este caso, las velocidades horizontal y vertical forman los catetos del triángulo, mientras que la suma de esas dos velocidades (la diagonal) es la hipotenusa:

Empleando el Teorema de Pitágoras, calculamos que la velocidad en diagonal de la bola es de:

7.071 pixeles/segundo. Un 41% más rápido.
En algún otro artículo explicaremos cómo hacer para que la bola vaya siempre a la misma velocidad, independientemente de la dirección en la que se esté moviendo.

hace 1 año 12 semanas
hace 1 año 20 semanas
hace 1 año 20 semanas
hace 1 año 20 semanas
hace 1 año 20 semanas
hace 1 año 20 semanas
hace 1 año 20 semanas
hace 1 año 20 semanas
hace 1 año 20 semanas
hace 1 año 20 semanas