So, Unity recently announced added 2D game support, with the addition of Box2D physics and a sprite manager.
But there’s a few tricks you still need to keep in mind. Changing the images frame by frame is just the tip of the iceberg for animation; to really make your game run beautifully, you have to understand how to use translation and rotation to your advantage.
We’ll start with the basics for now though:
[expand title=”Frame Changing” tag=”h2″]
So, you have your textures ready for animation. You may be using the publicly available SpriteManager script, the paid version, or Unity’s own new version, in which case frame advances should be pretty second nature. Let’s say you’re using planes and textures for now though. It’s an inefficient method, but if you’re doing a game jam for instance, you might want to throw together something that’s functional and good looking, but not necessarily efficient. It’s also a fairly comprehensive method that covers all steps, some of which are cut out by sprite managers.
First of all, you’re going to want a public Texture[] array, so you can drag your textures into the object from Unity’s editor and an integer currentTexture initialised to 0 in Start(). Next you want a NextTexture() function that works like this:
[cpp]NextTexture(){
currentTexture++;
if(currentTexture>=textureArray.Length) currentTexture=0;
AnimatedPlane.renderer.material.mainTexture = textureArray[currentTexture];}[/cpp]
This will change the plane’s texture to the next frame in the animation.
There are two easy ways to call this function: Coroutine recursion and fixed intervals.
Using fixed intervals is the quickest (but less precise) method. You’re going to need an int counter, initialised to 0 in your Start() function, and a FixedUpdate() function, which updates every Time.deltaTime (you can vary this yourself in Unity’s Time Manager).
Inside FixedUpdate(), place your conditional (e.g. if(walking)), and inside it increment your counter with counter++. Then set the following statement:
[cpp]if(counter>=animationDelay){
counter=0;
NextTexture();
}
[/cpp]
Where animationDelay is an arbitrary value of your own choosing. This will advance the frames at a constant rate (depending on the rate you set in Unity’s Time Manager.
The second method is to use recursion. The downside to this is that it’s clumsier to deal with conditionals, but you’ll get the exact time delay you want. This is especially useful if you want a certain frame to be of a longer or shorter length. You’ll need an IEnumerator TextureChanger() and to StartCoroutine(TextureChanger()) in Start().
[cpp]IEnumerator TextureChanger(){
yield return new WaitForSeconds(timeInterval);
if([conditions]) NextTexture();
}
[/cpp]
Where timeInterval is a float of your choosing. With these functions, you can drag any number of textures onto your GameObject and it’ll animate correctly, provided you give the right conditions.
Now let’s move on to something more interesting.[/expand]
[expand title=”Smooth Movement to a Point” tag=”h2″]
The following formula is the holy grail of animation:
$latex currentvalue+=(finalvalue-currentvalue)\times slidespeed &s=2$
where 0<slidespeed<1. I recommend 0.1f as a good slidespeed value.
This formula will allow you to animate your objects beautifully to a point. This is extremely
useful in sliding GUIs, character control, level spawning, camera following, colour fading/shifting etc.
Here’s an advanced version of it from my upcoming game Rotation Station that moves towards a lower point first and then back to a higher point to make the little bob at the end. Every tile moves down according to that formula, although each one has a random delay, and a random initial rotation, which also uses this formula to rotate it into its desired orientation.
A really good example of using this for character controls is in my recent Ludum Dare entry Rude Bear Radio. If you go over to Practice Mode and try out the Space Invaders/Zero Wing parodies, you’ll see these formulae in action, as shown in the gif.
So, let’s look at how to apply it in the above example (a GameObject following the mouse).
First of all, we need to know where the mouse is on the 2D area. In order to find that, we first place this code into the FixedUpdate() function of the sliding GameObject:
[cpp]
Vector3 MousePosition = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x,Input.mousePosition.y, transform.position.z-Camera.main.transform.position.z));
[/cpp]
So, this uses the mouse’s x and y position, and the distance from the camera to the sliding GameObject to determine the Mouse’s position in 3d coordinates. Now we’re going to adapt the formula from the start of this section. So, remember,
$latex x_{n+1}=x_{n}+(x_{final}-x_{n})\times c &s=2$
[cpp]
transform.position += new Vector3((MousePosition.x-transform.position.x),(MousePosition.y-transform.position.y),0)*slidespeed;
[/cpp]
And there you are – done! Two lines of code. (I personally write it as one line of code, but 223 characters is fairly long for one line). For something like a GUI, you would then write a statement saying if(Mathf.Abs(finalvalue-currentvalue)<someamount), currentvalue = finalvalue and sliding=false. This way your object won’t carry on trying to slide forever – once it’s past a reasonable value you can just snap it into place.
Let’s look at another example, which you can see in Rude Bear Radio’s hard mode Mario stage.
The background animates from black to white slowly, using this formula:
[cpp]background.renderer.material.color = (1-factor)*background.renderer.material.color+factor*desiredcolor;[/cpp]
You can see here that it follows the basic form. Next = current+(final-current)*factor, just via shorthand. The factor starts low. The code checks whether the R value is within a certain range of the color, and if it is, it shifts up the factor so it fades quicker. Once the R value gets really close to 1, it sets the desired colour to black. You could also check for R, G and B, and advance the colours in an array (similar to our NextTexture() function from before). You can see an example of this constantly in the background of my second LD entry Rude Bear Rising (which is a buggy mess, but a good example of this feature, and also uses this formula to focus the camera on the player once they move too far from the screen).
Okay, so this has all been easy so far, you just stick in a formula and it’s done. The next (and possibly most important principle of all) requires a bit more thought than that.[/expand]
[expand title=”Trigonometry and You (Or: Why you should’ve been
paying attention in Maths)” tag=”h2″]
Trigonometry is crucial to animation. Frames are all well and good, but they won’t make things look truly beautiful, and sometimes you can do without the frames at all.
e.g. The first time I entered Ludum Dare, my housemate hand drew the pictures for me. I had one still image for each character. The solution? Use puppets on sticks.
Now, the way to achieve motion like this is very simple. Sin for translation, cos for rotation.
To create animation like this, you’re going to want the waves to stop and continue when you let go and input again, otherwise the motion will be extremely sporadic.
So you want an overarching variable (which I called walkbob), which adds Time.deltaTime onto it in FixedUpdate as long as the object is moving. Then you make your functions.
[cpp]translation = maxHeight*Mathf.Sin(speed*walkbob);
rotation = maxRoll*Mathf.Cos(speed*walkbob/2);[/cpp]
Then you simply set the position and rotation to these values (e.g. transform.position = new Vector3(transform.position.x,translation,transform.position.y)).
This will handle motion like that. However, a kind of animation that requires a bit more thought is what I like to call trig dancing. It’s useful for making cute characters dance to the music. You can see it here in Rude Bear Radio.
So, here’s how it works. First of all, as soon as you intend to start moving your object, you want to take a float initialtime = Time.time. This is so that your object starts in the correct position and orientation and doesn’t suddenly leap into action.
Next, we’re going to think properly about trig functions and what they mean.
We’re using simple harmonic motion, and this follows the form:
$latex y=Asin(2pi ft-phi )&s=2$
where y is the current value, A is the amplitude, f the frequency, t is the elapsed time and phi is the phase. First of all, the amplitude is easy to determine. That’s the maximum height or rotation we want our object to have.
Next is the elapsed time and phase. We’re going to handle both at once easily by replacing t by (Time.time-initialtime). This reduces φ to 0. So, finally, we just need our frequency. I would heavily recommend fitting the frequency of this to the frequency of your music (which is especially easy if you wrote it yourself).
If you don’t already know the BPM of your music, go here and tap every beat until you know it. If you have rhythm it’ll take no time. If you don’t, no worries, we’ll just take advantage of the central limit theorem. Just keep tapping for the whole duration of your song. The error on the average value will decrease with every tap.
So now you know it in beats per minute. You’ll need to divide this value by 60 to find out how many beats per second. Then you may want to divide it by 2 or 4 (or even more), if you only want to use motion on half a bar, or a full bar. This value is the frequency, and you can get pi from Mathf.PI. So now you just want to set the object’s position to that value. So, say you’re only modifying the height:
[cpp]transform.position = new Vector3(transform.position.x,maxheight*Mathf.Sin(2*Mathf.PI*frequency*(Time.time-initialtime)),transform.position.z);[/cpp]
But this isn’t good enough. First of all, we want the object to arrive on the beat, so it should start at its maximum amplitude. We want to use cos in this case. But more importantly, it should be leaping from side to side, so it shouldn’t just slide up and down like a wave. It needs to use cos^2, so it abruptly stops at the 0 mark and becomes positive again. Therefore:
[cpp]transform.position = new Vector3(transform.position.x,maxheight*Mathf.Pow(Mathf.Cos(2*Mathf.PI*frequency*(Time.time-initialtime)),2),transform.position.z)[/cpp]
This takes care of the dancing height. Finally the rotation should use sin, such that the rotation and translation are out of phase. So:
[cpp]transform.rotation = Quaternion.EulerAngles(0, maxRotation* Mathf.Cos(2*Mathf.PI*frequency*Mathf.Sin(Time.time-initialtime)), 0);[/cpp]
Here there are two things to keep in mind: if you’re using a plane and you want it to face the camera, those values aren’t going to be 0 and 0, but pi/2 and –pi/2 respectively. But also of great importance is that I’ve used EulerAngles rather than Euler here. EulerAngles is deprecated in favour of Euler, because Euler uses degrees (which Unity usually handles) and EulerAngles uses radians. We’re doing proper maths – we’re working in radians, so use EulerAngles! Otherwise you’re going to have to put in a conversion factor too.
Here you can see a similar kind of animation from the title screen of my upcoming game how you can change the scale instead of the position in the same way. You’ll have to use half the period and -cos^2 for this.
This brings us to the final category of animation I’ll discuss today:
[/expand]
[expand title=”Texture Offsets” tag=”h2″]
You can use everything you’ve learnt so far to manipulate texture offsets to make cool animated backgrounds. You can see this in my VVVVVV parody in Rude Bear Radio, and on its main screen. I’ll leave this one as an exercise. Grab a plane, stick a repeating texture on it, write up a FixedUpdate() function, and fiddle trigonometrically with these properties:
renderer.material.mainTextureOffset
renderer.material.color
This will cause the wall to slide around and change colour. Finally, if you want things to look really interesting, you can also play with renderer.material.mainTextureScale. It makes a really interesting looking effect, but it is rather distracting, and you don’t want this to detract from your main gameplay.
Finally, you may want to look into lerp and slerp. These can make animation easier, but personally, I find it nicer to stick to the formula in the second section.[/expand]
Enjoy!
@Alexrosegames