Merci à Vincent Quarles pour avoir aidé à avoir aidé les pairs à réviser cet article.
Dans ce tutoriel, nous terminerons la mise en œuvre de la fonctionnalité de sauvegarde et de charge dans notre jeu. Dans le tutoriel précédent sur l'enregistrement et le chargement des données du jeu des joueurs dans Unity, nous avons réussi à enregistrer et à charger des données liées aux joueurs telles que les statistiques et les stocks, mais nous allons maintenant aborder la partie la plus difficile - les objets mondiaux. Le système final devrait rappeler les jeux de défilement des aînés - chaque objet enregistré exactement là où il était, pour une durée indéfinie.
Si vous avez besoin d'un projet pour vous entraîner, voici une version du projet que nous avons réalisé dans le dernier tutoriel. Il a été mis à niveau avec une paire d'objets interactables en jeu qui engendrent des objets - une potion et une épée. Ils peuvent être engendrés et ramassés
( Déshawned ), et nous devons enregistrer et charger correctement leur état. Une version finie du projet (avec Save System entièrement implémenté) peut être trouvée au bas de cet article.
PROJET PAGE GITHUB
Project Zip Download
Nous devons décomposer le système de sauvegarde et de chargement d'objets avant de le mettre en œuvre. D'abord et avant tout, nous avons besoin d'une sorte d'objet Master de niveau
qui apparaîtra et découragera les objets. Il doit engendrer des objets enregistrés dans le niveau (si nous chargeons le niveau et ne recommencez pas), Despawn a ramassé des objets, informer les objets qu'ils ont besoin de se sauver et de gérer les listes d'objets. Cela semble beaucoup, alors mettons cela dans un organigramme:
Fondamentalement, l'intégralité de la logique enregistre une liste d'objets à un disque dur - qui, la prochaine fois que le niveau sera chargé, sera traversé, et tous les objets de celui-ci ont été engendrés au fur et à mesure que nous commençons à jouer. Cela semble facile, mais le diable est dans les détails: comment savons-nous quels objets sauver et comment les engendrer-nous?
Dans l'article précédent, j'ai mentionné que nous utiliserons un système d'événement délégué pour avoir notifié les objets qu'ils doivent se sauver. Expliquons d'abord ce que sont les délégués et les événements.
Vous pouvez lire la documentation officielle du délégué et la documentation des événements officiels. Mais ne vous inquiétez pas: même moi, je ne comprends pas beaucoup de technologie dans la documentation officielle, donc je vais le mettre en anglais simple:
Vous pouvez considérer un délégué comme un plan de fonction. Il décrit à quoi ressemble une fonction: quel est son type de retour et quels arguments il accepte. Par exemple:
public delegate void SaveDelegate(object sender, EventArgs args);
Ce délégué décrit une fonction qui ne renvoie rien (void) et accepte deux arguments standard pour un framework .net / mono: un objet générique qui représente un expéditeur de l'événement et des arguments éventuels que vous pouvez utiliser pour transmettre diverses données. Vous n'avez pas vraiment à vous soucier de cela, nous pouvons simplement passer (null, null) comme arguments, mais ils doivent être là.
Alors, comment cela est-il lié aux événements?
Vous pouvez considérer un événement comme une boîte de fonctions
. Il accepte uniquement les fonctions qui correspondent au délégué (un plan), et vous pouvez en mettre et en supprimer des fonctions à l'exécution comme vous le souhaitez.
Ensuite, à tout moment, vous pouvez déclencher un événement, ce qui signifie exécuter toutes les fonctions qui sont actuellement dans la case - à la fois. Considérez la déclaration d'événement suivante:
public event SaveDelegate SaveEvent;
Cette syntaxe dit: Déclarez un événement public (n'importe qui peut s'y abonner - nous y arriverons plus tard), qui accepte les fonctions comme décrit par le délégué Savedelegate (voir ci-dessus), et il s'appelle SaveEvent.
L'abonnement à un événement signifie essentiellement «mettre une fonction dans la boîte». La syntaxe est assez simple. Supposons que notre événement soit déclaré dans notre classe GlobalObject bien connue et que nous avons une classe d'objets de potion nommée potiondroppable
qui doit s'abonner à un événement:
//In PotionDroppable's Start or Awake function: GlobalObject.Instance.SaveEvent += SaveFunction; //In PotionDroppable's OnDestroy() function: GlobalObject.Instance.SaveEvent -= SaveFunction; //[...] public void SaveFunction (object sender, EventArgs args) { //Here is code that saves this instance of an object. }
Expliquons la syntaxe ici. Nous devons d'abord avoir une fonction conforme aux normes déléguées décrites. Dans le script de l'objet, il existe une telle fonction nommée SaveFunction. Nous allons écrire le nôtre plus tard, mais pour l'instant supposons simplement qu'il s'agit d'une fonction de travail pour enregistrer l'objet sur un disque dur afin qu'il puisse être chargé plus tard.
Lorsque nous avons cela, nous mettons simplement cette fonction dans la case au début ou au réveil du script et en le supprimant lorsqu'elle est détruite. (Le désabonnement est assez important: si vous essayez d'appeler une fonction d'un objet détruit, vous obtiendrez des exceptions de référence nuls au moment de l'exécution). Nous le faisons en accédant à l'objet d'événement déclaré et en utilisant un opérateur d'addition suivi du nom de la fonction.
Remarque: nous n'appelons pas une fonction à l'aide de supports ou d'arguments; Nous utilisons simplement le nom de la fonction, rien d'autre.
Alors, expliquons ce que tout cela fait finalement dans un exemple de flux du jeu.
Supposons que, à travers le flux du jeu, les actions du joueur ont engendré deux épées et deux potions au monde (par exemple, le joueur a ouvert un coffre avec un butin).
Ces quatre objets enregistrent leurs fonctions dans l'événement de sauvegarde:
Maintenant, supposons que le joueur ramasse une épée et une potion du monde. Comme les objets sont «ramassés», ils déclenchent efficacement un changement dans l'inventaire du joueur puis se détruisent (en quelque sorte ruine la magie, je sais):
Et puis, supposons que le joueur décide de sauver le jeu - peut-être qu'ils ont vraiment besoin de répondre à ce téléphone qui a sonné trois fois maintenant (hé, vous avez fait un excellent jeu):
Fondamentalement, ce qui se passe, c'est que les fonctions de la boîte sont déclenchées une par une, et le jeu ne va nulle part tant que toutes les fonctions ne sont pas remplies. Chaque objet qui «se sauve» est essentiellement à l'écriture dans une liste - qui, sur la prochaine charge de jeu, sera inspectée par un objet Master Level et tous les objets qui sont dans la liste seront instanciés (engendrés). C'est tout, vraiment: si vous avez suivi l'article jusqu'à présent, vous êtes essentiellement prêt à commencer à l'implémenter immédiatement. Néanmoins, nous allons à quelques exemples de code concrètes ici, et comme toujours, il y aura un projet fini qui vous attendra à la fin de l'article si vous souhaitez voir à quoi il est censé ressembler.
passons d'abord en revue le projet existant et nous familiariser avec ce qui est déjà à l'intérieur.
Comme vous pouvez le voir, nous avons ces boîtes magnifiquement conçues qui agissent comme des frappeurs pour les deux objets que nous avons déjà mentionnés. Il y a une paire dans chacune des deux scènes. Vous pouvez immédiatement voir le problème: si vous transitionnez la scène ou utilisez f5 / f9 pour enregistrer / charger, les objets engendrés disparaîtront.
Les pontements de boîte et les objets engendrés eux-mêmes utilisent un mécanisme d'interface interactable simple qui nous offre la possibilité de reconnaître un raycast à ces objets, d'écrire du texte à l'écran et d'interagir avec eux en utilisant la clé [E].
pas grand-chose plus est présent. Nos tâches ici sont:
Comme vous pouvez le voir, ce n'est pas aussi trivial que l'on pourrait espérer qu'une telle fonction fondamentale serait. En fait, aucun moteur de jeu existant là-bas (CryEngine, UDK, Unreal Engine 4, autres) a vraiment une simple implémentation de fonction de sauvegarde / charge prête à partir. En effet, comme vous pouvez l'imaginer, les mécanismes de sauvegarde sont vraiment spécifiques à chaque jeu. Il y a plus de classes d'objets qui nécessitent généralement des économies; Ce sont des États mondiaux tels que des quêtes terminées / actives, une convivialité / hostilité de faction, même des conditions météorologiques actuelles dans certains jeux. Il devient assez complexe, mais avec la bonne base de la mécanique de sauvegarde, il est facile de le mettre à niveau avec plus de fonctionnalités.
Commençons d'abord avec les choses faciles - les listes d'objets. Les données de notre joueur sont enregistrées et chargées via une représentation simple des données dans la classe de sérialisables.
De la même manière, nous avons besoin de classes sérialisables qui représenteront nos objets. Pour les écrire, nous devons savoir quelles propriétés nous devons économiser - pour notre joueur, nous avions beaucoup de choses à économiser. Heureusement, pour les objets, vous aurez rarement besoin de plus que leur position mondiale. Dans notre exemple, nous devons seulement enregistrer la position des objets et rien d'autre.
Pour bien structurer notre code, nous commencerons par des classes simples à la fin de nos sérialisables:
public delegate void SaveDelegate(object sender, EventArgs args);
Vous vous demandez peut-être pourquoi nous n'utilisons pas simplement une classe de base. La réponse est que nous le pouvons, mais vous ne savez jamais vraiment quand vous devez ajouter ou modifier des propriétés spécifiques de l'élément qui doivent être enregistrées. Et en plus, cela est beaucoup plus facile pour la lisibilité au code.
MAINTENANT, vous avez peut-être remarqué que j'utilise beaucoup le terme droppable
. En effet, nous devons faire la différence entre les objets droppable (Spawnable) et les objets placables, qui suivent différentes règles d'économie et de frai. Nous y reviendrons plus tard.
Maintenant, contrairement aux données du joueur où nous savons qu'il n'y a vraiment qu'un seul joueur à un moment donné, nous pouvons avoir plusieurs objets comme les potions. Nous devons faire une liste dynamique et désigner quelle scène appartient cette liste: nous ne pouvons pas engendrer les objets de niveau2 dans le niveau1. Ceci est simple à faire, encore une fois dans les sérialisables. Écrivez ceci ci-dessous notre dernière classe:
public delegate void SaveDelegate(object sender, EventArgs args);
Le meilleur endroit pour faire des instances de ces listes serait notre classe GlobalControl:
public event SaveDelegate SaveEvent;
Nos listes sont à peu près bonnes à faire pour le moment: nous y accéderons à partir d'un objet de niveau de niveau plus tard lorsque nous devons engendrer des éléments et nous sauvegarder / charger à partir du disque dur de GlobalControl, comme nous le faisons déjà avec les données des joueurs .
Ah, enfin. Implémentons les trucs de l'événement célèbre.
dans GlobalControl:
//In PotionDroppable's Start or Awake function: GlobalObject.Instance.SaveEvent += SaveFunction; //In PotionDroppable's OnDestroy() function: GlobalObject.Instance.SaveEvent -= SaveFunction; //[...] public void SaveFunction (object sender, EventArgs args) { //Here is code that saves this instance of an object. }
Comme vous pouvez le voir, nous faisons de l'événement une référence statique, il est donc plus logique et plus facile de travailler avec plus tard.
Une dernière note concernant la mise en œuvre de l'événement: seule la classe qui contient la déclaration d'événements peut licencier un événement. Tout le monde peut s'y abonner en accédant à GlobalControl.SaveEvent = ..., mais seule la classe GlobalControl peut le tirer en utilisant SaveEvent (null, null);. Tenter d'utiliser GlobalControl.SaveEvent (null, null); d'ailleurs entraînera une erreur du compilateur!
Et c'est tout pour la mise en œuvre de l'événement! Abonnez-vous quelques trucs!
Maintenant que nous avons notre événement, nos objets doivent s'y abonner, ou, en d'autres termes, commencer à écouter
un événement et réagir quand il tire.
Nous avons besoin d'une fonction qui s'exécutera lorsqu'un événement tirera - pour chaque objet. Passons au script PotionDroppable dans le dossier Pickups
. Remarque: Sword n'a pas encore son script configuré; Nous allons le faire dans un instant!
Dans PotionDroppable, ajoutez ceci:
[Serializable] public class SavedDroppablePotion { public float PositionX, PositionY, PositionZ; } [Serializable] public class SavedDroppableSword { public float PositionX, PositionY, PositionZ; }
[Serializable] public class SavedDroppableList { public int SceneID; public List<SavedDroppablePotion> SavedPotions; public List<SavedDroppableSword> SavedSword; public SavedDroppableList(int newSceneID) { this.SceneID = newSceneID; this.SavedPotions = new List<SavedDroppablePotion>(); this.SavedSword = new List<SavedDroppableSword>(); } }
public List<SavedDroppableList> SavedLists = new List<SavedDroppableList>();
public delegate void SaveDelegate(object sender, EventArgs args); public static event SaveDelegate SaveEvent;
C'est là que tout notre revêtement de sucre syntaxique brille vraiment. C'est très lisible, facile à comprendre et facile à changer pour vos propres besoins lorsque vous en avez besoin! En bref, nous créons une nouvelle représentation de «potion» et les enregistrons dans la liste réelle.
Tout d'abord, un petit peu de préparation. Dans notre projet existant, nous avons une variable globale qui nous indique si la scène est chargée. Mais nous n'avons pas une telle variable pour nous dire si la scène est en cours de transition en utilisant la porte. Nous nous attendons à ce que tous les objets abandonnés soient toujours là lorsque nous reviendrons dans la salle précédente, même si nous n'avons sauvé / chargé de notre jeu entre les deux.
Pour ce faire, nous devons apporter un petit changement au contrôle global:
public delegate void SaveDelegate(object sender, EventArgs args);
public event SaveDelegate SaveEvent;
Maintenant, nous avons seulement besoin de lire les listes et de repousser les objets d'eux lorsque nous chargeons un jeu. C'est ce que fera l'objet maître de niveau. Créons un nouveau script et appelons-le
de niveau de niveau :
//In PotionDroppable's Start or Awake function: GlobalObject.Instance.SaveEvent += SaveFunction; //In PotionDroppable's OnDestroy() function: GlobalObject.Instance.SaveEvent -= SaveFunction; //[...] public void SaveFunction (object sender, EventArgs args) { //Here is code that saves this instance of an object. }
Le code s'exécute uniquement au début, que nous utilisons pour initialiser les listes enregistrées dans GlobalControl si nécessaire. Ensuite, nous demandons au GlobalControl si nous chargeons ou transitons une scène. Si nous commençons la scène à nouveau (comme un nouveau jeu ou autres), cela n'a pas d'importance - nous n'apparaisons aucun objet.
Si nous
sommes Chargement d'une scène, nous devons récupérer notre copie locale de la liste des objets enregistrés (juste pour économiser un peu de performances sur l'accès répété de GlobalControl, et pour rendre la syntaxe plus lisible).
Ensuite, nous traversons simplement la liste et engendrer tous les objets de potion à l'intérieur. La syntaxe exacte pour le frai est essentiellement l'une des surcharges de méthode instanciées. Nous devons lancer le résultat de la méthode instancielle dans GameObject (pour une raison quelconque, le type de retour par défaut est un objet simple) afin que nous puissions accéder à sa transformation et modifier sa position.
Nous devons mettre notre maître de niveau dans chaque scène et y attribuer les préfabriques valides:
[Serializable] public class SavedDroppablePotion { public float PositionX, PositionY, PositionZ; } [Serializable] public class SavedDroppableSword { public float PositionX, PositionY, PositionZ; }
Test initial
f5 et f9 , ou franchissez la porte (et toute combinaison de la combinaison de tel).
Cependant, il y a un autre problème à résoudre: nous n'enregistrons pas les épées.Il s'agit simplement de démontrer comment ajouter Nouveaux objets sauvages à votre projet une fois que vous avez des fondations similaires construites.
Alors, disons que vous avez déjà un nouveau système de création d'objets en place - comme nous le faisons déjà avec les objets Sword. Ils ne sont actuellement pas beaucoup interactables (au-delà de la physique de base), nous devons donc écrire un script similaire à celui de la potion qui nous permettra de «ramasser» une épée et de le faire enregistrer correctement.
Le préfabriqué de l'épée qui est actuellement engendré peut être trouvé dans le dossier Assets> Prefabs.
faisons que cela fonctionne. Accédez à Assets> Scripts> Pickups et vous verrez Script PotionDroppable
. À côté, créez un nouveau script SwordDroppable:
public delegate void SaveDelegate(object sender, EventArgs args);
N'oubliez pas l'implémentation de l'interface «interactable». C'est très important: sans elle, votre épée ne sera pas reconnue par la caméra Raycast et restera ininteractive. En outre, vérifiez que le préfabriqué d'épée appartient à la couche d'articles. Sinon, il sera à nouveau ignoré par le Raycast. Ajoutez maintenant ce script au premier enfant de l'épée préfabriale (qui a en fait le rendu de maillage et d'autres composants):
Maintenant, nous devons les engendrer. Dans le niveau maître, sous notre boucle pour engendre les potions:
public event SaveDelegate SaveEvent;
… et c'est tout. Chaque fois que vous avez besoin d'un nouveau type d'article enregistré:
Pour l'instant, le système est assez brut: il y a plusieurs fichiers de sauvegarde sur le disque dur, la rotation de l'objet n'est pas enregistrée (épées, joueur, etc.) et logique pour positionner un joueur pendant les transitions de scène (mais pas Chargement) est un peu excentrique.
Ce sont maintenant des problèmes mineurs à résoudre une fois le système solide en place, et je vous invite à essayer de bricoler avec ce tutoriel et un projet fini pour voir si vous pouvez améliorer le système.
mais même si c'est le cas, c'est déjà une méthode assez fiable et solide pour faire des mécanismes de sauvegarde / chargement dans votre jeu - même si cela peut différer de ces projets Exemple
.
Comme promis, voici le projet fini, si vous en avez besoin pour référence, ou parce que vous êtes resté coincé quelque part. Le système de sauvegarde est mis en œuvre conformément aux instructions de ce tutoriel et avec le même schéma de dénomination.
La meilleure façon d'implémenter un système de sauvegarde dans Unity 5 est d'utiliser la classe PlayerPrefs. PlayerPrefs est un moyen simple de stocker et de récupérer des données entre les séances de jeu. Il vous permet d'enregistrer et de charger des données sous forme d'entiers, de flotteurs et de chaînes. Cependant, il est important de noter que PlayerPrefs n'est pas sécurisé et ne doit pas être utilisé pour des données sensibles. Pour des données plus complexes ou sécurisées, vous voudrez peut-être envisager d'utiliser un formateur binaire ou un sérialiseur JSON.
Enregistrer les états de jeu et les paramètres du jeu Dans Unity, 5 peut être réalisé à l'aide de PlayerPrefs, de sérialisation JSON ou de formatage binaire. PlayerPrefs est la méthode la plus simple, vous permettant d'économiser et de charger des entiers, des flotteurs et des chaînes. La sérialisation JSON est un peu plus complexe mais permet plus de flexibilité et de sécurité. Le formatage binaire est la méthode la plus sécurisée mais aussi la plus complexe.
L'ordre d'exécution dans Unity 5 détermine la séquence dans quels scripts sont exécutés. Cela peut avoir un impact significatif sur les fonctionnalités de sauvegarde et de charge. Par exemple, si un script qui charge les données est exécuté avant le script qui enregistre les données, le jeu peut charger des données obsolètes. Par conséquent, il est crucial de s'assurer que les scripts liés aux données de sauvegarde et de chargement sont correctement commandés dans l'ordre d'exécution.
Sécurisation des données Enregistrer dans l'unité 5 peut être réalisé en utilisant le formatage binaire ou le chiffrement. Le formatage binaire convertit vos données en un format binaire qui n'est pas facilement lisible. Le cryptage ajoute une couche de sécurité supplémentaire en codant vos données d'une manière qui ne peut être décodée qu'avec une clé spécifique.
Bien que PlayerPrefs soit un moyen simple et pratique d'implémenter les fonctionnalités de sauvegarde et de charge dans Unity 5, il a plusieurs limites. Premièrement, il ne prend en charge que les entiers, les flotteurs et les chaînes. Deuxièmement, il n'est pas sécurisé et peut être facilement manipulé. Enfin, PlayerPrefs a une limite de taille, qui peut être un problème pour les jeux avec une grande quantité de données.
Enregistrer et charger Des structures de données complexes dans l'unité 5 peuvent être obtenues en utilisant la sérialisation JSON ou la mise en forme binaire. La sérialisation JSON vous permet de convertir des structures de données complexes en un format de chaîne qui peut être facilement enregistré et chargé. Le formatage binaire est une méthode plus sécurisée qui convertit vos données en format binaire.
Oui, vous pouvez enregistrer et charger des données sur différentes plates-formes dans Unity 5. Cependant, la méthode que vous choisissez pour enregistrer et charger des données peut affecter Compatibilité multiplateforme. For instance, PlayerPrefs is cross-platform compatible, but binary formatting may not be compatible across all platforms.
Troubleshooting issues with Enregistrer et charger les fonctionnalités dans Unity 5 peut être effectuée en vérifiant l'ordre d'exécution de vos scripts, en s'assurant que vos données sont correctement sérialisées ou formatées, et tester votre jeu sur les plates-formes sur lesquelles vous avez l'intention de le publier.
Optimisation des fonctionnalités de sauvegarde et de chargement dans l'unité 5 peut être réalisé en minimisant la quantité de données que vous enregistrez et chargez, en utilisant des structures de données efficaces et en veillant à ce que vos scripts soient bien optimisés.
La mise en œuvre des fonctionnalités de voile automatique dans Unity 5 peut être effectuée en créant un script qui enregistre automatiquement votre jeu à intervalles réguliers ou à des événements spécifiques. Ce script doit utiliser les mêmes méthodes pour enregistrer les données que votre système de sauvegarde manuel.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!