summaryrefslogtreecommitdiff
path: root/chickadee/game-loop.scm
diff options
context:
space:
mode:
Diffstat (limited to 'chickadee/game-loop.scm')
-rw-r--r--chickadee/game-loop.scm98
1 files changed, 98 insertions, 0 deletions
diff --git a/chickadee/game-loop.scm b/chickadee/game-loop.scm
new file mode 100644
index 0000000..09f8a7b
--- /dev/null
+++ b/chickadee/game-loop.scm
@@ -0,0 +1,98 @@
+;;; Chickadee Game Toolkit
+;;; Copyright © 2016, 2018 David Thompson <davet@gnu.org>
+;;;
+;;; Chickadee is free software: you can redistribute it and/or modify
+;;; it under the terms of the GNU General Public License as published
+;;; by the Free Software Foundation, either version 3 of the License,
+;;; or (at your option) any later version.
+;;;
+;;; Chickadee is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;;; General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with this program. If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+(define-module (chickadee game-loop)
+ #:export (run-game*
+ abort-game))
+
+
+;;;
+;;; Error handling
+;;;
+
+(define (call-with-error-handling handler thunk)
+ (if handler
+ (let ((stack #f))
+ (catch #t
+ (lambda ()
+ (thunk)
+ #f)
+ (lambda (key . args)
+ (handler stack key args)
+ #t)
+ (lambda (key . args)
+ (set! stack (make-stack #t 3)))))
+ (begin
+ (thunk)
+ #f)))
+
+(define-syntax-rule (with-error-handling handler body ...)
+ (call-with-error-handling handler (lambda () body ...)))
+
+(define (default-error-handler stack key args)
+ (apply throw key args))
+
+
+;;;
+;;; Game loop kernel
+;;;
+
+(define game-loop-prompt-tag (make-prompt-tag 'game-loop))
+
+(define (abort-game)
+ (abort-to-prompt game-loop-prompt-tag #f))
+
+(define* (run-game* #:key update render time error
+ (update-hz 60))
+ (let ((timestep (round (/ 1000 update-hz))))
+ (call-with-prompt game-loop-prompt-tag
+ (lambda ()
+ ;; Catch SIGINT and kill the loop.
+ (sigaction SIGINT
+ (lambda (signum)
+ (abort-game)))
+ ;; A simple analogy is that we are filling up a bucket
+ ;; with water. When the bucket fills up to a marked
+ ;; line, we dump it out. Our water is time, and each
+ ;; time we dump the bucket we update the game. Updating
+ ;; the game on a fixed timestep like this yields a
+ ;; stable simulation.
+ (let loop ((previous-time (time))
+ (buffer 0))
+ (let* ((current-time (time))
+ (delta (- current-time previous-time)))
+ (let update-loop ((buffer (+ buffer delta)))
+ (if (>= buffer timestep)
+ ;; Short-circuit the update loop if an error
+ ;; occurred, and reset the current time to now in
+ ;; order to discard the undefined amount of time
+ ;; that was spent handling the error.
+ (if (with-error-handling error (update timestep))
+ (loop (time) 0)
+ (begin
+ (usleep 1)
+ (update-loop (- buffer timestep))))
+ (begin
+ ;; We render upon every iteration of the loop, and
+ ;; thus rendering is decoupled from updating.
+ ;; It's possible to render multiple times before
+ ;; an update is performed.
+ (if (with-error-handling error (render (/ buffer timestep)))
+ (loop (time) 0)
+ (loop current-time buffer))))))))
+ (lambda (cont callback)
+ #f))))