Dans le tutoriel précédent, nous avons créé un système Entity basé sur des composants nus. Maintenant, nous allons utiliser ce système pour créer un jeu simple Asteroids.
Voici le jeu simple Asteroids que nous allons créer dans ce tutoriel. Il est écrit avec Flash et AS3, mais les concepts généraux s’appliquent à la plupart des langages..
Le code source complet est disponible sur GitHub.
Il y a six classes:
AsteroidsGame
, qui étend la classe de jeu de base et ajoute la logique spécifique à notre shoot-'em-up de l'espace.Navire
, quelle est la chose que vous contrôlez.Astéroïde
, qui est la chose que vous tirez sur.Balle
, quelle est la chose que vous tirez.Pistolet
, qui crée ces balles.Ennemi
, qui est un étranger errant qui est juste là pour ajouter un peu de variété au jeu.Passons en revue ces types d'entités un par un.
Navire
ClasseNous allons commencer avec le vaisseau du joueur:
paquet astéroïdes import com.iainlobb.gamepad.Gamepad; import com.iainlobb.gamepad.KeyCode; moteur d'importation. import engine.Entity; import engine.Game; moteur d'importation. Santé; moteur d'importation. Physique; import engine.View; import flash.display.GraphicsPathWinding; import flash.display.Sprite; / ** *… * @auteur Iain Lobb - [email protected] * / public class Le vaisseau s'étend à Entity protected var gamepad: Gamepad; fonction publique Ship () body = new Body (this); body.x = 400; body.y = 300; physique = nouvelle physique (this); physics.drag = 0,9; view = new Voir (this); view.sprite = new Sprite (); view.sprite.graphics.lineStyle (1.5, 0xFFFFFF); view.sprite.graphics.drawPath (vecteur.([1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]), vecteur. ([-7,3, 10,3, -5,5, 10,3, -7, 0,6, -0,5, -2,8, 6,2, 0,3, 4,5, 10,3, 6,3, 10,3, 11,1, -1,4, -0,2, -9,6, -11,9, - 1.3, -7.3, 10.3]), GraphicsPathWinding.NON_ZERO); santé = nouvelle santé (this); santé.hits = 5; health.died.add (onDied); arme = nouvelle arme à feu (cette); gamepad = new Gamepad (Game.stage, false); gamepad.fire1.mapKey (KeyCode.SPACEBAR); écrase la fonction publique update (): void super.update (); body.angle + = gamepad.x * 0.1; physics.thrust (-gamepad.y); if (gamepad.fire1.isPressed) arme.fire (); fonction protégée onDied (entité: entité): void destroy ();
Il y a pas mal de détails d'implémentation ici, mais l'essentiel à noter est que dans le constructeur, nous instancions et configurons Corps
, La physique
, Santé
, Vue
et Arme
Composants. (Le Arme
composant est en fait une instance de Pistolet
plutôt que la classe de base des armes.)
J'utilise les API de dessin graphique Flash pour créer mon navire (lignes 29 à 32), mais nous pourrions tout aussi facilement utiliser une image bitmap. Je crée également une instance de ma classe Gamepad. Il s'agit d'une bibliothèque open source que j'ai écrite il y a quelques années pour faciliter la saisie au clavier dans Flash..
J'ai aussi remplacé le mettre à jour
fonction de la classe de base pour ajouter un comportement personnalisé: après avoir déclenché tout le comportement par défaut avec super.update ()
nous tournons et poussons le vaisseau en fonction de la saisie au clavier et tirons avec l'arme si la touche de tir est enfoncée.
En écoutant le décédés
Signal de la composante santé, nous déclenchons la onDied
fonctionner si le joueur manque de points de vie. Lorsque cela se produit, nous disons simplement au navire de se détruire.
Pistolet
ClasseEnsuite, allumons ça Pistolet
classe:
paquet astéroïdes import engine.Entity; moteur d'importation.Arme; / ** *… * @auteur Iain Lobb - [email protected] * / public class Gun étend Arme fonction publique Gun (entité: Entity) super (entité); écrase la fonction publique fire (): void var bullet: Bullet = new Bullet (); bullet.targets = entity.targets; bullet.body.x = entity.body.x; bullet.body.y = entity.body.y; bullet.body.angle = entity.body.angle; bullet.physics.thrust (10); entity.entityCreated.dispatch (bullet); super feu ();
Ceci est une belle courte! Nous annulons simplement la Feu()
fonction pour créer un nouveau Balle
chaque fois que le joueur tire. Après avoir adapté la position et la rotation de la balle au navire et l'avoir poussée dans la bonne direction, nous expédions entité créée
afin qu'il puisse être ajouté au jeu.
Une bonne chose à ce sujet Pistolet
la classe est qu'il est utilisé à la fois par le joueur et les navires ennemis.
Balle
ClasseUNE Pistolet
crée une instance de cette Balle
classe:
package astéroïdes import engine.Body; import engine.Entity; moteur d'importation. Physique; import engine.View; import flash.display.Sprite; / ** *… * @author Iain Lobb - [email protected] * / public class Bullet étend l'entité public var age: int; fonction publique Bullet () body = new Body (this); body.radius = 5; physique = nouvelle physique (this); view = new Voir (this); view.sprite = new Sprite (); view.sprite.graphics.beginFill (0xFFFFFF); view.sprite.graphics.drawCircle (0, 0, body.radius); écraser la fonction publique update (): void super.update (); pour chaque (var cible: entité dans les cibles) if (body.testCollision (cible)) target.health.hit (1); détruire(); revenir; age ++; si (âge> 20) view.alpha - = 0,2; si (âge> 25 ans) détruit ();
Le constructeur instancie et configure le corps, la physique et la vue. Dans la fonction de mise à jour, vous pouvez maintenant voir la liste appelée des cibles
soyez pratique, alors que nous parcourons toutes les choses que nous voulons frapper et voyons s’il en est qui croisent la balle.
Ce système de collision ne peut atteindre des milliers de balles, mais convient pour la plupart des jeux occasionnels..
Si la balle a plus de 20 images, nous commençons à la faire disparaître, et si elle a plus de 25 images, nous la détruisons. Comme avec le Pistolet
, la Balle
est utilisé à la fois par le joueur et par l'ennemi - les instances ont simplement une liste de cibles différente.
En parlant de ça…
Ennemi
ClasseRegardons maintenant ce vaisseau ennemi:
package astéroïdes import engine.Body; import engine.Entity; moteur d'importation. Santé; moteur d'importation. Physique; import engine.View; import flash.display.GraphicsPathWinding; import flash.display.Sprite; / ** *… * @author Iain Lobb - [email protected] * / public class EnemyShip étend l'entité protected var turnDirection: Number = 1; fonction publique EnemyShip () body = new Body (this); body.x = 750; body.y = 550; physique = nouvelle physique (this); physics.drag = 0,9; view = new Voir (this); view.sprite = new Sprite (); view.sprite.graphics.lineStyle (1.5, 0xFFFFFF); view.sprite.graphics.drawPath (vecteur.([1, 2, 2, 2, 2]), vecteur. ([0, 10, 10, -10, 0, 0, -10, -10, 0, 10]), GraphicsPathWinding.NON_ZERO); santé = nouvelle santé (this); santé.hits = 5; health.died.add (onDied); arme = nouvelle arme à feu (cette); écrase la fonction publique update (): void super.update (); si (Math.random () < 0.1) turnDirection = -turnDirection; body.angle += turnDirection * 0.1; physics.thrust(Math.random()); if (Math.random() < 0.05) weapon.fire(); protected function onDied(entity:Entity):void destroy();
Comme vous pouvez le constater, il est assez similaire à la classe des joueurs. La seule vraie différence est que dans le mettre à jour()
fonction, plutôt que d'avoir le contrôle du joueur via le clavier, nous avons une "stupidité artificielle" pour rendre le vaisseau errer et tirer au hasard.
Astéroïde
ClasseL'autre type d'entité sur lequel le joueur peut tirer est l'astéroïde lui-même:
package astéroïdes import engine.Body; import engine.Entity; moteur d'importation. Santé; moteur d'importation. Physique; import engine.View; import flash.display.Sprite; / ** *… * @author Iain Lobb - [email protected] * / public class Asteroid étend l'entité fonction publique Asteroid () body = new Body (this); body.radius = 20; body.x = Math.random () * 800; body.y = Math.random () * 600; physique = nouvelle physique (this); physics.velocityX = (Math.random () * 10) - 5; physics.velocityY = (Math.random () * 10) - 5; view = new Voir (this); view.sprite = new Sprite (); view.sprite.graphics.lineStyle (1.5, 0xFFFFFF); view.sprite.graphics.drawCircle (0, 0, body.radius); santé = nouvelle santé (this); health.hits = 3; health.hurt.add (onHurt); écrase la fonction publique update (): void super.update (); pour chaque (var cible: entité dans les cibles) if (body.testCollision (cible)) target.health.hit (1); détruire(); revenir; fonction protégée onHurt (entité: entité): void body.radius * = 0.75; view.scale * = 0.75; si (body.radius) < 10) destroy(); return; var asteroid:Asteroid = new Asteroid(); asteroid.targets = targets; group.push(asteroid); asteroid.group = group; asteroid.body.x = body.x; asteroid.body.y = body.y; asteroid.body.radius = body.radius; asteroid.view.scale = view.scale; entityCreated.dispatch(asteroid);
J'espère que vous vous habituez à l'apparence de ces classes d'entités.
Dans le constructeur, nous initialisons nos composants et randomisons la position et la vitesse.
dans le mettre à jour()
fonction nous vérifions les collisions avec notre liste de cibles - qui dans cet exemple n'aura qu'un seul élément - le vaisseau du joueur. Si nous trouvons une collision, nous endommageons la cible puis détruisons l'astéroïde. Par contre, si l’astéroïde est lui-même endommagé (c’est-à-dire qu’il est touché par une balle du joueur), nous le rétrécissons et créons un deuxième astéroïde, créant l’illusion qu’il a été détruit en deux. Nous savons quand faire cela en écoutant le signal "blessé" du composant Santé..
AsteroidsGame
ClasseEnfin, regardons la classe AsteroidsGame qui contrôle l’ensemble du spectacle:
paquet astéroïdes import engine.Entity; import engine.Game; import flash.events.MouseEvent; import flash.filters.GlowFilter; import flash.text.TextField; / ** *… * @auteur Iain Lobb - [email protected] * / classe publique AsteroidsGame étend le jeu public var players: Vector.= nouveau vecteur. (); ennemis publics var: vecteur. = nouveau vecteur. (); public var messageField: TextField; fonction publique AsteroidsGame () substitue la fonction protégée startGame (): void var asteroid: Asteroid; pour (var i: int = 0; i < 10; i++) asteroid = new Asteroid(); asteroid.targets = players; asteroid.group = enemies; enemies.push(asteroid); addEntity(asteroid); var ship:Ship = new Ship(); ship.targets = enemies; ship.destroyed.add(onPlayerDestroyed); players.push(ship); addEntity(ship); var enemyShip:EnemyShip = new EnemyShip(); enemyShip.targets = players; enemyShip.group = enemies; enemies.push(enemyShip); addEntity(enemyShip); filters = [new GlowFilter(0xFFFFFF, 0.8, 6, 6, 1)]; update(); render(); isPaused = true; if (messageField) addChild(messageField); else createMessage(); stage.addEventListener(MouseEvent.MOUSE_DOWN, start); protected function createMessage():void messageField = new TextField(); messageField.selectable = false; messageField.textColor = 0xFFFFFF; messageField.width = 600; messageField.scaleX = 2; messageField.scaleY = 3; messageField.text = "CLICK TO START"; messageField.x = 400 - messageField.textWidth; messageField.y = 240; addChild(messageField); protected function start(event:MouseEvent):void stage.removeEventListener(MouseEvent.MOUSE_DOWN, start); isPaused = false; removeChild(messageField); stage.focus = stage; protected function onPlayerDestroyed(entity:Entity):void gameOver(); protected function gameOver():void addChild(messageField); isPaused = true; stage.addEventListener(MouseEvent.MOUSE_DOWN, restart); protected function restart(event:MouseEvent):void stopGame(); startGame(); stage.removeEventListener(MouseEvent.MOUSE_DOWN, restart); isPaused = false; removeChild(messageField); stage.focus = stage; override protected function stopGame():void super.stopGame(); players.length = 0; enemies.length = 0; override protected function update():void super.update(); for each (var entity:Entity in entities) if (entity.body.x > 850) entity.body.x - = 900; if (entity.body.x < -50) entity.body.x += 900; if (entity.body.y > 650) entity.body.y - = 700; si (entity.body.y < -50) entity.body.y += 700; if (enemies.length == 0) gameOver();
Cette classe est assez longue (enfin, plus de 100 lignes!) Car elle fait beaucoup de choses.
Dans démarrer jeu()
il crée et configure 10 astéroïdes, le navire et le navire ennemi, ainsi que le message "CLIQUEZ POUR COMMENCER".
le début()
la fonction met le jeu en pause et supprime le message, tandis que le jeu terminé
fonction met à nouveau le jeu en pause et restaure le message. le redémarrer()
la fonction écoute un clic de souris sur l'écran Game Over - quand cela se produit, le jeu est arrêté et redémarré.
le mettre à jour()
la fonction parcourt tous les ennemis et déforme tous ceux qui se sont éloignés de l'écran, tout en vérifiant la condition de victoire, à savoir qu'il ne reste plus aucun ennemi dans la liste des ennemis.
Il s’agit d’un moteur simple et d’un jeu simple, réfléchissons donc à la manière de l’élargir..
En plus d’étendre les composants individuels, nous pourrions parfois avoir besoin d’étendre la IEntity
interface pour créer des types spéciaux d'entité avec des composants spécialisés.
Par exemple, si nous créons un jeu de plate-forme et que nous avons un nouveau composant qui gère toutes les choses très spécifiques dont un personnage de jeu de plate-forme a besoin - sont-ils sur le terrain, touchent-ils un mur, depuis combien de temps en l'air, peuvent-ils faire un double saut, etc. - d'autres entités pourraient également avoir besoin d'accéder à ces informations. Mais cela ne fait pas partie de l'API d'entité principale, qui est intentionnellement très générale. Nous devons donc définir une nouvelle interface, qui donne accès à tous les composants d’entité standard, mais ajoute un accès à la PlatformController
composant.
Pour cela, nous ferions quelque chose comme:
package platformgame import engine.IEntity; / ** *… * @author Iain Lobb - [email protected] * / interface publique IPlatformEntity étend IEntity ensemble de fonctions platformController (valeur: PlatformController): void; function obtenir platformController (): PlatformController;
Toute entité ayant besoin de la fonctionnalité "plateforme" implémente ensuite cette interface, permettant à d’autres entités d’interagir avec le PlatformController
composant.
En osant même écrire sur l’architecture de jeu, j’ai peur d’agiter un nid d’opinion, mais c’est toujours une bonne chose, et j’espère au moins que je vous ai fait réfléchir à la façon dont vous organisez votre code.
En fin de compte, je ne crois pas que vous devriez vous attarder à la façon dont vous structurez les choses; peu importe ce qui fonctionne pour que votre jeu soit fait est la meilleure stratégie. Je sais qu'il existe des systèmes beaucoup plus avancés que celui que je décris ici, qui résolvent une gamme de problèmes autres que ceux dont j'ai parlé, mais ils peuvent avoir tendance à sembler très inconnus si vous êtes habitué à une architecture traditionnelle basée sur l'héritage..
J'aime l'approche que j'ai suggérée ici car elle permet d'organiser le code par objectif, en petites classes focalisées, tout en fournissant une interface extensible à typage statique et sans s'appuyer sur des fonctionnalités de langage dynamiques. Chaîne
recherches. Si vous souhaitez modifier le comportement d'un composant particulier, vous pouvez étendre ce composant et remplacer les méthodes que vous souhaitez modifier. Les classes ont tendance à rester très courtes, donc je ne me retrouve jamais à faire défiler des milliers de lignes pour trouver le code que je cherche.
Mieux encore, je peux utiliser un seul moteur suffisamment souple pour être utilisé dans tous les jeux que je crée, ce qui me fait gagner un temps considérable..