Techniques d'intégration multimédia
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);
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;
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()
{
}
}
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.
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}");
}
}
// ===============================================================
}
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()
{
}
}
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;
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.
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.
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!)
}
}
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);
}
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.
// 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...
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)
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.
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!
}