Animating Tiled texture
Author: Joachim Ante
Contents |
Description
This script animates a texture containing tiles of an animation. You can give it a framerate to determine the speed of the animation and set how many tiles on x, y there are.
Usage
Attach this script to the object that has a material with the tiled texture. To avoid distortion, the proportions of the object must be the same as the proportions of each tile (eg 1:2 for the sheet below).
Here is an example of how to lay out a texture for it (Thanks to BigBrainz for providing it):
JavaScript - AnimatedTextureUV.js
var uvAnimationTileX = 24; //Here you can place the number of columns of your sheet. //The above sheet has 24 var uvAnimationTileY = 1; //Here you can place the number of rows of your sheet. //The above sheet has 1 var framesPerSecond = 10.0; function Update () { // Calculate index var index : int = Time.time * framesPerSecond; // repeat when exhausting all frames index = index % (uvAnimationTileX * uvAnimationTileY); // Size of every tile var size = Vector2 (1.0 / uvAnimationTileX, 1.0 / uvAnimationTileY); // split into horizontal and vertical index var uIndex = index % uvAnimationTileX; var vIndex = index / uvAnimationTileX; // build offset // v coordinate is the bottom of the image in opengl so we need to invert. var offset = Vector2 (uIndex * size.x, 1.0 - size.y - vIndex * size.y); renderer.material.SetTextureOffset ("_MainTex", offset); renderer.material.SetTextureScale ("_MainTex", size); }
CSharp - SpritSheet.cs
This is just a CSharp version of the AnimatedTextureUV.js above.
public class SpriteSheet : MonoBehaviour { public int _uvTieX = 1; public int _uvTieY = 1; public int _fps = 10; private Vector2 _size; private Renderer _myRenderer; private int _lastIndex = -1; void Start () { _size = new Vector2 (1.0f / _uvTieX , 1.0f / _uvTieY); _myRenderer = renderer; if(_myRenderer == null) enabled = false; } // Update is called once per frame void Update() { // Calculate index int index = (int)(Time.timeSinceLevelLoad * _fps) % (_uvTieX * _uvTieY); if(index != _lastIndex) { // split into horizontal and vertical index int uIndex = index % _uvTieX; int vIndex = index / _uvTieY; // build offset // v coordinate is the bottom of the image in opengl so we need to invert. Vector2 offset = new Vector2 (uIndex * _size.x, 1.0f - _size.y - vIndex * _size.y); _myRenderer.material.SetTextureOffset ("_MainTex", offset); _myRenderer.material.SetTextureScale ("_MainTex", _size); _lastIndex = index; } } }
CSharp - SpritSheetNG.cs
The CSharp version of the script was not working with multiple rows so i made some changes.
public class SpriteSheetNG : MonoBehaviour { private float iX=0; private float iY=1; public int _uvTieX = 1; public int _uvTieY = 1; public int _fps = 10; private Vector2 _size; private Renderer _myRenderer; private int _lastIndex = -1; void Start () { _size = new Vector2 (1.0f / _uvTieX , 1.0f / _uvTieY); _myRenderer = renderer; if(_myRenderer == null) enabled = false; _myRenderer.material.SetTextureScale ("_MainTex", _size); } void Update() { int index = (int)(Time.timeSinceLevelLoad * _fps) % (_uvTieX * _uvTieY); if(index != _lastIndex) { Vector2 offset = new Vector2(iX*_size.x, 1-(_size.y*iY)); iX++; if(iX / _uvTieX == 1) { if(_uvTieY!=1) iY++; iX=0; if(iY / _uvTieY == 1) { iY=1; } } _myRenderer.material.SetTextureOffset ("_MainTex", offset); _lastIndex = index; } } }
CSharp - AnimateTiledTexture
A version using coroutines. Slightly faster since it doesn't update every frame and only sets the texture scale once.
using UnityEngine; using System.Collections; class AnimateTiledTexture : MonoBehaviour { public int columns = 2; public int rows = 2; public float framesPerSecond = 10f; //the current frame to display private int index = 0; void Start() { StartCoroutine(updateTiling()); //set the tile size of the texture (in UV units), based on the rows and columns Vector2 size = new Vector2(1f / columns, 1f / rows); renderer.sharedMaterial.SetTextureScale("_MainTex", size); } private IEnumerator updateTiling() { while (true) { //move to the next index index++; if (index >= rows * columns) index = 0; //split into x and y indexes Vector2 offset = new Vector2((float)index / columns - (index / columns), //x index (index / columns) / (float)rows); //y index renderer.sharedMaterial.SetTextureOffset("_MainTex", offset); yield return new WaitForSeconds(1f / framesPerSecond); } } }
CSharp - AnimateTiledTexture Extended By Bric Rogers (LITE3D)
An extended version of the AnimatedTiledTexture which adds a few advanced features.
- Scale - Scale the texture on the 3D object.
- Offset - Your texture will by default be centered on the 3D object. Use this to offset the texture
- Buffer - If your texture has grid lines or if some tiles extend into others, you can use this to buffer out unwanted artifacts
- PlayOnce - The animation will only be played one time through
- DisableUponCompletion - The objects renderer will be disabled when the animation is completed
- EnableEvents - Enable this if you want an event fired when the animation is completed
- PlayOnEnable - The animation will begin playing when the object is enabled
- NewMaterialInstance - Will create a copy of the material. This is useful if you want several AnimatedTiledTextures using the same material but playing them at different times.
using UnityEngine; using System.Collections; using System.Collections.Generic; public class AnimateTiledTexture : MonoBehaviour { public int _columns = 2; // The number of columns of the texture public int _rows = 2; // The number of rows of the texture public Vector2 _scale = new Vector3(1f, 1f); // Scale the texture. This must be a non-zero number. negative scale flips the image public Vector2 _offset = Vector2.zero; // You can use this if you do not want the texture centered. (These are very small numbers .001) public Vector2 _buffer = Vector2.zero; // You can use this to buffer frames to hide unwanted grid lines or artifacts public float _framesPerSecond = 10f; // Frames per second that you want to texture to play at public bool _playOnce = false; // Enable this if you want the animation to only play one time public bool _disableUponCompletion = false; // Enable this if you want the texture to disable the renderer when it is finished playing public bool _enableEvents = false; // Enable this if you want to register an event that fires when the animation is finished playing public bool _playOnEnable = true; // The animation will play when the object is enabled public bool _newMaterialInstance = false; // Set this to true if you want to create a new material instance private int _index = 0; // Keeps track of the current frame private Vector2 _textureSize = Vector2.zero; // Keeps track of the texture scale private Material _materialInstance = null; // Material instance of the material we create (if needed) private bool _hasMaterialInstance = false; // A flag so we know if we have a material instance we need to clean up (better than a null check i think) private bool _isPlaying = false; // A flag to determine if the animation is currently playing public delegate void VoidEvent(); // The Event delegate private List<VoidEvent> _voidEventCallbackList; // A list of functions we need to call if events are enabled // Use this function to register your callback function with this script public void RegisterCallback(VoidEvent cbFunction) { // If events are enabled, add the callback function to the event list if (_enableEvents) _voidEventCallbackList.Add(cbFunction); else Debug.LogWarning("AnimateTiledTexture: You are attempting to register a callback but the events of this object are not enabled!"); } // Use this function to unregister a callback function with this script public void UnRegisterCallback(VoidEvent cbFunction) { // If events are enabled, unregister the callback function from the event list if (_enableEvents) _voidEventCallbackList.Remove(cbFunction); else Debug.LogWarning("AnimateTiledTexture: You are attempting to un-register a callback but the events of this object are not enabled!"); } public void Play() { // If the animation is playing, stop it if (_isPlaying) { StopCoroutine("updateTiling"); _isPlaying = false; } // Make sure the renderer is enabled renderer.enabled = true; //Because of the way textures calculate the y value, we need to start at the max y value _index = _columns; // Start the update tiling coroutine StartCoroutine(updateTiling()); } public void ChangeMaterial(Material newMaterial, bool newInstance = false) { if (newInstance) { // First check our material instance, if we already have a material instance // and we want to create a new one, we need to clean up the old one if (_hasMaterialInstance) Object.Destroy(renderer.sharedMaterial); // create the new material _materialInstance = new Material(newMaterial); // Assign it to the renderer renderer.sharedMaterial = _materialInstance; // Set the flag _hasMaterialInstance = true; } else // if we dont have create a new instance, just assign the texture renderer.sharedMaterial = newMaterial; // We need to recalc the texture size (since different material = possible different texture) CalcTextureSize(); // Assign the new texture size renderer.sharedMaterial.SetTextureScale("_MainTex", _textureSize); } private void Awake() { // Allocate memory for the events, if needed if (_enableEvents) _voidEventCallbackList = new List<VoidEvent>(); //Create the material instance, if needed. else, just use this function to recalc the texture size ChangeMaterial(renderer.sharedMaterial, _newMaterialInstance); } private void OnDestroy() { // If we wanted new material instances, we need to destroy the material if (_hasMaterialInstance) { Object.Destroy(renderer.sharedMaterial); _hasMaterialInstance = false; } } // Handles all event triggers to callback functions private void HandleCallbacks(List<VoidEvent> cbList) { // For now simply loop through them all and call yet. for (int i = 0; i < cbList.Count; ++i) cbList[i](); } private void OnEnable() { CalcTextureSize(); if (_playOnEnable) Play(); } private void CalcTextureSize() { //set the tile size of the texture (in UV units), based on the rows and columns _textureSize = new Vector2(1f / _columns, 1f / _rows); // Add in the scale _textureSize.x = _textureSize.x / _scale.x; _textureSize.y = _textureSize.y / _scale.y; // Buffer some of the image out (removes gridlines and stufF) _textureSize -= _buffer; } // The main update function of this script private IEnumerator updateTiling() { _isPlaying = true; // This is the max number of frames int checkAgainst = (_rows * _columns); while (true) { // If we are at the last frame, we need to either loop or break out of the loop if (_index >= checkAgainst) { _index = 0; // Reset the index // If we only want to play the texture one time if (_playOnce) { if (checkAgainst == _columns) { // We are done with the coroutine. Fire the event, if needed if(_enableEvents) HandleCallbacks(_voidEventCallbackList); if (_disableUponCompletion) gameObject.renderer.enabled = false; // turn off the isplaying flag _isPlaying = false; // Break out of the loop, we are finished yield break; } else checkAgainst = _columns; // We need to loop through one more row } } // Apply the offset in order to move to the next frame ApplyOffset(); //Increment the index _index++; // Wait a time before we move to the next frame. Note, this gives unexpected results on mobile devices yield return new WaitForSeconds(1f / _framesPerSecond); } } private void ApplyOffset() { //split into x and y indexes. calculate the new offsets Vector2 offset = new Vector2((float)_index / _columns - (_index / _columns), //x index 1 - ((_index / _columns) / (float)_rows)); //y index // Reset the y offset, if needed if (offset.y == 1) offset.y = 0.0f; // If we have scaled the texture, we need to reposition the texture to the center of the object offset.x += ((1f / _columns) - _textureSize.x) / 2.0f; offset.y += ((1f / _rows) - _textureSize.y) / 2.0f; // Add an additional offset if the user does not want the texture centered offset.x += _offset.x; offset.y += _offset.y; // Update the material renderer.sharedMaterial.SetTextureOffset("_MainTex", offset); } }
The following is an example of how to use the events:
void Start() { if (_animatedTileTexture == null) { Debug.LogWarning("No animated tile texture script assigned!"); } else _animatedTileTexture.RegisterCallback(AnimationFinished); } // This function will get called by the AnimatedTiledTexture script when the animation is completed if the EnableEvents option is set to true void AnimationFinished() { // The animation is finished } public AnimateTiledTexture _animatedTileTexture; // A reference to AnimatedTileTexture object
CSharp - AnimateSpriteSheet.cs
This one will cycle through sheets from TOP LEFT to BOTTOM RIGHT regardless of size. The two simple scripts above did not seem to either follow top-left-bottom-right or work when sheets got larger than the one they were tested on.
Since many games rely on waiting for Coroutines to finish, the variable RunTimeInSeconds is supplied, so a script can do a WaitForSeconds() when waiting for it to return.
class AnimateSpriteSheet : MonoBehaviour { public int Columns = 5; public int Rows = 5; public float FramesPerSecond = 10f; public bool RunOnce = true; public float RunTimeInSeconds { get { return ( (1f / FramesPerSecond) * (Columns * Rows) ); } } private Material materialCopy = null; void Start() { // Copy its material to itself in order to create an instance not connected to any other materialCopy = new Material(renderer.sharedMaterial); renderer.sharedMaterial = materialCopy; Vector2 size = new Vector2(1f / Columns, 1f / Rows); renderer.sharedMaterial.SetTextureScale("_MainTex", size); } void OnEnable() { StartCoroutine(UpdateTiling()); } private IEnumerator UpdateTiling() { float x = 0f; float y = 0f; Vector2 offset = Vector2.zero; while (true) { for (int i = Rows-1; i >= 0; i--) // y { y = (float) i / Rows; for (int j = 0; j <= Columns-1; j++) // x { x = (float) j / Columns; offset.Set(x, y); renderer.sharedMaterial.SetTextureOffset("_MainTex", offset); yield return new WaitForSeconds(1f / FramesPerSecond); } } if (RunOnce) { yield break; } } } }
Note: In some cases, tiled textures may play in reverse. To correct this problem, replace the following line:
offset = new Vector2((float)index / columns - (index / columns), //x index (index / columns) / (float)rows); //y index
With:
offset = new Vector2((float)index / columns - (index / columns), //x index 1 - (index / columns) / (float)rows); //y index