Nov 26, 2020

Code Concepts: "Gyrusscopic Motion" pt. 2

Following up on the previous post about transforming horizontal movement into circular motion, the next step is traveling across multiple axes, thus turning our circular motion into spherical motion. This is actually a bit more complex than what I go into here, but this post is just about the basics.

In the last post, this was basically your normal 2D platformer movement translated into a circle. To add depth to the movement, we need one more axis of movement, which in keeping with convention will be dubbed zPlanet. If you wanted to do 3D graphics, zPlanet could of course undergo a translation to zScreen (or depth), but this post is just about the basics.

First we need to set up our z-movement, typically the correlation between up and down inputs using a zSpeed variable. Maintain the standard relationship of -- for up and ++ for down. If you want things to be rendered "over the horizon", you will want to make sure up is "up" on the visual side, but "down" on the invisible side. You can't simply toggle visibility of things either, you need to change the draw order. You will want a variable zFacing which you can toggle between -1 and +1. By default, set it to +1. Multiply your zSpeed by zFacing, which will cause the movement to ping-pong. You could use sine wave patterns for your movement, but again, this is about the basics and keeping trig to a minimum for beginners. Toggle zFacing whenever zPlanet crosses the thresholds, which in this case are 0 and radius*2. If you know your basic circle geometry, that's the diameter. Now you can either set the depths of things by the depth of your planet + zPlanet, or you can draw things before or after the planet depending on zPlanet. If you're using Game Maker, setting the depth would be best. Don't forget to flip the sprite when zPlanet is greater than radius.

var zSpeed = (keyboard_check(vk_down) - keyboard_check(vk_up)) * zFacing
zPlanet = zPlanet + zSpeed
if median(zPlanet, 0, radius*2) != zPlanet { 
zFacing *= -1
zPlanet = zPlanet - zSpeed
}
image_yscale = sign(radius - zPlanet | 1)

To find xScreen and yScreen, we just need to subtract zPlanet from yAngled.

 

The more astute will notice zPlanet doesn't follow the contours of the planet as closely as xPlanet due to the lack of trig here. Of course we can alleviate that quite simply. 

Just as we set x and y using some basic trig, we will need to do something similar with zPlanet in order to give it a truly spherical look. Since we will be using trig to manipulate zPlanet, we no longer need to multiply zSpeed and the requirement that zFacing be -1 or +1 can be discarded in favor of a simple binary mechanic (0 or 1). One other big change is we need a new permanent variable zAngled to keep track of the position following the contours of the planet. Consequently, instead of subtracting zPlanet from yAngled, now you will need to subtract zAngled.

The first two lines of code won't vary much.

var zSpeed = keyboard_check(vk_down) - keyboard_check(vk_up);
zPlanet = zPlanet + zSpeed;

Now we need to apply some trig to transform zPlanet to our conformist zAngled. Since zPlanet is a position along the surface of the planet, we need to normalize it to an angle relative to the circumference, just like we did to find xAngled before. We can then apply a little trig relative to the radius of the planetoid to find the proper zAngled. We need to add radius to the formula in order to properly position the instance within the planetoid. Sprite flipping will now also be based on zAngled.

zAngled = radius + dsin((zPlanet + circumference*3/4) / circumference * 360) * radius;
image_yscale = sign(radius - zAngled | 1)

Adjusting the depth can be handled by comparing zPlanet to the horizon while moving. Each hemisphere is defined by circumference/2, thus the horizons are defined by circumference/4. Note you need to further step zPlanet, otherwise the depth can fail to toggle when zig-zagging across horizons.

if zSpeed != 0 && zPlanet mod circumference/2 == 0 {
zFacing ^= 1; 
zPlanet += zSpeed;
}

Finally, all you need to do is base drawing depth on zFacing. With this code, a zFacing less than or equal to 0 would mean the sprite should be drawn before the planet, while a value greater than 0 would mean the sprite should be drawn after the planet. Tweak this mechanic to your liking.


In retrospect, it would probably be more logical to swap y* and z* variables. In the previous post, the planetoid was treated as a curved platformer environment, thus x was "along the surface" and y was "above the surface". However in this post, movement around the planetoid is more akin to a top-down environment, so both x and y should be "along the surface" with z "above the surface". This should become especially prudent when factoring planetary rotation into the equation. I didn't want to rename everything in this post out of concern it might make things even more confusing. 


Revision Update

Here is an updated code with y* and z* swapped.

var xSpeed = -keyboard_check(vk_left) + keyboard_check(vk_right);
var ySpeed = keyboard_check(vk_down) - keyboard_check(vk_up);

if (ySpeed != 0) {
    yPlanet = (yPlanet + ySpeed) mod circumference;
    if abs(yPlanet mod (circumference/2)) == 0 {
        yFacing ^= 1;
        yPlanet += ySpeed;
    }
}

var yAngled = radius + dsin((yPlanet + circumference*3/4) / circumference * 360) * radius;
image_yscale = sign(radius - yAngled | 1);

xPlanet = (xPlanet + xSpeed) mod circumference;

var jump = keyboard_check(vk_space);
if jump && zPlanet == 0
    zSpeed = -6;
    
if zPlanet != 0
    zSpeed += zGravity;
    
zPlanet -= zSpeed;
if zPlanet <= 0 {
    zPlanet = 0;
    zSpeed = 0;
}

var xAngled = 180 - xPlanet / circumference * 360;
image_angle = xAngled - 90;   //Subtract 90 so the top of the planet is "0"
x = room_width/2 + lengthdir_x(radius + zPlanet - yAngled , xAngled);
y = room_height/2 + lengthdir_y(radius + zPlanet - yAngled , xAngled);

No comments:

Post a Comment

©TheouAegis Productions™. Powered by Blogger.