//-------------------------------------------------------------------------------------------------------------------------------- // Cartoon FX // (c) 2012-2020 Jean Moreno //-------------------------------------------------------------------------------------------------------------------------------- using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace CartoonFX { [RequireComponent(typeof(ParticleSystem))] public class CFXR_ParticleText_Runtime : MonoBehaviour { [Header("Text")] public string text; public float size = 1f; public float letterSpacing = 0.44f; [Header("Colors")] public Color backgroundColor = new Color(0, 0, 0, 1); public Color color1 = new Color(1, 1, 1, 1); public Color color2 = new Color(0, 0, 1, 1); [Header("Delay")] public float delay = 0.05f; public bool cumulativeDelay = false; [Range(0f, 2f)] public float compensateLifetime = 0; [Header("Misc")] public float lifetimeMultiplier = 1f; [Range(-90f, 90f)] public float rotation = -5f; public float sortingFudgeOffset = 0.1f; public CFXR_ParticleTextFontAsset font; #if UNITY_EDITOR void OnValidate() { this.hideFlags = HideFlags.None; if (text == null || font == null) { return; } // parse text to only allow valid characters List allowed = new List(font.CharSequence.ToCharArray()); allowed.Add(' '); var chars = text.ToUpperInvariant().ToCharArray(); string newText = ""; foreach (var c in chars) { if (allowed.Contains(c)) { newText += c; } } text = newText; // prevent negative or 0 size size = Mathf.Max(0.001f, size); } #endif void Awake() { InitializeFirstParticle(); } float baseLifetime; float baseScaleX; float baseScaleY; float baseScaleZ; Vector3 basePivot; public void InitializeFirstParticle() { if (this.transform.childCount == 0) { Debug.LogError("CFXR_ParticleText_Runtime requires a child with a Particle System component to act as the model for other letters."); return; } var particleSystem = this.transform.GetChild(0).GetComponent(); baseLifetime = particleSystem.main.startLifetime.constant; baseScaleX = particleSystem.main.startSizeXMultiplier; baseScaleY = particleSystem.main.startSizeYMultiplier; baseScaleZ = particleSystem.main.startSizeZMultiplier; basePivot = particleSystem.GetComponent().pivot; } public void GenerateText(string text) { if (text == null || font == null || !font.IsValid()) { return; } if (this.transform.childCount == 0) { Debug.LogError("CFXR_ParticleText_Runtime requires a child with a Particle System component to act as the model for other letters."); return; } // process text and calculate total width offset float totalWidth = 0f; int charCount = 0; for (int i = 0; i < text.Length; i++) { if (char.IsWhiteSpace(text[i])) { if (i > 0) { totalWidth += letterSpacing * size; } } else { charCount++; if (i > 0) { int index = font.CharSequence.IndexOf(text[i]); var sprite = font.CharSprites[index]; float charWidth = sprite.rect.width + font.CharKerningOffsets[index].post + font.CharKerningOffsets[index].pre; totalWidth += (charWidth * 0.01f + letterSpacing) * size; } } } #if UNITY_EDITOR // delete all children in editor, to make sure we refresh the particle systems based on the first one if (!Application.isPlaying) { int length = this.transform.childCount; int overflow = 0; while (this.transform.childCount > 1) { Object.DestroyImmediate(this.transform.GetChild(this.transform.childCount - 1).gameObject); overflow++; if (overflow > 1000) { // just in case... Debug.LogError("Overflow!"); break; } } } #endif // calculate needed instantiations int childCount = this.transform.childCount - 1; // first one is the particle source and always deactivated if (childCount < charCount) { // instantiate new letter GameObjects if needed GameObject model = this.transform.GetChild(0).gameObject; for (int i = childCount; i < charCount; i++) { var newLetter = Instantiate(model); newLetter.transform.SetParent(this.transform); newLetter.transform.localPosition = Vector3.zero; newLetter.transform.localRotation = Quaternion.identity; } } // update each letter float offset = totalWidth / 2f; totalWidth = 0f; int currentChild = 0; for (int i = 0; i < text.Length; i++) { var letter = text[i]; if (char.IsWhiteSpace(letter)) { totalWidth += letterSpacing * size; } else { currentChild++; int index = font.CharSequence.IndexOf(text[i]); var sprite = font.CharSprites[index]; // calculate char particle size ratio var ratio = size * sprite.rect.width / 50f; // calculate char position totalWidth += font.CharKerningOffsets[index].pre * 0.01f * size; var position = (totalWidth-offset)/ratio; float charWidth = sprite.rect.width + font.CharKerningOffsets[index].post; totalWidth += (charWidth * 0.01f + letterSpacing) * size; // update particle system for this letter var letterObj = this.transform.GetChild(currentChild).gameObject; letterObj.name = letter.ToString(); var particleSystem = letterObj.GetComponent(); var mainModule = particleSystem.main; mainModule.startSizeXMultiplier = baseScaleX * ratio; mainModule.startSizeYMultiplier = baseScaleY * ratio; mainModule.startSizeZMultiplier = baseScaleZ * ratio; particleSystem.textureSheetAnimation.SetSprite(0, sprite); mainModule.startRotation = Mathf.Deg2Rad * rotation; mainModule.startColor = backgroundColor; var customData = particleSystem.customData; customData.enabled = true; customData.SetColor(ParticleSystemCustomData.Custom1, color1); customData.SetColor(ParticleSystemCustomData.Custom2, color2); if (cumulativeDelay) { mainModule.startDelay = delay * i; mainModule.startLifetime = Mathf.LerpUnclamped(baseLifetime, baseLifetime + (delay * (text.Length-i)), compensateLifetime); } else { mainModule.startDelay = delay; } mainModule.startLifetime = mainModule.startLifetime.constant * lifetimeMultiplier; // particle system renderer parameters var particleRenderer = particleSystem.GetComponent(); particleRenderer.enabled = true; particleRenderer.pivot = new Vector3(basePivot.x + position, basePivot.y, basePivot.z); particleRenderer.sortingFudge += i * sortingFudgeOffset; } } // set active state for needed letter only for (int i = 1, l = this.transform.childCount; i < l; i++) { this.transform.GetChild(i).gameObject.SetActive(i <= charCount); } // play all this.GetComponent().Play(true); } } #if UNITY_EDITOR [CustomEditor(typeof(CFXR_ParticleText_Runtime))] public class ParticleTextRuntimeEditor : Editor { CFXR_ParticleText_Runtime castTarget { get { return (CFXR_ParticleText_Runtime)this.target; } } GUIContent UpdateTextLabel = new GUIContent(" Update Text ", "Regenerate the text and create new letter GameObjects if needed."); public override void OnInspectorGUI() { var prefab = PrefabUtility.GetPrefabInstanceStatus(target); if (prefab != PrefabInstanceStatus.NotAPrefab) { EditorGUILayout.HelpBox("Cartoon FX Particle Text doesn't work on Prefab Instances, as it needs to destroy/create children GameObjects.\nYou can right-click on the object, and select \"Unpack Prefab Completely\" to make it an independent Game Object.", MessageType.Warning); return; } base.OnInspectorGUI(); GUILayout.Space(8); GUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button(UpdateTextLabel, GUILayout.Height(30))) { castTarget.InitializeFirstParticle(); castTarget.GenerateText(castTarget.text); } GUILayout.EndHorizontal(); } } #endif }