summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Thompson <dthompson2@worcester.edu>2018-08-26 09:52:37 -0400
committerDavid Thompson <dthompson2@worcester.edu>2018-08-26 09:52:47 -0400
commit3a259726918364985b141b9a15493353bbbcb495 (patch)
tree20e737a3fff39f028ad261726ee73ac099078995
parent78f66fa3ae49181ceb0574d57eac63bd603398d9 (diff)
Add 2D node primitives.
-rw-r--r--starling/node-2d.scm406
1 files changed, 406 insertions, 0 deletions
diff --git a/starling/node-2d.scm b/starling/node-2d.scm
new file mode 100644
index 0000000..293b227
--- /dev/null
+++ b/starling/node-2d.scm
@@ -0,0 +1,406 @@
+;;; Starling Game Engine
+;;; Copyright © 2018 David Thompson <davet@gnu.org>
+;;;
+;;; This program 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.
+;;;
+;;; This program 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 Starling. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; 2D game nodes.
+;;
+;;; Code:
+
+(define-module (starling node-2d)
+ #:use-module (chickadee math matrix)
+ #:use-module (chickadee math rect)
+ #:use-module (chickadee math vector)
+ #:use-module (chickadee render)
+ #:use-module (chickadee render color)
+ #:use-module (chickadee render font)
+ #:use-module (chickadee render framebuffer)
+ #:use-module (chickadee render shapes)
+ #:use-module (chickadee render sprite)
+ #:use-module (chickadee render texture)
+ #:use-module (chickadee render viewport)
+ #:use-module (chickadee scripting)
+ #:use-module (oop goops)
+ #:use-module (starling node)
+ #:export (<camera-2d>
+ target
+ offset
+ position
+ width
+ height
+
+ <view-2d>
+ camera
+ area
+
+ <canvas>
+ views
+
+ <node-2d>
+ origin
+ position
+ rotation
+ skew
+ pivot
+ move
+ move-to
+ teleport
+ rotate
+ rotate-to
+ zoom
+ zoom-to
+
+ <sprite>
+ texture
+
+ <animated-sprite>
+ atlas
+ animations
+ frame-duration
+ current-animation
+ start-time
+ change-animation
+
+ <filled-rect>
+ region
+ color
+
+ <label>
+ font
+ text))
+
+
+;;;
+;;; 2D Camera
+;;;
+
+;; Cameras define a view into the scene. They are attached to a 2D
+;; node and follow it around.
+
+(define-class <camera-2d> ()
+ (target #:accessor target #:init-form #f #:init-keyword #:target)
+ (offset #:getter offset #:init-form #v(0.0 0.0) #:init-keyword #:offset)
+ (position #:getter position #:init-form #v(0.0 0.0))
+ (last-position #:getter last-position #:init-form #v(0.0 0.0))
+ (width #:getter width #:init-keyword #:width)
+ (height #:getter height #:init-keyword #:height)
+ (projection-matrix #:accessor projection-matrix)
+ ;; Combined projection/view matrix
+ (view-matrix #:getter view-matrix #:init-form (make-identity-matrix4))
+ (framebuffer #:accessor framebuffer))
+
+(define-method (initialize (camera <camera-2d>) initargs)
+ (next-method)
+ ;; Initialize framebuffer and orthographic projection matrix based
+ ;; on the resolution of the camera.
+ (set! (framebuffer camera)
+ (make-framebuffer (width camera)
+ (height camera)
+ #:min-filter 'nearest
+ #:mag-filter 'nearest))
+ (set! (projection-matrix camera)
+ (orthographic-projection 0 (width camera) (height camera) 0 0 1)))
+
+;; This method can be overridden by subclasses to create custom camera
+;; movement.
+(define-method (follow-target (camera <camera-2d>) dt)
+ (let ((pos (position camera))
+ (target-pos (position (target camera)))
+ (offset (offset camera)))
+ (set-vec2-x! pos (- (vec2-x offset) (vec2-x target-pos)))
+ (set-vec2-y! pos (- (vec2-y offset) (vec2-y target-pos)))))
+
+(define-method (update (camera <camera-2d>) dt)
+ (when (target camera)
+ (let ((pos (position camera))
+ (last-pos (last-position camera))
+ (m (view-matrix camera)))
+ (vec2-copy! pos last-pos)
+ (follow-target camera dt)
+ (unless (and (= (vec2-x pos) (vec2-x last-pos))
+ (= (vec2-y pos) (vec2-y last-pos)))
+ (matrix4-translate! m pos)
+ (matrix4-mult! m m (projection-matrix camera))))))
+
+(define-syntax-rule (with-camera camera body ...)
+ (with-framebuffer (framebuffer camera)
+ (with-projection (if (target camera)
+ (view-matrix camera)
+ (projection-matrix camera))
+ body ...)))
+
+
+
+;;;
+;;; 2D View
+;;;
+
+;; Views render the output of a camera to a portion of the game
+;; window.
+
+(define-class <view-2d> ()
+ (camera #:accessor camera #:init-keyword #:camera)
+ (area #:getter area #:init-keyword #:area)
+ (viewport #:accessor viewport))
+
+(define-method (initialize (view <view-2d>) initargs)
+ (define (to-int x)
+ (inexact->exact (round x)))
+ (next-method)
+ (let ((r (area view)))
+ (set! (viewport view)
+ (make-viewport (to-int (rect-x r))
+ (to-int (rect-y r))
+ (to-int (rect-width r))
+ (to-int (rect-height r))))))
+
+(define %identity-matrix (make-identity-matrix4))
+
+(define-method (render (view <view-2d>))
+ (with-viewport (viewport view)
+ (with-projection (projection-matrix (camera view))
+ (draw-sprite* (framebuffer-texture (framebuffer (camera view)))
+ (area view)
+ %identity-matrix))))
+
+
+;;;
+;;; 2D Canvas
+;;;
+
+;; The canvas is the root of a 2D scene. It handles rendering one or
+;; more views.
+
+(define-class <canvas> (<node>)
+ (views #:accessor views #:init-form '() #:init-keyword #:views))
+
+(define-method (update (canvas <canvas>) dt)
+ (for-each (lambda (view)
+ (update (camera view) dt))
+ (views canvas)))
+
+(define-method (render* (canvas <canvas>) alpha)
+ ;; Draw scene from the viewpoint of each camera.
+ (for-each (lambda (view)
+ (with-camera (camera view)
+ (for-each (lambda (child)
+ (render* child alpha))
+ (children canvas))))
+ (views canvas))
+ (render canvas alpha))
+
+(define-method (render (canvas <canvas>) alpha)
+ (for-each render (views canvas)))
+
+
+;;;
+;;; 2D Game Node
+;;;
+
+(define-class <node-2d> (<node>)
+ (origin #:getter origin #:init-form #v(0.0 0.0) #:init-keyword #:origin)
+ (position #:getter position #:init-form #v(0.0 0.0) #:init-keyword #:position)
+ (rotation #:accessor rotation #:init-form 0.0 #:init-keyword #:rotation)
+ (scale #:getter scale #:init-form #v(1.0 1.0) #:init-keyword #:scale)
+ (skew #:getter skew #:init-form #v(0.0 0.0) #:init-keyword #:skew)
+ ;; Some extra position vectors for defeating "temporal aliasing"
+ ;; when rendering.
+ (last-position #:getter last-position #:init-form #v(0.0 0.0))
+ (render-position #:getter render-position #:init-form #v(0.0 0.0))
+ ;; Lazily computed transformation matrices.
+ (local-matrix #:getter local-matrix #:init-form (make-identity-matrix4))
+ (world-matrix #:getter world-matrix #:init-form (make-identity-matrix4))
+ (dirty-matrix? #:accessor dirty-matrix? #:init-form #t))
+
+(define (dirty! node)
+ (set! (dirty-matrix? node) #t))
+
+(define-method (pivot (node <node-2d>) x y)
+ "Change origin of NODE to (X, Y)."
+ (let ((o (origin node)))
+ (set-vec2-x! o x)
+ (set-vec2-y! o y)
+ (dirty! node)))
+
+(define-method (move (node <node-2d>) dx dy)
+ "Move NODE by (DX, DY)."
+ (let ((p (position node)))
+ (set-vec2-x! p (+ (vec2-x p) dx))
+ (set-vec2-y! p (+ (vec2-y p) dy))
+ (dirty! node)))
+
+(define-method (move-to (node <node-2d>) x y)
+ "Move NODE to (X, Y)."
+ (let ((p (position node)))
+ (set-vec2-x! p x)
+ (set-vec2-y! p y)
+ (dirty! node)))
+
+(define-method (teleport (node <node-2d>) x y)
+ "Move NODE to (X, Y) without applying animation smoothing."
+ (move-to node x y)
+ (let ((lp (last-position node)))
+ (set-vec2-x! lp x)
+ (set-vec2-y! lp y)))
+
+(define-method (rotate (node <node-2d>) dtheta)
+ "Rotate NODE about the Z axis by DTHETA radians."
+ (set! (rotation node) (+ (rotation node) dtheta))
+ (dirty! node))
+
+(define-method (rotate-to (node <node-2d>) theta)
+ "Rotate NODE to THETA radians about the Z axis."
+ (set! (rotation node) theta)
+ (dirty! node))
+
+(define-method (zoom (node <node-2d>) dsx dsy)
+ "Scale NODE by the scaling factor (DSX, DSY)."
+ (let ((s (scale node)))
+ (set-vec2-x! s (+ (vec2-x s) dsx))
+ (set-vec2-y! s (+ (vec2-y s) dsy))
+ (dirty! node)))
+
+(define-method (zoom (node <node-2d>) ds)
+ "Scale NODE by the scaling factor (DS, DS)."
+ (zoom node ds ds))
+
+(define-method (zoom-to (node <node-2d>) sx sy)
+ "Set the scaling factor for NODE to (SX, SY)."
+ (let ((s (scale node)))
+ (set-vec2-x! s sx)
+ (set-vec2-y! s sy)
+ (dirty! node)))
+
+(define-method (zoom-to (node <node-2d>) s)
+ "Set the scaling factor for node to (S, S)."
+ (zoom-to node s s))
+
+(define-method (compute-matrices! (node <node-2d>))
+ (let ((local (local-matrix node))
+ (world (world-matrix node)))
+ (matrix4-2d-transform! local
+ #:origin (origin node)
+ #:position (render-position node)
+ #:rotation (rotation node)
+ #:scale (scale node)
+ #:skew (skew node))
+ ;; Compute world matrix by multiplying by the parent node's
+ ;; matrix, if there is a 2D parent node, that is.
+ (if (and (parent node) (is-a? (parent node) <node-2d>))
+ (matrix4-mult! world local (world-matrix (parent node)))
+ (begin
+ (matrix4-identity! world)
+ (matrix4-mult! world world local)))))
+
+(define-method (update* (node <node-2d>) dt)
+ (vec2-copy! (position node) (last-position node))
+ (next-method))
+
+(define-method (render* (node <node-2d>) alpha)
+ ;; Compute the linearly interpolated rendering position, in the case
+ ;; that node has moved since the last update.
+ (let ((p (position node))
+ (lp (last-position node))
+ (rp (render-position node))
+ (beta (- 1.0 alpha)))
+ (unless (and (= (vec2-x lp) (vec2-x rp))
+ (= (vec2-y lp) (vec2-y rp)))
+ (set-vec2-x! rp (+ (* (vec2-x p) alpha) (* (vec2-x lp) beta)))
+ (set-vec2-y! rp (+ (* (vec2-y p) alpha) (* (vec2-y lp) beta)))
+ (set! (dirty-matrix? node) #t)))
+ ;; Recompute dirty matrices.
+ (when (dirty-matrix? node)
+ (compute-matrices! node)
+ (set! (dirty-matrix? node) #f)
+ ;; If the parent is dirty, all the children need to be marked as
+ ;; dirty, too.
+ (for-each (lambda (node)
+ (set! (dirty-matrix? node) #t))
+ (children node)))
+ (next-method))
+
+(define-method (activate (node <node-2d>))
+ (set! (dirty-matrix? node) #t))
+
+;; TODO: Add live asset reload
+(define-method (asset-ref x) x)
+
+
+;;;
+;;; Static Sprite
+;;;
+
+(define-class <sprite> (<node-2d>)
+ (texture #:accessor texture #:init-keyword #:texture))
+
+(define-method (render (sprite <sprite>) alpha)
+ (draw-sprite* (asset-ref (texture sprite))
+ (texture-gl-rect (texture sprite))
+ (world-matrix sprite)))
+
+
+;;;
+;;; Animated Sprite
+;;;
+
+(define-class <animated-sprite> (<sprite>)
+ (atlas #:accessor atlas #:init-keyword #:atlas)
+ (animations #:accessor animations #:init-keyword #:animations)
+ (frame-duration #:accessor frame-duration #:init-keyword #:frame-duration)
+ (current-animation #:accessor current-animation
+ #:init-keyword #:current-animation)
+ (start-time #:accessor start-time #:init-form 0))
+
+(define-method (on-enter (sprite <animated-sprite>))
+ (update sprite 0))
+
+(define-method (update (sprite <animated-sprite>) dt)
+ (let* ((anim (assq-ref (animations sprite) (current-animation sprite)))
+ (frame-duration (frame-duration sprite))
+ (anim-duration (* frame-duration (vector-length anim)))
+ (time (modulo (- (agenda-time) (start-time sprite)) anim-duration))
+ (frame (vector-ref anim (floor (/ time frame-duration))))
+ (texture-region (texture-atlas-ref (asset-ref (atlas sprite)) frame)))
+ (set! (texture sprite) texture-region)
+ (next-method)))
+
+(define-method (change-animation (sprite <animated-sprite>) name)
+ (set! (current-animation sprite) name)
+ (set! (start-time sprite) (with-agenda (agenda sprite) (agenda-time))))
+
+
+;;;
+;;; Filled Rectangle
+;;;
+
+(define-class <filled-rect> (<node-2d>)
+ (region #:accessor region #:init-keyword #:region)
+ (color #:accessor color #:init-form black #:init-keyword #:color))
+
+(define-method (render (r <filled-rect>) alpha)
+ (draw-filled-rect (region r) (color r) #:matrix (world-matrix r)))
+
+
+;;;
+;;; Text
+;;;
+
+(define-class <label> (<node-2d>)
+ (font #:accessor font #:init-keyword #:font)
+ (text #:accessor text #:init-form "" #:init-keyword #:text))
+
+(define-method (render (label <label>) alpha)
+ (draw-text* (asset-ref (font label)) (text label) (world-matrix label)))