rlm@25
|
1 #+title: A World for the Creatures
|
rlm@23
|
2 #+author: Robert McIntyre
|
rlm@23
|
3 #+email: rlm@mit.edu
|
rlm@25
|
4 #+description: Creating a Virtual World for AI constructs using clojure and JME3
|
rlm@25
|
5 #+keywords: JME3, clojure, virtual world, exception handling
|
rlm@23
|
6 #+SETUPFILE: ../../aurellem/org/setup.org
|
rlm@23
|
7 #+INCLUDE: ../../aurellem/org/level-0.org
|
rlm@23
|
8 #+babel: :mkdirp yes :noweb yes :exports both
|
rlm@23
|
9
|
rlm@25
|
10 * The World
|
rlm@23
|
11
|
rlm@25
|
12 There's no point in having senses if there's nothing to experience. In
|
rlm@25
|
13 this section I make some tools with which to build virtual worlds for
|
rlm@25
|
14 my characters to inhabit. If you look at the tutorials at [[http://www.jmonkeyengine.org/wiki/doku.php/jme3:beginner][the jme3
|
rlm@25
|
15 website]], you will see a pattern in how virtual worlds are normally
|
rlm@25
|
16 built. I call this "the Java way" of making worlds.
|
rlm@23
|
17
|
rlm@25
|
18 - The Java way:
|
rlm@25
|
19 - Create a class that extends =SimpleApplication= or =Application=
|
rlm@25
|
20 - Implement setup functions that create all the scene objects using
|
rlm@25
|
21 the inherited =assetManager= and call them by overriding the
|
rlm@25
|
22 =simpleInitApp= method.
|
rlm@25
|
23 - Create =ActionListeners= and add them to the =inputManager=
|
rlm@25
|
24 inherited from =Application= to handle key-bindings.
|
rlm@25
|
25 - Override =simpleUpdate= to implement game logic.
|
rlm@25
|
26 - Running/Testing an Application involves creating a new JVM,
|
rlm@25
|
27 running the App, and then closing everything down.
|
rlm@23
|
28
|
rlm@23
|
29
|
rlm@25
|
30 - A more Clojureish way:
|
rlm@25
|
31 - Use a map from keys->functions to specify key-bindings.
|
rlm@25
|
32 - Use functions to create objects separately from any particular
|
rlm@25
|
33 application.
|
rlm@25
|
34 - Use an REPL -- this means that there's only ever one JVM, and
|
rlm@25
|
35 Applications come and go.
|
rlm@25
|
36
|
rlm@25
|
37 Since most development work using jMonkeyEngine is done in Java, jme3
|
rlm@25
|
38 supports "the Java way" quite well out of the box. To work "the
|
rlm@25
|
39 clojure way", it necessary to wrap the jme3 elements that deal with
|
rlm@25
|
40 the Application life-cycle with a REPL driven interface.
|
rlm@25
|
41
|
rlm@25
|
42 The most important modifications are:
|
rlm@25
|
43
|
rlm@25
|
44 - Separation of Object life-cycles with the Application life-cycle.
|
rlm@25
|
45 - Functional interface to the underlying =Application= and
|
rlm@25
|
46 =SimpleApplication= classes.
|
rlm@25
|
47
|
rlm@25
|
48 ** Header
|
rlm@25
|
49 #+srcname: header
|
rlm@25
|
50 #+begin_src clojure :results silent
|
rlm@25
|
51 (ns cortex.world
|
rlm@25
|
52 "World Creation, abstracion over jme3's input system, and REPL
|
rlm@25
|
53 driven exception handling"
|
rlm@25
|
54 {:author "Robert McIntyre"}
|
rlm@25
|
55
|
rlm@25
|
56 (:use (clojure.contrib (def :only (defvar))))
|
rlm@25
|
57 (:use [pokemon [lpsolve :only [constant-map]]])
|
rlm@25
|
58 (:use [clojure.contrib [str-utils :only [re-gsub]]])
|
rlm@25
|
59
|
rlm@25
|
60 (:import com.jme3.math.Vector3f)
|
rlm@25
|
61 (:import com.jme3.scene.Node)
|
rlm@25
|
62 (:import com.jme3.system.AppSettings)
|
rlm@25
|
63 (:import com.jme3.system.JmeSystem)
|
rlm@25
|
64 (:import com.jme3.system.IsoTimer)
|
rlm@25
|
65 (:import com.jme3.input.KeyInput)
|
rlm@25
|
66 (:import com.jme3.input.controls.KeyTrigger)
|
rlm@25
|
67 (:import com.jme3.input.controls.MouseButtonTrigger)
|
rlm@25
|
68 (:import com.jme3.input.InputManager)
|
rlm@25
|
69 (:import com.jme3.bullet.BulletAppState)
|
rlm@25
|
70 (:import com.jme3.shadow.BasicShadowRenderer)
|
rlm@25
|
71 (:import com.jme3.app.SimpleApplication)
|
rlm@25
|
72 (:import com.jme3.input.controls.ActionListener)
|
rlm@25
|
73 (:import com.jme3.renderer.queue.RenderQueue$ShadowMode)
|
rlm@25
|
74 (:import org.lwjgl.input.Mouse))
|
rlm@25
|
75 #+end_src
|
rlm@25
|
76
|
rlm@25
|
77 ** General Settings
|
rlm@25
|
78 #+srcname: settings
|
rlm@25
|
79 #+begin_src clojure
|
rlm@25
|
80 (in-ns 'cortex.world)
|
rlm@23
|
81
|
rlm@23
|
82 (defvar *app-settings*
|
rlm@23
|
83 (doto (AppSettings. true)
|
rlm@23
|
84 (.setFullscreen false)
|
rlm@23
|
85 (.setTitle "Aurellem.")
|
rlm@23
|
86 ;; disable 32 bit stuff for now
|
rlm@23
|
87 ;;(.setAudioRenderer "Send")
|
rlm@23
|
88 )
|
rlm@23
|
89 "These settings control how the game is displayed on the screen for
|
rlm@23
|
90 debugging purposes. Use binding forms to change this if desired.
|
rlm@25
|
91 Full-screen mode does not work on some computers.")
|
rlm@23
|
92
|
rlm@23
|
93 (defn asset-manager
|
rlm@23
|
94 "returns a new, configured assetManager" []
|
rlm@23
|
95 (JmeSystem/newAssetManager
|
rlm@23
|
96 (.getResource
|
rlm@23
|
97 (.getContextClassLoader (Thread/currentThread))
|
rlm@23
|
98 "com/jme3/asset/Desktop.cfg")))
|
rlm@25
|
99 #+end_src
|
rlm@25
|
100
|
rlm@25
|
101 Normally, people just use the =AssetManager= inherited from
|
rlm@25
|
102 =Application= whenever they extend that class. However,
|
rlm@25
|
103 =AssetManagers= are useful on their own to create objects/ materials,
|
rlm@25
|
104 independent from any particular application. =(asset-manager)= makes
|
rlm@25
|
105 object creation less tightly bound to Application initialization.
|
rlm@25
|
106
|
rlm@25
|
107
|
rlm@25
|
108 ** Exception Protection
|
rlm@25
|
109 #+srcname: exceptions
|
rlm@25
|
110 #+begin_src clojure
|
rlm@25
|
111 (in-ns 'cortex.world)
|
rlm@23
|
112
|
rlm@23
|
113 (defmacro no-exceptions
|
rlm@23
|
114 "Sweet relief like I never knew."
|
rlm@23
|
115 [& forms]
|
rlm@23
|
116 `(try ~@forms (catch Exception e# (.printStackTrace e#))))
|
rlm@23
|
117
|
rlm@25
|
118 (defn thread-exception-removal
|
rlm@25
|
119 "Exceptions thrown in the graphics rendering thread generally cause
|
rlm@25
|
120 the entire REPL to crash! It is good to suppress them while trying
|
rlm@25
|
121 things out to shorten the debug loop."
|
rlm@25
|
122 []
|
rlm@23
|
123 (.setUncaughtExceptionHandler
|
rlm@23
|
124 (Thread/currentThread)
|
rlm@23
|
125 (proxy [Thread$UncaughtExceptionHandler] []
|
rlm@23
|
126 (uncaughtException
|
rlm@25
|
127 [thread thrown]
|
rlm@25
|
128 (println "uncaught-exception thrown in " thread)
|
rlm@25
|
129 (println (.getMessage thrown))))))
|
rlm@23
|
130
|
rlm@25
|
131 #+end_src
|
rlm@23
|
132
|
rlm@25
|
133 Exceptions thrown in the LWJGL render thread, if not caught, will
|
rlm@25
|
134 destroy the entire JVM process including the REPL and slow development
|
rlm@25
|
135 to a crawl. It is better to try to continue on in the face of
|
rlm@25
|
136 exceptions and keep the REPL alive as long as possible. Normally it
|
rlm@25
|
137 is possible to just exit the faulty Application, fix the bug,
|
rlm@25
|
138 reevaluate the appropriate forms, and be on your way, without
|
rlm@25
|
139 restarting the JVM.
|
rlm@23
|
140
|
rlm@25
|
141 ** Input
|
rlm@25
|
142 #+srcname: input
|
rlm@25
|
143 #+begin_src clojure
|
rlm@25
|
144 (in-ns 'cortex.world)
|
rlm@23
|
145
|
rlm@23
|
146 (defn all-keys
|
rlm@25
|
147 "Uses reflection to generate a map of string names to jme3 trigger
|
rlm@25
|
148 objects, which govern input from the keyboard and mouse"
|
rlm@23
|
149 []
|
rlm@25
|
150 (let [inputs (pokemon.lpsolve/constant-map KeyInput)]
|
rlm@23
|
151 (assoc
|
rlm@23
|
152 (zipmap (map (fn [field]
|
rlm@23
|
153 (.toLowerCase (re-gsub #"_" "-" field))) (vals inputs))
|
rlm@23
|
154 (map (fn [val] (KeyTrigger. val)) (keys inputs)))
|
rlm@23
|
155 ;;explicitly add mouse controls
|
rlm@23
|
156 "mouse-left" (MouseButtonTrigger. 0)
|
rlm@23
|
157 "mouse-middle" (MouseButtonTrigger. 2)
|
rlm@23
|
158 "mouse-right" (MouseButtonTrigger. 1))))
|
rlm@23
|
159
|
rlm@23
|
160 (defn initialize-inputs
|
rlm@25
|
161 "Establish key-bindings for a particular virtual world."
|
rlm@23
|
162 [game input-manager key-map]
|
rlm@25
|
163 (doall
|
rlm@25
|
164 (map (fn [[name trigger]]
|
rlm@25
|
165 (.addMapping
|
rlm@25
|
166 ^InputManager input-manager
|
rlm@25
|
167 name (into-array (class trigger)
|
rlm@25
|
168 [trigger]))) key-map))
|
rlm@25
|
169 (doall
|
rlm@25
|
170 (map (fn [name]
|
rlm@25
|
171 (.addListener
|
rlm@25
|
172 ^InputManager input-manager game
|
rlm@25
|
173 (into-array String [name]))) (keys key-map))))
|
rlm@23
|
174
|
rlm@23
|
175 #+end_src
|
rlm@23
|
176
|
rlm@25
|
177 These functions are for controlling the world through the keyboard and
|
rlm@25
|
178 mouse.
|
rlm@23
|
179
|
rlm@25
|
180 I reuse =constant-map= from [[../../pokemon-types/html/lpsolve.html#sec-3-3-4][=pokemon.lpsolve=]] to get the numerical
|
rlm@23
|
181 values for all the keys defined in the =KeyInput= class. The
|
rlm@23
|
182 documentation for =constant-map= is:
|
rlm@23
|
183
|
rlm@23
|
184 #+begin_src clojure :results output
|
rlm@23
|
185 (doc pokemon.lpsolve/constant-map)
|
rlm@23
|
186 #+end_src
|
rlm@23
|
187
|
rlm@23
|
188 #+results:
|
rlm@23
|
189 : -------------------------
|
rlm@23
|
190 : pokemon.lpsolve/constant-map
|
rlm@23
|
191 : ([class])
|
rlm@23
|
192 : Takes a class and creates a map of the static constant integer
|
rlm@23
|
193 : fields with their names. This helps with C wrappers where they have
|
rlm@23
|
194 : just defined a bunch of integer constants instead of enums
|
rlm@23
|
195
|
rlm@25
|
196 =(all-keys)= converts the constant names like =KEY_J= to the more
|
rlm@25
|
197 clojure-like =key-j=, and returns a map from these keys to
|
rlm@25
|
198 jMonkeyEngine =KeyTrigger= objects, which jMonkeyEngine3 uses as it's
|
rlm@25
|
199 abstraction over the physical keys. =all-keys= also adds the three
|
rlm@25
|
200 mouse button controls to the map.
|
rlm@23
|
201
|
rlm@23
|
202
|
rlm@25
|
203 #+begin_src clojure :exports both :results output
|
rlm@25
|
204 (require 'clojure.contrib.pprint)
|
rlm@25
|
205 (clojure.contrib.pprint/pprint
|
rlm@25
|
206 (take 10 (keys (cortex.world/all-keys))))
|
rlm@25
|
207 #+end_src
|
rlm@25
|
208
|
rlm@25
|
209 #+results:
|
rlm@25
|
210 #+begin_example
|
rlm@25
|
211 ("key-n"
|
rlm@25
|
212 "key-apps"
|
rlm@25
|
213 "key-pgup"
|
rlm@25
|
214 "key-f8"
|
rlm@25
|
215 "key-o"
|
rlm@25
|
216 "key-at"
|
rlm@25
|
217 "key-f9"
|
rlm@25
|
218 "key-0"
|
rlm@25
|
219 "key-p"
|
rlm@25
|
220 "key-subtract")
|
rlm@25
|
221 #+end_example
|
rlm@25
|
222
|
rlm@25
|
223 #+begin_src clojure :exports both :results output
|
rlm@25
|
224 (clojure.contrib.pprint/pprint
|
rlm@25
|
225 (take 10 (vals (cortex.world/all-keys))))
|
rlm@25
|
226 #+end_src
|
rlm@25
|
227
|
rlm@25
|
228 #+results:
|
rlm@25
|
229 #+begin_example
|
rlm@25
|
230 (#<KeyTrigger com.jme3.input.controls.KeyTrigger@6ec21e52>
|
rlm@25
|
231 #<KeyTrigger com.jme3.input.controls.KeyTrigger@a54d24d>
|
rlm@25
|
232 #<KeyTrigger com.jme3.input.controls.KeyTrigger@1ba5e91b>
|
rlm@25
|
233 #<KeyTrigger com.jme3.input.controls.KeyTrigger@296af9cb>
|
rlm@25
|
234 #<KeyTrigger com.jme3.input.controls.KeyTrigger@2e3593ab>
|
rlm@25
|
235 #<KeyTrigger com.jme3.input.controls.KeyTrigger@3f71d740>
|
rlm@25
|
236 #<KeyTrigger com.jme3.input.controls.KeyTrigger@4aeacb4a>
|
rlm@25
|
237 #<KeyTrigger com.jme3.input.controls.KeyTrigger@7cc88db2>
|
rlm@25
|
238 #<KeyTrigger com.jme3.input.controls.KeyTrigger@52cee11e>
|
rlm@25
|
239 #<KeyTrigger com.jme3.input.controls.KeyTrigger@c1da30b>)
|
rlm@25
|
240 #+end_example
|
rlm@25
|
241
|
rlm@25
|
242
|
rlm@25
|
243
|
rlm@25
|
244 ** World Creation
|
rlm@23
|
245 #+srcname: world
|
rlm@23
|
246 #+begin_src clojure :results silent
|
rlm@23
|
247 (in-ns 'cortex.world)
|
rlm@23
|
248
|
rlm@25
|
249 (defn no-op
|
rlm@25
|
250 "Takes any number of arguments and does nothing."
|
rlm@25
|
251 [& _])
|
rlm@25
|
252
|
rlm@23
|
253 (defn traverse
|
rlm@23
|
254 "apply f to every non-node, deeply"
|
rlm@23
|
255 [f node]
|
rlm@23
|
256 (if (isa? (class node) Node)
|
rlm@23
|
257 (dorun (map (partial traverse f) (.getChildren node)))
|
rlm@23
|
258 (f node)))
|
rlm@23
|
259
|
rlm@25
|
260 (defn world
|
rlm@25
|
261 "the =world= function takes care of the details of initializing a
|
rlm@25
|
262 SimpleApplication.
|
rlm@23
|
263
|
rlm@25
|
264 ***** Arguments:
|
rlm@25
|
265
|
rlm@25
|
266 - root-node : a com.jme3.scene.Node object which contains all of
|
rlm@25
|
267 the objects that should be in the simulation.
|
rlm@25
|
268
|
rlm@25
|
269 - key-map : a map from strings describing keys to functions that
|
rlm@25
|
270 should be executed whenever that key is pressed.
|
rlm@25
|
271 the functions should take a SimpleApplication object and a
|
rlm@25
|
272 boolean value. The SimpleApplication is the current simulation
|
rlm@25
|
273 that is running, and the boolean is true if the key is being
|
rlm@25
|
274 pressed, and false if it is being released. As an example,
|
rlm@25
|
275
|
rlm@25
|
276 {\"key-j\" (fn [game value] (if value (println \"key j pressed\")))}
|
rlm@25
|
277
|
rlm@25
|
278 is a valid key-map which will cause the simulation to print a
|
rlm@25
|
279 message whenever the 'j' key on the keyboard is pressed.
|
rlm@25
|
280
|
rlm@25
|
281 - setup-fn : a function that takes a SimpleApplication object. It
|
rlm@25
|
282 is called once when initializing the simulation. Use it to
|
rlm@25
|
283 create things like lights, change the gravity, initialize debug
|
rlm@25
|
284 nodes, etc.
|
rlm@25
|
285
|
rlm@25
|
286 - update-fn : this function takes a SimpleApplication object and a
|
rlm@25
|
287 float and is called every frame of the simulation. The float
|
rlm@25
|
288 tells how many seconds is has been since the last frame was
|
rlm@25
|
289 rendered, according to whatever clock jme is currently
|
rlm@25
|
290 using. The default is to use IsoTimer which will result in this
|
rlm@25
|
291 value always being the same.
|
rlm@25
|
292 "
|
rlm@23
|
293 [root-node key-map setup-fn update-fn]
|
rlm@23
|
294 (let [physics-manager (BulletAppState.)
|
rlm@25
|
295 shadow-renderer (BasicShadowRenderer.
|
rlm@25
|
296 (asset-manager) (int 256))]
|
rlm@25
|
297 (doto
|
rlm@25
|
298 (proxy [SimpleApplication ActionListener] []
|
rlm@25
|
299 (simpleInitApp
|
rlm@25
|
300 []
|
rlm@25
|
301 (no-exceptions
|
rlm@25
|
302 ;; allow AI entities as much time as they need to think.
|
rlm@25
|
303 (.setTimer this (IsoTimer. 60))
|
rlm@25
|
304 (.setFrustumFar (.getCamera this) 300)
|
rlm@25
|
305 ;; Create default key-map.
|
rlm@25
|
306 (initialize-inputs this (.getInputManager this) (all-keys))
|
rlm@25
|
307 ;; Don't take control of the mouse
|
rlm@25
|
308 (org.lwjgl.input.Mouse/setGrabbed false)
|
rlm@25
|
309 ;; add all objects to the world
|
rlm@25
|
310 (.attachChild (.getRootNode this) root-node)
|
rlm@25
|
311 ;; enable physics
|
rlm@25
|
312 ;; add a physics manager
|
rlm@25
|
313 (.attach (.getStateManager this) physics-manager)
|
rlm@25
|
314 (.setGravity (.getPhysicsSpace physics-manager)
|
rlm@25
|
315 (Vector3f. 0 -9.81 0))
|
rlm@25
|
316 ;; go through every object and add it to the physics
|
rlm@25
|
317 ;; manager if relevant.
|
rlm@25
|
318 (traverse (fn [geom]
|
rlm@25
|
319 (dorun
|
rlm@25
|
320 (for [n (range (.getNumControls geom))]
|
rlm@25
|
321 (do
|
rlm@25
|
322 (.add (.getPhysicsSpace physics-manager)
|
rlm@25
|
323 (.getControl geom n))))))
|
rlm@25
|
324 (.getRootNode this))
|
rlm@25
|
325 ;;(.addAll (.getPhysicsSpace physics-manager) (.getRootNode this))
|
rlm@25
|
326
|
rlm@25
|
327 ;; set some basic defaults for the shadow renderer.
|
rlm@25
|
328 ;; these can be undone in the setup function
|
rlm@25
|
329 (.setDirection shadow-renderer
|
rlm@25
|
330 (.normalizeLocal (Vector3f. -1 -1 -1)))
|
rlm@25
|
331 (.addProcessor (.getViewPort this) shadow-renderer)
|
rlm@25
|
332 (.setShadowMode (.getRootNode this)
|
rlm@25
|
333 RenderQueue$ShadowMode/Off)
|
rlm@25
|
334 ;; call the supplied setup-fn
|
rlm@25
|
335 (if setup-fn
|
rlm@25
|
336 (setup-fn this))))
|
rlm@25
|
337 (simpleUpdate
|
rlm@25
|
338 [tpf]
|
rlm@25
|
339 (no-exceptions
|
rlm@25
|
340 (update-fn this tpf)))
|
rlm@25
|
341 (onAction
|
rlm@25
|
342 [binding value tpf]
|
rlm@25
|
343 ;; whenever a key is pressed, call the function returned
|
rlm@25
|
344 ;; from key-map.
|
rlm@25
|
345 (no-exceptions
|
rlm@25
|
346 (if-let [react (key-map binding)]
|
rlm@25
|
347 (react this value)))))
|
rlm@25
|
348 ;; don't show a menu to change options.
|
rlm@25
|
349 (.setShowSettings false)
|
rlm@25
|
350 ;; continue running simulation even if the window has lost
|
rlm@25
|
351 ;; focus.
|
rlm@25
|
352 (.setPauseOnLostFocus false)
|
rlm@25
|
353 (.setSettings *app-settings*))))
|
rlm@23
|
354
|
rlm@23
|
355 (defn apply-map
|
rlm@25
|
356 "Like apply, but works for maps and functions that expect an
|
rlm@25
|
357 implicit map and nothing else as in (fn [& {}]).
|
rlm@25
|
358 ------- Example -------
|
rlm@25
|
359 (defn demo [& {:keys [www] :or {www \"oh yeah\"} :as env}]
|
rlm@25
|
360 (println www))
|
rlm@25
|
361 (apply-map demo {:www \"hello!\"})
|
rlm@25
|
362 -->\"hello\""
|
rlm@23
|
363 [fn m]
|
rlm@23
|
364 (apply fn (reduce #(into %1 %2) [] m)))
|
rlm@23
|
365
|
rlm@23
|
366 #+end_src
|
rlm@23
|
367
|
rlm@23
|
368
|
rlm@25
|
369 =(world)= is the most important function here. It presents a more
|
rlm@25
|
370 functional interface to the Application life-cycle, and all it's
|
rlm@25
|
371 objects except =root-node= are plain clojure data structures. It's now
|
rlm@25
|
372 possible to extend functionally by composing multiple functions
|
rlm@25
|
373 together, and to add more keyboard-driven actions by combining clojure
|
rlm@25
|
374 maps.
|
rlm@24
|
375
|
rlm@24
|
376
|
rlm@24
|
377
|
rlm@24
|
378 * COMMENT code generation
|
rlm@24
|
379 #+begin_src clojure :tangle ../src/cortex/world.clj
|
rlm@25
|
380 <<header>>
|
rlm@25
|
381 <<settings>>
|
rlm@25
|
382 <<exceptions>>
|
rlm@25
|
383 <<input>>
|
rlm@24
|
384 <<world>>
|
rlm@24
|
385 #+end_src
|