view org/world.org @ 26:bbffa41a12a9

moved apply-map to util.org from world.org, fixed some grammar problems, made examples more concise
author Robert McIntyre <rlm@mit.edu>
date Mon, 24 Oct 2011 05:41:50 -0700
parents 775d97247dd0
children 6372c108c5c6
line wrap: on
line source
1 #+title: A World for the Creatures
2 #+author: Robert McIntyre
3 #+email: rlm@mit.edu
4 #+description: Creating a Virtual World for AI constructs using clojure and JME3
5 #+keywords: JME3, clojure, virtual world, exception handling
6 #+SETUPFILE: ../../aurellem/org/setup.org
7 #+INCLUDE: ../../aurellem/org/level-0.org
8 #+babel: :mkdirp yes :noweb yes :exports both
10 * The World
12 There's no point in having senses if there's nothing to experience. In
13 this section I make some tools with which to build virtual worlds for
14 my characters to inhabit. If you look at the tutorials at [[http://www.jmonkeyengine.org/wiki/doku.php/jme3:beginner][the jme3
15 website]], you will see a pattern in how virtual worlds are normally
16 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 using
21 the inherited =assetManager= and call them by overriding the
22 =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 particular
33 application.
34 - Use an REPL -- this means that there's only ever one JVM, and
35 Applications come and go.
37 Since most development work using jMonkeyEngine is done in Java, jme3
38 supports "the Java way" quite well out of the box. To work "the
39 clojure way", it necessary to wrap the jme3 elements that deal with
40 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= and
46 =SimpleApplication= classes.
48 ** Header
49 #+srcname: header
50 #+begin_src clojure :results silent
51 (ns cortex.world
52 "World Creation, abstracion over jme3's input system, and REPL
53 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.jme3.math.Vector3f)
61 (:import com.jme3.scene.Node)
62 (:import com.jme3.system.AppSettings)
63 (:import com.jme3.system.JmeSystem)
64 (:import com.jme3.system.IsoTimer)
65 (:import com.jme3.input.KeyInput)
66 (:import com.jme3.input.controls.KeyTrigger)
67 (:import com.jme3.input.controls.MouseButtonTrigger)
68 (:import com.jme3.input.InputManager)
69 (:import com.jme3.bullet.BulletAppState)
70 (:import com.jme3.shadow.BasicShadowRenderer)
71 (:import com.jme3.app.SimpleApplication)
72 (:import com.jme3.input.controls.ActionListener)
73 (:import com.jme3.renderer.queue.RenderQueue$ShadowMode)
74 (:import org.lwjgl.input.Mouse))
75 #+end_src
77 ** General Settings
78 #+srcname: settings
79 #+begin_src clojure
80 (in-ns 'cortex.world)
82 (defvar *app-settings*
83 (doto (AppSettings. true)
84 (.setFullscreen false)
85 (.setTitle "Aurellem.")
86 ;; disable 32 bit stuff for now
87 ;;(.setAudioRenderer "Send")
88 )
89 "These settings control how the game is displayed on the screen for
90 debugging purposes. Use binding forms to change this if desired.
91 Full-screen mode does not work on some computers.")
93 (defn asset-manager
94 "returns a new, configured assetManager" []
95 (JmeSystem/newAssetManager
96 (.getResource
97 (.getContextClassLoader (Thread/currentThread))
98 "com/jme3/asset/Desktop.cfg")))
99 #+end_src
101 Normally, people just use the =AssetManager= inherited from
102 =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)= makes
105 object creation less tightly bound to Application initialization.
108 ** Exception Protection
109 #+srcname: exceptions
110 #+begin_src clojure
111 (in-ns 'cortex.world)
113 (defmacro no-exceptions
114 "Sweet relief like I never knew."
115 [& forms]
116 `(try ~@forms (catch Exception e# (.printStackTrace e#))))
118 (defn thread-exception-removal
119 "Exceptions thrown in the graphics rendering thread generally cause
120 the entire REPL to crash! It is good to suppress them while trying
121 things out to shorten the debug loop."
122 []
123 (.setUncaughtExceptionHandler
124 (Thread/currentThread)
125 (proxy [Thread$UncaughtExceptionHandler] []
126 (uncaughtException
127 [thread thrown]
128 (println "uncaught-exception thrown in " thread)
129 (println (.getMessage thrown))))))
131 #+end_src
133 Exceptions thrown in the LWJGL render thread, if not caught, will
134 destroy the entire JVM process including the REPL and slow development
135 to a crawl. It is better to try to continue on in the face of
136 exceptions and keep the REPL alive as long as possible. Normally it
137 is possible to just exit the faulty Application, fix the bug,
138 reevaluate the appropriate forms, and be on your way, without
139 restarting the JVM.
141 ** Input
142 #+srcname: input
143 #+begin_src clojure
144 (in-ns 'cortex.world)
146 (defn all-keys
147 "Uses reflection to generate a map of string names to jme3 trigger
148 objects, which govern input from the keyboard and mouse"
149 []
150 (let [inputs (pokemon.lpsolve/constant-map KeyInput)]
151 (assoc
152 (zipmap (map (fn [field]
153 (.toLowerCase (re-gsub #"_" "-" field))) (vals inputs))
154 (map (fn [val] (KeyTrigger. val)) (keys inputs)))
155 ;;explicitly add mouse controls
156 "mouse-left" (MouseButtonTrigger. 0)
157 "mouse-middle" (MouseButtonTrigger. 2)
158 "mouse-right" (MouseButtonTrigger. 1))))
160 (defn initialize-inputs
161 "Establish key-bindings for a particular virtual world."
162 [game input-manager key-map]
163 (doall
164 (map (fn [[name trigger]]
165 (.addMapping
166 ^InputManager input-manager
167 name (into-array (class trigger)
168 [trigger]))) key-map))
169 (doall
170 (map (fn [name]
171 (.addListener
172 ^InputManager input-manager game
173 (into-array String [name]))) (keys key-map))))
175 #+end_src
177 These functions are for controlling the world through the keyboard and
178 mouse.
180 I reuse =constant-map= from [[../../pokemon-types/html/lpsolve.html#sec-3-3-4][=pokemon.lpsolve=]] to get the numerical
181 values for all the keys defined in the =KeyInput= class. The
182 documentation for =constant-map= is:
184 #+begin_src clojure :results output
185 (doc pokemon.lpsolve/constant-map)
186 #+end_src
188 #+results:
189 : -------------------------
190 : pokemon.lpsolve/constant-map
191 : ([class])
192 : Takes a class and creates a map of the static constant integer
193 : fields with their names. This helps with C wrappers where they have
194 : just defined a bunch of integer constants instead of enums
196 #+begin_src clojure :exports both :results verbatim
197 (take 5 (vals (pokemon.lpsolve/constant-map KeyInput)))
198 #+end_src
200 #+results:
201 : ("KEY_ESCAPE" "KEY_1" "KEY_2" "KEY_3" "KEY_4")
203 =(all-keys)= converts the constant names like =KEY_J= to the more
204 clojure-like =key-j=, and returns a map from these keys to
205 jMonkeyEngine =KeyTrigger= objects, which jMonkeyEngine3 uses as it's
206 abstraction over the physical keys. =all-keys= also adds the three
207 mouse button controls to the map.
209 #+begin_src clojure :exports both :results output
210 (require 'clojure.contrib.pprint)
211 (clojure.contrib.pprint/pprint
212 (take 6 (cortex.world/all-keys)))
213 #+end_src
215 #+results:
216 : (["key-n" #<KeyTrigger com.jme3.input.controls.KeyTrigger@9f9fec0>]
217 : ["key-apps" #<KeyTrigger com.jme3.input.controls.KeyTrigger@28edbe7f>]
218 : ["key-pgup" #<KeyTrigger com.jme3.input.controls.KeyTrigger@647fd33a>]
219 : ["key-f8" #<KeyTrigger com.jme3.input.controls.KeyTrigger@24f97188>]
220 : ["key-o" #<KeyTrigger com.jme3.input.controls.KeyTrigger@685c53ff>]
221 : ["key-at" #<KeyTrigger com.jme3.input.controls.KeyTrigger@4c3e2e5f>])
224 ** World Creation
225 #+srcname: world
226 #+begin_src clojure :results silent
227 (in-ns 'cortex.world)
229 (defn no-op
230 "Takes any number of arguments and does nothing."
231 [& _])
233 (defn traverse
234 "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 world
241 "the =world= function takes care of the details of initializing a
242 SimpleApplication.
244 ***** Arguments:
246 - root-node : a com.jme3.scene.Node object which contains all of
247 the objects that should be in the simulation.
249 - key-map : a map from strings describing keys to functions that
250 should be executed whenever that key is pressed.
251 the functions should take a SimpleApplication object and a
252 boolean value. The SimpleApplication is the current simulation
253 that is running, and the boolean is true if the key is being
254 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 a
259 message whenever the 'j' key on the keyboard is pressed.
261 - setup-fn : a function that takes a SimpleApplication object. It
262 is called once when initializing the simulation. Use it to
263 create things like lights, change the gravity, initialize debug
264 nodes, etc.
266 - update-fn : this function takes a SimpleApplication object and a
267 float and is called every frame of the simulation. The float
268 tells how many seconds is has been since the last frame was
269 rendered, according to whatever clock jme is currently
270 using. The default is to use IsoTimer which will result in this
271 value always being the same.
272 "
273 [root-node key-map setup-fn update-fn]
274 (let [physics-manager (BulletAppState.)
275 shadow-renderer (BasicShadowRenderer.
276 (asset-manager) (int 256))]
277 (doto
278 (proxy [SimpleApplication ActionListener] []
279 (simpleInitApp
280 []
281 (no-exceptions
282 ;; allow AI entities as much time as they need to think.
283 (.setTimer this (IsoTimer. 60))
284 (.setFrustumFar (.getCamera this) 300)
285 ;; Create default key-map.
286 (initialize-inputs this (.getInputManager this) (all-keys))
287 ;; Don't take control of the mouse
288 (org.lwjgl.input.Mouse/setGrabbed false)
289 ;; add all objects to the world
290 (.attachChild (.getRootNode this) root-node)
291 ;; enable physics
292 ;; add a physics manager
293 (.attach (.getStateManager this) physics-manager)
294 (.setGravity (.getPhysicsSpace physics-manager)
295 (Vector3f. 0 -9.81 0))
296 ;; go through every object and add it to the physics
297 ;; manager if relevant.
298 (traverse (fn [geom]
299 (dorun
300 (for [n (range (.getNumControls geom))]
301 (do
302 (.add (.getPhysicsSpace physics-manager)
303 (.getControl geom n))))))
304 (.getRootNode this))
305 ;;(.addAll (.getPhysicsSpace physics-manager) (.getRootNode this))
307 ;; set some basic defaults for the shadow renderer.
308 ;; these can be undone in the setup function
309 (.setDirection shadow-renderer
310 (.normalizeLocal (Vector3f. -1 -1 -1)))
311 (.addProcessor (.getViewPort this) shadow-renderer)
312 (.setShadowMode (.getRootNode this)
313 RenderQueue$ShadowMode/Off)
314 ;; call the supplied setup-fn
315 (if setup-fn
316 (setup-fn this))))
317 (simpleUpdate
318 [tpf]
319 (no-exceptions
320 (update-fn this tpf)))
321 (onAction
322 [binding value tpf]
323 ;; whenever a key is pressed, call the function returned
324 ;; from key-map.
325 (no-exceptions
326 (if-let [react (key-map binding)]
327 (react this value)))))
328 ;; don't show a menu to change options.
329 (.setShowSettings false)
330 ;; continue running simulation even if the window has lost
331 ;; focus.
332 (.setPauseOnLostFocus false)
333 (.setSettings *app-settings*))))
334 #+end_src
337 =(world)= is the most important function here. It presents a more
338 functional interface to the Application life-cycle, and all its
339 arguments except =root-node= are plain immutable clojure data
340 structures. This makes it easier to extend functionally by composing
341 multiple functions together, and to add more keyboard-driven actions
342 by combining clojure maps.
346 * COMMENT code generation
347 #+begin_src clojure :tangle ../src/cortex/world.clj
348 <<header>>
349 <<settings>>
350 <<exceptions>>
351 <<input>>
352 <<world>>
353 #+end_src