Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Space Invaders - Partie 1 - La base du moteur de jeu

Compatible Director MX2004. Cliquer pour en savoir plus sur les compatibilités.Par matse, le 13 octobre 2005

Ce tutorial en 2 parties s'adresse tout particulièrement à ceux qui débutent avec Director et Lingo car il se veut très progressif et didactique dans l'approche du Lingo, en essayant de présenter le cheminement clairement plutôt que de mettre le code final directement à chaque étape.

Points abordés :

  • Contrôler un sprite avec la souris
  • Animer des sprites avec Lingo
  • Utiliser les scripts de comportement
  • Utiliser les scripts d'animation
  • Listes linéaires
  • Evènements
  • Détecter et gérer une collision entre 2 sprites

Etape 0 : Propriétés d'animation

Ouvrez le panneau de commande (Fenêtre > Panneau de commande) et réglez la vitesse d'animation sur 50 images par secondes pour avoir une bonne fluidité : le jeu est très léger, même de vieux ordinateurs n'auront aucun mal à le faire tourner.

image0.jpg

Etape 1 : Resources graphiques

Tout d'abord, il faut créer les acteurs graphiques nécessaires pour le jeu. En l'occurence nous avons besoin d'un acteur pour le vaisseau du joueur, un acteur pour les envahisseurs et un acteur pour représenter les tirs du vaisseau. Pour un jeu il vaut généralement mieux utiliser des acteurs bitmaps que vectoriels, car ces derniers sont plus lourds à afficher pour la machine et demandent plus de resources.

image1_1.jpg

Il est à noter qu'il est également possible d'utiliser des acteurs Flash si ceux-ci ne sont pas animés (fichier .swf importé dans la distribution). Dans ce cas pour conserver des performances proches de celles obtenues avec des bitmaps il faut utiliser une propriété méconnue de Director : STATIC. Avec une ligne de lingo telle que

on startmovie
 member("monActeurFlash1").static = true
 member("monActeurFlash2").static = true
 member("monActeurFlash3").static = true
end

os sprites Flash seront presque aussi rapides à afficher que des sprites bitmaps.

Quelque soit le type d'acteur que vous choisissez, placez sur la scène un sprite pour le vaisseau du joueur, une bonne quantité de vaisseaux ennemis et quelques tirs, mais ces derniers positionnés en dehors de la zone visible de l'animation car ils ne doivent apparaître à l'écran que lors d'un tir. Pensez à copier/coller vos sprites (pour les ennemis surtout) pour gagner du temps. Mettez l'encre de tous les sprites sur “encre seule” pour des bitmaps, fond transparent pour les autres (vectoriel…). Vous devriez obtenir quelque chose de comparable à ceci :

image1_2.jpg

Euh… j'ai oublié de vous prévenir mais vous l'aurez sûrement remarqué : je ne suis pas graphiste :-) …et j'aime bien le vert aussi ;-)

Etape 2 : Boucle et contrôle du vaisseau à la souris

A présent nous pouvons attaquer la partie Lingo de ce jeu, la première chose à faire est un script de frame qui permettra de boucler indéfiniment sur cette frame. Pour cela double cliquez à l'endroit voulu dans le scénario ou bien créez un script de comportement dans la distribution que vous glisserez ensuite dans le scénario.

Dans ce script, tapez le code Lingo suivant :

on exitframe me
  go to the frame
end

Cela fera boucler indéfiniment la “tête de lecture” à cet endroit de l'animation.

Maintenant, créez un nouveau comportement pour le sprite vaisseau et appelez le “joueur” par exemple.

image2.jpg

Dans ce script nous allons d'abord insérer le code pour déplacer le vaisseau avec la souris, pour cela il faut ajouter cette ligne dans le gestionnaire “on exitFrame” qui est un des gestionnaires éxécuté à chaque image (donc dans notre cas 50 fois par seconde) :

on exitframe me
 sprite(me.spritenum).loch = the mouseh
end

la propriété “me.spriteNum” renvoie le numéro du sprite sur lequel a été posé le comportement (me est la référence à l'instance du script)

Si vous appuyez sur lecture à présent vous constaterez que le sprite du vaisseau suit bien les mouvements de la souris sur l'axe horizontal mais aussi qu'il peut sortir de l'écran ce que nous voulons éviter. Pour cela nous devons contraindre le sprite du vaisseau en mettant une condition à son déplacement, ce qui donne le code Lingo suivant :

on exitframe me
 
 if the mouseh >= 0 then -- si la souris n'est pas en dehors à gauche
 
   if the mouseh < 640 then -- si la souris n'est pas en dehors à droite
 
     sprite(me.spritenum).loch = the mouseh -- la position horizontale du sprite est celle de la souris
 
   else
 
     sprite(me.spritenum).loch = 640 -- sinon, c à d si la souris est en dehors
     --à droite on positionne le sprite au bord droit
 
   end if
 
 else
 
   sprite(me.spritenum).loch = 0 -- si la souris est en dehors à gauche on positionne
  -- le sprite au bord gauche
 
 end if
 
end

Il s'agit en fait de deux conditions imbriquées : une pour le bord gauche de l'écran, l'autre pour le droit.

Si vous appuyez sur lecture vous verrez que le sprite du vaisseau déborde encore à moitié à gauche et à droite, mais pour ne pas compliquer on va dire qu'on fera ça avec les finitions.

Pour l'instant on a un vaisseau qui bouge avec la souris, on va maintenant faire bouger les envahisseurs.

Etape 3 : Animation de sprites grâce aux comportements et gestion d'évènement

sélectionnez tous vos sprites envahisseurs et ajoutez leur un nouveau comportement.

Les vaisseaux aliens doivent se déplacer jusqu'à ce que l'un d'entre eux touche un des bords de l'écran, auquel cas tous les aliens doivent descendre d'un cran et changer de direction. Ceci est très simple à réaliser en lingo avec les scripts de comportement.

Tout d'abord, il faut initialiser les variables dont on va avoir besoin. Pour cela il faut utiliser le gestionnaire “on beginSprite me” des comportements, qui a la particularité de n'être éxécuté qu'une seule fois : lors de l'apparition du sprite à l'écran. Il faut également déclarer le type et le nom des variables que l'on veut pouvoir utiliser dans le script, en les notant tout en haut du script elles seront disponibles dans tous les gestionnaires de celui-ci.

Note à propos de la notation des variables :
vous remarquerez que je note une propriété toujours précédée d'un “p”, et une globale toujours précédée d'un “g”, cette notation des variables permet de s'y retrouver plus facilement et d'éviter des étourderies. Ce système est simple mais on peut être plus précis en utilisant une autre lettre pour le type de propriété ou de globale (booléen, entier, nombre à virgule, texte, liste, image…), personnelement je note la plupart des types de manière compréhensible voire en entier ex : une propriété liste sera notée pListe, mais pour ça c'est chacun son truc, je souhaite surtout donner de bonnes pistes aux débutants.

property pDirectionGauche -- une booléenne (true/false) pour déterminer
-- si le sprite se déplace vers la droite (false) ou la gauche (true)
 
on beginsprite me
 pDirectionGauche = true
end

Ensuite nous allons utiliser cette variable avec une condition dans le gestionnaire “on exitFrame” que nous avons vu plus haut (éxécuté 50 fois par seconde si vous avez bien réglé la cadence de l'animation). Il faudra également une autre condition pour “détecter” le bord de l'écran et changer de direction

on exitframe me
 
 if pDirectionGauche then
 
   if sprite(me.spritenum).loch > 0 then -- si mon sprite n'a pas encore atteint le bord gauche de l'écran
 
     sprite(me.spritenum).loch = sprite(me.spritenum).loch - 1 -- déplace mon sprite d'un pixel vers la gauche
 
   else -- sinon (si mon sprite a atteint le bord gauche)
 
     sprite(me.spritenum).locv = sprite(me.spritenum).locv + 5 -- déplace mon sprite de 5 pixels vers le bas
 
     pDirectionGauche = false -- change de direction
 
   end if
 
 else
 
   if sprite(me.spritenum).loch < 640 then -- si mon sprite n'a pas encore atteint le bord droit de l'écran
 
     sprite(me.spritenum).loch = sprite(me.spritenum).loch + 1 -- déplace mon sprite d'un pixel vers la droite
 
   else
 
     sprite(me.spritenum).locv = sprite(me.spritenum).locv + 5 -- déplace mon sprite de 5 pixels vers le bas
 
     pDirectionGauche = true -- change de direction
 
   end if
 
 end if
 
end

Pour l'instant chaque sprite ne se préocupe que de lui-même, alors que nous voulons que dès qu'un sprite atteint le bord de l'écran il ordonne à tous les autres de changer de direction. Pour cela nous allons créer un évènement grâce à un script d'animation, dont les gestionnaires sont accessibles depuis n'importe quel autre script. Créez un script d'animation dans la distribution, et placez-y ce gestionnaire :

on changeDirection
 
 repeat with i = 10 to 57
 
   if sprite(i).pDirectionGauche then -- si le sprite allait à gauche
 
     sprite(i).locv = sprite(i).locv + 5 -- on descend ce sprite de 5 pixels
 
     sprite(i).pDirectionGauche = false -- maintenant il va à droite
 
   else -- s'il allait à droite
 
     sprite(i).locv = sprite(i).locv + 5 -- on descend ce sprite de 5 pixels
 
     sprite(i).pDirectionGauche = true -- on le fait repartir à gauche
 
   end if
 
 end repeat
 
end

Afin de rester simple, on note ici les n° de sprites en “statique” : si on rajoutait ou si on enlevait quelques envahisseurs il faudrait modifier ce bout de script, mais nous verrons plus loin comment faire ça en dynamique histoire encore une fois de ne pas embrouiller les débutants.

L'instruction repeat éxécute les instructions qu'elle contient en incrémentant i de 1 à chaque passage jusqu'à atteindre 57, ce qui permet de faire le tour de tous les envahisseurs en un clin d'oeil.

Maintenant il faut également qu'on modifie légèrement le gestionnaire “on exitFrame me” du comportement des envahisseurs afin de prendre en compte ce nouveau gestionnaire pour créer l'évènement :

image3.jpg

property pDirectionGauche
 
on beginsprite me
 pDirectionGauche = true
end
 
on exitframe me
 
 if pDirectionGauche then
 
   if sprite(me.spritenum).loch > 0 then -- si mon sprite n'a pas encore atteint le bord gauche de l'écran
 
     sprite(me.spritenum).loch = sprite(me.spritenum).loch - 1 -- déplace mon sprite d'un pixel vers la gauche
 
   else -- sinon (si mon sprite a atteint le bord gauche)      
 
     changeDirection() -- change de direction
 
   end if
 
 else
 
   if sprite(me.spritenum).loch < 640 then
 
     sprite(me.spritenum).loch = sprite(me.spritenum).loch + 1
 
   else      
 
     changeDirection()
 
   end if
 
 end if
 
end

A présent il ne nous reste plus qu'à permettre au joueur de détruire les envahisseurs en tirant avec la souris et nous aurons la base du moteur de jeu.

Etape 4 : Utilisation des listes

Etant donné que le joueur doit pouvoir tirer indéfiniment et que nous ne disposons que d'une quantité limitée de sprites il faut qu'un sprite de tir se rende à nouveau disponible dès qu'il a quitté l'écran. Pour cela nous allons utiliser une liste globale, que nous allons initialiser dans le gestionnaire “on beginSprite” du script de frame, qui contenait déjà un gestionnaire “on exitFrame” :

global gListeTirs
 
on beginsprite me
 gListeTirs = []
end
 
on exitframe me
 go to the frame
end

ensuite il faut que chaque sprite de tir vienne s'inscrire dans cette liste, ce que nous allons faire dans le gestionnaire “on beginSprite” (éxécuté une seule fois) d'un nouveau comportement :

global   gListeTirs -- la même variable que dans l'autre script, vu qu'elle est globale
 
property mySprite -- seule exception aux propiétés en "p"
 
on beginsprite me
 
 mySprite = sprite(me.spritenum) -- référence au sprite sur lequel est posé
 -- ce comportement, évite d'avoir à taper "sprite(me.spriteNum)" tout le temps 
 
 gListeTirs.add(mySprite) -- chaque sprite tir se rend disponible dans la liste
 
end

Maintenant nous allons écrire le code pour l'animation dans ce même comportement, avec une booléenne pour stopper l'animation ou non, un gestionnaire personnalisé qu'on appellera “on fire me” devra placer le sprite au bout du canon du sprite vaisseau (que nous allons également inscrire dans un globale pour plus de facilité) et commencer à bouger vers le haut. Pour l'instant on laissera de côté la détection de collisions avec les envahisseurs, on s'occupera juste de rendre à nouveau le sprite disponible lorsu'il sortira en haut de l'écran :

on déclare une nouvelle globale pour le sprite du joueur (le vaisseau) dans le gestionnaire “on beginSprite” de son script de comportement, pour que les sprites de tirs puissent se placer correctement au bout de son canon par la suite

global gSpriteJoueur
 
on beginsprite me
 gSpriteJoueur = sprite(me.spritenum)
end

Ensuite on s'occuppe du sprite tir :

global   gSpriteJoueur -- Référence au sprite du vaisseau
 
global   gListeTirs    -- la même variable que dans l'autre script, vu qu'elle est globale
 
property mySprite      -- seule exception aux propiétés en "p";)
 
property pMouvement    -- booléenne, détermine si le tir est en action ou non
 
on beginsprite me
 
 mySprite = sprite(me.spritenum) -- référence au sprite sur lequel est posé ce comportement, 
--évite d'avoir à taper "sprite(me.spriteNum)" tout le temps :)
 
 gListeTirs.add(mySprite) -- chaque sprite tir se rend disponible dans la liste
 
 pMouvement = false -- au départ tous les tirs sont à l'arrêt et disponibles
 
end
 
on exitframe me
 
 if pMouvement then
 
   if mySprite.locv >= 0 then
 
     mySprite.locv = mySprite.locv - 3
 
   else -- le sprite est sorti de l'écran
 
     mySprite.loc = point(-30,-30)
 
     pMouvement = false
 
     gListeTirs.add(mySprite) -- on ajoute le sprite à la liste des sprites disponibles
 
   end if
 
 end if
 
end
 
on fire me -- gestionnaire perso, on peut l'appeler comme on veut
 
 mySprite.loc = gSpriteJoueur.loc - point(0,15) -- le sprite tir prend la position du sprite du joueur
-- avec un décalage pour être placé au bout du canon, 
-- modifiez le 15 en fonction de votre sprite
 
 pMouvement = true
 
 gListeTirs.deleteone(mySprite) -- on retire le sprite de la liste des sprites disponibles
 
end

A présent on va rajouter le code pour générer l'évènement “fire” avec le clic gauche de la souris, on éxécutera le gestionnaire du premier sprite disponible dans la liste avec une condition pour vérifier d'abord que celle-ci n'est pas vide (ce qui générerait une erreur de script à l'éxécution). On va placer ce code dans le script de frame, dans un gestionnaire “on mouseDown” :

global gListeTirs
 
on beginsprite me
 gListeTirs = []
end
 
on exitframe me
 go to the frame
end
 
on mousedown me
 if gListeTirs <> [] then
   gListeTirs[1].fire()
 end if
end

Si vous appuyez sur lecture tout devrait fonctionner excepté les collisions avec les envahisseurs.

Etape 5 : Détection et gestion de collisions entre sprites

Pour commencer il faut déjà modifier légèrement le script des envahisseurs, afin de lui ajouter un état “mort” quand il a été touché et que chaque envahisseur s'inscrive dans une liste globale “gListeEnvahisseurs” pour les détections de collisions que nous allons faire ensuite : Comme précédemment, on initialise la liste des envahisseurs dans le gestionnaire “on exitFrame me” du script de frame :

global gListeTirs
 
global gListeEnvahisseurs
 
on beginsprite me
 gListeTirs = []
 gListeEnvahisseurs = []
end
 
on exitframe me
 go to the frame
end
 
on mousedown me
 if gListeTirs <> [] then
   gListeTirs[1].fire()
 end if
end

Puis on fait en sorte que chaque sprite envahisseur inscrive son n° de sprite dans la liste, dans le gestionnaire “on beginSprite me” de leur comportement, et on lui ajoute une condition “pMort” (booléenne) qui détermine s'il est actif ou non :

global gListeEnvahisseurs
 
property pDirectionGauche
 
property pMort
 
on beginsprite me
 
 pDirectionGauche = true
 
 pMort = false
 
 gListeEnvahisseurs.add(me.spritenum)
 
end
 
 
 
on exitframe me
 
 if not pMort then
 
   if pDirectionGauche then
 
     if sprite(me.spritenum).loch > 0 then -- si mon sprite n'a pas encore atteint le bord gauche de l'écran
 
       sprite(me.spritenum).loch = sprite(me.spritenum).loch - 1 -- déplace mon sprite d'un pixel vers la gauche
 
     else -- sinon (si mon sprite a atteint le bord gauche)      
 
       changeDirection() -- change de direction
 
     end if
 
   else
 
     if sprite(me.spritenum).loch < 640 then
 
       sprite(me.spritenum).loch = sprite(me.spritenum).loch + 1
 
     else      
 
       changeDirection()
 
     end if
 
   end if
 
 end if
 
end
 
 
on destruction me
 
 sprite(me.spritenum).loc = point(-50,-50) -- on place le sprite hors de l'écran
 
 pMort = true -- on le rend immobile
 
 gListeEnvahisseurs.deleteone(me.spritenum) -- et on l'efface de la liste des envahisseurs actifs
 
end

Vous remarquerez que j'ai également ajouté un gestionnaire personnalisé “on destruction me” qui sera activé lors d'une collision pour “tuer” l'envahisseur.

Il ne reste plus qu'à mettre en place la détection de collisions dans le gestionnaire “on exitFrame me” du script de comportement des tirs. Pour cela nous allons utiliser la fonction “sprite(i).intersects(numeroDeSprite)”, qui n'est pas la plus rapide mais qui respecte exactement la forme des sprites:

global   gSpriteJoueur -- Référence au sprite du vaisseau
 
global   gListeTirs    -- la même variable que dans l'autre script, vu qu'elle est globale
 
global   gListeEnvahisseurs --
 
property mySprite      -- seule exception aux propiétés en "p"
 
property pMouvement    -- booléenne, détermine si le tir est en action ou non
 
on beginsprite me
 
 mySprite = sprite(me.spritenum) -- référence au sprite sur lequel est posé ce comportement,
-- évite d'avoir à taper "sprite(me.spriteNum)" tout le temps :)
 
 gListeTirs.add(mySprite) -- chaque sprite tir se rend disponible dans la liste
 
 pMouvement = false -- au départ tous les tirs sont à l'arrêt et disponibles
 
end
 
 
 
on exitframe me
 
 if pMouvement then    
 
   repeat with i = 1 to gListeEnvahisseurs.count      
 
     if mySprite.intersects(gListeEnvahisseurs[i]) then
 
       sprite(gListeEnvahisseurs[i]).destruction()
 
       reset(me)
 
       exit
 
     end if
 
   end repeat
 
   if mySprite.locv >= 0 then
 
     mySprite.locv = mySprite.locv - 3
 
   else -- le sprite est sorti de l'écran
 
     reset(me)
 
   end if
 
 end if
 
end
 
on fire me -- gestionnaire perso, on peut l'appeler comme on veut
 
 mySprite.loc = gSpriteJoueur.loc - point(0,15) -- le sprite tir prend la position du sprite du joueur avec un décalage 
--pour être placé au bout du canon, modifiez le 15 en fonction de votre sprite
 
 pMouvement = true
 
 gListeTirs.deleteone(mySprite) -- on retire le sprite de la liste des sprites disponibles
 
end
 
 
 
on reset me
 
 mySprite.loc = point(-30,-30)
 
 pMouvement = false
 
 gListeTirs.add(mySprite) -- on ajoute le sprite à la liste des sprites disponibles
 
end

au passage j'ai également créé un gestionnaire pour le “reset” des sprites tirs car le code apparaissait dans 2 cas, il valait donc mieux l'externaliser.

Ca y est ! Appuyez sur lecture, vous avez un moteur de space invaders qu'il ne reste plus qu'à fignoler… ce que nous verrons dans une deuxième partie :-)