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 doLoop
It 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!