So I've heard the siren song of functional programming (or functional-programming-like-stuff via STL,anyway) before, and wasn't too happy with my results. "Ivory tower academic masturbation!" I declared, and let it be.
Lately, I've heard "functional programming" at least three times in recent memory. Once is happenstance; twice is coincidence; three times is conspiracy. Clearly I should look into this stuff. And - maybe my previous annoyance with it wasn't me. Maybe I was just using the wrong language. So the siren song begins again: you will be so much more productive. You can write an entire game just by hooking up the right algorithms to the right data structures. Come....come....
So, of the many functional programming languages out there, I decide to learn Scheme. I figure, if I'm going to learn this stuff, let's try to find a language that does just about nothing else. Then, once I've learned it, maybe I can apply the techniques in something more game-friendly like Python. And Joel mentioned it. And one of my ex-girlfriends used to take a class from one of its inventors. I don't even know Lisp, mind you.
About a day and a half into it I discovered that the one I was using (MIT Scheme) had a cute little vector graphics library, so I decided to see if I could write Spacewar. By 11 last night I hadn't finished, but I did get a little rotating spaceship to orbit a sun with a broken model for gravity.
So, my thoughts, so far?
It seems that just about every language except C++ lets you define your function objects on the fly, as you're writing the code. With C++ you have to stop, page up, write your function or functor, then go back to your code and reference it. Doing this breaks my "flow" and I find it...unpleasant. Even Java lets you write your functions on the fly, although you usually have to write more object-oriented cruft around your function that the function itself, and that's a nice thing about Scheme. And yes, map, reduce, for-each, and being forced to think recursively, all very nice.
(That's kind of where it ends, though. I remember back when I switched from assembly to C the joy of discovering that a function might actaully work, as written, the Very First Time I Tried It. I didn't have that experience with Scheme. Yes, I'm writing fewer lines of code but they take longer to write. And it seemed like most of my time was spent counting parentheses. If I'd written Spacewar in C++ it would be done by now!)
So...where will I go from here? I'm thinking Python.
For what it's worth, here's my beginnings-of-spacewar in Scheme - with unit tests. I'm sure you lisp/scheme guys will be horrified at how I'm butchering your language.
(define pi (acos -1))
(define-structure ship pos vel rot)
(define check!
(lambda (good?)
(if good? (display ".")
(begin
(display "Test failed!")(newline)))))(define epsilon 0.0001)
(define approx-equal?
(lambda (x y)
(and (> x (- y epsilon)) (< x (+ y epsilon)))))(define approx-equal?-test
(lambda ()
(begin
(check! (approx-equal? 2.0 (* 4 0.5)))
(check! (not (approx-equal? 2.0 4.0)))
(check! (not (approx-equal? 2.0 (+ 2.0 epsilon))))
(check! (approx-equal? -1.0 (+ -1.0 (/ epsilon 2.0)))))))
(define test-list (list approx-equal?-test))(define vector-approx-equal?
(lambda (v1 v2)
(not (memq #f (map approx-equal? (vector->list v1) (vector->list v2))))))(define vector-approx-equal?-test
(lambda ()
(begin
(check! (vector-approx-equal? (vector 0.0 1.0 -2.0) (vector 0.0 (* 2.0 0.5) -2.00009)))
(check! (not (vector-approx-equal? (vector 0.0 1.0) (vector 0.0001 1.0)))))))
(append! test-list (list vector-approx-equal?-test))(define vector2-xform-rot
(lambda (xy rot)
(vector (- (* (vector-ref xy 0) (cos rot))(* (vector-ref xy 1) (sin rot)))
(+ (* (vector-ref xy 0) (sin rot))(* (vector-ref xy 1) (cos rot))))
)
)(define vector2-xform-rot-test
(lambda ()
(begin
(check! (vector-approx-equal? (vector2-xform-rot (vector 0.0 0.0) (/ pi 2)) (vector 0.0 0.0)))
(check! (vector-approx-equal? (vector2-xform-rot (vector 1.0 0.0) (/ pi 2)) (vector 0.0 1.0)))
(check! (vector-approx-equal? (vector2-xform-rot (vector 0.0 1.0) (/ pi 2)) (vector -1.0 0.0))))))
(append! test-list (list vector2-xform-rot-test))(define vector-mul
(lambda (v scalar)
(vector-map (lambda (element) (* element scalar)) v)))(define vector-mul-test
(lambda ()
(check! (vector-approx-equal? (vector-mul (vector 0.0 1.0 1.5) 2.0) (vector 0.0 2.0 3.0)))))
(append! test-list (list vector-mul-test))(define vector-add
(lambda (v1 v2)
(list->vector (map + (vector->list v1) (vector->list v2)))))(define vector-add-test
(lambda ()
(check! (vector-approx-equal? (vector-add (vector 0.0 1.0 1.5) (vector 1.0 0.5 2.5))
(vector 1.0 1.5 4.0)))))
(append! test-list (list vector-add-test))(define square (lambda (k) (* k k)))
; too tired & lazy to check for zerovec - good thing I'm never shipping it
(define vector2-normalize
(lambda (v2)
(let ((factor-thingy (/ 1 (sqrt (+ (square( vector-first v2))
(square (vector-second v2)))))))
(vector-map (lambda (e) (* factor-thingy e)) v2))))(define vector2-normalize-test
(lambda ()
(begin
(check! (vector-approx-equal? (vector2-normalize (vector 0.5 0.0)) (vector 1.0 0.0)))
(check! (vector-approx-equal? (vector2-normalize (vector 0.0 1.1)) (vector 0.0 1.0)))
)
)
)
(append! test-list (list vector2-normalize-test))(define ship-update!
(lambda (my-ship delta-t) ; pos += velocity * delta-t
(let ((accel (vector-mul
(vector2-normalize
(vector-mul (ship-pos my-ship) -0.1))
0.1)))
(set-ship-vel! my-ship (vector-add (ship-vel my-ship) (vector-mul accel delta-t)))
(set-ship-pos! my-ship (vector-add (ship-pos my-ship)
(vector-mul (ship-vel my-ship) delta-t)
)
)
)
)
)(define ship-update!-test
(lambda ()
(let ((my-ship (make-ship (vector 0.1 0.0) (vector 0.1 0.0) (vector 0.0 0.0))))
(ship-update! my-ship 0.1)
(check! (vector-approx-equal? (ship-pos my-ship) (vector .11 .10))))))
;(append! test-list (list ship-update!-test))(define shape-xform
(lambda (shape pos rot)
(map (lambda (v2) (vector-add pos (vector2-xform-rot v2 rot))) shape)
)
)(define shape-xform-test
(lambda ()
(let ((result (shape-xform
(list (vector 0 0) (vector 1 0) (vector 0 1))
(vector 1 0)
(/ pi 2))))
(check! (vector-approx-equal? (first result) (vector 1.0 0.0)))
(check! (vector-approx-equal? (second result) (vector 1.0 1.0)))
(check! (vector-approx-equal? (third result) (vector 0.0 0.0)))
)
)
)
(append! test-list (list shape-xform-test))(define graphics-draw-shape
(lambda (gd shape)
(begin
(graphics-move-cursor gd (vector-first (first shape))
(vector-second (first shape)))
(for-each
(lambda (xy) (graphics-drag-cursor gd (vector-first xy)
(vector-second xy)))
shape))))(define ship-shape ; no pun intended. seriously
(list (vector 0.05 0.0)
(vector -0.05 0.025)
(vector -0.05 -0.025)
(vector 0.05 0.0)))(define ship-draw
(lambda (my-ship gd)
(graphics-draw-shape gd
(shape-xform ship-shape (ship-pos my-ship) (ship-rot my-ship)))))(define turn
(lambda (gd my-ship i)
(begin
(display "turn ")(display i)(newline)
(ship-update! my-ship 0.1)
(ship-draw my-ship gd))))(define delay
(lambda (msecs)
(let delay-until ((stoptime (+ (real-time-clock) (internal-time/seconds->ticks msecs))))
(if( > (real-time-clock) stoptime ) ()
(delay-until stoptime)))))(define delay-test
(lambda ()
(with-timings
(lambda () (delay 1.0))
(lambda (run-time gc-time real-time)
(begin
(check! (<= 1.0 (internal-time/ticks->seconds real-time)))
(check! ( > 2.0 (internal-time/ticks->seconds real-time)))
)
)
)
)
)
(append! test-list (list delay-test))(define game
(lambda (numturns)
(begin
(let ((gd (make-graphics-device))
(my-ship (make-ship (vector -0.5 0.5) (vector 0.1 0.0) 0)))
(let mainloop ((i 0))
(if (>= i numturns) #f
(begin
(graphics-clear gd)
(turn gd my-ship i)
(set-ship-rot! my-ship (+ 0.01 (ship-rot my-ship)))
(delay 0.01)
(mainloop (+ i 1))
)
)
)
(graphics-close gd)
)
)
)
)(define test-suite
(lambda ()
(for-each
(lambda (foo)
(foo))
test-list)))
(test-suite)
Recent Comments