Comment créer un coureur infini sur iOS : Cocos2D, automatisation, etc.
Publié: 2022-03-11Développer des jeux iOS peut être une expérience enrichissante en termes de croissance personnelle et financière. Plus tôt cette année, j'ai déployé un jeu basé sur Cocos2D, Bee Race, sur l'App Store. Son gameplay est simple : un coureur infini dans lequel les joueurs (dans ce cas, des abeilles) collectent des points et évitent les obstacles. Voir ici pour une démo.
Dans ce didacticiel, j'expliquerai le processus de développement de jeux pour iOS, de Cocos2D à la publication. Pour référence, voici une courte table des matières :
- Sprites et objets physiques
- Une brève introduction à Cocos2D
- Utiliser Cocos2D avec des storyboards
- Gameplay et (brève) description du projet
- Automatisez les travaux. Utilisez des outils. Soit cool.
- Facturation intégrée à l'application
- Jeu multijoueur avec le Game Center
- Marge d'amélioration
- Conclusion
Sprites et objets physiques
Avant d'entrer dans les détails, il sera utile de comprendre la distinction entre les sprites et les objets physiques.
Pour toute entité donnée qui apparaît sur l'écran d'un jeu de course sans fin, la représentation graphique de cette entité est appelée sprite , tandis que la représentation polygonale de cette entité dans le moteur physique est appelée objet physique .
Ainsi, le sprite est dessiné sur l'écran, soutenu par son objet physique correspondant, qui est ensuite géré par votre moteur physique. Cette configuration peut être visualisée ici, où les sprites sont affichés à l'écran, avec leurs homologues polygonaux physiques encadrés en vert :
Les objets physiques ne sont pas connectés à leurs sprites respectifs par défaut, ce qui signifie que vous, en tant que développeur iOS, pouvez choisir le moteur physique à utiliser et comment connecter les sprites et les corps. La manière la plus courante consiste à sous-classer le sprite par défaut et à lui ajouter un corps physique concret.
Dans cet esprit…
Un bref tutoriel sur le développement de jeux iOS Cocos2D
Cocos2D-iphone est un framework open source pour iOS qui utilise OpenGL pour l'accélération graphique matérielle et prend en charge les moteurs physiques Chipmunk et Box2D.
Tout d'abord, pourquoi avons-nous besoin d'un tel cadre? Eh bien, pour commencer, les frameworks implémentent les composants souvent utilisés du développement de jeux. Par exemple, Cocos2D peut charger des sprites (en particulier des feuilles de sprites (pourquoi ?)), lancer ou arrêter un moteur physique, et gérer correctement le timing et l'animation. Et il fait tout cela avec du code qui a été revu et testé de manière approfondie - pourquoi consacrer votre temps à réécrire du code probablement inférieur ?
Peut-être le plus important, cependant - le développement de jeux Cocos2D utilise l'accélération matérielle graphique . Sans une telle accélération, tout jeu de coureur infini iOS avec même un nombre modéré de sprites fonctionnera avec des performances particulièrement médiocres. Si nous essayons de créer une application plus compliquée, nous commencerons probablement à voir un effet "bullet-time" sur l'écran, c'est-à-dire plusieurs copies de chaque sprite alors qu'il tente de s'animer.
Enfin, Cocos2D optimise l'utilisation de la mémoire puisqu'il met en cache les sprites. Ainsi, tout sprite dupliqué nécessite un minimum de mémoire supplémentaire, ce qui est évidemment utile pour les jeux.
Utiliser Cocos2D avec des storyboards
Après tous les éloges que j'ai reçus de Cocos2D, il peut sembler illogique de suggérer d'utiliser des storyboards. Pourquoi ne pas simplement manipuler vos objets avec Cocos2D, etc. ? Eh bien, pour être honnête, pour les fenêtres statiques, il est souvent plus pratique d'utiliser Interface Builder de Xcode et son mécanisme Storyboard.
Tout d'abord, cela me permet de faire glisser et de positionner tous mes éléments graphiques pour mon jeu de coureur sans fin avec ma souris. Deuxièmement, l'API Storyboard est très, très utile. (Et oui, je connais Cocos Builder).
Voici un petit aperçu de mon Storyboard :
Le contrôleur de vue principal du jeu contient juste une scène Cocos2D avec quelques éléments HUD en haut :
Faites attention au fond blanc : c'est une scène Cocos2D, qui chargera tous les éléments graphiques nécessaires à l'exécution. Les autres vues (indicateurs en direct, pissenlits, boutons, etc.) sont toutes des vues standard de Cocoa, ajoutées à l'écran à l'aide d'Interface Builder.
Je ne m'attarderai pas sur les détails - si cela vous intéresse, des exemples peuvent être trouvés sur GitHub.
Gameplay et (brève) description du projet
(Pour vous motiver davantage, j'aimerais décrire mon jeu de coureur sans fin un peu plus en détail. N'hésitez pas à ignorer cette section si vous souhaitez passer à la discussion technique.)
Pendant le jeu en direct, l'abeille est immobile et le champ lui-même se précipite, apportant avec lui divers dangers (araignées et fleurs vénéneuses) et avantages (pissenlits et leurs graines).
Cocos2D a un objet caméra qui a été conçu pour suivre le personnage ; en pratique, il était moins compliqué de manipuler le CCLayer contenant le monde du jeu.
Les commandes sont simples : toucher l'écran déplace l'abeille vers le haut et une autre touche la déplace vers le bas.
La couche monde elle-même a en fait deux sous-couches. Lorsque le jeu démarre, la première sous-couche est peuplée de 0 à BUF_LEN et affichée initialement. La deuxième sous-couche est pré-renseignée de BUF_LEN à 2*BUF_LEN. Lorsque l'abeille atteint BUF_LEN, la première sous-couche est nettoyée et instantanément repeuplée de 2*BUF_LEN à 3*BUF_LEN, et la deuxième sous-couche est présentée. De cette façon, nous alternons entre les couches, ne conservant jamais les objets obsolètes, une partie importante pour éviter les fuites de mémoire.
En termes de moteurs physiques, j'ai utilisé Chipmunk pour deux raisons :
- Il est écrit en pur Objective-C.
- J'ai déjà travaillé avec Box2D, alors je voulais comparer les deux.
Le moteur physique n'était vraiment utilisé que pour la détection de collision. Parfois, on me demande : « Pourquoi n'avez-vous pas écrit votre propre détection de collision ? ». En réalité, cela n'a pas beaucoup de sens. Les moteurs physiques ont été conçus dans ce but : ils peuvent détecter les collisions entre des corps de formes complexes et optimiser ce processus. Par exemple, les moteurs physiques divisent souvent le monde en cellules et effectuent des contrôles de collision uniquement pour les corps dans les mêmes cellules ou dans des cellules adjacentes.
Automatisez les travaux. Utilisez des outils. Soit cool.
Un élément clé du développement de jeux de coureurs infinis indépendants est d'éviter de trébucher sur de petits problèmes. Le temps est une ressource cruciale lors du développement d'une application, et l'automatisation peut faire gagner énormément de temps.
Mais parfois, l'automatisation peut aussi être un compromis entre perfectionnisme et respect des délais. En ce sens, le perfectionnisme peut être un tueur d'Angry Birds.
Par exemple, dans un autre jeu iOS que je développe actuellement, j'ai construit un framework pour créer des mises en page à l'aide d'un outil spécial (disponible sur GitHub). Ce framework a ses limites (par exemple, il n'a pas de belles transitions entre les scènes), mais l'utiliser me permet de faire mes scènes en un dixième de temps.
Ainsi, bien que vous ne puissiez pas créer votre propre superframework avec des superoutils spéciaux, vous pouvez et devez automatiser autant de ces petites tâches que possible.
Dans la construction de ce coureur infini, l'automatisation était une fois de plus la clé. Par exemple, mon artiste m'enverrait des graphiques haute résolution via un dossier Dropbox spécial. Pour gagner du temps, j'ai écrit des scripts pour construire automatiquement des ensembles de fichiers pour les différentes résolutions cibles requises par l'App Store, en ajoutant également -hd ou @2x (ces scripts sont basés sur ImageMagick).
En termes d'outils supplémentaires, j'ai trouvé TexturePacker très utile - il peut regrouper des sprites dans des feuilles de sprites afin que votre application consomme moins de mémoire et se charge plus rapidement, car tous vos sprites seront lus à partir d'un seul fichier. Il peut également exporter des textures dans presque tous les formats de frameworks possibles. (Notez que TexturePacker n'est pas un outil gratuit, mais je pense que cela vaut le prix. Vous pouvez également consulter des alternatives gratuites comme ShoeBox.)

La principale difficulté associée à la physique du jeu est de créer des polygones appropriés pour chaque sprite. En d'autres termes, créer une représentation polygonale d'une abeille ou d'une fleur de forme obscure. N'essayez même pas de le faire à la main, utilisez toujours des applications spéciales, qui sont nombreuses. Certains sont même assez… exotiques, comme créer des masques vectoriels avec Inkspace, puis les importer dans le jeu.
Pour mon propre développement de jeu de course sans fin, j'ai créé un outil pour automatiser ce processus, que j'appelle Andengine Vertex Helper. Comme son nom l'indique, il a été initialement conçu pour le framework Andengine, bien qu'il fonctionne de manière appropriée avec un certain nombre de formats de nos jours.
Dans notre cas, nous devons utiliser le modèle plist :
<real>%.5f</real><real>%.5f</real>
Ensuite, nous créons un fichier plist avec des descriptions d'objet :
<?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>
Et un chargeur d'objet :
- (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); }
Pour tester comment les sprites correspondent à leur corps physique, voir ici.
Bien mieux, non ?
En résumé, automatisez toujours lorsque cela est possible. Même des scripts simples peuvent vous faire gagner beaucoup de temps. Et surtout, ce temps peut être utilisé pour la programmation au lieu de cliquer avec la souris. (Pour plus de motivation, voici un jeton XKCD.)
Facturation intégrée à l'application
Les blowballs collectés dans le jeu agissent comme une monnaie intégrée à l'application, permettant aux utilisateurs d'acheter de nouveaux skins pour leur abeille. Cependant, cette devise peut également être achetée avec de l'argent réel. Un point important à noter en ce qui concerne la facturation intégrée à l'application est de savoir si vous devez ou non effectuer des vérifications côté serveur pour la validité de l'achat. Étant donné que tous les biens achetables sont essentiellement égaux en termes de gameplay (modifiant simplement l'apparence de l'abeille), il n'est pas nécessaire d'effectuer une vérification du serveur pour la validité de l'achat. Cependant, dans de nombreux cas, vous devrez certainement le faire.
Pour en savoir plus, Ray Wenderlich propose le didacticiel de facturation intégré parfait.
Jeu multijoueur avec le Game Center
Dans les jeux mobiles, la socialisation ne se limite pas à ajouter un bouton "J'aime" sur Facebook ou à créer des classements. Pour rendre le jeu plus excitant, j'ai implémenté une version multijoueur.
Comment ça marche? Tout d'abord, deux joueurs sont connectés à l'aide du match-making en temps réel d'iOS Game Center. Comme les joueurs jouent réellement au même jeu de coureur infini, il ne doit y avoir qu'un seul ensemble d'objets de jeu. Cela signifie que l'instance d'un joueur doit générer les objets et que l'autre joueur les lira. En d'autres termes, si les appareils des deux joueurs généraient des objets de jeu, il serait difficile de synchroniser l'expérience.
Dans cet esprit, une fois la connexion établie, les deux joueurs s'envoient un nombre aléatoire. Le joueur avec le nombre le plus élevé agit en tant que "serveur", créant des objets de jeu.
Vous souvenez-vous de la discussion sur la génération du monde en portions ? Où avions-nous deux sous-couches, une de 0 à BUF_LEN et l'autre de BUF_LEN à 2*BUF_LEN ? Cette architecture n'a pas été utilisée par accident - il était nécessaire de fournir des graphismes fluides sur des réseaux retardés. Lorsqu'une partie des objets est générée, elle est emballée dans un plist et envoyée à l'autre joueur. La mémoire tampon est suffisamment grande pour permettre au deuxième joueur de jouer même avec un retard réseau. Les deux joueurs s'envoient leur position actuelle avec une période d'une demi-seconde, envoyant également leurs mouvements de haut en bas immédiatement. Pour adoucir l'expérience, la position et la vitesse sont corrigées toutes les 0,5 seconde avec une animation fluide, donc en pratique, il semble que l'autre joueur se déplace ou accélère progressivement.
Il y a certainement d'autres considérations à prendre en compte en ce qui concerne le gameplay multijoueur sans fin, mais j'espère que cela vous donnera une idée des types de défis impliqués.
Marge d'amélioration
Les jeux ne sont jamais terminés. Certes, il y a plusieurs domaines dans lesquels j'aimerais améliorer le mien, à savoir :
- Problèmes de contrôle : le tapotement est souvent un geste peu intuitif pour les joueurs qui préfèrent glisser.
La couche du monde est déplacée à l'aide de l'action CCMoveBy. C'était bien lorsque la vitesse de la couche mondiale était constante, puisque l'action CCMoveBy était cyclée avec 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]; }
Mais plus tard, j'ai ajouté une augmentation de la vitesse du monde pour rendre le jeu plus difficile au fur et à mesure :
-(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]; }
Ce changement provoquait une déchirure de l'animation à chaque redémarrage de l'action. J'ai essayé de régler le problème, en vain. Cependant, mes bêta-testeurs n'ont pas remarqué le comportement, j'ai donc reporté le correctif.
- D'une part, il n'a pas été nécessaire d'écrire ma propre autorisation pour le multijoueur lors de l'utilisation de Game Center ou de l'exécution de mon propre serveur de jeu. D'un autre côté, il est impossible de créer des bots, ce que j'aimerais changer.
Conclusion
Créer votre propre jeu de coureur infini indépendant peut être une expérience formidable. Et une fois que vous arrivez à l'étape de publication du processus, cela peut être un sentiment merveilleux lorsque vous publiez votre propre création dans la nature.
Le processus d'examen peut durer de plusieurs jours à plusieurs semaines. Pour en savoir plus, il existe un site utile ici qui utilise des données provenant de la foule pour estimer les temps de révision actuels.
De plus, je recommande d'utiliser AppAnnie pour examiner diverses informations sur toutes les applications de l'App Store, et l'inscription à certains services d'analyse comme Flurry Analytics peut également être utile.
Et si ce jeu vous a intrigué, assurez-vous de vérifier Bee Race dans le magasin.