JeuWeb - Crée ton jeu par navigateur
[POO] Utilisation du singleton - Version imprimable

+- JeuWeb - Crée ton jeu par navigateur (https://jeuweb.org)
+-- Forum : Discussions, Aide, Ressources... (https://jeuweb.org/forumdisplay.php?fid=38)
+--- Forum : Programmation, infrastructure (https://jeuweb.org/forumdisplay.php?fid=51)
+--- Sujet : [POO] Utilisation du singleton (/showthread.php?tid=2216)



[POO] Utilisation du singleton - uriak - 20-01-2008

Ce tutorial est destinée à montrer une application plus poussée de la POO, et de certains design patterns.

Prérequis : Je recommande avant lecture d'avoir parcouru les tutoriaux POO et Singleton pour comprendre la PPO et l'utilisation d'élément statiques pour une classe.

Objectif : Dans un jeu où les différents éléments sont gérés par des classes, on souhaite pouvoir accéder aux éléments propres au joueur facilement, tout en évitant des requêtes inutiles et de devoir faire des classes différentes pour ce qui appartient au joueur et les autres.

Contexte : Un joueur se trouve dans une zone située dans une région. Ces éléments ne sont pas susceptibles de changer en dehors d'une action du joueur.

Présentation de la classe CharacterManager

Code PHP :
<?php 
class CharacterManager
{
//inutilisés pour le moment
protected $output;
protected
$log;

//variable principale où sont stokées les valeurs définissant un joueur.
protected $profile = array("status" => 0);
protected
$id;

//constructeur, peut être appelé avec un identifiant ou un profil
public function __construct($profile, $complete = true)
{
//cas du profil, on recopie les array
if ($complete)
{
$this->profile = $profile;
$this->profile['status'] = 1;
}
else
//cas de l'ientifiant, une requête est alors réalisée.
{
$query = 'SELECT * FROM player WHERE id_player="'.$profile.'"';
$result = connexion::GetSingleton()->query($query);
if(
$data = mysql_fetch_array($result))
$this->profile = array("status" => 1,
"id" => $data['id_player'],
"name" => $data['name'],
"life" => $data['life'],
"stamina" => $data['stamina'],
"money" => $data['wealth'],
"x" => $data['x'],
"y" => $data['y']);
}
}
...
Cette classe sert à gérer un joueur quelconque. Elle s'instancie de deux manières, par identifiant ou par profil. Le profil permet d'utiliser une requête réalisée auparavant.

Une des actions en particulier, note le commentaire sur la mise à jour de factory.
Code PHP :
<?php 
public function GoToZone($id)
{
//Vérification, de la distance, on récupère les coordonnées d'arrivée de la zone.
$query = 'SELECT x,y from zone WHERE id_zone="'.$id.'"';
$result = connexion::GetSingleton()->query($query);
if (
$data = mysql_fetch_array($result))
{
$x = $data["x"]; $y = $data["y"];
//test de distance, la somme des valeurs absolue doit être égale à 1 : ie on se déplace vers une case adjacente.
if ((abs($x - $this->profile["x"]) + abs($y - $this->profile["y"] )!= 1))
{
return
false;
}

//mise à joueur du joueur en deux étapes : 1 mise à jour du profil BDD
$query = 'UPDATE player SET zone="'.$id.'", x="'.$x.'", y="'.$y.'" WHERE id_player="'.$this->profile["id"].'"';
//si la mise à jour fonctionne, on met à joueur le PlayerManager lui-même. Comme on l'a par pointeur, cela modifie aussi le profil dans Factory et en session.
if ($result = connexion::GetSingleton()->query($query))
{
$this->profile["zone"] = $id;
$this->profile["x"] = $x;
$this->profile["y"] = $y;
}
return
$id;
}
return
false;
}

Présentation de la classe Factory. En réalité Factory n'est pas une réelle Factory au sens du pattern, c'est à dire un classe qui en instancie d'autres. Elle en instancie mais une seule. Son but est de centraliser l'accès à la session et à la bdd pour les éléments appartenant au joueur.

Voyons déjà les membres

Code PHP :
<?php 
class Factory
{
//le singleton
private static $instance;
//identifiant servant à générer le joueur. Il n'est pas conservé d'une page à l'autre
private static $id = -1;
//indique si la clsse dispose d'un identifiant valide.
private $loaded = false;
//stocke les ids des différents sous-éléments, permet d'y accéder sans passer par les classes
protected $identifiers = array();
//indique une erreur de chargement
protected $status = -1;

//classes principales auxquelles on souhaite accéder.
protected $player;
protected
$zone;
protected
$region;
...

Maintenant le pattern Singleton. A noter que dans ce cas spécifique, l'instance unique se cherche d'abord en mémoire, puis en session puis en BDD.

Code PHP :
<?php 
//constructeur privé
private function __construct()
{
//pour la génération, on récupère l'identifiant en cours
$this->identifier["player"] = self::$id;
//génération des classes
$this->GeneratePlayer();
$this->GenerateZone();
$this->GenerateRegion();
if (
$this->status) $this->loaded;
}

//accesseur singleton : privé!
private static function GetSingleton()
{
//récupération de l'instance
if (!isset(self::$instance))
{
if (!isset(
$_SESSION["factory"]))
{
//génération de l'instance et mise en session
self::$instance = new Factory();
$_SESSION["factory"] = self::$instance;
}
else
{
//récupération de l'instancemise en session. Mise à jour de l'identifiant
self::$instance = $_SESSION["factory"];
self::$id = self::$instance->identifier["player"];
}
}
return
self::$instance;
}

Remarque : GetSingleton est privé !! en effet le but n'est pas d'acceder à la Factory elle-même, on va donc utiliser d'autres méthodes statiques.

Maintenant comment accède-t-on à nos classes ? Par l'intermédiaire d'accesseurs statiques... On accède ainsi à CharacterManager, ZoneManager et RegionManager (bâties sur le même modèle)

Code PHP :
<?php 
//acesseurs utilisés dans le code : membres statiques, renvoie directement le pointeur vers la classe demandée
public static function GetPlayer()
{
return
Factory::GetSingleton()->player;
}
public static function
GetZone()
{
return
Factory::GetSingleton()->zone;
}
public static function
GetRegion()
{
return
Factory::GetSingleton()->region;
}
//pour récupérer les id sans paser par les classes, vérifie l'absence de problème.
public static function GetID($type)
{
$fac = Factory::GetSingleton();
if (
$fac->loaded)
{
return
$fac->identifier[$type];
}
else return
false;
}

A ce stade en toute page, si je fais Factory::GetPlayer() je reçois un Pointeur sur une instance de CharacterManager, cette instance passe de page en page par le biais de la session. Si je la modifie (confère GoToZone) elle est également modifiée dans la Factory.

Dernière question : comment instancie-t-on au départ un joueur ? La technique utilisée pour le moment utilise le membre statique self::$îd (ce n'est pas la seue solution). Comme il est perdu au changement de page, on le régénère dans GetSingleton()

Code PHP :
<?php 
//régénère la factory : sert principalement au changement de joueur.
public static function ReCharge()
{
self::$instance = new Factory();
if(
self::$id != -1) self::$instance->loaded = true;
$_SESSION["factory"] = self::$instance;
return
self::$instance;
}
//changement de joueur
public static function SelectPlayer($id)
{
$fac = Factory::GetSingleton();
//on test si le changement est nécessaire, et on applique la commande
if ($id != self::$id or !$fac->loaded)
{
self::$id = $id;
$fac = Factory::ReCharge();
}
}

Voilà comment s'effectue un changement de joueur. Le membre loaded indique si on a une factory valide.

Enfin un exemple de la génération d'une des classes.

Code PHP :
<?php 
//génère le joueur depuis la BDD
private function GeneratePlayer()
{
//requête
$query = 'SELECT player.*, zone.id_zone, region.id_region, region.name FROM player LEFT JOIN
zone ON player.x=zone.x AND player.y=zone.y
LEFT JOIN region ON zone.region=region.id_region WHERE id_player="'
.$this->identifier["player"] .'"';
$result = connexion::GetSingleton()->query($query);

if(
$data = mysql_fetch_array($result))
{
$player = array( "id" => $data['id_player'],
"name" => $data[2],
"life" => $data['life'],
"stamina" => $data['stamina'],
"money" => $data['wealth'],
"x" => $data['x'],
"y" => $data['y'],
"zone" => $data['zone']);


//utilisé pour les requêtes suivantes : les requêtes sont séparées car peu fréquentes et maintenues unitaires
$this->identifier["zone"] = $data['id_zone'];
$this->identifier["region"] = $data['id_region'];

//le CharacterManager peut être crée à l'aide d'un identifiant ou d'un tableau contenant les données. Ici la seconde option est utilisée
$this->player = new CharacterManager($player);
$this->status = 1;
}
else
$this->status = 0;
}

On note que le chargement du joueur génère les identifiants de zone et région. Pourquoi ne pas faire une requête plus poussée avec des JOIN ? Pour garder les requêtes assez unitaires, elles ne seront pas appellées souvent, et restent plus faciles à mettre à jour ainsi. Note : je pourrais aussi les supprimer et m'appuyer entièrement sur des requêtes dans CharacterManager.

Une dernière chose, la mise à jour de certains éléments ne peut être faite uniquement via les pointeurs des classes. Pour changer de zone on tuiliser la méthode suivante :

Code PHP :
<?php 
//sert à modifier le sous-élément zone. Change en cascade la région si nécessaire
public static function SelectZone($id)
{
$fact = Factory::GetSingleton();
if (
$fact->identifier["zone"] != $id)
{
$fact->identifier["zone"] = $id;
$idR = $fact->identifier["region"];
$fact->GenerateZone();
if (
$fact->identifier["region"] != $idR) $fact->GenerateRegion();
}

}

On effectue ainsi une mise à jour si nécessaire de la région.
Ce dernier point est une faiblesse du script actuel, il faut créer des méthodes dépendantes de la hierarchie des éléments, mais c'est indispensable à un stade ou à un autre.
Pourquoi les identifiant sont stockés à part des classes ? Parce qu'il est inutile de vouloir les modifier comme des variables internes : changer d'indentifiant, c'est recharger la classe ^^

Voilà j'espère que ce tutorial, à défaut de servir réellement pour votre jeu, vous montre un peu plus l'utilisation de la POO et des membres statiques.