NLKEngine


¿Quieres aprender a hacer tu propio videojuego? Pues pasa que te enseño!


Era yo un chaval de Castellón, que vió en el escaparate de una tienda de Murcia, allá cuando tenía 9 años (rondaría el año 1982), lo que creo recordar como un Commodore 64. Desde ese día, mi fascinación total por estas máquinas que hicieron en mí tanto daño :) como para inventar el primer videojuego de máquina de escribir....


Si, es cierto :) .. se me iba la cabeza, me imaginaba que mi máquina de escribir era un ordenador. Pero bueno, terminé el caduco EGB (1988) y mis padres por mis notas (todas sacadas a conciencia! sabía lo que me jugaba) me regalaron un fantástico MSX. Ordenador con el que aprendí a programar en BASIC y más tarde en Ensamblador del Z80.

Este fué el comienzo, a más programaba más cosas quería hacer y allá por 1999 fué cuando se me ocurrió montar un "GameMaker"o sistema fácil de crear videojuegos. Para que todo el mundo pudiera hacerlos. Desde entonces vengo trabajando con esa idea. Fué cuando desarrollo el lenguaje script del motor para dar soporte de código a nivel de diseño y más tarde el script se apodera de todo y es con lo que se programa al 100% todos los juegos de NLKEngine.

Aunque inicialmente mi idea era crear un sistema de creación basado en un interface de usuario amigable. Poco a poco voy abandonando esa idea como programador que soy. Un interface me resta control y requiero del máximo control posible. De ahí que al final ganó la programación basada en script frente al interface. Lo cual, no se excluye porque realmente ahora (y así se usa), se pueden crear plantillas con interface hecho por script para hacer tipos de juego con el mismo script.

La cuestión, NLKEngine nace en 1999 y viene experimentando evoluciones, adaptación y pruebas, muchas pruebas, tanto en juegos 2D, como 3D, como herramientas, como apps, como emulación, etc. Se ha convertido en un sistema multiplataforma de mucho control. Que funciona con eficacia y puede demostrar su valía con todos los títulos publicados hasta el momento: Codename Silver, WantedGuns, BillyBlade, DraculaTwins, Saga Armix, Living Hell, Red Forces, Cosmic Tiles, Gazzel, etc. etc. etc.


El motor ha sido probado y saboreado por muchos programadores de diferentes niveles y estoy satisfecho con la reacción de todos. La verdad es que le ven potencial, lo disfrutan y ven que tiempo invertido es conseguir más y más cosas rapidamente, fácilmente.

Bueno, pues nada más. Que ya he vanagloriado bastante el motor :). Quiero aprovechar este blog para crear cursitos y lecciones de uso del NLKEngine. Para que cualquiera pueda hacerse su videojuego, su plataformas, su juego de naves, lo que sea. Que todo el mundo pueda disfrutar haciendose su juego y además aprender a programar. Que lo cierto es que es algo muy artesanal y divertido :)

¡Ánimo y suerte con vuestros proyectos!

¡Comencemos con el NLKEngine!

Lo primero es descargase el SDK dónde vienen ejemplos, guidelines y lo que es más importante: el ejecutable del motor.
Hay que registrarse en la web y aceptar las condiciones de uso. El SDK viene con todo lo necesario para comenzar. Con ejemplos, guías básicas y avanzadas.
El motivo final es que la gente pueda aprender a programar juegos, labor compleja más por la parte de terminarlos que por la parte de desarrollarlos. Y por supuesto, pasarlo bien mientras se hace.

Os propongo la siguiente estructura de carpetas para organizar los datos (recursos) del juego. Esta estructura colgará de una carpeta raíz llamada “data”.

Data\
    Textures\
    Music\
    Scripts\
    Sounds\


Luego en la raíz del proyecto tendremos los archivos de arranque que explicaremos más adelante junto el ejecutable Windows:

Config.pi
Main.pi
nlkEngine.exe


Además de estos (durante el desarrollo) tendremos el “command.ini” que simplemente nos permite establecer unos parámetros de arranque del ejecutable donde se indica si queremos ejecutar en debug, etc.

<command.ini>
-debug -run main.pi

Esto mismo podemos hacerlo sin el archivo “command.ini” teniendo un acceso directo o un archivo .BAT (el ejecutable permite argumentos para configurar su inicialización, esta información se puede encontrar en la documentación adjunta al SDK)

<run.bat>
           nlkEngine -debug -run main.pi


A continuación vamos a detallar lo que ocurre una vez arrancamos el ejecutable. Tras el arranque, antes de inicializar la pantalla, audio, etc. Se ejecuta el script “Config.pi”. Este script indica al ejecutable con que parámetros se quiere configurar todo.

class SERVICE
{
       defines:
             // Resolucion del IPAD
             RESX = 1024;
             RESY = 768;
            
       function Init ()
       {
             System_SetParam (SP_TITLE, "MyPROJECT");

       #if __MACHINE_PC__
             System_SetParam (SP_CURSOR, true);
       #else
             System_SetParam (SP_CURSOR, false);
       #endif
      
             System_SetParam (SP_RESX, RESX);
             System_SetParam (SP_RESY, RESY);
            
             System_SetParam (SP_SCREENROT, 1); // LANDSCAPE

             System_SetParam (SP_BPP, 16);
      
             System_SetParam (SP_SND_STEREO, true);
             System_SetParam (SP_SND_BITS, 16);
             System_SetParam (SP_SND_FREQ, 44100);


             // NO usamos 3D asi que no consumimos memoria para esto
             System_SetParam (SP_POOL_MAX3D, 0);           
             System_SetParam (SP_POOL_MAX3DALPHA, 0);
            
             // reservamos 256Kb para primitivas de dibujo inicialmente
             System_SetParam (SP_POOL_MAX2D, 256*1024);    
       }
}

Con <defines> crea 2 constantes globales (RESX, RESY) que serán las que usaré durante todo el juego para hacer referencia a la resolución de la pantalla. Hay que tener en cuenta que el motor trabaja con 2 resoluciones: La virtual y la real (la del dispositivo). La virtual es con la que trabajamos nosotros a la hora de dibujar, posicionar cosas, etc. La real será la que tenga la pantalla del dispositivo final. Por ejemplo: Aquí vamos a configurar que nuestra resolución virtual es 1024x768. Trabajaremos así en todo momento. Sin embargo, si arrancamos el juego en un IPHONE3 lo que ocurrirá es que automáticamente todo se redimensionara a la resolución de 480x320. Esta conversión es transparente para nosotros.

Otras cosas que hacemos en el “config.pi” es habilitar el cursor si estamos en PC (siempre nos facilita el trabajo ver por donde llevamos al puntero a la hora de hacer clicks). Indicar que usaremos LANDSCAPE en vez de PORTRAIT y algunos otros parámetros de audio y video.

Tras el config, el motor se inicializa y arranca el siguiente script “main.pi”. Realmente es el “main.pi” si no hemos establecido ningún otro por defecto. Podríamos establecer otro script de arranque usando el parámetro de exe “-run”.

Bien, ya estamos en el “main.pi”. Este script ha de ser un Servicio. Los Servicio son una herramienta del motor que permite la ejecución en hilos y síncrona de procesos.
Cuando arranca un servicio, lo primero es ejecutar su función INIT. Tras ello, en cada frame, se ejecuta primero el MOVE y luego el DRAW. En el MOVE actualizamos la lógica y en el DRAW procesamos primitivas de dibujo. Cuando borramos un servicio, previo a su borrado se ejecuta la función FINAL. Además de estas funciones también tienen la función START y STOP. Que se invocan cuando arranca o se detiene un Servicio.

import package "data/scripts/*.*"

class Service
{
       properties:
            
       function Init ()
       {
             Render_SetProperty (RP_INRESX, 1024);
             Render_SetProperty (RP_INRESY, 768);
            
             // Determinamos los paths por defecto para texturas, scripts, musica y fx
             System_SetDirectory (DIRECTORY_GAME, "data/");
             System_SetDirectory (DIRECTORY_TEXTURE, "textures/");
             System_SetDirectory (DIRECTORY_SCRIPT, "scripts/");
             System_SetDirectory (DIRECTORY_MUSIC, "music/");
             System_SetDirectory (DIRECTORY_SOUND, "sound/");
            
             // Arrancamos el modulo de juego
             game = Service_Run ("game", "game.pi");
       }
      
       function Final ()
       {
       }

       function Start ()
       {
       }

       function Stop ()
       {
       }
      
       function Move ()
       {
       }
      
       function Draw ()
       {
       }
}

Bueno, pues así comienza la ejecución de cualquier aplicación o juego en nlkEngine. A partir de aquí ya es cuestión de cómo se organicen los datos y el código. Todo se puede hacer de muchas formas distintas. Nosotros vamos a proponer la siguiente estructura de scripts que ubicaremos en “data/scripts/”.

Game [Service]
                Aquí se centraliza todas las partes de nuestro juego (menu, pantallas, etc.). Será la columna vertebral del mismo.

Menu
                Módulo que se encargará de gestionar el menú del juego. A partir de aquí, según el nivel de nuestro menú igual se requieren de submodulos, etc. Según la profundidad y complejidad del sistema de menú.

HUD
                Módulo encargado de representar el interface del juego mientras se juega.

Stage
                Módulo encargado de gestionar un escenario o pantalla.

Player
                Módulo encargado de controlar al jugador y gestionar sus acciones.


TUTORIAL 1 : HELLO WORLD!!

Vamos al típico ejemplo sencillo, directo, que muestra como mostrar un texto "hello world".
Creamos una carpeta para el ejemplo: "\myhelloworld" por ejemplo.

Ahí dentro meteremos el nlkengine.exe (ejecutable Windows dispnible en el SDK)

Si le damos al exe, se abrirá una pantalla en negro. Además en fullscreen. Eso es porque no encuentra ningún archivo de arranque ni nada. Verás que salen textos en amarillo, es información de depuración. Te estará indicando que no encuentra ningún archivo "main.pi".

NOTA: Si no se le indica otra cosa, "main.pi" es el nombre del script que busca por defecto el motor

Para quien no lo sepa, para salir de la aplicación tocará que hagas un ALT+F4 o bien ALT+TAB cambiar al escritorio y matar el proceso.

Bien, pues vamos a crear con un editor de texto el "main.pi". Te recomiendo el notepad++, más que nada porque en el SDK (carpeta Tools) viene el archivo para que puedas configurarte este editor con colorines.

Vamos poco a poco, vamos a crear nuestro archivo "main.pi"

class Service
{
     function Init ()
     {
     }
}

Ahora si le damos al "nlkengine.exe" vemos que se pone en fullscreen pero ya no saca ningún error. Bien!

Sin embargo, yo no se a vosotros, pero a mi, hacer pruebas y estar cambiando a fullscreen me supone un rollo, más que nada por el cambio de contexto y que se hace pesado y engorroso.
¿Cómo lo quitamos? bien, vamos a indicar al motor que queremos arrancar en modo debug, que por defecto trabaja en modo ventana y esto además nos permitirá usas la utilería de depuración que para las pruebas siempre va bien.

Para ello vamos a crearnos un archivo de texto "command.ini". Dentro vamos a meter esto:

-debug -run main.pi

Si ahora le damos al "nlkengine.exe" veremos que tenemos la pantalla negra pero ya en modo ventana. Mucho más cómodo. El "command.ini" es uno de los archivos que busca primero el motor. Le sirve para saber con que parámetros opcionales queremos arrancar. Por lo que ahora nos respecta, arrancaremos en modo debug y le decimos (aunque eso ya lo sabe por defecto) que queremos ejecutar el "main.pi"

Bueno, ya tenemos esto listo para seguir trabajando en nuestro "hello world".
Volvemos al "main.pi". Esto que ejecuta el motor. Un Servicio. Los servicios son como hilos de ejecución que nos permite enganchar código y con ello crear la lógica de nuestro programa.
Cuando un servicio arranca lo primero que hace es llamar a la funcion INIT.
Ahora mismo, tal y como tenemos nuestro "main.pi", en INIT no hay nada. Vamos a meter algo de código:

class Service
{
    properties:
        font = null;

    function Init ()
    {
        font = Font_Load ("Arial", 8, 16);
    }
}

¿Qué es todo esto?! bueno. A un servicio le podemos asociar propiedades y constantes. Con properties estamos creándole propiedades (variables de módulo). En "font" guardaremos el handle de la fuente "Arial" que queremos usar, ¿Para qué? pues para escribir nuestro mensaje "Hello World!!" :)

Pero, ¿Dónde decimos que queremos dibujar esto?¿Y en que lugar de la pantalla negra esta que sale?¿Y con que color de letra? .... tranquilidad!! poco a poco :)

Seguimos ampliando nuestro "main.pi"...


class Service
{
    properties:
        font = null;

    function Init ()
    {
        font = Font_Load ("Arial", 8, 16);
    } 

    function Draw ()
    {
        Render_Print (font, 10, 10, "Hello World!", ARGB(255,255,255,0));
    }
}

Ya lo tenemos, pues vamos a darle al ejecutable a ver que pasa...
Woala!! ya tenemos un "hello world!" enhorabuena! :)

TUTORIAL 2 : HOW TO MAKE A PONG


El propósito de este tutorial es que sirva como introducción al motor y al uso del lenguaje script.



El ejemplo es un juego sencillo, un juego estilo Pong, un clásico de toda la vida. En este juego, tenemos por un lado la "raqueta" que controla el jugador y por otro la pelota que rebotará con los límites de la pantalla. El objetivo es que la pelota no rebase la zona por donde el jugador puede moverse. En ese caso, si la pelota rebasa la zona, anotaremos un tanto en el marcador de goles.

Para la elaboración de este ejemplo, vamos a requerir de 4 archivos script:
<config.pi> este archivo sirve para configurar y establecer parámetros de inicialización del motor.
<main.pi> es el punto de entrada o arranque de nuestro juego o aplicación.
<ball.pi> controlará la lógica de la pelota
<player.pi> controlará la lógica de la raqueta
y por supuesto, un ejecutable del motor que se te facilita en el SDK que podrás descargar online
Bueno, vamos por ello!

Conceptos generales

Conviene conocer que los archivos con extensión .pi son archivos de texto normales que se pueden editar con cualquier editor de texto (PSPad, Notepad++, Wordpad, etc.)
Podéis mirar los tutoriales de configuración de editores como el PSPad o Notepad++ para poder activar el highlight syntax y otras características interesantes a la hora de trabajar con los scripts.
También conviene saber que el ejecutable que arrancará nuestro ejemplo, genera información relevante en el  archivo, por defecto, <gamelog.txt>. Aquí podemos encontrar errores de compilación, de ejecución, etc. NOTA: Recomendamos usar una herramienta de gestión de archivos de log como <BareTail.exe> aunque no es obligatorio ni necesario su uso.
El ejemplo que vamos a tratar a continuación, espera que los archivos .pi se encuentren ubicados al mismo nivel de carpeta que el ejecutable del motor obtenido con el SDK.
De esta manera, imaginemos que creamos una carpeta en nuestro disco duro. Tendremos este contenido:

\PongExample
      Ball.pi
      Player.pi
      Config.pi
      Main.pi 

      nlkEngine.exe

Archivo <Config.pi>

El motor tiene unos parámetros de inicialización por defecto. Realmente, el uso del archivo <config.pi> es opcional pero conviene conocerlo y prácticamente el 100% de las veces es necesario para determinar y especificar cómo queremos que arranque nuestra aplicación:

class Service
{
     defines:
        RESX = 1024;
        RESY = 768;

     function Init ()
     {
         System_SetParam (SP_TITLE, "Pong");
         System_SetParam (SP_CURSOR, false);
         System_SetParam (SP_RESX, RESX);
         System_SetParam (SP_RESY, RESY);
         System_SetParam (SP_BPP, 32);
         System_SetParam (RP_INRESX, RESX);
         System_SetParam (RP_INRESY, RESY);
     }
}


Con <defines> creamos 2 constantes globales (RESX, RESY) que serán las que se usaran durante todo el juego para hacer referencia a la resolución de la pantalla virtual. Hay que tener en cuenta que el motor trabaja con 2 resoluciones: La virtual y la real (la del dispositivo). La virtual es con la que trabajamos nosotros a la hora de dibujar, posicionar cosas, etc. La real será la que tenga la pantalla del dispositivo final. Por ejemplo: Aquí vamos a configurar que nuestra resolución virtual es 1024x768. Trabajaremos así en todo momento. Sin embargo, si arrancamos el juego en un IPHONE3G lo que ocurrirá es que automáticamente todo se redimensionara a la resolución de 480x320. Esta conversión es transparente para nosotros. Sin embargo, hay que tener en cuenta esto porque según que resoluciones finales podemos tener problemas de "aspect ratio" con la visualización de algunos gráficos.
Otras cosas que hacemos en el “config.pi” es habilitar el cursor si estamos en PC (siempre nos facilita el trabajo ver por donde llevamos al puntero a la hora de hacer clicks). Indicar que usaremos LANDSCAPE en vez de PORTRAIT y algunos otros parámetros de audio y video.
En nuestro caso, indicamos que queremos una ventana de 1024x768, igual que la resolución de trabajo.
Tras el config, el motor se inicializa y arranca el siguiente script “main.pi”. Realmente es el “main.pi” si no hemos establecido ningún otro por defecto. Podríamos establecer otro script de arranque usando el parámetro de exe “-run”.

Archivo <Main.pi>

Bien, ya estamos en el “main.pi”. Este script ha de ser un Servicio. Los Servicios son una herramienta del motor que permite la ejecución en hilos y síncrona de procesos. Así para entendernos, que permite ejecutar de forma paralela, "a la vez", código distinto.
Cuando arranca un servicio, lo primero es ejecutar su función INIT. Tras ello, en cada fotograma (frame), se ejecuta primero el MOVE y luego el DRAW. En el MOVE actualizamos la lógica y en el DRAW procesamos primitivas de dibujo. Cuando borramos un servicio, previo a su borrado se ejecuta la función FINAL. Además de estas funciones también tienen la función START y STOP. Que se invocan cuando arranca o se detiene un Servicio:

class SERVICE
{
    properties:

        ball = null;
        player = null;
        font = null;
        goals = 0;

   function Init ()
   {
       font = Font_Load ("Terminal", 30, 40);
       ball = Service_Run ("ball", "ball.pi");
       player = Service_Run ("player", "player.pi");
   }

  function Draw ()
  {
       Render_Print (font, RESX/2, 10, string(goals), ARGB(255,2555,255,255), DT_CENTER);
  }
}


En nuestro ejemplo, en la inicialización del servicio vamos a crear una fuente de letras y arrancaremos los servicios para la lógica de la pelota (ball) y del jugador (player).
En este mismo servicio, usaremos una propiedad "goals" que representaremos arriba y centrada horizontalmente, indicando los goles que nos ha marcado la pelota.

Archivo <Ball.pi>

Desde aquí controlaremos toda la lógica de la pelota:

class SERVICE
{
    constants:
      WIDTH = 8;
      SPEED = 8.0f;
    properties:
      x = 0;
      y = 0;
      dirX = 0;
      dirY = 0;
      main = null;
 
    function Init ()
    {
       main = Service_Get ("main");
    }


Definimos algunas constantes para determinar la velocidad inicial de la pelota (SPEED) y el tamaño de la caja (WIDTH) que usaremos para representarla.
Tendremos las propiedades (x, y) que determinarán la posición actual de la pantalla sobre nuestro escritorio virtual (RESX, RESY) de 1024x768 tal y como configuramos al inicio en el <config.pi>.
Las propiedades (dirx, diry) indicarán la dirección que sigue nuestra pelota.
Y en (main) guardaremos la referencia a nuestro modulo de arranque principal para actualizar la variable (main.goals)

function Start ()
{
    x = FRand (RESX/2, 1024);
    y = FRand (0, 768);
   dirX = SPEED;
   dirY = SPEED;
}


Trás un Service_Run se invoca primero el INIT del servicio y posteriormente el START del mismo. De esta forma, usaremos el START para inicializar la posición y velocidad de la pelota.

function Move ()
{
    _player = main.player;
    _bCollide = (PointInRect (x, y,
    _player.x, _player.y,
    _player.WIDTH, _player.HEIGHT));


Con la función PointInRect comprobamos si la pelota está tocando la raqueta. NOTA: Para distinguir de las propiedades y de las variables que se crean localmente, se recomienda usar el prefijo "_" delante del nombre. Pero eso ya queda a gusto de quien programa.

  x += dirX * GetFTime();
  y += dirY * GetFTime();
  if (_bCollide)
  {
     dirX = -dirX;
     x = _player.x+_player.WIDTH;
     dirX = dirX * 1.01f;
     dirY = dirY * 1.01f;
  }
  else if (x < 0)
  {
     // 1 punto menos
     main.goals++;
     Start ();
  }
  else if (x > 1024-SPEED)
  {
     dirX = -dirX;
     x = RESX - WIDTH;
     dirX = dirX * 1.01f;
     dirY = dirY * 1.01f;
  }
  if (y < 0)
  {
     dirY = -dirY;
     y = 0;
     dirX = dirX * 1.01f;
     dirY = dirY * 1.01f;
  }
  else if (y >= 768-SPEED)
  {
     dirY = -dirY;
     y = RESY - WIDTH;
     dirX = dirX * 1.01f;
     dirY = dirY * 1.01f;
  }
}


Con todas estas condiciones comprobamos que la pelota rebota contra los límites y también si se sale por la izquierda para indicar que se ha colado un gol.

   function Draw ()
   {
       Render_DrawBox (x, y, WIDTH, WIDTH, ARGB(255,255,255,255));
   }
}


Finalmente representamos la pelota con una caja de color blanco.
Efectivamente, este comportamiento lo podríamos complicar mucho más y poder tener una pelota con un gráfico animado, etc. Pero eso lo dejaremos para ejemplos más avanzados.

Archivo <Player.pi>

Por último vamos a ver que requerimos para mover la raqueta que golpeará la pelota y que impedirá que nos metan goles.

class Player
{

    constants:
      MIN_Y = 0;
      WIDTH = 10;
      HEIGHT = 100;
      MAX_Y = RESY-MIN_Y-HEIGHT;
      SPEED = 8.0f;

    properties:
      x = 100;
      y = 100;

   function Init ()
   {
   }

   function Final ()
   {
   }

   function Move ()
   {
       if (Input_IsKeyPressed (KEY_UP) && y > MIN_Y)
      {
          y -= GetFTime() * SPEED;
      }
      else if (Input_IsKeyPressed (KEY_DOWN) && y < MAX_Y)
      {
          y += GetFTime() * SPEED;
      }
   }

   function Draw ()
   {
       Render_DrawBox (x, y, WIDTH, HEIGHT, ARGB(255,255,255,255));
   }
}


La gestión es muy básica. Controlamos si el jugador pulsa las teclas de ARRIBA o ABAJO para desplazar la raqueta en esas direcciones.
En la función DRAW, representaremos la raqueta en forma de rectángulo de color blanco.
Al igual que con la pelota, podemos complicar la apariencia y comportamiento de la raqueta. Podemos hacer fácilmente un juego de dos jugadores con un poco más de lógica, arrancando otro servicio de player y  modificando el comportamiento de los goles y la pelota.
De la misma forma, este ejemplo está pensado para PC. Si quisieras ver lo mismo en una máquina IOS o ANDROID, las funciones de teclado no te servirían y deberías usar otro tipo de respuesta o eventos de entrada salida.
Pero esto es sólo el principio, poco a poco y en función de tus necesidades, podrás profundizar en las miles de formas de construir juegos y aplicaciones que te brinda este motor.

¡Ánimo!

TUTORIAL 3 : HOW TO MAKE A SNAKE


Pues eso .. un SNAKE básico de toda la vida, el de la serpiente que come manzanas :)



Podéis bajaros el ejecutable aquí <download>

ARCHIVOS

Pues al bajaros el ZIP y volcarlo a una carpeta veréis

nlkEngine.exe -> el ejecutable windows del NLKEngine
command.ini -> un archivito que le permite saber como queremos arrancar el motor
config.pi -> el archivo que configura el motor, resolución, etc.
main.pi -> el script de arranque y el que tendrá la base lógica de nuestro "juego"
snake.pi -> cada pieza de la serpiente, incluyendo la cabezota.

COMMAND.INI

En este archivo le decimos al motor que queremos arrancar en modo debug y que arranque el "main.pi" desde un comienzo. Realmente, si no se pone esto, es lo que asume por defecto, pero bueno, para que la gente vea como se pueden arrancar desde el inicio otros scripts que no sean el "main".

-debug -run main.pi

CONFIG.PI

Archivo de configuración. Puede existir o no, pero si no existe se asumen parámetros de configuración por defecto. Siempre es bueno tener un "config" y al menos configurar las resoluciones y poco más. Un titulillo para la ventanita y poco más.

class SERVICE 
{
   defines:
RESX = 512;
RESY = 512;

function Init() 
{
System_SetParam(SP_TITLE, "SUPER SNAKE");

System_SetParam(SP_RESX, RESX);
System_SetParam(SP_RESY, RESY);

System_SetParam(SP_INRESX, RESX);
System_SetParam(SP_INRESY, RESY);

System_SetParam(SP_BPP, 32);

System_SetParam(SP_CURSOR, false);
}
}

MAIN.PI

Aquí lo que hacemos es simplemente iniciar la cabeza de la serpiente (nuestro player particular) y gestionar la aparición de la manzana (cuadradote amarillo). El "main" es un SERVICE. Y los servicios tienen su propia forma de funcionar. Cuando se crean se ejecuta su función INIT. Cuando arrancan se ejecuta su función START. En cada frame se ejecuta su función MOVE y su DRAW a la hora de dibujar. Cuando paran ejecutan su función STOP y cuando se destruyen su función FINAL. Las funciones no es obligatorio tener que definirlas si no se van a usar.

class Service
{
constants:
APPLE_SIZE = 15;

properties:
head = null;
tail = null;
apple_x = 0;
apple_y = 0;

function Init ()
{
head = Service_Run ("head", "snake", ["x", 20, "y", 20]);
tail = head;
}

function Start ()
{
GenerateApple ();
}

function GenerateApple ()
{
apple_x = Rand(0, RESX-APPLE_SIZE);
apple_y = Rand(0, RESY-APPLE_SIZE);
}

function Move ()
{
_ret = IntersectRectWithRect (head.x-head.SIZE/2, head.y-head.SIZE/2, head.SIZE, head.SIZE, apple_x, apple_y, APPLE_SIZE, APPLE_SIZE);
if (_ret[0])
{
tail = Service_Run ("tail", "snake", ["head", tail]);
GenerateApple ();
}


function Draw ()
{
Render_DrawBox (apple_x, apple_y, APPLE_SIZE, APPLE_SIZE, ARGB(255,255,255,0));
}
}

SNAKE.PI

Nuestra serpiente. Que diferencia claramente el funcionamiento de la CABEZA (head) del resto de piezas (tails). La cabeza es la que mirará si pulsamos los cursores para ir tomando las distintas direcciones. Las colas lo que harán será seguir a su cabeza. Es una cuestión de transmisión de movimiento.
El "GetFTime" es una función que nos da el factor de tiempo a tener en cuenta para que los incrementos vayan siempre a la misma velocidad (dependiendo del framerate) en todas las máquinas.
El "IncreaseValue" lo que hace es una interpolación lineal entre un valor A hasta un valor B según una velocidad.

class Service
{
    constants:
SIZE = 10;
SPEED = 5.0f;

properties:
x = 0;
y = 0;
tail = null;
head = null;
dirX = 0;
dirY = 0;

function Init ()
{
if (head)
{
head.tail = this;
x = (head.x - head.dirX * SIZE);
y = (head.y - head.dirY * SIZE);
}
else
{
dirX = 1;
dirY = 0;
}
}

function Move ()
{
if (head == null)
{
if (Input_IsKeyPressed (KEY_RIGHT))
{
dirX = 1;
dirY = 0;
}
else if (Input_IsKeyPressed (KEY_LEFT))
{
dirX = -1;
dirY = 0;
}
else if (Input_IsKeyPressed (KEY_UP))
{
dirY = -1;
dirX = 0;
}
else if (Input_IsKeyPressed (KEY_DOWN))
{
dirY = 1;
dirX = 0;
}

x += dirX * GetFTime() * SPEED;
y += dirY * GetFTime() * SPEED;
}
else
{
_toX = (head.x - head.dirX * SIZE);
_toY = (head.y - head.dirY * SIZE);

if (x < _toX) dirX = 1;
else if (x > _toX) dirX = -1;
else dirX = 0;
if (y < _toY) dirY = 1;
else if (y > _toY) dirY = -1;
else dirY = 0;

x = IncreaseValue (x, _toX, GetFTime() * SPEED);
y = IncreaseValue (y, _toY, GetFTime() * SPEED);
}
}

function Draw ()
{
Render_DrawBox (x-SIZE/2, y-SIZE/2, SIZE, SIZE, ARGB(255,255,255,255));
}
}


TUTORIAL 4: HOW TO MAKE A PIPPOLS



== 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 ==

2 comentarios:

  1. Tens algum tutorial para jogos de nave?

    ResponderEliminar
    Respuestas
    1. Dentro del SDK hay una guía/ejemplo de como hacer un Invaders, igual te sirve. Un Pippols en cierta manera también es como un shooter vertical :)

      Eliminar