Background
For reference, I am building a little ECS game engine with:
- clojure
- threejs as one of the graphics targets
- shadow-cljs for builds
The full code for the example can be found here.
Problem
When I load the application in the browser, my game engine's main "game loop" blocks threejs from rendering to the screen with requestAnimationFrame, and stops keypresses from being handled until the game loop ends, upon which the functionality for both returns to normal.
So a typical run will look like:
- Refresh browser window
- I can see main game loop playing out in the console
- During the main game loop, nothing displays on the screen and no keypresses are logged.
- After the game finishes, the keypress events all "catch up", and are logged at once.
- Finally, the threejs renderer renders.
I'm really scratching my head on this one. I've tried:
- Running the game within a
goform, which had no effect. - Running the
:start-upstage first, then running the rest of the game loop in a setTimeout... - Not calling the
runfn frominitand trying to specify it should be run after load from the config file.
I would expect the key-events to be handled as soon as I press them during the game loop, and I would expect for the threejs renderer to render each time its function is called in the game loop (happens in the draw-scene! system in the supplied code).
Any pointers would be much appreciated.
Code
src/app.cljs
(ns app
(:require ["three" :as three]
[snake :refer [add-snake-plugin]]
[chaos.plugins.core :refer [add-core-plugins]]
[chaos.plugins.timer :as timer]
[chaos.engine.world :as chaos :refer [create-world
defsys
add-system
add-system-dependency
add-stage-dependency]]))
(defsys setup-threejs {}
(let [w (.-innerWidth js/window)
h (.-innerHeight js/window)
aspect (/ w h)
camera (three/PerspectiveCamera. 75 aspect 0.1 1000)
renderer (three/WebGLRenderer.)]
;; Setup renderer and dom elements.
(.setSize renderer w h)
(.. js/document -body (appendChild (.-domElement renderer)))
;; Move camera back
(set! (.. camera -position -z) 5)
(println "Cam Z:" (.. camera -position -z))
(println renderer)
[[:add [:resources :camera] camera]
[:add [:resources :scene] (three/Scene.)]
[:add [:resources :renderer] renderer]]))
(defsys add-cube {:resources [:scene]}
(let [scene (:scene resources)
geometry (three/BoxGeometry. 1 1 1)
material (three/MeshBasicMaterial. #js {:color 0x00ff00})
cube (three/Mesh. geometry material)]
(.add scene cube)
[]))
(defsys draw-scene! {:resources [:renderer :scene :camera]
:events :tick}
(println "Drawing...")
(let [{:keys [:renderer :scene :camera]} resources
render-scene #(.render renderer scene camera)]
(.. js/window (requestAnimationFrame render-scene))
[]))
(defsys capture-key-down {}
(let [raw (atom [])
add-event (fn [event]
(println "Keydown event!")
(swap! raw conj event))]
(.addEventListener js/window "keydown" add-event)
[[:add [:resources :key-down-events] raw]]))
(defsys handle-key-down {:resources [:key-down-events]}
(println "KEYS" (:key-down-events resources)))
;; ... Pruned some irrelevant systems ...
(defn ^:dev/after-load run []
(-> (create-world)
add-core-plugins ;; Main engine plugins (removing has no effect)
add-snake-plugin ;; The snake game library from another example (as above)
(add-system :start-up setup-threejs)
(add-system :start-up add-cube)
(add-system-dependency add-cube setup-threejs)
(add-system :render draw-scene!)
;; Set of irrelevant systems which essentially just exit the game after 5 seconds.
(add-system :start-up add-exit-timer)
(add-system :pre-step pass-time)
(add-system exit-after-5)
;; Gets key events from window
(add-system :start-up capture-key-down)
(add-stage-dependency :render :update)
chaos/play))
;; shadow-cljs entry point
(defn init []
(println "Refresh.")
(run))
shadow-cljs.edn
;; shadow-cljs configuration
{:deps true
:dev-http {8080 "public"}
:builds
{:app {:target :browser
:modules {:main {:init-fn app/init}}}}}
JavaScript in browsers in single-threaded but async. So when running a piece of code that's not broken up with
asyncor promises (orcore.asyncwhen it comes to CLJS), it will block everything else apart from web workers.Your engine doesn't seem to be using web workers, and chances are it wouldn't even benefit from them, so a plain
loopwill block everything until it exits.A solution to that would be to schedule loop iterations with
requestAnimationFrame, where each iteration keeps on scheduling the next one until it encounters a stop condition. So you can't useloopfor this scenario, at all - regardless where you use thatloop, since it will still block the whole main thread, since there is no any other thread.An example of how it would look:
Note that this function also doesn't return anything. But you can make it return either a
core.asyncchannel that gets a value when the looping ends, or a Promise that gets resolved eventually.