En este capítulo trataremos el input del usuario, hay varios tipos de input como acelerómetro teclado game cotroller... sin embargo nosotros solo veremos el touch input el más común en los juegos para móviles.
Además haremos que tanto el jugador como los enemigos disparen.
Para comenzar este capítulo haremos que nuestra nave se mueva allá donde el jugador ponga el dedo. Para ello debemos escuchar los touchEvent que se producen y decir a cocos que hacer en cada uno de ellos.
Crearemos una clase Touchcontroller que añadiremos como nodo hijo a la clase player y diga al player hacia donde debe moverse.
Analicemos paso por paso lo que en esta clase sucede. Para comenzar vemos en TouchController.h que declaramos una estructura llamada Controller, esta estructura será la encargada de almacenar la dirección que el usuario indica a la nave. Los macros usados ya nos son familiares de capítulos anteriores sin embargo aparecen 3 métodos onTouchBegan, onTouchMoved y onTouchEnded que son los que dirán a cocos qué hacer cuando un TouchEvent se produzca. En TouchController.cpp veremos cómo hacer esto.
En el método init() como los propios comentarios del código indican, creamos el listener mediante EventListenerTouchOneByOne::create() aquí creamos un listener para un touch event, si nuestro juego fuese multi touch usaríamos EventListenerTouchAllAtOnce::create() y los call back usados serian diferentes. Como no es el caso y solo necesitamos que el juego reconozca 1 toque a la vez, creamos este listener, le indicamos los call back usando el macro CC_CALLBACK_2 que es el macro usado para crear un std::bind a partir de la función indicada. Con el fin de mostrar qué es lo que hace este macro, para asignar onTouchBegan lo haremos a mano sin usar el macro CC_CALLBACK_2 que crea este std::bind a partir de una función que recibe 2 parámetros. Existen más macros de este tipo de forma que CC_CALLBACK_3 genera un std::bind a partir de una función de 3 parámetros, CC_CALLBACK_0 genera el bind a aprtir de una función sin parametros ...etc.
Una vez creado el listener, le decimos a cocos que lo use _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, this). Normalmente todo el proceso de crear y añadir el listener se hace en lo que sería nuestro GameLayer, sin embargo para este caso tan solo nos interesa que sea el controlador quién gestione el input.
Hay que tener en cuenta que este objeto será hijo de nuestro objeto Player por lo que tendríamos dos opciones, una sería llamar al método update del TouchController en el update del Player, sin embargo esa opción ya la conocemos pues es lo que usamos en GameLayer, la otra opción es añadir el update del TouchController de la misma forma que el update del GameLayer, con schedule(schedule_selector(TouchController::update))
Perfecto ya tenemos todo listo para implementar lo que el Touch Controller debe hacer.
En cada uno de los métodos del touchListener tan solo guardarnos las coordenadas que el usuario tocó, en una variable global y cuando el usuario levanta el dedo limpiamos esta variable asignándole Point(-1,-1) para saber que el usuario no está tocando la pantalla.
Ya casi hemos terminado con el controlador, echemos un ojo a su método update.
Bien, lo primero que hacemos antes de comenzar es limpiar todos los flags anteriores llamando al reset, luego comprobamos si el usuario está tocando la pantalla, de ser así, obtenemos la posición y el tamaño del Player llamando a getParent() pues este TouchController será un nodo hijo de Player como el anchor point está por defecto (es decir en el centro), al pedir la posición del Player nos dará su centro, en este caso nos interesa la esquina inferior izquierda, podemos cambiar el anchor point a Vec2(0,0) o restar a la posición que nos devuelve getPosition(), la mitad del alto y el ancho del sprite. Nos decantaremos por esta última opción para mantener el anchor point por defecto.
Una vez que ya tenemos la posición y el tamaño del Player tan solo miramos donde está poniendo el usuario el dedo con respecto a la posición de la nave y ponemos a true el flag correspondiente de la estructura Controller.
Nuestra clase TouchController ya está terminada, solo falta añadirla al Player y decirle a este como moverse.
Esta clase Player es idéntica a la del capítulo anterior, tan solo añadimos un nuevo campo TouchController* _controller y lo inicializamos en el método init() usando su crete() _controller = TouchController::create(); posteriormente, lo añadimos como hijo de Player addChild(_controller)
En el update de esta clase, obtenemos el Controller de TouchController y en función de los flags que estén en dicho controlador, movemos la nave en una dirección u otra.
No te olvides de llamar este método update en el update del GameLayer o usar el schedule_selector para incluir el update del Player en el Game loop
Ya está, si ejecutas ahora el juego y haces click sobre la pantalla, veras que la nave se dirigirá al puntero del ratón, de la misma forma en que irá hacia el lugar donde tocamos la pantalla cuando lancemos el juego en un Smartphone. Puedes descargar la carpeta Classes del proyecto aquí
El jugador ya puede mover la nave, ahora haremos que tanto el player como los enemigos disparen. Para comenzar crearemos una clase bullet que nos servirá tanto para las balas de los enemigos como para las balas del player por el momento, más adelante se separarán en clases independientes pero por ahora nos vale así.
No hay mucho que analizar de esta clase, ya sabemos para qué sirven todos los macros usados. En su método update simplemente movemos la bala hasta el final de la pantalla, si es una bala del player, se moverá hasta el final superior de la pantalla, si es una bala de un enemigo, se moverá hasta el final inferior de la pantalla. En ambos casos al llegar al final se llama a setVisisble(flase) por defecto ninguna bala será visible, veremos el por qué en la clase Player.
Así queda la clase Player tras incorporar la funcionalidad de disparar, analicemos poco a poco que es lo nuevo que se ha añadido y como funciona. Para comenzar, vemos en el método init() que hemos añadido un bucle que crea y añade a un vector todas las balas del Player, es un pool de objetos. En nuestro caso con 30 es suficiente. Creamos esas 30 balas y posteriormente las haremos hijas de la escena principal, no del Player, esto es muy importante, ahora veremos cómo y porque hacemos esto.
Como vemos sobrescribimos el método setParent, la razón es la siguiente, los objetos que se añaden como hijos de un padre, sufren las mismas transformaciones que los padres esto es, si añadimos un objeto Bullet al Player y movemos el Player, además de moverse la bala por su propio método update, su posición se actualizará con respecto a la posición de su padre, el objeto Player. Por esto simplemente sobrescribimos setParent para que en el momento en que el objeto Player sea añadido al GameLayer, podamos añadir las balas al GameLayer y así mantener la referencia en la clase Player gracias al _bulletPool. Veamos ahora cómo hacer que la nave dispare cada cierto tiempo.
scheduleShoot() usa lo que se denomina una secuencia en Cocos2d-x, una secuencia nos permite lanzar varias acciones con un orden determinado, en este caso construimos una secuencia que espera medio segundo y después llama al método shoot(). Para crear una acción a partir de una función usamos el macro CC_CALLBACK_0 que ya nos es familiar pues lo usamos en nuestro controlador.
El metodo shoot() lo único que hace es iterar sobre el pool de balas y activa aquella a la que le llegue el turno. Si la bala llega al final de la pantalla o como veremos más adelante, si intercepta un enemigo, la bala se desactiva. Para activar o desactivar la bala usamos setVisible(true|false).
Descarga la carpeta Classes del proyecto hasta este punto aquí