TheJach.com

Jach's personal blog

(Largely containing a mind-dump to myselves: past, present, and future)
Current favorite quote: "Supposedly smart people are weirdly ignorant of Bayes' Rule." William B Vogt, 2010

Clojure and jMonkeyEngine Tutorial 4 and 5

And here we go again with the next tutorials. I feel like I should start prefacing these with the following caveat: do not use my code in production unless you have considered alternative code. I've been playing around with these tutorials, last time I questionably used memfn when it wasn't really necessary. Today I'm lifting everything to the global (for the namespace) scope because that makes dynamic manipulation in the REPL very easy.

There was an interesting piece of prose in the very first tutorial which I think bears repeating:

When developing a game application, you want to:
  • Initialize the game scene
  • Trigger game actions
  • Respond to user input.



This is in essence what all video games boil down to. You put some stuff on the screen that does stuff and the player interacts with it in a way that changes stuff. I think it's insightful to keep this broad view in mind and not get too bogged down in the intricacies of your collision system, physics engine, graphics pipeline, AI, or what have you.

Anyway, the code for Tutorial 4:


(ns jme_clj.core
(:import [com.jme3 app.SimpleApplication
material.Material
math.Vector3f
math.ColorRGBA
scene.Geometry
scene.shape.Box

system.AppSettings
system.JmeSystem
]))

(def desktop-cfg (.getResource (.getContextClassLoader (Thread/currentThread))
"com/jme3/asset/Desktop.cfg"))

(def assetManager (JmeSystem/newAssetManager desktop-cfg))

(def ^:dynamic *app-settings* (doto (AppSettings. true)
(.setFullscreen false)
(.setTitle "jme_clj")))

(def b (Box. Vector3f/ZERO 1 1 1))
(def player (Geometry. "blue cube" b))
(def mat (Material. assetManager "Common/MatDefs/Misc/Unshaded.j3md"))
(.setColor mat "Color" ColorRGBA/Blue)
(.setMaterial player mat)

(defn update [tpf]
(.rotate player 0 (* 2 tpf) 0))

(def app (proxy [SimpleApplication] []
(simpleInitApp []
(org.lwjgl.input.Mouse/setGrabbed false)
(.attachChild (.getRootNode this) player))

(simpleUpdate [tpf]
(update tpf))))

(defn -main [& args]
(doto app
(.setShowSettings false)
(.setPauseOnLostFocus false)
(.setSettings *app-settings*)
(.start)))


If you remember my video from one of the previous tutorials, setting the module up this way allows for on-the-fly recoding. Since the true update function is called outside the proxy, in your text editor you can, for instance, change the 2 to a negative 2, and in the REPL enter the new definition for update, and the running game you started earlier by calling -main from the REPL will automatically start using the new function and the cube will rotate in the opposite direction. And as usual you can call arbitrary functions on those objects and watch them update in real-time. I made my cube green. Then I added a simple back and forth movement in the update to watch it oscillate. The world is yours. The tutorial suggests these exercises:

1. What happens if you give the rotate() method negative numbers?
2. Can you create two Geometries next to each other, and make one rotate twice as fast as the other? (use the tpf variable)
3. Can you make a cube that pulsates? (grows and shrinks)
4. Can you make a cube that changes color? (change and set the Material)
5. Can you make a rolling cube? (rotate around the x axis, and translate along the z axis)

These exercises are trivial to do and very fast to do as well because there's no recompile/relaunch step. It's quite fun actually.

The important new concept here is the tpf variable that gets passed into the update function. It stands for "time per frame". It's calculated by JME and allows you to easily make sure certain code runs at the same speed regardless of the true frame rate, an important tool in a game making use of the GPU where the frame rate can be well over 60 fps. How does this variable accomplish the goal?

Suppose our frame rate is 1000 fps. This means the update function gets called 1000 times every second. This corresponds to 2000 "units" of rotation per second, which is very fast. Now suppose we run the same game on another machine that only gets 60 fps. This corresponds to only 120 "units" of rotation per second, which is far slower. What if we really want it to be 2 "units" of rotation per second with a smooth transition that gets smoother if the computer is faster? Multiply by "tps". For the adequate machine that gets 1000 fps, the real time between successive frames is 1/1000 of a second. The multiplication by 2 guarantees that after 1000 frames the cube will be 2 units rotated away from where it started, and it gets there in 1000 sub-steps. Meanwhile the lower end computer has 1/60 of a second between each frame, so it gets to the same position (2 units rotated away from where it started) in 60 sub-steps. An even crappier machine that only gets 2 frames per second will get there in 2 steps, and the rotation will look very blocky. But it will be correct. In Clojure there's some potential to even macro-away this bookkeeping.

Tutorial 5 brings us to an interesting problem. User input, which until now was all handled by the engine, and state management. As you know, Clojure is a functional language and discourages state. Yet here in the example we have a "isRunning" variable that denotes whether the game is paused or not. It's a piece of state we just have to live with, but that's the nature of video games. Is there a way we could factor the state out if we really wanted to? Probably, but I don't think it would be very elegant. So I'll handle that bit of state with an atom.

Here's my code. It's fairly different from the tutorial, but it highlights both the annoyances of interfacing with certain Java code from Clojure but also the ease with which Clojure makes those annoyances disappear. I also cut out the additional handler for mouse controls to simplify my macro, but it shouldn't be too hard to make an even cooler macro that handles any amount of keys and mouse buttons by checking whether the binding identifier begins with KEY or BUTTON. (And remember this check happens at compile time, no efficiency is lost at run time.)


(ns jme_clj.core
(:import [com.jme3 app.SimpleApplication
material.Material
math.Vector3f
math.ColorRGBA
scene.Geometry
scene.shape.Box

input.KeyInput
input.controls.ActionListener
input.controls.AnalogListener
input.controls.KeyTrigger

system.AppSettings
system.JmeSystem
]))

(def desktop-cfg (.getResource (.getContextClassLoader (Thread/currentThread))
"com/jme3/asset/Desktop.cfg"))

(def assetManager (JmeSystem/newAssetManager desktop-cfg))

(def ^:dynamic *app-settings* (doto (AppSettings. true)
(.setFullscreen false)
(.setTitle "jme_clj")))

(def speed 5)
(def is-running? (atom true))

(def b (Box. Vector3f/ZERO 1 1 1))
(def player (Geometry. "blue cube" b))
(def mat (Material. assetManager "Common/MatDefs/Misc/Unshaded.j3md"))
(.setColor mat "Color" ColorRGBA/Blue)
(.setMaterial player mat)

(def action-listener (proxy [ActionListener] []
(onAction [name key-pressed tpf]
(if (and (= name "Pause") (not key-pressed))
(do (swap! is-running? not) (println "State of is-running?:" @is-running?))))))

(def analog-listener (proxy [AnalogListener] []
(onAnalog [name value tpf]
(if @is-running?
(cond
(= name "Rotate") (.rotate player 0 (* value speed) 0)
(= name "Right") (let [v (.getLocalTranslation player)]
(.setLocalTranslation player (+ (.x v) (* value speed))
(.y v) (.z v)))
(= name "Left") (let [v (.getLocalTranslation player)]
(.setLocalTranslation player (- (.x v) (* value speed))
(.y v) (.z v))))))))

(defmacro key-map [manager name key]
`(.addMapping ~manager ~name (into-array KeyTrigger [(KeyTrigger. (. KeyInput ~key))])))

(defmacro add-listener [manager listener & names]
`(.addListener ~manager ~listener (into-array String '~names)))

(defn init-keys [input-manager]
(doto input-manager
(key-map "Pause" KEY_P)
(key-map "Left" KEY_H)
(key-map "Right" KEY_L)
(key-map "Rotate" KEY_SPACE)
(add-listener action-listener "Pause")
(add-listener analog-listener "Left" "Right" "Rotate")))

(def app (proxy [SimpleApplication] []
(simpleInitApp []
(org.lwjgl.input.Mouse/setGrabbed false)
(.attachChild (.getRootNode this) player)
(init-keys (.getInputManager this)))))

(defn -main [& args]
(doto app
(.setShowSettings false)
(.setPauseOnLostFocus false)
(.setSettings *app-settings*)
(.start)))


The new concept here is the difference in analog and digital input handlers. Analog handlers are for gradually repeating events, and you use them when you want to do something based on whether a key is being continuously pressed rather than toggled. Digital "action" handlers on the other hand are regular toggles on/off, so discrete events like pausing a game would fall under that category. In PyGame, analog would be accessed by getting a list of currently pressed keys, whereas the 'digital' events are accessed in the standard pygame.events.get() loop. The analog events also send a "value" to the handler, which is a measure of its strength, and it's also helpfully pre-multiplied by the tps variable. For a keyboard the "value" will be consistent, but for say a joystick it might range from 0 to 255 depending on how far in some direction it is. It's a nifty feature of the engine.

I want to now draw your attention to the two macros, key-map and add-listener. While programming this I discovered that the input manager's addMapping method is actually variadic. I thought this was strange at first glance, but a second later it made sense. A lot of games have alternate controls for the same action, and so this eases the programming of those games marginally.

Anyway, because of the method declaration, one cannot simply pass a KeyTrigger object by itself from the Clojure end like you can from the Java end. It has to be in an array, and furthermore, it has to be in a Java array rather than a Clojure vector or list. Fortunately Clojure's into-array function takes care of that nicely, and thanks to the macro I don't have to type it in every instance. Similarly with the addListeners method, which is also variadic but the tutorial example indicates it's a regular array--nevertheless Clojure must still cast into an array of Strings.

Additionally I saved myself from an annoying amount of typing by putting the busywork cruft inside the macro. Because really, what's most important in key binding? Associating an action-string (if I didn't have to interface with the game engine, it could be anything, not just a string) with a key on the keyboard. That is the pure data involved. The actual representation of that data in-memory, i.e. with the engine's specific objects, is a secondary concern the programmer shouldn't have to spell out every time. Combined with doto, the savings in typing and increase in readability become even more. This is why I love Clojure.

Back to 3


Posted on 2012-09-11 by Jach

Tags: clojure, jMonkeyEngine, programming

Permalink: https://www.thejach.com/view/id/265

Trackback URL: https://www.thejach.com/view/2012/9/clojure_and_jmonkeyengine_tutorial_4_and_5

Back to the top

Back to the first comment

Comment using the form below

(Only if you want to be notified of further responses, never displayed.)

Your Comment:

LaTeX allowed in comments, use $$\$\$...\$\$$$ to wrap inline and $$[math]...[/math]$$ to wrap blocks.