For years, whenever I’ve learned a new programming language, there has eventually come a stage where I end up duplicating the game Asteroids. When I first did this, I was just a lad and chose it simply because the compiler that I was using at computer camp (Borland TurboC++) had graphics libraries that leaned towards this sort of game. Today, I’ve realized that Asteroids, although deceptively simple, forces the programmer into using a number of techniques that will show how well he or she has learned the language. Here's why I love Asteroids as a learning tool.

For those of you who don't know Asteroids, the concept is simple (although the variety of versions that have been made border on the absurd). You have a spaceship, you can fire some sort of Futuristic Space Weapon from its gun, and there are asteroids floating around the screen. You must destroy them all without getting hit by one yourself. Usually, asteroids will break apart into smaller asteroids when shot, down to a fundamental level, beyond which it’s all invisible turtles.

One of the main reasons that I like to use Asteroids for this is that it’s possible to make it geometrically instead of with sprites, programmi large, complex areas for the player to move through (which you would need to do if you’re making an adventure game of any reasonable size), or any of the other stumbling blocks that some games present to the casual programmer — that is to stay, stumbling blocks whose challenge is not in the programming but is instead in a protracted design phase for the game.


To get started, let’s assume a few simplifications. You can only shoot one bullet at a time, there are only three asteroids, and they don’t break apart when shot - the simply disappear. If you’re going to do this, you’re going to need to know (among other things):

  • How to write functions (or methods, or subroutines, or any other variation on the basic concept)

    Unless you’re into unreadable code, you will need to write methods that alter the state of the ship, the asteroids, fire the bullet, and whatever else needs to be done regularly.

  • How to implement a graphical interface in your language of choice.

    Writing a version of Asteroids that works from the command line isn’t really an option for a first attempt at the program, so you’re going to have to use a fairly limited variety of commands to the graphics library you’re choosing to use. In the case of C++ (and other supported languages), I would recommend using OpenGL - not only will it help you get a job someday, but it’s also standardized to the point that you can port the program to another platform and circumvent what are sometimes the messiest parts of the translation.

  • How to implement keyboard input.

    In many languages, keyboard input is easiest when we’re simply talking about reading strings from the command line. Unfortunately, unless you want to be typing ‘L’ and then hitting the Return key every time you want to turn left, you’re going to need to move to something a little bit more complex. Once again, I recommend using something standardized for the time being (like OpenGL, in fact), because nifty as it might be, your Uncle Billy’s personal I/O libraries won’t be doing much in the way of teaching you to program in any sort of standard way, and certainly won’t be helping you write something cross-platform. If you’re lucky, your language will support something relatively simple to implement. If you’re unlucky, you’re going to be spending a fair amount of time learning how streams are implemented in your language.

  • How to implement some basic physics and algebra.

    This is usually one of the last things I put in, simply because it’s more irritating than the other basic steps. However, if you want to have your ship perform in any sort of intuitive way when it’s moving in one direction and you boost in another, you’re going to have to put your ship’s movement in terms of vectors. If you want the asteroids to be able to bounce off of each other, you’re going to need to put in some sort of model to handle elastic collisions. If you want the bullets not to look bizarre as they leave the ship, you’re going to have to be handling your ship’s vector of movement and velocity in a sensical manner.


Okay, so you’re off! It’s working pretty well. You can fly around and shoot things. Maybe you’ve even added in a few bullets by now. But couldn’t it be better? Of course it could. Here are some things to try:

  • Making your program object-oriented.

    If you haven’t done this already, you should as soon as you can. Wouldn’t you like to have:

    • As many asteroids as you want, including smaller asteroids breaking off from larger ones, without having to keep track of every variable individually or through complex (and possibly confusing) use of lists.
    • Similarly, having a number of bullets at once, any one of which can strike any asteroid at any time without having to put in miles and miles of code, is nice to have.
    • You may have noticed by now that you’ve had to implement the Distance Formula, probably in a method of its own. If you’ve done this, it’s more than likely that you either have one large method with a huge variety of input variables or a smattering of different methods to handle collisions between different combinations of objects. Why not define a parent class for the ship, the asteroids, the bullets, and anything else that might be flying around so that you can put a number of variables and methods into the parent class and clean up your code, then add a universal distance formula?
    • Finally, what if you want to add in the option for a second player at some point? If you’ve been writing your code right, it’ll be a matter of (at the very most) a few dozen more lines of code and other changes in order to add this - instead of hundreds.
  • Adding in a high scores list which is maintained between launches.

    This is a nice way to learn about reading and writing to files. It usually is relatively simple to put in once you understand streams and the language’s approach to handling interaction with storage instead of just RAM. It also increases the replayability by a fair deal - if you try to tell me that you’ve never chosen one computer solitaire program over another simply because it keeps more detailed statistics, you’re lying.

  • Adding in a variety of configuration options and extra features.

    This part is a real test of how well you’ve been programming - if you did a good job, it shouldn’t be a huge deal to insinuate these features into your code. If you’ve done a bad job, you’ll have to spend a while reworking. Don’t worry, though: it builds plenty of character, and you can brag about it to your friends on the Internet. People who don’t understand computers are often impressed by how you spent an afternoon fixing problems beyond their comprehension.

And so on and so forth. By now, your program should be developed enough to send to friends or acquaintances. They may even volunteer to help you with the annoying parts of making the program (which is to say, designing nice pictures so that everything isn’t represented as polygons) or suggest features you should add.

If you’ve gotten to this point, it’s more than likely that you’re reasonably familiar with the language you’ve been doing this with, and can move up to making more complex projects that demand more than simply understanding the important concepts of the language. It’s also likely that you’ve found the style in which you program well. This, more than anything, means you’ve moved beyond simply being a coder and up to being a codist.

I’ve made Asteroids in about half of the languages I’ve ever learned (which is saying a fair amount), and each time I figure out something new about how it could or should be made. I can say with a fair degree of certainty that anybody learning a new language will benefit from taking on this project, and heartily congratulate anybody who attempts it after reading this node.


And for your viewing pleasure, here's Asteroids (partially functional) in Ruby. I'll update this as I come back to it: it's my current project, but it competes with my day job. In order to get this to work, you'll need the OpenGL Gem installed, but it should be a straightforward matter if you know your way around the command line.

#!/usr/bin/ruby -w -rubygems
# if the rubygems flag is removed here, it must be present when running the program.
# ex:
# > ruby -rubygems asteroids.rb

# an Asteroids clone in Ruby
# by Alex White
# code@alexwhite.biz
# This code may be used for any non-commercial purpose
# as long as I am cited for my work.
# If you don't cite me, I will find you and hurt you.

require 'opengl'
#require 'glut'
include Gl,Glu,Glut
include Math

$MAX_BULLETS = 300

class Meteor
	attr_accessor :x, :y, :angle, :velocity
	
	def initialize(x, y, angle, velocity = 0)
		@x = x
		@y = y
		@angle = angle
		@velocity = velocity
	end
	
	def distance other
			sqrt( (@x - other.x) ** 2 + (@y - other.y) ** 2)
	end
	
	def move
		@x += @velocity * cos(@angle)
		@x = 0 if @x > $width
		@x = $width if @x < 0
		@y += @velocity * sin(@angle)
		@y = 0 if @y > $height
		@y = $height if @y < 0
		return (@x == 0 or @x == $width or @y == 0 or @y == $height)
		return false
	end
	
	def is_dead?
		false
	end
	
	def kill
	end
end

$bulletsFired = -1
class Bullet < Meteor
	
	def initialize(x, y, angle, velocity, ttl)
		@ttl = ttl
		@id = $bulletsFired += 1
		# chose a velocity greater than the declared maximum
		super(x, y, angle, velocity)
	end
	
	def move
		@ttl -= 1
		return super
	end
	
	def kill
		@ttl = 0
	end
	
	def is_dead?
		@ttl < 1
	end
	
	def to_s
		"x: " + @x.to_s + ", y: " + @y.to_s + ", a: " + @angle.to_s + ", v: " + @velocity.to_s
	end
	
	def id
		@id
	end
	
	def has_ID? id
		@id == id
	end
	
	def draw
		glBegin(GL_POINTS)
			color(rand, rand, rand)
			glVertex2f(@x, @y)
		glEnd
	end
end

class Star < Bullet
	def initialize
		super(rand($width), rand($height), 0, 0, 0)
	end
	
	def is_dead?
		false
	end
end

class Asteroid < Meteor
	def initialize(x = -1, y = -1, radius = 30, health = 100)
		@radius = radius
		@health = health
		@startingHealth = health
		
		# this mess makes sure the asteroid isn't in the middle 3rd of the screen
		x = (-1 ** rand(2)) * (rand($width / 3) + $width / 3) + $width / 2 if x == -1
		y = (-1 ** rand(2)) * (rand($height / 3) + $height / 3) + $height / 2 if y == -1
		
		super(x, y, rand * 2 * PI, rand * 0.75)
	end
	
	def draw
		glBegin(GL_LINE_STRIP)
			for i in 0..20
				glVertex2f(@x + @radius * cos(i * PI / 10), @y + @radius * sin(i * PI / 10))
			end
		glEnd
	end
	
	def is_dead?
		@health < 1
	end
	
	def kill
		@health = 0
	end
	
	def damage amount
		@health -= amount
	end
	
	def health
		return @health.to_f / @startingHealth.to_f
	end
	
	def distance other
		return super other unless other.is_a? Ship 
		if other.is_a? Ship then
			return 0 # for now
		end
	end
	
	def is_touching? other
		return true if other.is_a? Bullet and (distance(other) <= @radius)
		if other.is_a? Ship then
			# there are linear equations that govern the intersection of lines
			# whose endpoints are known and a circle of known radius and center
		end
		return false
	end
	
	def child
		Asteroid.new(@x, @y, @radius / 2, @health / 2)
	end
end

class Ship < Meteor

	def initialize
		@facing = PI / 2
		@acceleration = 0.01
		@rotateSpeed = -0.02
		@maxVelocity = 1
		@radius = 20
		@bullets = Array.new
		@bulletTime = 300
		super($width/2, $height/2, @facing)
	end
	
	def rotate direction
		@facing += @rotateSpeed if direction == :right
		@facing -= @rotateSpeed if direction == :left
		@facing -= 2 * PI until @facing < 2 * PI
		@facing += 2 * PI until @facing > 0
	end
	
	# does not do what it should
	# eventually I will proabbly rename this
	def align
		if @facing < @angle then
			@facing += 2 * @rotateSpeed
		end
		if @facing > @angle then
			@facing -= 2 * @rotateSpeed
		end
		@facing = @angle if (@facing - @angle).abs < 2 * @rotateSpeed
	end
	
	def accelerate
		# a little vector dance
		xVel = @velocity * cos(@angle) + @acceleration * cos(@facing)
		yVel = @velocity * sin(@angle) + @acceleration * sin(@facing)
		
		@angle = atan(yVel / xVel)
		@velocity = xVel / cos(@angle)
		
		#clamping!	
		if @velocity < 0 then
			@velocity *= -1
			@angle += PI
		end
		@angle -= 2 * PI until @angle < 2 * PI
		@angle += 2 * PI until @angle > 0
		@velocity = @maxVelocity if @velocity.abs > @maxVelocity
		
	end
	
	def fire
		xVel = @velocity * cos(@angle) + cos(@facing)
		yVel = @velocity * sin(@angle) + sin(@facing)
		
		angle = atan(yVel / xVel)
		velocity = xVel / cos(angle)

		if velocity < 0 then
			velocity *= -1
			angle += PI
		end
		angle -= 2 * PI until angle < 2 * PI
		angle += 2 * PI until angle > 0
		
		@bullets.push(Bullet.new(points[0][0], points[0][1], angle, velocity, @bulletTime)) if @bullets.length < $MAX_BULLETS
	end
	
	def points
		[[@x + @radius * cos(@facing), @y + @radius * sin(@facing)],
		[@x + @radius * cos(@facing + (3 * PI / 4)), @y + @radius * sin(@facing + (3 * PI / 4))],
		[@x + @radius * cos(@facing + (5 * PI / 4)), @y + @radius * sin(@facing + (5 * PI / 4))]]
	end
	
	def draw
		color(1, 1, 1)
		glBegin(GL_LINE_LOOP); points.each do |vertex|
			glVertex2fv vertex
		end; glEnd
		@bullets.each do |bullet|
			bullet.draw
		end
	end
	
	def move asteroids
		super()
		@bullets.each do |bullet|
			bullet.kill if bullet.move
			for asteroid in asteroids
				if asteroid.is_touching? bullet
					asteroid.damage 1
					id = bullet.id
					bullet.kill
					asteroids.push(asteroid.child).push(asteroid.child) if asteroid.is_dead?
				end
			end
		end
		@bullets.delete_if { |bullet| bullet.is_dead? }
		asteroids.delete_if { |asteroid| asteroid.is_dead? }
	end
end

errors = [	"input" => "Invalid input.",
			"stupid" => "You've been stupid.",
			"dead" => "You're dead."
			]
commands = [ "quit" => "exit",
			 "help" => "help" ]
$fullscreen = false
$width = 1000; $height = 1000
$gameOver = false
$paused = false
$x = 0; $y = 0
$mode = 0
$slow = 10000
$slowUsed = false

$keys = Array.new
$turtle = Ship.new
$stars = Array.new
50.times { $stars.push(Star.new) }
$asteroids = Array.new
2.times { $asteroids.push(Asteroid.new) }

def parse(line)
	args = line.split(" ")
	print("Received command < " + args[0])
	if args.length > 1 then
		print(" > with arguments <")
		args[1...args.length].each { |a| print " " + a }
	end
	puts " >"
	command = commands[args[0]]
	eval(command)
	#eval(commands[args[0]]) if commands[args[0]] != nil#args[0] == :quit
end

def errorCheck(code)
	begin
		eval(code)
	rescue
		puts errors[$!]
	end
end

def parseLoop
	print "> "
	input = gets
	#errorCheck("unless input.empty? input.each(\", \") { |command| parse command }")
	input.each(", ") { |command| parse command } unless input.empty?
end

def color (r, g, b, a=1)
	glColor4f(r, g, b, a)
end

def background(r, g, b, a=0)
	glClearColor(r, g, b, a)
end

def display
	glClear(GL_COLOR_BUFFER_BIT)
	color(1, 1, 1)
	$turtle.draw
	$stars.each { |star| star.draw }
	
	$asteroids.each do |asteroid|
		color(1, asteroid.health, asteroid.health)
		asteroid.draw
	end
	
	color(1.0 - ($slow + 200).to_f / 10200, ($slow + 200).to_f / 10200.0, 0)
	glBegin(GL_POLYGON)
	glVertex2f($width - 100, $height - 30)
	glVertex2f($width - 100 + $slow / 100, $height - 30)
	glVertex2f($width - 100 + $slow / 100, $height)
	glVertex2f($width - 100, $height)
	glEnd
	
	glutSwapBuffers
end

def reshape (x, y)
end

def interpret
	case $mode
	when 0 # Asteroids mode
		$keys.each do |key|	
			case key
			when GLUT_KEY_UP
				$turtle.accelerate
				#$y += 0.1
			when GLUT_KEY_DOWN
				$turtle.align
				#$y -= 0.1
			when GLUT_KEY_RIGHT
				$turtle.rotate :right
				#$x += 0.1
			when GLUT_KEY_LEFT
				$turtle.rotate :left
				#$x -= 0.1
			when 32 # space bar
				$turtle.fire
			when 27 # esc
				exit
			when ?p
				if $paused then $paused = false
				else $paused = true end
				#if $slow > 0 then
				#	$slow -= 50
				#	$slowUsed = true
				#	p $slow
				#end
			end
		end
	end
end

def idle
	#$paused = false
	$slow += 5 if $slow < 10000
	$slowUsed = false
	interpret
	
	if not $paused then
	#if $slowUsed == false # THIS WILL CAUSE HUGE PROBLEMS
		#$turtle.move
		$turtle.move $asteroids
		$asteroids.each do |asteroid|
			asteroid.move
			#$turtle.management asteroid
		end
	end
	
	display
end

def keyUp(key, x, y)
	$keys.delete(key)
end

def keyDown(key, x, y)
	$keys.push key unless $keys.include? key
end

def init
	background(0, 0, 0)
	glMatrixMode(GL_PROJECTION)
	glLoadIdentity
	gluOrtho2D(0.0, $width, 0.0, $height)
end

def openGLInit
	glutInit
	
	#build the window
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB)
    glutInitWindowSize($width, $height)
    glutInitWindowPosition(100, 100)
    glutCreateWindow("Game")
	glutFullscreen if $fullscreen

	#program control callbacks
    glutDisplayFunc(method(:display).to_proc)
    glutIdleFunc(method(:idle).to_proc)
	glutReshapeFunc(method(:reshape).to_proc)
	
	#input callbacks
    glutKeyboardFunc(method(:keyDown).to_proc)
	glutSpecialFunc(method(:keyDown).to_proc)
    glutKeyboardUpFunc(method(:keyUp).to_proc)
    glutSpecialUpFunc(method(:keyUp).to_proc)
    glutIgnoreKeyRepeat(1)
	
    glutSetCursor(GLUT_CURSOR_FULL_CROSSHAIR)
	
    init
    glutMainLoop
end

openGLInit

Log in or register to write something here or to contact authors.