La deuxième étape !

title png
Le 20 mai 2011

Apprendre AS 3.0 en développant un jeu. Partie 2: Ennemis multiples

Introduction

Dans cette seconde partie de la traduction du tutoriel de Michael James Williams, nous allons apprendre comment ajouter de nombreux ennemis à notre jeu.

Préparation

Ouvrons le fichier FLA y les fichiers AS Enemy.as y AvoiderGame.as car nous allons devoir les éditer.

Prenons le contrôle de l'ennemi

Jetons un coup d'oeil sur la fonction constructeur de la classe Enemy:

public function Enemy()
        {
                x = 100;
                y = -40;
        }

Ce petit code signifie que chacune des instances de la classe Enemy va apparaître en X=100 et en Y=-40. Mais si nous gardons ceci tel quel, nous ne pourrons pas avoir un jeu intéressant. En effet, imaginez un instant la situation: vous allez devoir éviter un groupe d'ennemis qui apparaissent toujours au même endroit… ce n'est pas du tout attractif.
Aussi, nous allons changer la fonction constructeur de cette façon:

public function Enemy( startX:Number, startY:Number )
        {
                x = startX;
                y = startY;
        }

Ce que nous venons de faire c'est d'ajouter des paramètres à la fonction constructeur. Maintenant chaque fois que nous créerons une instance de la classe Enemy.as, nous allons devoir lui passer 2 paramètres pour indiquer à la fonction où elle doit créer notre ennemi.
Changeons la classe du document (AvoiderGame.as) et modifions le code qui crée un nouvel ennemi. Remplaçons la ligne

                enemy = new Enemy();

par celle-ci:

                 enemy = new Enemy( 100, -40 );

Si vous testez le jeu maintenant, vous allez voir que le jeu fonctionne comme avant, l'ennemi apparaît au même endroit, descend avec la même vitesse, etc. Alors, qu'est-ce que nous avons changé? Fondamentalement nous avons changé la manière de dire à Flash de faire quelque chose. Auparavant nous lui disions “Je veux qu'un ennemi apparaîsse dans un endroit donné”, maintenant nous lui disons “Quand je te dirai que j'ai besoin d'un ennemi sur la scène, je te dirai exactement où je le veux”. Et à quoi ça sert  de changer le code comme ça ? Ca sert surtout parce que, dans ce tutoriel, nous allons demander à Flash d'ajouter de nombreux ennemis, en différents endroits de l'écran.

Je vous propose d'essayer deux choses:

• Changez les valeurs entre parenthèses de la ligne que nous avons ajouté à la classe du document (AvoiderGame.as); essayez différentes valeurs pour voir comment la position initiale de l'ennemi change si nous modifions les valeurs.

• Essayez d'utiliser 1 seule valeur au lieu de 2 valeurs entre parenthèses.

Dans le premier cas, lorsque qu'on change les valeurs, notre ennemi change sa position initiale de descente. Si nous entrons (300,50), il apparaîtra en un lieu, et avec (150,200) dans un autre, différent.

Dans le second cas, nous allons voir que quand nous testons le jeu,  Flash nous signale une erreur “1136: Nombre d'arguments incorrects. 2 arguments sont attendus.” Ceci se produit parce que nous avons spécifié que la fonction constructeur de la classe Enemy devait recevoir 2 paramètres. S'il n'y en a qu'1, ou plus de 2, Flash génère une erreur au moment de la compilation, ce qui empêche que nous testions le jeu.


Clonage Irresponsable

Comment faire pour que plusieurs ennemis apparaissent sur l'écran?
Dans cette section (Clonage irresponsable), nous allons voir CE QUE NOUS NE DEVONS PAS FAIRE. Ne le faites pas dans votre fichier, c'est seulement pour vous montrer quelle est la forme la plus facile à utiliser mais qui ensuite vous amènerait plus de problèmes que n'importe quelle autre méthode, si vous l'utilisiez.

Ce que nous allons faire, c'est de copier littéralement le code que nous avons utilisé pour créer un ennemi et changer son nom pour créer autant d'ennemis que nous voulons.

D'abord nous ajoutons la variable de chaque ennemi dont nous allons avoir besoin:

public class AvoiderGame extends MovieClip
{
        public var enemy:Enemy;
        public var eric:Enemy;
        public var ernie:Enemy;
        public var emily:Enemy;
        public var avatar:Avatar;
      public var gameTimer:Timer;

A présent, nous indiquons au document de la classe où va démarrer chacun des ennemis:

public function AvoiderGame()
{
        enemy = new Enemy( 100, -40 );
        addChild( enemy );
        eric = new Enemy( 160, -120 );
        addChild( eric );
        ernie = new Enemy( 205, -60 );
        addChild( ernie );
        emily = new Enemy( 317, -85 );
        addChild( emily );
 
Ensuite nous modifions la fonction onTick pour que les ennemis se déplacent:

public function onTick( timerEvent:TimerEvent ):void
{
        enemy.moveDownABit();
        eric.moveDownABit();
        ernie.moveDownABit();
        emily.moveDownABit();

Enfin, nous vérifions pour chaque cas s'il y a collision contre l'avatar.

if ( avatar.hitTestObject( enemy ) )
{
        gameTimer.stop();
}                     
if ( avatar.hitTestObject( eric ) )
{
        gameTimer.stop();
}                     
if ( avatar.hitTestObject( ernie ) )
{
        gameTimer.stop();
}                     
if ( avatar.hitTestObject( emily ) )
{
        gameTimer.stop();
}

Maintenant testons le jeu. Apparemment le jeu fonctionne très bien, mais imaginez que nous désirions introduire 200 ennemis différents… le code va être très long et ce, sans raison.
Si vous avez essayé le code dans votre fichier, effacez tout le code que nous avons ajouté et continuons notre tutoriel.

Mettre une armée sur pied !

title png
Le code interdit ci-dessus avait l'avantage que chaque ennemi avait un nom spécifique. Mais … franchement, à quoi ça peut réellement nous servir? Avons-nous besoin, à un moment quelconque de faire référence à un ennemi en particulier pour qu'il agisse différemment des autres? Oui à la rigueur s'il arrive à un ennemi quelque chose de spécial qui n'arrive pas aux autres, par exemple s'il heurte un avatar ou s'il sort de l'écran, mais dans ces cas-là, nous pouvons interagir avec cet ennemi en particulier selon ce qu'il fait et non par son nom. Je vais tout de suite vous montrer ce que je veux dire.
Ce que nous allons faire c'est créer un groupe d'ennemis et utiliser le document de la classe comme un “Colonel” qui indique à l'armée ce qu'elle doit faire. Je vais appeler ce groupe army (armée en anglais). Nous allons utiliser un type de données appelé Array car ce type de données nous sert fondamentalement pour montrer une liste ordonnée d'éléments.
D'abord nous devons définir la variable army comme nous venons de définir le reste des variables. Comme elle doit être disponible pour tout le jeu, nous la définissons dans le document de la classe mais en dehors de la fonction contructeur:

public class AvoiderGame extends MovieClip
{
        public var army:Array;
        public var enemy:Enemy;
        public var avatar:Avatar;
        public var gameTimer:Timer;

Comme les ennemis vont être à l'intérieur de la variable army, il n'est plus nécessaire que la variable enemy soit "public", aussi nous allons l'effacer et nous allons modifier la fonction constructeur de cette façon:

public function AvoiderGame()
{
        army = new Array();
        var newEnemy = new Enemy( 100, -40 );
        army.push( newEnemy );
        addChild( newEnemy );

Qu'est-ce que nous venons de faire?

  • army = new Array(); - Là, nous définissons une nouvelle instance de type Array avec le nom de la variable que nous avons déclarée à l'extérieur de la fonction constructeur
  • var newEnemy = new Enemy (100,-40); - Pour ne pas nous  tromper, renommons enemy en newEnemy, puisque nous utiliserons cette variable pour créer les nouveaux ennemis. Comme nous n'avons déclaré aucune variable newEnemy (public ou private), nous devons la déclarer maintenant, en ajoutant var au début.
  • Army.push (newEnemy); - Là nous ajoutons l'instance créée dans  Array, appelée army (nous poussons cette instance dans Array, à la fin de la liste; rappelez-vous que le type de données Array est une liste ordonnée d'éléments.)
  • addChild (newEnemy); - Nous ajoutons newEnemy  sur la scène.

Maintenant nous devons faire en sorte que l'ennemi se déplace. Souvenez-vous, nous l'avons fait dans la fonction onTick. Nous allons avoir besoin d'une boucle appelée for each…in qui est une nouveauté en Actionscript 3.
Editez le code de la fonction onTick pour qu'il passe de:

public function onTick( timerEvent:TimerEvent ):void
{
        enemy.moveDownABit();
        avatar.x = mouseX;
        avatar.y = mouseY;

à:

public function onTick( timerEvent:TimerEvent ):void
{
        for each ( var enemy:Enemy in army )
        {
               enemy.moveDownABit();
        }
        avatar.x = mouseX;
        avatar.y = mouseY;

Ne prenez pas peur! Cette nouvelle fonction est assez simple, à dire vrai. La fonction for each…in parcourt tous les objets qui sont à l'intérieur de Array army et à chacun elle applique la fonction que nous avons mis entre parenthèses. For each signifie “pour chaque”.Nous disons donc à Flash “Pour chacune (for each) des variables enemy qui appartienne à la classe Enemy (var enemy:Enemy) et qui se trouve dans (in)  Array army, tu dois appeler la fonction enemy.moveDownABit ({enemy.moveDownABit();}).
Il ne nous reste plus qu'une chose à faire avant de tester le jeu:

vérifier les collisions.
Pour ce faire, nous devons déplacer le code qui détecte si l'avatar est en contact avec un ennemi, dans la fonction for each…in, pour vérifier s'il y a una collision avec chacun des ennemis séparément (cela va nous servir si plus tard nous voulons que certains ennemis aient une vie plus longue que d'autres). Donc, ajoutons ce code:

public function onTick( timerEvent:TimerEvent ):void
{
        for each ( var enemy:Enemy in army )
        {
               enemy.moveDownABit();
               if ( avatar.hitTestObject( enemy ) )
               {
                       gameTimer.stop();
               }
        }
        avatar.x = mouseX;
        avatar.y = mouseY;
}

Bien que le code soit correct, nous avons un petit problème … Vous voyez lequel? Eh bien, oui, nous sommes en train de vérifier si notre avatar est en contact avec l'ennemi avant d'actualiser la position de l'avatar. Vous pouvez le comprendre en regardant le code. Regardez, d'abord nous utilisons la fonction for each… in, et ensuite nous disons à l'avatar où est le X et le Y de la souris: donc nous voyons si l'ennemi était en collision et non s'il l'est. Pour modifier ça, nous n'avons qu'à intervertir l'ordre suivant lequel nous avons écrit le code:

public function onTick( timerEvent:TimerEvent ):void
{
        avatar.x = mouseX;
        avatar.y = mouseY;

        for each ( var enemy:Enemy in army )
        {
               enemy.moveDownABit();
               if ( avatar.hitTestObject( enemy ) )
               {
                       gameTimer.stop();
               }
        }
}

Là vous voyez bien, que d'abord nous actualisons le lieu où est l'avatar et ensuite nous regardons si dans ce lieu-là, il y a un ennemi qui est en collision avec l'avatar. Ce type d'erreurs est très  fréquent quand on commence à programmer. Il est important  de rappeler que Flash lit toujours le code de haut en bas: donc la première chose que nous écrirons va être la première chose qui arrivera (on peut sauter des lignes et des fonctions, mais à part ça Flash lit toujours de haut en bas).
Si vous testez le jeu maintenant, vous allez voir que tout marche comme avant.


Ajouter un élément à l'armée

Essayez ce qui suit:

public function onTick( timerEvent:TimerEvent ):void
        {
        var newEnemy:Enemy = new Enemy( 100, -15 );
        army.push( newEnemy );
        addChild( newEnemy );

Vous reconnaissez? C'est le code qui ajoute des ennemis dans la fonction onTick. Que fait-elle? Nous savons que la fonction onTick fonctionne 1 fois toutes les 25 millisecondes, donc ici, nous créons un nouvel ennemi à chaque Tick (impulsion).
Si nous testons le jeu, voilà ce que nous obtenons:

trainée

Hum! Grrr… Un ennemi est créé par impulsion au même endroit et si rapidement qu'ils se superposent tous les uns sur les autres. Essayons d'améliorer cela en permettant que les ennemis apparaissent chacun dans des lieux différents.
Pour cela, nous allons utiliser la fonction Math.Random(). Cette fonction génère un chiffre aléatoire entre 0 et 1. Comme notre scène a 400 px de large, nous allons lui indiquer de générer un chiffre aléatoire compris entre 0 et 400.

En voici le code:

public function onTick( timerEvent:TimerEvent ):void
{
        var randomX:Number = Math.random() * 400;
        var newEnemy:Enemy = new Enemy( randomX, -40 );
        army.push( newEnemy );
        addChild( newEnemy );

Maintenant nos ennemis vont être créés en un point X aléatoire, mais ils restent toujours au point -40 de l'axe des Y car nous voulons qu'ils apparaissent toujours au-dessus de la scène.
Regardez bien ce que nous sommes en train de faire:

D'abord, nous définissons une variable appelée randomX qui est du type de données Number (un type de données qui peut contenir des nombres, avec des virgules, des signes positifs, négatifs et des décimales).
Grâce au signe "égal", nous indiquons que le résultat de Math.Random() * 400 va être stocké dans la variable randomX. Ensuite nous passons cette valeur randomX comme paramètre, en créant un ennemi, ce qui veut dire que chaque fois que la fonction onTick s'active, nous allons avoir une valeur appartenant à x qui va  indiquer à l'ennemi où il doit démarrer.
Quand cette valeur s'activera à nouveau, la valeur X va être distincte et donc l'objet démarrera à un endroit différent.
Voyons voir ça:

multiples

Wow!!! Ok, mais il semblerait que nous donnions trop de place aux ennemis, aussi allons-nous réguler un peu la quantité d'ennemis de notre armée.


Moins d'ennemis par seconde, SVP!!!

Jusqu'à présent, il apparaîssait un ennemi par Tick. Comme chaque Tick se produit toutes les 25 millisecondes et que nous avons 40 ticks par seconde, nous arrivons à un total de 40 ennemis par seconde que nous devons esquiver. Je crois que le mieux serait qu'il y ait un maximum de 4 ennemis par secondes pour qu'il soit donné une chance de survivre à l'attaque. Comme 4 est le 1/10 de 40, nous devons réduire les ennemis par Tick au 1/10 de la valeur actuelle.
Nous pourrions trouver une formule pour que tous les 10 ticks apparaîsse un ennemi, mais cette solution est trop prévisible, il vaut mieux que nous créions une fonction qui donne 1 chance sur 10 de créer un ennemi, car ainsi parfois nous aurons plus de quatre ennemis par seconde et parfois moins, et cette option au moins donne la possibilité d'avoir une moyenne de 4 ennemis par seconde.

Maintenant que nous savons ce que nous voulons faire… comment allons-nous le faire? Nous savons que la fonction Math.Random() génère un nombre aléatoire entre 0 y 1. Si nous voulons que nos chances soient de 1/10, nous devrions créer une fonction qui n'ajoute un ennemi que si Math.Random est < 0.1, ( 0.1 es le 1/10 de 1).
Voyons-le dans le code et ainsi nous le comprendrons mieux:

public function onTick( timerEvent:TimerEvent ):void
{
        if ( Math.random() < 0.1 )
        {
               var randomX:Number = Math.random() * 400;
               var newEnemy:Enemy = new Enemy( randomX, -40 );
               army.push( newEnemy );
               addChild( newEnemy );
        }

Explication: toutes les fonctions Math.Random sont indépendantes entre elles, aussi les deux fonctions vont avoir un résultat différent. Le nouveau code ne va pas interférer sut la position X de l'ennemi.
Je vais un peu expliquer l'expression conditionnelle if.

On utilise cette fonction pour contrôler le flux du jeu de la manière suivante. D'abord on ajoute une condition entre parenthèses (dans notre cas Math.Random() est < 0.1, ce qui veut dire si el nombre généré est inférieur à  0.1) Si cette condition est vraie, le bloc de code entre accolades sera exécuté {}. Si la condition n'est pas vraie, le bloc complet sera ignoré.
En résumé, si la condition est vraie, le programme va ajouter un ennemi, sinon il ne fera rien. Plus tard nous verrons l'expression if…else qui permet d'exécuter un autre bloc de code si la condition n'est pas remplie.
Sauvegardez le jeu et testez-le. Vous devriez obtenir un resultat similaire à celui-ci:

plus leger

Rappelez-vous que chaque fois que vous démarrez le jeu, la position où apparaissent les ennemis va être aléatoire, c'est pour ça que le jeu sera parfois plus simple que d'autres fois.
Parfait!!! Que le jeu soit difficile ou très facile, ça dépend de la taille de l'avatar et de celle des ennemis. Essayez de changer la valeur de la déclaration conditionnelle if pour voir différents résultats et ajustez-les à la valeur qui fonctionne le mieux dans votre jeu.

Tout est une question de taille

Cette section n'est pas incluse dans le tutorial d'origine mais ça m'a paru intéressant de l'ajouter.
Imaginons que vous vouliez changer la taille de votre avatar et celle des ennemis. Vous pouvez bien sûr aller modifier le symbole de la bibliothèque et tout marchera très bien, mais comme nous apprenons Actionscript, nous allons le faire à travers le code.
Dans la fonction constructeur, cherchons le code qui inclut le Child de l'avatar et modifions ce qui suit :

                                         public function AvoiderGame()
                    {
                       army = new Array();
                      
                       avatar = new Avatar();
                       avatar.scaleX=0.7;
                       avatar.scaleY=0.7;
                       addChild( avatar );

Là, nous incluons deux nouvelles propriétés, scaleX et scaleY. Ces  propriétés nous permettent de contrôler la taille de notre symbole. La valeur 1 indique la taille d'origine, si nous lui assignons une autre valeur, l'échelle du symbole va changer. Je lui ai donné une valeur de 0.7 pour que l'avatar soit à 70% de sa taille d'origine, mais vous pouvez mettre la valeur qui vous paraîtra la plus adaptée à votre jeu.
Pour modifier la taille de l'ennemi, nous devons ajouter le même code mais en l'appliquant à l'instance newEnemy:

public function onTick( timerEvent:TimerEvent ):void
{
        if ( Math.random() < 0.1 )
        {
               var randomX:Number = Math.random() * 400;
               var newEnemy:Enemy = new Enemy( randomX, -15 );
               newEnemy.scaleX=0.7;
               newEnemy.scaleY=0.7;
               army.push( newEnemy );
               addChild( newEnemy );
        }

Vous avez vu que pour modifier una propriété de symbole, nous utilisons le point (.).
Si vous testez le jeu avec le nouveau code, vous allez voir qu'autant l'avatar que l'ennemi sont beaucoup plus petits.


Cette partie ne va pas trop être utile maintenant, mais plus tard, ça sera assez simple d'ajouter des power-ups qui agrandissent ou rapetissent notre personnage, puisqu'il suffira de modifier une de ses valeurs.

Je vous laisse quelques exercices qui vous permettront de pratiquer:
• [Facile] Pouvez-vous faire que les ennemis 'apparaissent  n'importe où sur l'axe Y? (vous savez comment le faire pour l'axe X, le procesus est le même)
• [Intermédiaire] Pouvez vous faire en sorte que les ennemis soient de tailles diverses? (Peut-être un petit Math.Random() pourra vous aider si vous l'utilisez avec l'échelle des ennemis.)
• [Difficile] Des ennemis apparaissent sur le bord du jeu, vous voyez la solution?


Conclusion

Nous avons terminé la deuxième partie du tutorial. 
La prochaine fois, nous allons ajouter un écran de Game Over pour avertir le joueur qu'il a perdu. Je crois que c'est plus agréable que de voir tout l'écran qui se fige sans qu'on puisse rien faire.
J'espère que tout va bien . Si vous avez des difficultés légères, écrivez-moi, si elles sont plus sévères, consultez le site de Michael ou écrivez-lui.

< Partie précédente    Partie suivante>