Aujourd'hui, nous allons parler de la façon de stocker, recevoir et transmettre des données à l'intérieur du jeu. À propos de la chose merveilleuse appelée ScriptableObject, et pourquoi elle est merveilleuse. Parlons un peu des avantages des singletones lors de l'organisation des scènes et des transitions entre eux.
Cet article décrit un morceau d'une manière longue et douloureuse de développer un jeu, différentes approches utilisées dans le processus. Très probablement, il y aura beaucoup d'informations utiles pour les débutants et rien de nouveau pour les «vétérans».
Liens entre scripts et objets
La première question à laquelle un développeur débutant est confronté est de savoir comment lier toutes les classes écrites ensemble et configurer les interactions entre elles.
Le moyen le plus simple consiste à spécifier directement un lien vers la classe:
public class MyScript : MonoBehaviour { public OtherScript otherScript; }
Et puis - liez manuellement le script via l'inspecteur.
Cette approche présente au moins un inconvénient important - lorsque le nombre de scripts dépasse plusieurs dizaines et que chacun d'eux nécessite deux ou trois liens entre eux, le jeu se transforme rapidement en site Web. Un coup d'œil suffit pour lui causer des maux de tête.
Il vaut bien mieux (à mon avis) d'organiser un système de messages et d'abonnements, à l'intérieur duquel nos objets recevront les informations dont ils ont besoin - et seulement cela! - sans nécessiter une demi-douzaine de liens entre eux.
Cependant, après avoir exploré le sujet, j'ai découvert que les solutions toutes faites dans Unity sont réprimandées par tous ceux qui ne sont pas paresseux. Il m'a semblé une tâche non triviale d'écrire un tel système à partir de zéro, et donc je vais chercher des solutions plus simples.
Scriptableobject
Il y a essentiellement deux choses à savoir sur ScriptableObject:
- Ils font partie des fonctionnalités implémentées dans Unity, comme MonoBehaviour.
- Contrairement à MonoBehaviour, ils ne sont pas liés aux objets de scène, mais existent en tant qu'actifs distincts et sont capables de stocker et de transférer des données entre les sessions de jeu .
Je suis immédiatement tombé amoureux de leur amour brûlant. Ils sont en quelque sorte devenus ma panacée pour tout problème:
- Besoin de stocker les paramètres du jeu? ScriptableObject!
- Créer un inventaire? ScriptableObject!
- Écrire de l'IA? ScriptableObject!
- Enregistrer des informations sur un personnage, un ennemi, un objet? ScriptableObject ne vous laissera jamais tomber!
Sans y réfléchir à deux fois, j'ai créé plusieurs classes de type ScriptableObject, puis un référentiel pour elles:
public class Database: ScriptableObject { public PlayerData playerData; public GameSettings gameSettings; public SpellController spellController; }
Chacun d'entre eux stocke en lui-même toutes les informations utiles et, éventuellement, des liens vers d'autres objets. Chacun d'eux suffit pour passer une fois par l'inspecteur - ils n'iront nulle part ailleurs.
Maintenant, je n'ai pas besoin de spécifier un nombre infini de liens entre les scripts! Pour chaque script, je peux spécifier une
fois un lien vers mon référentiel - et il recevra toutes les informations de là.
Ainsi, le calcul de la vitesse du personnage prend un aspect très élégant:
Et si, disons, un piège ne devrait se déclencher que sur un personnage en cours d'exécution:
if (database.playerData.isSprinting) Activate();
De plus, le personnage n'a besoin de rien savoir des sorts ou des pièges. Il récupère simplement les données du stockage. Pas mal? Pas mal.
Mais presque immédiatement, je rencontre un problème. ScriptableOnjects ne peut pas stocker directement des liens vers des objets de scène. En d'autres termes, je ne peux pas créer de lien vers le joueur, le lier via l'inspecteur et oublier pour toujours la question des coordonnées du joueur.
Et si vous y réfléchissez, cela a du sens! Les actifs existent hors scène et sont accessibles dans toutes les scènes. Et que se passe-t-il si vous laissez un lien vers un objet situé dans une autre scène à l'intérieur de l'actif?
Rien de bien.
Pendant longtemps, une béquille a fonctionné pour moi: un lien public est créé dans le référentiel, puis chaque objet, le lien dont vous souhaitez vous souvenir, a rempli ce lien:
public class PlayerController : MonoBehaviour { void Awake() { database.playerData.player = this.gameObject; } }
Ainsi, quelle que soit la scène, mon référentiel reçoit tout d'abord un lien vers le joueur et s'en souvient. Désormais, quiconque, par exemple, l'ennemi ne doit pas stocker de lien vers le joueur, ne doit pas le rechercher via FindWithTag () (ce qui est un processus très gourmand en ressources). Il ne fait qu'accéder au dépôt:
public Database database; Vector3 destination; void Update () { destination = database.playerData.player.transform.position; }
Il semblerait: le système est parfait! Mais non. Nous avons encore 2 problèmes.
- Je dois encore spécifier manuellement un lien vers le référentiel pour chaque script.
- Il est difficile d'attribuer des références à des objets de scène à l'intérieur d'un ScriptableObject.
À propos de la seconde plus en détail. Supposons qu'un joueur ait un sort léger. Le joueur le lance et le jeu dit au magasin: la lumière est jetée!
database.spellController.light.CastSpell();
Et cela donne lieu à une série de réactions:
- Une nouvelle (ou ancienne) lumière d'objet de jeu est créée au point du curseur.
- Un module GUI est lancé, nous indiquant que la lumière est active.
- Les ennemis obtiennent, disons, un bonus temporaire pour trouver un joueur.
Comment faire tout ça?
Il est possible pour chaque objet intéressé par la lumière, directement dans Update () et d'écrire, disent-ils, de cette façon et que, chaque trame suive la lumière (si (database.spellController.light.isActive)), et quand elle s'allume - réagissez! Et ne vous souciez pas que 90% du temps ce contrôle fonctionnera au ralenti. Sur plusieurs centaines d'objets.
Ou organisez tout cela sous forme de liens préparés. Il s'avère que la simple fonction CastSpell () devrait avoir accès aux liens vers le joueur, la lumière et la liste des ennemis. Et c'est au mieux. Beaucoup de liens, hein?
Vous pouvez, bien sûr, enregistrer tout ce qui est important dans notre référentiel lorsque la scène démarre, disperser les liens sur les actifs, qui, en général, ne sont pas destinés à cela ... Mais encore une fois, je viole le principe d'un référentiel unique, en le transformant en un réseau de liens.
Singleton
C'est là que le singleton entre en jeu. En fait, c'est un objet qui existe (et ne peut exister) qu'en une seule copie.
public class GameController : MonoBehaviour { public static GameController Instance;
Je l'enclenche sur un objet de scène vide. Appelons cela GameController.
Ainsi, j'ai un objet dans la scène qui stocke
toutes les informations sur le jeu. De plus, il peut se déplacer entre les scènes, détruire ses doubles (s'il existe déjà un autre GameController sur la nouvelle scène), transférer des données entre les scènes et, si vous le souhaitez, implémenter l'enregistrement / le chargement du jeu.
De tous les scripts déjà écrits, vous pouvez supprimer le lien vers l'entrepôt de données. Après tout, je n'ai plus besoin de le configurer manuellement. Toutes les références aux objets de scène sont supprimées du magasin et transférées vers notre GameController (nous en aurons probablement besoin pour enregistrer l'état de la scène lorsque nous quitterons le jeu). Et puis je le remplis avec toutes les informations nécessaires d'une manière qui me convient. Par exemple, dans Awake () du joueur et des ennemis (et des objets importants de la scène), il est prescrit d'ajouter un lien vers eux-mêmes dans le GameController. Depuis que je travaille maintenant avec Monobehaviour, les références aux objets de la scène s'y insèrent très organiquement.
Qu'obtenons-nous?
Tout objet peut obtenir
toutes les informations dont il a besoin sur le jeu:
if (GameController.Instance.database.playerData.isSprinting) ActivateTrap();
Dans ce cas, il n'est absolument pas nécessaire de configurer des liens entre les objets, tout est stocké dans notre GameController.
Maintenant, il n'y aura rien de compliqué à maintenir l'état de la scène. Après tout, nous avons déjà toutes les informations nécessaires: ennemis, objets, position du joueur, entrepôt de données. Il suffit de sélectionner les informations sur la scène que vous souhaitez enregistrer et de les écrire dans un fichier à l'aide de FileStream lorsque vous quittez la scène.
Les dangers
Si vous lisez jusqu'à présent, je devrais vous mettre en garde contre les dangers de cette approche.
Une très mauvaise situation est lorsque de nombreux scripts font référence à une variable à l'intérieur de notre ScriptableObject. Il n'y a rien de mal à obtenir une valeur, mais quand ils commencent à affecter une variable à différents endroits, c'est une menace potentielle.
Si nous avons une variable playerSpeed enregistrée et que nous avons besoin que le lecteur se déplace à différentes vitesses, nous ne devons pas modifier playerSpeed dans le stockage, nous devons l'obtenir, l'enregistrer dans une variable temporaire et lui appliquer déjà des modificateurs de vitesse.
Le deuxième point - si un objet a accès à quelque chose - c'est une grande puissance. Et une grande responsabilité. Et vous devez l'approcher avec prudence afin qu'un script de champignon ne casse pas accidentellement tous vos ennemis IA. Une encapsulation correctement configurée réduira les risques, mais ne vous en épargnera pas.
N'oubliez pas non plus que les singletones sont des créatures douces. Ne les abusez pas.
C'est tout pour aujourd'hui.
Beaucoup de choses ont été glanées dans les tutoriels officiels d'Unity, certains dans des tutoriels non officiels. Je devais arriver à quelque chose moi-même. Ainsi, les approches ci-dessus peuvent avoir leurs dangers et leurs inconvénients, ce que j'ai raté.
Et donc - la discussion est la bienvenue!