Just open up your favorite lisp’s REPL supported by
trivial-gamekit and follow this guide step by step copy-pasting code
snippets right into the REPL. Whole code of this little piece with almost every snippet included
can be found at the end of this guide.
First we need to install
trivial-gamekit system. This is quite easy to accomplish thanks to
Quicklisp. Check out this small tip on how to do that.
Now, when the system is successfully loaded, let’s define a main class that will manage our application:
(gamekit:defgame hello-gamekit () ())
Yes. That’s it. You totally configured an application that will use OpenGL graphics, OpenAL audio and mouse/keyboard input of a host OS. Let’s confirm it works!
Here we go. Canvas for your imagination to go wild onto is now ready!
You can close
trivial-gamekit’s window and release acquired system resources by using your OS
UI or with
trivial-gamekit allows you to configure host window to some degree through
(defvar *canvas-width* 800) (defvar *canvas-height* 600) (gamekit:defgame hello-gamekit () () (:viewport-width *canvas-width*) ; window's width (:viewport-height *canvas-height*) ; window's height (:viewport-title "Hello Gamekit!")) ; window's title
Alrighty, let’s bring a window back to continue our endeavor:
trivial-gamekit manages rendering loop for you. To draw anything on the screen you
just need to override
gamekit:draw generic function:
(defvar *black* (gamekit:vec4 0 0 0 1)) (defvar *origin* (gamekit:vec2 0 0)) (defmethod gamekit:draw ((app hello-gamekit)) ;; Let's draw a black box in the bottom-left corner (gamekit:draw-rect *origin* 100 100 :fill-paint *black*))
We can do a couple of observations from this example. First, unlike many other 2D drawing APIs,
trivial-gamekit uses bottom-left corner as an origin and y-axis pointing upwards for its 2D
coordinate system. Second, colors are represented by 4-element vectors made with
#'gamekit:vec4 with values for each element ranging from 0.0 to 1.0 (red/green/blue/alpha) and
2D coordinates are passed around using 2-element vectors.
While we are at it, it’s worth mentioning that 2D canvas has exactly the size we supplied to the
:viewport-height options. So for this case,
bottom-left corner of our canvas is (0, 0) and top-right corner is (799, 599).
Static black box is anything but exciting. Let’s introduce some motion!
(defvar *current-box-position* (gamekit:vec2 0 0)) (defun real-time-seconds () "Return seconds since certain point of time" (/ (get-internal-real-time) internal-time-units-per-second)) (defun update-position (position time) "Update position vector depending on the time supplied" (let* ((subsecond (nth-value 1 (truncate time))) (angle (* 2 pi subsecond))) (setf (gamekit:x position) (+ 350 (* 100 (cos angle))) (gamekit:y position) (+ 250 (* 100 (sin angle)))))) (defmethod gamekit:draw ((app hello-gamekit)) (update-position *current-box-position* (real-time-seconds)) (gamekit:draw-rect *current-box-position* 100 100 :fill-paint *black*))
Wooosh! Fast it moves!
#'w are used to set (via
#'setf) or get first, second,
third or forth element out of a vector accordingly. Feel free to tinker with
changing how the position of the box is calculated. Just paste an updated function back into the
REPL and you will see changes instantly! How awesome is that?
Quite cool, unlike this state-chaning code in our
gamekit has a special
function to separate game-related logic that needs to be done per-frame from the drawing
#'act. It is, just like
#'draw, called each frame, but in a bit different
environment. All non-rendering per-frame tasks should go there.
Gamekit exports several
draw-* functions that could be useful for 2D drawing. Let’s turn our
moving box into a sinus snake!
(defvar *curve* (make-array 4 :initial-contents (list (gamekit:vec2 300 300) (gamekit:vec2 375 300) (gamekit:vec2 425 300) (gamekit:vec2 500 300)))) (defun update-position (position time) (let* ((subsecond (nth-value 1 (truncate time))) (angle (* 2 pi subsecond))) (setf (gamekit:y position) (+ 300 (* 100 (sin angle)))))) (defmethod gamekit:act ((app hello-gamekit)) (update-position (aref *curve* 1) (real-time-seconds)) (update-position (aref *curve* 2) (+ 0.3 (real-time-seconds))))
(defmethod gamekit:draw ((app hello-gamekit)) (gamekit:draw-curve (aref *curve* 0) (aref *curve* 3) (aref *curve* 1) (aref *curve* 2) *black* :thickness 5.0))
Well, people won’t beleive us it is a snake, so I guess we need to leave a note for them to not confuse it for bezier curve with animated control points!
(defmethod gamekit:draw ((app hello-gamekit)) (gamekit:draw-text "A snake that is!" (gamekit:vec2 300 400)) (gamekit:draw-curve (aref *curve* 0) (aref *curve* 3) (aref *curve* 1) (aref *curve* 2) *black* :thickness 5.0))
#'gamekit:draw-text allows us to put a text onto the screen.
Anyway, quite a cringy snake, but it moves! We couldn’t ask for more.
Obvisously, we could and we do ask for more: where’s all the interactivity much needed in a game of any kind? So let’s put a head of our snake at a place under the cursor each time left mouse button is clicked.
(defvar *cursor-position* (gamekit:vec2 0 0)) (gamekit:bind-cursor (lambda (x y) "Save cursor position" (setf (gamekit:x *cursor-position*) x (gamekit:y *cursor-position*) y))) (gamekit:bind-button :mouse-left :pressed (lambda () "Copy saved cursor position into snake's head position vector" (let ((head-position (aref *curve* 3))) (setf (gamekit:x head-position) (gamekit:x *cursor-position*) (gamekit:y head-position) (gamekit:y *cursor-position*)))))
Just click around in the window to see how that turned out.
Now, for enhanced interactivity, let’s move snake’s head while left button is pressed - dragging it along the way!
(defvar *head-grabbed-p* nil) (gamekit:bind-cursor (lambda (x y) "When left mouse button is pressed, update snake's head position" (when *head-grabbed-p* (let ((head-position (aref *curve* 3))) (setf (gamekit:x head-position) x (gamekit:y head-position) y))))) (gamekit:bind-button :mouse-left :pressed (lambda () (setf *head-grabbed-p* t))) (gamekit:bind-button :mouse-left :released (lambda () (setf *head-grabbed-p* nil)))
As we can see from examples above, we can bind any action to key or mouse clicks using
#'bind-button function. First argument of the function is the button to track. Possible values
:mouse-middle for mouse buttons, and for keys you can
use values such as
:space and many others. Second
argument tells the gamekit which particular button state to assign action to. Available states
:repeating. And finally, last argument is a function without
arguments which will be called when specified key or button reaches supplied state.
To grab cursor movement one can use
#'bind-cursor function. Its only argument represents a
function of two arguments - x and y of mouse cursor position. The latter is called every time
mouse cursor changes its location.
We need a couple of resources prepared to continue with this guide. Download this (right click on the link -> save as) image1 and this sound file2 to
/tmp/hello-gamekit-assets/. Now let’s tell gamekit
where to find those with
(gamekit:register-resource-package :keyword "/tmp/hello-gamekit-assets/")
One can make quite a complex scene with just primitives like rectangles, ellipses, lines,
curves, etc. But for very intricate objects it is still much easier to just display a prepared
trivial-gamekit ready to help you with this too!
First, we need to tell gamekit where it can find our image. We would use
define-image for that:
(gamekit:define-image :snake-head "snake-head.png")
By default, upon initialization or when evaluating at runtime (in REPL), gamekit will load this
image using base path you provided with
#'register-resource-package of your main class
hello-gamekit in this case) merging it with a relative path specified in a second argument of
define-image. If you provide an absolute path, then base path would be ignored. First
argument of that function is used to reference this image in other places
define-image supports only .png images yet.
No need to tell gamekit anything else - your image should have been already loaded!
To put an image onto the screen you can use
#'gamekit:draw-image. It has two arguments. First
is the coords where image origin (its bottom-left corner) will be put. And the second one tells
which image gamekit should use. Let’s improve our
#'draw method with it!
(defmethod gamekit:draw ((app hello-gamekit)) (gamekit:print-text "A snake that is!" 300 400) (gamekit:draw-curve (aref *curve* 0) (aref *curve* 3) (aref *curve* 1) (aref *curve* 2) *black* :thickness 5.0) ;; let's center image position properly first (let ((head-image-position (gamekit:subt (aref *curve* 3) (gamekit:vec2 32 32)))) ;; then draw it where it belongs (gamekit:draw-image head-image-position :snake-head)))
A face appears!
trivial-gamekit can help you with tricking not only player eyes, but ears too! First, let’s
inform the gamekit where it can locate a sound with
define-audio macro. It supports a
couple audio formats including
(gamekit:define-sound :snake-grab "snake-grab.ogg")
For playing a sound
#'gamekit:play function is used. Let’s play this sound when we grabbing
(gamekit:bind-button :mouse-left :pressed (lambda () (gamekit:play :snake-grab) (setf *head-grabbed-p* t)))
Try to grab a snake’s head now. Amazing!
All snippets combined could be found in
repository. Clone it or fork it and start playing
trivial-gamekit any moment!