Implement new input event model.

Input events are now represented as maps containing the complete current state of the user's input devices.
The update method is called with a timeline of all input events since the last call to update.
This neatly solves the "stuck key" problem by making state transitions implicit.
It also solves the problem of different game states (ie. in-game and paused with menu) dealing with input differently.
If part of a game cares about keeping track of all of its input history, and part doesn't, they don't have to fight or know about each other.
This commit is contained in:
Jeremy Penner 2013-03-18 22:15:25 -04:00
parent eaedaee31a
commit 1e2de1dd99
2 changed files with 55 additions and 67 deletions

View file

@ -1,34 +1,25 @@
(ns hottub.core
(:require [hottub.slick :as slick]
[hottub.gs :as gs]
[hottub.resource :as res]))
[hottub.resource :as res]
[hottub.timeline :as tln]))
(defn -main [& args]
(res/start-resource-expiry-thread)
(slick/start-game "Test game"
{:state :game
:gs (gs/with-gs gs/gs-empty
:gs (gs/update-gs gs/gs-empty
(gs/add-index :name)
(gs/add-index :type)
(gs/set-entity (gs/gen-id) {:type :sprite :x 50 :y 50
:image "res/man.png" :name :man}))}))
(defmethod slick/update-game :game [state input delta]
(let [gsnew
(gs/with-gs (:gs state)
(defmethod slick/update-game :game [state inputtln delta]
(assoc state :gs (gs/update-gs (:gs state)
(let [idman (first (gs/q :name :man))
man (gs/entity idman)]
(loop [timeline (slick/input-timeline input)]
(let [[ts events] (first timeline)
timelinenext (next timeline)]
(doseq [{:keys [type action x y]} events]
(if (and (= type :mouse) (= action :move))
(gs/set-entity idman (assoc man :x x :y y))))
(if timelinenext
(recur timelinenext)
(slick/input-clear input ts)))))
(gs/get-gs))]
(assoc state :gs gsnew)))
(if-let [input (tln/timeline-last-value inputtln)]
(gs/set-entity idman (assoc man :x (:mousex input) :y (:mousey input))))))))
(defmethod slick/render-game :game [state graphics]
(gs/with-gs (:gs state)
@ -42,5 +33,5 @@
(defmethod slick/eval-with-bindings :default [state game f]
(binding [g game s state]
(let [gsnew (gs/with-gs (:gs state) (f) (gs/get-gs))]
(slick/game-setstate game (assoc state :gs gsnew)))))
(slick/game-setstate! game (assoc state :gs gsnew)))))

View file

@ -66,25 +66,30 @@
; input
(defprotocol InputTimeline
(input-timeline [this])
(input-clear [this tsfirst]))
(input-timeline-clear! [this]))
(defn- add-event [atln event]
(swap! atln tln/timeline-insert (u/timestamp) event))
(defn- add-key-event [atln skey action]
(if-let [key (get slick-key-to-key skey)]
(add-event atln {:type :key :action action :key key})))
(defn- add-mousebutton-event [atln sbutton action x y]
(if-let [button (get slick-mouse-to-mouse sbutton)]
(add-event atln {:type :mouse :action action :button button :x x :y y})))
(defn input-create []
(let [atln (atom (timeline-create))]
(let [atln (atom (timeline-create))
ainput (atom {:keys #{} :mousebuttons #{} :mousex 0 :mousey 0})
add-input (fn [& swapargs]
(let [input (apply swap! ainput swapargs)]
(swap! atln tln/timeline-insert (u/timestamp) input)))
key-event (fn [skey pressed]
(if-let [key (get slick-key-to-key skey)]
(add-input update-in [:keys] (if pressed conj disj) key)))
mouse-event (fn [x y & [smouse pressed]]
(if-not x
(println x y smouse pressed))
(add-input (fn [input]
(let [inputxy (assoc input :mousex x :mousey y)]
(if-let [mouse (get slick-mouse-to-mouse smouse)]
(update-in inputxy [:mousebuttons] (if pressed conj disj) mouse)
inputxy)))))]
(reify
InputTimeline
(input-timeline [this] @atln)
(input-clear [this tsafter] (swap! atln timeline-after tsafter))
(input-timeline-clear! [this] (u/reset-returning-old! atln (timeline-create)))
ControlledInputReciever
(inputEnded [this])
@ -93,22 +98,16 @@
(setInput [this input])
KeyListener
(keyPressed [this skey c] (add-key-event atln skey :pressed))
(keyReleased [this skey c] (add-key-event atln skey :released))
(keyPressed [this skey c] (key-event skey true))
(keyReleased [this skey c] (key-event skey false))
MouseListener
(mouseClicked [this sbutton x y count]
(add-mousebutton-event atln sbutton (if (= count 2) :double-click :click) x y))
(mouseDragged [this oldx oldy newx newy])
(mouseMoved [this oldx oldy newx newy] (add-event atln {:type :mouse
:action :move
:x newx
:y newy
:dx (- oldx newx)
:dy (- oldy newy)}))
(mousePressed [this sbutton x y] (add-mousebutton-event atln sbutton :down x y))
(mouseReleased [this sbutton x y] (add-mousebutton-event atln sbutton :up x y))
(mouseWheelMoved [this dy] (add-event atln {:type :mouse :action :wheel :distance dy})))))
(mouseClicked [this sbutton x y count])
(mouseDragged [this oldx oldy newx newy] (mouse-event newx newy))
(mouseMoved [this oldx oldy newx newy] (mouse-event newx newy))
(mousePressed [this sbutton x y] (mouse-event x y sbutton true))
(mouseReleased [this sbutton x y] (mouse-event x y sbutton false))
(mouseWheelMoved [this dy]))))
(def modified-namespaces (ns-tracker ["src" "test"]))
@ -118,17 +117,16 @@
:init create
:constructors {[String clojure.lang.PersistentArrayMap] [String]}
:state state
:extends org.newdawn.slick.BasicGame
:implements [hottub.slick.InputTimeline])
:extends org.newdawn.slick.BasicGame)
(defn game-create [title state]
[[title]
{:state (ref state)
{:state (atom state)
:repl (repl/repl-create 9999)
:container (atom nil)
:input (input-create)}])
(defn game-state-ref [this] (:state (.state this)))
(defn game-state-atom [this] (:state (.state this)))
(defn game-container [this] @(:container (.state this)))
(defn game-install-input [this]
@ -149,34 +147,33 @@
(game-install-input this)
(repl/repl-start
(:repl (.state this))
(fn [f] (eval-with-bindings @(game-state-ref this) this f))))
(fn [f] (eval-with-bindings @(game-state-atom this) this f))))
(defn game-update [this container delta]
(doseq [ns-sym (modified-namespaces)]
(try
(require ns-sym :reload)
(catch Exception e (println "Couldn't load" ns-sym (.getMessage e)))))
(catch Exception e
(println "Couldn't load" ns-sym)
(.printStackTrace e))))
(repl/repl-update (:repl (.state this)))
(dosync
(try
(if-let [statenew (update-game @(game-state-ref this) (:input (.state this)) delta)]
(ref-set (game-state-ref this) statenew))
(catch Exception e (println "Couldn't update" (:state @(game-state-ref this)) (.getMessage e))))))
(if-let [statenew (update-game @(game-state-atom this) (input-timeline-clear! (:input (.state this))) delta)]
(reset! (game-state-atom this) statenew))
(catch Exception e
(println "Couldn't update" (:state @(game-state-atom this)))
(.printStackTrace e))))
(defn game-render [this container graphics]
(try
(res/gc-expired-resources)
(render-game @(game-state-ref this) graphics)
(catch Exception e (prn "Couldn't render" (:state @(game-state-ref this)) (.getMessage e)))))
(render-game @(game-state-atom this) graphics)
(catch Exception e
(println "Couldn't render" (:state @(game-state-atom this)))
(.printStackTrace e))))
(defn game-setstate [this statenew]
(dosync (ref-set (game-state-ref this) statenew)))
(defn game-input_timeline [this]
(input-timeline (:input (.state this))))
(defn game-input_clear [this tsafter]
(input-clear (:input (.state this)) tsafter))
(defn game-setstate! [this statenew]
(reset! (game-state-atom this) statenew))
(defmethod update-game :default [state input delta])
(defmethod render-game :default [state graphics])