Como hacer un "Pippols" [WIP]


== WORK IN PROGRESS == 

Introducción

El propósito de este tutorial, además de enseñar más cositas sobre la programación de videojuegos y NLKEngine, es hacer tributo y mención al famoso juego para MSX con nombre "Pippols" desarrollado por Konami.



Como se suele hacer en estos casos, lo primero es saber de qué va el juego :), haberlo completado o ver algún video donde alguien se lo pase y conseguir rippear (extraer) los gráficos del juego original o que alguien te los haga. En mi caso he utilizado un emulador y capturando pantallas he ido sacando tiles y sprites a los distintos PNGs.

Con la música y sonidos he procedido de forma similar. Capturando de un emulador MSX.

Doy por supuesto que para poder seguir con este tutorial es necesario bajarse el NLKENGINE SDK y disponer de algún editor de textos (recomiendo notepad++ o pspad). Bueno, pues tras esta breve introducción, vamos por ello!

¿Cómo nos organizamos?

Bien, la estructura inicial de archivos y directorios que voy a plantear es esta:

data\
                scripts\
                               game.pi
                               stage.pi
                               title.pi
                               player.pi
                               enemy.pi
                               hud.pi
                               logo.pi
                images\
                sounds\
                musics\

main.pi
config.pi
command.ini
nlkEngine.exe

La idea es que el "main.pi" arranque "game.pi" y este sea el que se encargue de arrancar el logo, que arrancará el menú y este a su vez la partida. La idea es que "game" sea el módulo global donde todo los datos "globales" del juego se gestionen desde él (por ejemplo el stage por el que vamos, el score, etc.)
"main.pi" por tanto quedará como un mero punto de arranque. Más adelante lo vemos.

Cuando le damos al ejecutable, lo primero que ejecuta el config.pi. Aquí se leen parámetros de inicialización como la resolución de la ventana, si queremos fullscreen, etc.

CONFIG.pi

Aquí vamos a definir un par de constantes globales (RESX, RESY) que usaremos por todo el juego y que vamos a usar como resolución de trabajo. En nuestro caso será de 256x192 la misma que se usa en un MSX :)
Sin embargo nuestra ventana no va a tener esa resolución, más que nada por no quedarnos ciegos y porque además, queremos ver los pixeles en su tamaño original. De ahí que el motor distinga entre resolución de trabajo y resolución de dispositivo. Nosotros usamos siempre la de trabajo y es el motor quien se encarga de volcar a la de dispositivo. Por nuestra parte, creamos una ventana de 1024x768 (4 veces más grande que la de MSX)
TODO: Codigo fuente config.pi

MAIN.pi

Tras ejecutar el "config", el motor busca el script de arranque (por defecto main.pi). Como hemos dicho antes, lo único que hace este archivo será arrancar el "game" que es el que va a manejar todo el "cotarro". La espina dorsal del juego.
import package "data/scripts/*.*"

class Service
{
                function Init ()
                {
                               Service_Run ("game", "game");
                }
}

Vemos la primera línea "import package". Esto sirve para indicar que nuestros scripts se encuentran en esa carpeta relativa al proyecto. Podemos agregar tantas como queramos y con la complejidad de subcarpetas que queramos, pero, como es un tutorial de iniciación, no requerimos complicaciones, complicaciones fuera!
Los servicios cuando arrancan ejecutan una función con nombre "Init", si existe claro está. Nosotros lo que hacemos es que arranque nuestro script "game" y que le de al servicio el nombre "game" también.

Comencemos pues por el principio de nuestro juego. El logo, en este caso como el del juego original de Konami.

LOGO.pi

Es el encargado de sacar el logo de Konami con el fondo azul. Que sube hacia arriba y que pasados unos segundos pasa automáticamente a la titlescreen sino es que hemos pulsado alguna tecla previamente.


Vamos a ver el script de esto:

class Service
{
                constants:
                               LOGO_WIDTH = 96;
                               LOGO_HEIGHT = 34;
               
Definimos un par de constantes con el tamaño de nuestro logo. Es el ancho y el alto de la textura "logo.png". Podríamos sacar este tamaño con un par de funciones y no tenerlo como constante pero eso no mola de cara a la programación multiplataforma. Así que ya voy indicando cosas interesantes como está, nada de leer el tamaño de la textura porque igual en otra plataforma la textura se nos hace añicos, cambia de proporción o tamaño o vete tú a saber y es que el hardware de otros dispositivos puede obligarnos a trabajar en formatos o tamaños restrictivos.

                properties:
                               game = null;
                               logoTex = null;
                               posY = 0;
                               timer = 0;
                              
                function Init ()
                {
                               logoTex = Texture_Load ("logo.png");
                }

Aquí defino mis propiedades de módulo. En "game" tendré acceso al módulo principal (nuestro game.pi), en "logotex" me guardaré la textura para el logo, en "posY" tendré la posición del logo en pantalla, para simular que sube hacia arriba y por último, en "timer" tendré un temporizador para saltar al menú automáticamente si el usuario no pulsa ninguna tecla.

                function Final ()
                {
                               Texture_Delete (logoTex);
                }

La función Final, al igual que la Init, se llama automáticamente cuando se trata de Servicios. ¿Y cuando se llama? pues cuando el servicio se destruye o borra. Aquí lo que hacemos el liberar el handle a la textura previamente cargada en el Init.

                function Start ()
                {
                               posY = RESY;
                               _change ("upping");
                }
               
Trás el Init de un servicio se invoca al Start. Mientras que el Init sólo se invoca una vez, tras la creación de un servicio, el Start se puede invocar varias veces. Por ejemplo, si pausamos un Servicio y lo volvemos a reanudar (Service_Stop/Service_Start).

                function Move ()
                {
                               if (Input_IsAnyKeyPressed () >= 0)
                               {
                                               game.RunTitle ();
                               }
                }

Los servicios cuando arrancan o se activan, en cada frame, ejecutan su Move y su Draw. En el primero se gestiona la lógica del servicio y en el segundo se utilizan primitivas de render para dibujar lo que sea. En nuestro caso, en el Move miraremos si se pulsa alguna tecla y en el Draw dibujaremos un fondo azul y el logo de Konami (ver más abajo). Además del Move, algo chulo del NLKScript es que permite usar una máquina de estados para la lógica. O sea, que junto con el Move se ejecuta un estado. Si miramos la funcion Start, verás que uso: _change ("upping"). Con eso le estoy diciendo que seteo la máquina de estados al estado "upping". O sea, que en  cada frame además del Move ejecutará lo que hay en "upping". En este caso, "upping" lo que hace es actualizar "posY" y mirar que no pase de la línea 64. Una vez ocurre eso, paso al otro estado con nombre: "standby" donde miro que si pasan más de 3 segundos, aviso al módulo "game" que quiero arrancar la TitleScreen.

                state "upping"
                {
                               posY -= GetFTime() * 8.0f;
                               if (posY < 64)
                               {
                                               posY = 64;
                                               timer = GetTime();
                                               _change ("standby");
                               }
                }
               
                state "standby"
                {
                               if ((GetTime() - timer) >= 3000)
                               {
                                               game.RunTitle ();
                               }
                }
               
Ahora, en el "Draw", dibujamos un fondo azul y el logo centrado en horizontal. Como ves, uso "posY" para determinar la posición vertical donde dibujarlo.

                function Draw ()
                {
                               Render_DrawBox (0, 0, RESX, RESY, ARGB(255,32,32,247));
                               Render_DrawTex (logoTex, (RESX-LOGO_WIDTH)/2, posY, LOGO_WIDTH, LOGO_HEIGHT, 0, ARGB(255,255,255,255));
                }
}

Con "Render_DrawBox" dibujo una caja con un color de relleno sólido. Y con "Render_DrawTex" dibujo una textura con posición, tamaño, ángulo y tinte de color que se quiera. Para poder ver otros parámetros y funciones recomiendo echar un vistazo al archivo de ayuda que se entrega junto al SDK.

TITLE.pi

Aquí mostramos el logo del juego y desde aquí controlaremos el "PRESS SPACE KEY" y el inicio del modo demostración.

STAGE.pi

"stage.pi" será el script destinado a gestionar la jugabilidad, el escenario vamos. Aquí se creara un "player.pi", se decidirá que set de tiles cargar y ubicar los distintos enemigos e ir generándolos a medida que avanza el scroll.

HUD.pi

Desde "hud.pi" se controlará la parte de interfaz de usuario dedicada al juego. Esa zona de la derecha que indica HISCORE, SCORE, REST y el mapita de por dónde vamos.
El HISCORE será una propiedad que tendremos en nuestro módulo "game". Esta propiedad la guardaremos en el registro para recordar este valor siempre que ejecutemos el juego.
El SCORE será una propiedad temporal que sólo tendremos en cuenta durante la partida. Aún así, como permanece viva durante los distintos stages, habrá que tenerla en "game".
Lo mismo que el REST, el número de vidas que le queda a nuestro personaje.
El mapita representa la pantalla en la que estamos. En el vemos nuestra situación y su distancia para llegar al final del juego.


 == WORK IN PROGRESS ==

No hay comentarios:

Publicar un comentario