using System; using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace Kamgam.UGUIParticles { [ExecuteAlways] [RequireComponent(typeof(ParticleSystem))] public partial class ParticleSystemForImage : MonoBehaviour { public static Vector3 DefaultPosition = new Vector3(0f, 0f, -1000f); public static ParticleSystemForImage CreateParticleSystemForImage(ParticleImage image) { #if UNITY_EDITOR if (BuildPipeline.isBuildingPlayer) return null; #endif Logger.Log("Trying to create a particle system for ParticleImage " + image.name); ParticleSystemForImage system = null; GameObject go = null; try { // Create new system if not found go = new GameObject("Particle System (for Image)"); go.transform.SetParent(image.transform); go.transform.position = DefaultPosition; go.transform.localScale = Vector3.one; go.SetActive(false); var renderer = go.GetComponent(); if (renderer != null) Utils.SmartDestroy(renderer); go.AddComponent(); system = go.AddComponent(); system.ResetTransform(); system.InitializeAfterCreation(image); system.Play(); go.SetActive(true); // Will trigger OnEable() #if UNITY_EDITOR UnityEditor.EditorUtility.SetDirty(go); #endif Logger.Log("Particle System created."); } catch (System.Exception e) { if (go != null) Utils.SmartDestroy(go); throw e; } #if UNITY_EDITOR // Move component up (fail silently) if (system != null) { EditorApplication.delayCall += () => UnityEditorInternal.ComponentUtility.MoveComponentUp(system); } // Auto Play if (system != null) { ParticleImageEditor.StartPlaying(system.ParticleImage); } #endif return system; } /// /// Should the particle system start playing whenever it is shown? /// public bool PlayOnEnable = true; [SerializeField] [Tooltip("Defines the size conversion factor from particle system units to UI reference pixels (pixels refer to the reference resolution in the CanvasScaler component).")] protected int _pixelsPerUnit = 50; public int PixelsPerUnit { get => _pixelsPerUnit; set { if (value != _pixelsPerUnit) { _pixelsPerUnit = value; ParticleImage?.MarkDirtyRepaint(); } } } [SerializeField] protected Texture _texture; public Texture Texture { get => _texture; set { _texture = value; updateTexture(); } } public Material Material { get => ParticleImage.material; set { ParticleImage.material = value; } } public Color Color { get => ParticleImage.color; set { ParticleImage.color = value; } } [Header("Origin")] [SerializeField] [ShowIfAttribute("OriginTransform", null, ShowIfAttribute.DisablingType.ReadOnly)] public ParticlesOrigin _origin = ParticlesOrigin.Center; public ParticlesOrigin Origin { get => _origin; set { if (_origin == value) return; _origin = value; ParticleImage?.MarkDirtyRepaint(); } } [Tooltip("Specify a Transfrom or a RectTransfrom to use as the origin of particles.\n" + "If an origin transform is used then the value of 'Origin' is ignored. The origin will be at the center of the transform.")] public Transform OriginTransform; [Space(4)] [SerializeField] [Tooltip("The position is a delta value that is added to the origin position. It is always based on the ParticleImage RectTransform.")] protected float _positionX = 0f; public float PositionX { get => _positionX; set { _positionX = value; } } [SerializeField] protected ParticlesLengthUnit _positionXUnit = ParticlesLengthUnit.Percent; public ParticlesLengthUnit PositionXUnit { get => _positionXUnit; set { _positionXUnit = value; } } [Space(4)] [SerializeField] [Tooltip("The position is a delta value that is added to the origin position. It is always based on the ParticleImage RectTransform.")] protected float _positionY = 0f; public float PositionY { get => _positionY; set { _positionY = value; } } [SerializeField] protected ParticlesLengthUnit _positionYUnit = ParticlesLengthUnit.Percent; public ParticlesLengthUnit PositionYUnit { get => _positionYUnit; set { _positionYUnit = value; } } [Space(4)] [Header("Emitter")] [SerializeField] [Tooltip("Whether the shape of the particle emitter should be based on the ParticleImage rect transform or the particle system shape module.")] protected ParticlesEmitterShape _emitterShape = ParticlesEmitterShape.System; public ParticlesEmitterShape EmitterShape { get { return _emitterShape; } set { if (_emitterShape == value) return; _emitterShape = value; UpdateEmitterShape(_emitterShape); } } [Header("Attractor")] [SerializeField] protected bool _useAttractor = false; public bool UseAttractor { get => _useAttractor; set { if (value != _useAttractor) { _useAttractor = value; OnUseAttractorChanged(value); } } } public Transform Attractor; protected ParticleImage _particleImage; public ParticleImage ParticleImage { get { if (_particleImage == null) { _particleImage = this.GetComponentInParent(); } return _particleImage; } } public bool IsPlaying => ParticleSystem.isPlaying; public void Play() { ParticleSystem.Play(); } protected void updateTexture() { if (ParticleImage != null) { ParticleImage.canvasRenderer.SetTexture(_texture); } } public void Pause(bool withChildren = true) { ParticleSystem.Pause(withChildren); } public void Stop(bool withChildren = true, ParticleSystemStopBehavior stopBehaviour = ParticleSystemStopBehavior.StopEmitting) { ParticleSystem.Stop(withChildren, stopBehaviour); } protected ParticleSystem _particleSystem; public ParticleSystem ParticleSystem { get { if (_particleSystem == null && this != null) { _particleSystem = this.GetComponent(); } return _particleSystem; } } public void InitializeAfterCreation(ParticleImage image) { // Disable renderer, we don't need it. var renderer = ParticleSystem.GetComponent(); renderer.enabled = false; // Make sure the system is always simulated. var main = ParticleSystem.main; main.cullingMode = ParticleSystemCullingMode.AlwaysSimulate; main.simulationSpace = ParticleSystemSimulationSpace.Local; main.playOnAwake = false; main.maxParticles = 100; UpdateEmitterShape(image.EmitterShape); } public void ResetTransform() { transform.position = DefaultPosition; transform.localRotation = Quaternion.identity; } public void ApplyPositionDelta(Vector2 delta, RenderMode renderMode) { // TODO: Revisit later, sadly this fix causes too many side effects :-( !! // If the simulation space is world then we have to fix some odd behaviour of the particle system (see Support case 2024-10-31). // This only needs to be done is the canvas is a ScreenSpaceOverlay and is the simulation space is World. // Further more we only do this at runtime because we move the particle system to the root. // That's also why we don't do it in the prefab stage. //bool isScreenSpaceOverlay = renderMode == RenderMode.ScreenSpaceOverlay; //bool isWorld = ParticleSystem.main.simulationSpace == ParticleSystemSimulationSpace.World; //if (isScreenSpaceOverlay && isWorld && !isEditing() && !isInPrefabStage()) // MoveToRoot(); transform.position = new Vector3( DefaultPosition.x + delta.x, DefaultPosition.y + delta.y, DefaultPosition.z ); transform.localRotation = Quaternion.identity; } public void MoveToRoot() { _particleImage = ParticleImage; transform.parent = null; } static bool isEditing() { #if !UNITY_EDITOR return false; #else return !UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode; #endif } #if UNITY_EDITOR static bool isInPrefabStage() { #if UNITY_2021_2_OR_NEWER return UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage() != null; #else return UnityEditor.Experimental.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage() != null; #endif } #endif protected ParticlesEmitterShape? _lastKnownEmitterShape = null; protected ParticleSystemShapeType? _lastKnownEmitterShapeType = null; /// /// The last emitter shape is cached and it only updates if the shape has changed or if forseRefresh is set to true. /// /// /// public void UpdateEmitterShape(ParticlesEmitterShape shape, bool forceRefresh = false) { if (ParticleSystem == null) return; var shapeModule = ParticleSystem.shape; bool changed = !_lastKnownEmitterShape.HasValue || shape != _lastKnownEmitterShape.Value; _lastKnownEmitterShape = shape; if (changed || forceRefresh) { if (!_lastKnownEmitterShapeType.HasValue) _lastKnownEmitterShapeType = shapeModule.shapeType; switch (shape) { case ParticlesEmitterShape.BoxFill: shapeModule.shapeType = ParticleSystemShapeType.Rectangle; shapeModule.scale = new Vector3( ParticleImage.Width / ParticleImage.PixelsPerUnit, ParticleImage.Height / ParticleImage.PixelsPerUnit, 1.01f); // The 1.01f is used as a flag for none-user-created rect shape. // If the z scale is 1.01f it is assumed to have been set by this code. break; case ParticlesEmitterShape.System: default: // Only reset if there is a known value and it has not been changed by the user. // Check for 1.01 flag to determine if set by user. if (shapeModule.shapeType != ParticleSystemShapeType.Rectangle || !Mathf.Approximately(shapeModule.scale.z, 1.01f)) _lastKnownEmitterShapeType = null; // Reset if user changed it if (_lastKnownEmitterShapeType.HasValue) { // Reset to cone if rect was remembered as this is probably wrong. if (_lastKnownEmitterShapeType.Value == ParticleSystemShapeType.Rectangle) _lastKnownEmitterShapeType = ParticleSystemShapeType.Cone; shapeModule.shapeType = _lastKnownEmitterShapeType.Value; shapeModule.scale = Vector3.one; } break; } } } #if UNITY_EDITOR void OnValidate() { if (ParticleImage != null) { updateTexture(); UpdateEmitterShape(EmitterShape); ParticleImage.MarkDirtyRepaint(); } } #endif } }