view org/world.org @ 317:bb3f8a4af87f

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