Animating Tiled texture

From Unify Community Wiki
Jump to: navigation, search

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): Torchanimation 135.png

(Leo Nogueira) Adding a simple image with multiple rows for testing purposes and a modified version of the C# Script:
TilesTestPNG.png

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, or appear to jump around frames. To correct this problem, replace the following line:

			                             (index / columns) / (float)rows);          //y index

With:

			                             1 - (index / columns) / (float)rows);          //y index

Notice the

Personal tools
Namespaces

Variants
Actions
Navigation
Extras
Toolbox