Animating Oscillations

From Uzebox Wiki
Jump to navigation Jump to search

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 to sinePtr + 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