pixelboyz logo
Desarrollo de Videojuegos

Serie de tutoriales Cocos2d-x: Manejo de la entrada táctil y del mouse

image

Índice de contenido


En esta parte de la serie de tutoriales de Cocos2d-x, veremos cómo manejar los eventos táctiles y del mouse. Primero, debe tener en cuenta que, de forma predeterminada, Cocos2d-x trata el clic izquierdo del mouse como un toque, por lo que si solo tiene requisitos de entrada simples y no requiere soporte multitáctil (¡lo cual es notablemente diferente para realizar con un solo mouse!) , simplemente puede implementar controladores táctiles. Esta parte va a tener mucho código, ya que en realidad tenemos 3 tareas diferentes que cubrir aquí (táctil, multitáctil y mouse), aunque todas son muy similares en el comportamiento general.

Empecemos con un ejemplo ultra simple. Una vez más, asumo que has hecho el partes anteriores del tutorial y ya tiene un AppDelegate.

Manejar eventos de toque/clic

TouchScene.h

#pragma once

#include "cocos2d.h"

class TouchScene : public cocos2d::Layer
{
public:
    static cocos2d::Scene* createScene();
    virtual bool init();  

    virtual bool onTouchBegan(cocos2d::Touch*, cocos2d::Event*);
    virtual void onTouchEnded(cocos2d::Touch*, cocos2d::Event*);
    virtual void onTouchMoved(cocos2d::Touch*, cocos2d::Event*);
    virtual void onTouchCancelled(cocos2d::Touch*, cocos2d::Event*);
    CREATE_FUNC(TouchScene);

private:
   cocos2d::Label* labelTouchInfo;
};

TouchScene.cpp

#include "TouchScene.h"

USING_NS_CC;

Scene* TouchScene::createScene()
{
    auto scene = Scene::create();
    auto layer = TouchScene::create();
    scene->addChild(layer);

   return scene;
}

bool TouchScene::init()
{
    if ( !Layer::init() )
    {
        return false;
    }
    
   labelTouchInfo = Label::createWithSystemFont("Touch or clicksomewhere to begin", "Arial", 30);

   labelTouchInfo->setPosition(Vec2(
      Director::getInstance()->getVisibleSize().width / 2,
      Director::getInstance()->getVisibleSize().height / 2));

   auto touchListener = EventListenerTouchOneByOne::create();

   touchListener->onTouchBegan = CC_CALLBACK_2(TouchScene::onTouchBegan, this);
   touchListener->onTouchEnded = CC_CALLBACK_2(TouchScene::onTouchEnded, this);
   touchListener->onTouchMoved = CC_CALLBACK_2(TouchScene::onTouchMoved, this);
   touchListener->onTouchCancelled = CC_CALLBACK_2(TouchScene::onTouchCancelled, this);

   _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, this);
    
   this->addChild(labelTouchInfo);
   return true;
}

bool TouchScene::onTouchBegan(Touch* touch, Event* event)
{
   labelTouchInfo->setPosition(touch->getLocation());
   labelTouchInfo->setString("You Touched Here");
   return true;
}

void TouchScene::onTouchEnded(Touch* touch, Event* event)
{
   cocos2d::log("touch ended");
}

void TouchScene::onTouchMoved(Touch* touch, Event* event)
{
   cocos2d::log("touch moved");
}

void TouchScene::onTouchCancelled(Touch* touch, Event* event)
{
   cocos2d::log("touch cancelled");
}

Luego, si lo ejecuta, cuando realiza un toque o hace clic:

Como puede ver, donde toca en la pantalla se muestra una etiqueta de texto. Mirando en el fondo de esa captura de pantalla, puede ver que los eventos movidos por toque se activan y registran constantemente. Además, los eventos táctiles se activan cuando el usuario quita el dedo (o suelta el botón del mouse).

Ahora echemos un vistazo rápido al código. Nuestro archivo de encabezado es bastante sencillo. Además de los métodos normales, agregamos un cuarteto de funciones de controlador para manejar los diversos eventos táctiles posibles. También agregamos una variable miembro para nuestra etiqueta que se usa para dibujar el texto en la pantalla.

En el archivo cpp, creamos la escena como de costumbre. En init() creamos un EventListener de tipo EventListenerTouchOneByOne, que predeciblemente maneja toques, um, uno por uno (a diferencia de todos a la vez, que veremos más adelante). Luego mapeamos cada evento posible, toque iniciado, toque final, toque cancelado y toque movido, a su controlador de función correspondiente usando la macro CC_CALLBACK_2, pasando la función a ejecutar y el contexto (o destino). Esto también tendrá sentido más adelante, así que espera. Una cosa a tener en cuenta aquí, y un punto de confusión para mí, onTouchBegan tiene una firma diferente a cualquier otro evento, devolviendo un bool. No estoy completamente seguro de por qué este evento se maneja de manera diferente, me parece una mala idea personalmente, pero puede haber una buena razón de diseño que desconozco.

Lo último que hacemos es registrar nuestro EventListener para recibir eventos. Esto se hace con una llamada a del nodo miembro protegido _eventListener. Llamamos a addEventListenerWithSceneGraphPriority(), lo que básicamente significa que queremos que este evento se actualice tanto como sea posible. Veremos un ejemplo de establecer un nivel de prioridad diferente más adelante.

¿Qué es esta magia negra CC_CALLBACK_2?

En general, no soy un gran admirador del uso de macros en C++. En general, creo que llevan a los programadores a convertir eventualmente sus bibliotecas en lenguajes de metaprogramación y, en última instancia, ofuscar el código subyacente en nombre de la claridad. Esta, sin embargo, es una de las excepciones a la regla. CC_CALLBACK_2, y toda la familia CC_CALLBACK_ es simplemente un envoltorio alrededor de algún código C++ estándar, específicamente una llamada a std::bind. Aquí está el código de macro real:

#define CC_CALLBACK_0(__selector__,__target__, ...) std::bind(&__selector__,__target__, ##__VA_ARGS__)  #define CC_CALLBACK_1(__selector__,__target__, ...) std::bind(&__selector__,__target__, 
std::placeholders::_1, ##__VA_ARGS__)  #define CC_CALLBACK_2(__selector__,__target__, ...) std::bind(&__selector__,__target__, 
std::placeholders::_1, std::placeholders::_2, ##__VA_ARGS__)  #define CC_CALLBACK_3(__selector__,__target__, ...) std::bind(&__selector__,__target__, 
std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, ##__VA_ARGS__)

Básicamente, std::bind es vinculante para vincular parámetros a una función. Los marcadores de posición std::son, en última instancia, la cantidad de parámetros que espera su función. Entonces, por ejemplo, cuando llama a CC_CALLBACK_2, está diciendo que la función toma dos parámetros, en este caso, un punto táctil* y un puntero de evento*. De manera similar, CC_CALLBACK_1 esperaría que la función proporcionada tome un solo parámetro. Este tipo de código es increíblemente común en C++ 11, es increíblemente feo, difícil de leer y asimilar y es fácil escribir mal. En estos casos, el uso de macros brilla. Solo tenga en cuenta qué es lo que hace la macro que está llamando. Cada vez que encuentre una macro en el código, le recomiendo que haga clic derecho e «Ir a definición» o CTRL+clic si está en XCode, para ver lo que realmente hace, incluso si no tiene mucho sentido.

En la mayoría de los controladores táctiles, simplemente registramos que ocurrió el evento. En el caso de un inicio táctil (o inicio de clic), actualizamos la posición de la etiqueta donde el usuario hizo clic y mostramos la cadena «Tocaste aquí».

Ahora echemos un vistazo a un ejemplo que usa lambda en su lugar. Este ejemplo también entra en un poco más de detalle de lo que hay en ese puntero táctil que estamos pasando. El archivo de encabezado es básicamente el mismo, excepto que no hay funciones onTouch____.

Manejo de eventos táctiles usando Lambdas y manejo de coordenadas táctiles

TouchScene.cpp

#include "TouchScene.h"

USING_NS_CC;

Scene* TouchScene::createScene()
{
    auto scene = Scene::create();
    auto layer = TouchScene::create();
    scene->addChild(layer);

    return scene;
}

bool TouchScene::init()
{
    if ( !Layer::init() )
    {
        return false;
    }
    
   auto sprite = Sprite::create("HelloWorld.png");
   sprite->setPosition(Vec2(Director::getInstance()->getVisibleSize().width / 2,
      Director::getInstance()->getVisibleSize().height / 2));

    // Add a "touch" event listener to our sprite
   auto touchListener = EventListenerTouchOneByOne::create();
   touchListener->onTouchBegan = [](Touch* touch, Event* event) -> bool {

      auto bounds = event->getCurrentTarget()->getBoundingBox();

      if (bounds.containsPoint(touch->getLocation())){
         std::stringstream touchDetails;
         touchDetails << "Touched at OpenGL coordinates: " << 
            touch->getLocation().x << "," << touch->getLocation().y << std::endl <<
            "Touched at UI coordinate: " << 
            touch->getLocationInView().x << "," << touch->getLocationInView().y << std::endl <<
            "Touched at local coordinate:" <<
            event->getCurrentTarget()->convertToNodeSpace(touch->getLocation()).x << "," <<  
            event->getCurrentTarget()->convertToNodeSpace(touch->getLocation()).y << std::endl <<
            "Touch moved by:" << touch->getDelta().x << "," << touch->getDelta().y;

            MessageBox(touchDetails.str().c_str(), "Touched");
         }
      return true;
      };

   Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(touchListener,sprite);
   this->addChild(sprite, 0);
    
    return true;
}

Ahora cuando lo ejecutas:

imagen

En este ejemplo, el evento táctil solo se activará si el usuario hizo clic en el Sprite en la escena. Observe la primera línea en el controlador onTouchBegan que llamo event->getCurrentTarget()? Aquí es donde el contexto cobra importancia. En la linea:

Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority(touchListener,sprite);

El segundo parámetro, sprite, es lo que determina el objetivo del Evento. El objetivo se pasa como un nodo, pero se puede convertir si es necesario.

lambda?

Las Lambda son una característica nueva de C++ y probablemente sean algo que amarás u odiarás. Si viene de C ++ o C #, probablemente los encontrará hace mucho tiempo, ¡ciertamente lo hago!
Lambda es una expresión que suena aterradora que proviene del símbolo de aspecto aterrador Λ. En el mundo de las matemáticas, el cálculo Lambda básicamente le da a las matemáticas la capacidad de definir funciones, algo que nosotros, como programadores, ciertamente podemos apreciar. En el mundo de la programación, no es tan aterrador, una expresión lamdba también puede considerarse como un función anónima. En términos simples, le permite crear una función sin nombre donde la necesite. Como puede ver en este ejemplo, le permite colocar la lógica de manejo de eventos donde tiene más sentido, en lugar de dividirla en una función separada. También es una bendición cuando desea pasar una función como parámetro, una tarea muy común en las bibliotecas estándar de C++.
La sintaxis de las lambdas de C++ es bastante fea, pero sin duda son una valiosa adición al lenguaje. Lo que es más importante, a menudo pueden hacer que su código sea más fácil de expresar y, como tal, más fácil de comprender y mantener. Aprende a amar a la lambda y la lambda aprenderá a amarte. Tal vez.

En este ejemplo, usamos el nodo de destino solo para manejar los clics que ocurren dentro de los límites de nuestro nodo Sprite. Esto se hace probando si la ubicación táctil está dentro del cuadro delimitador del nodo. Si es así, mostramos una serie de detalles en un cuadro de mensaje. Recuerda de nuevo en esta parte del tutorial donde dije que hay múltiples sistemas de coordenadas, este es un ejemplo perfecto. Como puede ver en el cuadro de mensaje anterior, getLocation() y getLocationInView() devuelven valores diferentes, uno relativo a la esquina superior izquierda de la pantalla, mientras que el otro es relativo a la esquina inferior izquierda de la pantalla.

A veces también desea saber dónde se produjo el clic en relación con el nodo. Como en el ejemplo anterior, la coordenada local es la posición en la que se produjo el clic. relativo al origen del nodo. Para calcular esta ubicación, usamos la función auxiliar convertToNodeSpace(). Una última cosa que puede notar es que registré EventListener con Director() en lugar de _eventListener. Esta era la forma antigua de hacer las cosas y lo hice así por un par de razones. Primero, demostrar que se puede. En segundo lugar, debido a que _eventListener es un miembro protegido, solo tendría acceso a él si derivara mi propio objeto Sprite.

Ahora echemos un vistazo a un ejemplo multitáctil.

Tratar con Multi-touch

Multi-touch funciona más o menos de la misma manera, solo que con un conjunto separado de controladores de eventos. Sin embargo, hay algunas trampas. El grande es iOS. Fuera de la caja, Android simplemente funciona. Sin embargo, iOS requiere que realice un pequeño cambio en el código para habilitar la compatibilidad multitáctil. No te preocupes, es un proceso simple.

En su proyecto, busque el directorio /proj.ios_mac/ios y abra el archivo AppController.mm. Luego agregue la siguiente línea:

AppControllerMM

Simplemente agregue la línea [eaglView setMultipleTouchEnabled:YES]; en algún lugar después de la creación de eaglView. Ahora multitouch debería funcionar en su aplicación iOS, veamos un poco de código:

MultiTouchScene.h

pragma once

#include "cocos2d.h"

class MultiTouch : public cocos2d::Layer
{

    public:
        static cocos2d::Scene* createScene();

        virtual bool init();
        CREATE_FUNC(MultiTouch);
    private:
        const static int MAX_TOUCHES = 5;

    protected:
        cocos2d::Label* labelTouchLocations[MAX_TOUCHES];

};

MultiTouchScene.cpp

#include "MultiTouchScene.h"

USING_NS_CC;

Scene* MultiTouch::createScene()
{
    auto scene = Scene::create();
    auto layer = MultiTouch::create();
    scene->addChild(layer);

    return scene;
}

bool MultiTouch::init()
{
    if ( !Layer::init() )
    {
        return false;
    }

    // Create an array of Labels to display touch locations and add them to this node, defaulted to invisible
    for(int i= 0; i < MAX_TOUCHES; ++i) {
        labelTouchLocations[i] = Label::createWithSystemFont("", "Arial", 42);
        labelTouchLocations[i]->setVisible(false);
        this->addChild(labelTouchLocations[i]);
    }

    auto eventListener = EventListenerTouchAllAtOnce::create();

    //  Create an eventListener to handle multiple touches, using a lambda, cause baby, it's C++11
    eventListener->onTouchesBegan = [=](const std::vector<Touch*>&touches, Event* event){

        // Clear all visible touches just in case there are less fingers touching than last time
        std::for_each(labelTouchLocations,labelTouchLocations+MAX_TOUCHES,[](Label* touchLabel){
            touchLabel->setVisible(false);
        });

        // For each touch in the touches vector, set a Label to display at it's location and make it visible
        for(int i = 0; i < touches.size(); ++i){
            labelTouchLocations[i]->setPosition(touches[i]->getLocation());
            labelTouchLocations[i]->setVisible(true);
            labelTouchLocations[i]->setString("Touched");
        }
    };

    _eventDispatcher->addEventListenerWithSceneGraphPriority(eventListener, this);

    return true;
}

Aquí está el código que se ejecuta en mi iPad con varios dedos tocados:

IMG_0189

Por supuesto, no es la captura de pantalla más emocionante de la historia, pero como puede ver, cada ubicación que toca el usuario, se imprime una etiqueta. Echemos un vistazo rápido al código y veamos qué está pasando. En este punto, la mayor parte debería ser bastante familiar, así que concentrémonos en las diferencias.

Primero notará que agregué una serie de etiquetas de tamaño MAX_TOUCH. Elegí 5 porque, francamente, ese parece ser el límite de lo que podría registrar en el iPad. Lo tenía configurado en 10, pero nunca registró más de 5, ¡así que era 5! La verdad es que no puedo imaginar que un esquema de control que use más de 5 toques sea tan útil, por lo que 5 toques parece una limitación razonable, aunque estoy bastante seguro de que el hardware puede manejar más.

En nuestro init() comenzamos asignando cada una de nuestras etiquetas y configurando su visibilidad inicial como invisible. Luego creamos nuestro EventListener, esta vez creamos un EventListenerTouchAllAtOnce porque queremos, bueno, obtener todos los eventos táctiles al mismo tiempo. En lugar de manejar onTouchBegan, manejamos onTouchesBegan, que toma un std::vector (cuidado aquí, ya que cocos2d tiene su propia clase de vector… ¡el peligro de usar el espacio de nombres abusivo!) de Touch* así como también un Event*.

En el caso de toque(s), primero recorremos todas nuestras etiquetas y las configuramos como invisibles. Luego, para cada toque en el vector de toques, movemos una etiqueta a esa posición y la hacemos visible. Una vez más registramos el EventListener con el _eventDispatcher de nuestro nodo.

Entonces, hemos cubierto el toque y el toque múltiple, ¿qué pasa cuando quieres usar el mouse? ¡Sorprendentemente, hay usuarios con ratones con más de un botón después de todo! 😉

Manejo del ratón

En este punto, probablemente puedas adivinar el código que estoy a punto de escribir, ya que el proceso es notablemente similar, pero analicémoslo de todos modos. No me molestaré con el archivo .h, no hay nada especial allí.

MouseScene.cpp

#include "MouseScene.h"

USING_NS_CC;

cocos2d::Scene* MouseScene::createScene()
{
    auto scene = Scene::create();
    auto layer = MouseScene::create();
    scene->addChild(layer);

    return scene;
}

bool MouseScene::init()
{
    if ( !Layer::init() )
    {
        return false;
    }   

   auto listener = EventListenerMouse::create();
   listener->onMouseDown = [](cocos2d::Event* event){

      try {
         EventMouse* mouseEvent = dynamic_cast<EventMouse*>(event);
         mouseEvent->getMouseButton();
         std::stringstream message;
         message << "Mouse event: Button: " << mouseEvent->getMouseButton() << "pressed at point (" <<
            mouseEvent->getLocation().x << "," << mouseEvent->getLocation().y << ")";
         MessageBox(message.str().c_str(), "Mouse Event Details");

      }
      catch (std::bad_cast& e){
         // Not sure what kind of event you passed us cocos, but it was the wrong one
         return;
      }
   };

   listener->onMouseMove = [](cocos2d::Event* event){
      // Cast Event to EventMouse for position details like above
      cocos2d::log("Mouse moved event");
   };

   listener->onMouseScroll = [](cocos2d::Event* event){
      cocos2d::log("Mouse wheel scrolled");
   };

   listener->onMouseUp = [](cocos2d::Event* event){
      cocos2d::log("Mouse button released");
   };

   _eventDispatcher->addEventListenerWithFixedPriority(listener, 1);

    return true;
}

Ahora ejecútelo, mueva la rueda del mouse un par de veces, haga clic y verá:

imagen

Sí… tampoco muy emocionante. Como puede ver, cuando hace clic con el mouse, el botón se devuelve como un número. El botón izquierdo es 0, el medio es 1, el derecho es 2, etc. El código es muy familiar excepto que usamos un EventListenerMouse esta vez y manejamos onMouseDown, onMouseUp, onMouseMove y onMouseScroll. La única otra cosa a tener en cuenta es que debe convertir el puntero de evento proporcionado en un puntero de EventMouse para obtener acceso a los detalles del mouse.

Con la excepción de los gestos, eso debería cubrir prácticamente todas sus necesidades táctiles y de mouse. Los gestos en realidad no son compatibles desde el primer momento, pero existen extensiones. Además, todos los eventos táctiles y del mouse contienen información delta, así como datos sobre el toque/clic anterior, lo que debería hacer que rodar el suyo sea bastante simple.


Parte anterior Tabla de contenido siguiente parte



Source link

Tags :
Cocos2dx,del,entrada,manejo,mouse,serie,táctil,tutoriales

Comparte :

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *