|This article is the second of a series in which we are recreating classic video games in KPL code—in this article, Asteroids!|
Time Required: Less than 1 hour
Software: Kid's Programming Language
Have you seen the previous articles here at Coding4Fun about KPL, the Kid's Programming Language? The overview is here, and the article on KPL Pong is here. KPL is not just for kids; it's for anyone who wants to code fun stuff fast. Proving the point, this is the second of a series in which we are recreating classic video games in KPL code—in this article, Asteroids! Asteroids is a big step up from our Pong example—a lot more code here. Next in the series, by the way? Missile Command—just like the old days!
(Click image to zoom)
Here are some fun historical links about Asteroids, which Atari released in 1979:
KLOV.com is, of course, the Killer List of Videogames. Check out this historical highlight from the Wiki:
"In March 2004, Portland, Oregon resident Bill Carlton attempted to break the world record for playing an arcade version of Asteroids, playing over 27 hours before his machine malfunctioned, ending his record run. He scored 12.7 million points, putting him in 5th place in the all-time Asteroids rankings. In November 1982 Scott Safran set the still unbroken record of 41 million points."
Oilzine's article entertainingly describes how, in 1961, MIT's Tech Model Railroad Club got their hands on a DEC PDP-1 "minicomputer"—famously small because it was only the size of a large car! —and used it to invent Space War, the first precursor to Asteroids. One of them also invented the first joystick just so they could play this game! Talk about Coding4Fun!
Besides Asteroids, Atari also released Basic Programming in 1979—a fun predecessor to KPL. On the one hand, this description of graphical programming support back then makes KPL look pretty good:
"Graphics" - contains two colored squares that can be manipulated by your program.
On the other hand, this makes one think: we could program Atari consoles in 1979?! 27 years later, and now no one can program their consoles? Run my own KPL games on my Xbox or Xbox 360 or PS2 or PSP or Nintendo? And in February, when KPL v 2 lets me do easy 3D game programming, and use game controllers? I guess if we can't program our consoles now, it's because no one really wanted to do that, right?
Sorry, back to Asteroids now. Asteroids.kpl is a lot larger than Pong was, with over 1000 lines in the KPL file, but many of those are white space or comments. As with all KPL games and programs, it's open source, and available for download
Just the interesting stuff, please
This article will assume some basic programming skill, and rather than explaining every line of code in the KPL program, will focus on key techniques or algorithms which will be useful as you develop your own KPL games:
- Creating new animated sprites
- "Stamping" a starfield as a background
- Calculating directional vectors
- Adjusting game speed based on frame rate
- Finding a safe hyperjump spot
- Controlling game playability
I will also not repeat topics covered in previous articles, so if this is the first you have read about KPL and would like to catch up on them, they were published in this order:
This Asteroids article will make the most sense if you have installed KPL, and downloaded and unzipped the Asteroids program underneath your KPL install folder. The ideal way to start, of course, is to load Asteroids.kpl in the KPL IDE, click "Run the program," and play it a few times before examining the code.
KPL itself and Asteroids.kpl are available as freeware downloads from: http://www.kidsprogramminglanguage.com/download.htm. KPL does have a dependency on the .NET Framework 1.1, and the KPL install will "automagically" download and install the Framework if it is not already installed on your machine.
Creating New Animated Sprites
Graphical sprites are easily animated in KPL by using animated GIF files. An animated GIF is a GIF file with multiple "frames" in it, plus supporting data about whether to loop the animation, and how long to display each frame before switching to the next. KPL allows programmatic control of those behaviors, giving you a lot more flexibility than "hard-coding" this in the image file, but for a GIF which you want to animate for use on a web page, for instance, you need that control to be explicitly included in the GIF itself.
There are various shareware and freeware animated GIF editors available, so I won't recommend just one. The basic process is the same for each: when you create a new animated GIF, you add "frames" to it, and you load in the image file to use for each frame. You also tell it how long to display each frame. The animated image we used for Asteroids was of the ship itself—we wanted to give it a minor "pulsing" effect to help it stand out better against the classic Asteroids black-and-white screen. Here are each of the frames used, as well as the final animated GIF. We started with the image used for Frame 1 as the base image, and used MSPaint.EXE to modify it slightly to create the images for Frames 2 and 4. Frame 3 is the same image as Frame 1.
|Frame 1||Frame 2||Frame 3||Frame 4||Animated GIF|
Here's the KPL code which loads and controls the timing of the pulsing ship image:
LoadSprite( "Ship", "AnimatedShip.gif" )
Define Timeline As Int
Timeline = 100 // 100 milliseconds is 1/10 of a second
Timeline = 100
Timeline = 100
Timeline = 100
// Tell KPL to automate it for us, using the timeline
// we created above. True tells KPL to loop the animation.
SetSpriteAnimationTimeline( "Ship", True, Timeline )
"Stamping" a Starfield as a Game Background
Lots of games are set against a starfield, so here's the KPL code from Method DrawStars() that randomly creates and "stamps" a starfield as the background for Asteroids:
Define I As Int
Define Scale As Decimal
Clear( Black )
LoadSprite( "Star", "Star.gif" )
ShowSprite( "Star" )
For I = 1 To (ScreenWidth()/4)
MoveSpriteToPoint( "Star", Random( 1, ScreenWidth()), Random( 1, ScreenHeight()) )
Scale = Random( 1, 100 )
Scale = Scale / 100.0 - 0.1
ScaleSprite( "Star", Scale )
StampSprite( "Star" )
UnloadSprite( "Star" )
The code randomly moves the sprite "Star" around the screen, randomly resizing it each time using ScaleSprite(). At each location, it uses StampSprite() to make it a permanent part of the screen background. This is much more efficient than actually creating and maintaining 500 separate Star sprites, and enables KPL to redraw the background automatically as well.
Note the use of ScreenHeight() and ScreenWidth(). Since this game is best played "Maximized," ScreenHeight() and ScreenWidth() are system functions used throughout the program to make the display match the user's screen resolution. In this example, we even adjust the number of stars displayed based on the user's ScreenWidth(), so the starfield density is similar across all screen resolutions.
Calculating Directional Vectors
Directional vectors are the basis for nearly all aspects of Asteroids gameplay, including the movement of the asteroids, the movement of the ship, and the path of fired missiles. A directional vector is easily represented as the combination of some amount of movement along the x axis and some amount of movement along the y axis—and so the Point structure is a good way to record and work with Vector data:
X As Decimal
Y As Decimal
Define piOver180 As Decimal = (3.14159 / 180.0)
Function CalculateVector( Heading As Decimal ) As Point
Define Result As Point
Heading = (Heading - 90)
Define theta As Decimal = (Heading * piOver180) * -1
Result.X = Cos( theta )
Result.Y = Sin( theta ) * -1
This is getting into interesting trigonometry, because we must in order to handle directional vectors.
piOver180 is a pre-calculated constant that defines "radians per degree." The
Heading is in degrees, but the Cos() and Sin() functions take radians as a parameter, so
(Heading * piOver180) converts the Heading from degrees to radians for those calls.
Since the "forward" direction of our ship and missile sprites is actually straight up, we subtract 90 from our heading before we perform the vector calculation. And since computer coordinates' y axis increases downward instead of upward, we also multiply by -1.
Adjusting Game Speed Based on Frame Rate
The range in the performance characteristics of computers capable of running KPL is pretty amazing: from eight-year-old machines with no graphics accelerator running Windows 98 to brand-new gaming-optimized 64-bit machines. Moore's Law suggests that some computers running KPL will literally have fifty times as much performance as others. It's critical to accommodate a machine's performance characteristics in any game, or else the range of computers capable of playing it will be greatly limited, and the game will quickly become unplayable as new, faster computers become available.
Asteroids.kpl keeps track of a "frame time" and uses it to adjust the game speed as needed. This declaration at the top of the file defines the variable used for this adjustment:
// Tracks the amount of time it took to draw the last frame,
// which is used in calculating how much to move every sprite
// during each pass through the main game loop.
Define secondsPerFrame As Decimal = 0.0
Code in the main program loop in Method Main() constantly recalculates this value:
// Keep track of frame count and frame start time for animation
startTime = TickCount()
// Keep track of the amount of time it took to draw this frame,
// as this value is used to determine the amount of movement for
// the various animation calculations.
endTime = TickCount()
secondsPerFrame = (endTime - startTime) * 0.001
And here is the function that is used to adjust movement distance based on the
secondsPerFrame value. Note that the return value from
Min( 85, AmountPerSecond * secondsPerFrame )
can never be more than 85, so that even on slow computers it is the maximum distance moved in pixels. This can result in choppy rather than smooth movement, but at least allows the game to be played:
// AdjustForFrameRate allows the game objects to move at roughly
// the same speed on computers of any speed, although on slower
// computers which take longer to draw individual frames the
// amount of movement per frame will be higher, and the
// animations will be a little choppier. This is why on very
// slow computers it may look like the animation is "dropping
// frames", since the ship moves in bigger increments to
// compensate for the slow frame rate.
Function AdjustForFrameRate( AmountPerSecond As Decimal ) As Decimal
Return Min( 85, AmountPerSecond * secondsPerFrame )
Finding a Safe Hyperjump Spot in Real Time
The Hyperjump button, "H", will instantly engage the Hyperjump computer, popping your ship momentarily out of spacetime while it computes a safe location to return to spacetime—and saving your butt from that incoming asteroid! When you, the human ship captain, fail to engage Hyperjump quickly enough to save your ship, the Hyperjump computer takes over for you, instantly engaging and saving the ship. Each time this happens, the ship and shields take a beating, though. More than a few times and you and your ship aren't going to make the jump in time, and the two of you will be a yucky splat on the windshield of some nameless asteroid spinning silently along its monotonous path through the cold vastness of space. Grim! That Hyperjump spot algorithm better be a good one!
The challenge for this one is implementing an algorithm that can do this in real-time, given that dozens of asteroids and dozens of fragments can be spinning and moving across nearby space. Not an easy problem. This is a great example of the difference between the way human brains work and computer brains work. It's fairly instantaneous to any of us when we look at an Asteroid field where the largest open space—and therefore the safest spot—is located. Computers can't do that nearly as easily as we can.
The first algorithm I thought about, with the priority being that I needed something to work in real time, was as follows: quickly calculate, for every asteroid displayed, how far it is from all other asteroids. Whichever asteroid is farthest from all other asteroids, hyperjump the ship to just "behind" that asteroid, based on the asteroid's trajectory. This was a pretty cool solution, and did indeed work in real time; but it ended up being much more complex code, and having some quirks that made me go with this simpler approach:
Define Safe As Bool = False
Define I As Int
Define Distance As Decimal
While Not Safe
// Pick a random screen location, not too close to the edge
Ship.X = Random(100,ScreenWidth() - 100)
Ship.Y = Random(75,ScreenHeight() - 75)
// Update the status bar message to show why time (might)
// be stopped
Status("Timespace escape! Hyperjump computer seeking safe jump coordinates!")
Safe = True
I = 1
// While the spot is still safe and we haven't
// checked all asteroids
While Safe And I <= MaxAsteroids
// Calculate the distance from this asteroid.
// The + 40 uses the centerpoint of the
// asteroid for the calculation. Note there is
// no SQUARE() function or ^2 operator in
// KPL v 1 - there will be in KPL v 2 – so we
// just multiply
Distance = Sqrt(((Asteroids[I].X + 40 - Ship.X) * (Asteroids[I].X + 40 - Ship.X)) + ((Asteroids[I].Y + 40 - Ship.Y) * (Asteroids[I].Y + 40 -
// If it's closer than 200 pixels, this is not
// a safe spot
If Distance < 200 Then
Safe = False
// Increment I to check the next Asteroid
I = I + 1
// If the spot still seems safe, we'll
// check the next asteroid
// Clear the status indicator - if we then try
// another spot, this will cause a "blink" effect so
// you, the ship captain, on hold outside of
// normal spacetime, know the hyperjump computer
// is still working on the problem...
// We will not exit this method until the ship has been
// placed at a safe location. My testing shows this is
// rarely more than a blink, but since this is based on
// Random(), and since the screen can get crowded, it can
// take a second or more. This is not ideal, but it is
// simple - and the "escape from timespace into a
// hyperjump" story is a pretty good rationalization! :D
Controlling game playability
Playability obviously ought to be the point of any game. The more playable a game is, the more fun people have with it, the more times they play it, the more they tell the friends about it, the more magazines and websites tell people about it, the more famous you become as a game developer, the more money you make, the more bizarre obsessions and personality quirks you can indulge in, etc., etc. I'm not thinking of any specific examples when I say that.
These are some of the reasons why it's important to consider playability from the beginning, and to implement playability controls as "parameters" in the program so they can be easily adjusted by the programmer to make the game as playable as possible. The other advantage of doing this, as discussed in previous articles, is that parameters that are tweakable make it easy for players to adjust gameplay themselves—if they have access to code, as they do with KPL programs. Some people like games faster, some slower. And since they'll be doing that in KPL code, they'll be polishing some programming skills along the way.
Asteroids.KPL does this with numerous gameplay parameters. Changing these values can pretty drastically change the way the game plays:
Define MaxHyperJumps As Int = 3 // initial jumps, you get more
// jumps as a bonus, see below
Define ShipLives As Int = 3 // initial ships, you get more ships
// as a bonus, see below
// Asteroid Settings
Define MaxAsteroids As Int = 30 // max number of asteroids on the
// screen at once
Define AsteroidLoadSpeed As Int = 500 // wait time in
// milliseconds for each set of
// asteroids to be sent
Define MaxAsteroidSpeed As Int = 10 // Max Start speed on
Define AsteroidLoadSpeedBonusUpdate As Int = 20 // when a bonus
// is made the game sends more
// asteroids faster
Define MaxAsteroidSpeedBonusUpdate As Int = 1 // when a bonus is
// made the game speeds up some
// of the asteroids
// Asteroid Fragment Settings
Define MaxAsteroidFrags As Int = 20
Define MinAsteroidFragmentsPerHit As Int = 1
Define MaxAsteroidFragmentsPerHit As Int = 5
Define MaxAsteroidFragSpeed As Int = 10
// Score Keeping
Define AsteroidsToEndOfGame As Int = 500 // if you hit this many
// Asteroids the game will end
// even if you have ships left
Define LargeAsteroidHitScore As Int = 500 // number of point for
// each asteroid hit
Define SmallAsteroidHitScore As Int = 250 // number of point for
// each Asteroid Fragment hit
Define NewShipBonus As Int = 10000 // you get 1 new ship and 1
// hyperjump for every
// NewShipBonus points
Most of these are pretty obvious, but I want to mention one in particular, because of its importance to controlling the "pace" of the game. People have different preferences about game pace, but in general, a game should start a little slow and easy, and steadily become faster and/or more difficult over time. If that increase happens slowly enough, the user doesn't consciously notice it, but they certainly do get caught up by it and drawn into it. Done right, they're leaning back all comfortable and lazy at the start, and by the end of the game they're leaning forward in their chair and banging on the keyboard! Pace also, of course, determines how long a game actually lasts—the faster it gets, the harder it gets, the sooner it will end. One specific parameter which I would encourage you to play with in order to see playability difference based on pace is MaxAsteroidSpeedBonusUpdate. The starting value of 1 cause some asteroid speeds to increase at a 1 pixel per move each time a bonus ship is awarded. This ends up making the game last a fairly long time, and only very incrementally increases the pace of the game. Try setting that value to 5, running the game again, and seeing how much difference it makes!
Your mission, should you choose to accept it, is twofold.
First, the UFO:
This sneaky, evil, small and tough-to-shoot alien spaceship was an unpredictable, difficult, and big-bonus-point part of the original asteroids—assuming you managed to kill him before he killed you, of course—and we have not yet put it into this KPL version of Asteroids. Right-click, Save Image As... will let you drop the UFO image from this page right into the Asteroids folder.
Second, the safe spot:
Can you come up with a really cool real-time algorithm that works better than our JumpToSafeSpot() code?
Should you take on the KPL code for either of these, and send them back to me, we will be most happy to publish your code, give you lots of kudos, and tell people by the thousands how good of a game developer you are, etc. See above for where that could take you. Mail me at firstname.lastname@example.org if you come up with code for these—or any other cool KPL code, for that matter!
Moral of the story? Coding4Fun isn't just a name—it's the point!
Ah, a teaser: anyone remember Missile Command? Check it out below. Yes, that's KPL, and yes, the game rocks! Watch for the article here next month!