samedi 26 mai 2012

Comment pauser un jeu dans Unity?

Pauser le jeu dans Unity est relativement facile. Si toutes vos fonctions sont dépendantes du taux de rafraîchissement (c'est le cas des animations), la commande suivante va pauser le jeu:
Time.timeScale = 0; 
La variable timeScale contrôle à qu'elle vitesse le temps s'écoule dans l'engin de jeu Unity. Une valeur inférieure à 1 va créer un effet de ralentie et 0, fige tout dans le temps. Pour résumer le jeu, remettez la valeur de timeScale à 1:
Time.timeScale = 1; 
Pour le code indépendant du taux de rafraîchissement, je suggère d'utiliser des messages (la fonction SendMessage). Une classe PauseGame contient deux fonctions statique: Pause et Resume. La fonction Pause met timeScale à 0 et envoie un message OnPauseGame à tous les objets dans la scène. De même, la fonction Resume remet timeScale à 1 et envoie le message global OnResumeGame. Les messages s'utilisent dans le code comme les fonctions Update et Start de Unity. La classe qui a besoin de pauser et de résumer implémente les fonctions OnPauseGame et OnResumeGame et ajoute le code spécifique pour cette classe:
public class A : MonoBehaviour
{
 void OnPauseGame()
 {
  //Do something to pause.
 }
 
 void OnResumeGame()
 {
  //Do something to resume.
 }
}
Pour mon script PauseGame (ci-dessous), j'ai ajouter la variable paused. Cette variable indique si le jeu est pausé ou non. Cette variable est utile pour bloquer du code dans la fonction Update comme suit:
public class B : MonoBehaviour
{
 void Update()
 {
  if ( !PauseGame.paused )
  {
   //Do something
  }
 }
}
Le script PauseGame (ci-dessous) pause le jeu lorsque la touche du bouton Pause est pressée. Vous devez définir ce bouton dans le menu Edit->Project Settings->Input. Dans mon cas, j'ai utilisé les touches P et Esc. Le script suit le modèle Singleton, d'où la variable s_instance. Ce modèle assure qu'une seule instance du script est créée. J'ai ajouté ce script sur ma caméra. Vous pouvez placer ce script n'importe où, mais il est important d'en avoir une copie dans votre scène pour pouvoir pauser le jeu.
/**
 * \file
 * 
 * \author Mentalogicus
 * \date 2012
 * 
 */

using UnityEngine;
using System.Collections;
using Mentalogicus;


/// \brief Pause the game with the button "Pause" defined in Unity Editor.
/// 
/// 
/// This class use the singleton modèle.
/// 
/// The game is paused by setting Time.timeScale to 0. Also, two messages are send:
/// - OnPauseGame: when the game is paused
/// - OnResumeGame: when the game is restarted
/// 
/// Those messages must be used in you script to manage codes that are not
/// frame independant (like those using absolute Time).
/// 
/// To use it, place the function OnPauseGame like this:
/// 
/// void OnPauseGame()
/// {
///  Do something.
/// }
/// 
/// 
/// 
[AddComponentMenu("Mentalogicus/Behaviour/PauseGame")]
public class PauseGame : MonoBehaviour
{
 #region Public members
 
 /// 
 /// Name of the message sent when pausing the game.
 /// 
 public const string ON_PAUSE_GAME = "OnPauseGame";
 
 /// 
 /// Name of the message sent when resuming the game
 /// 
 public const string ON_RESUME_GAME = "OnResumeGame";
 
 /// 
 /// Name of the button to pause or resume the game.
 /// 
 public const string BUTTON_NAME = "Pause";
 
 #endregion
 
 #region Private members
 
 /// 
 /// Flag that indicate if the game is paused.
 /// True-> the game is paused.
 /// False-> the game is running.
 /// 
 private bool _paused = false;
 
 /// 
 /// The s_instance store the single instance of PauseGame.
 /// 
 private static PauseGame s_instance = null;
 
 #endregion
 
 #region Properties
 
 /// 
 /// Return the instance of PauseGame.
 /// 
 /// 
 /// The instance.
 /// 
 public static PauseGame instance
 {
  get 
  {
   if ( !s_instance )
   {
    FindInstance();
   }
   return s_instance; 
  }
 }
 
 /// 
 /// Gets a value indicating whether the game is paused.
 /// 
 /// 
 /// true if paused; otherwise, false.
 /// 
 public static bool paused
 {
  get { return instance._paused; } 
 }
 
 #endregion
 
 #region Constructors
 
 /// 
 /// Initializes a new instance of the  class.
 /// 
 private PauseGame()
 {
  this._paused = false;
 }
 
 #endregion
 
 #region Static methods
 
 /// 
 /// Pause the game.
 /// 
 public static void Pause()
 {
  s_instance.PauseFunc();
 }
  
 /// 
 /// Resume the game.
 /// 
 public static void Resume()
 {
  s_instance.ResumeFunc();
 }
 
 /// 
 /// Finds the instance in the game.
 /// 
 private static void FindInstance()
 {
  s_instance = FindObjectOfType( typeof(PauseGame)) as PauseGame;
  
  if (!s_instance)
  {
   s_instance = null;
   throw( new UnityException("You need to add an active PauseGame script on a GameOjbect in your scene."));
  }

 }
 
 #endregion
 
 #region Private Methods
 
 /// 
 /// Awake this instance.
 /// 
 private void Awake()
 {
  FindInstance();
 } 
 
 /// 
 /// Pause the game and broadcast an OnPauseGame message to all GameObject of the game.
 /// 
 private void PauseFunc()
 {
  this._paused = true;
  
  Time.timeScale = 0;
  
  //Broadcasst the OnPauseGame event to all object in the game.
  Object[] objects = FindObjectsOfType (typeof(GameObject));
  
  foreach (GameObject go in objects) 
  {
   go.SendMessage (ON_PAUSE_GAME, SendMessageOptions.DontRequireReceiver);
  }
 }
 
 /// 
 /// Resume the game and broadcast an OnResumeGame message to all GameObject in the game.
 /// 
 private void ResumeFunc()
 {
  this._paused = false;
  
  Time.timeScale = 1;
  
  //Broadcasst the OnResumeGame event to all object in the game.
  Object[] objects = FindObjectsOfType (typeof(GameObject));
  
  foreach (GameObject go in objects) 
  {
   go.SendMessage (ON_RESUME_GAME, SendMessageOptions.DontRequireReceiver);
  }
 }
 
 /// 
 /// When Pause button is pressed, pause or resume the game
 /// depending on the actual state of the game.
 /// 
 private void Update ()
 {
  if ( Input.GetButtonUp( BUTTON_NAME ) )
  {
   if ( !this._paused )
   {
    this.Pause();
   }
   else
   {
    this.Resume();
   }
  }
 }
 
 #endregion
 
} //End class
PauseGame.cs by Mentalogicus is licensed under a Creative Commons Attribution 3.0 Unported License.

vendredi 18 mai 2012

Attention au thread dans Unity 3D.

Hier, j'ai passé une partie de la journée à chercher une faute dans un de mes codes. Le message d'erreur était le suivant: get_transform can only be called from the main thread. Constructors and field initializers will be executed from the loading thread when loading a scene. Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function. Après bien des recherches et des modifications inutiles, j'ai trouvé le coupable: System.Timers. Voici un code bien simple qui reproduit l'erreur ci-dessus:
using UnityEngine;
using System.Collections;
using System.Timers;

public class DemoTimer : MonoBehaviour
{
 private Timer _timer;
 
 void Start ()
 {
  this._timer = new Timer( 5 );
  this._timer.Elapsed += new ElapsedEventHandler( OnTimeEvent);
  this._timer.Start();
 }
 
 void OnTimeEvent( object sender, ElapsedEventArgs e )
 {
  //L'erreur se produit ici.
  this.transform.position = new Vector3(0,0,0); 
 }
  
}
Pourquoi ce code est fautif? Voici la raison. Unity utilise un seul thread. Un thread (http://fr.wikipedia.org/wiki/Thread_%28informatique%29) est un processus indépendant. Le timer crée un nouveau thread. Lorsque l'intervalle de temps est écoulé, la fonction OnTimeEvent est appelée dans le thread du timer. L'object this est créé dans le thread d'Unity. Lorsque j'écris
this.transform.position = new Vector3(0,0,0);
je tente d'accéder à un object qui vie dans un autre thread. Le thread timer ne sait pas quoi faire avec l'object this et me retourne une erreur. En règle générale, il est préférable de ne pas utiliser de thread avec Unity. L'engin de jeu Unity n'est pas thread safe. L'alternative à System.Timers dans Unity est d'utiliser une coroutine comme suit:
public class DemoTimer : MonoBehaviour
{
 void Start ()
 {
  StartCoroutine(this.OnTimeEvent()); 
 }
 
 IEnumerator OnTimeEvent()
 {
     yield return new WaitForSeconds(1);
  
  Debug.Log("Tic");
  this.transform.position = new Vector3(0,0,0);
 }
}

jeudi 10 mai 2012

Comment compiler un jeu PC dans Unity 3D?

Pour compiler un jeu dans Unity, sélectionner le menu File->Build Settings... ou File->Build. Aux fins de ce tutoriel, je clique sur Build Settings.
Le menu suivant apparaît:
L'encadré du haut offre la liste des scènes dans le projet. Le numéro à droite indique l'ordre des scènes. La scène la plus en haut est chargée en premier. Pour changer l'ordre des scènes, glissez la scène à l'endroit désiré. Les scènes doivent être cochées pour être ajoutées au jeu final.

L'encadré Platform indique la plate-forme sur lequel nous voulons que le jeu soit exécuté. Dans le cas présent, je choisis PC and Mac Standalone.

Avant de construire le jeu, cliquons sur le boutons Player Settings. Un menu apparaît dans l'inspecteur:
Ce menu permet d'ajuster avec plus de détails l'aspect visuel de l'exécutable du jeu, de sélectionner les tailles d'écran, si par défaut le jeu démarre en plein écran, etc. L'encadré Default Icon et le menu Icon change l'icône de votre exécutable. Donc, au lieu d'avoir l'icône d'Unity sur votre fichier .exe, vous aurez votre propre icône.
C'est tellement plus jolie et professionnel!
Le menu Splash Image vous permet d'ajouter une image qui va apparaître dans la fenêtre qui ouvre juste après avoir exécuté le jeu:
La fenêtre de démarrage d'un jeu en Unity.
Une fois que vous avez terminé avec les paramètres, appuyez sur Build ou Build and Run. Unity va demander l'emplacement de l'exécutable.

Important:
Unity crée un exécutable et un dossier de données. Dans mon exemple, j'utilise un projet nommé Space Junk. Lorsque je construis l'exécutable, je me retrouve avec le fichier Space Junk.exe et le dossier Space Junk_Data.
Si vous voulez distribuer votre jeu, vous devez fournir l'exécutable et le dossier de données. J'ai appris cette leçon à la dure. Je voulais montrer mon démo Space Junk à un ami. J'ai partagé l'exécutable sur Dropox. Dans ma tête, un exécutable contient le jeu en entier. Je pensais en terme d'installateur à l'époque. Unity ne crée par un installateur, mais un jeu avec ces données. Mon ami l’essaie et ne réussit pas à l'exécuter. J’essaie sur une machine différente et rien ne fonctionne. J'ai finalement découvert le problème en cherchant sur le Web.

lundi 7 mai 2012

Truc pour organiser le code C#

Le C# (et aussi le C++) offre une instruction de précompilation fort utile pour organiser le code: #region. Cette commande spécifie un bloc de code que vous pouvez réduire ou développer en cliquant sur le - ou + à la gauche de la page d'édition. Une région se termine par la commande #endregion. Voici un exemple dans MonoDevelop:
Cliquer sur l'image pour l'agrandir.
Une région Logical Operators a été créée avec les commandes #region et #endregion. Un petit symbole - entouré d'un carré est à la gauche de la commande #region et permet de réduire ce bloc de code. Une fois réduit, le code ressemble à ceci:
Cliquer sur l'image pour l'agrandir.
Les fonctions à l'intérieur de la région ont disparu et sont remplacées par une étiquette indiquant Logical Operators. MonoDevelop ajoute automatiquement dans le haut à droite une liste des régions définies par l'utilisateur.
La liste des régions.
En cliquant sur un élément de la liste, MonoDevelop place le curseur au début de la région. Très utile pour naviguer rapidement dans le code. De plus, MonoDevelop ajoute les régions dans le Document Outline et regroupe les fonctions de chaque région.

Il est possible d'imbriqués les régions. Par exemple, ajoutons une région Bidon qui entoure la région Logical Operators:
Cliquer sur l'image pour l'agrandir.
Deux icônes d'offuscation apparaissent comme attendu. Par contre, voici deux comportements inattendus et désagréables:
  1.  La liste des régions n'est pas imbriquée. La région Bidon apparaît après la région Logical Operators!
  2. Dans le Document Outline, la région Logical Operators a été remplacée par la région Bidon
MonoDevelop ne semble pas supporter complètement les régions imbriquées. Je ne sais pas quand le problème sera réglé. Si quelqu'un est au courant du développement de MonoDevelop et connaît la réponse, faites-le-moi savoir.

vendredi 4 mai 2012

Comment documenter les scripts dans Unity?

Les commentaires dans les scripts sont importants pour expliquer leur fonctionnement. L'utilisation des commentaires varie d'une école de pensée à une autre. Certains prônent l'utilisation massive de commentaires pour expliquer les intentions du code. D'autres croient que le code devrait être auto explicatif et libre de commentaires. Ma position sur le sujet est mitoyenne. J'utilise des noms de fonctions et de variables évocatrices, claires et indiquant leur fonction le plus souvent possible. Malgré tout, si je ne commente pas régulièrement, j'ai de la difficulté à saisir mon propre code après quelques mois. Alors, je documente.

L'ajout d'un commentaire se fait en ajoutant deux barres obliques "//":
//Un commentaire vraiment inutile.
string action = "Commente moi!"
ou pour un bloc de commentaires, avec "/*" et "*/":
/*
Il était une fois, dans l'ouest, une princesse prisonnière
dans une taverne miteuse gardée par des truands à la peau
tannée par le soleil et au regard sauvage, etc., etc., etc.
*/
string action = "Commente, mais commente égal!"
et aussi avec "///":
///Parce que // fait tellement 1990.
string action = "Commente moi!"

Dans MonoDevelop, si vous tapez "///" avant une entité (une variable, une fonction, une classe, etc.) vous obtenez:
/// 
/// Du XML???
/// 
private string action = "Commente moi!" 
MonoDevelop crée un commentaire avec un tag XML. Ce code XML est utilisé par un programme externe pour générer automatiquement la documentation. Les générateurs de documentation sont des programmes qui lisent le code source et les commentaires, l'analysent et retournent une documentation en format texte, HTML, XML, Latex, etc. qu'il est possible de consulter et d'effectuer des recherches. C'est très pratique pour se retrouver dans un gros projet avec des centaines de scripts. Mon générateur de documentation favori est Doxygen. Je vais parler de Doxygen dans un prochain billet.

L'utilisation du système de documentation avec l'XML amène un autre énorme avantage, en plus de la génération automatique de la documentation. Si vous glisser votre souris au-dessus d'une entité du code (une variable, une fonction, un enum, etc.), une info bulle jaune apparaît qui ressemble à ceci:

L'information de cet encadré n'est pas très utile. Nous apprenons que _selected est un booléen. Avec l'ajout d'un commentaire XML, voici le résultat:
Notre commentaire apparaît dans l'info bulle jaune! Cette description va apparaître si le curseur glisse au-dessus de la variable _selected quel que soit sa position dans le code. Si une variable publique est commentée, la description va aussi apparaître si la variable est utilisée dans une autre classe. Ce petit encadré peut sauver du temps. Imaginer que vous êtes dans le code. Vous travaillez sur une fonction, mais vous ne vous souvenez plus de l'intention d'une fonction ou de ce que stocke une variable. Normalement, vous naviguez dans le code, allez à la fonction ou à la variable, lisez la description et revenez à votre fonction. C'est long et inutile. Avec les commentaires XML, glisser le curseur sur la variable ou la fonction mystérieuse et voilà la description (si bien sur vous l'avez documentée).

Les commentaires en XML sont plus longs que les commentaires normaux, mais je recommande de les utiliser systématiquement pour permettre de générer la doc automatiquement et pour les info bulles. La longueur des commentaires n'est pas un problème réel avec les éditeurs modernes. Mono Develop permet de cacher les commentaires en appuyant sur le symbole +:
Avant.
Après.
Peu importe la longueur du commentaire, une fois refermé, il ne prendra qu'une ligne, vous permettant de lire le code sans obstruction.