Como hacer un "Pong"

Introducción

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!

No hay comentarios:

Publicar un comentario