Salut Cet article concerne la création de vos propres composants visuels dans l'interface utilisateur à l'aide de l'exemple d'un composant pour visualiser un système de particules dans
Canvas 'e.
Ces informations seront utiles pour implémenter divers effets dans l'interface utilisateur, et peuvent également être utilisées pour générer un maillage ou l'optimiser.
Un peu de théorie ou par où commencer la création de composants
La base de l'interface utilisateur d'Unity est
Canvas . C'est lui qui est utilisé par le système de rendu pour afficher la géométrie «multicouche», conformément à la hiérarchie interne des éléments de l'interface utilisateur.
Tout composant d'interface utilisateur visuelle doit hériter de la classe
Graphic (ou de sa classe
MaskableGraphic dérivée), qui transmet toutes les données nécessaires au composant
CanvasRenderer pour le rendre. Les données sont créées dans la méthode
OnPopulateMesh , qui est appelée chaque fois qu'un composant doit mettre à jour sa géométrie (par exemple, lors du redimensionnement d'un élément).
VertexHelper est passé en tant que paramètre, ce qui aide à générer un maillage pour l'interface utilisateur.
Création de composants
Base
Nous commençons l'implémentation en créant un script
UIParticleSystem qui hérite de la classe
MaskableGraphic .
MaskableGraphic est un dérivé de la classe
Graphic et permet en outre de travailler avec des masques. Substituez la méthode
OnPopulateMesh . La base pour travailler avec
VertexHelper pour générer les sommets d'un maillage de système de particules de maillage ressemblera à ceci:
public class UIParticleSystem : MaskableGraphic { protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); ... int particlesCount = ... ; for (int i = 0; i < particlesCount; i++) { Color vertexColor = ... ; Vector2[] vertexUV = ... ; UIVertex[] quadVerts = new UIVertex[4]; for (int j = 0; j < 4; j++) { Vector3 vertixPosition = ... ; quadVerts[j] = new UIVertex() { position = vertixPosition, color = vertexColor, uv0 = vertexUV }; } vh.AddUIVertexQuad(quadVerts); } } }
Tout d'abord, vous devez effacer
VertextHelper des données existantes en appelant la méthode
Clear . Après cela, vous pouvez commencer à le remplir avec de nouvelles données sur les pics. Pour cela, la méthode
AddUIVertexQuad sera utilisée, ce qui vous permet d'ajouter des informations sur 4 sommets à la fois. Cette méthode a été sélectionnée pour sa facilité d'utilisation, car chaque particule est un rectangle. Chaque sommet est décrit par un objet
UIVertex . De tous les paramètres, nous devons remplir uniquement la position, la couleur et certaines coordonnées du scan
UV .
VertexHelperVertexHelper dispose d'un ensemble complet de méthodes pour ajouter des informations de sommet, ainsi qu'une paire pour recevoir les données actuelles. Pour une géométrie plus complexe, la meilleure solution serait de sélectionner la méthode AddUIVertexStream , qui accepte une liste de sommets et une liste d'indices.
Étant donné que chaque image, la position des particules, leur couleur et d'autres paramètres changeront, le maillage de leur rendu doit également être mis à jour.
Pour ce faire, chaque trame appellera la méthode
SetVerticesDirty , qui définira l'indicateur sur la nécessité de recompter de nouvelles données, ce qui conduira à l'appel à la méthode
OnPopulateMesh . De même pour un matériau, si ses propriétés changent, vous devez appeler la méthode
SetMaterialDirty .
protected void Update() { SetVerticesDirty(); }
Substituez la propriété
mainTexture . Il indique quelle texture sera transmise au
CanvasRenderer et utilisée dans le matériau, la
propriété de shader
_MainTex . Pour ce faire, créez un champ
ParticleImage , qui sera renvoyé par la propriété
mainTexture .
public Texture ParticleImage; public override Texture mainTexture { get { return ParticleImage; } }
Système de particules
Les données pour générer des sommets de maillage seront prises à partir du composant
ParticleSystem , qui est engagé dans tous les calculs sur l'emplacement des particules, leur taille, leur couleur, etc.
Le composant ParticleSystemRenderer sera impliqué dans le rendu des particules, qui devra être désactivé, donc d'autres composants seront également responsables de la création du maillage et de son rendu dans l'interface utilisateur -
UIParticleSystem et
CanvasRenderer .
Créez les champs nécessaires au fonctionnement et initialisez-les dans la méthode
Awake .
UIBehaviourAwake , comme la plupart des méthodes, doit être redéfini ici, car ils sont répertoriés comme virtuels dans UIBehaviour . La classe UIBehaviour elle-même est abstraite et ne contient pratiquement aucune logique de travail, mais elle est fondamentale pour la classe Graphic .
private ParticleSystem _particleSystem; private ParticleSystemRenderer _particleSystemRenderer; private ParticleSystem.MainModule _main; private ParticleSystem.Particle[] _particles; protected override void Awake() { base.Awake(); _particleSystem = GetComponent<ParticleSystem>(); _main = _particleSystem.main; _particleSystemRenderer = GetComponent<ParticleSystemRenderer>(); _particleSystemRenderer.enabled = false; int maxCount = _main.maxParticles; _particles = new ParticleSystem.Particle[maxCount]; }
Le champ
_particles sera utilisé pour stocker les
particules ParticleSystem , et
_main est utilisé pour plus de commodité avec le module
MainModule .
Ajoutons la méthode OnPopulateMesh, en prenant toutes les données nécessaires directement du système de particules. Créez les variables d'assistance
Vector3 [] _quadCorners et
Vector2 [] _simpleUV .
_quadCorners contient les coordonnées des 4 coins du rectangle, par rapport au centre de la particule. La taille initiale de chaque particule est considérée comme un carré avec des côtés 1x1.
_simpleUV - coordonnées du scan
uv , dans ce cas toutes les particules utilisent la même texture sans aucun déplacement.
private Vector3[] _quadCorners = new Vector3[] { new Vector3(-.5f, -.5f, 0), new Vector3(-.5f, .5f, 0), new Vector3(.5f, .5f, 0), new Vector3(.5f, -.5f, 0) }; private Vector2[] _simpleUV = new Vector2[] { new Vector2(0,0), new Vector2(0,1), new Vector2(1,1), new Vector2(1,0), };
protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); int particlesCount = _particleSystem.GetParticles(_particles); for (int i = 0; i < particlesCount; i++) { var particle = _particles[i]; Vector3 particlePosition = particle.position; Color vertexColor = particle.GetCurrentColor(_particleSystem) * color; Vector3 particleSize = particle.GetCurrentSize3D(_particleSystem); Vector2[] vertexUV = _simpleUV; Quaternion rotation = Quaternion.AngleAxis(particle.rotation, Vector3.forward); UIVertex[]quadVerts = new UIVertex[4]; for (int j = 0; j < 4; j++) { Vector3 cornerPosition = rotation * Vector3.Scale(particleSize, _quadCorners[j]); Vector3 vertexPosition = cornerPosition + particlePosition; vertexPosition.z = 0; quadVerts[j] = new UIVertex(); quadVerts[j].color = vertexColor; quadVerts[j].uv0 = vertexUV[j]; quadVerts[j].position = vertexPosition; } vh.AddUIVertexQuad(quadVerts); } }
vertexPositionTout d'abord, la position locale du sommet par rapport au centre de la particule est calculée en tenant compte de sa taille (opération Vector3.Scale (particuleSize, _quadCorners [j]) ) et de la rotation (multipliant la rotation du quaternion par un vecteur). Après la position de la particule elle-même est ajoutée au résultat
Créons maintenant une interface utilisateur simple pour le test à l'aide de composants standard.

Ajouter un
UIParticleSystem au composant
ParticleSystem
Exécutez la scène et vérifiez le résultat du composant.

Les particules sont affichées en fonction de leur position dans la hiérarchie et prennent en compte les masques utilisés. Lorsque vous modifiez la résolution de l'écran et ses proportions, ainsi que lorsque vous modifiez la propriété
Rendere Mode de
Canvas , les particules se comportent de la même manière que tout autre composant visuel dans
Canvas et ne s'affichent que dans celui-ci.
SimulationSpace
Parce que nous plaçons le système de particules à l'intérieur de l'interface utilisateur, il y a un problème avec le paramètre
SimulationSpace . Lorsqu'elles sont simulées dans l'espace mondial, les particules ne sont pas affichées où elles devraient. Par conséquent, nous ajoutons le calcul de la position des particules en fonction de la valeur du paramètre.
protected override void OnPopulateMesh(VertexHelper vh) { ... Vector3 particlePosition; switch (_main.simulationSpace) { case ParticleSystemSimulationSpace.World: particlePosition = _rectTransform.InverseTransformPoint(particle.position); break; case ParticleSystemSimulationSpace.Local: particlePosition = particle.position; break; case ParticleSystemSimulationSpace.Custom: if (_main.customSimulationSpace != null) particlePosition = _rectTransform.InverseTransformPoint( _main.customSimulationSpace.TransformPoint(particle.position) ); else particlePosition = particle.position; break; default: particlePosition = particle.position; break; } ... }
Simuler les propriétés de ParticleSystemRenderer
Maintenant, nous implémentons une partie de la fonctionnalité
ParticleSystemRenderer . À savoir, les propriétés de
RenderMode ,
SortMode ,
Pivot .
Renderder
Nous nous limitons au fait que les particules ne seront toujours localisées que dans le plan de la toile. Par conséquent, nous n'implémentons que deux valeurs:
Billboard et
StretchedBillboard .
Créons notre énumération
CanvasParticleSystemRenderMode pour cela.
public enum CanvasParticleSystemRenderMode { Billboard = 0, StretchedBillboard = 1 }
public CanvasParticleSystemRenderMode RenderMode; public float SpeedScale = 0f; public float LengthScale = 1f; protected override void OnPopulateMesh(VertexHelper vh) { ... Quaternion rotation; switch (RenderMode) { case CanvasParticleSystemRenderMode.Billboard: rotation = Quaternion.AngleAxis(particle.rotation, Vector3.forward); break; case CanvasParticleSystemRenderMode.StretchedBillboard: rotation = Quaternion.LookRotation(Vector3.forward, particle.totalVelocity); float speed = particle.totalVelocity.magnitude; particleSize = Vector3.Scale(particleSize, new Vector3(LengthScale + speed * SpeedScale, 1f, 1f)); rotation *= Quaternion.AngleAxis(90, Vector3.forward); break; default: rotation = Quaternion.AngleAxis(particle.rotation, Vector3.forward); break; } ... }
Si vous sélectionnez le paramètre
StretchedBillboard , la taille des particules dépendra des
paramètres LengthScale et
SpeedScale et sa rotation sera dirigée uniquement dans la direction du mouvement.

Mode de tri
De même, créez l'énumération
CanvasParticlesSortMode . et nous implémentons uniquement le tri par durée de vie des particules.
public enum CanvasParticlesSortMode { None = 0, OldestInFront = 1, YoungestInFront = 2 }
public CanvasParticlesSortMode SortMode;
Pour le tri, nous devons stocker des données sur la durée de vie des particules, qui seront stockées dans la variable
_particleElapsedLifetime . Le tri est implémenté à l'aide de la méthode
Array.Sort .
private float[] _particleElapsedLifetime; protected override void Awake() { ... _particles = new ParticleSystem.Particle[maxCount]; _particleElapsedLifetime = new float[maxCount]; } protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); int particlesCount = _particleSystem.GetParticles(_particles); for (int i = 0; i < particlesCount; i++) _particleElapsedLifetime[i] = _particles[i].startLifetime - _particles[i].remainingLifetime; switch (SortMode) { case CanvasParticlesSortMode.None: break; case CanvasParticlesSortMode.OldestInFront: Array.Sort(_particleElapsedLifetime, _particles, 0, particlesCount,Comparer<float>.Default); Array.Reverse(_particles, 0, particlesCount); break; case CanvasParticlesSortMode.YoungestInFront: Array.Sort(_particleElapsedLifetime, _particles, 0, particlesCount, Comparer<float>.Default); break; } ... }
Pivot
Créez un champ de
pivot pour décaler le point central de la particule.
public Vector3 Pivot = Vector3.zero;
Et lors du calcul de la position du sommet, nous ajoutons cette valeur.
Vector3 cornerPosition = Vector3.Scale(particleSize, _quadCorners[j] + Pivot); Vector3 vertexPosition = rotation * cornerPosition + particlePosition; vertexPosition.z = 0;
Taille ajustable
Si l'élément auquel le système de particules est attaché n'a pas de tailles fixes ou qu'il peut changer au moment de l'exécution, il serait bon d'adapter la taille du système de particules. Rendons la
forme source proportionnelle à la taille de l'élément.
La méthode
OnRectTransformDimensionsChange est appelée lorsque le composant
RectTransform est
redimensionné . Nous redéfinissons cette méthode en implémentant un changement d'échelle de la forme pour l'adapter aux dimensions de
RectTransform .
Créez d'abord les variables du composant
RectTransform et du module
ShapeModule . Pour désactiver la mise à l'échelle des formes, créez la variable
ScaleShapeByRectTransform .
En outre, la mise à l'échelle doit être effectuée lorsque le composant est activé pour définir son échelle initiale.
private RectTransform _rectTransform; private ParticleSystem.ShapeModule _shape; public bool ScaleShapeByRectTransform; protected override void Awake() { ... _rectTransform = GetComponent<RectTransform>(); _shape = _particleSystem.shape; ... } protected override void OnEnable() { base.OnEnable(); ScaleShape(); } protected override void OnRectTransformDimensionsChange() { base.OnRectTransformDimensionsChange(); ScaleShape(); } protected void ScaleShape() { if (!ScaleShapeByRectTransform) return; Rect rect = _rectTransform.rect; var scale = Quaternion.Euler(_shape.rotation) * new Vector3(rect.width, rect.height, 0); scale = new Vector3(Mathf.Abs(scale.x), Mathf.Abs(scale.y), Mathf.Abs(scale.z)); _shape.scale = scale; }
Lors du calcul, il convient de considérer la rotation de la
forme . Les valeurs du résultat final doivent être prises modulo, car elles peuvent se révéler négatives, ce qui affectera la direction du mouvement des particules.
Pour tester l'opération, exécutez l'animation de
redimensionnement RectTransform avec un système de particules attaché.

Initialisation
Afin que le script s'exécute correctement dans l'éditeur et évite les erreurs lors de l'appel de la méthode
OnRectTransformDimensionsChange , nous mettons l'initialisation des variables dans une méthode distincte. Et ajoutez son appel aux
méthodes OnPopulateMesh et
OnRectTransformDimensionsChange .
ExecuteInEditModeVous n'avez pas besoin de spécifier l'attribut ExecuteInEditMode , car Le graphique implémente déjà ce comportement et le script est exécuté dans l'éditeur.
private bool _initialized; protected void Initialize() { if (_initialized) return; _initialized = true; _rectTransform = GetComponent<RectTransform>(); _particleSystem = GetComponent<ParticleSystem>(); _main = _particleSystem.main; _textureSheetAnimation = _particleSystem.textureSheetAnimation; _shape = _particleSystem.shape; _particleSystemRenderer = GetComponent<ParticleSystemRenderer>(); _particleSystemRenderer.enabled = false; _particleSystemRenderer.material = null; var maxCount = _main.maxParticles; _particles = new ParticleSystem.Particle[maxCount]; _particlesLifeProgress = new float[maxCount]; _particleRemainingLifetime = new float[maxCount]; } protected override void Awake() { base.Awake(); Initialize(); } protected override void OnPopulateMesh(VertexHelper vh) { Initialize(); ... } protected override void OnRectTransformDimensionsChange() { #if UNITY_EDITOR Initialize(); #endif ... }
La méthode
OnRectTransformDimensionsChange peut être
appelée plus tôt que
Awake . Par conséquent, à chaque appel, il est nécessaire d'initialiser les variables.
Performance et optimisation
Ce rendu de particules est un peu plus cher que l'utilisation de
ParticleSystemRenderer , qui nécessite une utilisation plus prudente, notamment sur les appareils mobiles.
Il convient également de noter que si au moins un des éléments
Canvas est marqué comme
sale , cela entraînera un recalcul de la géométrie entière du
Canvas et la génération de nouvelles commandes de rendu. Si l'interface utilisateur contient beaucoup de géométrie complexe et ses calculs, il vaut la peine de la diviser en plusieurs toiles intégrées.

PS: Tout le code source et les démos sont des
liens git .
L'article a été lancé il y a près d'un an après avoir été obligé d'utiliser ParticleSystem dans l'interface utilisateur. A cette époque, je n'ai pas trouvé de solution similaire, et les solutions disponibles n'étaient pas optimales pour la tâche en cours. Mais quelques jours avant la publication de cet article, lors de la collecte de matériel, j'ai accidentellement trouvé une solution similaire en utilisant la méthode Graphic.OnPopulateMesh. Par conséquent, je considère qu'il est nécessaire de spécifier un
lien vers le référentiel .