Mastered Shootorial #4 and have nothing to play with until next week? Try making your enemy ships smarter!
The Shootorial teaches you how to make enemy ships that fly across the screen at random speeds. If you downloaded the full game code, you probably also know how to make ships zig-zag across the screen. But what if you want a smoother, sinusoidal motion, or smart enemy ships that follow your ship up and down?
Below is some code for various alternative ship movement.
First, a few preliminaries.
All of this code goes in EnemyShip.as.
Since we're now dealing with movement in both the x and the y directions, we need a speed variable for both x and y. I've removed the
speed variable and replaced it with:
var v_x;
var v_y;
v_x represents velocity in the x axis.
v_y represents velocity in the y axis.
Now, let's experiment.
First, let's have a ship falling in an arc, as if shot down.
Update your
onEnterFrame() function as follows. First, find where you set
_x -= speed as in the Shootorial. Replace it with this:
_x -= v_x;
_y += v_y;
v_y += v_x/100;
This updates
_y every frame with the y velocity, and updates the y velocity by increasing it some fraction of the x velocity. I picked v_x/100 so that the ships would fly most of the way across the screen before falling off the bottom, but you can scale however you see fit. Note that positive y is down in flash, so you have to increase v_y and y in order to make a ship move toward the bottom of the screen.
To make it a bit more interesting, let's also have the falling ships slowly rotate toward the ground, as if they're crashing. After the last line of code above, add:
_rotation -= 0.4;
You can use different values if you'd like. This will make the enemy ship slowly rotate counterclockwise as it falls.
Nothing too complicated yet.
Next, let's make a ship travel in a sinusoid.
I'm going to explain how I derived the code here. If you don't care and just want the code, skip forward a bit. You probably need some basic knowledge of trigonometry and calculus to understand this.
A ship traveling in a sinusoid is like a zig-zagging ship, but the movement will be much smoother. The basic movement we want to get here is of the form y = a*sin(x/T) + C.
C is the center line; the ship will oscillate around that line.
a is the amplitude--the maximum distance the ship will go on either side of
C is
a pixels.
T is the period, and controls how many frequently the ship will move from maximum to minimum.
In order to create sinusoidal motion, we need to calculate the appropriate starting v_y, and the acceleration per frame. Here comes the math:
y = a*sin(x/T) + C
We take the derivative to find the equation for the speed, dy/dt:
dy/dt = (a/T)*cos(x/T)*dx/dt
Where
dy/dt = v_y and
dx/dt = v_x.
We could probably stop here and just use this equation, but calculate cos is a pretty slow operation, and if we do some more math we can come up with something faster. Let's find a formula for the acceleration instead; then we can set v_y += acceleration.
acceleration = d^2y/dt^2 = -1 * (a/T^2)*sin(x/T)*(dx/dt)^2 + (a/T)*cos(x/T)*d^2x/dt^2
Since our x speed is constant, the second term is zero. So we have:
acceleration = d^2y/dt^2 = -1 * (a/T^2)*sin(x/T)*v_x^2
Now let's solve for sin(x/T) in terms of y:
sin(x/T) = (y-C)/a
So
acceleration = -1 * (a/T^2)*((y-C)/a)*v_x^2
This isn't enough, though. We need to calculate an initial v_y based depending on what point in the curve we start at. Based on our equations above,
y_0 = a*sin(x_0/T)+C
dy_0/dt = (a/T)*cos(x_0/T)*(dx_0/dt)
If we solve for x_0, we get:
x_0 = T*arcsin((y_0-C)/a)
Since v_x is constant,
dx_0/dt = v_x.
So now we have:
dy_0/dt = v_y_0 = (a/T)*cos(asin((y_0-C)/a))*v_x
We have a cos and arcsin calculation here, but we only have to do it once per enemy ship instead of once per frame.
If we use these formulas in the enemyShip class, this is what we get:
class enemyShip extends MovieClip {
var v_x;
var v_y;
// variables for sinusoid math; a = amp, C = offset, T = period
var a;
var C;
var T;
function onLoad()
{
a=100;
C=150;
T=100;
_x=650;
v_x= Math.random()*5 + 5;
_y= 200*Math.random()+50;
v_y = (a/T)*Math.cos(Math.asin((_y-C)/a))*(v_x);
}
function onEnterFrame()
{
_x -= v_x;
_y += v_y;
v_y += -1*(a/(T*T))*((_y-C)/a) * (v_x) * (v_x);
if(_x < -50) { this.removeMovieClip(); }
}
}
Note that a sinusoid repeats every 2*PI, or approximately 6 units. In order to make the ship motion smooth, I set T = 100, which means the sinusoid will be about 600 pixels long. This gives us about one full oscillation over the screen.
Of course, this will be much more interesting if we can make our enemy ships center about random lines, oscillate at random amplitudes, and oscillate at random speeds (periods). Let's do just that!
Change the lines above for a=, C=, and T= to this:
a=Math.random()*50 + 50;
C=Math.random()*100 + 100;
T=Math.random()*50 + 75;
Your oscillation amplitude will now vary from 50-100; the center point from 100-200; and the period from 75-125. If you compile and run this code, you'll notice that every so often one ship will just go straight and not oscillate at all. This happens because the initial random _y value that you set now sometimes falls outside the valid range for the defined sinusoid, and one of the calculations returns NaN (Not A Number). To fix this, you have to change the
_y= line in the onLoad function:
_y=C-a+2*Math.random()*a;
This starts your ship at a y value that is within
a pixels of
C--that is, a valid point on the sinusoid.
Now, let's make ships with some AI. We want enemies to be able to track our ship, but if they do it too perfectly and too instantaneously, it becomes impossible to dodge them. So we'll have to handicap the enemy a bit.
define a
var max_speed at the top of the class. In your onLoad() function, insert the following line:
max_speed = _root.ship.velocity / 2;
This will make the enemy ship only capable of moving at half your ship's speed. Also, don't forget to reset your onLoad() calculation of _y to what it was before; remember, we changed it for sinusoidal movement.
Now for the AI. Replace the sinusoid calculations in onEnterFrame() with the following code:
_x -= v_x;
_y += v_y;
if( _x < _root.ship._x && v_y != 0)
{
if ( v_y < -0.5) { v_y += 0.5; }
else if ( v_y > 0.5 ) { v_y -= 0.5; }
else { v_y = 0; }
}
else if( _x >= _root.ship._x ) {
var delt_y = _root.ship._y - _y;
var target_v_y = delt_y / 2;
if (v_y < target_v_y && v_y < max_speed) { v_y += 0.5; }
else if (v_y > target_v_y && v_y > (-1*max_speed)) { v_y -= 0.5; }
}
if(_x < -50) { this.removeMovieClip();
}
This code does a few things. First, if the enemy ship has already passed your ship, it slowly decelerates until it's moving in a straight line again. Second, if the enemy ship has not passed your ship, it calculates the distance to your ship and sets a target velocity. I set the target velocity to distance/2; the higher the dividing factor, the more accurate the enemy ship will be (e.g. delt_y / 5). The lower the dividing factor, the less accurate the enemy ship will be; it will accidentally overshoot, go past past your ship vertically and have to adjust. Now, if the current v_y hasn't reached the maximum speed, and it's less than the target speed (or minimum speed and higher than the target speed, respectively), you accelerate by a fixed step. The result is an enemy ship that tries to follow your ship.
Now let's put these all together and have the game randomly send one of the above three enemy ships. The code below is my complete version:
class enemyShip extends MovieClip {
var v_x;
var v_y;
var move_type;
// variables for sinusoid math; a = amp, C = offset, T = period
var a;
var C;
var T;
// variables for AI movement
var max_speed;
function onLoad()
{
a=Math.random()*50 + 50;
C=Math.random()*100 + 100;
T=Math.random()*50 + 75;
max_speed = _root.ship.velocity / 2;
_x=650;
v_x= Math.random()*5 + 5;
move_type = Math.floor(Math.random()*3);
if(move_type!=1)
{
_y= 200*Math.random()+50;
v_y = 0;
}
else if(move_type==1)
{
// y_0 must be within the defined sinusoid
_y=C-a+2*Math.random()*a;
v_y = (a/T)*Math.cos(Math.asin((_y-C)/a))*(v_x);
}
}
function onEnterFrame()
{
_x -= v_x;
_y += v_y;
if(move_type==0) {
v_y += v_x/100;
_rotation -= 0.4;
}
else if(move_type==1) {
v_y += -1*(a/(T*T))*((_y-C)/a) * (v_x) * (v_x);
}
else if(move_type==2)
{
if( _x < _root.ship._x && v_y != 0)
{
if ( v_y < -0.5) { v_y += 0.5; }
else if ( v_y > 0.5 ) { v_y -= 0.5; }
else { v_y = 0; }
}
else if( _x >= _root.ship._x ) {
var delt_y = _root.ship._y - _y;
var target_v_y = delt_y / 2;
if (v_y < target_v_y && v_y < max_speed) { v_y += 0.5; }
else if (v_y > target_v_y && v_y > (-1*max_speed)) { v_y -= 0.5; }
}
}
if(_x < -50) { this.removeMovieClip(); }
}
}
I hope this is helpful to people! Feel free to reply with improvements on my code or innovative ship movement algorithms of your own.