作为参考,我正在构建一个小型 ECS 游戏引擎:
示例的完整代码可以在这里找到。
当我在浏览器中加载应用程序时,我的游戏引擎的主“游戏循环”会阻止 Threejs 使用
requestAnimationFrame
渲染到屏幕, 停止处理按键,直到游戏循环结束,此时两个功能都将停止恢复正常。因此,典型的运行将如下所示:
这个问题我真是摸不着头脑。我试过:
以
go
首先运行
:start-up
不从
run
调用
init
我希望在游戏循环期间按下按键事件后立即处理它们,并且我希望 Threejs 渲染器在每次在游戏循环中调用其函数时进行渲染(发生在
draw-scene!
系统中)在提供的代码中)。任何指点将不胜感激。
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 是单线程但异步的。因此,当运行一段未用
async
或 Promise(或 CLJS 时为
core.async
)分解的代码时,它将阻止 除了 Web Worker 之外的所有其他内容。
你的引擎似乎没有使用网络工作者,很可能它甚至不会从它们中受益,所以一个简单的
loop
将阻止一切,直到它退出。
解决方案是使用
requestAnimationFrame
安排循环迭代,其中每次迭代都会继续安排下一次迭代,直到遇到停止条件。因此,在这种情况下,你根本不能使用 loop
- 无论你在哪里使用 loop
,因为它仍然会阻塞整个主线程,因为没有任何其他线程。
它的外观示例:
(defn run-world [state]
(let [next-state (step state)]
(when-not (stop? next-state)
(js/requestAnimationFrame #(run-world next-state)))))
请注意,此函数也不返回任何内容。但你可以让它返回一个在循环结束时获取值的
core.async
通道,或者最终得到解决的 Promise。