Mercurial > cortex
view org/world.org @ 41:cce471a4108a
done improving the touch article for tonight
author | Robert McIntyre <rlm@mit.edu> |
---|---|
date | Thu, 03 Nov 2011 10:42:28 -0700 |
parents | 183744c179e6 |
children | 00d0e1639d4b |
line wrap: on
line source
1 #+title: A Virtual World for Sensate Creatures2 #+author: Robert McIntyre3 #+email: rlm@mit.edu4 #+description: Creating a Virtual World for AI constructs using clojure and JME35 #+keywords: JME3, clojure, virtual world, exception handling6 #+SETUPFILE: ../../aurellem/org/setup.org7 #+INCLUDE: ../../aurellem/org/level-0.org8 #+BABEL: :mkdirp yes :noweb yes :exports both10 * The World12 There's no point in having senses if there's nothing to experience. In13 this section I make some tools with which to build virtual worlds for14 my characters to inhabit. If you look at the tutorials at [[http://www.jmonkeyengine.org/wiki/doku.php/jme3:beginner][the jme315 website]], you will see a pattern in how virtual worlds are normally16 built. I call this "the Java way" of making worlds.18 - The Java way:19 - Create a class that extends =SimpleApplication= or =Application=20 - Implement setup functions that create all the scene objects using21 the inherited =assetManager= and call them by overriding the22 =simpleInitApp= method.23 - Create =ActionListeners= and add them to the =inputManager=24 inherited from =Application= to handle key-bindings.25 - Override =simpleUpdate= to implement game logic.26 - Running/Testing an Application involves creating a new JVM,27 running the App, and then closing everything down.30 - A more Clojureish way:31 - Use a map from keys->functions to specify key-bindings.32 - Use functions to create objects separately from any particular33 application.34 - Use a REPL -- this means that there's only ever one JVM, and35 Applications come and go.37 Since most development work using jMonkeyEngine is done in Java, jme338 supports "the Java way" quite well out of the box. To work "the39 clojure way", it necessary to wrap the JME3 elements that deal with40 the Application life-cycle with a REPL driven interface.42 The most important modifications are:44 - Separation of Object life-cycles with the Application life-cycle.45 - Functional interface to the underlying =Application= and46 =SimpleApplication= classes.48 ** Header49 #+srcname: header50 #+begin_src clojure :results silent51 (ns cortex.world52 "World Creation, abstracion over jme3's input system, and REPL53 driven exception handling"54 {:author "Robert McIntyre"}56 (:use (clojure.contrib (def :only (defvar))))57 (:use [pokemon [lpsolve :only [constant-map]]])58 (:use [clojure.contrib [str-utils :only [re-gsub]]])60 (:import com.aurellem.capture.IsoTimer)62 (:import com.jme3.math.Vector3f)63 (:import com.jme3.scene.Node)64 (:import com.jme3.system.AppSettings)65 (:import com.jme3.system.JmeSystem)66 (:import com.jme3.input.KeyInput)67 (:import com.jme3.input.controls.KeyTrigger)68 (:import com.jme3.input.controls.MouseButtonTrigger)69 (:import com.jme3.input.InputManager)70 (:import com.jme3.bullet.BulletAppState)71 (:import com.jme3.shadow.BasicShadowRenderer)72 (:import com.jme3.app.SimpleApplication)73 (:import com.jme3.input.controls.ActionListener)74 (:import com.jme3.renderer.queue.RenderQueue$ShadowMode)75 (:import org.lwjgl.input.Mouse))76 #+end_src78 ** General Settings79 #+srcname: settings80 #+begin_src clojure81 (in-ns 'cortex.world)83 (defvar *app-settings*84 (doto (AppSettings. true)85 (.setFullscreen false)86 (.setTitle "Aurellem.")87 ;; The "Send" AudioRenderer supports sumulated hearing.88 (.setAudioRenderer "Send"))89 "These settings control how the game is displayed on the screen for90 debugging purposes. Use binding forms to change this if desired.91 Full-screen mode does not work on some computers.")93 (defn asset-manager94 "returns a new, configured assetManager" []95 (JmeSystem/newAssetManager96 (.getResource97 (.getContextClassLoader (Thread/currentThread))98 "com/jme3/asset/Desktop.cfg")))99 #+end_src101 Normally, people just use the =AssetManager= inherited from102 =Application= whenever they extend that class. However,103 =AssetManagers= are useful on their own to create objects/ materials,104 independent from any particular application. =(asset-manager)= makes105 object creation less tightly bound to a particular Application106 Instance.109 ** Exception Protection110 #+srcname: exceptions111 #+begin_src clojure112 (in-ns 'cortex.world)114 (defmacro no-exceptions115 "Sweet relief like I never knew."116 [& forms]117 `(try ~@forms (catch Exception e# (.printStackTrace e#))))119 (defn thread-exception-removal120 "Exceptions thrown in the graphics rendering thread generally cause121 the entire REPL to crash! It is good to suppress them while trying122 things out to shorten the debug loop."123 []124 (.setUncaughtExceptionHandler125 (Thread/currentThread)126 (proxy [Thread$UncaughtExceptionHandler] []127 (uncaughtException128 [thread thrown]129 (println "uncaught-exception thrown in " thread)130 (println (.getMessage thrown))))))132 #+end_src134 Exceptions thrown in the LWJGL render thread, if not caught, will135 destroy the entire JVM process including the REPL and slow development136 to a crawl. It is better to try to continue on in the face of137 exceptions and keep the REPL alive as long as possible. Normally it138 is possible to just exit the faulty Application, fix the bug,139 reevaluate the appropriate forms, and be on your way, without140 restarting the JVM.142 ** Input143 #+srcname: input144 #+begin_src clojure145 (in-ns 'cortex.world)147 (defn all-keys148 "Uses reflection to generate a map of string names to jme3 trigger149 objects, which govern input from the keyboard and mouse"150 []151 (let [inputs (pokemon.lpsolve/constant-map KeyInput)]152 (assoc153 (zipmap (map (fn [field]154 (.toLowerCase (re-gsub #"_" "-" field))) (vals inputs))155 (map (fn [val] (KeyTrigger. val)) (keys inputs)))156 ;;explicitly add mouse controls157 "mouse-left" (MouseButtonTrigger. 0)158 "mouse-middle" (MouseButtonTrigger. 2)159 "mouse-right" (MouseButtonTrigger. 1))))161 (defn initialize-inputs162 "Establish key-bindings for a particular virtual world."163 [game input-manager key-map]164 (doall165 (map (fn [[name trigger]]166 (.addMapping167 ^InputManager input-manager168 name (into-array (class trigger)169 [trigger]))) key-map))170 (doall171 (map (fn [name]172 (.addListener173 ^InputManager input-manager game174 (into-array String [name]))) (keys key-map))))176 #+end_src178 These functions are for controlling the world through the keyboard and179 mouse.181 I reuse =constant-map= from [[../../pokemon-types/html/lpsolve.html#sec-3-2-4][=pokemon.lpsolve=]] to get the numerical182 values for all the keys defined in the =KeyInput= class. The183 documentation for =constant-map= is:185 #+begin_src clojure :results output :exports both186 (doc pokemon.lpsolve/constant-map)187 #+end_src189 #+results:190 : -------------------------191 : pokemon.lpsolve/constant-map192 : ([class])193 : Takes a class and creates a map of the static constant integer194 : fields with their names. This helps with C wrappers where they have195 : just defined a bunch of integer constants instead of enums197 #+begin_src clojure :exports both :results verbatim198 (take 5 (vals (pokemon.lpsolve/constant-map KeyInput)))199 #+end_src201 #+results:202 : ("KEY_ESCAPE" "KEY_1" "KEY_2" "KEY_3" "KEY_4")204 =(all-keys)= converts the constant names like =KEY_J= to the more205 clojure-like =key-j=, and returns a map from these keys to206 jMonkeyEngine =KeyTrigger= objects, which jMonkeyEngine3 uses as it's207 abstraction over the physical keys. =all-keys= also adds the three208 mouse button controls to the map.210 #+begin_src clojure :exports both :results output211 (require 'clojure.contrib.pprint)212 (clojure.contrib.pprint/pprint213 (take 6 (cortex.world/all-keys)))214 #+end_src216 #+results:217 : (["key-n" #<KeyTrigger com.jme3.input.controls.KeyTrigger@9f9fec0>]218 : ["key-apps" #<KeyTrigger com.jme3.input.controls.KeyTrigger@28edbe7f>]219 : ["key-pgup" #<KeyTrigger com.jme3.input.controls.KeyTrigger@647fd33a>]220 : ["key-f8" #<KeyTrigger com.jme3.input.controls.KeyTrigger@24f97188>]221 : ["key-o" #<KeyTrigger com.jme3.input.controls.KeyTrigger@685c53ff>]222 : ["key-at" #<KeyTrigger com.jme3.input.controls.KeyTrigger@4c3e2e5f>])224 ** World Creation225 #+srcname: world226 #+begin_src clojure :results silent227 (in-ns 'cortex.world)229 (defn no-op230 "Takes any number of arguments and does nothing."231 [& _])233 (defn traverse234 "apply f to every non-node, deeply"235 [f node]236 (if (isa? (class node) Node)237 (dorun (map (partial traverse f) (.getChildren node)))238 (f node)))240 (defn world241 "the =world= function takes care of the details of initializing a242 SimpleApplication.244 ***** Arguments:246 - root-node : a com.jme3.scene.Node object which contains all of247 the objects that should be in the simulation.249 - key-map : a map from strings describing keys to functions that250 should be executed whenever that key is pressed.251 the functions should take a SimpleApplication object and a252 boolean value. The SimpleApplication is the current simulation253 that is running, and the boolean is true if the key is being254 pressed, and false if it is being released. As an example,256 {\"key-j\" (fn [game value] (if value (println \"key j pressed\")))}258 is a valid key-map which will cause the simulation to print a259 message whenever the 'j' key on the keyboard is pressed.261 - setup-fn : a function that takes a SimpleApplication object. It262 is called once when initializing the simulation. Use it to263 create things like lights, change the gravity, initialize debug264 nodes, etc.266 - update-fn : this function takes a SimpleApplication object and a267 float and is called every frame of the simulation. The float268 tells how many seconds is has been since the last frame was269 rendered, according to whatever clock jme is currently270 using. The default is to use IsoTimer which will result in this271 value always being the same.272 "273 [root-node key-map setup-fn update-fn]274 (let [physics-manager (BulletAppState.)]275 (doto276 (proxy [SimpleApplication ActionListener] []277 (simpleInitApp278 []279 (no-exceptions280 ;; allow AI entities as much time as they need to think.281 (.setTimer this (IsoTimer. 60))282 (.setFrustumFar (.getCamera this) 300)283 ;; Create default key-map.284 (initialize-inputs this (.getInputManager this) (all-keys))285 ;; Don't take control of the mouse286 (org.lwjgl.input.Mouse/setGrabbed false)287 ;; add all objects to the world288 (.attachChild (.getRootNode this) root-node)289 ;; enable physics290 ;; add a physics manager291 (.attach (.getStateManager this) physics-manager)292 (.setGravity (.getPhysicsSpace physics-manager)293 (Vector3f. 0 -9.81 0))294 ;; go through every object and add it to the physics295 ;; manager if relevant.296 (traverse (fn [geom]297 (dorun298 (for [n (range (.getNumControls geom))]299 (do300 (.add (.getPhysicsSpace physics-manager)301 (.getControl geom n))))))302 (.getRootNode this))303 ;; call the supplied setup-fn304 (if setup-fn305 (setup-fn this))))306 (simpleUpdate307 [tpf]308 (no-exceptions309 (update-fn this tpf)))310 (onAction311 [binding value tpf]312 ;; whenever a key is pressed, call the function returned313 ;; from key-map.314 (no-exceptions315 (if-let [react (key-map binding)]316 (react this value)))))317 ;; don't show a menu to change options.318 (.setShowSettings false)319 ;; continue running simulation even if the window has lost320 ;; focus.321 (.setPauseOnLostFocus false)322 (.setSettings *app-settings*))))323 #+end_src326 =(world)= is the most important function here. It presents a more327 functional interface to the Application life-cycle, and all its328 arguments except =root-node= are plain immutable clojure data329 structures. This makes it easier to extend functionally by composing330 multiple functions together, and to add more keyboard-driven actions331 by combining clojure maps.335 * COMMENT code generation336 #+begin_src clojure :tangle ../src/cortex/world.clj :noweb yes337 <<header>>338 <<settings>>339 <<exceptions>>340 <<input>>341 <<world>>342 #+end_src