using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Coffee.UIParticleExtensions;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Serialization;
using UnityEngine.UI;
using Random = UnityEngine.Random;
[assembly: InternalsVisibleTo("Coffee.UIParticle.Editor")]
namespace Coffee.UIExtensions
{
///
/// Render maskable and sortable particle effect ,without Camera, RenderTexture or Canvas.
///
[ExecuteAlways]
[RequireComponent(typeof(RectTransform))]
[RequireComponent(typeof(CanvasRenderer))]
public class UIParticle : MaskableGraphic, ISerializationCallbackReceiver
{
public enum MeshSharing
{
None,
Auto,
Primary,
PrimarySimulator,
Replica
}
public enum PositionMode
{
Relative,
Absolute
}
public enum AutoScalingMode
{
None,
UIParticle,
Transform
}
[HideInInspector]
[SerializeField]
internal bool m_IsTrail;
[HideInInspector]
[FormerlySerializedAs("m_IgnoreParent")]
[SerializeField]
private bool m_IgnoreCanvasScaler;
[HideInInspector]
[SerializeField]
private bool m_AbsoluteMode;
[Tooltip("Particle effect scale")]
[SerializeField]
private Vector3 m_Scale3D = new Vector3(10, 10, 10);
[Tooltip("Animatable material properties.\n" +
"If you want to change the material properties of the ParticleSystem in Animation, enable it.")]
[SerializeField]
internal AnimatableProperty[] m_AnimatableProperties = new AnimatableProperty[0];
[Tooltip("Particles")]
[SerializeField]
private List m_Particles = new List();
[Tooltip("Mesh sharing.\n" +
"None: disable mesh sharing.\n" +
"Auto: automatically select Primary/Replica.\n" +
"Primary: provides particle simulation results to the same group.\n" +
"Primary Simulator: Primary, but do not render the particle (simulation only).\n" +
"Replica: render simulation results provided by the primary.")]
[SerializeField]
private MeshSharing m_MeshSharing = MeshSharing.None;
[Tooltip("Mesh sharing group ID.\n" +
"If non-zero is specified, particle simulation results are shared within the group.")]
[SerializeField]
private int m_GroupId;
[SerializeField]
private int m_GroupMaxId;
[Tooltip("Relative: The particles will be emitted from the scaled position of ParticleSystem.\n" +
"Absolute: The particles will be emitted from the world position of ParticleSystem.")]
[SerializeField]
private PositionMode m_PositionMode = PositionMode.Relative;
[SerializeField]
[Tooltip("Prevent the root-Canvas scale from affecting the hierarchy-scaled ParticleSystem.")]
private bool m_AutoScaling = true;
[SerializeField]
[Tooltip("Transform: Transform.lossyScale (=world scale) will be set to (1, 1, 1)." +
"UIParticle: UIParticle.scale will be adjusted.")]
private AutoScalingMode m_AutoScalingMode = AutoScalingMode.Transform;
[SerializeField]
private bool m_ResetScaleOnEnable;
private readonly List _renderers = new List();
private int _groupId;
private Camera _orthoCamera;
private DrivenRectTransformTracker _tracker;
///
/// Should this graphic be considered a target for ray-casting?
///
public override bool raycastTarget
{
get { return false; }
set { }
}
///
/// Mesh sharing.
/// None: disable mesh sharing.
/// Auto: automatically select Primary/Replica.
/// Primary: provides particle simulation results to the same group.
/// Primary Simulator: Primary, but do not render the particle (simulation only).
/// Replica: render simulation results provided by the primary.
///
public MeshSharing meshSharing
{
get { return m_MeshSharing; }
set { m_MeshSharing = value; }
}
///
/// Mesh sharing group ID.
/// If non-zero is specified, particle simulation results are shared within the group.
///
public int groupId
{
get { return _groupId; }
set
{
if (m_GroupId == value) return;
m_GroupId = value;
if (m_GroupId != m_GroupMaxId)
{
ResetGroupId();
}
}
}
public int groupMaxId
{
get { return m_GroupMaxId; }
set
{
if (m_GroupMaxId == value) return;
m_GroupMaxId = value;
ResetGroupId();
}
}
///
/// Particle position mode.
/// Relative: The particles will be emitted from the scaled position of the ParticleSystem.
/// Absolute: The particles will be emitted from the world position of the ParticleSystem.
///
public PositionMode positionMode
{
get { return m_PositionMode; }
set { m_PositionMode = value; }
}
///
/// Particle position mode.
/// Relative: The particles will be emitted from the scaled position of the ParticleSystem.
/// Absolute: The particles will be emitted from the world position of the ParticleSystem.
///
public bool absoluteMode
{
get { return m_PositionMode == PositionMode.Absolute; }
set { positionMode = value ? PositionMode.Absolute : PositionMode.Relative; }
}
///
/// Prevents the root-Canvas scale from affecting the hierarchy-scaled ParticleSystem.
///
[Obsolete("The autoScaling is now obsolete. Please use the autoScalingMode instead.", false)]
public bool autoScaling
{
get { return m_AutoScalingMode != AutoScalingMode.None; }
set
{
autoScalingMode = value ? AutoScalingMode.Transform : AutoScalingMode.None;
}
}
///
/// Auto scaling mode.
/// Transform: Transform.lossyScale (=world scale) will be set to (1, 1, 1).
/// UIParticle: UIParticle.scale will be adjusted.
///
public AutoScalingMode autoScalingMode
{
get { return m_AutoScalingMode; }
set
{
if (m_AutoScalingMode == value) return;
m_AutoScalingMode = value;
UpdateTracker();
}
}
internal bool useMeshSharing
{
get { return m_MeshSharing != MeshSharing.None; }
}
internal bool isPrimary
{
get
{
return m_MeshSharing == MeshSharing.Primary
|| m_MeshSharing == MeshSharing.PrimarySimulator;
}
}
internal bool canSimulate
{
get
{
return m_MeshSharing == MeshSharing.None
|| m_MeshSharing == MeshSharing.Auto
|| m_MeshSharing == MeshSharing.Primary
|| m_MeshSharing == MeshSharing.PrimarySimulator;
}
}
internal bool canRender
{
get
{
return m_MeshSharing == MeshSharing.None
|| m_MeshSharing == MeshSharing.Auto
|| m_MeshSharing == MeshSharing.Primary
|| m_MeshSharing == MeshSharing.Replica;
}
}
///
/// Particle effect scale.
///
public float scale
{
get { return m_Scale3D.x; }
set { m_Scale3D = new Vector3(value, value, value); }
}
///
/// Particle effect scale.
///
public Vector3 scale3D
{
get { return m_Scale3D; }
set { m_Scale3D = value; }
}
///
/// Particle effect scale.
///
public Vector3 scale3DForCalc
{
get { return autoScalingMode == AutoScalingMode.UIParticle ? m_Scale3D.GetScaled(canvasScale) : m_Scale3D; }
}
public List particles
{
get { return m_Particles; }
}
///
/// Get all base materials to render.
///
public IEnumerable materials
{
get
{
for (var i = 0; i < _renderers.Count; i++)
{
var r = _renderers[i];
if (!r || !r.material) continue;
yield return r.material;
}
}
}
public override Material materialForRendering
{
get { return null; }
}
///
/// Paused.
///
public bool isPaused { get; private set; }
public Vector3 parentScale { get; private set; }
public Vector3 canvasScale { get; private set; }
protected override void OnEnable()
{
ResetGroupId();
UpdateTracker();
UIParticleUpdater.Register(this);
RegisterDirtyMaterialCallback(UpdateRendererMaterial);
if (0 < particles.Count)
{
RefreshParticles(particles);
}
else
{
RefreshParticles();
}
base.OnEnable();
// Reset scale for upgrade.
if (m_ResetScaleOnEnable)
{
m_ResetScaleOnEnable = false;
transform.localScale = Vector3.one;
}
}
///
/// This function is called when the behaviour becomes disabled.
///
protected override void OnDisable()
{
UpdateTracker();
UIParticleUpdater.Unregister(this);
_renderers.ForEach(r => r.Reset());
UnregisterDirtyMaterialCallback(UpdateRendererMaterial);
base.OnDisable();
}
///
/// Callback for when properties have been changed by animation.
///
protected override void OnDidApplyAnimationProperties()
{
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
UpdateTracker();
}
#endif
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
}
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
if (m_IgnoreCanvasScaler || m_AutoScaling)
{
m_IgnoreCanvasScaler = false;
m_AutoScaling = false;
m_AutoScalingMode = AutoScalingMode.Transform;
m_ResetScaleOnEnable = true;
#if UNITY_EDITOR
EditorApplication.delayCall += () =>
{
if (!this || !gameObject || !transform) return;
transform.localScale = Vector3.one;
m_ResetScaleOnEnable = false;
EditorUtility.SetDirty(this);
};
#endif
}
if (m_AbsoluteMode)
{
m_AbsoluteMode = false;
m_PositionMode = PositionMode.Absolute;
}
}
public void Play()
{
particles.Exec(p => p.Simulate(0, false, true));
isPaused = false;
}
public void Pause()
{
particles.Exec(p => p.Pause());
isPaused = true;
}
public void Resume()
{
isPaused = false;
}
public void Stop()
{
particles.Exec(p => p.Stop());
isPaused = true;
}
public void StartEmission()
{
particles.Exec(p =>
{
var emission = p.emission;
emission.enabled = true;
});
}
public void StopEmission()
{
particles.Exec(p =>
{
var emission = p.emission;
emission.enabled = false;
});
}
public void Clear()
{
particles.Exec(p => p.Clear());
isPaused = true;
}
public void SetParticleSystemInstance(GameObject instance)
{
SetParticleSystemInstance(instance, true);
}
public void SetParticleSystemInstance(GameObject instance, bool destroyOldParticles)
{
if (!instance) return;
foreach (Transform child in transform)
{
var go = child.gameObject;
go.SetActive(false);
if (destroyOldParticles)
{
Misc.Destroy(go);
}
}
var tr = instance.transform;
tr.SetParent(transform, false);
tr.localPosition = Vector3.zero;
RefreshParticles(instance);
}
public void SetParticleSystemPrefab(GameObject prefab)
{
if (!prefab) return;
SetParticleSystemInstance(Instantiate(prefab.gameObject), true);
}
public void RefreshParticles()
{
RefreshParticles(gameObject);
}
private void RefreshParticles(GameObject root)
{
if (!root) return;
root.GetComponentsInChildren(true, particles);
particles.RemoveAll(x => x.GetComponentInParent(true) != this);
for (var i = 0; i < particles.Count; i++)
{
var ps = particles[i];
var tsa = ps.textureSheetAnimation;
if (tsa.mode == ParticleSystemAnimationMode.Sprites && tsa.uvChannelMask == 0)
{
tsa.uvChannelMask = UVChannelFlags.UV0;
}
}
RefreshParticles(particles);
}
public void RefreshParticles(List particles)
{
// #246: Nullptr exceptions when using nested UIParticle components in hierarchy
_renderers.Clear();
foreach (Transform child in transform)
{
var uiParticleRenderer = child.GetComponent();
if (uiParticleRenderer != null)
{
_renderers.Add(uiParticleRenderer);
}
}
for (var i = 0; i < _renderers.Count; i++)
{
_renderers[i].Reset(i);
}
var j = 0;
for (var i = 0; i < particles.Count; i++)
{
var ps = particles[i];
if (!ps) continue;
GetRenderer(j++).Set(this, ps, false);
if (ps.trails.enabled)
{
GetRenderer(j++).Set(this, ps, true);
}
}
}
internal void UpdateTransformScale()
{
canvasScale = canvas.rootCanvas.transform.localScale.Inverse();
parentScale = transform.parent.lossyScale;
if (autoScalingMode != AutoScalingMode.Transform) return;
var newScale = parentScale.Inverse();
if (transform.localScale != newScale)
{
transform.localScale = newScale;
}
}
internal void UpdateRenderers()
{
if (!isActiveAndEnabled) return;
for (var i = 0; i < _renderers.Count; i++)
{
var r = _renderers[i];
if (!r)
{
RefreshParticles(particles);
break;
}
}
var bakeCamera = GetBakeCamera();
for (var i = 0; i < _renderers.Count; i++)
{
var r = _renderers[i];
if (!r) continue;
r.UpdateMesh(bakeCamera);
}
}
internal void UpdateParticleCount()
{
for (var i = 0; i < _renderers.Count; i++)
{
var r = _renderers[i];
if (!r) continue;
r.UpdateParticleCount();
}
}
internal void ResetGroupId()
{
_groupId = m_GroupId == m_GroupMaxId
? m_GroupId
: Random.Range(m_GroupId, m_GroupMaxId + 1);
}
protected override void UpdateMaterial()
{
}
///
/// Call to update the geometry of the Graphic onto the CanvasRenderer.
///
protected override void UpdateGeometry()
{
}
private void UpdateRendererMaterial()
{
for (var i = 0; i < _renderers.Count; i++)
{
var r = _renderers[i];
if (!r) continue;
r.maskable = maskable;
r.SetMaterialDirty();
}
}
internal UIParticleRenderer GetRenderer(int index)
{
if (_renderers.Count <= index)
{
_renderers.Add(UIParticleRenderer.AddRenderer(this, index));
}
if (!_renderers[index])
{
_renderers[index] = UIParticleRenderer.AddRenderer(this, index);
}
return _renderers[index];
}
private Camera GetBakeCamera()
{
if (!canvas) return Camera.main;
// When render mode is ScreenSpaceCamera or WorldSpace, use world camera.
var root = canvas.rootCanvas;
if (root.renderMode != RenderMode.ScreenSpaceOverlay)
{
return root.worldCamera ? root.worldCamera : Camera.main;
}
// When render mode is ScreenSpaceOverlay, use ortho-camera.
if (!_orthoCamera)
{
// Find existing ortho-camera.
foreach (Transform child in transform)
{
var cam = child.GetComponent();
if (cam && cam.name == "[generated] UIParticleOverlayCamera")
{
_orthoCamera = cam;
break;
}
}
// Create ortho-camera.
if (!_orthoCamera)
{
var go = new GameObject("[generated] UIParticleOverlayCamera") { hideFlags = HideFlags.DontSave | HideFlags.NotEditable };
go.SetActive(false);
go.transform.SetParent(transform, false);
_orthoCamera = go.AddComponent();
_orthoCamera.enabled = false;
}
}
//
var size = ((RectTransform)root.transform).rect.size;
_orthoCamera.orthographicSize = Mathf.Max(size.x, size.y) * root.scaleFactor;
_orthoCamera.transform.SetPositionAndRotation(new Vector3(0, 0, -1000), Quaternion.identity);
_orthoCamera.orthographic = true;
_orthoCamera.farClipPlane = 2000f;
return _orthoCamera;
}
private void UpdateTracker()
{
if (!enabled || !autoScaling || autoScalingMode != AutoScalingMode.Transform)
{
_tracker.Clear();
}
else
{
_tracker.Add(this, rectTransform, DrivenTransformProperties.Scale);
}
}
}
}