A Clojure game engine built using LWJGL (Light Weight Java Game Library).
Clunk is the successor to quip, it's cleaner, (hopefully) faster, and allows you access to OpenGL for when the going gets tough.
It's currently very early in it's development, but it has support for sprites (static images, animated spritesheets and text), audio, scenes and transitions, collision detection, and tweens (inspired by Phaser 3 tweens).
It's still a little rough round the edges, please feel free to raise issues for anything that trips you up.
Create a new game using the Leiningen template:
lein new com.kimbsy/clunk my-game
Take a look at the games in the examples directory, these simple games demonstrate various clunk features.
To make a simple game start with the clunk.core/game
function and run it with clunk.core/start!
:
(ns my-game
;; common aliases
(:require [clunk.collision :as collision]
[clunk.core :as c]
[clunk.delay :as delay]
[clunk.input :as i]
[clunk.palette :as p]
[clunk.sprite :as sprite]
[clunk.tween :as tween]
[clunk.util :as u]
[clunk.scene :as scene]
[clunk.audio :as audio]))
(def game (c/game {:title "Example Clunk Game"
:size [1200 800]
:init-scenes-fn init-scenes ;; @TODO: implement me!
:current-scene :demo
:assets {:image {:captain-spritesheet "resources/img/captain.png"
:heart "resources/img/heart.png"}
:audio {:music "resources/audio/music/music.ogg"}}}))
(defn -main [& args]
(c/start! game))
Define the scenes in your game as a map:
(defn init-scenes
[state]
{:demo {:sprites (demo-sprites state)
:update-fn update-demo
:draw-fn draw-demo!}
:menu {:sprites (menu-sprites state)
:update-fn update-menu
:draw-fn draw-menu!}})
Scene update functions should take the current state and return the new state. Many clunk namespaces have ready-made update functions. The clunk.sprite/update-state
function moves sprites based on their velocity and updates animations, the clunk.collision/update-state
function checks for sprite collisions and applies appropriate on-collide
functions (see section on collisions below), the clunk.delay/update-state
function updates any ongoing delays and exeuted ones which are finished (see secion on delays below) the clunk.tween/update-state
function updates sprite tweens (see section on tweens below).
(defn update-demo
[state]
(-> state
sprite/update-state
collision/update-state
delay/update-state
tween/update-state))
Scene draw functions take the current game state and should draw it to the screen. Often this will just consist of a call to clunk.core/draw-background!
to set the background colour, then a call to clunk.sprite/draw-scene-sprites!
:
(defn draw-demo!
[state]
(c/draw-background! (p/hex->rgba "#3A435E"))
(sprite/draw-scene-sprites! state))
Take a look at the basic-sprite example game.
A sprite has a position, a velocity and a number of other fields for managing collision detection and animation as appropriate. Each built-in sprite function takes at least the :sprite-group
(any keyword) and an [x y]
position vector.
In order to use images for your sprites, you must declare them in the game config :assets
in the :image
map. You can refer to them by the key they are associated with.
The default clunk.sprite/sprite
function returns a minimal sprite which draws itself as a red box. You can give it a new :draw-fn
using keyword args to override this.
The clunk.sprite/geometry-sprite
function returns a sprite which draws a polygon shape specified by a sequence of points ([x y]
vectors) relative to the sprite pos
. These points should be listed in counter-clockwise (CCW) order.
The clunk.sprite/image-sprite
function creates a sprite which draws an image. (This image must be declared as an :asset
in the game config).
The clunk.sprite/animated-sprite
function creates an image sprite which draws sections of a sprite sheet image, configured by its :animations
and :current-animation
keyword args.
The clunk.sprite/text-sprite
function creates a sprite which draws itself as text, with options for choosing the font, size color etc.
(defn demo-sprites
[state]
[(sprite/sprite :player [100 100])
(sprite/geometry-sprite :poly
[350 350]
[[0 0]
[10 0]
[5 10]])
(sprite/image-sprite :health
[200 200]
[32 32]
:heart)
(sprite/animated-sprite :captain
[300 300]
[240 360]
:captain-spritesheet
[1680 1440]
:animations {:none {:frames 1
:y-offset 0
:frame-delay 100}
:idle {:frames 4
:y-offset 1
:frame-delay 15}
:run {:frames 4
:y-offset 2
:frame-delay 8}
:jump {:frames 7
:y-offset 3
:frame-delay 8}}
:current-animation :none)
(sprite/text-sprite :title-text
[200 50]
"Press enter to play!")])
Each sprite has a :debug?
field which, if true
, will draw the collision-detection boundary for the sprite as well as its precise position.
For custom fields each sprite creation function takes an optional :extra
keyword arg where you can pass a map which will get merged into the sprite map.
(sprite/sprite :player
[100 100]
:debug? true
:extra {:health 100
:damage 20})
Take a look at the collision-detection example game.
To detect collisions between sprites in your scene you must do two things.
clunk.collision/update-state
.:colliders
key.(defn update-demo
[state]
(-> state
sprite/update-state
collision/update-state ;; <= this one
delay/update-state
tween/update-state))
(defn init-scenes
[state]
{:demo {:sprites (demo-sprites state)
:colliders (demo-colliders) ;; @TODO: implement me!
:update-fn update-demo
:draw-fn draw-demo!}})x
A collider can be created with the clunk.collision/collider
function. It takes the :sprite-group
of the first kind of sprite (group-a-key), the :sprite-group
of the second kind of sprite (group-b-key), the function to call which modifies A when a collision is detected (collide-fn-a), and the function which modifies B when a collision is detected (collide-fn-b).
The two collide-fn functions take both sprites as arguments, and should return the updated version of their first argument (collide-fn-a return the new sprite a, collide-fn-b returns the new b) the other sprite is pased in for reference only. If a collide-fn function returns nil
then that sprite will be removed from the scene.
(defn demo-colliders
[]
[(collision/collider
:captain
:health
(fn [captain _health]
(-> animated-captain
(update :health + 100)
(sprite/set-animation :jump)))
(fn [health _captain]
nil))])
@TODO: document specifying collision detection functions
Take a look at the tweens example game.
To use tweens in your scene you must add the clunk.tween/update-state
function to your scene update function.
(defn update-demo
[state]
(-> state
sprite/update-state
collision/update-state
delay/update-state
tween/update-state)) ;; <= this one
Tweens are an incredibly flexible tool. They allow you to modify an attribute of a sprite by a some amount, over an optionally defined duration (:step-count
, the number of frames over which the change will occur), following a specific progress curve (:easing-fn
, a large number of built-in easing functions are provided in the clunk.tween
namespace).
This change can then be performed in reverse (by setting :yoyo?
to true
), and the whole process can be repeated any number of times (set :repeat-times
to ##Inf
for continuous looping).
In addition you can specify trigger functions which happen when the tween starts to yoyo (:on-yoyo-fn
), when the whole cycle starts to repeat (:on-repeat-fn
), and when the tween completes (:on-complete-fn
).
For fields which are not a single numeric value (like pos
, vel
, etc.) you can specify an :update-fn
and (if needed) a :yoyo-update-fn
which take the current value of the field, and a delta to modify it by (over the course of the tween the delta add up to the desired change) you can modify the field in whatever way makes sense. For 2d vector fields like pos
and vel
, the functions clunk.tween/tween-<x|y>-fn
and clunk.tween/tween-<x|y>-yoyo-fn
are provided to modify the x
and y
values of these fields.
You can create a tween with clunk.tween/tween
and attach it to a sprite with clunk.tween/add-tween
.
;; create a sprite
(-> (sprite/image-sprite :bouncing-heart
[200 300]
[32 32]
:heart)
(tween/add-tween
;; spin 360, then reverse
(tween/tween :rotation
360
:yoyo? true
:repeat-times ##Inf))
(tween/add-tween
;; move from left to right, then reverse
(tween/tween :pos
100
:update-fn tween/tween-x-fn
:yoyo? true
:yoyo-update-fn tween/tween-x-yoyo-fn
:repeat-times ##Inf))
(tween/add-tween
;; move up and then reverse, describing a parabola because of the `:easing-fn`
(tween/tween :pos
-200
:step-count 50
:easing-fn tween/ease-out-quad
:update-fn tween/tween-y-fn
:yoyo? true
:yoyo-update-fn tween/tween-y-yoyo-fn
:repeat-times ##Inf)))
Take a look at the sounds example game.
To play an audio file (currently *.ogg
files are supported, conversion tools are widely available), you must declare it in the game config :assets
in the :audio
map. This ensures the file is loaded before the game starts and can be played using the key it is associated with.
You can play loaded files with clunk.audio/play!
passing in the reference key you set. You can loop the audio playback by setting the optional :loop?
keyword arg to true
. This function additionally returns a reference to the audio source, you can call clunk.audio/stop!
passing in this source reference to stop it early. This is a necessity when looping audio as they will not stop on their own.
@TODO: add example games demonstrating different kinds of input handling.
There are three ways of handling use input in clunk.
:held-keys
fieldThe game state maintains the set of currently held keys in the :held-keys
field.
(if (contains? (:held-keys state) i/K_ENTER)
(prn "ENTER IS HELD")
(prn "ENTER IS NOT HELD"))
You can make a sprite clickable with the clunk.input/add-on-click
function which takes a sprite and a on-click function, this function takes the state and the sprite and returns an updated state.
(-> (sprite/image-sprite :balloon
[200 200]
[32 32]
:balloon)
(i/add-on-click (fn [state s]
(pop-balloon state s) ;; @TODO: implement me!
state)))
The most flexible option is to define event handlers. Mouse buttons, mouse movement, window resize and keyboard events are supported.
First define an event handler function, this takes the current state, the triggered event, and should return the new state.
(defn mouse-handler-1
[state e]
(audio/play! :laser-gun)
state)
This handler function must be attached to your scene by putting it in the appropriate <event-type>-fns
collection.
(defn init-scenes
[state]
{:demo {:sprites (demo-sprites state)
:update-fn update-demo
:draw-fn draw-demo!
:mouse-button-fns [left-click-fire right-click-reload]
:mouse-movement-fns [move-reticule]
:key-fns [player-movement]
:window-resize-fns [update-world-bounds]}})
You technically only need one handler for each type of event, but having multiple small ones can make control flow simpler. Event handlers are invoked in the order they are listed.
The clunk.input/is
function is a helpful way of pattern matching on the events. Mouse events will have :button
and :action
fields, key events will have :key
and :action
fields.
(defn key-handler-1
[state e]
(if (i/is e :key i/K_SPACE :action i/PRESS)
(update state :some-useful-flag not)
state))
You can create your on events which will be handled in the same way using the clunk.core/enqueue-event!
function. Your event should be a map containing an :event-type
and any data that your handler will need. Your handlers should be registered in the scene under the :<event-type>-fns
key.
;; Anywhere in your game code
(c/enqueue-event! {:event-type :my-event
:time 42
:space 9000})
;; Event handler
(defn handle-custom-events
[state {:keys [time space]}
(modify-foo state time space)])
;; Register event handlers in the scene
(defn init-scenes
[state]
{:demo {:sprites (demo-sprites state)
:update-fn update-demo
:draw-fn draw-demo!
:my-event-fns [handle-custom-events]}})
This can be handy as a bit of an impure escape hatch since your handler (when processed, normally on the next frame) is passed the whole game state and can modify anything it likes.
Generally you should avoid overusing this feature since it leads to hard to debug code.
@TODO: add an example game demonstrating delays
Delays allow you to execute code after a certain amount of time has passed. This is very useful for complex animations, cutscenes, spawning enemies/items etc.
To use delays in your scene you must add the clunk.delay/update-state
function to your scene update function.
(defn update-demo
[state]
(-> state
sprite/update-state
collision/update-state
delay/update-state ;; <= this one
tween/update-state))
Delays can be created with the clunk.delay/delay-fn
function, it takes a duration in milliseconds and a function which takes the current game state (when the delay finishes) and returns the new game state.
(delay/delay-fn 1000 (fn [state] (spawn-enemy state)))
You can add these delays to your scene with the clunk.delay/add-delay-fn
function.
(delay/add-delay-fn
state
(delay/delay-fn ... )))
When a delay duration passes, the function is invoked. Only delays on the current scene are processed, so if we switch scenes any active delays will be paused until we return.
Often it's desirable to add a bunch of delays to the scene at the same time with timings that are relative to each other (very helpful for cutscenes, especially when fine-tuning the timings). You can create a list of delays with the clunk.delay/sequential-delay-fns
function which takes a collection of [duration function]
tuples. These can be added to the scene with the clunk.delay/add-delay-fns
function.
(delay/add-delay-fns
state
(delay/sequential-delay-fns
[[0 (fn [state] (player-enters state))]
[1000 (fn [state] (bad-guy-enters state))]
[100 (fn [state] (epic-fight-music state))]]
:initial-delay 1000))
@TODO: document what fields exist and what they're for.
The clunk/util
and clunk/palette
namespaces provide a number of helper functions for positioning sprites, working with 2d vectors, working with polygon point collections, creating and modifying colours etc.
Can you improve this documentation? These fine people already did:
Kimbsy & Dave KimberEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close