Skip to content

Sprites in Motion

Alban edited this page Sep 8, 2020 · 32 revisions

Sprites in Motion

This is a tutorial for using the sprite functions; as an example we will write a game.

Press shift-return on each example to follow along.

Lets start with a static scene; the hero at the bottom of the screen.

The invaders are in a line at the top.

(image-size 800 600) 
 

(load-sprites "images/bg1.jpg" 0)	
(load-sprites "images/h1.png" 1)	
(load-sprites "images/D-17.png" 20)
 
(draw-sprite 0 0.0 10.0)
(draw-sprite 1 400.0 460.0)

(for x from 120.0 to 700.0 step 88.0
	(draw-scaled-rotated-sprite
		20 x 20.0 0.0 0.65) )
 
(show 1)	

That is essentially a picture; there is nothing animated.

We certainly need the hero ship to move from left to right when the player presses a key.

Arranging things into functions helps.

(define load-all-sprites 
 (lambda ()
   (load-sprites "images/bg1.jpg" 0)	
   (load-sprites "images/h1.png" 1)	
   (load-sprites "images/D-17.png" 20)))

(define clear-scene 
  (lambda ()
    (clear-image 0.0 0.0 0.0 1.0)
    (draw-sprite 0 0.0 10.0)))

(define draw-invaders 
 (lambda (x y)
   (for x-pos from x to 700.0 step 88.0
	(draw-scaled-rotated-sprite
		20 x-pos y 0.0 0.65))))

(define draw-hero 
  (lambda (x) 
    (draw-sprite 1 x 460.0)))


The same scene is now drawn using the more meaningful functions.

 
(load-all-sprites)
(clear-scene)
(draw-invaders 120.0 20.0)
(draw-hero  400.0)
(show 0)

We can see that drawing the scene will require the hero and invaders positions at least.

(define draw-scene 
 (lambda (hx ix iy )
   (clear-scene)
   (draw-invaders ix iy)
   (draw-hero hx)))

Now it is easy to try the scene out with different positions.

 
(draw-scene 140.0 168.0 20.0)
(show 0)

Lets test the aliens sweeping straight down the screen

(for y from 20.0 to 400.0 step 0.5 
 (draw-scene 400.0 100.0 y ) (show 0))

The position of the hero is going to depend on a key press by the player.

So we should define a variable for that.

(define h-x 400.0)

h-x will be the hero placement.

As we set through each frame of the game; we will need to read the keys the player pressed.

Look at the results from (graphics-keys)

(graphics-keys)

Click on the image pane at the top so it has focus and press left arrow.

See that graphics keys now shows

(left . #t).

A way to check for this and change the h-x variable is as follows

(when 
 (and (< (cdr (assq 'recent keys)) 50) (cdr (assq 'left keys)))   
 (set! h-x (- h-x 1.0)))

In english "when the left key was pressed in the last 50 ms move the hero to the left."

It makes sense to write a function to check for the keys we can add more checks later.

To recap a bit our program now looks like this

;; example 1 - move hero with arrow keys.

(define h-x 400.0)

(define load-all-sprites 
 (lambda ()
   (load-sprites "images/bg1.jpg" 0)	
   (load-sprites "images/h1.png" 1)	
   (load-sprites "images/D-17.png" 20)))

(define clear-scene 
  (lambda ()
    (clear-image 0.0 0.0 0.0 1.0)
    (draw-sprite 0 0.0 10.0)))

(define draw-invaders 
 (lambda (x y)
   (for x-pos from x to 700.0 step 88.0
	(draw-scaled-rotated-sprite
		20 x-pos y 0.0 0.65))))

(define draw-hero 
  (lambda (x) 
    (draw-sprite 1 x 460.0)))

(define draw-scene 
 (lambda (hx ix iy )
   (clear-scene)
   (draw-invaders ix iy)
   (draw-hero hx)))

(define check-keys
  (lambda (keys)
    (when (and (< (cdr (assq 'recent keys)) 50)
               (cdr (assq 'left keys)))
      (set! h-x (- h-x 1.0)))
    (when (and (< (cdr (assq 'recent keys)) 50)
               (cdr (assq 'right keys)))
      (set! h-x (+ h-x 1.0)))))

(load-all-sprites)

(define game-step 
  (lambda ()
   (check-keys (graphics-keys))
   (draw-scene h-x 100.0 20.0))) 

(game-step)(show 0)

We have a definition for game-step that moves the hero when the arrow keys are pressed.

To test this we place game step into a timer so it gets repeated every 60 ms.

(set-every-function 1000 33 0 
		(lambda ()
		  (game-step)(gc)))

This really appears to do nothing; however it is redrawing the scene every 33 ms.

If you now focus on the image pane the hero ship moves left and right when you use the arrow keys.

You may spot that seems a bit slow.

You can tune that by changing this function; lets make it move by 4 instead of by 1.

(define check-keys
  (lambda (keys)
    (when (and (< (cdr (assq 'recent keys)) 50)
               (cdr (assq 'left keys)))
      (set! h-x (- h-x 4.0)))
    (when (and (< (cdr (assq 'recent keys)) 50)
               (cdr (assq 'right keys)))
      (set! h-x (+ h-x 4.0)))))

This is still not great; we can improve it later.

It seems like it would be cheating to let the player move off the screen; away from any future bombs.

Lets restrict the players movement.

Move the player as far as reasonable to the left; and then look at the h-x variable.

h-x

It looks like it should go no lower than 1.0

The other side is less obvious; move as far as reasonable to the right; and look at h-x again.

I make that 713.0 on the right.

(define check-hx 
 (lambda () 
  (when (< h-x 1.0) (set! h-x 1.0))
  (when (> h-x 713.0) (set! h-x 713.0))))

In the game-step after checking the keys we can check h-x.

Before we draw the scene.

(define game-step 
  (lambda ()
   (check-keys (graphics-keys))
   (check-hx)
   (draw-scene h-x 100.0 20.0))) 

The movement is now checked.

Enemy Ships

Lets animate the enemy ships a little.

These deserve to be animated individually rather than as a row.

So we need to track them; perhaps in a list.

First lets define an enemy by creating a new-enemy.

(define new-enemy 
  (lambda (x y xn yn s)
   (list (list (list x y) (list xn yn) s))))

Where

x y are initial position

xn yn are offsets to move the sprite

s is the sprite bank to plot

A list of enemies is needed; we can start with two

(define enemies '())

(set! enemies 
	(append enemies 
		(new-enemy 10.0 20.0 1.0 0.0 20)))

(set! enemies 
	(append enemies 
		(new-enemy 120.0 20.0 1.0 0.0 20)))

Lets display the list of enemies.

To do that we use map to map the draw-scaled-rotated-sprite function to each enemy in the list.

(define draw-invaders
  (lambda ()
    (map (lambda (e)
           (apply
             draw-scaled-rotated-sprite
             (list (caddr e) (caar e) (cadar e) 0.0 0.65)))
         enemies)))

(define draw-scene 
 (lambda ( hx )
   (clear-scene)
   (draw-invaders)
   (draw-hero hx)))

(define game-step 
  (lambda ()
   (check-keys (graphics-keys))
   (check-hx)
   (draw-scene h-x)))

Changes to draw-scene and game-step are needed as draw-scene no longer needs three arguments.

This should now draw two enemies and the hero.

(game-step)(show 0)

Lets actually move the enemies.

A function can move an enemy by updating the x and y position

(define move-enemy 
 (lambda (e)
	(let* 	([xy (car e)]
	         [xn (cadr e)]
		 [newxy (map (lambda (x y) (+ x y)) xy xn)]
                 [s (caddr e)])
	(list newxy xn s))))

xn and s are not changed

newxy is updated from xy + xn.

As we can move one enemy - we can move them all.

(define move-enemies 
	(lambda () 
	  (set! enemies  
		(map (lambda (e) 
		  (move-enemy e)) enemies))))

(define game-step 
  (lambda ()
   (check-keys (graphics-keys))
   (check-hx)
   (move-enemies)
   (draw-scene h-x)))

Time to recap again; our code now looks like this.

;; example 2 - enemy list

(define h-x 400.0)

(define load-all-sprites 
 (lambda ()
   (load-sprites "images/bg1.jpg" 0)	
   (load-sprites "images/h1.png" 1)	
   (load-sprites "images/D-17.png" 20)))

(define clear-scene 
  (lambda ()
    (clear-image 0.0 0.0 0.0 1.0)
    (draw-sprite 0 0.0 10.0)))
	
	
(define new-enemy 
  (lambda (x y xn yn s)
   (list (list (list x y) (list xn yn) s))))
   
(define enemies '())

(define enemy-wave-one
 (lambda ()

  (for y from 0.0 to -1850.0 step -350.0 

  (set! enemies 
	(append enemies 
		(new-enemy 10.0 y 0.0 0.8 20)))
   (set! enemies 
	(append enemies 
		(new-enemy 110.0 y 0.0 1.0 20))) 
   (set! enemies 
	(append enemies 
		(new-enemy 210.0 y 0.0 0.8 20)))

  (set! enemies 
	(append enemies 
		(new-enemy 400.0 y 0.0 0.8 20)))
   (set! enemies 
	(append enemies 
		(new-enemy 500.0 y 0.0 1.0 20))) 
   (set! enemies 
	(append enemies 
		(new-enemy 600.0 y 0.0 0.8 20)))
 )))
   

(define draw-invaders
  (lambda ()
    (map (lambda (e)
           (apply
             draw-scaled-rotated-sprite
             (list (caddr e) (caar e) (cadar e) 0.0 0.65)))
         enemies)))
		 
		 
(define move-enemy 
 (lambda (e)
	(let* 	([xy (car e)]
	         [xn (cadr e)]
		 [newxy (map (lambda (x y) (+ x y)) xy xn)]
                 [s (caddr e)])
	(list newxy xn s))))
	
(define move-enemies 
	(lambda () 
	  (set! enemies  
		(map (lambda (e) 
		  (move-enemy e)) enemies))))

(define draw-hero 
  (lambda (x) 
    (draw-sprite 1 x 460.0)))

(define check-hx 
 (lambda () 
  (when (< h-x 1.0) (set! h-x 1.0))
  (when (> h-x 713.0) (set! h-x 713.0))))

(define draw-scene 
 (lambda ( hx )
   (clear-scene)
   (draw-invaders)
   (draw-hero hx)))


(define check-keys
  (lambda (keys)
    (when (and (< (cdr (assq 'recent keys)) 50)
               (cdr (assq 'left keys)))
      (set! h-x (- h-x 4.0)))
    (when (and (< (cdr (assq 'recent keys)) 50)
               (cdr (assq 'right keys)))
      (set! h-x (+ h-x 4.0)))))

(load-all-sprites)
(enemy-wave-one)

(define game-step 
  (lambda ()
   (check-keys (graphics-keys))
   (check-hx)
   (move-enemies)
   (draw-scene h-x)))

And can be run with the timer function.

 
(set-every-function 1000 33 0 
		(lambda ()
		  (game-step)(gc)))

A side note on Render mode 2.

The 0 at the end of the every function is the render mode 0-2.

Mode 2 is interesting.

In mode 2; a display list is used; the list is updated using the "add" variants of the graphics commands.

e.g use add-draw-rect instead of draw-rect - these functions take the same parameters.

A command is added to a list; all the commands are run in the order they were added; just after the scheme script returns.

This provides two benefits.

The drawing commands are sent in batches to the graphics card - making the drawing much more efficient.

Since the graphics commands in mode 2 all run after the scheme script has finished; a new scheme script can run while the commands to update the display are being run by the application.

This allows a little more multi-processing.

Clone this wiki locally