Brian Embleton : Clone of the Atari classic. This is more of a toy than a game - the player gets infinite lives and there's no difficulty adjustment. I also added sound support using Snack with the original Asteroids sound effects [L1 ] (link broken). The result was was quite enjoyable. If you would like to use sounds, unzip the media folder and place it in the same folder as the source.
See also another Asteroids, and Simple Space Ship Game
# Create an asteroid at X,Y
proc asteroid {size X Y} {
global rocks options
set coords {}
set points [expr $size*5.0]
for {set i 0} {$i < $points} {incr i} {
set r [expr $size*(rand()*2 + 3)]
set rad [expr 2*3.14159 * $i/$points]
set x [expr $X + $r*cos($rad)]
set y [expr $Y + $r*sin($rad)]
lappend coords $x
lappend coords $y
}
set id [.c create polygon $coords -fill gray[expr int(rand()*5)*5+40] -tags rock]
set dx [expr (rand()*$options(rock_speed)+.3)*pow(-1,int(rand()*2)) ]
set dy [expr (rand()*$options(rock_speed)+.3)*pow(-1,int(rand()*2)) ]
set rocks($id) [list $id $size $X $Y $dx $dy]
return $id
}
# Create the ship object
# Place it in the middle of the field.
# I dont do any proximity calculations so there is a good
# chance the ship will blow up right away.
proc makeShip {} {
global ship canvas_width canvas_height score
global ship_dir
incr score(ships)
set x [expr $canvas_width/2]
set y [expr $canvas_height/2]
set dx 0
set dy 0
# ship_dir is in radians
set ship_dir 0
set id [.c create line $x $y [expr $x+1] $y -fill blue -width 1 -arrow last -arrowshape {12 14 5} -tags ship]
set ship [list $id "" $x $y $dx $dy]
}
# The ship crashed. Remove the canvas object and reset the global variable
proc resetShip {} {
global ship
set id [lindex $ship 0]
.c delete $id
set ship ""
after 4000 makeShip
}
proc drawShip {} {
global ship ship_dir after_procs
if {$ship == ""} { return }
foreach {id size x y dx dy} $ship {}
set x2 [expr $x+10*cos($ship_dir)]
set y2 [expr $y+10*sin($ship_dir)]
.c coords $id $x $y $x2 $y2
}
# Adjust the ships angle to 0-2pi
proc correctShipDir {} {
global ship_dir
set twopi [expr 2*3.14159]
if {$ship_dir < 0} { set ship_dir [expr $twopi+$ship_dir] }
if {$ship_dir > $twopi} { set ship_dir [expr $ship_dir-$twopi] }
}
# increment the ships direction to the left or right
proc rotateShip {dir} {
global ship ship_dir options
if {$ship == ""} { return }
set ship_dir [expr $ship_dir + $dir*$options(ship_turn_speed)]
correctShipDir
}
# For Mouse Aiming
# Immediately adjust the ships direction to point
# directly at the cursor. Perhaps I could add in stepper logic
# to slowly rotate the ship around, as if the player was actually
# using the keyboard controls, to make gameplay more like the original
proc mouseOver {x y} {
global ship ship_dir
if {$ship == ""} { return }
foreach {id size X Y dx dy} $ship {}
set dx [expr $x.0-$X]; set dy [expr $y.0-$Y]
set ship_dir [expr atan2($dy, $dx)]
correctShipDir
}
# Mouse control header.
# use [after] to implement repeating thruster controls
proc thrust_mouse {} {
global mouse_timers options
shipThrust
set mouse_timers(thrust) [after 100 "thrust_mouse"]
}
# ... and to turn off the thrusters when button 3 is released
proc cancel_thrust_mouse {} {
global mouse_timers
after cancel $mouse_timers(thrust)
}
# Ship thruster adjustment
# change the ships velocity based on the current direction and speed
# Also, prevent the ship from going faster than the max speed
proc shipThrust {} {
global ship ship_dir options
if {$ship == ""} { return }
# add a small proportion of the ships dir to the movement vectors
set dx [lindex $ship 4]
set dy [lindex $ship 5]
set dx [expr $dx + $options(ship_thrust) * cos($ship_dir)]
set dy [expr $dy + $options(ship_thrust) * sin($ship_dir)]
set mag [expr sqrt($dx*$dx + $dy*$dy)]
if {$mag > $options(max_ship_velocity)} {
set dx [expr $options(max_ship_velocity)*cos($ship_dir)]
set dy [expr $options(max_ship_velocity)*sin($ship_dir)]
}
lset ship 4 $dx
lset ship 5 $dy
if {!$options(playing_thrust)} {
playSound thrust
set options(playing_thrust) 1
after 500 {
global options
set options(playing_thrust) 0
}
}
}
# Mouse control header for button 1 down
proc fire_mouse {} {
global mouse_timers options
fire
set mouse_timers(fire) [after 200 "fire_mouse"]
}
# Mouse button 1 release
proc cancel_fire_mouse {} {
global mouse_timers
after cancel $mouse_timers(fire)
}
# Create a new bullet with the same direction as the ship
# The maximum number of bullets is defined in the options,
# as well as rate of fire and maximum range
proc fire {} {
global ship ship_dir bullets options score
if {$ship == ""} { return }
# only x bullets on the screen at a time
if {[array size bullets] == $options(max_bullets)} { return }
incr score(shots_fired)
# create a new bullet at the front of the ship
foreach {id size x y dx dy} $ship {}
set X [expr $x+12*cos($ship_dir)]
set Y [expr $y+12*sin($ship_dir)]
set x1 [expr $X-1]; set x2 [expr $X+1]
set y1 [expr $Y-1]; set y2 [expr $Y+1]
set id [.c create oval $x1 $y1 $x2 $y2 -fill white -outline gray75 -tags bullet]
set dx [expr $options(bullet_speed)*cos($ship_dir)]
set dy [expr $options(bullet_speed)*sin($ship_dir)]
set bullet [list $id "" $X $Y $dx $dy]
set bullets($id) $bullet
# destroy bullets after x seconds
after [expr 1000*$options(bullet_range)/$options(bullet_speed)/$options(frame_rate)] "dropBullet $id"
playSound fire
}
# Destroy the bullet objects when they get to their maximum range
proc dropBullet {id} {
global bullets
array unset bullets $id
.c delete $id
}
# For when the asteroids are shot or the ship crashes
# there are 3 sizes of rocks: 1, 2 and 3
# sizes 2 and 3 break into 3 or 4 rocks of sizes 1 and 2
proc explode {id} {
global rocks
foreach {id size X Y dx dy} $rocks($id) {}
.c delete $id
array unset rocks $id
# all rocks gone now?
if {[array size rocks] == 0} { after 3000 newLevel }
# if size is 2 or 3, make 3 or 4 smaller rocks
if {$size > 1} {
set size [incr size -1]
set n [expr int(rand()*2+3)]
for {set i 0} {$i < $n} {incr i} {
set rad [expr 2*3.14159*$i/$n]
set x [expr $X + $size*2*cos($rad)]
set y [expr $Y + $size*2*sin($rad)]
asteroid $size $x $y
}
}
playSound boom
}
# Generic movement function
# given an object {id size x y dx dy},
# add the velocities to the locations, use the canvas move command,
# and return the new item properties
proc move {item} {
global canvas_height canvas_width
foreach {id size x y dx dy} $item {}
set nx [expr $x+$dx]
set ny [expr $y+$dy]
if {$nx < 0} { set nx [expr $canvas_width+$nx] }
if {$ny < 0} { set ny [expr $canvas_height+$ny] }
if {$nx > $canvas_width} { set nx [expr $nx-$canvas_width] }
if {$ny > $canvas_height} { set ny [expr $ny-$canvas_height] }
lset item 2 $nx
lset item 3 $ny
set ddx [expr $nx-$x]
set ddy [expr $ny-$y]
.c move $id $ddx $ddy
return $item
}
# Detects rock collisions with the given canvas object
# If there is a rock at the same location, return its id
# otherwise, return null
proc detectCollision {item} {
foreach {id size x y dx dy} $item {}
set ids [lsort -integer [.c find overlapping [expr $x-3] [expr $y-3] [expr $x+3] [expr $y+3]]]
if {[llength $ids] > 1} {
set i [lsearch $ids $id]
set object [lindex [lreplace $ids $i $i] 0]
if {[.c gettags $object] == "rock"} { return $object }
}
return ""
}
proc updateScreen {} {
global rocks ship bullets score
# move each rock
foreach id [array names rocks] {
set rock $rocks($id)
set rocks($id) [move $rock]
}
if {$ship != ""} {
# move the ship
set ship [move $ship]
set id [lindex $ship 0]
drawShip
# check for ship collisions
set rock_id [detectCollision $ship]
if {$rock_id != ""} {
resetShip
explode $rock_id
}
}
# move the bullets
foreach id [array names bullets] {
set bullet $bullets($id)
set bullets($id) [move $bullet]
# check for collisions
set rock_id [detectCollision $bullet]
if {$rock_id != ""} {
dropBullet $id
switch [lindex $rocks($rock_id) 1] {
1 {incr score(small_rocks)}
2 {incr score(med_rocks)}
3 {incr score(big_rocks)}
}
updateHitScore
explode $rock_id
}
}
}
proc newLevel {} {
global canvas_height canvas_width score
incr score(stage)
# create random field
for {set i 0} {$i < 30} {incr i} {
asteroid [expr int(rand()*3)+1] [expr rand()*$canvas_width] [expr rand()*$canvas_height]
}
}
# not actually used, but useful for debugging
proc clear {} {
global rocks
foreach id [array names rocks] {
array unset rocks $id
.c delete $id
}
}
proc updateHitScore {} {
global score
foreach {var val} [array get score] { set $var $val }
set score(total_rocks) [expr $small_rocks+$med_rocks+$big_rocks]
set score(rocks) "$score(total_rocks) ($small_rocks/$med_rocks/$big_rocks)"
set score(accuracy) [format %.1f%% [expr 100.0*$score(total_rocks)/$shots_fired]]
}
proc resetScore {} {
global score
array set score {
ships 0
stage 0
shots_fired 0
accuracy 0
small_rocks 0
med_rocks 0
big_rocks 0
total_rocks 0
rocks "0 (0/0/0)"
rating "ensign"
}
}
proc loadSounds {} {
# define the sound objects
snack::sound sound_music
snack::sound sound_thrust
snack::sound sound_fire
snack::sound sound_boom
# if the files dont exist, it wont affect playing the sounds
catch {
sound_music configure -load "media/duduhn.wav"
sound_thrust configure -load "media/fhhh.wav"
sound_fire configure -load "media/bullet.wav"
sound_boom configure -load "media/boom.wav"
}
}
proc playMusic {} {
global options
if {$options(play_music)} {
sound_music play -command "after 100 playMusic"
} else {
after 500 playMusic
}
}
proc playSound {snd} {
sound_$snd play
}
proc doLoop {} {
global options rocks
updateScreen
after [expr 1000/$options(frame_rate)] doLoop
}
package require snack
set canvas_height 480
set canvas_width 640
array set options {
frame_rate 30
rock_speed 1.8
ship_turn_speed 0.2
ship_thrust 0.3
max_ship_velocity 2
max_bullets 10
bullet_speed 4
bullet_range 400
play_music 1
playing_thrust 0
}
frame .f
label .f.l1 -bg black -fg green -text "New Ships: "
label .f.l2 -bg black -fg green -width 5 -anchor w -textvariable score(ships)
label .f.l3 -bg black -fg green -text "Level: "
label .f.l4 -bg black -fg green -width 5 -anchor w -textvariable score(stage)
label .f.l5 -bg black -fg green -text "Shots Fired: : "
label .f.l6 -bg black -fg green -width 6 -anchor w -textvariable score(shots_fired)
label .f.l7 -bg black -fg green -text "Hits: "
label .f.l8 -bg black -fg green -width 15 -anchor w -textvariable score(rocks)
label .f.l9 -bg black -fg green -text "Accuracy: "
label .f.l10 -bg black -fg green -width 6 -anchor w -textvariable score(accuracy)
for {set i 1} {$i <= 10} {incr i} {
pack .f.l$i -side left
}
canvas .c -width $canvas_width -height $canvas_height -bg black -cursor tcross
bind . <Key-Up> {shipThrust}
bind . <Key-Left> {rotateShip -1}
bind . <Key-Right> {rotateShip 1}
bind . <Key-space> {fire}
bind .c <Motion> "mouseOver %x %y"
bind .c <Button-1> {fire_mouse}
bind .c <ButtonRelease-1> {cancel_fire_mouse}
bind .c <Button-3> {thrust_mouse}
bind .c <ButtonRelease-3> {cancel_thrust_mouse}
checkbutton .music -text "Music On" -variable options(play_music)
label .help -justify left -text "Aim:\tMouse or Left/Right\nFire:\tButton 1 or Spacebar\nThrust:\tButton 2 or Up"
pack .f .c .music .help -side top
loadSounds
resetScore
makeShip
newLevel
playMusic
doLoopIt looks great! Is there away to tweak the moving right or left so it's more responsive? The arrow key are a little sluggish by default.
Nice, but I bet you'd get noticeably better performance if you braced your expressions. It's a good habit to get into.
I didn't know about the performance benefit of placing expressions in braces. Thanks for the info!