annotate org/world.org @ 29:6372c108c5c6

cleaned up util.org
author Robert McIntyre <rlm@mit.edu>
date Mon, 24 Oct 2011 12:35:15 -0700
parents bbffa41a12a9
children 183744c179e6
rev   line source
rlm@25 1 #+title: A World for the 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@25 34 - Use an 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@25 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@25 60 (:import com.jme3.math.Vector3f)
rlm@25 61 (:import com.jme3.scene.Node)
rlm@25 62 (:import com.jme3.system.AppSettings)
rlm@25 63 (:import com.jme3.system.JmeSystem)
rlm@25 64 (:import com.jme3.system.IsoTimer)
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@25 74 (:import org.lwjgl.input.Mouse))
rlm@25 75 #+end_src
rlm@25 76
rlm@25 77 ** General Settings
rlm@25 78 #+srcname: settings
rlm@25 79 #+begin_src clojure
rlm@25 80 (in-ns 'cortex.world)
rlm@23 81
rlm@23 82 (defvar *app-settings*
rlm@23 83 (doto (AppSettings. true)
rlm@23 84 (.setFullscreen false)
rlm@23 85 (.setTitle "Aurellem.")
rlm@23 86 ;; disable 32 bit stuff for now
rlm@23 87 ;;(.setAudioRenderer "Send")
rlm@23 88 )
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@25 105 object creation less tightly bound to Application initialization.
rlm@25 106
rlm@25 107
rlm@25 108 ** Exception Protection
rlm@25 109 #+srcname: exceptions
rlm@25 110 #+begin_src clojure
rlm@25 111 (in-ns 'cortex.world)
rlm@23 112
rlm@23 113 (defmacro no-exceptions
rlm@23 114 "Sweet relief like I never knew."
rlm@23 115 [& forms]
rlm@23 116 `(try ~@forms (catch Exception e# (.printStackTrace e#))))
rlm@23 117
rlm@25 118 (defn thread-exception-removal
rlm@25 119 "Exceptions thrown in the graphics rendering thread generally cause
rlm@25 120 the entire REPL to crash! It is good to suppress them while trying
rlm@25 121 things out to shorten the debug loop."
rlm@25 122 []
rlm@23 123 (.setUncaughtExceptionHandler
rlm@23 124 (Thread/currentThread)
rlm@23 125 (proxy [Thread$UncaughtExceptionHandler] []
rlm@23 126 (uncaughtException
rlm@25 127 [thread thrown]
rlm@25 128 (println "uncaught-exception thrown in " thread)
rlm@25 129 (println (.getMessage thrown))))))
rlm@23 130
rlm@25 131 #+end_src
rlm@23 132
rlm@25 133 Exceptions thrown in the LWJGL render thread, if not caught, will
rlm@25 134 destroy the entire JVM process including the REPL and slow development
rlm@25 135 to a crawl. It is better to try to continue on in the face of
rlm@25 136 exceptions and keep the REPL alive as long as possible. Normally it
rlm@25 137 is possible to just exit the faulty Application, fix the bug,
rlm@25 138 reevaluate the appropriate forms, and be on your way, without
rlm@25 139 restarting the JVM.
rlm@23 140
rlm@25 141 ** Input
rlm@25 142 #+srcname: input
rlm@25 143 #+begin_src clojure
rlm@25 144 (in-ns 'cortex.world)
rlm@23 145
rlm@23 146 (defn all-keys
rlm@25 147 "Uses reflection to generate a map of string names to jme3 trigger
rlm@25 148 objects, which govern input from the keyboard and mouse"
rlm@23 149 []
rlm@25 150 (let [inputs (pokemon.lpsolve/constant-map KeyInput)]
rlm@23 151 (assoc
rlm@23 152 (zipmap (map (fn [field]
rlm@23 153 (.toLowerCase (re-gsub #"_" "-" field))) (vals inputs))
rlm@23 154 (map (fn [val] (KeyTrigger. val)) (keys inputs)))
rlm@23 155 ;;explicitly add mouse controls
rlm@23 156 "mouse-left" (MouseButtonTrigger. 0)
rlm@23 157 "mouse-middle" (MouseButtonTrigger. 2)
rlm@23 158 "mouse-right" (MouseButtonTrigger. 1))))
rlm@23 159
rlm@23 160 (defn initialize-inputs
rlm@25 161 "Establish key-bindings for a particular virtual world."
rlm@23 162 [game input-manager key-map]
rlm@25 163 (doall
rlm@25 164 (map (fn [[name trigger]]
rlm@25 165 (.addMapping
rlm@25 166 ^InputManager input-manager
rlm@25 167 name (into-array (class trigger)
rlm@25 168 [trigger]))) key-map))
rlm@25 169 (doall
rlm@25 170 (map (fn [name]
rlm@25 171 (.addListener
rlm@25 172 ^InputManager input-manager game
rlm@25 173 (into-array String [name]))) (keys key-map))))
rlm@23 174
rlm@23 175 #+end_src
rlm@23 176
rlm@25 177 These functions are for controlling the world through the keyboard and
rlm@25 178 mouse.
rlm@23 179
rlm@25 180 I reuse =constant-map= from [[../../pokemon-types/html/lpsolve.html#sec-3-3-4][=pokemon.lpsolve=]] to get the numerical
rlm@23 181 values for all the keys defined in the =KeyInput= class. The
rlm@23 182 documentation for =constant-map= is:
rlm@23 183
rlm@23 184 #+begin_src clojure :results output
rlm@23 185 (doc pokemon.lpsolve/constant-map)
rlm@23 186 #+end_src
rlm@23 187
rlm@23 188 #+results:
rlm@23 189 : -------------------------
rlm@23 190 : pokemon.lpsolve/constant-map
rlm@23 191 : ([class])
rlm@23 192 : Takes a class and creates a map of the static constant integer
rlm@23 193 : fields with their names. This helps with C wrappers where they have
rlm@23 194 : just defined a bunch of integer constants instead of enums
rlm@23 195
rlm@26 196 #+begin_src clojure :exports both :results verbatim
rlm@26 197 (take 5 (vals (pokemon.lpsolve/constant-map KeyInput)))
rlm@26 198 #+end_src
rlm@26 199
rlm@26 200 #+results:
rlm@26 201 : ("KEY_ESCAPE" "KEY_1" "KEY_2" "KEY_3" "KEY_4")
rlm@26 202
rlm@25 203 =(all-keys)= converts the constant names like =KEY_J= to the more
rlm@25 204 clojure-like =key-j=, and returns a map from these keys to
rlm@25 205 jMonkeyEngine =KeyTrigger= objects, which jMonkeyEngine3 uses as it's
rlm@25 206 abstraction over the physical keys. =all-keys= also adds the three
rlm@25 207 mouse button controls to the map.
rlm@23 208
rlm@25 209 #+begin_src clojure :exports both :results output
rlm@25 210 (require 'clojure.contrib.pprint)
rlm@25 211 (clojure.contrib.pprint/pprint
rlm@26 212 (take 6 (cortex.world/all-keys)))
rlm@25 213 #+end_src
rlm@25 214
rlm@25 215 #+results:
rlm@26 216 : (["key-n" #<KeyTrigger com.jme3.input.controls.KeyTrigger@9f9fec0>]
rlm@26 217 : ["key-apps" #<KeyTrigger com.jme3.input.controls.KeyTrigger@28edbe7f>]
rlm@26 218 : ["key-pgup" #<KeyTrigger com.jme3.input.controls.KeyTrigger@647fd33a>]
rlm@26 219 : ["key-f8" #<KeyTrigger com.jme3.input.controls.KeyTrigger@24f97188>]
rlm@26 220 : ["key-o" #<KeyTrigger com.jme3.input.controls.KeyTrigger@685c53ff>]
rlm@26 221 : ["key-at" #<KeyTrigger com.jme3.input.controls.KeyTrigger@4c3e2e5f>])
rlm@25 222
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@23 274 (let [physics-manager (BulletAppState.)
rlm@25 275 shadow-renderer (BasicShadowRenderer.
rlm@25 276 (asset-manager) (int 256))]
rlm@25 277 (doto
rlm@25 278 (proxy [SimpleApplication ActionListener] []
rlm@25 279 (simpleInitApp
rlm@25 280 []
rlm@25 281 (no-exceptions
rlm@25 282 ;; allow AI entities as much time as they need to think.
rlm@25 283 (.setTimer this (IsoTimer. 60))
rlm@25 284 (.setFrustumFar (.getCamera this) 300)
rlm@25 285 ;; Create default key-map.
rlm@25 286 (initialize-inputs this (.getInputManager this) (all-keys))
rlm@25 287 ;; Don't take control of the mouse
rlm@25 288 (org.lwjgl.input.Mouse/setGrabbed false)
rlm@25 289 ;; add all objects to the world
rlm@25 290 (.attachChild (.getRootNode this) root-node)
rlm@25 291 ;; enable physics
rlm@25 292 ;; add a physics manager
rlm@25 293 (.attach (.getStateManager this) physics-manager)
rlm@25 294 (.setGravity (.getPhysicsSpace physics-manager)
rlm@25 295 (Vector3f. 0 -9.81 0))
rlm@25 296 ;; go through every object and add it to the physics
rlm@25 297 ;; manager if relevant.
rlm@25 298 (traverse (fn [geom]
rlm@25 299 (dorun
rlm@25 300 (for [n (range (.getNumControls geom))]
rlm@25 301 (do
rlm@25 302 (.add (.getPhysicsSpace physics-manager)
rlm@25 303 (.getControl geom n))))))
rlm@25 304 (.getRootNode this))
rlm@25 305 ;;(.addAll (.getPhysicsSpace physics-manager) (.getRootNode this))
rlm@25 306
rlm@25 307 ;; set some basic defaults for the shadow renderer.
rlm@25 308 ;; these can be undone in the setup function
rlm@25 309 (.setDirection shadow-renderer
rlm@25 310 (.normalizeLocal (Vector3f. -1 -1 -1)))
rlm@25 311 (.addProcessor (.getViewPort this) shadow-renderer)
rlm@25 312 (.setShadowMode (.getRootNode this)
rlm@25 313 RenderQueue$ShadowMode/Off)
rlm@25 314 ;; call the supplied setup-fn
rlm@25 315 (if setup-fn
rlm@25 316 (setup-fn this))))
rlm@25 317 (simpleUpdate
rlm@25 318 [tpf]
rlm@25 319 (no-exceptions
rlm@25 320 (update-fn this tpf)))
rlm@25 321 (onAction
rlm@25 322 [binding value tpf]
rlm@25 323 ;; whenever a key is pressed, call the function returned
rlm@25 324 ;; from key-map.
rlm@25 325 (no-exceptions
rlm@25 326 (if-let [react (key-map binding)]
rlm@25 327 (react this value)))))
rlm@25 328 ;; don't show a menu to change options.
rlm@25 329 (.setShowSettings false)
rlm@25 330 ;; continue running simulation even if the window has lost
rlm@25 331 ;; focus.
rlm@25 332 (.setPauseOnLostFocus false)
rlm@25 333 (.setSettings *app-settings*))))
rlm@23 334 #+end_src
rlm@23 335
rlm@23 336
rlm@25 337 =(world)= is the most important function here. It presents a more
rlm@26 338 functional interface to the Application life-cycle, and all its
rlm@26 339 arguments except =root-node= are plain immutable clojure data
rlm@26 340 structures. This makes it easier to extend functionally by composing
rlm@26 341 multiple functions together, and to add more keyboard-driven actions
rlm@26 342 by combining clojure maps.
rlm@24 343
rlm@24 344
rlm@24 345
rlm@24 346 * COMMENT code generation
rlm@29 347 #+begin_src clojure :tangle ../src/cortex/world.clj :noweb yes
rlm@25 348 <<header>>
rlm@25 349 <<settings>>
rlm@25 350 <<exceptions>>
rlm@25 351 <<input>>
rlm@24 352 <<world>>
rlm@24 353 #+end_src