Table of content

Previous: 5.a Defining the polar coordinate system

# b. Circles, orbits, planets and spaceships

To illustrate all this, let us look at the small level lesson5.wed. It contains a short demo with a spaceship orbiting a planet. Both the spaceship and the planet are also orbiting a death star which keeps shooting missiles in all directions (see Figure 5.5).

Figure 5.5: Diagram of what is going on in lesson5.wed. At the center, a star rotates on itself. It also shoots missiles along directions defined by angle_rocket. The blue planet moves around a circle centered around the star, with radius r_planet and angular velocity ang_planet. It also rotates on itself with the same angular velocity ang_planet so that its white spot always faces the star. Finally, a spaceship orbits around the blue planet with angular velocity ang_ship following a circle or radius r_ship.

Note that I created spaceships, rockets and planets as entities, not particles, since I needed to be able to set their pan and tilt angles. We now go through the code.

The action a_star for the death star is easy to understand. With the statement

    my.pan += 0.5*time	(5.7)

we get it to revolve around its center by increasing its pan angle by 0.5 degrees every tick (see Figure 5.5). 0.5 here is what is called an angular velocity. We all know ordinary velocity: it tells how fast we travel. Whether it is measured in kilometers per hour, meters per second, miles per hour or quants per tick, it basically tells us how much distance we cover during a given time interval. Angular velocity is the same thing except it specifies how much we have turned, so how many degrees have been added or subtracted to an angle, in a given time interval. In 3DGS, angular velocities are measured in degrees per tick. So it is accurate to say that the star is rotating with an angular velocity of 0.5 degrees per tick.

When we know the angular velocity of an object, we can very easily compute how much time it will take for that object to make a complete turn: just divide 360 degrees (= 1 complete turn) by the angular velocity. For instance, for the angular velocity 0.5 degrees/tick above, it will take 360/0.5 = 720 ticks = 45 seconds for the death star to rotate once on itself. These notions will be very important now that we use angles to specify the position of points in space (in polar, cylindrical and spherical coordinate systems).

Besides rotating on itself, the star is also shooting a missile every 16 ticks. The delay is programmed like in lesson 4. We will concentrate here on how the action picks the direction the missile is launched with. We want the missile to go from the death star to any place in the level. As seen earlier, this is the definition of a radial direction (see Figure 5.3(left)) so we can use equations (5.5) and (5.6) to define it. We first choose the vector's length, velocity, between 50 and 60 quants/tick. Then, we draw the angle angle_rocket defining the direction of the vector vel_rocket in the plane, between 0 and 360 degrees (see Figure 5.5). The polar coordinate system therefore allows us very simply to randomly generate velocity vectors vel_rocket which can point to any place in the level from the origin with equal probability. Note also that vel_rocket and angle_rocket are global variables since we need them to move and orient the missile in action a_rocket (see below).

    var vel_rocket;
    var angle_rocket;
    string missile = <missile.pcx>;

    action a_star
    {
    	camera.arc = 100;
    	my.oriented = on;
    	my.facing = off;
    	my.scale_x = 2;
    	my.scale_y = 2;
    	my.ambient = 100;
    	my.light = on;
    	my.red = 255;
    	my.green = 255;
    	my.blue = 255;

    	var velocity;
    	var delay;

    	while(1)
    	{
    		// death star rotates
    		my.pan += 0.5*time;

    		// shoots things
    		if (delay==16)
    		{
    			// launch missile
    			velocity = 50+random(10);
    			angle_rocket = random(360);
    			vel_rocket.x = velocity*cos(angle_rocket);
    			vel_rocket.y = velocity*sin(angle_rocket);
    			ent_create(missile,my.x,a_rocket);
    		}

    		// update counter
    		if (delay>0)
    		{
    			delay -= time;
    		}
    		else
    		{
    			delay = 16;
    		}
    		wait(1);
    	}
    }

We then create the missile entity using ent_create (see equation (1.2)), and assign it the following action.

    action a_rocket
    {
    	my.oriented = on;
    	my.facing = off;
    	my.tilt = 90;
    	my.pan = 180+angle_rocket;   	// turn the missile so it points
    					// where it is going.
    	my.ambient = 100;
    	my.light = on;
    	my.red = 255;
    	my.green = 255;
    	my.blue = 255;
    	my.scale_x = 1;
    	my.scale_y = 1;

    	my.skill1 = vel_rocket.x;
    	my.skill2 = vel_rocket.y;
    	my.skill3 = 16;

    	while(1)
    	{
    		if (my != null)
    		{
    			if (my.skill3>0)
    			{
    				my.x += my.skill1*time;
    				my.y += my.skill2*time;
    				my.skill3 -= time;
    			}
    			else
    			{
    				remove(me);
    			}
    		}
    		wait(1);
    	}
    }

We use my.skill3 as counter to delete the missile after 16 ticks. We store in my.skill1 and my.skill2 the x and y components of the initial velocity of the rocket, vel_rocket.x and vel_rocket.y, since the global variable vel_rocket is modified by the action of the death star each time another missile is created and fired. This way the missile will continue to move in the direction along which it has been launched (otherwise, we would get missiles which would change directions every 16 ticks). [We use a similar trick in lesson 7 to make particles move on the surface of spheres]. We also use angle angle_rocket to orient the sprite of the missile so that it points in the direction where it is going.

A planet is orbiting around the death star: it is just a sprite with the following action attached to it.

    var r_planet = 500;
    var ang_planet = 2;
    var pos_planet;

    action a_planet
    {
    	my.oriented = on;
    	my.facing = off;
    	my.scale_x = 1.5;
    	my.scale_y = 1.5;
    	my.ambient = 100;
    	my.light = on;
    	my.red = 255;
    	my.green = 255;
    	my.blue = 255;

    	while(1)
    	{
    		// orbits around the star
    		pos_planet.x = r_planet*cos(ang_planet*total_ticks);
    		pos_planet.y = r_planet*sin(ang_planet*total_ticks);
    		pos_panet.z = 70;
    		vec_set(my.x,pos_planet.x);
    		// rotates onto itself too
    		my.pan += ang_speed_planet*time;
    		wait(1);
    	}
    }

We want the blue planet to move on a circle at a constant speed around the death star. So, according to Figure 5.3 (right), what we have to do is use the formula of equations (5.5) and (5.6), fix the radius to some value and let the phi angle increase at some constant rate. This is exactly what

    pos_planet.x = r_planet*cos(ang_planet*total_ticks);
    pos_planet.y = r_planet*sin(ang_planet*total_ticks);

do. The position of the planet is given at each instant by the vector pos_planet. We choose r_planet as the radius of the circular orbit. But what is that factor ang_planet*total_ticks in the sine and cosine functions? What does it do?

The product ang_planet*total_ticks is what makes the planet move around the circle. Indeed, total_ticks is a system-defined timer which starts out as 0 (when we launch the level) and then increases by time ticks at each frame (see manual). Multiplying ang_planet by total_ticks therefore gives a quantity equal to an angle that increases by ang_planet*time at each frame. Finally, replacing angle phi by ang_planet*total_ticks in equations (5.5) and (5.6) makes the planet "move" along the circle with angular velocity ang_planet (see Figure 5.5).

Also, we want the white spot on the planet to always face the star. We therefore rotate the blue planet, using its pan angle, at the exact same angular velocity ang_plane using the following statement:

    my.pan += ang_planet*time.

You can check that this works correctly by running the level.

Alright, so far we have one star which rotates on itself, and a planet which circles around it. No big deal. Ok, so let's try something more radical. What about something which moves around a circle whose center also moves on a circle. Just take a look at the spaceship in Figure 5.5 to see what I mean. How can we pull this off?

For this, we only need to generalize equations (5.5) and (5.6) a bit. Thanks to vector algebra, we know that to move an object from one location to another, we just have to add some vector to the vector position of this object. The is true not only for vectors in Cartesian coordinates, but also in polar coordinates. So, if a point P has coordinates (Px,Py), then the following equations

    x = r*cos(phi) + Px		(5.8)
    y = r*sin(phi) + Py		(5.9)

give the position of the point (r,phi) after is has been moved by vector P. As a consequence, if we fix r to some value and let phi go from 0 to 360 degrees, we get a circle centered not on point (0,0) but instead on point P = (Px,Py). :-)

Further, if we fix instead phi to some value and let r run freely, we get radial lines which originate from point P.

Well, that was not hard at all, but it gets even better. Nothing keeps us from letting point P move about in space with time. This way, we can program entities so that they move in circles around moving objects! That makes certainly for interesting game ideas. Can anybody say '2-dimensional stealth space game'? ;-)

Now that we understand the theory behind it, here is the action for the spaceship:

    var ang_ship = 3;
    var r_ship = 150;

    action a_ship
    {
    	proc_late();
    	my.oriented = on;
    	my.facing = off;
    	my.scale_x = 0.75;
    	my.scale_y = 0.75;
    	my.pan = 180;
    	my.ambient = 100;
    	my.light = on;
    	my.red = 255;
    	my.green = 255;
    	my.blue = 255;

    	while(1)
    	{
    		// orbits around the planet
    		my.x = pos_planet.x + r_ship*cos(ang_ship*total_ticks);
    		my.y = pos_planet.y + r_ship*sin(ang_ship*total_ticks);
    		wait(1);
    	}
    }

We indeed find instructions

    my.x = pos_planet.x + r_ship*cos(ang_ship*total_ticks);
    my.y = pos_planet.y + r_ship*sin(ang_ship*total_ticks);

which are identical to equations (5.8) and (5.9) if we take the position of the planet, pos_planet, as the center of the circle, r_ship as the circle's radius, and if ang_ship is the angular velocity of the spaceship moving on it.

As a finishing comment, note the proc_late() instruction in action a_ship. Why did we put it there? As indicated in the manual, adding proc_late() to an action makes sure that the entity carrying this action is moved last. In other words, we want the blue planet to move first and then the spaceship to follow. This makes sure that the ship moves around a circle centered on the current position of the planet, not where it was at the previous frame (which would look weird).

I can almost hear some readers think "I hate trigonometry but I'm alright at manipulating equations. Isn't there a way of expressing a circle in Cartesian coordinates and using it to make a spaceship go around a planet?"

Well, first, using polar, cylindrical and spherical coordinates does not mean doing trigonometry. I only used trigonometry and right-angle triangles to show you where equations (5.5) and (5.6) came from, instead of just stating them arbitrarily (I'll do that with spherical coordinates). To use polar coordinates, all you need to understand geometrically is the notion of distance to the origin, and that of the angle of this distance with the x axis. That's it. Second, of course, there exist equations in Cartesian coordinates for a circle of radius r centered on an arbitrary point with coordinates x0 and x0. Here it is:

    (x-x0)2 + (y-y0)2 = r2.

However, note that it is in an awkward form (x and y are contained in a single equation). Compare that to the equations of the same circle in polar coordinates:

    x = r*cos(phi) + x0
    y = r*sin(phi) + y0

Here we readily have access to x and y, while in the former we have to use root-mean squares and work out the signs. Much more complicated and error-prone, but it is one's own choice. Just realize however that things will get much more complicated when shifting to 3 dimensions where we want to represent spheres and cylinders.

Next: Lesson 6. Cylindrical coordinates: the "Soul Reaver portal" effect