== 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
scripts\
game.pi
stage.pi
title.pi
player.pi
enemy.pi
hud.pi
logo.pi
images\
sounds\
musics\
main.pi
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");
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