Logo du site

Jeu Vidéo 3

Techniques d'intégration multimédia

Construction de Classes C#

Quelques bonnes habitudes

  • Évitez le préfixe this devant le nom des propriétés ou des appels de méthodes. Utiliser this partout, c'est plus lourd à l'écriture, et ce n'est pas nécessaire au bon fonctionnement d'une classe en C#.
  • Toujours utiliser le caractère de soulignement (underscore) devant le nom d'un champ (field). C'est un peu plus lourd pour l'écriture, mais cela prévient toute ambiguité quand à la nature de la variable concernée.
  • Nommer les constantes en majuscules uniquement: LARGEUR_TOTALE, NB_PAGES, VIE_MAX, etc...
  • Typez avec précision les variables et champs lors de leur déclaration (par exemple, si un int est suffisant, ne pas typer en float).
  • Commentez brièvement toutes les déclarations de champs.
  • Commentez toutes les méthodes, en particulier les méthodes publiques, en prenant la peine de préciser la nature des paramètres attendus, de la valeur retournée (void si il n'y en a pas) et des limites possibles de la méthode.
  • Ne rédiger qu'une seule classe par script. Ne pas créer plusieurs classes dans le même fichier: Même si c'est parfois possible, cela rend souvent le code moins flexible et plus difficile à maintenir.
  • Regroupez vos classes dans un dossier Scripts placés dans le dossier Assets votre projet.
  • Utilisez la notation "camelCase" pour les variables locales, les paramètres, les champs et les accesseurs / mutateurs (getter / setters).
  • Utilisez la notation "PascalCase" pour noms de classes et de fonctions.
  • Utiliser une classe GameManager comme classe de base dans un projet est un choix fréquent et recommandé.
  • Afin de respecter la notion d'encapsulation en POO, les champs sont pratiquement toujours définis comme privés. On n'y accède alors que par l'intermédiaire de fonctions spécialisées appelées accesseurs (getters) ou mutateurs (setters). Un champ ainsi "accessible" prend le nom de propriété. Au besoin, on pourra aussi utiliser [SerializeField] pour exposer un champ dans l'inspecteur de Unity.
  • Seules les méthodes qui peuvent être utilisables de l'extérieur de la classe sont publiques: Les autres sont définies comme privées.

Créer un objet (Instancier une classe)

Pour créer un objet (une instance issue de la classe):

NomDeClasse uneVariable = new NomDeClasse(param1, param2, etc.);

// Exemple de création d'un objet avec la classe Vector3 de Unity:
Vector3 monVecteur=new Vector3(1f,2f,0);

Namespace (Utilisation de classes issues d'un même "espace de nom")

Plusieurs fichiers de classes peuvent se trouver dans un même dossier. C'est particulièrement le cas pour les nombreuses classes prédéfines disponibles dans Unity. Au début du code d'une classe, il est nécessaire d'inclure des instructions qui indiquent quels "groupes de classes" sont requis pour ce script.

Par exemple, dans Unity les classes comportent à la base ces instructions:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

...qui font références à des dossiers contenant les fichiers des classes les plus utilisées lors de la confection d'un projet.

Cependant, dans certains cas vous devrez ajouter à cette liste les "namespaces" nécessaires pour utiliser certaines fonctionnalités particulières du langage. Voici 2 ajouts assez courants:

//Afin d'utiliser des instructions liées à l'interface usager (UI)
using UnityEngine.UI;

//Afin d'utiliser des instructions liées aux événements (pour un bouton par exemple)
using UnityEngine.EventSystems;

Créer une classe C#

Il faut d'abord créer un fichier .cs de script (dans Unity, clic de droit sur la fenêtre du dossier Scripts (Assets) >Create>C# Script). L'icône du script nouvellement créé devrait apparaître parmi les Assets dans la fenêtre de projet. En double-cliquant sur l'icône, le script devrait s'ouvrir dans votre éditeur de code et ressembler à ceci:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NomDeVotreClasse : MonoBehaviour
{
    void Start()
    {
       
    }

    void Update()
    {
        
    }
}

Initialisation d'un objet et constructeur de classe

En POO, le constructeur d'une classe porte toujours le nom de la classe. Il s'agit de la fonction qui est automatiquement appelée lorsque l'on instancie un objet à partir de la classe avec l'instruction new. En ce sens, c'est un genre de fonction d'initialisation.

Unity cependant change un peu cette approche, car les classes y sont le plus souvent associées à des GameObject qui sont instanciés dès leur entrée en scène. C'est donc le plus souvent Unity qui se charge d'instancier les classes, et c'est pourquoi les classes en Unity ne comportent habituellement pas de constructeur explicite (cela signifie qu'il n'y a pas de méthode portant le même nom que la classe dans la majorité des cas).

Les méthodes de base Awake() et Start() sont plutôt utilisées pour initialiser les objets au moment de leur instanciation.


Classe C# avec constructeur explicite

Il est cependant possible de créer votre propre classe C# plus classique, comportant un constructeur explicite. Une telle classe dans le contexte du développement en Unity est souvent utilisée en tant que classe utilitaire, au sein d'une autre classe, et est instanciée manuellement.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    void Start()
    {
        Contact[] mesContacts = new Contact[3];
		
        mesContacts[0] = new Contact("Bob", "111-2222");
        mesContacts[1] = new Contact("Yvette", "333-4444");
        mesContacts[2] = new Contact("Paul", "555-6666");

        mesContacts[0].Afficher(); // Affiche Nom: Bob, Tel: 111-2222
    }

    // Classe utilitaire Contact ======================================
	// Remarque: Cette classe est déclarée dans la classe Gamemanager.
	// On a donc ici une classe dans une classe...
    public class Contact{
        public string _nom;
        public string _telephone;
		
		// Constructeur "Classique" ------------------
        public Contact(string unNom, string unTelephone)
        {
            _nom = unNom;
            _telephone = unTelephone;
        }

        public void Afficher()
        {
            Debug.Log($"Nom: {_nom}, Tel: {_telephone}");
        }
    }
	//  ===============================================================

}

Créer des champs

Tous les champs d'une classe doivent être déclarés à l'extérieur des méthodes de cette classe. L'usage courant veut qu'on les déclare au tout début du code de la classe, avant le constructeur et que l'on commente chaque déclaration:

// Exemple de syntaxe:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Tresor : MonoBehaviour
{
	private string _description="Épée magique"; // description de ce trésor
	private Sprite _sprite; // le sprite qui représente ce trésor
	private int _valeur=100; // sa valeur en pièces d'or

    void Start()
    {

    }
}

Une autre pratique consiste à procéder à l'initialisation de l'extérieur du constructeur en définissant une méthode d'initialisation de l'objet:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Tresor : MonoBehaviour
{
	private string _description; //  description de ce trésor
	private Sprite _sprite; // le sprite qui représente ce trésor
	private int _valeur; // sa valeur en pièces d'or

    void Start()
    {

    }
	
	public void init(string description, Sprite sprite, int valeur){
		_description=description;
		_sprite=sprite;
		_valeur=valeur;
	}
}

Ainsi, il sera possible d'initialiser l'objet "de l'extérieur" de sa classe. Supposons qu'on a une référence à un objet Tresor dans la variable tresor1, ainsi que la référence à un autre Sprite dans la variable spriteCoffre on pourrait le modifier ainsi:

tresor1.init("Coffre du Mage",spriteCoffre,250);

Bien entendu, Unity offre aussi la possibilité d'exposer les champs dans l'interface, et ainsi permettre leur modification sans passer par la prog...

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Tresor : MonoBehaviour
{
	[SerializeField]
	private string _description; //  description de ce trésor
	
	[SerializeField]
	private Sprite _sprite; // le sprite qui représente ce trésor
	
	[SerializeField]
	private int _valeur; // sa valeur en pièces d'or

    void Start()
    {

    }
}

Champs publics ou privés

Si aucun modificateur d'accès n'est précisé, les champs d'une classe sont considérées d'usage privé par défaut. Le modificateur d'accès private signifie que le champ ne peut être accédé que par des méthodes appartenant à classe elle-même.

Afin de respecter plus facilement la notion d'encapsulation en POO, il est d'usage courant de rendre privé (private) l'accès aux champs la majorité du temps et de n'utiliser que des accesseurs ("getter") ou mutateurs ("setter") pour y accéder de l'extérieur (voir plus bas). Au besoin (souvent par paresse...), on pourra toujours définir un champ comme public (public): on y aura alors accès de l'extérieur de la classe, pour le meilleur... et pour le pire.

Dans Unity, le fait de rendre un champ public a aussi pour effet d'exposer ce champ dans l'interface. Il est cependant recommandé de conserver le modificateur private et de plutôt exposer les champs en utilisant l'instruction [SerializeField]:

[SerializeField]
private string _nom;

[SerializeField]
private int _age;

Remarque: En situation d'héritage, si on veut que la classe fille puisse utiliser un champ de sa classe mère, ce champ doit être déclaré protected au lieu de private.

Truc de pro: Il est recommandé de précèder le nom d'un champ d'un symbole de soulignement ("underscore") pour bien le repérer dans le code.

Constantes

Une constante est une variable particulière: sa valeur est prédéterminée et ne peut pas être modifiée pendant l'exécution du code. On utilise les constantes (privées ou publiques, statiques ou non) pour indiquer des valeurs ne devant pas varier dans un code.

Remarque: Une collection (telle qu'un Array) ne peut être const en C#, mais pour un champ, on obtient le même résultat en spécifiant utilisant le mot-clé readonly ("en lecture seulement").

Un exemple:

// code dans SystemeSolaire.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SystemeSolaire : MonoBehaviour
{
	// valeur de l'Unité Astronomique (UA)    
    private const float _UA:Number=149597870.691f; // ... distance Terre-Soleil, en km
	
	// les noms des planètes...
    private readonly string[] _PLANETES=new string[] {
		"Mercure","Vénus","Terre","Mars","Jupiter","Saturne","Neptune","Uranus"
	};
	
    void Start()
    {
		Debug.Log(_PLANETES[0]); // Affiche Mercure
		_PLANETES[0] = "Pluton"; //ERREUR! Impossible de modifier cette valeur (readonly!)
		
		Debug.Log(_UA); // Affiche 149597870.691
		_UA=2.5f; //ERREUR! Impossible de modifier cette valeur (constante!)
    }
}

Méthode (fonction d'une classe)

Les fonctions d'une classe portent le nom particulier de "méthodes". Tout comme les champs, elles peuvent aussi être privées ou publiques (voir plus bas). Voici la syntaxe de base pour une méthode:

// syntaxe de base pour une méthode publique  
public typeRetour NomDeMethode(type param1, type param2){
  // Placer le code de la méthode ici...
}

La valeur typeRetour dans cette syntaxe identifie le type de donnée retournée par la méthode (string, int, bool, GameObject, float, etc).

Exemple:

// Exemple 1(méthode publique retourne la somme des entiers d'un tableau):
public int Somme(int[] tableau){
  float total=0;
  for(int i=0;i < tableau.Length;i++){
    total+=tableau[i];
  } 
  return total;
} 

// Exemple 2: Si la méthode ne retourne rien (pas de return)
// on indique alors void comme type de retour.
public void saluer(string nom){
  Debug.Log("salut " + nom);
} 

Méthodes publiques ou privées

Les méthodes d'une classe sont considérées privées (private) par défaut, si aucun modificateur d'accès n'est précisé. Le modificateur d'accès private signifie que la méthode ne peut être accédée que par d'autres méthodes appartenant à la même classe.

On spécifie pour la vaste majorité des méthodes une restriction d'accès publique (public) ou privée (private), car ces 2 extrêmes sont plus faciles à gérer: Soit on donne accès libre à tous les appels venant d'autres classes (public), soit on n'autorise que les appels faits à partir de la classe qui détient la méthode.

Remarque: En situation d'héritage, si on veut que la classe fille puisse utiliser une méthode de sa classe mère, cette méthode doit être déclarée protected au lieu de private).

// Exemple des portées de méthodes publiques et privées:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Rond : MonoBehaviour
{  
    private float _rayon; // rayon de ce rond
	
    void Start()
    {
		_rayon=5f;
    }
	
	public void AfficherSurface(){
      Debug.Log($"La surface de ce cercle est: {Aire()}");
      // ici, on fait appel à la fonction privée Aire()
      // mais ça marche, car aire() fait partie ce de code
      // (elle appartient donc à l'objet lui-même et
      // il a le droit de l'utiliser... )
    }
	
	private float Aire(){
      float aireCercle = 3.1416f * _rayon * _rayon;
      return aireCercle;
    }
}

// Dans une autre classe, on teste notre objet rond 
// (on suppose ici qu'on possède une référence à lui dans la variable oRond):

oRond.AfficherSurface(); // Ça marche: Un message indiquant l'aire du rond s'affiche.

Debug.Log(oRond.Aire());// MESSAGE D'ERREUR!! 
// La méthode Aire() ne peut pas être accédée de l'extérieur de la classe Rond:
// Elle est privée...

Accesseur ("getter") et Mutateur ("setter")

Le rôle d'un accesseur ou d'un mutateur est de rendre possible l'accès à un champ normalement privé, mais de façon indirecte. Cela permet ensuite de programmer dans la classe des contrôles supplémentaires afin de protéger l'objet contre les changement potentiellement problématiques de valeurs de champs.

Afin d'illustrer le rôle des accesseurs et mutateurs, considérez la mise en situation suivante:

Vous avez réalisé une classe qui définit un être humain. Dans cette classe, un champ privé détermine son age (_age).

La valeur de ce champ peut fluctuer d'elle-même par des mécanismes déjà au sein de votre classe (une méthode vieillir() par exemple). Mais si vous souhaitez permettre aux programmeurs de régler eux même l'age d'un humain, votre code devra permettre l'accès à _age.

private int _age; // nom du champ AVEC underscore

public int age{ // nom de la propriété, SANS underscore
  get { return _age; }
  set { _age = value; }
}

// On fait appel à lui (de l'extérieur de la classe) de la façon suivante:

objetHumain.age=20; // on accède la propriété par son mutateur (setter)
Debug.Log(objetHumain.age); // on accède la propriété par son accesseur (getter)

Important: Par convention, un accesseur/mutateur (une propriété donc) possède un nom similaire à celui du champ qu'il permet d'accéder. Par exemple, l'accesseur du champ privé _pointage sera la propriété pointage. On voit ici l'intérêt de précéder les noms de champs d'un caractère de soulignement (underscore): cela permet de créer aisément des propriétés avec un nom presque identique au champ qu'elles accèdent (donc moins de confusion).

Il est cependant important d'assurer un contrôle sur la valeur reçue par notre setter: L'age ne peut pas dépasser 130 et il ne peut pas aller en dessous de 0 (sinon, l'humain en question n'est pas né!). Donc, voici une variante qui permet de modifier l'age tout en empêchant qu'il ne soit modifié au delà de ces limites. La valeur de la variable est donc protégée, et le fonctionnement de l'objet est plus sûr:

private int _age;

public int age{
  get { return _age; }
  set {
  	if(value<0 || value >120){
		Debug.LogError($"Valeur reçue: {value}: _age n'a pas été modifié");
	}else{
		_age=value;
	}
  }
}

// Il est possible d'accéder à l'age d'un individu, mais on ne peut le modifier
// de l'extérieur de la classe. C'est comme si le champ était en lecture seulement.

Héritage

Lorsqu'une classe hérite d'une autre classe, cela signifie que des champs et méthodes de la classe mère pourraient être utilisées par la classe fille. Il est alors possible de structurer la programmation en blocs logiques (regroupants des concepts semblables) en classes liées entre-elles de façon hiérarchique.

Cet exemple montre en plus l'utilisation de virtual. Une méthode possédant cette caractéristique peut être surchargée par une méthode portant le même nom dans une classe fille. Concrètement, cela signifie ici que pour l'orc la méthode Attaquer de sa propre classe "remplace" (override) pour lui la méthode Attaquer de la classe Personnage.

// Classe Personnage.cs

using UnityEngine;
using System.Collections;
 
public class Personnage : MonoBehaviour {
 	protected int _vie=1;
	
	public void PerdreVie(int degats)
	{
 		_vie-=degats;
		if(_vie<=0){
			Debug.Log("...mort");
		}else{
			Debug.Log("Ouch!");
		}
		// Tous les Personnages peuvent accéder
		// à cette méthode de base (et perdre des points de vies)
	}
 
	public virtual void Attaquer(Personnage cible)
	{
		// De base, un Personnage attaque à mains nues!
		Debug.Log("Un coup de poing!");
		cible.PerdreVie(1); // On fait 1 point de dégat...
		
		// Tous les Personnages peuvent accéder
		// à cette attaque de base
		// mais grâce à l'attribut virtual
		// elle peut être supplantée dans une
		// classe fille.
	 }
}

// Classe Orc.cs

using UnityEngine;
using System.Collections;
 
public class Orc : Personnage {
 	
	// Constructeur
	public Orc(){
		_vie=5;
	}
 
	public override void Attaquer(Personnage cible)
	{
		// L'orc attaque avec une épée!
		Debug.Log("Un coup d'épée!");
		cible.PerdreVie(3); // On fait 3 points de dégat...
		
		// Grâce à l'attribut override,
		// cette méthode vient supplanter la
		// méthode de même nom dans la classe mère
		// pour tous les orcs.
	 }
}

// Classe Mendiant.cs

using UnityEngine;
using System.Collections;
 
public class Mendiant : Personnage {
 	
	// Constructeur
	public Mendiant(){
		_vie=2;
	}
	
	// Le mendiant n'attaque qu'avec ses poings
	// (C'est l'attaque de base dans Personnage)
	// il n'a donc pas besoin de sa propre méthode Attaquer()
}


// Dans la classe GameManager.as on peut faire un petit combat:
void Start()
{
	Orc gurk = new Orc();
	Mendiant paul = new Mendiant();
	paul.Attaquer(gurk); // Paul va infliger 1 point de dégat à Gurk et il fera "Ouch!"
	gurk.Attaquer(paul); // Gurk va infliger 3 points de dégats et tuer Paul!
}