Améliorer le flux de travail des scènes avec les objets scriptables (ScriptableObjects)

La gestion de plusieurs scènes dans Unity peut être un défi, et l'amélioration de ce flux de travail est cruciale pour la performance de votre jeu et la productivité de votre équipe. Nous vous proposons ici quelques conseils pour mettre en place vos flux de travail Scene de manière à les adapter à des projets plus importants.
La plupart des jeux comportent plusieurs niveaux, et les niveaux contiennent souvent plus d'une scène. Dans les jeux où les scènes sont relativement petites, vous pouvez les diviser en différentes sections à l'aide de préfabriqués. Cependant, pour les activer ou les instancier pendant le jeu, vous devez faire référence à tous ces préfabriqués. Cela signifie qu'au fur et à mesure que votre jeu prend de l'ampleur et que ces références occupent plus d'espace dans la mémoire, il devient plus efficace d'utiliser les scènes.
Vous pouvez décomposer vos niveaux en une ou plusieurs scènes Unity. Il est donc essentiel de trouver le meilleur moyen de les gérer tous. Vous pouvez ouvrir plusieurs scènes dans l'éditeur et au moment de l'exécution en utilisant l'édition multi-scène. La division des niveaux en plusieurs scènes a également l'avantage de faciliter le travail d'équipe en évitant les conflits de fusion dans les outils de collaboration tels que Git, SVN, Unity Collaborate, etc.
Dans la vidéo ci-dessous, nous montrons comment charger un niveau plus efficacement en décomposant la logique du jeu et les différentes parties du niveau en plusieurs scènes Unity distinctes. Ensuite, en utilisant le mode de chargement de scène additif lors du chargement de ces scènes, nous chargeons et déchargeons les parties nécessaires en même temps que la logique du jeu, qui est persistante. Nous utilisons des préfabriqués comme "ancres" pour les scènes, ce qui offre également une grande flexibilité lorsque l'on travaille en équipe, car chaque scène représente une partie du niveau et peut être éditée séparément.
Vous pouvez toujours charger ces scènes en mode édition et appuyer sur Play à tout moment, afin de les visualiser toutes ensemble lors de la création du level design.
Nous présentons deux méthodes différentes pour charger ces scènes. La première est basée sur la distance, ce qui est bien adapté aux niveaux non intérieurs comme un monde ouvert. Cette technique est également utile pour certains effets visuels (comme le brouillard, par exemple) afin de masquer le processus de chargement et de déchargement.
La seconde technique utilise un déclencheur pour vérifier quelles scènes doivent être chargées, ce qui est plus efficace lorsque l'on travaille avec des intérieurs.
Maintenant que tout est géré à l'intérieur du niveau, vous pouvez ajouter une couche au-dessus pour mieux gérer les niveaux.
Nous voulons garder une trace des différentes scènes de chaque niveau ainsi que de tous les niveaux pendant toute la durée du jeu. Une façon possible de le faire est d'utiliser des variables statiques et le modèle singleton dans vos scripts MonoBehaviour, mais il y a quelques problèmes avec cette solution. L'utilisation du modèle singleton permet d'établir des connexions rigides entre vos systèmes, et n'est donc pas strictement modulaire. Les systèmes ne peuvent exister séparément et dépendront toujours l'un de l'autre.
Un autre problème concerne l'utilisation de variables statiques. Comme vous ne pouvez pas les voir dans l'inspecteur, vous devez modifier le code pour les définir, ce qui complique la tâche des artistes ou des concepteurs de niveaux qui souhaitent tester le jeu facilement. Lorsque des données doivent être partagées entre les différentes scènes, vous utilisez des variables statiques combinées à DontDestroyOnLoad, mais ce dernier doit être évité dans la mesure du possible.
Pour stocker des informations sur les différentes scènes, vous pouvez utiliser ScriptableObject, une classe sérialisable principalement utilisée pour stocker des données. Contrairement aux scripts MonoBehaviour, qui sont utilisés comme composants attachés aux GameObjects, les ScriptableObjects ne sont attachés à aucun GameObject et peuvent donc être partagés entre les différentes Scènes de l'ensemble du projet.
Vous voulez pouvoir utiliser cette structure pour les niveaux, mais aussi pour les scènes du menu de votre jeu. Pour ce faire, créez une classe GameScene qui contient les différentes propriétés communes entre les niveaux et les menus.
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Remarquez que la classe hérite de ScriptableObject et non de MonoBehaviour. Vous pouvez ajouter autant de propriétés que nécessaire pour votre jeu. Après cette étape, vous pouvez créer des classes Level et Menu qui héritent toutes deux de la classe GameScene qui vient d'être créée - ce sont donc également des objets scriptables.
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
L'ajout de l'attribut CreateAssetMenu en haut de la page permet de créer un nouveau niveau à partir du menu Assets dans Unity. Vous pouvez faire de même pour la classe Menu. Vous pouvez également inclure une énumération pour pouvoir choisir le type de menu dans l'inspecteur.
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Maintenant que vous pouvez créer des niveaux et des menus, ajoutons une base de données qui répertorie les niveaux et les menus pour en faciliter la consultation. Vous pouvez également ajouter un index pour suivre le niveau actuel du joueur. Ensuite, vous pouvez ajouter des méthodes pour charger un nouveau jeu (dans ce cas, le premier niveau sera chargé), pour rejouer le niveau actuel et pour passer au niveau suivant. Notez que seul l'index change entre ces trois méthodes, vous pouvez donc créer une méthode qui charge le niveau avec un index pour l'utiliser plusieurs fois.
Type de bloc inconnu "codeBlock", veuillez spécifier un sérialiseur pour ce type dans la propriété `serializers.types`.
Il existe également des méthodes pour les menus, et vous pouvez utiliser le type d'énumération que vous avez créé précédemment pour charger le menu spécifique que vous souhaitez - assurez-vous simplement que l'ordre dans l'énumération et l'ordre dans la liste des menus est le même.
Vous pouvez enfin créer un ScriptableObject de niveau, de menu ou de base de données à partir du menu Assets en cliquant avec le bouton droit de la souris dans la fenêtre de projet.

À partir de là, il vous suffit d'ajouter les niveaux et les menus dont vous avez besoin, d'ajuster les paramètres, puis de les ajouter à la base de données Scènes. L'exemple ci-dessous montre à quoi ressemblent les données relatives au niveau 1, au menu principal et aux scènes.

Il est temps d'appeler ces méthodes. Dans cet exemple, le bouton Next Level de l'interface utilisateur (UI) qui apparaît lorsque le joueur atteint la fin du niveau appelle la méthode NextLevel. Pour attacher la méthode au bouton, cliquez sur le bouton plus de l'événement On Click du composant Button pour ajouter un nouvel événement, puis faites glisser l'objet scriptable Scenes Data dans le champ de l'objet et choisissez la méthode NextLevel de ScenesData, comme illustré ci-dessous.

Vous pouvez maintenant procéder de la même manière pour les autres boutons - pour rejouer le niveau ou aller au menu principal, etc. Vous pouvez également référencer le ScriptableObject à partir de n'importe quel autre script pour accéder aux différentes propriétés, comme l'AudioClip pour la musique de fond ou le profil de post-traitement, et les utiliser dans le niveau.
- Minimiser le chargement/déchargement
Dans le script ScenePartLoader présenté dans la vidéo, vous pouvez voir qu'un joueur peut entrer et sortir du collisionneur plusieurs fois, ce qui déclenche le chargement et le déchargement répétés d'une scène. Pour éviter cela, vous pouvez ajouter une coroutine avant d'appeler les méthodes de chargement et de déchargement de la scène dans le script, et arrêter la coroutine si le joueur quitte le trigger.
- Conventions d'appellation
Un autre conseil général est d'utiliser des conventions de dénomination solides dans le projet. L'équipe doit se mettre d'accord au préalable sur la manière de nommer les différents types de ressources - des scripts et des scènes aux matériaux et autres éléments du projet. Cela facilitera non seulement pour vous, mais aussi pour vos coéquipiers, le travail sur le projet et sa maintenance. C'est toujours une bonne idée, mais dans ce cas particulier, c'est crucial pour la gestion de la scène avec les objets scriptables. Notre exemple utilise une approche directe basée sur le nom de la scène, mais il existe de nombreuses solutions différentes qui s'appuient moins sur le nom de la scène. Il est préférable d'éviter l'approche basée sur les chaînes de caractères car si vous renommez une scène Unity dans un contexte donné, cette scène ne se chargera pas dans une autre partie du jeu.
- Outillage sur mesure
Une façon d'éviter la dépendance de nom à l'échelle du jeu est de configurer votre script pour qu'il fasse référence aux scènes en tant que type d'objet. Cela vous permet de glisser-déposer un élément de scène dans un inspecteur et d'obtenir son nom en toute sécurité dans un script. Cependant, comme il s'agit d'une classe Editor, vous n'avez pas accès à la classe AssetDatabase au moment de l'exécution. Vous devez donc combiner les deux types de données pour obtenir une solution qui fonctionne dans l'Editor, qui évite les erreurs humaines et qui fonctionne toujours au moment de l'exécution. Vous pouvez vous référer à l'interface ISerializationCallbackReceiver pour un exemple de mise en œuvre d'un objet qui, lors de la sérialisation, peut extraire le chemin de la chaîne de caractères de l'élément de la scène et le stocker pour l'utiliser au moment de l'exécution.
En outre, vous pouvez également créer un inspecteur personnalisé pour faciliter l'ajout rapide de scènes aux paramètres de construction à l'aide de boutons, au lieu de devoir les ajouter manuellement via ce menu et de devoir les synchroniser.
À titre d'exemple de ce type d'outil, jetez un coup d'œil à cette excellente application open source réalisée par le développeur JohannesMP (il ne s'agit pas d'une ressource officielle de Unity).
Ce post montre juste une façon dont les ScriptableObjects peuvent améliorer votre flux de travail lorsque vous travaillez avec des Scènes multiples combinées avec des Prefabs. La gestion des scènes varie considérablement d'un jeu à l'autre - il n'existe pas de solution unique applicable à toutes les structures de jeu. Il est tout à fait judicieux de mettre en place votre propre outil personnalisé pour s'adapter à l'organisation de votre projet.
Nous espérons que ces informations vous aideront dans votre projet ou qu'elles vous inciteront à créer vos propres outils de gestion des scènes.
Si vous avez des questions, n'hésitez pas à nous en faire part dans les commentaires. Nous aimerions connaître les méthodes que vous utilisez pour gérer les scènes de votre jeu. Et n'hésitez pas à nous suggérer d'autres cas d'utilisation que vous souhaiteriez que nous traitions dans de futurs articles de blog.