martes, 6 de diciembre de 2011

Tutorial 1: Creación de escena en tiempo de ejecución a partir de documento XML

Hola de nuevo a todos,

En este tutorial se va a tratar la creación de una escena en tiempo de ejecución a partir de un documento XML. Esto nos dará la libertad de definir escenas a partir de un documento XML separando estas definiciones de nuestro código fuente. Esto es una gran idea, ya que podemos, por ejemplo, alojar esas escenas en un servidor y luego nuestra aplicación sólo tendría que pedirlas y analizarlas, y ya tendríamos nuestro mundo cargado.

Por otro lado, me parece interesante trabajar con documentos XML, ya que son muy útiles para archivos de configuración, almacenar partidas, niveles y muchas cosas más.

Vamos a instalar previamente una librería para el manejo de documentos XML. He elegido la librería XML Parser compatible con Delphi 4..7, 2005..2007, 2009..2010, XE, C++Builder 5 y posteriores.

Nos descargamos la librería de aquí. Bajaros el archivo xmlparser-1.0.20.zip y descomprimidlo. Con BCB6 abierto nos vamos a Component --> Install Component --> Into new package, pulsamos en "Browse..." en la caja de texto de "Unit file name", en tipo de archivo seleccionamos "Pascal unit (*.pas)" y seleccionamos los archivos "LibXmlComps.pas" y "LibXmlParser.pas", en "Package file name" ponemos un nombre a nuestro paquete, por ejemplo "XML_Parser" y pulsamos OK.

Primero nos preguntará si queremos instalarlo:


Pulsamos en Yes.

Lo instalará y si todo ha ido bien veremos este diálogo:


Una nueva pestaña llamada XML, aparecerá en el IDE.

Una vez que tenemos instalada la librería, vamos a generar en tiempo de ejecución un pequeño escenario.

Lo primero de todo, es definir nuestro documento XML con el escenario. Esto es muy importante, se puede decir que son los cimientos sobre los que crecerá la aplicación, podemos hacer una definición compleja o no, dependiendo de lo que queramos mostrar, en esto juega un papel importante la imaginación.

Yo he hecho una definición bastante simple, os pongo un ejemplo de mi escenario:

<?xml version="1.0" encoding="UTF-8" standalone= "yes"?>
<surface width="1000" height="1000" />
<item x="0" y="0" type="0" resource="1" id="item0" />
<item_group x1="1" y1="1" x2="20" y2="20" type="1" resource="1" id="item0" />
<item_group x1="40" y1="10" x2="45" y2="20" type="0" resource="1" />
<item_group x1="80" y1="50" x2="90" y2="70" type="2" resource="1" />
<item_group x1="70" y1="40" x2="72" y2="50" type="3" resource="1" />
<item_group x1="93" y1="30" x2="97" y2="40" type="4" resource="1" />
<item_group x1="30" y1="20" x2="38" y2="30" type="0" resource="1" />

Como podéis observar he definido:

- La etiqueta surface con los atributos width y height. Esto definirá el ancho y el alto de nuestra superficie sobre la que se sustentarán todos los elementos de la escena.

- La etiqueta item con los atributos x, y, type, resource e id. Esto definirá las coordenadas x e y en la superficie de un determinado tipo de elemento e indicará si ese elemento es un recurso o no. Los tipos pueden tomar los valores del 0 a 4, es decir, habrá cinco tipos y dependiendo del tipo nuestro elemento será de un color u otro. El valor de resource podrá tomar los valores 0 o 1, esto significará que nuestro elemento puede ser un recurso, algo con el que el personaje podrá interactuar de manera directa o no.

- La etiqueta item_group con los atributos x1, y1, x2, y2, type y resource. Esto definirá un grupo de elementos que ocupan un área concreta de la superficie definida por x1, y1, x2 e y2. Sobre los atributos type y resource poco más hay que decir ya que los expliqué en la etiqueta item.

Bien, con esto nos podemos poner manos a la obra a realizar lo que se denomina parser o analizador.

Abrimos BCB6 si no lo tenemos ya abierto y vamos a "File" --> "New" --> "Application".

Antes de nada, deberéis arrastrar un TGLSceneViewer y un TGLScene al formulario, los encontraréis en la pestaña GLScene de vuestro IDE. Luego, dentro del TGLScene, añadís una cámara de tipo TGLCamera y se la asignáis al TGLSceneViewer. Para poder ver algo, cambiad los valores de la cámara desde la vista de diseño a los valores que os digo a continuación. Poned la cámara apuntando para abajo, esto es, cambiad los valores de Direction a 0, 0, -1 y los valores de Up los podéis dejar a 0, 1, 0, los valores de Position los podéis dejar a 0, 0, 700 y el valor de DepthOfView lo podéis poner a 1000. Si tenéis problemas con esto podéis ponerme algún comentario y os ayudaré, siempre que pueda :)

Vamos con el grueso de la aplicación, creamos un archivo de constantes para las etiquetas que va a recibir nuestro parser, yo le suelo llamar "Constants.h" y en el voy metiendo todos los tags. Os pongo un ejemplo y vosotros le vais completando:

#ifndef CONSTANTS_H_
#define CONSTANTS_H_


#include <iostream>


const AnsiString kSurfaceXMLTag = "surface";
const AnsiString kWidthXMLTag = "width";
const AnsiString kHeightXMLTag = "height";


#endif /*CONSTANTS_H_*/


Vamos ahora con el .h de nuestro formulario, definimos un atributo privado para nuestro parser, es decir, en la cláusula private metemos la definición de lo que será nuestro objeto parser:

private:
      TXmlParser* XmlParser;


También en el .h definimos los métodos que usaremos para analizar nuestro documento XML. Los ponemos de ámbito privado también:


private: // User declarations
        void ApplySource();
        void FillTree();
        void ScanElement();

El .h quedaría con todo esto así:

private: // User declarations
        void ApplySource();
        void FillTree();
        void ScanElement();


private:
      TXmlParser* XmlParser;

En el constructor de nuestro formulario (en el .cpp) instanciamos el objeto de la clase TXmlParser y cargamos el origen de los datos, en este caso nuestro documento XML estará en nuestro PC en una carpeta local:

__fastcall TForm1::TForm1(TComponent* Owner)
        : TForm(Owner)
{

      XmlParser = new TXmlParser;
      ApplySource();
}

Veamos en detalle el método ApplySource():

void TForm1::ApplySource()
{
        XmlParser->LoadFromFile("C:\\Archivos de programa\\Borland\\CBuilder6\\Projects\\xmlfiles\\test.xml");
        FillTree();
}

Como podemos ver llamamos al método LoadFromFile pasando como parámetro una cadena de texto con la ruta del documento XML que hemos definido anteriormente. Finalmente, cuando el documento está cargado en el objeto llamamos a FillTree().

Veamos en detalle el método FillTree():

void TForm1::FillTree()
{
  XmlParser->Normalize = true;
  XmlParser->StartScan();
  ScanElement();
}


Este método inicia el escaneo o análisis del documento XML mediante el método StartScan() y posteriormente llama al método ScanElement().

El método ScanElement() es el más importante ya que es donde se realiza el parser. Vamos a verlo en detalle por partes:

void TForm1::ScanElement()
{
    while (XmlParser->Scan())
    {
      switch(XmlParser->CurPartType)
      {
      case ptXmlProlog:
        {
        }
        break;
      case ptDtdc:
        {
        }
        break;
      case ptStartTag:
        {
        }
        break;
      case ptEmptyTag:
        {
        }
        break;
      case ptEndTag:
        {
        }
        break;
      case ptContent:
        {
        }
        break;
      case ptCData:
        {
        }
        break;
      case ptComment:
        {
        }
        break;
      case ptPI:
        {
        }
        break;
     }
    }
}

Bien, vemos que en este método tenemos un bucle while que se iterará hasta que se haya leído todo el documento XML luego dentro del bucle tenemos un switch que discernirá el tipo (TPartType) de la parte actual leída, los tipos pueden ser estos: ptNone, ptXmlProlog, ptComment, ptPI, ptDtdc, ptStartTag, ptEmptyTag, ptEndTag, ptContent, ptCData. Nos interesa el ptEmptyTag, será en ese caso donde meteremos código para generar nuestra escena.

Veamos ese caso por separado:

      case ptEmptyTag:
        {
        if (kSurfaceXMLTag == XmlParser->CurName)
            {
            TGLCube *surfaceGLCube = new TGLCube(this);
            surfaceGLCube->Name = "surfaceGLCube";
            surfaceGLCube->Parent = GLScene1->Objects;
            surfaceGLCube->CubeDepth = 0;
            }
        else if (kItemXMLTag == XmlParser->CurName)
            {
            CustomCube *itemGLCube = new CustomCube(this);
            itemGLCube->Parent = GLScene1->Objects;
            }


        TAttrList* list = XmlParser->CurAttr;
        for (int i(0); i < list->Count; i++)
            {
            if (kSurfaceXMLTag == XmlParser->CurName)
                {
                if (kWidthXMLTag == list->Name(i))
                    {
                    TGLCube* surfaceGLCube = (TGLCube*)GLScene1->FindSceneObject("surfaceGLCube");
                    surfaceGLCube->CubeWidth = StrToFloat(list->Value(i));
                    }
                else if (kHeightXMLTag == list->Name(i))
                    {
                    TGLCube* surfaceGLCube = (TGLCube*)GLScene1->FindSceneObject("surfaceGLCube");
                    surfaceGLCube->CubeHeight = StrToFloat(list->Value(i));
                    }
                }


            if (kItemXMLTag == XmlParser->CurName)
                {
                if (kXXMLTag == list->Name(i))
                    {
                    TGLCube* itemGLCube = (TGLCube*)GLScene1->FindSceneObject("itemGLCube");
                    TGLCube* surfaceGLCube = (TGLCube*)GLScene1->FindSceneObject("surfaceGLCube");
                    itemGLCube->Position->X = itemGLCube->CubeWidth * (StrToFloat(list->Value(i)) + 1) - surfaceGLCube->CubeWidth / 2;
                    }
                else if (kYXMLTag == list->Name(i))
                    {
                    TGLCube* itemGLCube = (TGLCube*)GLScene1->FindSceneObject("itemGLCube");
                    TGLCube* surfaceGLCube = (TGLCube*)GLScene1->FindSceneObject("surfaceGLCube");
                    itemGLCube->Position->Y = itemGLCube->CubeHeight * (-(StrToFloat(list->Value(i)) + 1)) + surfaceGLCube->CubeHeight / 2;
                    }
                else if (kTypeXMLTag == list->Name(i))
                    {
                    CustomCube* itemGLCube = (CustomCube*)GLScene1->FindSceneObject("itemGLCube");
                    itemGLCube->setType(StrToInt(list->Value(i)));
                    }
                else if (kResourceXMLTag == list->Name(i))
                    {
                    CustomCube* itemGLCube = (CustomCube*)GLScene1->FindSceneObject("itemGLCube");
                    itemGLCube->setResource(StrToBool(list->Value(i)));
                    }
                else if (kIdXMLTag == list->Name(i))
                    {
                    }
                if (i == list->Count - 1)
                    {
                    TGLCube* itemGLCube = (TGLCube*)GLScene1->FindSceneObject("itemGLCube");
                    itemGLCube->Name = list->Value(kIdXMLTag);
                    }
                }
            }


            if (kItemGroupXMLTag == XmlParser->CurName)
                {
                for (int i(StrToInt(list->Value(kX1XMLTag))); i < StrToInt(list->Value(kX2XMLTag)); i++)
                    for (int j(StrToInt(list->Value(kY1XMLTag))); j < StrToInt(list->Value(kY2XMLTag)); j++)
                        {
                        CustomCube *itemGLCube = new CustomCube(this);
                        itemGLCube->Parent = GLScene1->Objects;


                        TGLCube* surfaceGLCube = (TGLCube*)GLScene1->FindSceneObject("surfaceGLCube");
                        itemGLCube->Position->X = itemGLCube->CubeWidth * (i + 1) - surfaceGLCube->CubeWidth / 2;
                        itemGLCube->Position->Y = itemGLCube->CubeWidth * (-(j + 1)) + surfaceGLCube->CubeHeight / 2;
                        itemGLCube->setType(StrToInt(list->Value(kTypeXMLTag)));
                        itemGLCube->setResource(StrToBool(list->Value(kResourceXMLTag)));
                        itemGLCube->Name = "id_" +
                                           IntToStr(i) + "_" +
                                           list->Value(kX2XMLTag) + "_" +
                                           IntToStr(j) + "_" +
                                           list->Value(kY2XMLTag);
                        }
                }
        }
        break;

Dentro de este case, podemos ver que el código se bifurca, dependiendo de si el nombre de la etiqueta leída es "surface" (kSurfaceXMLTag) o "item" (kItemXMLTag). Si el nombre de la etiqueta leída es "surface" (kSurfaceXMLTag) se creará un objeto de tipo TGLCube y se añadirá a la escena. Si el nombre de la etiqueta leída es "item" (kItemXMLTag) se creará un objeto de tipo CustomCube (es un tipo creado por nosotros, al final del tutorial se verá en detalle) y se añadirá a la escena.

Bien, después de estas dos alternativas, viene un bucle for. Lo que hace este bucle es leer los atributos de la etiqueta en cuestión que ha sido leída.

- Si la etiqueta leída fue "surface" se leen los atributos "width" y "height" y se actualiza el objeto que creamos antes de tipo TGLCube para la superficie.

- Si la etiqueta leída fue "item" se leen los atributos "x", "y", "resource", "type" e "id" y se actualiza el objeto que creamos antes de tipo CustomCube para el item suelto.

Fuera del for, si la etiqueta leída fue "item_group" se van recorriendo todos los elementos desde y1 a y2 por cada elemento desde x1 a x2 y se va generando un objeto de tipo CustomCube para cada elemento. Nótese que aquí la forma de coger el atributo en cuestión se hace así: list->Value(<tipo>) en vez de leer el list->Name(i). Es otra forma igual de efectiva pero requiere conocer el nombre del atributo a leer.

Con esto ya tendríamos nuestra escena montada a partir de un documeto XML que hemos leído localmente.

Os dejo la implementación de CustomCube, es muy sencilla:

CustomCube.h:

#ifndef CUSTOMCUBE_H
#define CUSTOMCUBE_H


#include "GLObjects.hpp"
#include "GLObjectTypes.h"


class CustomCube: public TGLCube
{
public:
        __fastcall CustomCube(Classes::TComponent* AOwner);
        __fastcall ~CustomCube();
        int Id() {return id;};
void setId(int id) {this->id = id;};
void setType(int type);
void setResource(bool resource) {this->resource = resource;};
private:
        int id;
        GLObjectType type;
        bool resource;
};


#endif


CustomCube.cpp:

#include "DataModel/CustomCube.h"


__fastcall CustomCube::CustomCube(Classes::TComponent* AOwner): TGLCube(AOwner)
{
    this->Name = "itemGLCube";
    this->CubeWidth = 10;
    this->CubeHeight = 10;
    this->CubeDepth = 10;
    this->Position->Z = 5;
}


__fastcall CustomCube::~CustomCube()
{
}


void CustomCube::setType(int type)
{
    this->type = type;
    switch (type)
        {
        case EMud:
            {
            this->Material->FrontProperties->Diffuse->AsWinColor = clRed;
            }
            break;
case EWood:
            {
            this->Material->FrontProperties->Diffuse->AsWinColor = clOlive;
            }
            break;
case EMetal:
            {
            this->Material->FrontProperties->Diffuse->AsWinColor = clGray;
            }
            break;
case EWater:
            {
            this->Material->FrontProperties->Diffuse->AsWinColor = clBlue;
            }
            break;
case EPowder:
            {
            this->Material->FrontProperties->Diffuse->AsWinColor = clBlack;
            }
            break;
        }
}

Si todo ha ido bien al compilar veréis algo como esto:




En el próximo tutorial explicaré cómo movernos por nuestra escena, es sencillo. Hasta el próximo tutorial. ¡Espero vuestros comentarios!

domingo, 30 de octubre de 2011

Tutorial 0: Configurando el entorno

Hola,

Esta es mi primera entrada en el blog, espero que sea la primera de muchas y espero que sea de vuestro agrado :)

Tengo en mente realizar una serie de tutoriales utilizando GLScene, no se si más adelante hablaré sobre otros temas pero por el momento me centraré en GLScene.

¿Por qué GLScene? Me parece una librería bastante buena para iniciarse en el mundo del desarrollo de escenas 3D, ya que no tendremos que bajar a un nivel tan bajo como si usáramos OpenGL y es intuitiva, ya que está basada en componentes visuales que podemos arrastrar a un formulario (si profundizáis en el tema y queréis hacer aplicaciones más complejas, veréis que lo mejor para tener una buena estructura de proyecto es crear los componentes mediante código). Con GLScene, podremos utilizar un lenguaje de programación orientado a objetos como Object Pascal o C++ y podremos integrarlo con la VCL de Borland o con la LCL de Lazarus (open source).

En este primer tutorial, vamos a centrarnos en la instalación de todas las herramientas necesarias para poder desarrollar videojuegos que hagan uso de la librería GLScene, utilizando C++ como lenguaje de programación y por tanto Borland C++ Builder en su versión 6 como IDE.

Puede que esto no sea lo más lógico, ya que GLScene está escrita en Object Pascal (aunque posteriormente se ha adaptado a C++). Puede que lo lógico fuera utilizar alguna versión de Borland Delphi o Lazarus, pero pienso que C++ es un lenguaje que está más cerca del desarrollo de videojuegos, así que para los principiantes les sirve para habituarse a trabajar con C++.

Como ya comenté, voy a utilizar Borland C++ Builder 6, a partir de ahora lo llamaré BCB6. Lo primero que debéis de tener en cuenta es que este IDE y compilador no es gratuito. Si no tenéis pensado adquirir este IDE, ya sabéis que hay otra alternativa libre para desarrollar con GLScene, pero habría que utilizar Lazarus (Object Pascal).

Bien, instalamos Borland C++ Builder 6.

Os pongo unas capturas de pantalla de cómo sería el proceso.








Ahora vamos a instalar GLScene, lo podéis descargar de aquí, esta versión está preparada para compilar sin problemas en BCB6.

Descomprimid el .zip donde queráis.

Abrid BCB6 e id a Tools --> Environment Options --> Library --> Library path --> ...



Desde aquí, añadimos la carpeta GLSceneBCB6\Source y todas las subcarpetas del .zip que habéis descomprimido anteriormente.

Después abrimos el archivo GLSceneBCB6.bpk, pulsamos en Compile y luego en Install.



Si todo ha ido bien os aparecerá una ventana como esta:


¡Voila! Ahora tenemos GLScene instalado en nuestro IDE y podemos empezar a cacharrear con él. Os recomiendo que antes de seguir con los tutoriales os hagáis este otro tutorial de David Martín de Vidales que es de iniciación, seguro que luego tenéis mil dudas más :)