Animating Oscillations
Introduction
In coding a scrolling shooter or platformer, one needs to be able to define mathematical patterns for enemy behavior as legions fly by the player or run around on the screen. One such pattern is a legion in a line which flys in a follow the leader sine wave pattern. There is a math library with trigonometry functions for AVR, but that's not ideal when you are trying to squeeze cycles from game logic. This page explains a simple alternative to calculating functions using lookup tables of integer or unsigned values. It's quite simple, but the concept could be extended to non trivial animation routines using discrete tables or other simple discrete math techniques. I hope it inspires you to think of other more efficient methods of mathematical approximation for your Uzebox project.
Sine Tables and Precision
If you look at the appendix below you can see that I have created a lookup table that I will convert into 3 possible arrays. The arrays are each going to be either 64 or 128 elements depending on if you want to duplicate the values for the second half (negative mirror) of the cycle. I initially created these in a Google spreadsheet, to get the repetitive math out of the way. Then I copied them into Vim and ran a couple regular expressions to reformat them into a C array.
By using different coefficients (16, 32, and 64) you can obtain more or less precision / granularity in your sine value approximations. Since a sine wave is uniform, one could only store a quarter cycle in the array and achieve the rest through some pointer math.
Now that we have our array we can loop a pointer through it. Supposing that I want my sprite to move from right to left across the screen in a wave pattern I can blit it like this in my game loop (note that I'm not checking for screen edges for simplicity).
sinePtr = (sinePtr + 1) % 128; // for the full array with second half cycle MoveSprite(spriteIndex, mySprite.pos.x--, mySprite.pos.yOffset+sine16[sinePtr], mySprite.width, mySprite.height);
There's quite a bit you could do here.
- If you want the ocillations to be more dramatic you can stick a coefficient in front of the
sine16[sinePtr]
. - If you want to oscillate faster you can change
sinePtr + 1
tosinePtr + 2
. - If you want the swing to be less than 16 you can divide by two.
By now it should be apparent that if you find yourself using the sine16 array and multiplying by 2 you should probably be using the sine32 array without a multiplier for smoother animation.
Other Effects
Circular Animation
This uses two pointers, one is 32 slots ahead of the other (90 degrees).
sinePtrY = (sinePtrY + 1) % 128; sinePtrX = (sinePtrY + 32) % 128; //-- Move in a 16 pixel radius about the center point xOffset, yOffset MoveSprite(spriteIndex, mySprite.xOffset+sine16[sinePtrX], mySprite.yOffset+sine16[sinePtrY], mySprite.width, mySprite.height);
Sawtooth Attack Pattern
In this pattern the sprite moves across the screen and strike downward like a snake. Anyone who has use both probed on an oscilloscope in polar mode on a single sine signal will recognize this diagonal movement. We modify it with additional x positioning so the sprite moves across the screen while it executes the diagonal strike pattern.
sinePtr = (sinePtr + 1) % 128; MoveSprite(spriteIndex, (mySprite.pos.x--) + sine16[sinePtr], mySprite.yOffset+sine16[sinePtrY], mySprite.width, mySprite.height);
Bounding Pattern
In this pattern the sprite bounds across the screen like a rabbit. It simply only uses half of the sine cycle.
sinePtr = (sinePtr + 1) % 64; MoveSprite(spriteIndex, mySprite.pos.x--, mySprite.yOffset+sine16[sinePtrY], mySprite.width, mySprite.height);
Conclusion
By simply storing the results of your favorite mathematical functions into a small array you can achieve all sorts of animation. Think about ways you can modify the values from the lookup tables using multiplication, division (by two for savings ;) ), addition, subtraction and also interesting ways to traverse the table or only portions of it with some pointer math.
Here's one. I could have my sprite coming right at the player, then just before he gets there I invoke a single cycle of the bounding pattern to leap over the player. The player must remember to duck or at least not jump when this happens.
Appendix
Sine Arrays
char sine16[128] = {0, 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 8, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 12, 12, 11, 11, 10, 10, 9, 8, 8, 7, 6, 5, 5, 4, 3, 2, 2, 1, 0, -1, -2, -2, -3, -4, -5, -5, -6, -7, -8, -8, -9, -10, -10, -11, -11, -12, -12, -13, -13, -14, -14, -14, -15, -15, -15, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -16, -15, -15, -15, -14, -14, -14, -13, -13, -12, -12, -11, -11, -10, -10, -9, -8, -8, -7, -6, -5, -5, -4, -3, -2, -2, -1};
Sine Tables
Index | Angle (degrees) | Angle (radians) | Sine16 | Sine32 | Sine64 |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 |
1 | 2.8125 | 0.04908738521234 | 1 | 2 | 3 |
2 | 5.625 | 0.098174770424681 | 2 | 3 | 6 |
3 | 8.4375 | 0.14726215563702 | 2 | 5 | 9 |
4 | 11.25 | 0.19634954084936 | 3 | 6 | 12 |
5 | 14.0625 | 0.2454369260617 | 4 | 8 | 16 |
6 | 16.875 | 0.29452431127404 | 5 | 9 | 19 |
7 | 19.6875 | 0.34361169648638 | 5 | 11 | 22 |
8 | 22.5 | 0.39269908169872 | 6 | 12 | 24 |
9 | 25.3125 | 0.44178646691106 | 7 | 14 | 27 |
10 | 28.125 | 0.49087385212341 | 8 | 15 | 30 |
11 | 30.9375 | 0.53996123733575 | 8 | 16 | 33 |
12 | 33.75 | 0.58904862254809 | 9 | 18 | 36 |
13 | 36.5625 | 0.63813600776043 | 10 | 19 | 38 |
14 | 39.375 | 0.68722339297277 | 10 | 20 | 41 |
15 | 42.1875 | 0.73631077818511 | 11 | 21 | 43 |
16 | 45 | 0.78539816339745 | 11 | 23 | 45 |
17 | 47.8125 | 0.83448554860979 | 12 | 24 | 47 |
18 | 50.625 | 0.88357293382213 | 12 | 25 | 49 |
19 | 53.4375 | 0.93266031903447 | 13 | 26 | 51 |
20 | 56.25 | 0.98174770424681 | 13 | 27 | 53 |
21 | 59.0625 | 1.03083508945915 | 14 | 27 | 55 |
22 | 61.875 | 1.07992247467149 | 14 | 28 | 56 |
23 | 64.6875 | 1.12900985988383 | 14 | 29 | 58 |
24 | 67.5 | 1.17809724509617 | 15 | 30 | 59 |
25 | 70.3125 | 1.22718463030851 | 15 | 30 | 60 |
26 | 73.125 | 1.27627201552085 | 15 | 31 | 61 |
27 | 75.9375 | 1.32535940073319 | 16 | 31 | 62 |
28 | 78.75 | 1.37444678594553 | 16 | 31 | 63 |
29 | 81.5625 | 1.42353417115788 | 16 | 32 | 63 |
30 | 84.375 | 1.47262155637022 | 16 | 32 | 64 |
31 | 87.1875 | 1.52170894158256 | 16 | 32 | 64 |
32 | 90 | 1.5707963267949 | 16 | 32 | 64 |
33 | 92.8125 | 1.61988371200724 | 16 | 32 | 64 |
34 | 95.625 | 1.66897109721958 | 16 | 32 | 64 |
35 | 98.4375 | 1.71805848243192 | 16 | 32 | 63 |
36 | 101.25 | 1.76714586764426 | 16 | 31 | 63 |
37 | 104.0625 | 1.8162332528566 | 16 | 31 | 62 |
38 | 106.875 | 1.86532063806894 | 15 | 31 | 61 |
39 | 109.6875 | 1.91440802328128 | 15 | 30 | 60 |
40 | 112.5 | 1.96349540849362 | 15 | 30 | 59 |
41 | 115.3125 | 2.01258279370596 | 14 | 29 | 58 |
42 | 118.125 | 2.0616701789183 | 14 | 28 | 56 |
43 | 120.9375 | 2.11075756413064 | 14 | 27 | 55 |
44 | 123.75 | 2.15984494934298 | 13 | 27 | 53 |
45 | 126.5625 | 2.20893233455532 | 13 | 26 | 51 |
46 | 129.375 | 2.25801971976766 | 12 | 25 | 49 |
47 | 132.1875 | 2.30710710498 | 12 | 24 | 47 |
48 | 135 | 2.35619449019234 | 11 | 23 | 45 |
49 | 137.8125 | 2.40528187540469 | 11 | 21 | 43 |
50 | 140.625 | 2.45436926061703 | 10 | 20 | 41 |
51 | 143.4375 | 2.50345664582937 | 10 | 19 | 38 |
52 | 146.25 | 2.55254403104171 | 9 | 18 | 36 |
53 | 149.0625 | 2.60163141625405 | 8 | 16 | 33 |
54 | 151.875 | 2.65071880146639 | 8 | 15 | 30 |
55 | 154.6875 | 2.69980618667873 | 7 | 14 | 27 |
56 | 157.5 | 2.74889357189107 | 6 | 12 | 24 |
57 | 160.3125 | 2.79798095710341 | 5 | 11 | 22 |
58 | 163.125 | 2.84706834231575 | 5 | 9 | 19 |
59 | 165.9375 | 2.89615572752809 | 4 | 8 | 16 |
60 | 168.75 | 2.94524311274043 | 3 | 6 | 12 |
61 | 171.5625 | 2.99433049795277 | 2 | 5 | 9 |
62 | 174.375 | 3.04341788316511 | 2 | 3 | 6 |
63 | 177.1875 | 3.09250526837745 | 1 | 2 | 3 |
64 | 180 | 3.14159265358979 | 0 | 0 | 0 |