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 (ns hottub.core
(:require [hottub.slick :as slick] (:require [hottub.slick :as slick]
[hottub.gs :as gs] [hottub.gs :as gs]
[hottub.resource :as res])) [hottub.resource :as res]
[hottub.timeline :as tln]))
(defn -main [& args] (defn -main [& args]
(res/start-resource-expiry-thread) (res/start-resource-expiry-thread)
(slick/start-game "Test game" (slick/start-game "Test game"
{:state :game {:state :game
:gs (gs/with-gs gs/gs-empty :gs (gs/update-gs gs/gs-empty
(gs/add-index :name) (gs/add-index :name)
(gs/add-index :type) (gs/add-index :type)
(gs/set-entity (gs/gen-id) {:type :sprite :x 50 :y 50 (gs/set-entity (gs/gen-id) {:type :sprite :x 50 :y 50
:image "res/man.png" :name :man}))})) :image "res/man.png" :name :man}))}))
(defmethod slick/update-game :game [state input delta] (defmethod slick/update-game :game [state inputtln delta]
(let [gsnew (assoc state :gs (gs/update-gs (:gs state)
(gs/with-gs (:gs state)
(let [idman (first (gs/q :name :man)) (let [idman (first (gs/q :name :man))
man (gs/entity idman)] man (gs/entity idman)]
(loop [timeline (slick/input-timeline input)] (if-let [input (tln/timeline-last-value inputtln)]
(let [[ts events] (first timeline) (gs/set-entity idman (assoc man :x (:mousex input) :y (:mousey input))))))))
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)))
(defmethod slick/render-game :game [state graphics] (defmethod slick/render-game :game [state graphics]
(gs/with-gs (:gs state) (gs/with-gs (:gs state)
@ -42,5 +33,5 @@
(defmethod slick/eval-with-bindings :default [state game f] (defmethod slick/eval-with-bindings :default [state game f]
(binding [g game s state] (binding [g game s state]
(let [gsnew (gs/with-gs (:gs state) (f) (gs/get-gs))] (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 ; input
(defprotocol InputTimeline (defprotocol InputTimeline
(input-timeline [this]) (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 [] (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 (reify
InputTimeline InputTimeline
(input-timeline [this] @atln) (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 ControlledInputReciever
(inputEnded [this]) (inputEnded [this])
@ -93,22 +98,16 @@
(setInput [this input]) (setInput [this input])
KeyListener KeyListener
(keyPressed [this skey c] (add-key-event atln skey :pressed)) (keyPressed [this skey c] (key-event skey true))
(keyReleased [this skey c] (add-key-event atln skey :released)) (keyReleased [this skey c] (key-event skey false))
MouseListener MouseListener
(mouseClicked [this sbutton x y count] (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] (mouse-event newx newy))
(mouseDragged [this oldx oldy newx newy]) (mouseMoved [this oldx oldy newx newy] (mouse-event newx newy))
(mouseMoved [this oldx oldy newx newy] (add-event atln {:type :mouse (mousePressed [this sbutton x y] (mouse-event x y sbutton true))
:action :move (mouseReleased [this sbutton x y] (mouse-event x y sbutton false))
:x newx (mouseWheelMoved [this dy]))))
: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})))))
(def modified-namespaces (ns-tracker ["src" "test"])) (def modified-namespaces (ns-tracker ["src" "test"]))
@ -118,17 +117,16 @@
:init create :init create
:constructors {[String clojure.lang.PersistentArrayMap] [String]} :constructors {[String clojure.lang.PersistentArrayMap] [String]}
:state state :state state
:extends org.newdawn.slick.BasicGame :extends org.newdawn.slick.BasicGame)
:implements [hottub.slick.InputTimeline])
(defn game-create [title state] (defn game-create [title state]
[[title] [[title]
{:state (ref state) {:state (atom state)
:repl (repl/repl-create 9999) :repl (repl/repl-create 9999)
:container (atom nil) :container (atom nil)
:input (input-create)}]) :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-container [this] @(:container (.state this)))
(defn game-install-input [this] (defn game-install-input [this]
@ -149,34 +147,33 @@
(game-install-input this) (game-install-input this)
(repl/repl-start (repl/repl-start
(:repl (.state this)) (: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] (defn game-update [this container delta]
(doseq [ns-sym (modified-namespaces)] (doseq [ns-sym (modified-namespaces)]
(try (try
(require ns-sym :reload) (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))) (repl/repl-update (:repl (.state this)))
(dosync
(try (try
(if-let [statenew (update-game @(game-state-ref this) (:input (.state this)) delta)] (if-let [statenew (update-game @(game-state-atom this) (input-timeline-clear! (:input (.state this))) delta)]
(ref-set (game-state-ref this) statenew)) (reset! (game-state-atom this) statenew))
(catch Exception e (println "Couldn't update" (:state @(game-state-ref this)) (.getMessage e)))))) (catch Exception e
(println "Couldn't update" (:state @(game-state-atom this)))
(.printStackTrace e))))
(defn game-render [this container graphics] (defn game-render [this container graphics]
(try (try
(res/gc-expired-resources) (res/gc-expired-resources)
(render-game @(game-state-ref this) graphics) (render-game @(game-state-atom this) graphics)
(catch Exception e (prn "Couldn't render" (:state @(game-state-ref this)) (.getMessage e))))) (catch Exception e
(println "Couldn't render" (:state @(game-state-atom this)))
(.printStackTrace e))))
(defn game-setstate [this statenew] (defn game-setstate! [this statenew]
(dosync (ref-set (game-state-ref this) statenew))) (reset! (game-state-atom this) statenew))
(defn game-input_timeline [this]
(input-timeline (:input (.state this))))
(defn game-input_clear [this tsafter]
(input-clear (:input (.state this)) tsafter))
(defmethod update-game :default [state input delta]) (defmethod update-game :default [state input delta])
(defmethod render-game :default [state graphics]) (defmethod render-game :default [state graphics])