Game Flash Final Exam
Car Time Trial Race
1. Car
movement is not the most difficult part of a racing game, but if you want to simulate
realistic (almost realistic) movement you have to take in consideration some of
the aspects described below.
2.
Download and open car_race_start.fla
3. Click
on the first frame (the only frame) of the "defs" layer and press F9
to display the Actions Window for this frame. Enter the following code on the
defs layer:
ACTIONSCRIPT:
1.
car1.code = "player";
2. //this variable will decide if the specified car is controlled by a human player or by the computer
3.
acceleration = 0.4;
4.
//the
acceleration variable will add to the speed variable on every enterFrame event
(in this case 24 times per second); a higher value translates in a faster
acceleration of the car
5.
speedDecay = 0.96;
6.
//when the
car is not accelerated (the UP Key is released), the car will have to slow down
smoothly; the speed will multiply with this value (less than 1); the lower this
value is, the faster the car will slow down
7.
rotationStep = 10;
8.
//this is
the number of degrees that will increase or decrease the car's rotation (angle)
when the left or right keys are pressed
9.
maxSpeed = 10;
10.//this is the speed limit on our track; increase it if you want the
car to go faster
11.backSpeed = 1;
12.//this is the speed limit when going backwards
4. OK!
Now let's see what we can do with these
variables.
Click on the first frame
of the second layer ("actions") and if the Actions windows is not
open, press F9 to display it.
We will discuss this
script in a few moments, but first let's see how Flash "understands"
movement and coordinates.
Just a
bit of trigonometry and Flash
Flash is using the
classic Cartesian Coordinate System
(a grid based system with a horizontal axis OX and a vertical axis OY).
You notice in the
attached picture that in Flash the Y axis is inverted meaning that the negative
side of the Y axis is positioned higher than the positive side. So the lower a
coordinate is, the higher it's value will be.
Because Flash understand
only horizontal and vertical vectors we will have to calculate the horizontal
and the vertical components of the actual "speed".
So, from trigonometry we
know (in this case) that:
sin(angle) = speedx/speed and
cos(angle) = speedy/speed
so... we know the angle
(angle=car._rotation) and we know the speed. That's all we need know. Is it?
No. You need to know one more thing:
The Math class
implemented in Macromedia
Flash does not work with angles measured in degrees. Instead we will
have to provide angles measured in radians (an alternative unit measurement for
angles).
The only case in which
you will use degrees is when actually rotating the movieclips.
Using the simple equation
below you will be able to transform degrees into radians:
angle_radians = angle_degrees * (PI/180)
Now we can easily
calculate the X and Y components of the car's speed:
speedx = Math.sin(_rotation*(Math.PI/180))*speed;
speedy =
Math.cos(_rotation*(Math.PI/180))*speed*-1;
Well, you already figured
out why the sign of the Y component has to be inverted ;)
And now let's get back to
Flash and our Actions Window. Next I will explain what the "step"
function is all about. The "step" function will be executed on every
enterFrame event (on the "stepper" layer you will find an empty
movieclip the executes the onClipEvent (enterFrame) routine). Enter the
following code on the actions layer:
ACTIONSCRIPT:
1.
function step(who) {
2.
//check to see if the car in question is controlled by the player
or by the computer
3.
if (_root["car"+who].code == "player") {
4.
//we will constantly
decrease speed by multiplying it with a number below 1, but only if speed if
higher than 0.3; a lower value will only consume resources and movement will
not even be noticed so we will set the speed variable to 0
5.
if (this["speed"+who]>0.3) {
6.
this["speed"+who] *= _root.speedDecay;
7.
} else {
8.
this["speed"+who] = 0;
9.
}
10. //the car will react to certain key presses that we will capture
using the Key.isDown method as follows
11. //accelerate - we add a certain value to the speed variable if the
UP key is pressed and the speed is lower than it's maximum alowed value
12. if (Key.isDown(Key.UP) && this["speed"+who]<_root.maxSpeed) {
13. this["speed"+who] += _root.acceleration;
14. }
15. //brake (reverse) - same thing, but here we subtract
16. if (Key.isDown(Key.DOWN)) {
17. this["speed"+who] -= _root.backSpeed;
18. }
19. //steer left - well, we could simply add or subtract a fixed angle
(in degrees) to/from the car's rotation, but that's not good enough. In order
to simulate a natural movement, steering must depend on speed, otherwise you
will be able to rotate your car even if it's almost stopped and it will look
like a propeller :)
20. if (Key.isDown(Key.LEFT) && this["speed"+who]>0.3) {
21. _root["car"+who]._rotation -= _root.rotationStep*(this["speed"+who]/_root.maxSpeed);
22. }
23. //steer right - you already know what happens here
24. if (Key.isDown(Key.RIGHT) && this["speed"+who]>0.3) {
25. _root["car"+who]._rotation += _root.rotationStep*(this["speed"+who]/_root.maxSpeed);
26. }
27. this["rotation"+who] = _root["car"+who]._rotation;
28. //we calculate the two components of speed (X axis and Y axis) - we
have already discussed this part of the function above
29. this["speedx"+who] = Math.sin(this["rotation"+who]*(Math.PI/180))*this["speed"+who];
30. this["speedy"+who] = Math.cos(this["rotation"+who]*(Math.PI/180))*this["speed"+who]*-1;
31. //apply the components on the actual position of the car - we add
the X component of the speed to the X coordinate of the car and the Y component
of the speed to the Y coordinate
32. _root["car"+who]._x += this["speedx"+who];
33. _root["car"+who]._y += this["speedy"+who];
34. //position the shadow of the car - when the car steers, we want the
shadow to keep it X and Y coordinates and always stay on one side of the car (whatever
side that would be)
35. _root["shadow"+who]._x = _root["car"+who]._x-4;
36. _root["shadow"+who]._y = _root["car"+who]._y+2;
37. _root["shadow"+who]._rotation = _root["car"+who]._rotation;
38. }
39. if (_root["car"+who].code == "computer") {
40. }
41.}
That's it! We already
have a moving car. Now we can move on to collisions.
Collisions
1. We all
know why collisions are important in a Racing Game... Because we don't want the
car to leave the track, because we want to force the player to use a specific
way, because we want him/her to avoid collisions in order to get the best time
(or win the race).
Collisions are a very
important part of a racing game and 70% of the game feeling and success depends
on good collisions.
We don't want the car to
get stuck in the non accessible areas (NAA), we don't want it to lose all speed
although it hardly touches those areas and we definitely don't want it to
bounce back (by reversing the speed).
In other words we don't
want to give the player a hard time, but on the contrary, an enjoyable game. So
when the car touches the NAA we must try to correct it's trajectory and of
course apply a speed penalty depending on the angle of collision and collision
duration.
Using
four points to detect collisions
As you can see in the
attached picture, we will pick four points, one on every side of the car and
check to see if any of them "touches" the NAA.
For example if the Left
Side Point is inside the NAA (hits the NAA) then we will have to apply a speed
penalty and increase the angle (_rotation) of the car. Why do we do that?
Because of what we discussed earlier: we must try to correct the car's
trajectory. So what we do here is force
the car to steer right.
OK, I hope everything is
clear up to this point. And since we are speaking of points, let's see how we
calculate their coordinates. To simplify things we will take the Left Side
Point as an example.
When the car's rotation
is 0 our job is very simple: the LSP coordinates are x=car._x-20 (20 pixels to
the left of the car's center point) and y=car._y
But the car will not
always have an angle of 0. Well, here comes the tricky part. There are a few
ways to calculate the four points even if the angle is not 0 (for example you
can use the sine and the cosine functions) and for this tutorial I chose the
simple way (I don't know if it's the optimum way, but it's very simple):
We define the Left Side
Point as if the car's rotation was 0:
car.pointLeft = {x:-20, y:0}; //this is an
Object
and then we transform the
point's coordinated from local (related to the car's clip) to global (related
to the _root clip where we will test the collisions):
car.localToGlobal(car.pointLeft);
Now we have our Left Side
Point coordinates that we can use to check the collision:
car.pointLeft.x and car.pointLeft.y
Can it get any simpler?
:)
2. And
again back to our Actions Window. Click on the first frame of the
"actions" layer and if the Actions Window is not open press F9 to
display it.
Because we must modify
the way the car moves remove the code you entered before and enter the code
listed below:
ACTIONSCRIPT:
function step(who) {
//check to see if the car in question is controlled by the player or by the computer
if (_root["car"+who].code == "player") {
//we will constantly decrease speed by multiplying it with a number below 1
if (this["speed"+who]>0.3) {
this["speed"+who] *= _root.speedDecay;
} else {
this["speed"+who] = 0;
}
//the car will react to certain keys
//accelerate
if (Key.isDown(Key.UP) && this["speed"+who]<_root.maxSpeed) {
this["speed"+who] += _root.acceleration;
}
//brake (reverse)
if (Key.isDown(Key.DOWN)) {
this["speed"+who] -= _root.backSpeed;
}
//steer left
if (Key.isDown(Key.LEFT) && Math.abs(this["speed"+who])>0.3) {
_root["car"+who]._rotation -= _root.rotationStep*(this["speed"+who]/_root.maxSpeed);
}
//steer right
if (Key.isDown(Key.RIGHT) && Math.abs(this["speed"+who])>0.3) {
_root["car"+who]._rotation += _root.rotationStep*(this["speed"+who]/_root.maxSpeed);
}
this["rotation"+who] = _root["car"+who]._rotation;
//we calculate the two components of speed (X axis and Y axis)
this["speedx"+who] = Math.sin(this["rotation"+who]*(Math.PI/180))*this["speed"+who];
this["speedy"+who] = Math.cos(this["rotation"+who]*(Math.PI/180))*this["speed"+who]*-1;
//apply the components on the actual position of the car
_root["car"+who]._x += this["speedx"+who];
_root["car"+who]._y += this["speedy"+who];
//the collisions
//define the four collision points
_root["car"+who].pointLeft = {x:-20, y:0};
_root["car"+who].localToGlobal(_root["car"+who].pointLeft);
_root["car"+who].pointRight = {x:20, y:0};
_root["car"+who].localToGlobal(_root["car"+who].pointRight);
_root["car"+who].pointFront = {x:0, y:-25};
_root["car"+who].localToGlobal(_root["car"+who].pointFront);
_root["car"+who].pointBack = {x:0, y:25};
_root["car"+who].localToGlobal(_root["car"+who].pointBack);
//let's use some shorter variable names :)
this["lpx"+who] = _root["car"+who].pointLeft.x;
this["lpy"+who] = _root["car"+who].pointLeft.y;
this["rpx"+who] = _root["car"+who].pointRight.x;
this["rpy"+who] = _root["car"+who].pointRight.y;
this["fpx"+who] = _root["car"+who].pointFront.x;
this["fpy"+who] = _root["car"+who].pointFront.y;
this["bpx"+who] = _root["car"+who].pointBack.x;
this["bpy"+who] = _root["car"+who].pointBack.y;
//check for collisions
if (_root.terrain.hitTest(this["lpx"+who], this["lpy"+who], true)) {
_root["car"+who]._rotation += 5;
this["speed"+who] *= 0.85;
}
if (_root.terrain.hitTest(this["rpx"+who], this["rpy"+who], true)) {
_root["car"+who]._rotation -= 5;
this["speed"+who] *= 0.85;
}
if (_root.terrain.hitTest(this["fpx"+who], this["fpy"+who], true)) {
this["speed"+who] = -1;
}
if (_root.terrain.hitTest(this["bpx"+who], this["bpy"+who], true)) {
this["speed"+who] = 1;
}
//position the shadow of the car
_root["shadow"+who]._x = _root["car"+who]._x-4;
_root["shadow"+who]._y = _root["car"+who]._y+2;
_root["shadow"+who]._rotation = _root["car"+who]._rotation;
//checkpoints
if (_root["car"+who].hitTest(_root["checkpoint"+_root["currentCheckpoint"+who]])) {
//if the current checkpoint is the start line - increase the lap number
if (_root["currentCheckpoint"+who] == 1) {
if (_root["currentLap"+who] != 0) {
_root.setBestLap();
}
if (_root["currentLap"+who] == _root.totalLaps){
_root.gotoAndStop("finish");
}else{
_root["currentLap"+who]++;
}
_root.currentLapTXT = _root["currentLap"+who]+"/10";
}
_root["currentCheckpoint"+who]++;
//if the current checkpoint is the last checkpoint - set the next checkpoint to the start line
if (_root["currentCheckpoint"+who]>_root.checkpoints) {
_root["currentCheckpoint"+who] = 1;
}
}
}
if (_root["car"+who].code == "computer") {
}
}
3. Add a layer for the ground and give it an instance name of terrain.
Add a layer for the stepper and place the stepper from the library to the stage on the layer
onClipEvent(load){
speed1 = 0;
}
onClipEvent(enterFrame){
_root.step(1);
}
Hard? Not so hard :)
Where
are we running?
1. Up to
now we have a 100% functional game engine but no game. We don't have a goal. So
we'll have to add one and because this tutorial is about a time trial racing
game we will add laps and timers.
2. Add
two lkeyframes on every layer and create a layer for labels with the labels
"readyset", "go" and "finish". In the first
frame a movie clip will
play saying "ready, set, go". When "go" is displayed _root
will move to the frame labeled "go" where the car can move.
Create a new layer for
the messages. In frames 1 and 2 the Ready , Set, go message will display. Place
the movie clip readySet on the stage.
Why the car will not move
in the first frame? Well, that's because the "stepper" movieClip only
exists in the second frame, so that's where the "step" function will
be executed.
On the second frame of
the "actions" layer you will also find two new variables. Those
variables will be used to store the exact time when the race started
(initialTime|) and the exact time when the current lap started (lapTime).
On the
stepper layer clear frames 1 and 3. The code below should be on
1.
onClipEvent(load){
2.
speed1 = 0;
3.
}
4.
onClipEvent(enterFrame){
5.
_root.setTimes();
6.
_root.step(1);
7.
}
Add the following to the defs layer frame 2:
stop();
initialTime = getTimer();
lapTime = initialTime;
Add a stop to frame 3 of the defs layer.
****CHANGE DEFS LAYER FRAME 1:
delete the existing code in layer 1 and replace with the following:
stop();
car1.code = "player";totalLaps = 10;
acceleration = 0.4;
speedDecay = 0.96;
rotationStep = 10;
maxSpeed = 10;
backSpeed = 1;
currentCheckpoint1 = 1;
currentLap1 = 0;
checkpoints = 2;currentLapTXT = "1/10";
When the game is over,
after the player finishes ten laps, _root will move to the "finish"
frame where an information movieClip will be displayed. Place the finishAnimation
on the stage in Frame 3 of the messages layer so it will move from the right
off the stage to the center of the stage. A hint: make sure the clip’s
registration point is in the center of the stage.
3. OK!
What we need to do next is determine whether the player has finished a lap or
not, so we will add two movieClip (a red line on the right side: instance name:
checkpoint1) and check if the car "touched" this movieClip, and if it
did, than you know that a lap is finished... hmm... not really :)
First of all the car will
"touch" this movieClip for more than one frame. Maybe for two, maybe
for ten, maybe for one hundred frames, you cannot determine this number because
it depends on the car's speed. And instead of increasing the number of laps
with one, you will increase it with two, ten or one hundred laps, so the race
will be ready quite fast.
The second problem,
assuming that you solved the first one is that one player will go past the
finish line (the red line on the right instance
name: checkpoint1) and then return immediately to the same line and
"touch" it again, increasing the number of laps even though the lap
is not completed. This problem can be solved in a few ways but we will choose
the solution that fixes both our problems: we will add a checkpoint (a red line
to the left: instance name: checkpoint2).
This checkpoint will be placed somewhere around the middle of the race so that
the player will lose more time returning to the finish line than he will lose
by completing the lap. Of course if you want a more secured race you will add
more than one checkpoint.
4. Open
the actions window for the first frame of the "actions" layer. We
have to new functions both related to timing the race - setTimes (calculates
and sets the total race time) and setBestLap (calculates and sets the best lap
time). We'll take them one at a time and see what they do.
First delete the
following from the end of your code:
1.
}
2.
if
(_root["car"+who].code == "computer") {
3.
}
4.
}
Then
add the following
ACTIONSCRIPT:
1. //checkpoints
2. if
(_root["car"+who].hitTest(_root["checkpoint"+_root["currentCheckpoint"+who]]))
{
3. //if the current checkpoint is the start
line - increase the lap number
4. if
(_root["currentCheckpoint"+who] == 1) {
5. if (_root["currentLap"+who] != 0)
{
6. _root.setBestLap();
7. }
8. if (_root["currentLap"+who] ==
_root.totalLaps){
9. _root.gotoAndStop("finish");
10.}else{
11._root["currentLap"+who]++;
12.}
13._root.currentLapTXT =
_root["currentLap"+who]+"/10";
14.}
15._root["currentCheckpoint"+who]++;
16.//if the current checkpoint is the last
checkpoint - set the next checkpoint to the start line
17.if (_root["currentCheckpoint"+who]>_root.checkpoints)
{
18._root["currentCheckpoint"+who] =
1;
19.}
20.}
21.}
22.if (_root["car"+who].code ==
"computer") {
23.}
24.}
Now
add the following
ACTIONSCRIPT:
1.
function setTimes() {
2.
//we calculate the time elapsed from the moment the race started in
millisecond
3.
timeElapsed = getTimer()-_root.initialTime;
4.
//we calculate the minutes, seconds and tens of seconds and set
them to their respective variables
5.
milliseconds = timeElapsed;
6.
seconds
= Math.floor(milliseconds/1000);
7.
minutes
= Math.floor(seconds/60);
8.
minutesTXT = minutes;
9.
secondsTXT = seconds-minutes*60;
10. tensTXT = Math.round((milliseconds-seconds*1000)/10);
11. //if
the minutes, seconds or the tens of seconds number has only one character we
add a "0" before it - that's just because we want the time to look
good ;)
12. if (minutesTXT<10) {
13. minutesTXT = "0"+minutesTXT;
14. }
15. if (secondsTXT<10) {
16. secondsTXT = "0"+secondsTXT;
17. }
18. if (tensTXT<10) {
19. tensTXT = "0"+tensTXT;
20. }
21. //we
put all three variables in one that will be used in the timers tables
22. _root.totalTimeTXT = minutesTXT+"."+secondsTXT+"."+tensTXT;
23.}
24.//and the second function
25.function setBestLap() {
26. //this
function does the exact same thing as the first one, only here we will use the
time elapsed from the last time the car has passed the finish line
27. bestTime = getTimer()-_root.lapTime;
28. milliseconds = bestTime;
29. //we
don't calculate the lap time if the car passes the finish/start line for the
first time
30. if (oldMilliseconds>milliseconds || oldMilliseconds == null) {
31. oldMilliseconds = milliseconds;
32. seconds = Math.floor(milliseconds/1000);
33. minutes = Math.floor(seconds/60);
34. minutesTXT = minutes;
35. secondsTXT = seconds-minutes*60;
36. tensTXT = Math.round((milliseconds-seconds*1000)/10);
37. if (minutesTXT<10) {
38. minutesTXT = "0"+minutesTXT;
39. }
40. if (secondsTXT<10) {
41. secondsTXT = "0"+secondsTXT;
42. }
43. if (tensTXT<10) {
44. tensTXT = "0"+tensTXT;
45. }
46. _root.bestLapTXT = minutesTXT+"."+secondsTXT+"."+tensTXT;
47. }
48. //we
set the initial time to the moment the car passed the finish line
49. _root.lapTime = getTimer();
50.}
To Play again, add code to the Play again button. First open the finish movie clip, then click on the Play again button to select it. Add the code:
on (release){
_root.gotoAndPlay("readySet");
}
5. That's it :) Now let's
move on to the final graphic touches and see our completed game.
Finishing
the game
1. The
final graphic touches... Well, there's nothing to explain here. You can express
yourself in any way and create graphics after your own taste.
Just remember to set the
alpha of the green ground movie clip and the red checkpoint movie clips to 0.
For Example:
A Student example: