annotate org/world.org @ 52:00d0e1639d4b

simplified world, got buggy physics ragdoll working
author Robert McIntyre <rlm@mit.edu>
date Mon, 14 Nov 2011 21:30:23 -0700
parents 183744c179e6
children 4b5f00110d8c
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@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@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@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@34 60 (:import com.aurellem.capture.IsoTimer)
rlm@34 61
rlm@25 62 (:import com.jme3.math.Vector3f)
rlm@25 63 (:import com.jme3.scene.Node)
rlm@25 64 (:import com.jme3.system.AppSettings)
rlm@25 65 (:import com.jme3.system.JmeSystem)
rlm@25 66 (:import com.jme3.input.KeyInput)
rlm@25 67 (:import com.jme3.input.controls.KeyTrigger)
rlm@25 68 (:import com.jme3.input.controls.MouseButtonTrigger)
rlm@25 69 (:import com.jme3.input.InputManager)
rlm@25 70 (:import com.jme3.bullet.BulletAppState)
rlm@25 71 (:import com.jme3.shadow.BasicShadowRenderer)
rlm@25 72 (:import com.jme3.app.SimpleApplication)
rlm@25 73 (:import com.jme3.input.controls.ActionListener)
rlm@25 74 (:import com.jme3.renderer.queue.RenderQueue$ShadowMode)
rlm@25 75 (:import org.lwjgl.input.Mouse))
rlm@25 76 #+end_src
rlm@25 77
rlm@25 78 ** General Settings
rlm@25 79 #+srcname: settings
rlm@25 80 #+begin_src clojure
rlm@25 81 (in-ns 'cortex.world)
rlm@23 82
rlm@23 83 (defvar *app-settings*
rlm@23 84 (doto (AppSettings. true)
rlm@23 85 (.setFullscreen false)
rlm@23 86 (.setTitle "Aurellem.")
rlm@34 87 ;; The "Send" AudioRenderer supports sumulated hearing.
rlm@34 88 (.setAudioRenderer "Send"))
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@34 105 object creation less tightly bound to a particular Application
rlm@34 106 Instance.
rlm@25 107
rlm@25 108
rlm@25 109 ** Exception Protection
rlm@25 110 #+srcname: 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@25 143 #+srcname: input
rlm@25 144 #+begin_src clojure
rlm@25 145 (in-ns 'cortex.world)
rlm@23 146
rlm@23 147 (defn all-keys
rlm@25 148 "Uses reflection to generate a map of string names to jme3 trigger
rlm@25 149 objects, which govern input from the keyboard and mouse"
rlm@23 150 []
rlm@25 151 (let [inputs (pokemon.lpsolve/constant-map KeyInput)]
rlm@23 152 (assoc
rlm@23 153 (zipmap (map (fn [field]
rlm@23 154 (.toLowerCase (re-gsub #"_" "-" field))) (vals inputs))
rlm@23 155 (map (fn [val] (KeyTrigger. val)) (keys inputs)))
rlm@23 156 ;;explicitly add mouse controls
rlm@23 157 "mouse-left" (MouseButtonTrigger. 0)
rlm@23 158 "mouse-middle" (MouseButtonTrigger. 2)
rlm@23 159 "mouse-right" (MouseButtonTrigger. 1))))
rlm@23 160
rlm@23 161 (defn initialize-inputs
rlm@25 162 "Establish key-bindings for a particular virtual world."
rlm@23 163 [game input-manager key-map]
rlm@25 164 (doall
rlm@25 165 (map (fn [[name trigger]]
rlm@25 166 (.addMapping
rlm@25 167 ^InputManager input-manager
rlm@25 168 name (into-array (class trigger)
rlm@25 169 [trigger]))) key-map))
rlm@25 170 (doall
rlm@25 171 (map (fn [name]
rlm@25 172 (.addListener
rlm@25 173 ^InputManager input-manager game
rlm@34 174 (into-array String [name]))) (keys key-map))))
rlm@23 175
rlm@23 176 #+end_src
rlm@23 177
rlm@25 178 These functions are for controlling the world through the keyboard and
rlm@25 179 mouse.
rlm@23 180
rlm@34 181 I reuse =constant-map= from [[../../pokemon-types/html/lpsolve.html#sec-3-2-4][=pokemon.lpsolve=]] to get the numerical
rlm@23 182 values for all the keys defined in the =KeyInput= class. The
rlm@23 183 documentation for =constant-map= is:
rlm@23 184
rlm@34 185 #+begin_src clojure :results output :exports both
rlm@23 186 (doc pokemon.lpsolve/constant-map)
rlm@23 187 #+end_src
rlm@23 188
rlm@23 189 #+results:
rlm@23 190 : -------------------------
rlm@23 191 : pokemon.lpsolve/constant-map
rlm@23 192 : ([class])
rlm@23 193 : Takes a class and creates a map of the static constant integer
rlm@23 194 : fields with their names. This helps with C wrappers where they have
rlm@23 195 : just defined a bunch of integer constants instead of enums
rlm@23 196
rlm@26 197 #+begin_src clojure :exports both :results verbatim
rlm@26 198 (take 5 (vals (pokemon.lpsolve/constant-map KeyInput)))
rlm@26 199 #+end_src
rlm@26 200
rlm@26 201 #+results:
rlm@26 202 : ("KEY_ESCAPE" "KEY_1" "KEY_2" "KEY_3" "KEY_4")
rlm@26 203
rlm@25 204 =(all-keys)= converts the constant names like =KEY_J= to the more
rlm@25 205 clojure-like =key-j=, and returns a map from these keys to
rlm@25 206 jMonkeyEngine =KeyTrigger= objects, which jMonkeyEngine3 uses as it's
rlm@25 207 abstraction over the physical keys. =all-keys= also adds the three
rlm@25 208 mouse button controls to the map.
rlm@23 209
rlm@25 210 #+begin_src clojure :exports both :results output
rlm@25 211 (require 'clojure.contrib.pprint)
rlm@25 212 (clojure.contrib.pprint/pprint
rlm@26 213 (take 6 (cortex.world/all-keys)))
rlm@25 214 #+end_src
rlm@25 215
rlm@25 216 #+results:
rlm@26 217 : (["key-n" #<KeyTrigger com.jme3.input.controls.KeyTrigger@9f9fec0>]
rlm@26 218 : ["key-apps" #<KeyTrigger com.jme3.input.controls.KeyTrigger@28edbe7f>]
rlm@26 219 : ["key-pgup" #<KeyTrigger com.jme3.input.controls.KeyTrigger@647fd33a>]
rlm@26 220 : ["key-f8" #<KeyTrigger com.jme3.input.controls.KeyTrigger@24f97188>]
rlm@26 221 : ["key-o" #<KeyTrigger com.jme3.input.controls.KeyTrigger@685c53ff>]
rlm@26 222 : ["key-at" #<KeyTrigger com.jme3.input.controls.KeyTrigger@4c3e2e5f>])
rlm@25 223
rlm@25 224 ** World Creation
rlm@23 225 #+srcname: world
rlm@23 226 #+begin_src clojure :results silent
rlm@23 227 (in-ns 'cortex.world)
rlm@23 228
rlm@25 229 (defn no-op
rlm@25 230 "Takes any number of arguments and does nothing."
rlm@25 231 [& _])
rlm@25 232
rlm@23 233 (defn traverse
rlm@23 234 "apply f to every non-node, deeply"
rlm@23 235 [f node]
rlm@23 236 (if (isa? (class node) Node)
rlm@23 237 (dorun (map (partial traverse f) (.getChildren node)))
rlm@23 238 (f node)))
rlm@23 239
rlm@25 240 (defn world
rlm@25 241 "the =world= function takes care of the details of initializing a
rlm@25 242 SimpleApplication.
rlm@23 243
rlm@25 244 ***** Arguments:
rlm@25 245
rlm@25 246 - root-node : a com.jme3.scene.Node object which contains all of
rlm@25 247 the objects that should be in the simulation.
rlm@25 248
rlm@25 249 - key-map : a map from strings describing keys to functions that
rlm@25 250 should be executed whenever that key is pressed.
rlm@25 251 the functions should take a SimpleApplication object and a
rlm@25 252 boolean value. The SimpleApplication is the current simulation
rlm@25 253 that is running, and the boolean is true if the key is being
rlm@25 254 pressed, and false if it is being released. As an example,
rlm@25 255
rlm@25 256 {\"key-j\" (fn [game value] (if value (println \"key j pressed\")))}
rlm@25 257
rlm@25 258 is a valid key-map which will cause the simulation to print a
rlm@25 259 message whenever the 'j' key on the keyboard is pressed.
rlm@25 260
rlm@25 261 - setup-fn : a function that takes a SimpleApplication object. It
rlm@25 262 is called once when initializing the simulation. Use it to
rlm@25 263 create things like lights, change the gravity, initialize debug
rlm@25 264 nodes, etc.
rlm@25 265
rlm@25 266 - update-fn : this function takes a SimpleApplication object and a
rlm@25 267 float and is called every frame of the simulation. The float
rlm@25 268 tells how many seconds is has been since the last frame was
rlm@25 269 rendered, according to whatever clock jme is currently
rlm@25 270 using. The default is to use IsoTimer which will result in this
rlm@25 271 value always being the same.
rlm@25 272 "
rlm@23 273 [root-node key-map setup-fn update-fn]
rlm@34 274 (let [physics-manager (BulletAppState.)]
rlm@25 275 (doto
rlm@25 276 (proxy [SimpleApplication ActionListener] []
rlm@25 277 (simpleInitApp
rlm@25 278 []
rlm@25 279 (no-exceptions
rlm@25 280 ;; allow AI entities as much time as they need to think.
rlm@25 281 (.setTimer this (IsoTimer. 60))
rlm@25 282 (.setFrustumFar (.getCamera this) 300)
rlm@25 283 ;; Create default key-map.
rlm@25 284 (initialize-inputs this (.getInputManager this) (all-keys))
rlm@25 285 ;; Don't take control of the mouse
rlm@25 286 (org.lwjgl.input.Mouse/setGrabbed false)
rlm@25 287 ;; add all objects to the world
rlm@25 288 (.attachChild (.getRootNode this) root-node)
rlm@25 289 ;; enable physics
rlm@25 290 ;; add a physics manager
rlm@25 291 (.attach (.getStateManager this) physics-manager)
rlm@25 292 (.setGravity (.getPhysicsSpace physics-manager)
rlm@25 293 (Vector3f. 0 -9.81 0))
rlm@25 294 ;; go through every object and add it to the physics
rlm@25 295 ;; manager if relevant.
rlm@52 296 ;;(traverse (fn [geom]
rlm@52 297 ;; (dorun
rlm@52 298 ;; (for [n (range (.getNumControls geom))]
rlm@52 299 ;; (do
rlm@52 300 ;; (cortex.util/println-repl
rlm@52 301 ;; "adding " (.getControl geom n))
rlm@52 302 ;; (.add (.getPhysicsSpace physics-manager)
rlm@52 303 ;; (.getControl geom n))))))
rlm@52 304 ;; (.getRootNode this))
rlm@25 305 ;; call the supplied setup-fn
rlm@52 306 ;; simpler !
rlm@52 307 (.addAll (.getPhysicsSpace physics-manager) root-node)
rlm@25 308 (if setup-fn
rlm@25 309 (setup-fn this))))
rlm@25 310 (simpleUpdate
rlm@25 311 [tpf]
rlm@25 312 (no-exceptions
rlm@25 313 (update-fn this tpf)))
rlm@25 314 (onAction
rlm@25 315 [binding value tpf]
rlm@25 316 ;; whenever a key is pressed, call the function returned
rlm@25 317 ;; from key-map.
rlm@25 318 (no-exceptions
rlm@25 319 (if-let [react (key-map binding)]
rlm@25 320 (react this value)))))
rlm@25 321 ;; don't show a menu to change options.
rlm@25 322 (.setShowSettings false)
rlm@25 323 ;; continue running simulation even if the window has lost
rlm@25 324 ;; focus.
rlm@25 325 (.setPauseOnLostFocus false)
rlm@25 326 (.setSettings *app-settings*))))
rlm@23 327 #+end_src
rlm@23 328
rlm@23 329
rlm@25 330 =(world)= is the most important function here. It presents a more
rlm@26 331 functional interface to the Application life-cycle, and all its
rlm@26 332 arguments except =root-node= are plain immutable clojure data
rlm@26 333 structures. This makes it easier to extend functionally by composing
rlm@26 334 multiple functions together, and to add more keyboard-driven actions
rlm@26 335 by combining clojure maps.
rlm@24 336
rlm@24 337
rlm@24 338
rlm@24 339 * COMMENT code generation
rlm@29 340 #+begin_src clojure :tangle ../src/cortex/world.clj :noweb yes
rlm@25 341 <<header>>
rlm@25 342 <<settings>>
rlm@25 343 <<exceptions>>
rlm@25 344 <<input>>
rlm@24 345 <<world>>
rlm@24 346 #+end_src