annotate org/world.org @ 567:7837ca42d82c

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