rlm@34: #+title: A Virtual World for Sensate Creatures rlm@23: #+author: Robert McIntyre rlm@23: #+email: rlm@mit.edu rlm@25: #+description: Creating a Virtual World for AI constructs using clojure and JME3 rlm@25: #+keywords: JME3, clojure, virtual world, exception handling rlm@23: #+SETUPFILE: ../../aurellem/org/setup.org rlm@23: #+INCLUDE: ../../aurellem/org/level-0.org rlm@29: #+BABEL: :mkdirp yes :noweb yes :exports both rlm@23: rlm@70: * The World rlm@23: rlm@25: There's no point in having senses if there's nothing to experience. In rlm@25: this section I make some tools with which to build virtual worlds for rlm@25: my characters to inhabit. If you look at the tutorials at [[http://www.jmonkeyengine.org/wiki/doku.php/jme3:beginner][the jme3 rlm@25: website]], you will see a pattern in how virtual worlds are normally rlm@25: built. I call this "the Java way" of making worlds. rlm@23: rlm@25: - The Java way: rlm@25: - Create a class that extends =SimpleApplication= or =Application= rlm@25: - Implement setup functions that create all the scene objects using rlm@25: the inherited =assetManager= and call them by overriding the rlm@25: =simpleInitApp= method. rlm@25: - Create =ActionListeners= and add them to the =inputManager= rlm@25: inherited from =Application= to handle key-bindings. rlm@25: - Override =simpleUpdate= to implement game logic. rlm@25: - Running/Testing an Application involves creating a new JVM, rlm@25: running the App, and then closing everything down. rlm@23: rlm@23: rlm@25: - A more Clojureish way: rlm@25: - Use a map from keys->functions to specify key-bindings. rlm@25: - Use functions to create objects separately from any particular rlm@25: application. rlm@34: - Use a REPL -- this means that there's only ever one JVM, and rlm@25: Applications come and go. rlm@25: rlm@25: Since most development work using jMonkeyEngine is done in Java, jme3 rlm@25: supports "the Java way" quite well out of the box. To work "the rlm@34: clojure way", it necessary to wrap the JME3 elements that deal with rlm@25: the Application life-cycle with a REPL driven interface. rlm@25: rlm@25: The most important modifications are: rlm@25: rlm@25: - Separation of Object life-cycles with the Application life-cycle. rlm@25: - Functional interface to the underlying =Application= and rlm@25: =SimpleApplication= classes. rlm@67: rlm@25: ** Header rlm@66: #+name: header rlm@25: #+begin_src clojure :results silent rlm@25: (ns cortex.world rlm@306: "World Creation, abstraction over jme3's input system, and REPL rlm@25: driven exception handling" rlm@25: {:author "Robert McIntyre"} rlm@25: rlm@34: (:import com.aurellem.capture.IsoTimer) rlm@34: rlm@25: (:import com.jme3.math.Vector3f) rlm@25: (:import com.jme3.scene.Node) rlm@25: (:import com.jme3.system.AppSettings) rlm@25: (:import com.jme3.system.JmeSystem) rlm@25: (:import com.jme3.input.KeyInput) rlm@25: (:import com.jme3.input.controls.KeyTrigger) rlm@25: (:import com.jme3.input.controls.MouseButtonTrigger) rlm@25: (:import com.jme3.input.InputManager) rlm@25: (:import com.jme3.bullet.BulletAppState) rlm@25: (:import com.jme3.shadow.BasicShadowRenderer) rlm@25: (:import com.jme3.app.SimpleApplication) rlm@25: (:import com.jme3.input.controls.ActionListener) rlm@25: (:import com.jme3.renderer.queue.RenderQueue$ShadowMode) rlm@66: (:import org.lwjgl.input.Mouse) rlm@66: (:import com.aurellem.capture.AurellemSystemDelegate)) rlm@66: rlm@25: #+end_src rlm@25: rlm@65: ** General Settings rlm@66: #+name: settings rlm@25: #+begin_src clojure rlm@25: (in-ns 'cortex.world) rlm@23: rlm@315: (def ^:dynamic *app-settings* rlm@315: "These settings control how the game is displayed on the screen for rlm@315: debugging purposes. Use binding forms to change this if desired. rlm@315: Full-screen mode does not work on some computers." rlm@23: (doto (AppSettings. true) rlm@23: (.setFullscreen false) rlm@23: (.setTitle "Aurellem.") rlm@306: ;; The "Send" AudioRenderer supports simulated hearing. rlm@315: (.setAudioRenderer "Send"))) rlm@23: rlm@23: (defn asset-manager rlm@23: "returns a new, configured assetManager" [] rlm@23: (JmeSystem/newAssetManager rlm@23: (.getResource rlm@23: (.getContextClassLoader (Thread/currentThread)) rlm@23: "com/jme3/asset/Desktop.cfg"))) rlm@25: #+end_src rlm@25: rlm@25: Normally, people just use the =AssetManager= inherited from rlm@25: =Application= whenever they extend that class. However, rlm@25: =AssetManagers= are useful on their own to create objects/ materials, rlm@25: independent from any particular application. =(asset-manager)= makes rlm@34: object creation less tightly bound to a particular Application rlm@34: Instance. rlm@25: rlm@25: ** Exception Protection rlm@66: #+name: exceptions rlm@25: #+begin_src clojure rlm@25: (in-ns 'cortex.world) rlm@23: rlm@23: (defmacro no-exceptions rlm@23: "Sweet relief like I never knew." rlm@23: [& forms] rlm@23: `(try ~@forms (catch Exception e# (.printStackTrace e#)))) rlm@23: rlm@25: (defn thread-exception-removal rlm@25: "Exceptions thrown in the graphics rendering thread generally cause rlm@25: the entire REPL to crash! It is good to suppress them while trying rlm@25: things out to shorten the debug loop." rlm@25: [] rlm@23: (.setUncaughtExceptionHandler rlm@23: (Thread/currentThread) rlm@23: (proxy [Thread$UncaughtExceptionHandler] [] rlm@23: (uncaughtException rlm@25: [thread thrown] rlm@25: (println "uncaught-exception thrown in " thread) rlm@25: (println (.getMessage thrown)))))) rlm@23: rlm@25: #+end_src rlm@23: rlm@25: Exceptions thrown in the LWJGL render thread, if not caught, will rlm@25: destroy the entire JVM process including the REPL and slow development rlm@25: to a crawl. It is better to try to continue on in the face of rlm@25: exceptions and keep the REPL alive as long as possible. Normally it rlm@25: is possible to just exit the faulty Application, fix the bug, rlm@25: reevaluate the appropriate forms, and be on your way, without rlm@25: restarting the JVM. rlm@23: rlm@25: ** Input rlm@66: #+name: input rlm@25: #+begin_src clojure rlm@25: (in-ns 'cortex.world) rlm@23: rlm@65: (defn static-integer? rlm@65: "does the field represent a static integer constant?" rlm@65: [#^java.lang.reflect.Field field] rlm@65: (and (java.lang.reflect.Modifier/isStatic (.getModifiers field)) rlm@65: (integer? (.get field nil)))) rlm@65: rlm@65: (defn integer-constants [class] rlm@65: (filter static-integer? (.getFields class))) rlm@65: rlm@320: (defn constant-map rlm@65: "Takes a class and creates a map of the static constant integer rlm@65: fields with their names. This helps with C wrappers where they have rlm@65: just defined a bunch of integer constants instead of enums" rlm@65: [class] rlm@65: (let [integer-fields (integer-constants class)] rlm@65: (into (sorted-map) rlm@65: (zipmap (map #(.get % nil) integer-fields) rlm@65: (map #(.getName %) integer-fields))))) rlm@320: (alter-var-root #'constant-map memoize) rlm@65: rlm@23: (defn all-keys rlm@25: "Uses reflection to generate a map of string names to jme3 trigger rlm@25: objects, which govern input from the keyboard and mouse" rlm@23: [] rlm@65: (let [inputs (constant-map KeyInput)] rlm@23: (assoc rlm@23: (zipmap (map (fn [field] rlm@320: (.toLowerCase (.replaceAll field "_" "-"))) (vals inputs)) rlm@23: (map (fn [val] (KeyTrigger. val)) (keys inputs))) rlm@23: ;;explicitly add mouse controls rlm@23: "mouse-left" (MouseButtonTrigger. 0) rlm@23: "mouse-middle" (MouseButtonTrigger. 2) rlm@23: "mouse-right" (MouseButtonTrigger. 1)))) rlm@23: rlm@23: (defn initialize-inputs rlm@25: "Establish key-bindings for a particular virtual world." rlm@404: [game input-manager key-map] rlm@25: (doall rlm@25: (map (fn [[name trigger]] rlm@25: (.addMapping rlm@25: ^InputManager input-manager rlm@25: name (into-array (class trigger) rlm@25: [trigger]))) key-map)) rlm@25: (doall rlm@25: (map (fn [name] rlm@25: (.addListener rlm@25: ^InputManager input-manager game rlm@34: (into-array String [name]))) (keys key-map)))) rlm@23: rlm@23: #+end_src rlm@23: rlm@25: These functions are for controlling the world through the keyboard and rlm@25: mouse. rlm@23: rlm@65: =constant-map= gets the numerical values for all the keys defined in rlm@65: the =KeyInput= class. rlm@23: rlm@26: #+begin_src clojure :exports both :results verbatim rlm@65: (take 5 (vals (cortex.world/constant-map KeyInput))) rlm@26: #+end_src rlm@26: rlm@26: #+results: rlm@26: : ("KEY_ESCAPE" "KEY_1" "KEY_2" "KEY_3" "KEY_4") rlm@26: rlm@25: =(all-keys)= converts the constant names like =KEY_J= to the more rlm@25: clojure-like =key-j=, and returns a map from these keys to rlm@25: jMonkeyEngine =KeyTrigger= objects, which jMonkeyEngine3 uses as it's rlm@25: abstraction over the physical keys. =all-keys= also adds the three rlm@25: mouse button controls to the map. rlm@23: rlm@25: #+begin_src clojure :exports both :results output rlm@317: (clojure.pprint/pprint rlm@26: (take 6 (cortex.world/all-keys))) rlm@25: #+end_src rlm@25: rlm@25: #+results: rlm@317: : (["key-n" #] rlm@317: : ["key-apps" #] rlm@317: : ["key-pgup" #] rlm@317: : ["key-f8" #] rlm@317: : ["key-o" #] rlm@317: : ["key-at" #]) rlm@25: rlm@25: ** World Creation rlm@66: #+name: world rlm@23: #+begin_src clojure :results silent rlm@23: (in-ns 'cortex.world) rlm@23: rlm@25: (defn no-op rlm@25: "Takes any number of arguments and does nothing." rlm@25: [& _]) rlm@25: rlm@23: (defn traverse rlm@23: "apply f to every non-node, deeply" rlm@23: [f node] rlm@23: (if (isa? (class node) Node) rlm@23: (dorun (map (partial traverse f) (.getChildren node))) rlm@23: (f node))) rlm@23: rlm@25: (defn world rlm@25: "the =world= function takes care of the details of initializing a rlm@25: SimpleApplication. rlm@23: rlm@404: ,***** Arguments: rlm@25: rlm@25: - root-node : a com.jme3.scene.Node object which contains all of rlm@25: the objects that should be in the simulation. rlm@25: rlm@25: - key-map : a map from strings describing keys to functions that rlm@25: should be executed whenever that key is pressed. rlm@25: the functions should take a SimpleApplication object and a rlm@25: boolean value. The SimpleApplication is the current simulation rlm@25: that is running, and the boolean is true if the key is being rlm@25: pressed, and false if it is being released. As an example, rlm@25: rlm@25: {\"key-j\" (fn [game value] (if value (println \"key j pressed\")))} rlm@25: rlm@25: is a valid key-map which will cause the simulation to print a rlm@25: message whenever the 'j' key on the keyboard is pressed. rlm@25: rlm@25: - setup-fn : a function that takes a SimpleApplication object. It rlm@25: is called once when initializing the simulation. Use it to rlm@25: create things like lights, change the gravity, initialize debug rlm@25: nodes, etc. rlm@25: rlm@25: - update-fn : this function takes a SimpleApplication object and a rlm@25: float and is called every frame of the simulation. The float rlm@25: tells how many seconds is has been since the last frame was rlm@25: rendered, according to whatever clock jme is currently rlm@25: using. The default is to use IsoTimer which will result in this rlm@25: value always being the same. rlm@25: " rlm@23: [root-node key-map setup-fn update-fn] rlm@34: (let [physics-manager (BulletAppState.)] rlm@67: (JmeSystem/setSystemDelegate (AurellemSystemDelegate.)) rlm@25: (doto rlm@25: (proxy [SimpleApplication ActionListener] [] rlm@25: (simpleInitApp rlm@25: [] rlm@25: (no-exceptions rlm@25: ;; allow AI entities as much time as they need to think. rlm@25: (.setTimer this (IsoTimer. 60)) rlm@25: (.setFrustumFar (.getCamera this) 300) rlm@25: ;; Create default key-map. rlm@25: (initialize-inputs this (.getInputManager this) (all-keys)) rlm@25: ;; Don't take control of the mouse rlm@25: (org.lwjgl.input.Mouse/setGrabbed false) rlm@25: ;; add all objects to the world rlm@25: (.attachChild (.getRootNode this) root-node) rlm@25: ;; enable physics rlm@25: ;; add a physics manager rlm@25: (.attach (.getStateManager this) physics-manager) rlm@25: (.setGravity (.getPhysicsSpace physics-manager) rlm@25: (Vector3f. 0 -9.81 0)) rlm@25: ;; go through every object and add it to the physics rlm@25: ;; manager if relevant. rlm@52: ;;(traverse (fn [geom] rlm@52: ;; (dorun rlm@52: ;; (for [n (range (.getNumControls geom))] rlm@52: ;; (do rlm@52: ;; (cortex.util/println-repl rlm@52: ;; "adding " (.getControl geom n)) rlm@52: ;; (.add (.getPhysicsSpace physics-manager) rlm@52: ;; (.getControl geom n)))))) rlm@52: ;; (.getRootNode this)) rlm@25: ;; call the supplied setup-fn rlm@52: ;; simpler ! rlm@52: (.addAll (.getPhysicsSpace physics-manager) root-node) rlm@25: (if setup-fn rlm@25: (setup-fn this)))) rlm@25: (simpleUpdate rlm@25: [tpf] rlm@25: (no-exceptions rlm@25: (update-fn this tpf))) rlm@25: (onAction rlm@25: [binding value tpf] rlm@25: ;; whenever a key is pressed, call the function returned rlm@25: ;; from key-map. rlm@25: (no-exceptions rlm@25: (if-let [react (key-map binding)] rlm@25: (react this value))))) rlm@25: ;; don't show a menu to change options. rlm@25: (.setShowSettings false) rlm@25: ;; continue running simulation even if the window has lost rlm@25: ;; focus. rlm@25: (.setPauseOnLostFocus false) rlm@25: (.setSettings *app-settings*)))) rlm@67: rlm@23: #+end_src rlm@23: rlm@23: rlm@25: =(world)= is the most important function here. It presents a more rlm@26: functional interface to the Application life-cycle, and all its rlm@26: arguments except =root-node= are plain immutable clojure data rlm@26: structures. This makes it easier to extend functionally by composing rlm@26: multiple functions together, and to add more keyboard-driven actions rlm@26: by combining clojure maps. rlm@24: rlm@24: rlm@24: rlm@24: * COMMENT code generation rlm@29: #+begin_src clojure :tangle ../src/cortex/world.clj :noweb yes rlm@25: <
> rlm@25: <> rlm@25: <> rlm@25: <> rlm@24: <> rlm@24: #+end_src