jueves, 10 de abril de 2014

Cocos2dx (HelloCocos2dx)

Categoría: Cocos2dx

Nivel: Básico


Una vez instalado el entorno y conocido los conceptos básicos llega la hora de ponerlos en práctica, para ello en este artículo voy a crear un proyecto de prueba y explicar su estructura para familiarizarse con el código.

Como el desarrollo va a realizarse mediante el lenguaje c++, es muy recomendable leerse algún documento con buenas prácticas y convenciones usadas en la escritura de código c++, cocos2dx tiene el documento '%cocos2dx%/docs/CODING_STYLE.md' que es un fork del google c++ oficial guide (se puede convertir el documento 'markdown' a 'pdf' mediante pandoc).

El que no tenga muchos conocimiento de c/c++, puede aprender la base en c.conclase.net, es una página muy buena en español con mucho recursos, el único problema es que no toca el nuevo estándar c++ 11, pero la base del lenguaje es la misma y lo explica muy bien.

Como guía de referencia del lenguaje puede usarse Referencia de lenguaje C++ del msdn de Microsoft.

En cualquier caso intentare explicar algunos conceptos del lenguaje sobre el transcurso de los artículos.


Creación del proyecto:

La creación de los proyectos para todas las plataformas se realiza mediante el script en Python que existe en la ruta '%cocos2dx%\tools\cocos2d-console\bin\cocos.py', si se han seguido los pasos del artículo dedicado a la instalación, se habrá creado una variable de entorno a la ruta, de modo que no es necesario entrar en ella para ejecutar el script.

Como ejemplo crearé un proyecto para todas las plataformas disponibles usando c++, para ello se abre una consola y se inserta el siguiente comando:

cocos new MyGame -p com.dgzornoza.games -l cpp -d D:\dgzornoza\Projects\Games

(Esto realizara una copia de las plantillas en el directorio de salida especificado)

Una vez termine el proceso se accede al directorio del proyecto y se podrá ver la estructura con todos los marcos de aplicación de las diferentes plataformas, como he comentado en anteriores artículos usare Visual Studio para la edición de código y la plataforma 'Win32', posteriormente se podrá compilar en cualquier otra plataforma.

Se accede a la carpeta proj.win32 y se ejecuta el archivo con la solución de VS en mi caso 'MyGame.sln', en este punto ya se puede compilar y ejecutar el proyecto con F5 tras lo cual se mostrara una ventana con el logotipo de cocos2dx y el texto 'Hello World'.

La información de depuración en la esquina inferior izquierda indica el número de vértices, el número de llamadas a la función de dibujo de la librería de render (actualmente OpenGL) y el número de frames por segundo.


Estructura:

La solución de VS contiene por defecto los siguientes proyectos:

  • libaudio: Librería para trabajar con audio.

  • libchipmunk: Librería para trabajar con físicas.

  • libcocos2d: Librería con el núcleo de cocos2dx.

  • MyGame: Proyecto creado para desarrollar el juego.

Como mínimo debe existir una referencia al núcleo de cocos2dx 'libcocos2d', la librería de audio y físicas no tienen por qué usarse, de hecho, en lugar de chipmunk se puede usar box2d que es otra librería de físicas que también incluye cocos2dx.

Si se abre el explorador de soluciones se podrá ver la estructura de los proyectos:

Me voy a centrar en el proyecto 'MyGame' que es el proyecto del juego de ejemplo. El marco de aplicación para la plataforma win32 reside en la carpeta con el mismo nombre y aquí está el punto de entrada de la aplicación que en cada plataforma será diferente. En principio el marco de aplicación no hay que tocarlo, solo nos centraremos en el código común del juego que es el mismo para todas las plataformas y reside en la carpeta 'Classes', conteniendo los siguientes archivos:

AppDelegate.h/.cpp: Archivos de declaración y definición de una clase para implementar una aplicación en cocos2dx. Esta es la clase principal del juego y la que gestionara los eventos de la aplicación, por defecto se implementan las siguientes funciones virtuales:

  • applicationDidFinishLaunching: Función que será invocada cuando se inicializa la aplicación, en ella por defecto se configura el director con la escena principal del juego.

  • applicationDidEnterBackground: Función que será invocada cuando la aplicación pasa a estar inactiva, por ejemplo al recibir una llamada en un dispositivo móvil.

  • applicationWillEnterForeground: Función que será invocada cuando la aplicación vuelve a estar activa.

HelloWorldScene.h/.cpp: Archivos de declaración y definición de la escena de ejemplo suministrada por la plantilla. Este es el archivo que contiene la escena de ejemplo que detallare a continuación.


Estructura:

Antes de ver el código, un repaso a la jerarquía básica de objetos del scenegraph en cocos2dx:

Escena (clase Scene) 1 -> * Capas (clase Layer) 1 -> * Nodos (clase Node) 1 -> * Sprites (clase Sprite)

De este modo una escena contiene una o varias capas, una capa contiene uno o varios nodos y un nodo contiene uno o varios sprites.

También hay que recordar que cualquier objeto en el SceneGraph debe ser un nodo o heredar de él, lo que significa que tanto las escenas, capas y sprites son básicamente nodos, heredan de un nodo y por tanto su comportamiento básico.


HelloWorldScene.h:

El archivo ' HelloWorldScene.h' contiene el código con la declaración de una clase para implementar una capa de ejemplo:

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__

#include "cocos2d.h"

class HelloWorld : public cocos2d::Layer
{
public:
    // there's no 'id' in cpp, so we recommend returning the class instance pointer
    static cocos2d::Scene* createScene();

    // Here's a difference. Method 'init' in cocos2d-x returns bool, 
    // instead of returning 'id' in cocos2d-iphone
    virtual bool init();  
    
    // a selector callback
    void menuCloseCallback(cocos2d::Ref* pSender);
    
    // implement the "static create()" method manually
    CREATE_FUNC(HelloWorld);
};

#endif // __HELLOWORLD_SCENE_H__

En este código existen algunas cosas importantes a tener en cuenta:

  1. 1. El nombre de la definición '__HELLOWORLD_SCENE_H__', aunque puede usarse el que se quiera, una de las recomendaciones es usar '__<PROJECT>_<PATH>_<FILE>_H__'.

  2. La clase hereda de una capa (clase Layer), Esta es la forma en cocos2dx de definir una capa, se hereda de Layer y se implementa el código necesario, de modo que la clase HelloWorld es una capa.

  3. La escena en el ejemplo se crea mediante la función estática 'createScene()', Es una forma muy básica de crear una escena sin tener que crear una clase para ella, yo no soy partidario de esta forma de crear la escena ya que rompe con uno de los principios SOLID en la programación OO, en concreto el principio de responsabilidad única y además puede llegar a confundir, debería ser la escena la que creara sus capas y no al contrario. En cualquier caso, esto solo es una plantilla base, en un proyecto real se suele tener una o varias clases para las escenas.

  4. La función de inicialización de la capa 'init()' en esta función se inicializara la capa, yo generalmente la establezco como una función protegida para asegurar de que solo sea llamada por la propia clase o alguna clase heredada.

  5. 'menuCloseCallback' es una declaración de una función Callback que en el ejemplo está asignada al botón para cerrar la aplicación.

  6. La macro 'CREATE_FUNC' es la parte más importante de este archivo de ejemplo y la que mejor hay que comprender. Como bien es sabido c/c++ es un lenguaje en el cual la memoria dinámica es gestionada por el programador, esto significa que toda memoria dinámica creada deberá ser liberada explícitamente por el programador, de no ser así se crearan fugas de memoria conocidas como 'Memory Leak', casi todos los motores gráficos que he usado implementan en su arquitectura algún mecanismo para evitar fugas de memoria al trabajar con ellos, generalmente algún mecanismo de cuenta de referencias y cocos2dx es uno de ellos. Como cocos2dx es un proyecto que hereda de la versión para IOS, se ha mantenido el sistema ARC (Automatic Reference Counting) de Objetive-C sobre el que dedicare un artículo junto con los Smart pointers de c++.

    Si se establece el cursor en la macro y se pulsa F12 se navegara a su definición, en este ejemplo la macro será desplegada en el siguiente código:

    static HelloWorld* create()
    {
        HelloWorld *pRet = new HelloWorld();
        if (pRet && pRet->init())
        {
            pRet->autorelease();
            return pRet;
        }
        else
        {
            delete pRet;
            pRet = NULL;
            return NULL;
        }
    }
    

    Aquí se puede ver que se crea un método 'create()' en el cual se crea en memoria dinámica la capa 'HelloWorld', en caso de poder crearse será invocado el método 'init()' del punto 4. Si el método 'init()' retorna true se procederá a insertar la referencia en el pool del sistema de conteo de referencias ARC mediante el método 'autorelease()' y será retornada la referencia de la capa creada.

    (NOTA: En cocos2dx cualquier objeto del engine que sea insertado en el pool ARC mediante 'autorelease()', será liberado automáticamente y no tendremos que liberarlo manualmente.)

    En caso de que 'init()' retorne false se liberara la memoria de la capa y se retorna 'NULL'.

    (NOTA: Hay que tener en cuenta que NULL es una macro que se define como 0, en c++ 11 existe una nueva palabra reservada 'nullptr' que debería de comenzar a usarse en lugar de NULL, en la versión 3.0 de cocos2dx ya comienza a implementarse el estándar c++11 y poco a poco se irán corrigiendo estos detalles.)


HelloWorldScene.cpp:

El archivo .cpp contiene la definición de las funciones declaradas en el archivo .h con la capa de ejemplo.

Al principio del archivo se pueden ver las siguientes instrucciones:

#include "HelloWorldScene.h"

USING_NS_CC;

La directiva '#include' indica el archivo con la declaración de las funciones. La macro 'USING_NS_CC' será desplegada en el código 'using namespace cocos2d', el cual añade el namespace cocos2d para evitar tener que usarlo constantemente al llamar a sus funciones.

(NOTA: en el archivo .cpp se permite añadir las directivas using con namespaces de librerías, pero en los archivos de cabecera .h no es una buena práctica.)

La definición para crear la escena del ejemplo es bastante sencilla:

Scene* HelloWorld::createScene()
{
    // 'scene' is an autorelease object
    auto scene = Scene::create();
    
    // 'layer' is an autorelease object
    auto layer = HelloWorld::create();

    // add layer as a child to scene
    scene->addChild(layer);

    // return the scene
    return scene;
}

Cada línea está bien comentada y no hay mucho que decir. Solo comentar que en las clases de cocos2dx casi siempre existirá una función estática 'create()' para crear las instancias, esto permite añadir la referencia al pool ARC para liberarse automáticamente. Si nos fijamos en la creación de la capa 'HelloWorld::create()' se puede ver que se invoca a la función desplegada por la macro 'CREATE_FUNC' comentada anteriormente, la macro ha creado la función estática de creación de la capa del ejemplo incluyéndola en el pool ARC del mismo modo que hacen las clases del SceneGraph de cocos2dx.

Por último se añade la capa como hija de la escena y se retorna. Esta función es llamada desde la función 'applicationDidFinishLaunching()' del archivo AppDelegate.cpp que es la función que inicializara la aplicación con la escena y capa que aquí se crea.

La siguiente definición es la del método 'init()', hay que recordar que este método era llamado desde la función estática 'create()' definida en la macro 'CREATE_FUNC'.

bool HelloWorld::init()
{
    //////////////////////////////
    // 1. super init first
    if ( !Layer::init() )
    {
        return false;
    }
    
    Size visibleSize = Director::getInstance()->getVisibleSize();
    Point origin = Director::getInstance()->getVisibleOrigin();

    /////////////////////////////
    // 2. add a menu item with "X" image, which is clicked to quit the program
    //    you may modify it.

    // add a "close" icon to exit the progress. it's an autorelease object
    auto closeItem = MenuItemImage::create(
                                           "CloseNormal.png",
                                           "CloseSelected.png",
                                           CC_CALLBACK_1(HelloWorld::menuCloseCallback, this));
    
    closeItem->setPosition(Point(origin.x + visibleSize.width - closeItem->getContentSize().width/2 ,
                                origin.y + closeItem->getContentSize().height/2));

    // create menu, it's an autorelease object
    auto menu = Menu::create(closeItem, NULL);
    menu->setPosition(Point::ZERO);
    this->addChild(menu, 1);

    /////////////////////////////
    // 3. add your codes below...

    // add a label shows "Hello World"
    // create and initialize a label
    
    auto label = LabelTTF::create("Hello World", "Arial", 24);
    
    // position the label on the center of the screen
    label->setPosition(Point(origin.x + visibleSize.width/2,
                            origin.y + visibleSize.height - label->getContentSize().height));

    // add the label as a child to this layer
    this->addChild(label, 1);

    // add "HelloWorld" splash screen"
    auto sprite = Sprite::create("HelloWorld.png");

    // position the sprite on the center of the screen
    sprite->setPosition(Point(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));

    // add the sprite as a child to this layer
    this->addChild(sprite, 0);
    
    return true;
}

Generalmente lo primero que se hace es intentar inicializar la clase base mediante el mismo método virtual 'init()'.

Las siguientes dos líneas obtienen las dimensiones del área visible en el dispositivo y el punto donde comienza el área visible, esto sirve para adaptar a las diferentes pantallas de los dispositivos y le dedicare un artículo entero.

Las líneas de código del apartado 2, crean un ítem de menú con una imagen, este ítem establece como función callback 'HelloWorld::menuCloseCallback' que es asignada mediante la macro 'CC_CALLBACK_1' y será llamada al pulsar el ítem con la imagen.

El apartado 3 crea el texto "Hello World" mediante un nodo de tipo LabelTTF y también un Sprite con el logo de cocos2dx.

NOTA: la clase LabelTTF está obsoleta en la versión 3.0 y ya no debe usarse, en su lugar se ha creado una nueva clase para el texto llamada 'Label', mediante esta clase se pueden crear textos con fuentes TTF (True Type Font) y BMF (BitMap Font). El código equivalente para crear el texto con la nueva clase es:

auto label = Label::create("Hello World", "Arial", 24);

('auto' es una nueva palabra reservada en c++ 11 que añade la característica de inferencia de tipos a c++, el compilador infiere el tipo y ayuda a mantener ciertas partes de código más limpias y legibles. No hay que abusar de ella ya que oculta la visualización del tipo al programador)

Al final si todo ha sido correcto, la función retorna 'true', recordando el código de la macro 'CREATE_FUNC' si la función retorna false, el puntero será liberado de la memoria dinámica y no será creada la capa, retornando NULL, de modo que para indicar que la inicialización ha sido correcta debe retornarse true.

La última función de la clase, es la definición de la función callback del ítem de menú con la imagen, el código es el siguiente:

void HelloWorld::menuCloseCallback(Ref* pSender)
{
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WP8) || (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT)
 MessageBox("You pressed the close button. Windows Store Apps do not implement a close button.","Alert");
    return;
#endif

    Director::getInstance()->end();

#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
    exit(0);
#endif
}

Esta función implementa el cierre de la aplicación al pulsar el ítem de menú en las diferentes plataformas. En la función se pueden observar el uso de las macros que definen la plataforma de destino, la macro 'CC_TARGET_PLATFORM' se establece en una plataforma u otra dependiendo de las directivas del preprocesador especificadas en el proyecto (hay que recordar que cada plataforma tiene un proyecto propio con el marco de la aplicación), de este modo se permite escribir código específico para cada plataforma en caso de ser requerido en cualquier momento.

Aquí termina el ejemplo usado en la plantilla de creación de un nuevo proyecto en cocos2dx, siguiendo los pasos del anterior artículo dedicado a la compilación, se puede ejecutar en las diferentes plataformas para ver el resultado.


¿Has tenido algún problema para seguir el código?

Saludos y… "a fluzear"

1 comentario:

  1. Sigue cuando puedas con estos tutoriales, se agradecen muchísimo.

    ResponderEliminar