Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox



La programmation orientée objet sous Director : le jeu du morpion

Compatible Director MX2004. Cliquer pour en savoir plus sur les compatibilités.Par glurp (Elliot Coene), le 13 août 2009

Tout comme la programmation classique sous Director, la programmation orientée objet (POO) est particulière et très souple. Cependant, pour respecter les bases de la POO, je n'abuserai pas de cette souplesse dans ce tutorial.

En image :

morpion.jpg

En action (cliquez pour jouer) :

Tout d'abord, qu'est-ce que la POO en quelques mots ?

C'est un principe qui permet d'organiser son code pour que certaines parties restent privées, contrairement à la programmation classique avec laquelle il est vite facile de tout mélanger, surtout pour les gros projets. Un objet est un modèle qui possède ses fonctions et ses variables propres. On ne peut pas utiliser un objet directement (en tout cas sous Director), il faut en créer des instances. Si vous utilisez Director fréquemment, vous verrez au cours de ce tutorial que la notion d'objet ne vous est pas étrangère. En effet sachez pour l'exemple qu'un sprite est un objet et qu'un behavior (comportement) qui lui est attaché est une instance…

me, c'est quoi ?

Vous vous êtes sûrement déjà posé la question. Pourquoi ce me, pourquoi doit-on le mettre dans les script de behavior et pourquoi pas dans les script de movie ? Et bien me, c'est moi ! me signifie tout simplement “l'adresse mémoire de l'instance”. C'est un peu barbare pour le moment, mais vous comprendrez qu'il est très important puisque c'est le seul moyen de communiquer avec l'instance.

Commençons la programmation !

Tout d'abord je vous invite à créer un nouveau fichier director de 640*480 pixels avec un fond noir. Puis, puisqu'il s'agit d'un jeu de morpion, je vous invite à importer trois images (un X, un O, un vide) de 60*60 pixels. Ensuite nous allons préparer les champs textes, créons en 3 ; celui pour le score du joueur 1 sera nommé scoreJ1_txt, celui pour le score du joueur 2 sera nommé scoreJ2_txt, et celui pour annoncer le joueur en cours sera nommé joueurCourant_txt.

Pour cet exemple, nous allons faire l'effort ne pas utiliser la timeline (ligne du temps) de Director, ceci nous forcera à tout faire nous même sans utiliser les objets de Director. La seule chose que nous allons y mettre est le stop, que je positionne à la frame 30 par défaut.

Le stop est un behavior, son code est simple et permet de boucler sur une frame.

on exitframe me
  go to the frame
end

Bien, nous allons pouvoir commencer la programmation du jeu proprement dite. Ajoutons un script parent au cast et nommons le “morpion_script”. Pour le moment le fichier est vide, ajoutons y le code suivant :

on new me
 
  return me
 
end
 
on destroy me
 
end

Il s'agit du code minimum que doit contenir un objet. En POO on dit que new est le constructeur et destroy est le destructeur. Vous avez reconnu notre ami “me” qui, comme pour les behavior, doit être passé en paramètre de toutes les fonctions de l'objet. Le “return me” est également indispensable puisqu'il renvoit l'adresse de l'instance là où elle a été créée.

Créons maintenant un script de Movie (Animation) global pour initialiser notre application. Ce script contiendra simplement ;

global monMorpion
 
on prepareMovie
  monMorpion = script("morpion_script").new()
end
 
on stopmovie
  monMorpion.destroy()
  monMorpion = void
end

Vous voyez donc que j'ai initialisé une variable globale qui contient l'adresse mémoire de l'instance de l'objet morpion_script. Cette variable est réutilisée dans le stopMovie pour appeler la commande destroy() de l'instance. Le langage Lingo utilise la syntaxe en point pour communiquer avec les objets.

Pour le moment, notre programme ne fait pas grand chose si vous le lancez… Etoffons un peu notre objet morpion_script. Nous savons qu'un morpion implique une grille de 3*3, nous allons donc utiliser une liste à deux dimensions aussi appelée matrice.

property pCases
 
on new me
 
  me.init()
 
  return me
 
end
 
on init me
 
  -- Déclaration de la liste des lignes --
  pCases = []
 
  repeat with tLine = 1 to 3
 
    -- Déclaration de la liste des colonnes --
    pCases[tLine] = []
 
    repeat with tCol = 1 to 3
      -- Instanciation des objets cases --
      pCases[tLine][tCol] = script("case_script").new(tLine, tCol)
    end repeat
 
  end repeat
 
  put "Initialisation Morpion terminée"
 
end
 
on destroy me
 
  repeat with tLine = 1 to 3
    repeat with tCol = 1 to 3
      pCases[tLine][tCol].destroy()
    end repeat
  end repeat
 
  put "Destruction Morpion terminée"
 
end

Vous remarquez que j'ai rempli la grille avec des instances au script “case_script” alors qu'il n'existe pas encore. Si vous lancez l'application à ce moment, vous aurez le message “Erreur de script : Object expected”. Nous allons donc définir “case_script”.

property pLine, pCol
 
on new me, tLine, tCol
 
  me.init(tLine, tCol)
 
  return me
 
end
 
on init me, tLine, tCol
 
  pLine = tLine
  pCol = tCol
 
  put "Initialisation de la case "&pLine&" "&pCol
 
end
 
on destroy me
 
end
 
-- Accesseurs --
on getCol me
  return pCol
end
on getLine me
  return pLine
end

La formulation de new me permet de passer des paramètres à l'instanciation. J'ai stocké ces deux paramètres dans des variables de propriétés. En POO, les variables des objets sont normallement privées. Cela signifie qu'elles ne doivent être visible qu'à l'intérieur de l'instance. Sous Director il est possible de consulter les variables de propriété depuis l'extérieur grâce à la syntaxe en point (instance.propriété), mais pour rester fidèle aux principes de la POO, on préfèrera utiliser des Accesseurs pour les consulter et des Mutateurs pour les modifier. Pour cet exemple, pCol et pLine ne doivent pas être modifiés mais seulement consultés, nous ne créons donc que les accesseurs.

Pour cloturer le “case_script” nous allons lui ajouter une valeur et une représentation graphique à laquelle nous allons attacher une interaction.

global monMorpion
 
property pLine, pCol
property pValeur, pSprite
 
on new me, tLine, tCol
 
  me.init(tLine, tCol)
 
  return me
 
end
 
on init me, tLine, tCol
 
  pLine = tLine
  pCol = tCol
 
  pValeur = ""
 
  -- Initialisation du sprite --
  pSprite = findEmptyChannel()
  tPositionX = 260+((tCol-1)*62)
  tPositionY = 180+((tLine-1)*62)
  channel(pSprite).makescriptedsprite(member("vide"), point(tPositionX, tPositionY))
 
  -- Attache le script au sprite --
  sprite(pSprite).scriptInstanceList.add(me)  
 
  put "Initialisation de la case "&pLine&" "&pCol
 
end
 
on destroy me
  resetChannel(pSprite)
end
 
-- Accesseurs --
on getCol me
  return pCol
end
on getLine me
  return pLine
end
 
on getValeur me
  return pValeur
end
 
-- Interactivité --
on mouseenter me
  if pValeur = "" then
    cursor 280
  end if
end
on mouseleave me
  cursor 0
end
on mouseup me
 
  if pValeur = "" then
 
    -- Récupère la valeur du joueur courant --
    tJoueurCourant = monMorpion.getJoueurCourant()    
    pValeur = tJoueurCourant.getValeur()    
    sprite(pSprite).member = member(pValeur)
 
    -- Passer au tour suivant --
    monMorpion.tourSuivant()
 
    cursor 0      
 
  end if    
 
end

Le code de case_script est terminé, mais si on lance l'application et qu'on essaie de cliquer sur une case on a une erreur “Handler not found in object”. En effet, nous faisons appel à plusieurs fonctions non-définie ; findEmptyChannel(), resetChannel(), monMorpion.getJoueurCourant() et monMorpion.tourSuivant().

Commençons par définir les deux premières, en global. Notre script de Movie devient donc ;

global monMorpion
 
on prepareMovie
  monMorpion = script("morpion_script").new()
end
 
on stopmovie
  monMorpion.destroy()
  monMorpion = void
end
 
-- Fonctions globales --
on findEmptyChannel
 
  tChannel = 1
  repeat with i=1 to 50
    if sprite(i).puppet = false and sprite(i).membernum = 0 then
      tChannel = i
      exit repeat
    end if    
  end repeat
 
  return tChannel
 
end
 
on resetChannel tChannel
 
  sprite(tChannel).membernum = 0
  sprite(tChannel).blend = 100
  sprite(tChannel).visible = true
  sprite(tChannel).scriptInstanceList = []
  sprite(tChannel).ink = 0
 
  channel(tChannel).removeScriptedSprite()
 
end

Ensuite, il faut définir les deux fonctions propres à monMorpion. Mais nous remarquons alors qu'il manque l'élément le plus important à notre logique de jeu ; les joueurs !

Toujours en POO, nous allons créer un nouvel objet “joueur_script” assez simple (libre à vous de le complexifier en y ajoutant des noms ou des avatars).

property pId, pScore
property pSprite, pMember, pValeur
 
on new me, tId
 
  me.init(tId)
 
  return me
 
end
 
on init me, tId
 
  pId = tId
 
  -- Définition de la valeur symbol --
  if pId = 1 then 
    pValeur = "x"
  else
    pValeur = "o"
  end if
 
  -- Affichage du score --
  pMember = member("scoreJ"&pId&"_txt")  
  pSprite = findEmptyChannel()
  channel(pSprite).makescriptedsprite(pMember, point(50+((pId-1)*460),230))
 
  me.setScore(0)
 
end
 
on destroy me
  resetChannel(pSprite)
end
 
-- Accesseurs --
on getScore me
  return pScore
end
on getId me
  return pId
end
on getValeur me
  return pValeur
end
 
-- Mutateurs --
on setScore me, tScore
  pScore = tScore
  pMember.text = "Score J"&pId&" : "&pScore    
end

Nous pouvons dès lors ajouter les joueurs au sein de l'objet “morpion_script”. Nous en profitons également pour gérer l'appel à la fonction monMorpion.tourSuivant() qui vérifiera si la partie est terminée (gagnant ou match nul) ou si la partie continue ;

property pCases, pJoueurs
property pJoueurCourant, pNbCoups, pTextCourantSprite
 
on new me
 
  me.init()
 
  return me
 
end
 
on init me
 
  -- Déclaration de la liste des lignes --
  pCases = []
 
  repeat with tLine = 1 to 3
 
    -- Déclaration de la liste des colonnes --
    pCases[tLine] = []
 
    repeat with tCol = 1 to 3
      -- Instanciation des objets cases --
      pCases[tLine][tCol] = script("case_script").new(tLine, tCol)
    end repeat
 
  end repeat
 
  -- Déclaration de la liste des joueurs --
  pJoueurs = []
 
  repeat with i = 1 to 2
    pJoueurs[i] = script("joueur_script").new(i)
  end repeat
 
  -- Joueur Courant --
  pJoueurCourant = pJoueurs[1]
 
  pTextCourantSprite = findEmptyChannel()
  channel(pTextCourantSprite).makescriptedsprite(member("joueurCourant_txt"), point(230,50))
  sprite(pTextCourantSprite).member.text = "JOUEUR 1 JOUE"
 
  pNbCoups = 0
 
  put "Initialisation Morpion terminée"
 
end
 
on destroy me
 
  repeat with tLine = 1 to 3
    repeat with tCol = 1 to 3
      pCases[tLine][tCol].destroy()
    end repeat
  end repeat
 
  repeat with i = 1 to 2
    pJoueurs[i].destroy()
  end repeat
 
  resetChannel(pTextCourantSprite)
 
  put "Destruction Morpion terminée"
 
end
 
-- Accesseurs --
on getJoueurCourant me
  return pJoueurCourant
end
 
on tourSuivant me
 
  pNbCoups = pNbCoups + 1
 
  -- Vérifier s'il y a un gagnant --
  if me.checkGagnant() then
    pJoueurCourant.setScore(pJoueurCourant.getScore()+1)
 
    updatestage()
    alert("Joueur "&pJoueurCourant.getId()&" gagne !")
 
    me.resetGrille()
 
    pNbCoups = 0
 
  else if pNbCoups = 9 then
 
    updatestage()
    alert("Match Nul")
 
    me.resetGrille()
 
    pNbCoups = 0
 
  else
 
    if pJoueurCourant = pJoueurs[1] then
      pJoueurCourant = pJoueurs[2]
      sprite(pTextCourantSprite).member.text = "JOUEUR 2 JOUE"
    else
      pJoueurCourant = pJoueurs[1]
      sprite(pTextCourantSprite).member.text = "JOUEUR 1 JOUE"
    end if
 
  end if
 
end
 
on checkGagnant me
 
  -- Ligne horizontale --
  repeat with tLine = 1 to 3
    if pCases[tLine][1].getValeur()=pJoueurCourant.getValeur() and \
pCases[tLine][2].getValeur()=pJoueurCourant.getValeur() and \
pCases[tLine][3].getValeur()=pJoueurCourant.getValeur()  then
      return true
    end if
  end repeat
 
  -- Colonne verticale --
  repeat with tCol = 1 to 3
    if pCases[1][tCol].getValeur()=pJoueurCourant.getValeur() and \
pCases[2][tCol].getValeur()=pJoueurCourant.getValeur() and \
pCases[3][tCol].getValeur()=pJoueurCourant.getValeur()  then
      return true
    end if
  end repeat
 
  -- Diagonales --
  if pCases[1][1].getValeur()=pJoueurCourant.getValeur() and \
pCases[2][2].getValeur()=pJoueurCourant.getValeur() and \
pCases[3][3].getValeur()=pJoueurCourant.getValeur()  then
    return true
  end if
 
  if pCases[3][1].getValeur()=pJoueurCourant.getValeur() and \
pCases[2][2].getValeur()=pJoueurCourant.getValeur() and \
pCases[1][3].getValeur()=pJoueurCourant.getValeur()  then
    return true
  end if
 
  -- Rien --
  return false
 
end
 
on resetGrille me
 
  repeat with tLine = 1 to 3
    repeat with tCol = 1 to 3
      pCases[tLine][tCol].reset()
    end repeat
  end repeat
 
end

Pour gérer la succession des parties, vous remarquerez l'apparition de la fonction resetGrille. Cette dernière appelle une fonction reset sur chaque case qu'il faudra définir comme suit dans le code de l'objet “case_script” ;

on reset me
  pValeur = ""
  sprite(pSprite).member = member("vide")
end

Voilà, la programmation du jeu est terminée, bon amusement ! ;-)

morpioncast.jpg

Sources Director MX 2004 :