Personnages de sprites modulaires et leur animation

Ce billet de blog est entièrement consacré à mon système d'animation de personnages, il est rempli de conseils utiles et d'extraits de code.

Au cours des deux derniers mois, j'ai créé jusqu'à 9 nouvelles actions de joueurs (des choses amusantes comme bloquer avec un bouclier, esquiver un saut et des armes), 17 nouveaux objets portables, 3 ensembles d'armures (plaque, soie et cuir) et 6 types de coiffures. J'ai également fini de créer tous les outils et automatisations, donc tout est déjà utilisé dans le jeu. Dans l'article, je dirai comment j'ai réussi!


J'espère que ces informations sont utiles et prouvent qu'il n'est pas nécessaire d'être un génie pour créer indépendamment de tels outils / automatisation.

Brève description


Au départ, je voulais vérifier s'il était possible de combiner des sprites superposés avec des animateurs synchronisés pour créer un personnage modulaire avec des coiffures, des équipements et des objets portables remplaçables. Est-il possible de combiner une animation pixel dessinée à la main avec un personnage vraiment personnalisable.

Bien sûr, ces fonctions sont activement utilisées dans les jeux 3D et 2D avec des sprites pré-rendus ou dans les jeux 2D avec une animation squelettique, mais pour autant que je sache, il n'y a pas beaucoup de jeux qui combinent une animation créée manuellement et des personnages modulaires (généralement parce que le processus s'avère être trop monotone).


J'ai déterré cet ancien GIF de mon premier mois avec Unity. En fait, ce sprite modulaire a été l'une de mes premières expériences dans le développement de jeux!

J'ai créé un prototype en utilisant le système d'animation Unity, puis ajouté une chemise, une paire de pantalons, une coiffure et trois articles pour tester le concept. Cela a nécessité 26 animations distinctes.

À ce moment-là, j'ai créé toute mon animation dans Photoshop et je n'ai pas pris la peine d'automatiser le processus, donc c'était très ennuyeux. Puis j'ai pensé: "Donc, l'idée de base a fonctionné, plus tard j'ajouterai de nouvelles animations et de nouveaux équipements." Il s'est avéré que «plus tard», c'est quelques années plus tard.

En mars de cette année, j'ai dessiné la conception d'une grande quantité d'armure (voir mon article précédent) et j'ai remarqué comment ce processus peut être rendu plus pratique. J'ai continué de reporter l'implémentation, car même avec l'automatisation, j'étais nerveux que rien ne fonctionnerait.

Je m'attendais à devoir abandonner la personnalisation du personnage et créer le seul personnage principal, comme dans la plupart des jeux avec animation manuelle. Mais j'avais un plan d'action, et il était temps de vérifier si je pouvais vaincre ce monstre!



Spoiler: Il est avéré formidable. Ci-dessous, je vais révéler mes *** secrets ***

Système de sprite modulaire


I. Connaissez vos limites


J'ai passé beaucoup d'art pré-test et de surveillance du temps pour comprendre combien il peut prendre un tel travail, et si oui ou non accessible pour moi un même niveau de qualité.

J'ai noté toutes mes idées d'animation, les ai rassemblées dans une feuille de calcul et les ai organisées selon divers critères, tels que l'utilité, la beauté et l'utilisation répétée. À ma grande surprise, la toute première de cette liste a été l'animation en fonte de l'objet (potions, bombes, couteaux, haches, balle).

J'ai trouvé une partition numérique pour chaque animation et j'ai tout abandonné avec de mauvaises performances. Je initialement prévu pour créer un 6 ensembles d'armure, mais vite rendu compte qu'il était trop, et jeté les trois types.

L'aspect du suivi du temps s'est avéré très important, et je recommande fortement de l'utiliser pour répondre à des questions telles que: "Combien d'ennemis puis-je me permettre de créer dans le jeu?" Après quelques essais, j'ai pu extrapoler une évaluation assez précise. Avec des travaux supplémentaires sur les animations, j'ai continué à suivre le temps et à réviser mes attentes.

Je vais partager mon exemplaire du magazine pour les deux derniers mois. Veuillez noter que ce temps s'ajoute à mon travail habituel, où je passe 30 heures par semaine:

https://docs.google.com/spreadsheets/d/1Nbr7lujZTB4pWMsuedVcgBYS6n5V-rHrk1PxeGxr6Ck/edit?usp=sharing

II. Changer la palette pour un avenir meilleur


En utilisant judicieusement les couleurs de la conception de l'image-objet, vous pouvez dessiner une image-objet et créer de nombreuses variations différentes en changeant la palette. Vous pouvez modifier non seulement la couleur, mais aussi de créer différents activer et désactiver les éléments (par exemple, remplacer des couleurs sur la transparence).

Chaque jeu d'armure a 3 variations, et le mélange de parties supérieure et inférieure, vous pouvez obtenir beaucoup de combinaisons. J'ai l'intention de mettre en œuvre un système dans lequel vous pouvez collecter un ensemble d'armures pour l'apparence du personnage et un autre pour ses caractéristiques (comme dans Terraria).


Ce faisant, j'ai été agréablement surpris par les curieuses combinaisons découvertes. Si vous connectez le dessus de la plaque avec un fond en soie, vous pouvez obtenir quelque chose dans le style d'un mage de guerre.

Il est préférable de modifier les palettes en utilisant les couleurs codant la valeur dans l'image-objet afin de pouvoir les prendre plus tard pour trouver la vraie couleur dans la palette. Je sais que je simplifie un peu, alors voici une vidéo pour commencer:


Je ne vais pas tout expliquer en détail, mais je vais plutôt parler des moyens de mettre en œuvre cette technique dans Unity, et de leurs avantages et inconvénients.

1. Rechercher la texture de chaque palette


C'est la meilleure stratégie pour créer des variations d'ennemis, d'arrière-plans et de tout ce dont beaucoup de sprites ont la même palette / matière. Différents matériaux ne peuvent pas être regroupés en lots, même s'ils utilisent le même sprite / atlas. Travailler avec des textures est assez pénible, mais vous pouvez changer de palette en temps réel en remplaçant les matériaux à l'aide de SpriteRenderer.sharedMaterial.SetTexture ou MaterialPropertyBlock si vous avez besoin de palettes différentes pour chaque instance du matériau. Voici un exemple de fonction de fragment de shader:

sampler2D _MainTex; sampler2D _PaletteTex; float4 _PaletteTex_TexelSize; half4 frag(v2f input) : SV_TARGET { half4 lookup = tex2D(_MainTex, input.uv); half4 color = tex2D(_PaletteTex, half2(lookup.r * (_PaletteTex_TexelSize.x / 0.00390625f), 0.5)); color.a *= lookup.a; return color * input.color; } 

2. Tableau de couleurs


J'ai décidé de prendre cette décision parce que je devais remplacer les palettes à chaque fois que l'apparence du personnage change (par exemple, lors de la mise en place d'objets) et créer des palettes dynamiquement (pour afficher les couleurs de cheveux et de peau choisies par le joueur). Il me semblait que lors de l'exécution et dans l'éditeur à cet effet est beaucoup plus facile de travailler avec des tableaux.

Code:

 sampler2D _MainTex; half4 _Colors[32]; half4 frag(v2f input) : SV_TARGET { half4 lookup = tex2D(_MainTex, input.uv); half4 color = _Colors[round(lookup.r * 255)]; color.a *= lookup.a; return color * input.color; } ); sampler2D _MainTex; half4 _Colors[32]; half4 frag(v2f input) : SV_TARGET { half4 lookup = tex2D(_MainTex, input.uv); half4 color = _Colors[round(lookup.r * 255)]; color.a *= lookup.a; return color * input.color; } 

J'ai présenté mes palettes comme un type ScriptableObject et utilisé l'outil MonoBehaviour pour les modifier. Ayant longtemps travaillé sur l'édition de palettes dans le processus de création d'animations dans Aseprite, j'ai réalisé de quels outils j'avais besoin et j'ai écrit ces scripts en conséquence. Si vous souhaitez écrire votre propre outil pour éditer des palettes, voici quelques fonctions que je recommande vivement d'implémenter:

- Mise à jour des palettes sur divers matériaux lors de l'édition des couleurs pour afficher les changements en temps réel.

- Attribuer des noms et changer l'ordre des couleurs dans la palette (utilisez le champ pour stocker l'index des couleurs, pas son ordre dans le tableau).

- Sélectionnez et modifiez plusieurs couleurs à la fois. (Astuce: vous pouvez copier et coller les champs Couleur dans Unity: il suffit de cliquer sur une couleur, de copier, de cliquer sur une autre couleur, de coller - maintenant ce sont les mêmes!)

- Appliquer la couleur de superposition à toute la palette

- Palette d'enregistrement en texture

3. Recherche de texture uniforme pour toutes les palettes


Si vous souhaitez passer des palettes à la volée, mais en même temps, vous avez besoin d'un traitement par lots pour réduire le nombre d'appels de tirage, vous pouvez utiliser cette technique. Il peut être utile pour les plates-formes mobiles, mais son utilisation est assez peu pratique.

Tout d'abord, vous devrez regrouper toutes les palettes dans une grande texture. Ensuite, vous utilisez la couleur spécifiée dans le composant SpriteRenderer (couleur vertex AKA) pour déterminer la ligne à lire de la texture de la palette dans le shader. Autrement dit, la palette de ce sprite est contrôlée via SpriteRenderer.color. couleur sommet - c'est la seule SpriteRenderer de propriété, qui peut être modifié sans perturber batchinga (en supposant que tous les matériaux sont les mêmes).

Dans la plupart des cas, il est préférable d'utiliser le canal alpha pour contrôler l'index, car vous n'aurez probablement pas besoin d'un tas de sprites avec une transparence différente.

Code:

 sampler2D _MainTex; sampler2D _PaletteTex; float4 _PaletteTex_TexelSize; half4 frag(v2f input) : SV_TARGET { half4 lookup = tex2D(_MainTex, input.uv); half2 paletteUV = half2( lookup.r * _(PaletteTex_TexelSize.x / 0.00390625f), input.color.a * _(PaletteTex_TexelSize.y / 0.00390625f) ) half4 color = tex2D(_PaletteTex, paletteUV); color.a *= lookup.a; color.rgb *= input.color.rgb; return color; } 


Les merveilles du remplacement des palettes et des couches de sprite. Tant de combinaisons.

III. Automatisez tout et utilisez les bons outils.


Pour implémenter cette fonction, l'automatisation était absolument nécessaire, car en conséquence j'ai obtenu environ 300 animations et des milliers de sprites.

Ma première étape a été de créer un exportateur pour Aseprite afin de gérer mon schéma de couche de sprite fou en utilisant une interface de ligne de commande pratique. Il est juste un script perl, qui contourne toutes les couches et les étiquettes dans mon dossier et l'image de Aseprite dans un certains noms de structure des répertoires et fichiers, afin que je puisse les lire plus tard.

Ensuite, je l'ai écrit à l'unité importateur. Aseprite affiche un fichier JSON pratique avec des données d'image, vous pouvez donc créer des ressources d'animation par programmation. Traitement Aseprite JSON et écrire ce type de données se sont révélées être assez fastidieux, donc je les amener ici. Vous pouvez facilement les charger dans Unity en utilisant JsonUtility.FromJson <AespriteData>, n'oubliez pas d'exécuter Aseprite avec l'option --format 'json-array'.

Code:

 [System.Serializable] public struct AespriteData { [System.Serializable] public struct Size { public int w; public int h; } [System.Serializable] public struct Position { public int x; public int y; public int w; public int h; } [System.Serializable] public struct Frame { public string filename; public Position frame; public bool rotated; public bool trimmed; public Position spriteSourceSize; public Size sourceSize; public int duration; } [System.Serializable] public struct Metadata { public string app; public string version; public string format; public Size size; public string scale; } public Frame[] frames; public Metadata meta; } du [System.Serializable] public struct AespriteData { [System.Serializable] public struct Size { public int w; public int h; } [System.Serializable] public struct Position { public int x; public int y; public int w; public int h; } [System.Serializable] public struct Frame { public string filename; public Position frame; public bool rotated; public bool trimmed; public Position spriteSourceSize; public Size sourceSize; public int duration; } [System.Serializable] public struct Metadata { public string app; public string version; public string format; public Size size; public string scale; } public Frame[] frames; public Metadata meta; } 

Côté Unity, j'ai eu de sérieux problèmes à deux endroits: charger / découper une feuille de sprite et créer un clip d'animation. Un exemple clair m'aiderait beaucoup, alors voici un extrait de code de mon importateur pour que vous ne souffriez pas autant:

Code:

 TextureImporter textureImporter = AssetImporter.GetAtPath(spritePath) as TextureImporter; textureImporter.spriteImportMode = SpriteImportMode.Multiple; SpriteMetaData[] spriteMetaData = new SpriteMetaData[aespriteData.frames.Length]; // Slice the spritesheet according to the aesprite data. for (int i = 0; i < aespriteData.frames.Length; i++) { AespriteData.Position spritePosition = aespriteData.frames[i].frame; spriteMetaData[i].name = aespriteData.frames[i].filename; spriteMetaData[i].rect = new Rect(spritePosition.x, spritePosition.y, spritePosition.w, spritePosition.h); spriteMetaData[i].alignment = (int)SpriteAlignment.Custom; // Same as "Pivot" in Sprite Editor. spriteMetaData[i].pivot = new Vector2(0.5f, 0f); // Same as "Custom Pivot" in Sprite Editor. Ignored if alignment isn't "Custom". } textureImporter.spritesheet = spriteMetaData; AssetDatabase.ImportAsset(spritePath, ImportAssetOptions.ForceUpdate); Object[] assets = AssetDatabase.LoadAllAssetsAtPath(spritePath); // The first element in this array is actually a Texture2D (ie the sheet itself). for (int i = 1; i < assets.Length; i++) { sprites[i - 1] = assets[i] as Sprite; } // Create the animation. AnimationClip clip = new AnimationClip(); clip.frameRate = 40f; float frameLength = 1f / clip.frameRate; ObjectReferenceKeyframe[] keyframes = new ObjectReferenceKeyframe[aespriteData.frames.Length + 1]; // One extra keyframe is required at the end to express the last frame's duration. float time = 0f; for (int i = 0; i < keyframes.Length; i++) { bool lastFrame = i == keyframes.Length - 1; ObjectReferenceKeyframe keyframe = new ObjectReferenceKeyframe(); keyframe.value = sprites[lastFrame ? i - 1 : i]; keyframe.time = time - (lastFrame ? frameLength : 0f); keyframes[i] = keyframe; time += lastFrame ? 0f : aespriteData.frames[i].duration / 1000f; } EditorCurveBinding binding = new EditorCurveBinding(); binding.type = typeof(SpriteRenderer); binding.path = ""; binding.propertyName = "m_Sprite"; AnimationUtility.SetObjectReferenceCurve(clip, binding, keyframes); AssetDatabase.CreateAsset(clip, "Assets/Animation/" + name + ".anim"); AssetDatabase.SaveAssets(); Sprite Editor. TextureImporter textureImporter = AssetImporter.GetAtPath(spritePath) as TextureImporter; textureImporter.spriteImportMode = SpriteImportMode.Multiple; SpriteMetaData[] spriteMetaData = new SpriteMetaData[aespriteData.frames.Length]; // Slice the spritesheet according to the aesprite data. for (int i = 0; i < aespriteData.frames.Length; i++) { AespriteData.Position spritePosition = aespriteData.frames[i].frame; spriteMetaData[i].name = aespriteData.frames[i].filename; spriteMetaData[i].rect = new Rect(spritePosition.x, spritePosition.y, spritePosition.w, spritePosition.h); spriteMetaData[i].alignment = (int)SpriteAlignment.Custom; // Same as "Pivot" in Sprite Editor. spriteMetaData[i].pivot = new Vector2(0.5f, 0f); // Same as "Custom Pivot" in Sprite Editor. Ignored if alignment isn't "Custom". } textureImporter.spritesheet = spriteMetaData; AssetDatabase.ImportAsset(spritePath, ImportAssetOptions.ForceUpdate); Object[] assets = AssetDatabase.LoadAllAssetsAtPath(spritePath); // The first element in this array is actually a Texture2D (ie the sheet itself). for (int i = 1; i < assets.Length; i++) { sprites[i - 1] = assets[i] as Sprite; } // Create the animation. AnimationClip clip = new AnimationClip(); clip.frameRate = 40f; float frameLength = 1f / clip.frameRate; ObjectReferenceKeyframe[] keyframes = new ObjectReferenceKeyframe[aespriteData.frames.Length + 1]; // One extra keyframe is required at the end to express the last frame's duration. float time = 0f; for (int i = 0; i < keyframes.Length; i++) { bool lastFrame = i == keyframes.Length - 1; ObjectReferenceKeyframe keyframe = new ObjectReferenceKeyframe(); keyframe.value = sprites[lastFrame ? i - 1 : i]; keyframe.time = time - (lastFrame ? frameLength : 0f); keyframes[i] = keyframe; time += lastFrame ? 0f : aespriteData.frames[i].duration / 1000f; } EditorCurveBinding binding = new EditorCurveBinding(); binding.type = typeof(SpriteRenderer); binding.path = ""; binding.propertyName = "m_Sprite"; AnimationUtility.SetObjectReferenceCurve(clip, binding, keyframes); AssetDatabase.CreateAsset(clip, "Assets/Animation/" + name + ".anim"); AssetDatabase.SaveAssets(); l' TextureImporter textureImporter = AssetImporter.GetAtPath(spritePath) as TextureImporter; textureImporter.spriteImportMode = SpriteImportMode.Multiple; SpriteMetaData[] spriteMetaData = new SpriteMetaData[aespriteData.frames.Length]; // Slice the spritesheet according to the aesprite data. for (int i = 0; i < aespriteData.frames.Length; i++) { AespriteData.Position spritePosition = aespriteData.frames[i].frame; spriteMetaData[i].name = aespriteData.frames[i].filename; spriteMetaData[i].rect = new Rect(spritePosition.x, spritePosition.y, spritePosition.w, spritePosition.h); spriteMetaData[i].alignment = (int)SpriteAlignment.Custom; // Same as "Pivot" in Sprite Editor. spriteMetaData[i].pivot = new Vector2(0.5f, 0f); // Same as "Custom Pivot" in Sprite Editor. Ignored if alignment isn't "Custom". } textureImporter.spritesheet = spriteMetaData; AssetDatabase.ImportAsset(spritePath, ImportAssetOptions.ForceUpdate); Object[] assets = AssetDatabase.LoadAllAssetsAtPath(spritePath); // The first element in this array is actually a Texture2D (ie the sheet itself). for (int i = 1; i < assets.Length; i++) { sprites[i - 1] = assets[i] as Sprite; } // Create the animation. AnimationClip clip = new AnimationClip(); clip.frameRate = 40f; float frameLength = 1f / clip.frameRate; ObjectReferenceKeyframe[] keyframes = new ObjectReferenceKeyframe[aespriteData.frames.Length + 1]; // One extra keyframe is required at the end to express the last frame's duration. float time = 0f; for (int i = 0; i < keyframes.Length; i++) { bool lastFrame = i == keyframes.Length - 1; ObjectReferenceKeyframe keyframe = new ObjectReferenceKeyframe(); keyframe.value = sprites[lastFrame ? i - 1 : i]; keyframe.time = time - (lastFrame ? frameLength : 0f); keyframes[i] = keyframe; time += lastFrame ? 0f : aespriteData.frames[i].duration / 1000f; } EditorCurveBinding binding = new EditorCurveBinding(); binding.type = typeof(SpriteRenderer); binding.path = ""; binding.propertyName = "m_Sprite"; AnimationUtility.SetObjectReferenceCurve(clip, binding, keyframes); AssetDatabase.CreateAsset(clip, "Assets/Animation/" + name + ".anim"); AssetDatabase.SaveAssets(); sur TextureImporter textureImporter = AssetImporter.GetAtPath(spritePath) as TextureImporter; textureImporter.spriteImportMode = SpriteImportMode.Multiple; SpriteMetaData[] spriteMetaData = new SpriteMetaData[aespriteData.frames.Length]; // Slice the spritesheet according to the aesprite data. for (int i = 0; i < aespriteData.frames.Length; i++) { AespriteData.Position spritePosition = aespriteData.frames[i].frame; spriteMetaData[i].name = aespriteData.frames[i].filename; spriteMetaData[i].rect = new Rect(spritePosition.x, spritePosition.y, spritePosition.w, spritePosition.h); spriteMetaData[i].alignment = (int)SpriteAlignment.Custom; // Same as "Pivot" in Sprite Editor. spriteMetaData[i].pivot = new Vector2(0.5f, 0f); // Same as "Custom Pivot" in Sprite Editor. Ignored if alignment isn't "Custom". } textureImporter.spritesheet = spriteMetaData; AssetDatabase.ImportAsset(spritePath, ImportAssetOptions.ForceUpdate); Object[] assets = AssetDatabase.LoadAllAssetsAtPath(spritePath); // The first element in this array is actually a Texture2D (ie the sheet itself). for (int i = 1; i < assets.Length; i++) { sprites[i - 1] = assets[i] as Sprite; } // Create the animation. AnimationClip clip = new AnimationClip(); clip.frameRate = 40f; float frameLength = 1f / clip.frameRate; ObjectReferenceKeyframe[] keyframes = new ObjectReferenceKeyframe[aespriteData.frames.Length + 1]; // One extra keyframe is required at the end to express the last frame's duration. float time = 0f; for (int i = 0; i < keyframes.Length; i++) { bool lastFrame = i == keyframes.Length - 1; ObjectReferenceKeyframe keyframe = new ObjectReferenceKeyframe(); keyframe.value = sprites[lastFrame ? i - 1 : i]; keyframe.time = time - (lastFrame ? frameLength : 0f); keyframes[i] = keyframe; time += lastFrame ? 0f : aespriteData.frames[i].duration / 1000f; } EditorCurveBinding binding = new EditorCurveBinding(); binding.type = typeof(SpriteRenderer); binding.path = ""; binding.propertyName = "m_Sprite"; AnimationUtility.SetObjectReferenceCurve(clip, binding, keyframes); AssetDatabase.CreateAsset(clip, "Assets/Animation/" + name + ".anim"); AssetDatabase.SaveAssets(); . TextureImporter textureImporter = AssetImporter.GetAtPath(spritePath) as TextureImporter; textureImporter.spriteImportMode = SpriteImportMode.Multiple; SpriteMetaData[] spriteMetaData = new SpriteMetaData[aespriteData.frames.Length]; // Slice the spritesheet according to the aesprite data. for (int i = 0; i < aespriteData.frames.Length; i++) { AespriteData.Position spritePosition = aespriteData.frames[i].frame; spriteMetaData[i].name = aespriteData.frames[i].filename; spriteMetaData[i].rect = new Rect(spritePosition.x, spritePosition.y, spritePosition.w, spritePosition.h); spriteMetaData[i].alignment = (int)SpriteAlignment.Custom; // Same as "Pivot" in Sprite Editor. spriteMetaData[i].pivot = new Vector2(0.5f, 0f); // Same as "Custom Pivot" in Sprite Editor. Ignored if alignment isn't "Custom". } textureImporter.spritesheet = spriteMetaData; AssetDatabase.ImportAsset(spritePath, ImportAssetOptions.ForceUpdate); Object[] assets = AssetDatabase.LoadAllAssetsAtPath(spritePath); // The first element in this array is actually a Texture2D (ie the sheet itself). for (int i = 1; i < assets.Length; i++) { sprites[i - 1] = assets[i] as Sprite; } // Create the animation. AnimationClip clip = new AnimationClip(); clip.frameRate = 40f; float frameLength = 1f / clip.frameRate; ObjectReferenceKeyframe[] keyframes = new ObjectReferenceKeyframe[aespriteData.frames.Length + 1]; // One extra keyframe is required at the end to express the last frame's duration. float time = 0f; for (int i = 0; i < keyframes.Length; i++) { bool lastFrame = i == keyframes.Length - 1; ObjectReferenceKeyframe keyframe = new ObjectReferenceKeyframe(); keyframe.value = sprites[lastFrame ? i - 1 : i]; keyframe.time = time - (lastFrame ? frameLength : 0f); keyframes[i] = keyframe; time += lastFrame ? 0f : aespriteData.frames[i].duration / 1000f; } EditorCurveBinding binding = new EditorCurveBinding(); binding.type = typeof(SpriteRenderer); binding.path = ""; binding.propertyName = "m_Sprite"; AnimationUtility.SetObjectReferenceCurve(clip, binding, keyframes); AssetDatabase.CreateAsset(clip, "Assets/Animation/" + name + ".anim"); AssetDatabase.SaveAssets(); 

Si vous ne l’avez pas encore fait, croyez-moi, il est très facile de commencer à créer vos propres outils. L'astuce la plus simple consiste à placer un GameObject dans la scène avec le MonoBehaviour attaché, qui a l'attribut [ExecuteInEditMode]. Ajoutez un bouton et vous êtes prêt pour la bataille! N'oubliez pas que vos outils personnels n'ont pas besoin d'être beaux, ils peuvent être purement utilitaires.

Code:

 [ExecuteInEditMode] public class MyCoolTool : MonoBehaviour { public bool button; void Update() { if (button) { button = false; DoThing(); } } } à [ExecuteInEditMode] public class MyCoolTool : MonoBehaviour { public bool button; void Update() { if (button) { button = false; DoThing(); } } } 

Lorsque vous travaillez avec des images-objets, il est assez facile d'automatiser des tâches standard (par exemple, créer des textures de palette ou remplacer des couleurs par lots dans plusieurs fichiers d'images-objets). Voici un exemple dont apprendre à changer leurs sprites.

Code:

 string path = "Assets/Whatever/Sprite.png"; Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(path); TextureImporter textureImporter = AssetImporter.GetAtPath(path) as TextureImporter; if (!textureImporter.isReadable) { textureImporter.isReadable = true; AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); } Color[] pixels = texture.GetPixels(0, 0, texture.width, texture.height); for (int i = 0; i < pixels.Length; i++) { // Do something with the pixels, eg replace one color with another. } texture.SetPixels(pixels); texture.Apply(); textureImporter.isReadable = false; // Make sure textures are marked as un-readable when you're done. There's a performance cost to using readable textures in your project that you should avoid unless you plan to change a sprite at runtime. byte[] bytes = ImageConversion.EncodeToPNG(texture); File.WriteAllBytes(Application.dataPath + path.Substring(6), bytes); AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); Sprite.png"; string path = "Assets/Whatever/Sprite.png"; Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(path); TextureImporter textureImporter = AssetImporter.GetAtPath(path) as TextureImporter; if (!textureImporter.isReadable) { textureImporter.isReadable = true; AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); } Color[] pixels = texture.GetPixels(0, 0, texture.width, texture.height); for (int i = 0; i < pixels.Length; i++) { // Do something with the pixels, eg replace one color with another. } texture.SetPixels(pixels); texture.Apply(); textureImporter.isReadable = false; // Make sure textures are marked as un-readable when you're done. There's a performance cost to using readable textures in your project that you should avoid unless you plan to change a sprite at runtime. byte[] bytes = ImageConversion.EncodeToPNG(texture); File.WriteAllBytes(Application.dataPath + path.Substring(6), bytes); AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); 

Comment j'ai dépassé les opportunités Mecanim: Plainte


Au fil du temps, le prototype de système de sprite modulaire que j'ai créé à l'aide de Mecanim est devenu le plus gros problème lors de la mise à niveau d'Unity, car l'API changeait constamment et était mal documentée. Dans le cas d'une machine à états simple, il serait sage de pouvoir interroger le statut de chaque clip ou de modifier les clips au moment de l'exécution. Mais non! Pour des raisons de productivité L'unité fait cuire des clips dans leurs états et nous oblige à utiliser pour leurs quarts de travail du système de remplacements maladroit.

Mecanim lui-même n'est pas un si mauvais système, mais il me semble qu'il ne parvient pas à réaliser sa principale caractéristique déclarée - la simplicité. L'idée des développeurs était de remplacer ce qui semblait compliqué et douloureux (script) par quelque chose de simple (machine à états visuels). Cependant:

- Toute machine à états finis non triviale se transforme rapidement en un réseau sauvage de nœuds et de connexions, dont la logique est dispersée sur différentes couches.

- Les cas d'utilisation simples sont entravés par des exigences système généralisées. Pour jouer une ou deux animations, vous devez créer un nouveau contrôleur et affecter des états / transitions. Bien sûr, il y a un gaspillage excessif de ressources.

- C'est drôle qu'en conséquence, vous devez encore écrire du code, car pour que la machine d'état fasse quelque chose d'intéressant, vous avez besoin d'un script qui appelle Animator.SetBool et des méthodes similaires.

- Pour une utilisation multiple de la machine d'état avec d'autres clips, vous devez la dupliquer et remplacer les clips manuellement. À l'avenir, vous devrez apporter des modifications à plusieurs endroits.

- Si vous souhaitez modifier ce qui est dans un état au moment de l'exécution, vous rencontrez des problèmes. La solution est soit une mauvaise API, soit un graphe fou avec un nœud pour chaque animation possible.


L'histoire de la façon dont les développeurs Firewatch sont entrés dans l' enfer des scripts visuels . Le plus drôle, c'est que lorsque le locuteur montre les exemples les plus simples, ils ont toujours l'air fous. Les spectateurs gémissent littéralement à 12:41 . Ajoutez les énormes coûts de maintenance, et vous comprendrez pourquoi je ne fortement pas comme ce système.

Beaucoup de ces problèmes ne sont même pas la faute des développeurs Mecanim, mais simplement le résultat naturel d'idées incompatibles: vous ne pouvez pas créer un système commun et en même temps simple, et la description de la logique à l'aide d'images est plus difficile que de simples mots / symboles (quelqu'un se souvient-il des organigrammes UML?) . J'ai rappelé un fragment du rapport de Zack McClendon à Practice NYC 2018 , et si vous avez le temps, je vous recommande de regarder l'intégralité de la vidéo!

Cependant, je l'ai compris. Les scripts visuels sont toujours censurés par des nerds agressifs "écrivez votre propre moteur" qui ne comprennent pas les besoins de l'artiste. De plus, on ne peut nier que la plupart du code ressemble à un jargon technique incompréhensible.

Si vous êtes déjà un petit programmeur et faites des jeux avec des sprites, alors vous devrez peut-être réfléchir à deux fois. Quand j'ai commencé, j'étais sûr que je ne pourrais jamais écrire quelque chose lié au moteur mieux que les développeurs Unity.

Et tu sais quoi? Il s'est avéré que l'animateur de sprite n'est qu'un script qui change le sprite après un nombre de secondes spécifié. Quoi qu'il en soit, je devais encore écrire le mien. Depuis lors, j'ai ajouté des événements d'animation et d'autres fonctions à mon projet spécifique, mais la version de base que j'ai écrite en une demi-journée couvre 90% de mes besoins. Il ne comprend que 120 lignes et peut être téléchargé gratuitement à partir d'ici: https://pastebin.com/m9Lfmd94 . Merci d'avoir lu mon article. A très bientôt!

Source: https://habr.com/ru/post/fr468991/


All Articles