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!

2 comentarios:

  1. que raro pense que ya habia dejado un comentario aqui:

    hola vengo del foro de stratos buscando gente con la que colaborar en proyectos para aprender programacion orientada a videojuegos. Ahora mismo estoy estudiando java, aunque me puedo adaptar a cualquier otro lenguaje. Ademas, tambien estoy trasteando con algunos motores graficos como unity y tengo algunos diseños de juegos interesantes. Mi blog (que comparto con un amigo) es http://codebreakers3d.blogspot.com/ un saludo!

    ResponderEliminar
  2. Hola Greevil,

    Sí, ya habías dejado un comentario, pero fue en los comentarios del tutorial 0.

    Como te digo en el otro comentario, lo primero decirte que me alegra mucho de que alguien haya comentado en el blog y que espero que te pases por aquí más a menudo.

    Me gustaría que la gente se interesara por GLScene y que hiciéramos pequeños proyectos con esta herramienta en concreto. ¿Estarías interesado? ¿Te gusta más modelar o la programación?

    Me he pasado por tu blog y está muy bien.

    Saludos.

    ResponderEliminar