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