summaryrefslogtreecommitdiff
path: root/chickadee/game-loop.scm
blob: 8dceb24ab61da7de35d90f9a93f79eab5ac54036 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
;;; Chickadee Game Toolkit
;;; Copyright © 2016, 2018 David Thompson <dthompson2@worcester.edu>
;;;
;;; 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
            current-timestep))


;;;
;;; Error handling
;;;

(define (call-with-error-handling handler thunk)
  "Call THUNK and respond to any exceptions with HANDLER.  Return #t if
an error was handled."
  (if handler
      (let ((stack #f))
        (define (pre-unwind-handler . args)
          (set! stack (make-stack #t 4)))
        (define (throw-handler)
          (with-throw-handler #t thunk pre-unwind-handler)
          #f)
        (define (exception-handler e)
          (handler e stack)
          #t)
        (with-exception-handler exception-handler throw-handler #:unwind? #t))
      (begin
        (thunk)
        #f)))

(define-syntax-rule (with-error-handling handler body ...)
  (call-with-error-handling handler (lambda () body ...)))


;;;
;;; 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 current-timestep (make-parameter 0.0))

(define* (run-game* #:key init update render time error
                    (update-hz 60))
  (let ((timestep (/ 1.0 update-hz)))
    (parameterize ((current-timestep timestep))
      (call-with-prompt game-loop-prompt-tag
        (lambda ()
          ;; Catch SIGINT and kill the loop.
          (sigaction SIGINT
            (lambda (signum)
              (abort-game)))
          (init)
          ;; 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.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.0)
                        (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))
                            (usleep 1))
                          (loop (time) 0.0)
                          (loop current-time buffer))))))))
        (lambda (cont callback)
          #f)))))