From 178a36514a4e60b38e148041359896f189e4112c Mon Sep 17 00:00:00 2001 From: Kulltero <33698270+Kulltero@users.noreply.github.com> Date: Thu, 20 Oct 2022 14:51:34 +0200 Subject: [PATCH 1/8] Create Community.UI.Animation.cs Creates the Animation Behaviour aswell as the AnimationProperty Class. These 2 serve as the Foundation of the animation System, new animation types can be added by adding new cases into the AnimateProperty() function --- Community.UI.Animation.cs | 115 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 Community.UI.Animation.cs diff --git a/Community.UI.Animation.cs b/Community.UI.Animation.cs new file mode 100644 index 0000000..a58c908 --- /dev/null +++ b/Community.UI.Animation.cs @@ -0,0 +1,115 @@ +using UnityEngine; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Facepunch.Extend; +using System.IO; + +#if CLIENT + +public partial class CommunityEntity +{ + private class Animation : MonoBehaviour + { + public List properties = new List(); + + public void Start() + { + foreach(var prop in properties) + { + prop.routine = StartCoroutine(prop.Animate()); + } + } + + public void Kill() + { + foreach(var prop in properties) + { + StopCoroutine(prop.routine); + } + properties.Clear(); + } + } + + private class AnimationProperty + { + public float duration = 0f; + public float delay = 0f; + public bool repeat = false; + public bool repeatDelay = 0f; + public string type; + public string from; + public string to; + + public Coroutine routine; + + public IEnumerator Animate() + { + if(delay > 0f) yield return new WaitForSeconds(delay); + + if(repeatDelay <= 0f) repeatDelay = duration; + + do + { + AnimateProperty(); + if(repeatDelay > 0f) yield return new WaitForSeconds(repeatDelay); + } + while(repeat); + } + + public void AnimateProperty() + { + switch(type){ + case "Opacity": + { + float fromOpacity; + float toOpacity; + + // we need a valid toOpacity + if(!float.TryParse(to, out toOpacity)) break; + // unlike the toOpacity, a from value is optional. if we omit a from value the crossfade will instead use the current alpha + float.TryParse(from, out fromOpacity); + + foreach(var c in gameObject.GetComponents()){ + if(fromOpacity != null) c.canvasRenderer.SetAlpha(fromOpacity); + c.CrossFadeAlpha(toOpacity, duration, true); + } + + break; + } + case "Color": + { + Color fromColor = ColorEx.Parse(from); + Color toColor = ColorEx.Parse(to); + + //dont use the from color if the from value was ommited + bool useFrom = string.IsNullOrEmpty(from); + + foreach(var c in gameObject.GetComponents()){ + if(useFrom) c.canvasRenderer.SetColor(fromColor); + c.CrossFadeColor(toColor, duration, true, false); + } + + break; + } + case "ColorWithAlpha": + { + Color fromColor = ColorEx.Parse(from); + Color toColor = ColorEx.Parse(to); + + //dont use the from color if the from value was ommited + bool useFrom = string.IsNullOrEmpty(from); + + foreach(var c in gameObject.GetComponents()){ + if(useFrom) c.canvasRenderer.SetColor(fromColor); + c.CrossFadeColor(toColor, duration, true, true); + } + + break; + } + } + } + } +} + +#endif From 6e9a924566a3a044f528766cbd209f82b61322d1 Mon Sep 17 00:00:00 2001 From: Kulltero <33698270+Kulltero@users.noreply.github.com> Date: Thu, 20 Oct 2022 15:23:05 +0200 Subject: [PATCH 2/8] Add Animation Case & Kill Animation on Destroy --- CommunityEntity.UI.cs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CommunityEntity.UI.cs b/CommunityEntity.UI.cs index 25bdc9f..0af16eb 100644 --- a/CommunityEntity.UI.cs +++ b/CommunityEntity.UI.cs @@ -321,6 +321,30 @@ private void CreateComponents( GameObject go, JSON.Object obj ) go.AddComponent(); break; } + case "Animation": + { + List props = new List(); + foreach(var prop in Obj.GetArray("properties")) + { + props.Add(new AnimationProperty{ + duration = prop.GetFloat("duration", 0f), + delay = prop.GetFloat("delay", 0f), + repeat = prop.GetBoolean("repeat", false), + repeatDelay = prop.GetFloat("repeatDelay", 0f), + type = prop.GetString("type", null), + from = prop.GetString("from", null), + to = prop.GetString("to", null) + }); + } + // dont bother creating the animation component if it has no properties + if(props.Count == 0) break; + + var a = go.AddComponent(); + a.properties = props; + + a.Start(); + break; + } } } @@ -386,6 +410,12 @@ private void DestroyPanel( string pnlName ) if ( !panel ) return; + + var animation = panel.GetComponent(); + if(animation){ + animation.Kill(); + } + var fadeOut = panel.GetComponent(); if ( fadeOut ) { From 42a3d25e3754dde9a791311395184879c4bf481a Mon Sep 17 00:00:00 2001 From: Kulltero <33698270+Kulltero@users.noreply.github.com> Date: Thu, 20 Oct 2022 15:27:56 +0200 Subject: [PATCH 3/8] force the Coroutine to wait atleast 1 frame this prevents an Edgecase if both repeatDelay & delay are set to 0. probably still a bad Time, but atleast it wont freeze the Client --- Community.UI.Animation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Community.UI.Animation.cs b/Community.UI.Animation.cs index a58c908..ff0b160 100644 --- a/Community.UI.Animation.cs +++ b/Community.UI.Animation.cs @@ -53,6 +53,7 @@ public IEnumerator Animate() { AnimateProperty(); if(repeatDelay > 0f) yield return new WaitForSeconds(repeatDelay); + else yield return null; } while(repeat); } From a282fd0517b50cdd34541f7c34aa3adb1ff01b2c Mon Sep 17 00:00:00 2001 From: Kulltero <33698270+Kulltero@users.noreply.github.com> Date: Fri, 21 Oct 2022 12:34:58 +0200 Subject: [PATCH 4/8] Changed repeat from boolean to an integer allows for limited repetition if positive, unlimited repetition if negative --- CommunityEntity.UI.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CommunityEntity.UI.cs b/CommunityEntity.UI.cs index 0af16eb..685d4e9 100644 --- a/CommunityEntity.UI.cs +++ b/CommunityEntity.UI.cs @@ -329,7 +329,7 @@ private void CreateComponents( GameObject go, JSON.Object obj ) props.Add(new AnimationProperty{ duration = prop.GetFloat("duration", 0f), delay = prop.GetFloat("delay", 0f), - repeat = prop.GetBoolean("repeat", false), + repeat = prop.GetInt("repeat", 0), repeatDelay = prop.GetFloat("repeatDelay", 0f), type = prop.GetString("type", null), from = prop.GetString("from", null), From 221cb2f9998034873ba6a1838698f77f46f2713d Mon Sep 17 00:00:00 2001 From: Kulltero <33698270+Kulltero@users.noreply.github.com> Date: Fri, 21 Oct 2022 12:40:23 +0200 Subject: [PATCH 5/8] changed repeat from bool to integer creates the ability to repeat an animation a limited amount of times. repeat 0 = the animation will not repeat repeat <0 = the animation will repeat until the panel is destroyed repeat >0 = the animation will repeat for `repeat ` amount of times (it will run `repeat` + 1 times in total) --- Community.UI.Animation.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Community.UI.Animation.cs b/Community.UI.Animation.cs index ff0b160..f0842ad 100644 --- a/Community.UI.Animation.cs +++ b/Community.UI.Animation.cs @@ -35,7 +35,7 @@ private class AnimationProperty { public float duration = 0f; public float delay = 0f; - public bool repeat = false; + public int repeat = 0; public bool repeatDelay = 0f; public string type; public string from; @@ -43,6 +43,8 @@ private class AnimationProperty public Coroutine routine; + public int completedRounds = 0; + public IEnumerator Animate() { if(delay > 0f) yield return new WaitForSeconds(delay); @@ -52,10 +54,11 @@ public IEnumerator Animate() do { AnimateProperty(); + completedRounds++; if(repeatDelay > 0f) yield return new WaitForSeconds(repeatDelay); else yield return null; } - while(repeat); + while(repeat < 0 || (repeat > 0 && completedRounds <= repeat)); } public void AnimateProperty() From 1cf3dd350be76b4900a67a7351f5992a6d6b5483 Mon Sep 17 00:00:00 2001 From: Kulltero <33698270+Kulltero@users.noreply.github.com> Date: Fri, 21 Oct 2022 12:45:33 +0200 Subject: [PATCH 6/8] Auto Kill Animation Coroutine OnDestroy Kill allready gets called in the DestroyPanel function, but it will not trigger on child panels, attaching the function to OnDestroy ensures that coroutines get killed off properly --- Community.UI.Animation.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Community.UI.Animation.cs b/Community.UI.Animation.cs index f0842ad..b6dc8c6 100644 --- a/Community.UI.Animation.cs +++ b/Community.UI.Animation.cs @@ -13,6 +13,8 @@ private class Animation : MonoBehaviour { public List properties = new List(); + public bool isKilled = false; + public void Start() { foreach(var prop in properties) @@ -23,12 +25,16 @@ public void Start() public void Kill() { + isKilled = true; foreach(var prop in properties) { StopCoroutine(prop.routine); } properties.Clear(); } + private void OnDestroy(){ + if(!isKilled) Kill(); + } } private class AnimationProperty From 4d54f0ce1a66a39e88b06c431be8316c85f12a6c Mon Sep 17 00:00:00 2001 From: Kulltero <33698270+Kulltero@users.noreply.github.com> Date: Mon, 24 Oct 2022 17:40:34 +0200 Subject: [PATCH 7/8] Caching, optimization when alpha = 0 - changed from Animation from MonoBehaviour to FacepunchBehaviour - Caching the Graphic components on Start(), should be better over constantly calling GetComponents, - this meains that Start will have to be called after all components are added (follow up commit comming) - Added Reference to Animation to each property on start, this is required because properties didnt have a reference to the gameObject so they couldnt get the graphics to change its properties prior to caching - Allowed the ability to Toggle Transparent Mesh Culling => likely a performance improvement if there are alot of elements with Alpha 0 - Color Animations will be instant/skipped if IsHidden & the alpha remains at 0 (or if its an Alphaless Transition) --- Community.UI.Animation.cs | 56 ++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/Community.UI.Animation.cs b/Community.UI.Animation.cs index b6dc8c6..e87d083 100644 --- a/Community.UI.Animation.cs +++ b/Community.UI.Animation.cs @@ -9,16 +9,22 @@ public partial class CommunityEntity { - private class Animation : MonoBehaviour + private class Animation : FacepunchBehaviour { public List properties = new List(); + public List cachedGraphics = new List(); + + public bool isHidden = false; + public bool isKilled = false; public void Start() { + CacheGraphics(); foreach(var prop in properties) { + prop.anim = this; prop.routine = StartCoroutine(prop.Animate()); } } @@ -35,6 +41,23 @@ public void Kill() private void OnDestroy(){ if(!isKilled) Kill(); } + + public void CacheGraphics(){ + cachedGraphics.Clear(); + gameObject.GetComponents(cachedGraphics); + } + public void ToggleTCulling(bool b){ + foreach(var c in cachedGraphics){ + c.cullTransparentMesh = b; + } + } + public void WaitToggleHidden(float delay = 0f){ + if(delay <= 0f) ToggleTCulling(true); + else Invoke(new Action(() => { + ToggleTCulling(true); + isHidden = true; + }), delay); + } } private class AnimationProperty @@ -47,6 +70,8 @@ private class AnimationProperty public string from; public string to; + public Animation anim; + public Coroutine routine; public int completedRounds = 0; @@ -79,11 +104,16 @@ public void AnimateProperty() if(!float.TryParse(to, out toOpacity)) break; // unlike the toOpacity, a from value is optional. if we omit a from value the crossfade will instead use the current alpha float.TryParse(from, out fromOpacity); - - foreach(var c in gameObject.GetComponents()){ + // disable TCulling if the toOpacity or FromOpacity are Higher than 0f, if the fromOpacity check was ommited this wouldnt trigger if its an animation from > 0 to 0 + if(anim.isHidden && toOpacity > 0f || (fromOpacity != null && fromOpacity > 0f)){ + anim.ToggleTCulling(false); + anim.isHidden = false; + } + foreach(var c in anim.cachedGraphics){ if(fromOpacity != null) c.canvasRenderer.SetAlpha(fromOpacity); c.CrossFadeAlpha(toOpacity, duration, true); } + if(!anim.isHidden && toOpacity <= 0f) anim.WaitToggleHidden(duration); break; } @@ -95,9 +125,9 @@ public void AnimateProperty() //dont use the from color if the from value was ommited bool useFrom = string.IsNullOrEmpty(from); - foreach(var c in gameObject.GetComponents()){ - if(useFrom) c.canvasRenderer.SetColor(fromColor); - c.CrossFadeColor(toColor, duration, true, false); + foreach(var c in anim.cachedGraphics){ + if(useFrom && !anim.isHidden) c.canvasRenderer.SetColor(fromColor); + c.CrossFadeColor(toColor, (anim.isHidden ? 0f : duration), true, false); } break; @@ -109,12 +139,18 @@ public void AnimateProperty() //dont use the from color if the from value was ommited bool useFrom = string.IsNullOrEmpty(from); - - foreach(var c in gameObject.GetComponents()){ - if(useFrom) c.canvasRenderer.SetColor(fromColor); - c.CrossFadeColor(toColor, duration, true, true); + if(anim.isHidden && toColor.a > 0f || (useFrom && fromColor.a > 0f)){ + anim.ToggleTCulling(false); + anim.isHidden = false; + } + bool doInstant = toColor.a <= 0f && (useFrom && fromColor.a <= 0f) || anim.isHidden; + foreach(var c in anim.cachedGraphics){ + if(useFrom && !doInstant) c.canvasRenderer.SetColor(fromColor); + c.CrossFadeColor(toColor, (doInstant ? 0f : duration), true, true); } + if(!anim.isHidden && toColor.a <= 0f) anim.WaitToggleHidden(duration); + break; } } From f6ba8cf13199d2dfe66bc2803c2b62426ab162a7 Mon Sep 17 00:00:00 2001 From: Kulltero <33698270+Kulltero@users.noreply.github.com> Date: Mon, 24 Oct 2022 18:13:59 +0200 Subject: [PATCH 8/8] Delaying Start() call after Components Ensured there can only be one Animation Component per object, merging properties if multiple are present Delaying Start until after all components are created - this ensures animations with 0 delay are applied to all components - ensures no components are missed by the CacheGraphics() Call in Start() --- CommunityEntity.UI.cs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/CommunityEntity.UI.cs b/CommunityEntity.UI.cs index 685d4e9..41cde7c 100644 --- a/CommunityEntity.UI.cs +++ b/CommunityEntity.UI.cs @@ -80,6 +80,12 @@ public void AddUI( RPCMessage msg ) { CreateComponents( go, component.Obj ); } + + Animation anim = go.GetComponent(); + if(anim){ + if(anim.properties.Count == 0) Object.Destroy(anim); + else anim.Start(); + } if ( json.ContainsKey( "fadeOut" ) ) { @@ -104,7 +110,7 @@ private GameObject FindPanel( string name ) private void CreateComponents( GameObject go, JSON.Object obj ) { - // + // // This is the 'stupid' but 'safe & predictable way of doing this. // switch ( obj.GetString( "type", "UnityEngine.UI.Text" ) ) @@ -323,10 +329,12 @@ private void CreateComponents( GameObject go, JSON.Object obj ) } case "Animation": { - List props = new List(); + // Ensure there's only ever one Animation Component per gameObject, adding onto their existing Properties if one allready exists + Animation anim = go.GetComponent(); + if(!anim) anim = go.AddComponent(); foreach(var prop in Obj.GetArray("properties")) { - props.Add(new AnimationProperty{ + anim.properties.Add(new AnimationProperty{ duration = prop.GetFloat("duration", 0f), delay = prop.GetFloat("delay", 0f), repeat = prop.GetInt("repeat", 0), @@ -336,13 +344,6 @@ private void CreateComponents( GameObject go, JSON.Object obj ) to = prop.GetString("to", null) }); } - // dont bother creating the animation component if it has no properties - if(props.Count == 0) break; - - var a = go.AddComponent(); - a.properties = props; - - a.Start(); break; } }