#if UNITY_EDITOR using System; using UnityEditor; #endif using UnityEngine; using UnityEngine.UI; namespace Kamgam.UGUIParticles { public enum ParticlesLengthUnit { Pixels = 0, Percent = 1 } public enum ParticlesOrigin { Center = 9 , Top = 10 , Bottom = 11 , Left = 12 , Right = 13 , TopLeft = 14 , TopRight = 15 , BottomLeft = 16 , BottomRight = 17 } public enum ParticlesEmitterShape { System = 0, BoxFill = 1 } [ExecuteAlways] public partial class ParticleImage : MaskableGraphic, ILayoutElement, ICanvasRaycastFilter { /// /// Triggered once the element is shown. It takes canvas group alpha and the transform hierarchy active state into account. /// public event System.Action OnShow; /// /// Triggered once the element is hidden. It takes canvas group alpha and the transform hierarchy active state into account. /// public event System.Action OnHide; public bool PlayOnEnable { get => ParticleSystemForImage.PlayOnEnable; set => ParticleSystemForImage.PlayOnEnable = value; } public int PixelsPerUnit { get => ParticleSystemForImage.PixelsPerUnit; set => ParticleSystemForImage.PixelsPerUnit = value; } public Texture Texture { get => ParticleSystemForImage.Texture; set => ParticleSystemForImage.Texture = value; } public ParticlesOrigin Origin { get => ParticleSystemForImage.Origin; set => ParticleSystemForImage.Origin = value; } public Transform OriginTransform { get => ParticleSystemForImage.OriginTransform; set => ParticleSystemForImage.OriginTransform = value; } [System.NonSerialized] protected RectTransform _originRectTransform; [System.NonSerialized] protected Transform _originTransform; public ParticlesEmitterShape EmitterShape { get => ParticleSystemForImage.EmitterShape; set => ParticleSystemForImage.EmitterShape = value; } public float PositionX { get => ParticleSystemForImage.PositionX; set => ParticleSystemForImage.PositionX = value; } public ParticlesLengthUnit PositionXUnit { get => ParticleSystemForImage.PositionXUnit; set => ParticleSystemForImage.PositionXUnit = value; } public float PositionY { get => ParticleSystemForImage.PositionY; set => ParticleSystemForImage.PositionY = value; } public ParticlesLengthUnit PositionYUnit { get => ParticleSystemForImage.PositionYUnit; set => ParticleSystemForImage.PositionYUnit = value; } protected RectTransform _rectTransform; public RectTransform RectTransform { get { if (_rectTransform == null) { _rectTransform = transform as RectTransform; } return _rectTransform; } } public float Width => RectTransform.rect.width; public float Height => RectTransform.rect.height; public bool IsPlaying => ParticleSystemForImage.IsPlaying; /// /// Use this in a context where auto-creation of the particle system may not be allowed. /// public bool HasParticleSystemForImage { get { if (_particleSystemForImage == null || _particleSystemForImage.gameObject == null) _particleSystemForImage = this.GetComponentInChildren(includeInactive: true); return _particleSystemForImage != null && _particleSystemForImage.gameObject != null; } } protected ParticleSystemForImage _particleSystemForImage; public ParticleSystemForImage ParticleSystemForImage { get { if (_particleSystemForImage == null || _particleSystemForImage.gameObject == null) { _particleSystemForImage = this.GetComponentInChildren(includeInactive: true); if (_particleSystemForImage == null) { _particleSystemForImage = ParticleSystemForImage.CreateParticleSystemForImage(this); } } return _particleSystemForImage; } } public ParticleSystem ParticleSystem { get { if (ParticleSystemForImage == null || ParticleSystemForImage.gameObject == null) return null; return ParticleSystemForImage.ParticleSystem; } } [System.NonSerialized] protected ParticleSystem.Particle[] _particles; protected Vector3[] _initialWorldCorners = null; protected override void OnEnable() { base.OnEnable(); if (IsInPlayModeOrInBuild() && PlayOnEnable) { ParticleSystemForImage.Play(); } // Update rect transforms positions if (_initialWorldCorners == null) _initialWorldCorners = new Vector3[4]; RectTransform.GetWorldCorners(_initialWorldCorners); if (ParticleSystemForImage.UseAttractor) { ParticleSystemForImage.EnableAttractor(); } MarkDirtyRepaint(); } public float? _lastWidth = null; public float? _lastHeight = null; public void Update() { if (!_lastWidth.HasValue || _lastWidth.Value != Width) { _lastWidth = Width; ParticleSystemForImage.UpdateEmitterShape(ParticleSystemForImage.EmitterShape, forceRefresh: true); } if (!_lastHeight.HasValue || _lastHeight.Value != Height) { _lastHeight = Height; ParticleSystemForImage.UpdateEmitterShape(ParticleSystemForImage.EmitterShape, forceRefresh: true); } MarkDirtyRepaint(); } public void MarkDirtyRepaint() { SetVerticesDirty(); } public bool IsInPlayModeOrInBuild() { #if UNITY_EDITOR return UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode; #else return true; #endif } public RenderMode GetRenderMode() { var renderMode = RenderMode.ScreenSpaceOverlay; if (canvas == null) renderMode = RenderMode.ScreenSpaceOverlay; else renderMode = canvas.renderMode; // Check if the camera matches the render mode and if not alter the render mode. if (renderMode == RenderMode.ScreenSpaceCamera && canvas != null) { Camera cam = canvas.worldCamera; // If no camera is specified then it will behave like screen scpae OVERLAY. if (cam == null) renderMode = RenderMode.ScreenSpaceOverlay; } return renderMode; } public bool IsInEditor() { #if UNITY_EDITOR return true; #else return false; #endif } public void Play() { ParticleSystemForImage.Play(); } public void Pause(bool withChildren = true) { ParticleSystemForImage.Pause(withChildren); } public void Stop(bool withChildren = true, ParticleSystemStopBehavior stopBehaviour = ParticleSystemStopBehavior.StopEmitting) { ParticleSystemForImage.Stop(withChildren, stopBehaviour); } // Mesh Data caches UIVertex[] _vertices; int[] _triangles; protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); if (!IsActiveInHierarchyAndEnabled()) return; if (!HasParticleSystemForImage) return; updateVisibilityEvents(); int maxParticles = ParticleSystem.main.maxParticles; if (_particles == null || _particles.Length < maxParticles) _particles = new ParticleSystem.Particle[maxParticles]; if (_vertices == null || _vertices.Length < maxParticles * 4) { _vertices = new UIVertex[maxParticles * 4]; _triangles = new int[maxParticles * 6]; } // Fetch particles from ParticleSystem int numParticlesAlive = ParticleSystem.GetParticles(_particles); float width = RectTransform.rect.width; float height = RectTransform.rect.height; Vector3 origin = resolveOrigin(ParticleSystemForImage.Origin, width, height); // Abort if num of active particles is zero if (numParticlesAlive == 0) return; int vertex = 0; int index = 0; // Cache values needed for every particle float startRotationConst = ParticleSystem.main.startRotation.constant * Mathf.Rad2Deg; // Create one quad per particle for (int i = 0; i < numParticlesAlive; i++) { var position = new Vector3( (_particles[i].position.x - ParticleSystemForImage.DefaultPosition.x) * PixelsPerUnit, (_particles[i].position.y - ParticleSystemForImage.DefaultPosition.y) * PixelsPerUnit, _particles[i].position.z * PixelsPerUnit ); // Compensate pivot position position.x += -RectTransform.pivot.x * width; position.y += -RectTransform.pivot.y * height; var size = _particles[i].GetCurrentSize(ParticleSystem) * PixelsPerUnit; var color = _particles[i].GetCurrentColor(ParticleSystem) * this.color; var rotation = Quaternion.Euler(_particles[i].rotation3D); // Interpret "alignToDirection" as always align to direction (not only at the start). if (ParticleSystem.shape.alignToDirection) { var velocity2D = ((Vector2)_particles[i].velocity).normalized; var angle = Mathf.Atan2(velocity2D.y, velocity2D.x) * Mathf.Rad2Deg; rotation = Quaternion.Euler(0f, 0f, -angle + startRotationConst); } createQuad(ref vertex, _vertices, ref index, _triangles, origin + position, rotation, size, color); } // Move all the other particles off screen and make them invisible for (int i = numParticlesAlive; i < maxParticles; i++) { var position = new Vector3(-300000, -300000, -300000); var size = _particles[i].GetCurrentSize(ParticleSystem) * 0.001f; var color = new Color(0, 0, 0, 0); var rotation = Quaternion.identity; createQuad(ref vertex, _vertices, ref index, _triangles, position, rotation, size, color); } writeMeshData(_vertices, _triangles, vh); if (ParticleSystemForImage != null) { canvasRenderer.SetTexture(ParticleSystemForImage.Texture); } } private void createQuad(ref int vertex, UIVertex[] vertices, ref int index, int[] triangles, Vector3 pos, Quaternion rotation, float size, Color tint) { // The coordinate system starts top left and clock-wise oriented tris are front facing. vertices[vertex].position = new Vector3(pos.x - size * 0.5f, pos.y - size * 0.5f, 0f); vertices[vertex].position = rotateAround(pos, vertices[vertex].position, rotation); vertices[vertex].uv0 = new Vector2(0f, 0f); vertices[vertex].color = tint; vertex++; vertices[vertex].position = new Vector3(pos.x + size * 0.5f, pos.y - size * 0.5f, 0f); vertices[vertex].position = rotateAround(pos, vertices[vertex].position, rotation); vertices[vertex].uv0 = new Vector2(1f, 0f); vertices[vertex].color = tint; vertex++; vertices[vertex].position = new Vector3(pos.x + size * 0.5f, pos.y + size * 0.5f, 0f); vertices[vertex].position = rotateAround(pos, vertices[vertex].position, rotation); vertices[vertex].uv0 = new Vector2(1f, 1f); vertices[vertex].color = tint; vertex++; vertices[vertex].position = new Vector3(pos.x - size * 0.5f, pos.y + size * 0.5f, 0f); vertices[vertex].position = rotateAround(pos, vertices[vertex].position, rotation); vertices[vertex].uv0 = new Vector2(0f, 1f); vertices[vertex].color = tint; vertex++; triangles[index++] = (ushort)(vertex - 4); triangles[index++] = (ushort)(vertex - 3); triangles[index++] = (ushort)(vertex - 1); triangles[index++] = (ushort)(vertex - 3); triangles[index++] = (ushort)(vertex - 2); triangles[index++] = (ushort)(vertex - 1); } private void writeMeshData(UIVertex[] vertices, int[] triangles, VertexHelper vh) { vh.Clear(); int existingVertexCount = vh.currentVertCount; int vCount = vertices.Length; int tCount = triangles.Length; for (int i = 0; i < vCount; i++) { vh.AddVert(vertices[i]); } for (int i = 0; i < tCount; i += 3) { vh.AddTriangle( existingVertexCount + triangles[i + 2], existingVertexCount + triangles[i + 1], existingVertexCount + triangles[i + 0] ); } } /// /// Calculates the particle system origin in the space of the ParticleImage rect. /// The result is the position relative to the BOTTOM LEFT corner of the rect.
/// Unless the simulation space is WORLD SPACE, then it will always return the center rect pos. ///
/// /// /// /// The local position relative to the BOTTOM LEFT corner of the ParticleImage rect. private Vector3 resolveOrigin(ParticlesOrigin originSetting, float width, float height) { var unitX = PositionXUnit; var unitY = PositionYUnit; if (OriginTransform != null) { _originRectTransform = OriginTransform as RectTransform; if (_originRectTransform == null) { _originTransform = OriginTransform as Transform; } } else { _originRectTransform = null; _originTransform = null; } // How far the particle origin should be from the calculated origin. Vector3 offset = new Vector3( PositionXUnit == ParticlesLengthUnit.Pixels ? PositionX : width * PositionX / 100f, PositionYUnit == ParticlesLengthUnit.Pixels ? PositionY : height * PositionY / 100f ); // Reset the particle system position to the default if (ParticleSystem.main.simulationSpace == ParticleSystemSimulationSpace.Local) { ParticleSystemForImage.ResetTransform(); } Vector3 origin = new Vector3(0, 0, 0); Vector3 originRelativeToBottomLeft = origin; Vector2 deltaLocal = Vector2.zero; if (_originRectTransform != null || _originTransform != null) { Vector2 localPos; if (_originRectTransform != null) localPos = WorldSpaceToUISpace(_originRectTransform.TransformPoint(_originRectTransform.rect.center), worldPosFromRect: true, RectTransform, GetRenderMode()); else localPos = WorldSpaceToUISpace(OriginTransform, RectTransform, GetRenderMode()); originRelativeToBottomLeft = localPos + (Vector2) offset; if (ParticleSystem.main.simulationSpace == ParticleSystemSimulationSpace.Local) { // Move the particles inside the ParticleImage element. origin = localPos; ParticleSystemForImage.ResetTransform(); } else if (ParticleSystem.main.simulationSpace == ParticleSystemSimulationSpace.World) { localPos += (Vector2) offset; // Here we do NOT move the particles inside the VisualElement. // Instead we move the particle system in the WORLD. That way we can retain the // properties set by the SimulationSpace setting (old particles will be left behind). deltaLocal = localPos - new Vector2(width * 0.5f, height * 0.5f); var deltaInWorldSpace = deltaLocal / (float)PixelsPerUnit; // Debug.Log(deltaLocal + " > " + " > " + deltaInWorldSpace); ParticleSystemForImage.ApplyPositionDelta(deltaInWorldSpace, GetRenderMode()); // Center pos is the default origin. origin.x = width * 0.5f; origin.y = height * 0.5f; } else { // Custom Space (not supported) // Center pos is the default origin. origin.x = width * 0.5f; origin.y = height * 0.5f; } } else { switch (originSetting) { case ParticlesOrigin.BottomRight: origin.x = width; origin.y = 0f; break; case ParticlesOrigin.Top: origin.x = width * 0.5f; origin.y = height; break; case ParticlesOrigin.Bottom: origin.x = width * 0.5f; origin.y = 0f; break; case ParticlesOrigin.Left: origin.x = 0f; origin.y = height * 0.5f; break; case ParticlesOrigin.Right: origin.x = width; origin.y = height * 0.5f; break; case ParticlesOrigin.TopRight: origin.x = width; origin.y = height; break; case ParticlesOrigin.TopLeft: origin.y = height; break; case ParticlesOrigin.Center: origin.x = width * 0.5f; origin.y = height * 0.5f; break; case ParticlesOrigin.BottomLeft: default: origin.x = 0f; origin.y = 0f; break; } originRelativeToBottomLeft = origin; applyWorldSimulationSpace(ref origin, ref deltaLocal, offset); } if (ParticleSystem.main.simulationSpace == ParticleSystemSimulationSpace.World) { ParticleSystemForImage.UpdateAttractorPosition(originRelativeToBottomLeft, width, height); return new Vector3(origin.x, origin.y, origin.z); } else { var result = new Vector3(origin.x + offset.x, origin.y + offset.y, origin.z); ParticleSystemForImage.UpdateAttractorPosition(result, width, height); return result; } } private void applyWorldSimulationSpace(ref Vector3 origin, ref Vector2 deltaLocal, Vector3 positionDeltaInPx) { if (ParticleSystem.main.simulationSpace == ParticleSystemSimulationSpace.World) { if (_initialWorldCorners != null) { var center = new Vector3( _initialWorldCorners[0].x + (_initialWorldCorners[2].x - _initialWorldCorners[0].x) * 0.5f, _initialWorldCorners[0].y + (_initialWorldCorners[2].y - _initialWorldCorners[0].y) * 0.5f, _initialWorldCorners[0].z + (_initialWorldCorners[2].z - _initialWorldCorners[0].z) * 0.5f ); deltaLocal = GetCenterPosLocalDelta(center, RectTransform); // Here we do NOT move the particles inside the UI. // Instead we move the particle system in the WORLD. That way we can retain the // properties set by the SimulationSpace setting (old particles will be left behind). var deltaInWorldSpace = (deltaLocal + (Vector2)positionDeltaInPx) / (float)PixelsPerUnit; // we also add the position delta here. ParticleSystemForImage.ApplyPositionDelta(deltaInWorldSpace, GetRenderMode()); origin.x -= deltaLocal.x; origin.y -= deltaLocal.y; } } } /// /// Returns the distance vector between the source center and the target rect center in target local space. /// /// /// /// public Vector2 GetCenterPosLocalDelta(RectTransform source, RectTransform target) { var sourceWorldCenter = source.TransformPoint(source.rect.center); return GetCenterPosLocalDelta(sourceWorldCenter, target); } /// /// Returns the distance vector between the world position and the target rect center in target local space. /// /// /// /// public Vector2 GetCenterPosLocalDelta(Vector3 sourceWorldCenter, RectTransform target) { var sourcePosInTaget = target.InverseTransformPoint(sourceWorldCenter); var deltaInTarget = new Vector2( target.rect.center.x - sourcePosInTaget.x , target.rect.center.y - sourcePosInTaget.y ); return deltaInTarget; } public Vector2 WorldSpaceToUISpace(RectTransform source, RectTransform target, RenderMode renderMode) { return WorldSpaceToUISpace(source.position, worldPosFromRect: true, target, renderMode); } public Vector2 WorldSpaceToUISpace(Transform source, RectTransform target, RenderMode renderMode) { return WorldSpaceToUISpace(source.position, worldPosFromRect: false, target, renderMode); } /// /// Transform the absolute world space position to the local UI position based on the camera and the rect of the given ui target.
/// The result is a position is in the target's local space relative to the BOTTOM LEFT corner of the rect. ///
/// /// /// /// /// public Vector2 WorldSpaceToUISpace(Vector3 worldSpacePos, bool worldPosFromRect, RectTransform target, RenderMode renderMode) { bool isScreenSpaceOverlay = false; Camera worldCam = null; Camera uiCam = null; switch (renderMode) { case RenderMode.ScreenSpaceOverlay: isScreenSpaceOverlay = true; break; case RenderMode.ScreenSpaceCamera: if (canvas.worldCamera != null) { worldCam = canvas.worldCamera; uiCam = canvas.worldCamera; } else { isScreenSpaceOverlay = true; } break; case RenderMode.WorldSpace: worldCam = canvas.worldCamera; uiCam = canvas.worldCamera; break; default: break; } if (isScreenSpaceOverlay) { if (worldPosFromRect) { worldCam = null; uiCam = null; } else { worldCam = Camera.main != null ? Camera.main : Camera.current; uiCam = null; } } var screenPos = RectTransformUtility.WorldToScreenPoint(worldCam, worldSpacePos); RectTransformUtility.ScreenPointToLocalPointInRectangle(target, screenPos, uiCam, out Vector2 localPos); // Shift to lower left corner localPos.x += target.rect.width * target.pivot.x; localPos.y += target.rect.height * target.pivot.y; return localPos; } private Vector3 rotateAround(Vector3 pivot, Vector3 point, float angle) { if (angle == 0f) return point; Quaternion rot = Quaternion.Euler(new Vector3(0f, 0f, angle)); var result = point - pivot; result = rot * result; result = pivot + result; return result; } private Vector3 rotateAround(Vector3 pivot, Vector3 point, Quaternion rotation) { var result = point - pivot; result = rotation * result; result = pivot + result; return result; } protected bool? _lastKnownVisibility = null; protected void updateVisibilityEvents() { bool isShown = IsActiveInHierarchyAndEnabled(); bool changed = false; if (_lastKnownVisibility.HasValue) { if (isShown != _lastKnownVisibility.Value) changed = true; } else { changed = true; } _lastKnownVisibility = isShown; if (changed) { if (isShown) onShow(); else onHide(); } } public bool IsActiveInHierarchyAndEnabled() { return gameObject.activeInHierarchy && enabled; } public void Show() { bool wasShown = IsActiveInHierarchyAndEnabled(); gameObject.SetActive(true); if (!wasShown) onShow(); } public void Hide() { bool wasShown = IsActiveInHierarchyAndEnabled(); gameObject.SetActive(false); if (wasShown) onHide(); } public void Toggle() { if (IsActiveInHierarchyAndEnabled()) { Hide(); } else { Show(); } } protected void onShow() { #if UNITY_EDITOR if (!EditorApplication.isPlaying) return; #endif if (PlayOnEnable) ParticleSystemForImage.Play(); OnShow?.Invoke(this); } protected void onHide() { #if UNITY_EDITOR if (!EditorApplication.isPlaying) return; #endif OnHide?.Invoke(this); } #if UNITY_EDITOR protected override void OnValidate() { base.OnValidate(); MarkDirtyRepaint(); } #endif // I N T E R F A C E S /// /// See ILayoutElement.CalculateLayoutInputHorizontal. /// public virtual void CalculateLayoutInputHorizontal() { } /// /// See ILayoutElement.CalculateLayoutInputVertical. /// public virtual void CalculateLayoutInputVertical() { } /// /// See ILayoutElement.minWidth. /// public virtual float minWidth { get { return 0; } } /// /// If there is a sprite being rendered returns the size of that sprite. /// In the case of a slided or tiled sprite will return the calculated minimum size possible /// public virtual float preferredWidth { get { return 0; } } /// /// See ILayoutElement.flexibleWidth. /// public virtual float flexibleWidth { get { return -1; } } /// /// See ILayoutElement.minHeight. /// public virtual float minHeight { get { return 0; } } /// /// If there is a sprite being rendered returns the size of that sprite. /// In the case of a slided or tiled sprite will return the calculated minimum size possible /// public virtual float preferredHeight { get { return 0; } } /// /// See ILayoutElement.flexibleHeight. /// public virtual float flexibleHeight { get { return -1; } } /// /// See ILayoutElement.layoutPriority. /// public virtual int layoutPriority { get { return 0; } } /// /// Calculate if the ray location for this image is a valid hit location. Takes into account a Alpha test threshold. /// /// The screen point to check against /// The camera in which to use to calculate the coordinating position /// If the location is a valid hit or not. /// Also see See:ICanvasRaycastFilter. public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera) { return true; } } }