Dans cette série de didacticiels, je vais vous montrer comment créer un jeu de tir double-stick inspiré de Geometry Wars, avec des graphismes au néon, des effets de particules incroyables et une musique géniale, pour iOS avec C ++ et OpenGL ES 2.0..
Plutôt que de compter sur une infrastructure de jeu ou une bibliothèque de sprites existante, nous tenterons de programmer aussi près que possible du matériel (ou du "métal nu"). Étant donné que les appareils fonctionnant sous iOS fonctionnent avec un matériel plus petit que celui d'un ordinateur de bureau ou d'une console de jeux, cela nous permettra d'en obtenir le maximum pour notre argent..
Articles SimilairesL'objectif de ces tutoriels est de passer en revue les éléments nécessaires qui vous permettront de créer votre propre jeu mobile de haute qualité pour iOS, à partir de zéro ou à partir d'un jeu de bureau existant. Je vous encourage à télécharger et à jouer avec le code, ou même à l'utiliser comme base pour vos propres projets..
Nous aborderons les sujets suivants au cours de cette série:
Voici ce que nous aurons à la fin de la série:
Et voici ce que nous aurons à la fin de cette première partie:
La musique et les effets sonores que vous pouvez entendre dans ces vidéos ont été créés par RetroModular, et vous pouvez lire comment il l'a fait dans notre section audio..
Les sprites sont de Jacob Zinman-Jeanes, notre designer Tuts + résident.
La police que nous utiliserons est une police bitmap (en d’autres termes, pas une "police" réelle, mais un fichier image), ce que j’ai créé pour ce tutoriel..
Toutes les illustrations peuvent être trouvées dans les fichiers source.
Commençons.
Avant de plonger dans les détails du jeu, parlons de la bibliothèque d’utilitaires et du code d’application Bootstrap que j’ai fournis pour prendre en charge le développement de notre jeu..
Bien que nous utilisions principalement C ++ et OpenGL pour coder notre jeu, nous aurons besoin de classes d’utilitaires supplémentaires. Ce sont toutes des classes que j'ai écrites pour aider au développement dans d'autres projets, elles sont donc testées et utilisables pour de nouveaux projets tels que celui-ci..
package.h
: Un en-tête de commodité utilisé pour inclure tous les en-têtes pertinents de la bibliothèque d’utilitaires. Nous allons l'inclure en précisant #include "Utility / package.h"
sans avoir à inclure autre chose.Nous tirerons parti de certains modèles de programmation éprouvés utilisés en C ++ et dans d'autres langages..
tSingleton
: Implémente une classe singleton en utilisant un motif "Meyers Singleton". Il est basé sur des modèles, et extensible, afin que nous puissions abstraire tout le code singleton en une seule classe.optionnel
: Ceci est une fonctionnalité de C ++ 14 (appelée std :: optionnel
) qui n’est pas encore tout à fait disponible dans les versions actuelles de C ++ (nous sommes toujours à C ++ 11). C'est également une fonctionnalité disponible dans XNA et C # (où elle s'appelle Nullable
.) Cela nous permet d’avoir des paramètres "optionnels" pour les méthodes. Il est utilisé dans le tSpriteBatch
classe.Puisque nous n'utilisons pas une structure de jeu existante, nous aurons besoin de quelques classes pour traiter des mathématiques en coulisses..
tMath
: Une classe statique qui fournit certaines méthodes allant au-delà de ce qui est disponible en C ++, telles que la conversion de degrés en radians ou d'arrondir des nombres en puissances de deux.tVecteur
: Ensemble de base de classes Vector, fournissant des variantes à 2, 3 et 4 éléments. Nous avons également typé cette structure pour les points et les couleurs.tMatrix
: Deux définitions de matrice, une variante 2x2 (pour les opérations de rotation) et une option 4x4 (pour la matrice de projection requise pour afficher les éléments à l'écran),tRect
: Une classe de rectangle fournissant l'emplacement, la taille et une méthode pour déterminer si les points se trouvent ou non dans des rectangles.Bien que OpenGL soit une API puissante, elle est basée sur le C et la gestion des objets peut être quelque peu difficile à faire en pratique. Donc, nous aurons une petite poignée de classes pour gérer les objets OpenGL pour nous.
tSurface
: Offre un moyen de créer un bitmap basé sur une image chargée à partir de l'ensemble de l'application.texture
: Enveloppe l'interface aux commandes de texture d'OpenGL et charge tSurfaces
en textures.t Shader
: Enveloppe l'interface du compilateur de shader d'OpenGL, facilitant la compilation des shaders.tProgramme
: Encapsule l'interface dans l'interface du programme shader d'OpenGL, qui est essentiellement la combinaison de deux t Shader
Des classes.Ces classes représentent ce qui se rapproche le plus d'un "framework de jeu"; ils fournissent des concepts de haut niveau qui ne sont pas typiques d'OpenGL, mais qui sont utiles pour le développement de jeux.
tViewport
: Contient l’état de la fenêtre. Nous l'utilisons principalement pour gérer les changements d'orientation des périphériques.tAutosizeViewport
: Une classe qui gère les modifications apportées à la fenêtre d'affichage. Il gère directement les changements d'orientation de l'appareil et adapte la fenêtre d'affichage à l'écran de l'appareil, de sorte que le rapport d'aspect reste le même, ce qui signifie que rien ne sera étiré ni écrasé..tSpriteFont
: Nous permet de charger une "police bitmap" à partir du lot d’applications et de l’utiliser pour écrire du texte à l’écran.tSpriteBatch
: Inspiré par XNA SpriteBatch
classe, j’ai écrit cette classe pour résumer le meilleur de ce dont notre jeu a besoin. Cela nous permet de trier les sprites lors des dessins de manière à obtenir les meilleurs gains de vitesse possibles sur le matériel dont nous disposons. Nous allons également l'utiliser directement pour écrire du texte à l'écran.Un ensemble minimal de cours pour compléter les choses.
tTimer
: Une minuterie système, utilisée principalement pour les animations.tInputEvent
: Définitions de classe de base pour fournir des changements d'orientation (inclinaison du périphérique), des événements tactiles et un événement de "clavier virtuel" pour émuler une manette de jeu plus discrètement.tSon
: Une classe dédiée au chargement et à la lecture des effets sonores et de la musique.Nous aurons également besoin de ce que j'appelle le code "Boostrap", c'est-à-dire un code qui résume comment une application démarre, ou "démarre".
Voici ce qu'il y a dedans Bootstrap
:
AppDéléguer
: Cette classe gère le lancement de l'application, ainsi que les événements de suspension et de reprise lorsque l'utilisateur appuie sur le bouton Accueil.ViewController
: Cette classe gère les événements d'orientation de périphérique et crée notre vue OpenGLOpenGLView
: Cette classe initialise OpenGL, demande au périphérique d'actualiser à 60 images par seconde et gère les événements tactiles.Dans ce tutoriel, nous allons créer un jeu de tir double-stick; le joueur contrôlera le navire à l'aide de commandes tactiles multiples à l'écran.
Nous allons utiliser un certain nombre de classes pour accomplir ceci:
Entité
: La classe de base pour les ennemis, les balles et le vaisseau du joueur. Les entités peuvent se déplacer et être dessinées.Balle
et PlayerShip
.EntityManager
: Garde la trace de toutes les entités du jeu et effectue la détection de collision.Contribution
: Aide à gérer les entrées depuis l'écran tactile.Art
: Charge et conserve des références aux textures nécessaires au jeu.Du son
: Charge et conserve des références aux sons et à la musique.MathUtil
et Les extensions
: Contient des méthodes statiques utiles etGameRoot
: Contrôle la boucle principale du jeu. C'est notre classe principale.Le code de ce tutoriel se veut simple et facile à comprendre. Toutes les fonctionnalités ne seront pas conçues pour répondre à tous les besoins possibles. au contraire, il ne fera que ce qu'il doit faire. En gardant les choses simples, il vous sera plus facile de comprendre les concepts, puis de les modifier et de les développer dans votre propre jeu unique..
Ouvrez le projet Xcode existant. GameRoot est la classe principale de notre application.
Nous allons commencer par créer une classe de base pour nos entités de jeu. Regardez le
classe Entity public: enum Kind kDontCare = 0, kBullet, kEnemy, kBlackHole,; protected: tTexture * mImage; tColor4f mColor; tPoint2f mPosition; tVector2f mVelocity; flottement mOrientation; float mRadius; bool mIsExpired; Genre gentil; entité publique(); Entité virtuelle ~ (); tDimension2f getSize () const; mise à jour virtuelle du vide () = 0; tirage au sort virtuel (tSpriteBatch * spriteBatch); tPoint2f getPosition () const; tVector2f getVelocity () const; void setVelocity (const tVector2f & nv); float getRadius () const; bool isExpired () const; Kind getKind () const; void setExpired (); ;
Toutes nos entités (ennemis, balles et le vaisseau du joueur) ont certaines propriétés de base, telles qu'une image et une position. mIsExpired
sera utilisé pour indiquer que l'entité a été détruite et devrait être supprimé de toute liste contenant une référence à celle-ci.
Ensuite, nous créons un EntityManager
suivre nos entités et les mettre à jour et les dessiner:
classe EntityManager: tSingleton publicprotected: std :: list Mentités; std :: list mAddedEntities; std :: list mBullets; bool mIsUpdating; protected: EntityManager (); public: int getCount () const; void add (Entity * entity); void addEntity (entité * entité); void update (); vide draw (tSpriteBatch * spriteBatch); bool isColliding (Entity * a, Entity * b); classe d'amis tSingleton ; ; void EntityManager :: add (entité * entité) if (! mIsUpdating) addEntity (entité); else mAddedEntities.push_back (entité); void EntityManager :: update () mIsUpdating = true; pour (std :: list :: iterator iter = mEntities.begin (); iter! = mEntities.end (); iter ++) (* iter) -> update (); if ((* iter) -> isExpired ()) * iter = NULL; mIsUpdating = false; pour (std :: list :: iterator iter = mAddedEntities.begin (); iter! = mAddedEntities.end (); iter ++) addEntity (* iter); mAddedEntities.clear (); mEntities.remove (NULL); pour (std :: list :: iterator iter = mBullets.begin (); iter! = mBullets.end (); iter ++) if ((* iter) -> isExpired ()) delete * iter; * iter = NULL; mBullets.remove (NULL); void EntityManager :: draw (tSpriteBatch * spriteBatch) pour (std :: list :: iterator iter = mEntities.begin (); iter! = mEntities.end (); iter ++) (* iter) -> draw (spriteBatch);
Rappelez-vous que si vous modifiez une liste en effectuant une itération dessus, vous obtiendrez une exception d'exécution. Le code ci-dessus s’occupe de cela en mettant en file d'attente toutes les entités ajoutées lors de la mise à jour dans une liste séparée et en les ajoutant une fois la mise à jour terminée.
Nous devrons charger des textures si nous voulons dessiner quelque chose, nous allons donc créer une classe statique pour contenir des références à toutes nos textures:
classe Art: tSingleton publicprotected: tTexture * mPlayer; tTexture * mSeeker; tTexture * mWanderer; tTexture * mBullet; tTexture * mPointer; protégé: Art (); public: tTexture * getPlayer () const; tTexture * getSeeker () const; tTexture * getWanderer () const; tTexture * getBullet () const; tTexture * getPointer () const; classe d'amis tSingleton ; ; Art :: Art () mPlayer = new tTexture (tSurface ("player.png")); mSeeker = new tTexture (tSurface ("seeker.png")); mWanderer = new tTexture (tSurface ("wanderer.png")); mBullet = new tTexture (tSurface ("bullet.png")); mPointer = new tTexture (tSurface ("pointer.png"));
Nous chargeons l'art en appelant Art :: getInstance ()
dans GameRoot :: onInitView ()
. Cela provoque la Art
singleton pour être construit et appeler le constructeur, Art :: Art ()
.
De plus, un certain nombre de classes devront connaître les dimensions de l’écran. Nous avons donc les membres suivants dans GameRoot
:
tDimension2f mViewportSize; tSpriteBatch * mSpriteBatch; tAutosizeViewport * mViewport;
Et dans le GameRoot
constructeur, nous définissons la taille:
GameRoot :: GameRoot (): mViewportSize (800, 600), mSpriteBatch (NULL)
La résolution 800x600px est celle utilisée par le Shape Blaster original basé sur XNA. Nous pourrions utiliser la résolution de notre choix (par exemple, une résolution plus proche de la résolution spécifique d'un iPhone ou d'un iPad), mais nous nous en tiendrons à la résolution d'origine simplement pour nous assurer que notre jeu correspond à l'aspect et à la convivialité de l'original..
Maintenant, nous allons passer en revue le PlayerShip
classe:
class PlayerShip: entité publique, tsingleton publicprotected: static const int kCooldownFrames; int mCooldowmRemaining; int mFramesUntilRespawn; protected: PlayerShip (); public: void update (); vide draw (tSpriteBatch * spriteBatch); bool getIsDead (); kill kill (); classe d'amis tSingleton ; ; PlayerShip :: PlayerShip (): mCooldowmRemaining (0), mFramesUntilRespawn (0) mImage = Art :: getInstance () -> getPlayer (); mPosition = tPoint2f (GameRoot :: getInstance () -> getViewportSize (). x / 2, GameRoot :: getInstance () -> getViewportSize (). y / 2); mRadius = 10;
Nous avons fait PlayerShip
un singleton, définissez son image et placez-la au centre de l'écran.
Enfin, ajoutons le vaisseau du joueur à la EntityManager
. Le code en GameRoot :: onInitView
ressemble à ça:
// Dans GameRoot :: onInitView EntityManager :: getInstance () -> add (PlayerShip :: getInstance ());… glClearColor (0,0,0,1); glEnable (GL_BLEND); glBlendFunc (GL_SRC_ALPHA, GL_ONE); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glHint (GL_GENERATE_MIPMAP_HINT, GL_DONT_CARE); glDisable (GL_DEPTH_TEST); glDisable (GL_CULL_FACE);
Nous dessinons les sprites avec mélange additif, ce qui fait partie de ce qui leur donnera leur look "néon". Nous ne voulons pas non plus de flou ou de mélange, nous utilisons donc GL_NEAREST
pour nos filtres. Nous n’avons pas besoin des tests de profondeur ni de l’abattage de la face arrière (cela ne fait qu’ajouter une surcharge inutile de toute façon), nous le désactivons donc..
Le code en GameRoot :: onRedrawView
ressemble à ça:
// Dans GameRoot :: onRedrawView EntityManager :: getInstance () -> update (); EntityManager :: getInstance () -> draw (mSpriteBatch); mSpriteBatch-> draw (0, Art :: getInstance () -> getPointer (), Input :: getInstance () -> getMousePosition (), tOptional()); mViewport-> run (); glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); mSpriteBatch-> end (); glFlush ();
Si vous lancez le jeu à ce stade, vous devriez voir votre vaisseau au centre de l'écran. Cependant, il ne répond pas aux entrées. Ajoutons quelques entrées au jeu suivant.
Pour le mouvement, nous utiliserons une interface multi-touch. Avant de nous lancer en force avec les manettes de jeu à l'écran, nous allons simplement avoir une interface tactile de base opérationnelle.
Dans l’original Shape Blaster pour Windows, le déplacement du lecteur pouvait être effectué à l’aide des touches WASD du clavier. Pour viser, ils pourraient utiliser les touches fléchées ou la souris. Ceci est censé imiter les commandes à deux manettes de Geometry Wars: une manette analogique pour le mouvement, une pour la visée.
Etant donné que Shape Blaster utilise déjà le concept de déplacement du clavier et de la souris, le moyen le plus simple d’ajouter une entrée consiste à émuler les commandes du clavier et de la souris par le toucher. Nous allons commencer par les mouvements de la souris, car Touch et souris partagent un composant similaire: un point contenant les coordonnées X et Y.
Nous allons créer une classe statique pour garder une trace des différents périphériques d’entrée et pour basculer entre les différents types de visée:
classe Input: tSingleton public protected: tPoint2f mMouseState; tPoint2f mLastMouseState; tPoint2f mFreshMouseState; std :: vectormKeyboardState; std :: vector mLastKeyboardState; std :: vector mFreshKeyboardState; bool mIsAimingWithMouse; uint8_t mLeftEngaged; uint8_t mRightEngaged; public: enum KeyType kUp = 0, kLeft, kDown, kRight, kW, kA, kS, kD,; protected: tVector2f GetMouseAimDirection () const; protégé: entrée (); public: tPoint2f getMousePosition () const; void update (); // Vérifie si une touche vient d'être pressée bool wasKeyPressed (KeyType) const; tVector2f getMovementDirection () const; tVector2f getAimDirection () const; void onKeyboard (const tKeyboardEvent & msg); void onTouch (const tTouchEvent & msg); classe d'amis tSingleton; ; void Input :: update () mLastKeyboardState = mKeyboardState; mLastMouseState = mMouseState; mKeyboardState = mFreshKeyboardState; mMouseState = mFreshMouseState; if (mKeyboardState [kLeft] || mKeyboardState [kRight] || mKeyboardState [kUp] || mKeyboardState [kDown]) mIsAimingWithMouse = false; else if (mMouseState! = mLastMouseState) mIsAimingWithMouse = true;
Nous appelons Entrée :: update ()
au début de GameRoot :: onRedrawView ()
pour que la classe d'entrée fonctionne.
Comme indiqué précédemment, nous utiliserons le clavier
indiquer plus tard dans la série pour rendre compte du mouvement.
Maintenant faisons le bateau tirer.
Tout d'abord, nous avons besoin d'une classe pour les balles.
class Bullet: public Entité public: Bullet (const tPoint2f & position, const tVector2f & vélocité); void update (); ; Bullet :: Bullet (const tPoint2f & position, const tVector2f & vélocité) mImage = Art :: getInstance () -> getBullet (); mPosition = position; mVelocity = vélocité; mOrientation = atan2f (mVelocity.y, mVelocity.x); mRadius = 8; mKind = kBullet; void Bullet :: update () if (mVelocity.lengthSquared ()> 0) mOrientation = atan2f (mVelocity.y, mVelocity.x); mPosition + = mVelocity; if (! tRectf (0, 0, GameRoot :: getInstance () -> getViewportSize ()).) contient (tPoint2f ((int32_t) mPosition.x, (int32_t) mPosition.y))) mIsExpired = true;
Nous voulons un bref délai de récupération entre les balles, nous aurons donc une constante pour cela:
const int PlayerShip :: kCooldownFrames = 6;
De plus, nous ajouterons le code suivant à PlayerShip :: Mise à jour ()
:
tVector2f objectif = Entrée :: getInstance () -> getAimDirection (); if (aim.lengthSquared ()> 0 && mCooldowmRemaining <= 0) mCooldowmRemaining = kCooldownFrames; float aimAngle = atan2f(aim.y, aim.x); float cosA = cosf(aimAngle); float sinA = sinf(aimAngle); tMatrix2x2f aimMat(tVector2f(cosA, sinA), tVector2f(-sinA, cosA)); float randomSpread = tMath::random() * 0.08f + tMath::random() * 0.08f - 0.08f; tVector2f vel = 11.0f * (tVector2f(cosA, sinA) + tVector2f(randomSpread, randomSpread)); tVector2f offset = aimMat * tVector2f(35, -8); EntityManager::getInstance()->add (new Bullet (mPosition + offset, vel)); offset = aimMat * tVector2f (35, 8); EntityManager :: getInstance () -> add (new Bullet (mPosition + offset, vel)); tSound * curShot = Sound :: getInstance () -> getShot (); if (! curShot-> isPlaying ()) curShot-> play (0, 1); if (mCooldowmRemaining> 0) mCooldowmRemaining--;
Ce code crée deux puces qui voyagent en parallèle. Cela ajoute un peu de hasard à la direction, ce qui rend les coups étalés un peu comme une mitrailleuse. Nous additionnons deux nombres aléatoires, ce qui rend leur somme plus susceptible d'être centrée (autour de zéro) et moins susceptible d'envoyer des puces au loin. Nous utilisons une matrice à deux dimensions pour faire pivoter la position initiale des balles dans la direction où elles se déplacent..
Nous avons également utilisé deux nouvelles méthodes d'assistance:
Extensions :: NextFloat ()
: Retourne un nombre aléatoire entre une valeur minimale et une valeur maximale.MathUtil :: FromPolar ()
: Crée un tVector2f
d'un angle et d'une magnitude.Voyons donc à quoi ils ressemblent:
// Dans les extensions float Extensions :: nextFloat (float minValue, float maxValue) return (float) tMath :: random () * (maxValue - minValue) + minValue; // Dans MathUtil tVector2f MathUtil :: fromPolar (angle de flottant, magnitude du flottant) grandeur de retour * tVector2f ((float) cosf (angle), (float) sinf (angle));
Il y a encore une chose que nous devrions faire maintenant que nous avons l'inital Contribution
classe: dessinons un curseur de souris personnalisé pour mieux voir où le navire vise. Dans GameRoot.Draw
, dessine simplement Art mPointer
à la position de la souris.
mSpriteBatch-> draw (0, Art :: getInstance () -> getPointer (), Input :: getInstance () -> getMousePosition (), tOptional());
Si vous testez le jeu maintenant, vous pourrez toucher n'importe où sur l'écran pour viser le flot continu de balles, ce qui est un bon début.
Dans la partie suivante, nous compléterons le jeu initial en ajoutant des ennemis et un score..