Space Invaders Part 4
My goals for the 4th iteration were to:
- allow the tank to shoot at the invaders
- have invaders 'die' when hit
To implement these goals we need a number of new elements and behaviours:
- the world now has a collection of bullets.
- bullets have a location and consist of a single block.
- bullets move upwards until they collide with a space invader (cause the invader to disappear)
In addition play testing showed that we need to restrict the number of bullets on the screen (or introduce a delay between shots). Without this you can just fire a lethal stream of bullets taking out all the UFO's very quickly.
Bullets, tanks and ufos turned out to be structured very similarly. I should have implemented these using plain posn's as this would have allowed more function sharing.
Code
(require racket/base)
(define-struct ufo (x y direction) #:transparent)
(define-struct bullet (x y) #:transparent )
(define-struct tank (x y) #:transparent)
(define-struct world (ufos tank bullets))
; constants
(define WIDTH 300) ; the maximal number of blocks horizontally
(define HEIGHT 300) ; the maximal number of blocks horizontally
(define BULLET_DIST 5) ; the distance travelled by a bullet
(define SIZE 10) ; the size of the block making up the ufo
(define UFO-WIDTH 50)
(define MAX-BULLETS 2) ; max number of bullets on screen at a time
(define BLOCK ;rendered as red squares with black rims - stolen from tetris
(overlay (rectangle (- SIZE 1) (- SIZE 1) "solid" "red")
(rectangle SIZE SIZE "outline" "black")))
(define TEST-UFO (make-ufo 30 30 1)) ; for testing
(define TEST-UFOS (list (make-ufo 30 30 1)
(make-ufo 80 30 1)
(make-ufo 130 30 1))); for testing
; initial tank position should be middle of the screen, one block up
; from the bottom
(define TANK (make-tank (/ WIDTH 2) (- HEIGHT (* SIZE 2))))
; creates a row of ufos on the y-cordinate
; Integer Ufos -> Ufos
(define (make-ufos-for-y y-cordinate ufos)
(cond ((empty? ufos) (make-ufos-for-y y-cordinate
(cons (make-ufo 20 y-cordinate 1) ufos)))
((> (+ (ufo-x (first ufos)) UFO-WIDTH) WIDTH) ufos)
(else (printf "first ~a" (+ (ufo-x (first ufos))))
(make-ufos-for-y y-cordinate
(cons (make-ufo (+ (ufo-x (first ufos))
UFO-WIDTH) y-cordinate 1) ufos )))))
; create a collection of rows of ufos
; Integer Ufos -> Ufos
(define (make-ufos rows ufos)
(cond ((= 0 rows) ufos)
(else (make-ufos (- rows 1)
(append ufos (make-ufos-for-y (- ( * rows 60) 30)
empty))))))
; draw a triangular shaped alien
(define (draw-ufo ufo background)
(place-image/align BLOCK (ufo-x ufo) (ufo-y ufo) "left" "bottom"
(place-image/align BLOCK (- (ufo-x ufo) SIZE) (ufo-y ufo) "left" "bottom"
(place-image/align BLOCK (+ (ufo-x ufo) SIZE) (ufo-y ufo) "left" "bottom"
(place-image/align BLOCK (ufo-x ufo) (+ (ufo-y ufo) SIZE) "left" "bottom"
background)))))
; draw a tank shaped tank
(define (draw-tank tank background)
(place-image/align BLOCK (tank-x tank) (tank-y tank) "left" "bottom"
(place-image/align BLOCK (- (tank-x tank) SIZE) (tank-y tank) "left" "bottom"
(place-image/align BLOCK (+ (tank-x tank) SIZE) (tank-y tank) "left" "bottom"
(place-image/align BLOCK (tank-x tank) (+ (tank-y tank) SIZE)
"left" "bottom"
(place-image/align BLOCK (- (tank-x tank) SIZE) (+ (tank-y tank) SIZE )
"left" "bottom"
(place-image/align BLOCK (+ (tank-x tank) SIZE) (+ (tank-y tank) SIZE)
"left" "bottom"
(place-image/align BLOCK (tank-x tank) (- (tank-y tank) SIZE)
"left" "bottom"
background))))))))
; Draw a bullet on the background
; bullet, scene -> scene
(define (draw-bullet bullet background)
(place-image/align BLOCK (bullet-x bullet)
(bullet-y bullet) "left" "bottom" background))
; Draw a collection of bullets
(define (draw-bullets bullets background)
(cond ((empty? bullets) background)
(else (draw-bullet (first bullets)
(draw-bullets (rest bullets) background)))))
; Move the bullets;
; If the bullet moves past the top of the screen, remove it.
(define (move-bullets bullets)
(cond ((empty? bullets) bullets)
(else (cond ((> 0 (bullet-y (first bullets)))
(move-bullets (rest bullets)))
(else
(cons (move-bullet (first bullets))
(move-bullets (rest bullets))))))))
; simple test case for moving a list of bullets
(check-expect (move-bullets (list (make-bullet 100 100) (make-bullet 90 100)))
(list (make-bullet 100 (- 100 BULLET_DIST))
(make-bullet 90 (- 100 BULLET_DIST))))
; Move a bullet
(define (move-bullet bullet)
(make-bullet (bullet-x bullet) (- (bullet-y bullet) BULLET_DIST)))
; basic bullet moving test
(check-expect (move-bullet (make-bullet 50 50)) (make-bullet 50 (- 50 BULLET_DIST)))
; draw the collection of ufos on to the background.
; a simple recursive function
; Ufos Scene -> Scene
(define (draw-ufos ufos background)
(cond ((empty? ufos) background)
(else (draw-ufo (first ufos) (draw-ufos (rest ufos) background)))))
; draws the world as a scene
; World -> Scene
(define (render-scene world)
(draw-ufos (world-ufos world)
(draw-tank (world-tank world)
(draw-bullets (world-bullets world)
(empty-scene WIDTH HEIGHT)))))
; move the ufo left or right according to it's current direction
; when it reaches the edge of the screen, flip the direction the ufo
; is travelling
(define (move-ufo ufo)
(future-ufo-x ufo) (ufo-direction ufo) WIDTH)
(cond ((> 10 (future-ufo-x ufo)) (change-ufo-direction ufo))
((< (- WIDTH 10) (future-ufo-x ufo)) (change-ufo-direction ufo))
(else (make-ufo (future-ufo-x ufo)
(ufo-y ufo) (ufo-direction ufo)))))
; returns an identical ufo but with the direction changed
(define (change-ufo-direction ufo)
(make-ufo (ufo-x ufo) (+ (ufo-y ufo) 30) (* -1 (ufo-direction ufo))))
; simplest test for changing direction.
(check-expect (change-ufo-direction (make-ufo 10 10 1)) (make-ufo 10 40 -1))
; moves each ufo one at a time
(define (move-ufos ufos)
(cond ((empty? ufos) ufos)
(else (cons (move-ufo (first ufos)) (move-ufos (rest ufos))))))
; moves the tank left or right
; the direction is positive/negative mulitplier that will
; determine in which direction the tank moves
; we use the future-tank-x function to determine if the tank would be moved
; into a position outside of the game word.
; tank direction -> tank
(define (move-tank tank direction)
(cond ((> 10 (future-tank-x direction tank)) tank)
((< (- WIDTH 20) (future-tank-x direction tank)) tank)
(else (make-tank (future-tank-x direction tank) (tank-y tank) ))))
; returns the position the tank will be in if it moves in *direction*
; used for collision detection with walls and actual moving of tank
; Integer Tank -> Integer
(define (future-tank-x direction tank)
(+ (* SIZE direction) (tank-x tank) ))
; just like future-tank-x but for ufos
; the diference is that the ufo holds the direction internally
(define (future-ufo-x ufo)
(+ (* 1 (ufo-direction ufo)) (ufo-x ufo) ))
; Fire a bullet and returns new collection of bullets
; Allows only MAX-BULLETS on screen
; World -> Bullets
(define (fire-tank world)
(cond ((eq? MAX-BULLETS (length (world-bullets world))) (world-bullets world))
(else
(cons (make-bullet (tank-x (world-tank world) )
(tank-y (world-tank world) )) (world-bullets world) ))))
; respond to keyboard input.
; Left - Move tank left
; Right - Move tank right
; Space - Fire (not yet)
(define (handle-key-events world ke)
(make-world (world-ufos world)
(cond
[(string=? "left" ke) (move-tank (world-tank world) -1)]
[(string=? "right" ke) (move-tank (world-tank world) 1) ]
[else (world-tank world)])
(cond
[(key=? " " ke) (fire-tank world)]
[else (world-bullets world)]
)))
; bullets/ufo collision detector
; returns true in the event of a collision
; ufo, bullets -> boolean
(define (collide? ufo bullets)
(cond ((empty? bullets) false)
((collide-helper? (first bullets) ufo) true)
( else (collide? ufo (rest bullets)))))
; bullet/ufo collision helper. detects collision between a single bullet and
; a ufo
; ufo, bullet -> boolean
(define (collide-helper? bullet ufo)
(cond ((and (between? (bullet-x bullet) (- (ufo-x ufo) SIZE 5)
(+ (ufo-x ufo) SIZE 5))
(between? (bullet-y bullet) (- (ufo-y ufo) SIZE 5)
(+ (ufo-y ufo) SIZE 5)))
true)
(else false)))
; helper function to determine if a number is within a range.
; i'm sure there must be a similar one defined in the libs, but I
; couldn't find it.
; Integer, Integr, Integer -> Boolean
(define (between? x low high)
(cond ((and (> x low) (< x high) true))
(else false)))
(check-expect (between? 50 10 100) true)
(check-expect (between? 10 50 100) false)
; remove any ufos from the collection that have been hit by a bullet
; ufos, bullets -> ufos
(define (bullet-collision-ufos ufos bullets)
(cond ((empty? ufos) ufos )
(else (cond ((collide? (first ufos) bullets)
(bullet-collision-ufos (rest ufos) bullets))
(else (cons (first ufos)
(bullet-collision-ufos (rest ufos) bullets)))))))
; On each clock tick, move the world further in time.
; this calls the bullet-collision-ufos function twice - this calculates which
; bullets and ufos are left. I wish I didn't have to call this twice but not
; sure how else to save the value...
(define (progress-world world)
(make-world
(bullet-collision-ufos (move-ufos (world-ufos world))
(world-bullets world))
(world-tank world)
(move-bullets (world-bullets world))))
; This is the big bang function that drives the game.
(define (space-invaders-main rate)
(big-bang (make-world (make-ufos 3 empty ) TANK empty)
(on-key handle-key-events)
(on-tick progress-world rate)
(to-draw render-scene)))
(space-invaders-main 0.01)
; TESTS
; check moving ufo left and right works as expected
(check-expect (move-ufo (make-ufo 10 10 1)) (make-ufo 20 10 1))
(check-expect (move-ufo (make-ufo 20 10 -1)) (make-ufo 10 10 1))
; check hitting the edge of the screen changes the ufos direction
(check-expect (move-ufo (make-ufo WIDTH 10 1)) (make-ufo WIDTH 10 -1))
; check the ufo change direction changes left and right
(check-expect (change-ufo-direction (make-ufo 10 10 1)) (make-ufo 10 10 -1))
(check-expect (change-ufo-direction (make-ufo 10 10 -1)) (make-ufo 10 10 1))
; simple example - a single block in an empty landscape
(check-expect (draw-ufo TEST-UFO (empty-scene WIDTH HEIGHT))
(place-image/align BLOCK (ufo-x TEST-UFO) (ufo-y TEST-UFO) "left" "bottom"
(place-image/align BLOCK (- (ufo-x TEST-UFO) SIZE) (ufo-y TEST-UFO)
"left" "bottom"
(place-image/align BLOCK (+ (ufo-x TEST-UFO) SIZE) (ufo-y TEST-UFO)
"left" "bottom"
(place-image/align BLOCK (ufo-x TEST-UFO) (+ (ufo-y TEST-UFO) SIZE)
"left" "bottom"
(empty-scene WIDTH HEIGHT))))))
; test drawing three ufo's via the draw-ufos function
(check-expect (draw-ufos TEST-UFOS (empty-scene WIDTH HEIGHT))
(draw-ufo (first TEST-UFOS)
(draw-ufo (second TEST-UFOS)
(draw-ufo (third TEST-UFOS) (empty-scene WIDTH HEIGHT)))))
; test the tank drawing works
(check-expect (draw-tank TANK (empty-scene WIDTH HEIGHT))
(place-image/align BLOCK (tank-x TANK) (tank-y TANK) "left" "bottom"
(place-image/align BLOCK (- (tank-x TANK) SIZE) (tank-y TANK) "left" "bottom"
(place-image/align BLOCK (+ (tank-x TANK) SIZE) (tank-y TANK) "left" "bottom"
(place-image/align BLOCK (tank-x TANK) (+ (tank-y TANK) SIZE) "left" "bottom"
(place-image/align BLOCK (- (tank-x TANK) SIZE) (+ (tank-y TANK) SIZE )
"left" "bottom"
(place-image/align BLOCK (+ (tank-x TANK) SIZE) (+ (tank-y TANK) SIZE)
"left" "bottom"
(place-image/align BLOCK (tank-x TANK) (- (tank-y TANK) SIZE)
"left" "bottom"
(empty-scene WIDTH HEIGHT)))))))))
No comments:
Post a Comment