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.
We'll be making tables of Sine Wave approximation (http://en.wikipedia.org/wiki/Sine_wave) and looking at interesting animation applications of stored discrete function results.
Back in the day of "pocket calculators" this technique was essential. For more info and some formulas for generating trig tables see http://en.wikipedia.org/wiki/Generating_trigonometric_tables.
Note: There are other approximation techniques for trig functions that aren't discussed here because they use floating point numbers. They are interesting though, http://lab.polygonal.de/2007/07/18/fast-and-accurate-sinecosine-approximation/.
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 regular expression to reformat them into a C array (specifically - highlight the entire column and copy from Google, paste into Vim, highlight the lines pasted using visual mode, then run s/\n/, /g
.)
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 used both probes 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, // 0 - 42.1875 deg 11, 12, 12, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 16, 16, // 45 - 87.1875 deg 16, 16, 16, 16, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 12, 12, // 90 - 132.1875 deg 11, 11, 10, 10, 9, 8, 8, 7, 6, 5, 5, 4, 3, 2, 2, 1, // 135 - 177.1875 deg 0, -1, -2, -2, -3, -4, -5, -5, -6, -7, -8, -8, -9, -10, -10, -11, // 180 - 222.1875 deg -11, -12, -12, -13, -13, -14, -14, -14, -15, -15, -15, -16, -16, -16, -16, -16, // 225 - 267.1875 deg -16, -16, -16, -16, -16, -16, -15, -15, -15, -14, -14, -14, -13, -13, -12, -12, // 270 - 312.1875 deg -11, -11, -10, -10, -9, -8, -8, -7, -6, -5, -5, -4, -3, -2, -2, -1}; // 315 - 357.1875 deg
char sine32[128] = {0, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 16, 18, 19, 20, 21, // 0 - 42.1875 deg 23, 24, 25, 26, 27, 27, 28, 29, 30, 30, 31, 31, 31, 32, 32, 32, // 45 - 87.1875 deg 32, 32, 32, 32, 31, 31, 31, 30, 30, 29, 28, 27, 27, 26, 25, 24, // 90 - 132.1875 deg 23, 21, 20, 19, 18, 16, 15, 14, 12, 11, 9, 8, 6, 5, 3, 2, // 135 - 177.1875 deg 0, -2, -3, -5, -6, -8, -9, -11, -12, -14, -15, -16, -18, -19, -20, -21, // 180 - 222.1875 deg -23, -24, -25, -26, -27, -27, -28, -29, -30, -30, -31, -31, -31, -32, -32, -32, // 225 - 267.1875 deg -32, -32, -32, -32, -31, -31, -31, -30, -30, -29, -28, -27, -27, -26, -25, -24, // 270 - 312.1875 deg -23, -21, -20, -19, -18, -16, -15, -14, -12, -11, -9, -8, -6, -5, -3, -2}; // 315 - 357.1875 deg
char sine64[128] = {0, 3, 6, 9, 12, 16, 19, 22, 24, 27, 30, 33, 36, 38, 41, 43, // 0 - 42.1875 deg 45, 47, 49, 51, 53, 55, 56, 58, 59, 60, 61, 62, 63, 63, 64, 64, // 45 - 87.1875 deg 64, 64, 64, 63, 63, 62, 61, 60, 59, 58, 56, 55, 53, 51, 49, 47, // 90 - 132.1875 deg 45, 43, 41, 38, 36, 33, 30, 27, 24, 22, 19, 16, 12, 9, 6, 3, // 135 - 177.1875 deg 0, -3, -6, -9, -12, -16, -19, -22, -24, -27, -30, -33, -36, -38, -41, -43, // 180 - 222.1875 deg -45, -47, -49, -51, -53, -55, -56, -58, -59, -60, -61, -62, -63, -63, -64, -64, // 225 - 267.1875 deg -64, -64, -64, -63, -63, -62, -61, -60, -59, -58, -56, -55, -53, -51, -49, -47, // 270 - 312.1875 deg -45, -43, -41, -38, -36, -33, -30, -27, -24, -22, -19, -16, -12, -9, -6, -3}; // 315 - 357.1875 deg
Sine Tables
Note, this only goes to index 64, but the last 64 is just a negative mirror. Cut and paste is your friend. Also, you probably just want the C arrays above :P
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 |