Unity3D Прискорити обробку 2D анімації в рази? Легко

стаття мені хотілося б розповісти про те, як була прискорена відтворення монстрів при створенні гри Alien Massacre. Дане рішення підійде для будь-яких проектів, які испольуют спрайтовую анімацію.

В результаті розробки мобільної гри виявилося, що досить вузьким місцем стало програвання великої кількості анімованих об'єктів на сцені. У результаті сформувалися наступні вимоги:

  • 1 Необхідно забезпечити обробку великої кількості анімованих об'єктів на сцені. Адже ми хочемо, щоб гравець відстрілювався від полчищ монстрів.
  • 2 Прогрес анімації повинен бути різний для кожного з об'єктів. Адже ми не хочемо, щоб моби ходили строєм.
Рішення «з коробки»
Безумовно, перше рішення було простим: все зробити за допомогою вбудованого в UnityEngine компонента Animator. Подивимося, що з цього виходить.

Як атласу з вихідної анімацією будемо використовувати зломонстра з 24 кадрами спрайтову анімації 64х64 пікселів кожен:



У Unity3D задаємо тип текстури sprite і в SpriteEditor нарізаємо його на 24 шматка. Робимо для нього анімацію і закидаємо все це на порожній об'єкт. Тут саме час згадати про те, що у нас була умова про різний прогрес анімації для різних об'єктів. Не питання! Хвилина роботи і скрипт готовий.

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);
}
}
}


Тепер збираємо все це у купу і отримуємо рішення, яке Unity3D надає «з коробки».



Забігаючи вперед, скажу, що рішення «з коробки» має досить непогану продуктивність і високу гнучкість. Налаштовувати аніматори вже давно звикли всі, хто працюють в Unity3D. Але що робити, якщо ваш додаток вимагає більшої продуктивності?

Рішення «зроби сам»
Почнемо з загального концепту:

  • Зробимо розрахунок прогресу анімації в вертексном шейдере
  • можна закодувати інформацію про початковому кадрі анімації («локальний прогрес») в альфа-каналі кольору вертекса (щоб не втратити батчинг)
  • Створимо компонент, який спрощує налаштування анімації в Unity Editor
  • Створимо компонент, який буде розраховувати «глобальний» прогрес анімації
Почнемо з шейдера відтворення.

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"
}


Далі перейдемо до компоненту, який дозволяє легко настроювати параметри анімації з 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;
}
}
}


Цей скрипт працює в редакторі Unity3D і дозволяє відразу побачити зміни будь-яких параметрів на сцені, що робить настройку простою і зручною. Не забуваємо створити матеріал з вище написаним шейдером і призначити його в MeshRenderer. В редакторі Unity3D все це має виглядати наступним чином:



Ну і тепер залишилося найпростіше, написати глобальний лічильник кадрів. Ось і він:

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);
}
}
}


Для коректної роботи один компонент KalitaAtlasAC контролює безліч компонентів KalitaAnimation. Так як параметри встановлюються через sharedMaterial, то у відповідне поле (animation) KalitaAtlasAC затягується будь-який з безлічі об'єктів, що контролюються.

Тестування
Що ж, настав час для тестування. Для тіста робимо невеликий скрипт, який дозволяє створювати бажану кількість об'єктів на сцені.

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.4 f;
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;
}
}
}


Порівняємо результати. Спершу запустимо в UnityEditor з завданням промалювати 20000 об'єктів.

При використанні Unity3D Animator на моєму ноутбуці Dell M4800 отримуємо близько 5 FPS:



Запускаємо тугіше завдання з KalitaAtlasAC + KalitaAnimation і отримуємо 20+ FPS:



Що ж буде при тестуванні на реальному девайсі? Зменшимо кількість створюваних об'єктів до 2000, ми ж все-таки на мобільному пристрої працювати будемо. В якості піддослідного під рукою опинився Samsung Galaxy S3 — i9300. При використанні Unity3D Animator отримуємо близько 9-10 FPS:



А при використанні KalitaAtlasAC + KalitaAnimation в результаті маємо 35+ FPS:



Підсумки
Якщо ви використовуєте велику кількість анімованих об'єктів, які використовують спрайтовую анімацію, запропонована техніка дозволить зменшити витрати на обробку до чотирьох разів, що може бути вельми критично для мобільних додатків.

До речі, залишилися rgb компоненти кольору вертекса можна використовувати в якості Overlay, як це зробити показано в демо проекті.

Демо проект можна скачати тут.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.