Cómo construir un corredor infinito en iOS: Cocos2D, automatización y más
Publicado: 2022-03-11Desarrollar juegos para iOS puede ser una experiencia enriquecedora en términos de crecimiento personal y financiero. A principios de este año, implementé un juego basado en Cocos2D, Bee Race, en la App Store. Su mecánica es sencilla: un corredor infinito en el que los jugadores (en este caso, las abejas) acumulan puntos y sortean obstáculos. Ver aquí para una demostración.
En este tutorial, explicaré el proceso detrás del desarrollo de juegos para iOS, desde Cocos2D hasta la publicación. Como referencia, aquí hay una breve tabla de contenido:
- Sprites y objetos físicos
- Una breve introducción a Cocos2D
- Uso de Cocos2D con guiones gráficos
- Jugabilidad y descripción (breve) del proyecto
- Automatice los trabajos. Usa herramientas. Relájate.
- Facturación en la aplicación
- Juego multijugador con Game Center
- Margen de mejora
- Conclusión
Sprites y objetos físicos
Antes de entrar en los detalles ásperos, será útil comprender la distinción entre sprites y objetos físicos.
Para cualquier entidad dada que aparece en la pantalla de un juego de corredor sin fin, la representación gráfica de esa entidad se denomina sprite , mientras que la representación poligonal de esa entidad en el motor de física se denomina objeto físico .
Entonces, el sprite se dibuja en la pantalla, respaldado por su objeto físico correspondiente, que luego es manejado por su motor de física. Esa configuración se puede visualizar aquí, donde los sprites se muestran en la pantalla, con sus contrapartes poligonales físicas delineadas en verde:
Los objetos físicos no están conectados a sus respectivos sprites de forma predeterminada, lo que significa que usted, como desarrollador de iOS, puede elegir qué motor de física usar y cómo conectar sprites y cuerpos. La forma más común es crear una subclase del sprite predeterminado y agregarle un cuerpo físico concreto.
Con eso en mente…
Un breve tutorial sobre el desarrollo de juegos iOS Cocos2D
Cocos2D-iphone es un marco de código abierto para iOS que utiliza OpenGL para la aceleración de gráficos de hardware y es compatible con los motores de física Chipmunk y Box2D.
En primer lugar, ¿por qué necesitamos un marco de este tipo? Bueno, para empezar, los marcos implementan los componentes más utilizados del desarrollo de juegos. Por ejemplo, Cocos2D puede cargar sprites (en particular, hojas de sprites (¿por qué?)), iniciar o detener un motor de física y manejar adecuadamente el tiempo y la animación. Y hace todo esto con un código que ha sido revisado y probado exhaustivamente. ¿Por qué dedicar su propio tiempo a reescribir un código probablemente inferior?
Sin embargo, quizás lo más importante es que el desarrollo de juegos de Cocos2D utiliza aceleración de hardware de gráficos . Sin tal aceleración, cualquier juego de corredor infinito de iOS con incluso una cantidad moderada de sprites se ejecutará con un rendimiento notablemente bajo. Si tratamos de hacer una aplicación más complicada, probablemente comenzaremos a ver un efecto de "tiempo de viñeta" en la pantalla, es decir, múltiples copias de cada sprite mientras intenta animarse.
Finalmente, Cocos2D optimiza el uso de la memoria ya que almacena en caché los sprites. Por lo tanto, los sprites duplicados requieren una memoria adicional mínima, lo que obviamente es útil para los juegos.
Uso de Cocos2D con guiones gráficos
Después de todos los elogios que le he dado a Cocos2D, puede parecer ilógico sugerir el uso de guiones gráficos. ¿Por qué no simplemente manipular sus objetos con Cocos2D, etc.? Bueno, para ser honesto, para las ventanas estáticas, a menudo es más conveniente usar el Interface Builder de Xcode y su mecanismo Storyboard.
En primer lugar, me permite arrastrar y posicionar todos mis elementos gráficos para mi juego de corredor sin fin con mi mouse. En segundo lugar, la API Storyboard es muy, muy útil. (Y sí, sé lo de Cocos Builder).
Aquí hay un vistazo rápido de mi guión gráfico:
El controlador de vista principal del juego solo contiene una escena Cocos2D con algunos elementos HUD en la parte superior:
Presta atención al fondo blanco: es una escena de Cocos2D, que cargará todos los elementos gráficos necesarios en tiempo de ejecución. Otras vistas (indicadores en vivo, dientes de león, botones, etc.) son todas vistas estándar de Cocoa, agregadas a la pantalla usando Interface Builder.
No me detendré en los detalles; si está interesado, puede encontrar ejemplos en GitHub.
Jugabilidad y descripción (breve) del proyecto
(Para proporcionar algo más de motivación, me gustaría describir mi juego de corredor sin fin con un poco más de detalle. Siéntase libre de omitir esta sección si desea continuar con la discusión técnica).
Durante el juego en vivo, la abeja está inmóvil, y el campo en sí está corriendo, trayendo consigo varios peligros (arañas y flores venenosas) y ventajas (dientes de león y sus semillas).
Cocos2D tiene un objeto de cámara que fue diseñado para seguir al personaje; en la práctica, era menos complicado manipular el CCLayer que contenía el mundo del juego.
Los controles son simples: tocar la pantalla mueve la abeja hacia arriba y otro toque la mueve hacia abajo.
La capa del mundo en sí tiene dos subcapas. Cuando comienza el juego, la primera subcapa se completa de 0 a BUF_LEN y se muestra inicialmente. La segunda subcapa se llena por adelantado desde BUF_LEN hasta 2*BUF_LEN. Cuando la abeja alcanza BUF_LEN, la primera subcapa se limpia y se repuebla instantáneamente de 2*BUF_LEN a 3*BUF_LEN, y se presenta la segunda subcapa. De esta forma, alternamos entre capas, nunca reteniendo objetos obsoletos, parte importante para evitar pérdidas de memoria.
En términos de motores de física, usé Chipmunk por dos razones:
- Está escrito en Objective-C puro.
- He trabajado con Box2D antes, así que quería comparar los dos.
El motor de física en realidad solo se usó para la detección de colisiones. A veces, me preguntan: "¿Por qué no escribiste tu propia detección de colisiones?". En realidad, no tiene mucho sentido eso. Los motores de física fueron diseñados para ese mismo propósito: pueden detectar colisiones entre cuerpos de formas complicadas y optimizar ese proceso. Por ejemplo, los motores de física a menudo dividen el mundo en celdas y realizan comprobaciones de colisión solo para cuerpos en la misma celda o en celdas adyacentes.
Automatice los trabajos. Usa herramientas. Relájate.
Un componente clave del desarrollo de juegos indie infinite runner es evitar tropezar con problemas pequeños. El tiempo es un recurso crucial al desarrollar una aplicación, y la automatización puede ahorrar mucho tiempo.
Pero a veces, la automatización también puede ser un compromiso entre el perfeccionismo y el cumplimiento de su fecha límite. En este sentido, el perfeccionismo puede ser un asesino de Angry Birds.
Por ejemplo, en otro juego de iOS que estoy desarrollando actualmente, construí un marco para crear diseños usando una herramienta especial (disponible en GitHub). Este framework tiene sus limitaciones (por ejemplo, no tiene buenas transiciones entre escenas), pero usarlo me permite hacer mis escenas en una décima parte del tiempo.
Entonces, si bien no puede construir su propio supermarco con superherramientas especiales, aún puede y debe automatizar tantas de estas pequeñas tareas como sea posible.
En la construcción de este corredor infinito, la automatización fue clave una vez más. Por ejemplo, mi artista me enviaba gráficos de alta resolución a través de una carpeta especial de Dropbox. Para ahorrar tiempo, escribí algunas secuencias de comandos para crear automáticamente conjuntos de archivos para las diversas resoluciones de destino requeridas por la App Store, agregando también -hd o @2x (dichas secuencias de comandos se basan en ImageMagick).
En términos de herramientas adicionales, TexturePacker me pareció muy útil: puede empaquetar sprites en hojas de sprites para que su aplicación consuma menos memoria y se cargue más rápido, ya que todos sus sprites se leerán desde un solo archivo. También puede exportar texturas en casi todos los formatos de marcos posibles. (Tenga en cuenta que TexturePacker no es una herramienta gratuita, pero creo que vale la pena el precio. También puede consultar alternativas gratuitas como ShoeBox).

La principal dificultad asociada con la física del juego es crear polígonos adecuados para cada sprite. En otras palabras, crear una representación poligonal de una abeja o flor de forma oscura. Ni siquiera intente hacer esto a mano, use siempre aplicaciones especiales, de las cuales hay muchas. Algunos incluso son bastante... exóticos, como crear máscaras vectoriales con Inkspace y luego importarlas al juego.
Para el desarrollo de mi propio juego de corredor sin fin, creé una herramienta para automatizar este proceso, que llamo Andengine Vertex Helper. Como sugiere el nombre, inicialmente se diseñó para el marco de Andengine, aunque en la actualidad funcionará adecuadamente con una serie de formatos.
En nuestro caso, necesitamos usar el patrón plist:
<real>%.5f</real><real>%.5f</real>
A continuación, creamos un archivo plist con descripciones de objetos:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>jet_ant</key> <dict> <key>vertices</key> <array> <real>-0.18262</real><real>0.08277</real> <real>-0.14786</real><real>-0.22326</real> <real>0.20242</real><real>-0.55282</real> <real>0.47047</real><real>0.41234</real> <real>0.03823</real><real>0.41234</real> </array> </dict> </dict> </plist>
Y un cargador de objetos:
- (void)createBodyAtLocation:(CGPoint)location{ float mass = 1.0; body = cpBodyNew(mass, cpMomentForBox(mass, self.sprite.contentSize.width*self.sprite.scale, self.sprite.contentSize.height*self.sprite.scale)); body->p = location; cpSpaceAddBody(space, body); NSString *path =[[NSBundle mainBundle] pathForResource:@"obj _descriptions" ofType:@"plist"]; // <- load plist NSDictionary *objConfigs = [[[NSDictionary alloc] initWithContentsOfFile:path] autorelease]; NSArray *vertices = [[objConfigs objectForKey:namePrefix] objectForKey:@"vertices"]; shape = [ChipmunkUtil polyShapeWithVertArray:vertices withBody:body width:self.sprite.contentSize.width height:self.sprite.contentSize.height]; shape->e = 0.7; shape->u = 1.0; shape->collision_type = OBJ_COLLISION_TYPE; cpSpaceAddShape(space, shape); }
Para probar cómo los sprites se corresponden con sus cuerpos físicos, mira aquí.
Mucho mejor, ¿verdad?
En resumen, siempre automatice cuando sea posible. Incluso los scripts simples pueden ahorrarle toneladas de tiempo. Y lo que es más importante, ese tiempo se puede usar para programar en lugar de hacer clic con el mouse. (Para una motivación adicional, aquí hay un token XKCD).
Facturación en la aplicación
Las bolas de aire recolectadas en el juego actúan como una moneda en la aplicación, lo que permite a los usuarios comprar nuevas máscaras para su abeja. Sin embargo, esta moneda también se puede comprar con dinero real. Un punto importante a tener en cuenta con respecto a la facturación en la aplicación es si necesita o no realizar comprobaciones del lado del servidor para verificar la validez de la compra. Dado que todos los bienes que se pueden comprar son esencialmente iguales en términos de juego (solo alteran la apariencia de la abeja), no es necesario realizar una verificación del servidor para verificar la validez de la compra. Sin embargo, en muchos casos, definitivamente tendrás que hacerlo.
Para obtener más información, Ray Wenderlich tiene el tutorial de facturación perfecto en la aplicación.
Juego multijugador con Game Center
En los juegos móviles, socializar es más que simplemente agregar un botón "Me gusta" de Facebook o configurar tablas de clasificación. Para hacer el juego más emocionante, implementé una versión multijugador.
¿Como funciona? En primer lugar, se conectan dos jugadores mediante el emparejamiento en tiempo real de iOS Game Center. Como los jugadores realmente están jugando el mismo juego de corredor infinito, debe haber un solo conjunto de objetos de juego. Eso significa que la instancia de un jugador debe generar los objetos, y la del otro jugador los leerá. En otras palabras, si los dispositivos de ambos jugadores estuvieran generando objetos de juego, sería difícil sincronizar la experiencia.
Con eso en mente, una vez establecida la conexión, ambos jugadores se envían un número aleatorio. El jugador con el número más alto actúa como “servidor”, creando objetos de juego.
¿Recuerdas la discusión sobre la generación del mundo en porciones? ¿Dónde teníamos dos subcapas, una de 0 a BUF_LEN y la otra de BUF_LEN a 2*BUF_LEN? Esta arquitectura no se usó por accidente: era necesaria para proporcionar gráficos fluidos en redes retrasadas. Cuando se genera una parte de los objetos, se empaqueta en un plist y se envía al otro jugador. El búfer es lo suficientemente grande como para permitir que el segundo jugador juegue incluso con un retraso en la red. Ambos jugadores se envían mutuamente su posición actual con un lapso de medio segundo, enviando también sus movimientos de subida y bajada de forma inmediata. Para suavizar la experiencia, la posición y la velocidad se corrigen cada 0,5 segundos con una animación suave, por lo que en la práctica parece que el otro jugador se mueve o acelera gradualmente.
Ciertamente, hay más consideraciones que hacer con respecto al modo de juego multijugador sin fin, pero espero que esto te dé una idea de los tipos de desafíos involucrados.
Margen de mejora
Los juegos nunca se terminan. Es cierto que hay varias áreas en las que me gustaría mejorar, a saber:
- Problemas de control: tocar suele ser un gesto poco intuitivo para los jugadores que prefieren deslizarse.
La capa mundial se mueve mediante la acción CCMoveBy. Esto estaba bien cuando la velocidad de la capa mundial era constante, ya que la acción CCMoveBy se ciclaba con CCRepeatForever:
-(void) infiniteMove{ id actionBy = [CCMoveBy actionWithDuration: BUFFER_DURATION position: ccp(-BUFFER_LENGTH, 0)]; id actionCallFunc = [CCCallFunc actionWithTarget:self selector:@selector(requestFillingNextBuffer)]; id actionSequence = [CCSequence actions: actionBy, actionCallFunc, nil]; id repeateForever = [CCRepeatForever actionWithAction:actionSequence]; [self.bufferContainer runAction:repeateForever]; }
Pero luego, agregué un aumento de velocidad mundial para hacer el juego más difícil a medida que avanza:
-(void) infiniteMoveWithAccel { float duration = BUFFER_DURATION-BUFFER_ACCEL*self.lastBufferNumber; duration = max(duration, MIN_BUFFER_DURATION); id actionBy = [CCMoveBy actionWithDuration: duration position: ccp(-BUFFER_LENGTH, 0)]; id restartMove = [CCCallFunc actionWithTarget:self selector:@selector(infiniteMoveWithAccel)]; id fillBuffer = [CCCallFunc actionWithTarget:self selector:@selector(requestFillingNextBuffer)]; id actionSequence = [CCSequence actions: actionBy, restartMove, fillBuffer, nil]; [self.bufferContainer runAction:actionSequence]; }
Este cambio hizo que la animación se rompiera en cada reinicio de la acción. Traté de solucionar el problema, sin éxito. Sin embargo, mis probadores beta no notaron el comportamiento, así que pospuse la corrección.
- Por un lado, no ha habido necesidad de escribir mi propia autorización para multijugador cuando uso Game Center o ejecuto mi propio servidor de juegos. Por otro lado, ha hecho que sea imposible crear bots, algo que me gustaría cambiar.
Conclusión
Crear tu propio juego de corredor infinito independiente puede ser una gran experiencia. Y una vez que llega al paso de publicación del proceso, puede ser una sensación maravillosa a medida que libera su propia creación en la naturaleza.
El proceso de revisión puede variar desde varios días hasta varias semanas. Para obtener más información, aquí hay un sitio útil que utiliza datos de fuentes múltiples para estimar los tiempos de revisión actuales.
Además, recomiendo usar AppAnnie para examinar diversa información sobre todas las aplicaciones en la App Store, y también puede ser útil registrarse en algunos servicios de análisis como Flurry Analytics.
Y si este juego te ha intrigado, asegúrate de visitar Bee Race en la tienda.