annotate org/world.org @ 207:bb3b75bf1664

fixed source links
author Robert McIntyre <rlm@mit.edu>
date Thu, 09 Feb 2012 04:28:31 -0700
parents 0235c32152af
children 7e7f8d6d9ec5
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@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@66 56 (:use (clojure.contrib (def :only (defvar 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@23 84 (defvar *app-settings*
rlm@23 85 (doto (AppSettings. true)
rlm@23 86 (.setFullscreen false)
rlm@23 87 (.setTitle "Aurellem.")
rlm@34 88 ;; The "Send" AudioRenderer supports sumulated hearing.
rlm@34 89 (.setAudioRenderer "Send"))
rlm@23 90 "These settings control how the game is displayed on the screen for
rlm@23 91 debugging purposes. Use binding forms to change this if desired.
rlm@25 92 Full-screen mode does not work on some computers.")
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@25 220 (require 'clojure.contrib.pprint)
rlm@25 221 (clojure.contrib.pprint/pprint
rlm@26 222 (take 6 (cortex.world/all-keys)))
rlm@25 223 #+end_src
rlm@25 224
rlm@25 225 #+results:
rlm@26 226 : (["key-n" #<KeyTrigger com.jme3.input.controls.KeyTrigger@9f9fec0>]
rlm@26 227 : ["key-apps" #<KeyTrigger com.jme3.input.controls.KeyTrigger@28edbe7f>]
rlm@26 228 : ["key-pgup" #<KeyTrigger com.jme3.input.controls.KeyTrigger@647fd33a>]
rlm@26 229 : ["key-f8" #<KeyTrigger com.jme3.input.controls.KeyTrigger@24f97188>]
rlm@26 230 : ["key-o" #<KeyTrigger com.jme3.input.controls.KeyTrigger@685c53ff>]
rlm@26 231 : ["key-at" #<KeyTrigger com.jme3.input.controls.KeyTrigger@4c3e2e5f>])
rlm@25 232
rlm@25 233 ** World Creation
rlm@66 234 #+name: world
rlm@23 235 #+begin_src clojure :results silent
rlm@23 236 (in-ns 'cortex.world)
rlm@23 237
rlm@25 238 (defn no-op
rlm@25 239 "Takes any number of arguments and does nothing."
rlm@25 240 [& _])
rlm@25 241
rlm@23 242 (defn traverse
rlm@23 243 "apply f to every non-node, deeply"
rlm@23 244 [f node]
rlm@23 245 (if (isa? (class node) Node)
rlm@23 246 (dorun (map (partial traverse f) (.getChildren node)))
rlm@23 247 (f node)))
rlm@23 248
rlm@25 249 (defn world
rlm@25 250 "the =world= function takes care of the details of initializing a
rlm@25 251 SimpleApplication.
rlm@23 252
rlm@25 253 ***** Arguments:
rlm@25 254
rlm@25 255 - root-node : a com.jme3.scene.Node object which contains all of
rlm@25 256 the objects that should be in the simulation.
rlm@25 257
rlm@25 258 - key-map : a map from strings describing keys to functions that
rlm@25 259 should be executed whenever that key is pressed.
rlm@25 260 the functions should take a SimpleApplication object and a
rlm@25 261 boolean value. The SimpleApplication is the current simulation
rlm@25 262 that is running, and the boolean is true if the key is being
rlm@25 263 pressed, and false if it is being released. As an example,
rlm@25 264
rlm@25 265 {\"key-j\" (fn [game value] (if value (println \"key j pressed\")))}
rlm@25 266
rlm@25 267 is a valid key-map which will cause the simulation to print a
rlm@25 268 message whenever the 'j' key on the keyboard is pressed.
rlm@25 269
rlm@25 270 - setup-fn : a function that takes a SimpleApplication object. It
rlm@25 271 is called once when initializing the simulation. Use it to
rlm@25 272 create things like lights, change the gravity, initialize debug
rlm@25 273 nodes, etc.
rlm@25 274
rlm@25 275 - update-fn : this function takes a SimpleApplication object and a
rlm@25 276 float and is called every frame of the simulation. The float
rlm@25 277 tells how many seconds is has been since the last frame was
rlm@25 278 rendered, according to whatever clock jme is currently
rlm@25 279 using. The default is to use IsoTimer which will result in this
rlm@25 280 value always being the same.
rlm@25 281 "
rlm@23 282 [root-node key-map setup-fn update-fn]
rlm@34 283 (let [physics-manager (BulletAppState.)]
rlm@67 284 (JmeSystem/setSystemDelegate (AurellemSystemDelegate.))
rlm@25 285 (doto
rlm@25 286 (proxy [SimpleApplication ActionListener] []
rlm@25 287 (simpleInitApp
rlm@25 288 []
rlm@25 289 (no-exceptions
rlm@25 290 ;; allow AI entities as much time as they need to think.
rlm@25 291 (.setTimer this (IsoTimer. 60))
rlm@25 292 (.setFrustumFar (.getCamera this) 300)
rlm@25 293 ;; Create default key-map.
rlm@25 294 (initialize-inputs this (.getInputManager this) (all-keys))
rlm@25 295 ;; Don't take control of the mouse
rlm@25 296 (org.lwjgl.input.Mouse/setGrabbed false)
rlm@25 297 ;; add all objects to the world
rlm@25 298 (.attachChild (.getRootNode this) root-node)
rlm@25 299 ;; enable physics
rlm@25 300 ;; add a physics manager
rlm@25 301 (.attach (.getStateManager this) physics-manager)
rlm@25 302 (.setGravity (.getPhysicsSpace physics-manager)
rlm@25 303 (Vector3f. 0 -9.81 0))
rlm@25 304 ;; go through every object and add it to the physics
rlm@25 305 ;; manager if relevant.
rlm@52 306 ;;(traverse (fn [geom]
rlm@52 307 ;; (dorun
rlm@52 308 ;; (for [n (range (.getNumControls geom))]
rlm@52 309 ;; (do
rlm@52 310 ;; (cortex.util/println-repl
rlm@52 311 ;; "adding " (.getControl geom n))
rlm@52 312 ;; (.add (.getPhysicsSpace physics-manager)
rlm@52 313 ;; (.getControl geom n))))))
rlm@52 314 ;; (.getRootNode this))
rlm@25 315 ;; call the supplied setup-fn
rlm@52 316 ;; simpler !
rlm@52 317 (.addAll (.getPhysicsSpace physics-manager) root-node)
rlm@25 318 (if setup-fn
rlm@25 319 (setup-fn this))))
rlm@25 320 (simpleUpdate
rlm@25 321 [tpf]
rlm@25 322 (no-exceptions
rlm@25 323 (update-fn this tpf)))
rlm@25 324 (onAction
rlm@25 325 [binding value tpf]
rlm@25 326 ;; whenever a key is pressed, call the function returned
rlm@25 327 ;; from key-map.
rlm@25 328 (no-exceptions
rlm@25 329 (if-let [react (key-map binding)]
rlm@25 330 (react this value)))))
rlm@25 331 ;; don't show a menu to change options.
rlm@25 332 (.setShowSettings false)
rlm@25 333 ;; continue running simulation even if the window has lost
rlm@25 334 ;; focus.
rlm@25 335 (.setPauseOnLostFocus false)
rlm@25 336 (.setSettings *app-settings*))))
rlm@67 337
rlm@23 338 #+end_src
rlm@23 339
rlm@23 340
rlm@25 341 =(world)= is the most important function here. It presents a more
rlm@26 342 functional interface to the Application life-cycle, and all its
rlm@26 343 arguments except =root-node= are plain immutable clojure data
rlm@26 344 structures. This makes it easier to extend functionally by composing
rlm@26 345 multiple functions together, and to add more keyboard-driven actions
rlm@26 346 by combining clojure maps.
rlm@24 347
rlm@24 348
rlm@24 349
rlm@24 350 * COMMENT code generation
rlm@29 351 #+begin_src clojure :tangle ../src/cortex/world.clj :noweb yes
rlm@25 352 <<header>>
rlm@25 353 <<settings>>
rlm@25 354 <<exceptions>>
rlm@25 355 <<input>>
rlm@24 356 <<world>>
rlm@24 357 #+end_src