From: Dave Watson
Subject: Imperative vs. 'lispier' style game programming
Date: 
Message-ID: <8d2fc94f.0407162049.625ffb50@posting.google.com>
Over the past few days I've been working on a simple 'invaders' clone,
but as I started programming in C and not a functional language, I
think it is coming out too imperative.   I am simply asking for
pointers on how those with more experience than I think I could make
such a game use a 'lispier' style.

Most of the work is done in the lambda function returned from
make-game-update-fn, but it has lots of 'let' variables and calls lots
of functions that use setf, and lots of classes are used...the way one
would program in c or java, not lisp :(

The following are the main parts that I feel are too imperative in
their style:
(full program can be found here:
http://students.washington.edu/djwatson/invaders.tar.bz)

invaders.lisp
---------

;;Game structures

(defclass img ()
  ((surface :accessor img-surface :initarg :surface :initform nil
:allocation :class)
   (x :accessor img-x :initarg :x)
   (y :accessor img-y :initarg :y)
   (health :accessor img-health :initarg :health)
   (width :accessor img-width :initform *invader-size* :allocation
:class)
   (height :accessor img-height :initform *invader-size* :allocation
:class)))

;cut rest of classes

;;Normal functions

(defun run-sdl-event-loop (update-fn key-fn unkey-fn)
  "the main sdl loop: pass in functions to make it do things"
  (sdl:event-loop
   (:key-down (scan-code key mod unicode)
              (funcall key-fn key))
   (:key-up (scan-code key mod unicode)
            (funcall unkey-fn key))
   (:quit ()
          (return))
   (:resize (width height)
            (when *print-debug-info*
              (format t "Resized width = ~A height = ~A~%" width
height)))
   (:idle ()
          (funcall update-fn))))

(defmacro make-fn (fn-name make-args make-lets fn-args doc &body body)
  "creates a function-returning-function with name fn-name,
args to creator function make-args, let about lambda make-lets,
and args to created funciton fn-args, doc for creator as doc"
  `(defun ,fn-name ,make-args
     ,doc
     (let ,make-lets
       #'(lambda ,fn-args
           ,@body))))

(make-fn make-game-update-fn (surface)
         ((prev-time (get-universal-time))
          (prev-ticks (get-internal-real-time))
          (invaders (init-invaders))
          (fighter (make-instance 'fighter))
          (shots nil)
          (alien-shots nil)
          (blockades (init-blockades))) nil
         "game update function creator"
         (let* ((cur-time (get-universal-time))
                (change-time (- cur-time prev-time))
                (cur-ticks (get-internal-real-time))
                (change-ticks (- cur-ticks prev-ticks)))
           (setf prev-time cur-time
                 *change-time* change-time
                 prev-ticks cur-ticks
                 *change-ticks* change-ticks
                 *move-amount* (/ *change-ticks*
internal-time-units-per-second)
                 *background* *background-scene*) ;FIXME: temporary
until we get a help screen going
           (when (>= 0 (img-health fighter))
             (throw 'game-over 'lost))
           (when (= 0 (length invaders))
             (throw 'game-over 'playing))
           (when *background*
             (sdl:blit-surface *background* sgum:+null-pointer+
surface sgum:+null-pointer+)
             (move-invaders invaders)
             (decf (fighter-shot-time-left fighter) *move-amount*)
             (setf shots (detect-hit-shots (append blockades invaders
alien-shots)
                                           (move-shots (append
(check-fighter-keys fighter) shots))))
             (setf (fighter-shots-left fighter) (- *max-shots* (length
shots)))
             (setf alien-shots (detect-hit-shots (append (list
fighter) blockades)
                                                 (move-shots (append
(check-shoot invaders) alien-shots))))
             (setf blockades (detect-hit-shots invaders blockades))
             (setf invaders (la-rec invaders (> (img-health cur) 0)))
             (blit-imgs (concatenate 'list alien-shots shots blockades
(list fighter) invaders) surface)
             (sdl:flip surface)
             (update-frame-count))))

(defun fire-fighter-shot (fighter)
  "fires a shot from the fighter (returns the new shot)"
  (when (and (> (fighter-shots-left fighter) 0) (<=
(fighter-shot-time-left fighter) 0))
    (setf (fighter-shot-time-left fighter) *time-between-shots*)
    (decf (fighter-shots-left fighter) 2)
    (mix:play-channel 0 (ship-shot-sound fighter) 0)
    (list
     (make-instance 'shot :x (+ (img-x fighter) (img-width fighter))
:y (img-y fighter))
     (make-instance 'shot :x (img-x fighter) :y (img-y fighter)))))

(defun move-fighter (fighter direction)
  "moves the fighter in the specified direction (1 for right, -1 for
left)"
  (let ((new-pos (+ (img-x fighter) (* direction
*fighter-move-speed*))))
    (when (and (> new-pos 0) (< new-pos (- *x-resolution* (img-width
fighter))))
      (setf (img-x fighter) new-pos))))

(let ((invader-direction 1)
      (down-direction 0)
      (down-count 0))
  (defun move-invaders (invaders)
    "move the invaders"
    (let ((move-amount (/ (* 60 *current-level* *move-amount*) (1+
(log (length invaders))))))
      (if (= 0 down-direction)
          (dolist (cur invaders)        ;moving in the sideways
direction
            (incf (img-x cur) (* invader-direction move-amount))
            (when (and (< (- (img-x cur) *current-level*) 0) (= -1
invader-direction))
              (setf down-direction 1))
            (when (and (> (+ (img-x cur) *current-level*) (-
*x-resolution* (img-width cur)))
                       (= 1 invader-direction))
              (setf down-direction -1)))
        (progn                          ;moving in the down direction
          (incf down-count move-amount)
          (when (> down-count (/ *invader-size* 2))
            (setf invader-direction down-direction
                  down-direction 0
                  down-count 0))
          (dolist (cur invaders)
            (incf (img-y cur) move-amount)
            (when (> (+ (img-y cur) *invader-size*) *y-resolution*)
              (throw 'game-over 'lost)))))
      invaders)))

(defun check-shoot (invaders)
  "checks if the invaders should shoot"
  (let ((shots nil) (shoot-amount (floor (* (/ (/ *num-invaders*
*move-amount*) 10)
					    (1+ (log (length invaders)))))))
    (dolist (cur invaders)
      (when (= 0 (random shoot-amount))
        (setf shots (cons (make-instance 'alien-shot
                                         :x (+ (/ (img-width cur) 2)
(img-x cur))
                                         :y (+ (img-height cur) (img-y
cur)))
                          shots))))
    shots))

(defun move-shots (shots)
  "moves the shots"
  (dolist (cur shots)
    (incf (img-y cur) (* *move-amount* (shot-dy cur))))
          shots)

(defun detect-hit-shots (ships shots)
  "checks if there were any hits between ships and shots"
  (dolist (shot shots)
    (dolist (ship ships)
      (when (collision ship shot)
        (decf (img-health ship))
        (decf (img-health shot)))))
  (la-rec shots (and (> (img-health cur) 0) (on-screenp cur))))

;;Main function
(defun invaders () 
  "starts a new game of lisp invaders"
  (unwind-protect
      (let ((surface (init-sdl))
            (state 'playing))
        (awhile state
                (setf state (catch 'game-over
                              (cond
                               ((eq 'playing it) (progn (incf
*current-level*)
                                                       
(run-sdl-event-loop
                                                        
(make-game-update-fn surface)
                                                        
(make-game-key-fn surface)
                                                        
(make-game-unkey-fn))))
                               ((eq 'lost it) (throw 'game-over nil))
                               (t (throw 'game-over nil)))))))
    (deinit-sdl)))

From: Kenny Tilton
Subject: Re: Imperative vs. 'lispier' style game programming
Date: 
Message-ID: <Jc9Kc.29201$oW6.5757205@twister.nyc.rr.com>
Dave Watson wrote:

> Over the past few days I've been working on a simple 'invaders' clone,
> but as I started programming in C and not a functional language, I
> think it is coming out too imperative.

"too many notes, my dear Mozart"? :)

    I am simply asking for
> pointers on how those with more experience than I think I could make
> such a game use a 'lispier' style.

Pointer: don't worry about Lispy style, per se. Does the game not work? 
Was it hard to debug? Is it slow? Is it hard to maintain/extend? In 
short, are you actually having some problem with the program? All I hear 
is a concern that some imagined aesthetic has been violated.

And if there is some actual problem, then I wager there will be a 
specific small chunk of code to ask about improving. That makes it 
easier to respond vs "here is the whole program".

> 
> Most of the work is done in the lambda function returned from
> make-game-update-fn, but it has lots of 'let' variables and calls lots
> of functions that use setf, and lots of classes are used...the way one
> would program in c or java, not lisp :(

No, Lisp welcomes all paradigms.

> 
> The following are the main parts that I feel are too imperative in
> their style:
> (full program can be found here:
> http://students.washington.edu/djwatson/invaders.tar.bz)
> 
> invaders.lisp
> ---------
> 
> ;;Game structures
> 
> (defclass img ()
>   ((surface :accessor img-surface :initarg :surface :initform nil
> :allocation :class)
>    (x :accessor img-x :initarg :x)
>    (y :accessor img-y :initarg :y)
>    (health :accessor img-health :initarg :health)
>    (width :accessor img-width :initform *invader-size* :allocation
> :class)
>    (height :accessor img-height :initform *invader-size* :allocation
> :class)))
> 
> ;cut rest of classes
> 
> ;;Normal functions
> 
> (defun run-sdl-event-loop (update-fn key-fn unkey-fn)
>   "the main sdl loop: pass in functions to make it do things"
>   (sdl:event-loop
>    (:key-down (scan-code key mod unicode)
>               (funcall key-fn key))
>    (:key-up (scan-code key mod unicode)
>             (funcall unkey-fn key))
>    (:quit ()
>           (return))
>    (:resize (width height)
>             (when *print-debug-info*
>               (format t "Resized width = ~A height = ~A~%" width
> height)))
>    (:idle ()
>           (funcall update-fn))))
> 
> (defmacro make-fn (fn-name make-args make-lets fn-args doc &body body)
>   "creates a function-returning-function with name fn-name,
> args to creator function make-args, let about lambda make-lets,
> and args to created funciton fn-args, doc for creator as doc"
>   `(defun ,fn-name ,make-args
>      ,doc
>      (let ,make-lets
>        #'(lambda ,fn-args
>            ,@body))))
> 
> (make-fn make-game-update-fn (surface)
>          ((prev-time (get-universal-time))
>           (prev-ticks (get-internal-real-time))
>           (invaders (init-invaders))
>           (fighter (make-instance 'fighter))
>           (shots nil)
>           (alien-shots nil)
>           (blockades (init-blockades))) nil
>          "game update function creator"
>          (let* ((cur-time (get-universal-time))
>                 (change-time (- cur-time prev-time))
>                 (cur-ticks (get-internal-real-time))
>                 (change-ticks (- cur-ticks prev-ticks)))
>            (setf prev-time cur-time
>                  *change-time* change-time
>                  prev-ticks cur-ticks
>                  *change-ticks* change-ticks
>                  *move-amount* (/ *change-ticks*
> internal-time-units-per-second)
>                  *background* *background-scene*) ;FIXME: temporary
> until we get a help screen going
>            (when (>= 0 (img-health fighter))
>              (throw 'game-over 'lost))
>            (when (= 0 (length invaders))
>              (throw 'game-over 'playing))
>            (when *background*
>              (sdl:blit-surface *background* sgum:+null-pointer+
> surface sgum:+null-pointer+)
>              (move-invaders invaders)
>              (decf (fighter-shot-time-left fighter) *move-amount*)
>              (setf shots (detect-hit-shots (append blockades invaders
> alien-shots)
>                                            (move-shots (append
> (check-fighter-keys fighter) shots))))
>              (setf (fighter-shots-left fighter) (- *max-shots* (length
> shots)))
>              (setf alien-shots (detect-hit-shots (append (list
> fighter) blockades)
>                                                  (move-shots (append
> (check-shoot invaders) alien-shots))))
>              (setf blockades (detect-hit-shots invaders blockades))
>              (setf invaders (la-rec invaders (> (img-health cur) 0)))
>              (blit-imgs (concatenate 'list alien-shots shots blockades
> (list fighter) invaders) surface)
>              (sdl:flip surface)
>              (update-frame-count))))
> 
> (defun fire-fighter-shot (fighter)
>   "fires a shot from the fighter (returns the new shot)"
>   (when (and (> (fighter-shots-left fighter) 0) (<=
> (fighter-shot-time-left fighter) 0))
>     (setf (fighter-shot-time-left fighter) *time-between-shots*)
>     (decf (fighter-shots-left fighter) 2)
>     (mix:play-channel 0 (ship-shot-sound fighter) 0)
>     (list
>      (make-instance 'shot :x (+ (img-x fighter) (img-width fighter))
> :y (img-y fighter))
>      (make-instance 'shot :x (img-x fighter) :y (img-y fighter)))))

One thing you might do is add a fighter slot to shot (or make fighter 
into a global if I understand things right) and then let an after method 
on initialize-instance take care of:

 >     (setf (fighter-shot-time-left fighter) *time-between-shots*)
 >     (decf (fighter-shots-left fighter) 2)
 >     (mix:play-channel 0 (ship-shot-sound fighter) 0)

And a style quibble is that the test for shots/time-left should be 
outside fire-fighter-shot, or the name should be fire-if-possible.


> 
> (defun move-fighter (fighter direction)
>   "moves the fighter in the specified direction (1 for right, -1 for
> left)"
>   (let ((new-pos (+ (img-x fighter) (* direction
> *fighter-move-speed*))))
>     (when (and (> new-pos 0) (< new-pos (- *x-resolution* (img-width
> fighter))))
>       (setf (img-x fighter) new-pos))))
> 
> (let ((invader-direction 1)
>       (down-direction 0)
>       (down-count 0))
>   (defun move-invaders (invaders)
>     "move the invaders"
>     (let ((move-amount (/ (* 60 *current-level* *move-amount*) (1+
> (log (length invaders))))))
>       (if (= 0 down-direction)
>           (dolist (cur invaders)        ;moving in the sideways
> direction
>             (incf (img-x cur) (* invader-direction move-amount))
>             (when (and (< (- (img-x cur) *current-level*) 0) (= -1
> invader-direction))
>               (setf down-direction 1))
>             (when (and (> (+ (img-x cur) *current-level*) (-
> *x-resolution* (img-width cur)))
>                        (= 1 invader-direction))
>               (setf down-direction -1)))
>         (progn                          ;moving in the down direction
>           (incf down-count move-amount)
>           (when (> down-count (/ *invader-size* 2))
>             (setf invader-direction down-direction
>                   down-direction 0
>                   down-count 0))
>           (dolist (cur invaders)
>             (incf (img-y cur) move-amount)
>             (when (> (+ (img-y cur) *invader-size*) *y-resolution*)
>               (throw 'game-over 'lost)))))
>       invaders)))
> 
> (defun check-shoot (invaders)
>   "checks if the invaders should shoot"
>   (let ((shots nil) (shoot-amount (floor (* (/ (/ *num-invaders*
> *move-amount*) 10)
> 					    (1+ (log (length invaders)))))))
>     (dolist (cur invaders)
>       (when (= 0 (random shoot-amount))
>         (setf shots (cons (make-instance 'alien-shot
>                                          :x (+ (/ (img-width cur) 2)
> (img-x cur))
>                                          :y (+ (img-height cur) (img-y
> cur)))
>                           shots))))
>     shots))

(defun check-shoot (invaders)
    (loop with shoot-amount = (floor <etc>)
       for invader in invaders
       when (zerop (random shoot-amount))
       collect (make-instance 'alien-shot ....))

Or where you call the above:

    (let ((shoot-amount <etc))
        (mapcan (lambda (invader)
                   (when (zerop...)
                        (list (make-instance 'alien-shot...))) invaders))

I guess this is a fair case of a "lispier" approach.

> 
> (defun move-shots (shots)
>   "moves the shots"
>   (dolist (cur shots)
>     (incf (img-y cur) (* *move-amount* (shot-dy cur))))
>           shots)
> 
> (defun detect-hit-shots (ships shots)
>   "checks if there were any hits between ships and shots"
>   (dolist (shot shots)
>     (dolist (ship ships)
>       (when (collision ship shot)
>         (decf (img-health ship))
>         (decf (img-health shot)))))
>   (la-rec shots (and (> (img-health cur) 0) (on-screenp cur))))

With my Cells package you could have:

   (defmodel shot ()
      ((img-health <etc>
          :initform (c-drifter (<initial-value>)
                       (when (time-stepped-p *wolrd*)
                         (loop for ship in (ships *world*)
                               when (collision ship self)
                               summing -1))))

And similarly for the ship instance. Is this Lispier? It is declarative 
anyway, which is nice. It lets me break up code like yours into so many 
small rules for the state maintained from time step to step. So the 
update function just becomes:

     (incf (time-step *world*))

or:

     (setf (current-time *world*) (get-internal-real-time))

or something.


> 
> ;;Main function
> (defun invaders () 
>   "starts a new game of lisp invaders"
>   (unwind-protect
>       (let ((surface (init-sdl))
>             (state 'playing))
>         (awhile state
>                 (setf state (catch 'game-over
>                               (cond
>                                ((eq 'playing it) (progn (incf
> *current-level*)
>                                                        
> (run-sdl-event-loop
>                                                         
> (make-game-update-fn surface)
>                                                         
> (make-game-key-fn surface)
>                                                         
> (make-game-unkey-fn))))
>                                ((eq 'lost it) (throw 'game-over nil))
>                                (t (throw 'game-over nil)))))))
>     (deinit-sdl)))

kt

-- 
Cells? Cello? Celtik?: http://www.common-lisp.net/project/cells/
Why Lisp? http://alu.cliki.net/RtL%20Highlight%20Film
From: Jens Axel Søgaard
Subject: Re: Imperative vs. 'lispier' style game programming
Date: 
Message-ID: <40f9095c$0$305$edfadb0f@dread11.news.tele.dk>
Dave Watson wrote:
> Over the past few days I've been working on a simple 'invaders' clone,
> but as I started programming in C and not a functional language, I
> think it is coming out too imperative.   I am simply asking for
> pointers on how those with more experience than I think I could make
> such a game use a 'lispier' style.

Felleisen has additional material for HTDP on his webpage. Among
them are a chapter on interactive games. In section 2 he describes
a simple UFO game inspired by invaders.

<http://www.ccs.neu.edu/home/matthias/HtDP/Extended/igames.html>

The note explains how to structure the program in a functional
style.


Incidently the UFO game appears in

<http://www.ccs.neu.edu/home/matthias/Presentations/ecoop2004.pdf>

where the object oriented and the functional approach is compared.

-- 
Jens Axel S�gaard
From: Dave Watson
Subject: Re: Imperative vs. 'lispier' style game programming
Date: 
Message-ID: <8d2fc94f.0407171557.7411b4e5@posting.google.com>
> Felleisen has additional material for HTDP on his webpage. Among
> them are a chapter on interactive games. In section 2 he describes
> a simple UFO game inspired by invaders.
> 
> <http://www.ccs.neu.edu/home/matthias/HtDP/Extended/igames.html>
> 
> The note explains how to structure the program in a functional
> style.
> 

This is exactly what I was looking for, thank you!

-Dave