📜 ⬆️ ⬇️

Unity3D Speed ​​up drawing 2D animations at times? Easy

In this article, I would like to talk about how monster painting was accelerated when creating the game Alien Massacre. This solution is suitable for any projects that use sprite animation.

As a result of the development of a mobile game, it turned out that playing a large number of animated objects on the stage was a bottleneck. As a result, the following requirements emerged:


Out of the box solution


Of course, the first solution was simple: do everything with the help of the Animator component already built into UnityEngine. Let's see what comes out of it.

As an atlas with the original animation, we will use a malicious monster with 24 frames of sprite animation 64x64 pixels each:
')


In Unity3D we set the type of texture to sprite and in SpriteEditor we cut it into 24 pieces. Make an animation for it and throw it all on an empty object. Now is the time to recall the fact that we had a condition about different animation progress for various objects. No problem! A minute of work and the script is ready.

AnimationOffset.cs
using UnityEngine; namespace Kalita { [RequireComponent(typeof(Animator))] public class AnimationOffset : MonoBehaviour { public int Offset; public bool IsRandomOffset; private void Start() { var animator = GetComponent<Animator>(); var runtimeController = animator.runtimeAnimatorController; var clip = runtimeController.animationClips[0]; if (IsRandomOffset) Offset = Random.Range(0, (int) (clip.length*clip.frameRate)); var time = (Offset*clip.length/clip.frameRate); animator.Update(time); } } } 


Now we collect all this in a heap and we receive the decision which Unity3D gives "from a box".



Looking ahead, I would say that the out-of-the-box solution has quite a good performance and high flexibility. Everyone who works in Unity3D has long been accustomed to customize animators. But what if your application requires more performance?

Do-it-yourself solution


Let's start with a general concept:


Let's start with the render shader.

KalitaAtlasDrawer.shader
 Shader "Kalita/KalitaAtlasDrawer" { Properties { _MainTex ("Texture Atlas (RGBA)", 2D) = "" {} _Frame("Frame", float) = 0 _TotalFrames("Total Frames Count in Sequence", float) = 1 } SubShader { Tags { "Queue"="Transparent" } Blend SrcAlpha OneMinusSrcAlpha Cull Off pass { CGPROGRAM #pragma vertex vert #pragma fragment frag sampler2D _MainTex; float4 _MainTex_ST; float _Frame; float _TotalFrames; struct appData { float4 vertex : POSITION; fixed4 color : COLOR; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert (appData v) { v2f o; o.pos = mul (UNITY_MATRIX_MVP, v.vertex); float frame = (_Frame + v.color.a*255) % (_TotalFrames + 1); float offset = frame / _TotalFrames; o.uv = v.uv; o.uv.x += offset; return o; } fixed4 frag (v2f i) : COLOR { fixed4 color = tex2D (_MainTex, i.uv); return color; } ENDCG } } FallBack "Diffuse" } 


Next, we turn to the component that allows you to easily customize the animation settings from the Unity Editor.

KalitaAnimation.cs
 using UnityEngine; namespace Kalita { [ExecuteInEditMode] [RequireComponent(typeof (MeshFilter))] [RequireComponent(typeof (MeshRenderer))] public class KalitaAnimation : MonoBehaviour { public Material RendererMaterial { get { return meshRenderer.sharedMaterial; } } public Vector2 InGameSize = Vector2.one; public Vector2 Anchor = new Vector2(.5f, .5f); public int FramesCount = 1; public bool IsRandomStartAnimation; public byte StartFrame; private MeshFilter filter; private MeshRenderer meshRenderer; private void Awake() { filter = GetComponent<MeshFilter>(); meshRenderer = GetComponent<MeshRenderer>(); BuildMesh(); SetAnimationOffset(); } #if UNITY_EDITOR && !TEST_RUNNING private void Update() { if (Application.isPlaying) return; BuildMesh(); SetAnimationOffset(); var mat = meshRenderer.sharedMaterial; mat.mainTextureScale = new Vector2(1f / FramesCount, 1); } #endif private void BuildMesh() { var anchor = Anchor; anchor.Scale(InGameSize); anchor /= 2; var mesh = BuildQuad(InGameSize, anchor, new Vector2(1f / FramesCount, 1f)); filter.mesh = mesh; } private void SetAnimationOffset() { var mesh = filter.sharedMesh; mesh.name = "Plane"; var cnt = mesh.vertexCount; var clrs = mesh.colors32; if (clrs.Length != cnt) clrs = new Color32[cnt]; if (IsRandomStartAnimation && Application.isPlaying) StartFrame = (byte)Random.Range(0, 255); for (int i = 0; i < cnt; i++) clrs[i].a = StartFrame; mesh.colors32 = clrs; } public static Mesh BuildQuad(Vector2 size, Vector2 anchor, Vector2 uvStep) { var dx = size.x / 2; var dy = size.y / 2; var vertices = new[] { new Vector3(-dx + anchor.x, -dy + anchor.y, 0), new Vector3(dx + anchor.x, -dy + anchor.y, 0), new Vector3(dx + anchor.x, dy + anchor.y, 0), new Vector3(-dx + anchor.x, dy + anchor.y, 0), }; var uvs0 = new[] { uvStep, new Vector2(0, uvStep.y), new Vector2(0, 0), new Vector2(uvStep.x, 0), }; var indices = new[] { 0, 1, 2, 0, 2, 3 }; var mesh = new Mesh { vertices = vertices, uv = uvs0, triangles = indices }; mesh.Optimize(); return mesh; } } } 


This script works in the Unity3D editor and allows you to immediately see the change of any parameters on the stage, which makes the setup easy and convenient. Do not forget to create material with the above written shader and assign it to MeshRenderer. In the Unity3D editor, all this should look like this:



Well, now the simplest thing is to write a global frame counter. Here he is:

KalitaAtlasAC.cs
 using UnityEngine; namespace Kalita { [ExecuteInEditMode] public class KalitaAtlasAC : MonoBehaviour { public KalitaAnimation Animation; public float FrameRate = 24; [HideInInspector] public int CurrentGlobalFrame; private float lastGlobalFrameUpdateTime; private void Awake() { if (Animation == null) Animation = GetComponentInChildren<KalitaAnimation>(); } private void Update() { if (FrameRate <= 0) return; var t = Time.time; var nextUpdateTime = lastGlobalFrameUpdateTime + 1f/FrameRate; if (t < nextUpdateTime) return; var dt = t - lastGlobalFrameUpdateTime; lastGlobalFrameUpdateTime = t; //If we run too slow, we shoud add several frames per update CurrentGlobalFrame += (int) (dt*FrameRate); CurrentGlobalFrame %= Animation.FramesCount; Animation.RendererMaterial.SetFloat("_Frame", CurrentGlobalFrame); } } } 


For correct operation, one KalitaAtlasAC component controls the many KalitaAnimation components. Since the parameters are set via sharedMaterial, any of the many controlled objects is delayed in the corresponding field (animation) of KalitaAtlasAC.

Testing


Well, it's time for testing. For the test we make a small script that allows you to create the desired number of objects on the scene.

HabrSpawner.cs
 using System.Collections.Generic; using UnityEngine; namespace Kalita { public class HabrSpawner : MonoBehaviour { public List<GameObject> Objects = new List<GameObject>(); public int MobsToSpawn; private int mobOnScene; public Vector2 SpawnZone = new Vector2(10, 10); private void Start() { Screen.sleepTimeout = SleepTimeout.NeverSleep; SpawnMany(); } private void Update() { if (spawnMany) { spawnMany = false; SpawnMany(); } } [SerializeField] private bool spawnMany; private void SpawnMany() { const int layers = 5; var rectBorderSize = Vector2.one*2.4f; var mobsPerLayer = MobsToSpawn / layers; var zone = SpawnZone; for (int j = 0; j < layers; j++) { for (int i = 0; i < mobsPerLayer; i++) Spawn(zone); zone -= rectBorderSize; } } private void Spawn(Vector2 zone) { if (Objects.Count == 0) return; var i = Random.Range(0, Objects.Count); var o = Instantiate(Objects[i]); var p = GetRandomPositionOnRect(zone); Spawn(o, p); } private void Spawn(GameObject o, Vector2 pos) { mobOnScene++; o.SetActive(true); o.transform.position = pos; } private void OnGUI() { var w = 150; var h = 20; var x = 100; var y = 0; var rect = new Rect(x, y, w, h); //+One mob is source mob GUI.Label(rect, "MobsOnScene: " + (mobOnScene + 1)); } private Vector2 GetRandomPositionOnRect(Vector2 size) { var spawnRect = size; var resultPos = new Vector2(); switch (Random.Range(0, 4)) { case 0: // Top resultPos.x = Random.Range(0, spawnRect.x) - (spawnRect.x) / 2f; resultPos.y = spawnRect.y / 2; break; case 1: // Right resultPos.x = spawnRect.x / 2; resultPos.y = Random.Range(0, spawnRect.y) - (spawnRect.y) / 2; break; case 2: // Bottom resultPos.x = Random.Range(0, spawnRect.x) - (spawnRect.x) / 2; resultPos.y = -spawnRect.y / 2; break; case 3: // Left resultPos.x = -spawnRect.x / 2; resultPos.y = Random.Range(0, spawnRect.y) - (spawnRect.y) / 2; break; } return resultPos; } } } 


Compare the results. At first we will start in UnityEditor with the task to draw 20000 objects.

When using Unity3D Animator on my Dell M4800 laptop, we get about 5 FPS:



We start the same task with KalitaAtlasAC + KalitaAnimation and get 20+ FPS:



What will happen when testing on a real device? We will reduce the number of created objects to 2000, but we will still work on a mobile device. The Samsung Galaxy S3 - i9300 turned out to be a handy test subject. When using Unity3D Animator, we get about 9-10 FPS:



And when using KalitaAtlasAC + KalitaAnimation, we end up with 35+ FPS:



Results


If you use a large number of animated objects that use sprite animation, the proposed technique will reduce the cost of drawing up to four times, which can be very critical for mobile applications.

By the way, the remaining rgb components of the vertex color can be used as Overlay, as shown in the demo project.

The demo project can be downloaded here: bitbucket.org/Philipp0K/kalitaanimator

Source: https://habr.com/ru/post/302796/


All Articles