Feb 2, 2022

Multi-directional Medusa Head Movement (cooler than you might think)

If you're reading these blog posts at all, you're probably not a math whiz. It's okay if you're not -- I'm not either. I was decent at it throughout high school, but I'm amazed that I ever aced any of the tests in calculus! My point is, if you're a math whiz, you could probably figure out how to code multi-directional sinusoids on your own. This post is for all us non-whizzes.

Quick refresher, Konami's sinusoid algorithm is essentially

vspeed += (ystart - y) / 256 / amplitude;
y += vspeed;
x += hspeed;

It's found in games such as Castlevania, Contra, Gyruss, Life Force, Getsu Fuhma Den, and Wai Wai World in one form or another.  The code makes the object accelerate toward its starting coordinate every step, which results in the sinusoidal movement we've all grown to loathe. 

To rotate the results of any function, you can derive a parametric equation. That is, given y=f(x) and x=t (where t is the parametric variable), y is thus f(t). In simpler terms, instead of performing all the movement operations on x and y, you perform them on t and f(t). The derived parametric algorithm is

x = t * cos(direction) + f(t) * sin(direction)
y = f(t) * cos(direction) - t * sin(direction) 

The signs were reversed because sin() in many coding languages is 180° off from the rest of the environment. Anyway, you can then use some math magic to try to further derive a single algorithm.

This is a lot more straight-forward in the case of rotating the sinusoid algorithm, since we calculate x and y using separate algorithms, so we have no need of a single algorithm. So now let t be how far along the x-axis the instance has moved and let f(t) be how far along the y-axis the instance has moved. To keep track of these, we need to create two new variables for our sinusoid code and use them in place of x and y to calculate positional movement. We then pass those new variables through the two algorithms to find x and y.

xpos = 0
ypos = 0

////////////////////

xpos += hspeed
ypos += vspeed
vspeed += -ypos / 256
x = xstart + xpos * cos(direction) + ypos * sin(direction)
y = ystart + ypos * cos(direction) - xpos * sin(direction)

Notice xpos and ypos are initialized to 0, not to the current values of x and y. This is because we need to measure the distance traveled along each axis, which is defined as [x,y]-[xstart,ystart]. As a result, since both variables start at 0, we can remove ystart from the equation. However, since both variables start at 0, we have to factor xstart and ystart back into the equation when calculating the real coordinates.

You should save the cos() and sin() values to variables in order to speed up the code. If you were trying to implement this on actual NES hardware, you'd probably want to consider a massive LUT with pre-calculated values. That may seem like a lot of wasted memory, but you only need values for angles between 0 and 90 (exclusively), since every other angle is just a translation of those angles. For example, xpos*sin(45) is the same as -xpos*sin(315), so if the angle is 315, you'd just look up the value in the Sine45[ind] LUT for whichever value xpos currently is and then negate it.

With this algorithm, we can now create any object which moves in a sinusoidal pattern at any angle. You could have Medusa Heads attacking from 8 directions! If you want to mirror/flip the objects every step in order to halve the number of objects in the room like Konami did with Sypha's ice spell, you can negate xpos and ypos in the x and y equations every other step.

Diclaimer: There are still some facets of the algorithm I haven't come to grips with. For example, reversing the direction appears to require swapping xpos and ypos around. Nonetheless, I have encountered some neato movement patterns as a result of messing with utilizing a timer to manipulate the variables. Just play around with it and see if you come up with anything that suits your needs.

No comments:

Post a Comment

©TheouAegis Productions™. Powered by Blogger.