For reference, I am building a little ECS game engine with:
The full code for the example can be found here.
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:
I'm really scratching my head on this one. I've tried:
go
form, which had no effect.:start-up
stage first, then running the rest of the game loop in a setTimeout...run
fn from init
and 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.
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 async
or promises (or core.async
when 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 loop
will 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 use loop
for this scenario, at all - regardless where you use that loop
, since it will still block the whole main thread, since there is no any other thread.
An example of how it would look:
(defn run-world [state]
(let [next-state (step state)]
(when-not (stop? next-state)
(js/requestAnimationFrame #(run-world next-state)))))
Note that this function also doesn't return anything. But you can make it return either a core.async
channel that gets a value when the looping ends, or a Promise that gets resolved eventually.