Files
play-clj/src/play_clj/core.clj
Remington Covert 4120eed58a Fixes memory leak and input issue.
The input issue is caused by the fact that the previous screen is
still rendered, even after .setScreen is called. Executing on
the gl thread fixes this.

The documentation says that old screens need to be disposed manually.
This instantly freed up a lot of memory when running my game.
2015-03-07 08:01:32 -08:00

575 lines
27 KiB
Clojure

(ns play-clj.core
(:require [clojure.set]
[play-clj.entities :as e]
[play-clj.math :as m]
[play-clj.utils :as u])
(:import [com.badlogic.gdx Application Audio Files Game Gdx Graphics Input
InputMultiplexer InputProcessor Net Preferences Screen]
[com.badlogic.gdx.audio Sound Music]
[com.badlogic.gdx.assets AssetManager]
[com.badlogic.gdx.assets.loaders AsynchronousAssetLoader
ParticleEffectLoader]
[com.badlogic.gdx.assets.loaders.resolvers
InternalFileHandleResolver]
[com.badlogic.gdx.files FileHandle]
[com.badlogic.gdx.graphics Camera Color GL20 OrthographicCamera
PerspectiveCamera Pixmap Pixmap$Format PixmapIO Texture
VertexAttributes$Usage]
[com.badlogic.gdx.graphics.g2d Batch ParticleEffect]
[com.badlogic.gdx.graphics.g3d ModelBatch]
[com.badlogic.gdx.graphics.glutils ShapeRenderer]
[com.badlogic.gdx.input GestureDetector
GestureDetector$GestureListener]
[com.badlogic.gdx.maps MapLayer MapLayers MapObject MapObjects
MapProperties]
[com.badlogic.gdx.maps.tiled TiledMap TiledMapTileLayer
TiledMapTileLayer$Cell TmxMapLoader]
[com.badlogic.gdx.maps.tiled.renderers
BatchTiledMapRenderer
HexagonalTiledMapRenderer
IsometricStaggeredTiledMapRenderer
IsometricTiledMapRenderer
OrthogonalTiledMapRenderer]
[com.badlogic.gdx.math Vector2 Vector3]
[com.badlogic.gdx.scenes.scene2d Actor Stage]
[com.badlogic.gdx.scenes.scene2d.utils ActorGestureListener
ChangeListener ClickListener DragListener FocusListener]
[com.badlogic.gdx.utils ScreenUtils Timer$Task]
[play_clj.entities BundleEntity ShapeEntity]))
(load "core_basics")
(load "core_cameras")
(load "core_graphics")
(load "core_listeners")
(load "core_utils")
(defn ^:private normalize
[entities]
(some->> entities
list
flatten
(remove nil?)
vec))
(defn ^:private wrapper
[screen-atom screen-fn]
(screen-fn))
(defn ^:private reset-changed!
[e-atom e-old e-new]
(when (and (not= e-old e-new)
(compare-and-set! e-atom e-old e-new))
e-new))
(defn ^:private add-to-timeline!
[screen-atom entities]
(let [screen @screen-atom]
(when (:timeline screen)
(swap! screen-atom
update-in
[:timeline]
conj
[(:total-time screen) entities]))))
(defn defscreen*
[screen entities
{:keys [on-show on-render on-hide on-pause on-resize on-resume on-timer]
:as options}]
(let [execute-fn! (fn [func & {:keys [] :as options}]
(when func
(let [screen-map (merge @screen options)
old-entities @entities]
(some->> (with-meta
#(normalize (func screen-map old-entities))
(meta func))
(wrapper screen)
(reset-changed! entities old-entities)
(update-screen! @screen)))))
execute-fn-on-gl! (fn [& args]
(on-gl (apply execute-fn! args)))
update-fn! (fn [func & args]
(apply swap! screen func args))]
{:screen screen
:entities entities
:execute-fn! execute-fn!
:execute-fn-on-gl! execute-fn-on-gl!
:update-fn! update-fn!
:options options
:show (fn []
; if using a physics engine in a REPL, we need to forcibly dispose
; the world so it is cleaned up properly
(some-> @screen :world :object .dispose)
; set the initial values in the screen map
(update-fn! assoc
:execute-fn! execute-fn!
:execute-fn-on-gl! execute-fn-on-gl!
:update-fn! update-fn!
:options options
:on-timer on-timer
:layers nil
:input-listeners (input-listeners options execute-fn!)
:ui-listeners (ui-listeners options execute-fn!))
; run :on-show
(execute-fn! on-show)
; update the physics contact listener if a :world was created
(some->> (contact-listener @screen options execute-fn!)
(update-fn! assoc :contact-listener)
update-screen!))
:render (fn [d]
(swap! screen update-in [:total-time] #(+ (or %1 0) %2) d)
(some->> (execute-fn! on-render :delta-time d)
(add-to-timeline! screen)))
:hide #(execute-fn! on-hide)
:pause #(execute-fn! on-pause)
:resize (fn [w h]
(execute-fn! on-resize :width w :height h)
(update-screen! @screen))
:resume #(execute-fn! on-resume)}))
(defmacro defscreen
"Defines a screen, and creates vars for all the functions inside of it. All
functions take a screen map and entities vector as arguments, and return the
entities list at the end with any desired changes. If a function returns nil,
the entities list is not changed.
Below are all the possible screen functions. Some of them get special arguments
via the screen map.
; main screen functions
(defscreen my-screen
; the screen first shows
:on-show
(fn [screen entities]
entities)
; the screen must be rendered (many times per second)
:on-render
(fn [screen entities]
(println (:delta-time screen)) ; time (ms) elapsed since last frame
(println (:total-time screen)) ; time (ms) elapsed since first :on-show
entities)
; the screen was replaced
:on-hide
(fn [screen entities]
entities)
; the screen was resized
:on-resize
(fn [screen entities]
(println (:width screen)) ; the new width of the screen
(println (:height screen)) ; the new height of the screen
entities)
; the screen resumed from a paused state
:on-resume
(fn [screen entities]
entities)
; the screen paused
:on-pause
(fn [screen entities]
entities)
; a timer created with add-timer! executed
:on-timer
(fn [screen entities]
(println (:id screen)) ; the id supplied when the timer was created
entities))
; input functions
; Tip: convert :input-x and :input-y to screen coordinates with input->screen,
; or just use (game :x) and (game :y) instead
(defscreen my-screen
; a key was pressed
:on-key-down
(fn [screen entities]
(println (:key screen)) ; the key that was pressed (see key-code)
entities)
; a key was typed
:on-key-typed
(fn [screen entities]
(println (:character screen)) ; the character that was pressed
entities)
; a key was released
:on-key-up
(fn [screen entities]
(println (:key screen)) ; the key that was released (see key-code)
entities)
; the mouse was moved without pressing any buttons
:on-mouse-moved
(fn [screen entities]
(println (:input-x screen)) ; the x position of the mouse
(println (:input-y screen)) ; the y position of the mouse
entities)
; the mouse wheel was scrolled
:on-scrolled
(fn [screen entities]
(println (:amount screen)) ; the amount scrolled
entities)
; the screen was touched or a mouse button was pressed
:on-touch-down
(fn [screen entities]
(println (:input-x screen)) ; the x position of the finger/mouse
(println (:input-y screen)) ; the y position of the finger/mouse
(println (:pointer screen)) ; the pointer for the event
(println (:button screen)) ; the mouse button that was pressed (see button-code)
entities)
; a finger or the mouse was dragged
:on-touch-dragged
(fn [screen entities]
(println (:input-x screen)) ; the x position of the finger/mouse
(println (:input-y screen)) ; the y position of the finger/mouse
(println (:pointer screen)) ; the pointer for the event
entities)
; a finger was lifted or a mouse button was released
:on-touch-up
(fn [screen entities]
(println (:input-x screen)) ; the x position of the finger/mouse
(println (:input-y screen)) ; the y position of the finger/mouse
(println (:pointer screen)) ; the pointer for the event
(println (:button screen)) ; the mouse button that was released (see button-code)
entities))
; gesture functions
(defscreen my-screen
; the user dragged over the screen and lifted
:on-fling
(fn [screen entities]
(println (:velocity-x screen)) ; the x-axis velocity (s)
(println (:velocity-y screen)) ; the y-axis velocity (s)
(println (:button screen)) ; the mouse button that was pressed (see button-code)
entities)
; the user pressed for a long time
:on-long-press
(fn [screen entities]
(println (:input-x screen)) ; the x position of the finger/mouse
(println (:input-y screen)) ; the y position of the finger/mouse
entities)
; the user dragged a finger over the screen
:on-pan
(fn [screen entities]
(println (:input-x screen)) ; the x position of the finger/mouse
(println (:input-y screen)) ; the y position of the finger/mouse
(println (:delta-x screen)) ; the x-axis distance moved
(println (:delta-y screen)) ; the y-axis distance moved
entities)
; the user is no longer panning
:on-pan-stop
(fn [screen entities]
(println (:input-x screen)) ; the x position of the finger/mouse
(println (:input-y screen)) ; the y position of the finger/mouse
(println (:pointer screen)) ; the pointer for the event
(println (:button screen)) ; the mouse button that was pressed (see button-code)
entities)
; the user performed a pinch zoom gesture
:on-pinch
(fn [screen entities]
(println (:initial-pointer-1 screen)) ; the start position of finger 1 (see the x and y functions)
(println (:initial-pointer-2 screen)) ; the start position of finger 2 (see the x and y functions)
(println (:pointer-1 screen)) ; the end position of finger 1 (see the x and y functions)
(println (:pointer-2 screen)) ; the end position of finger 2 (see the x and y functions)
entities)
; the user tapped
:on-tap
(fn [screen entities]
(println (:input-x screen)) ; the x position of the finger/mouse
(println (:input-y screen)) ; the y position of the finger/mouse
(println (:count screen)) ; the number of taps
(println (:button screen)) ; the mouse button that was pressed (see button-code)
entities)
; the user performed a pinch zoom gesture
:on-zoom
(fn [screen entities]
(println (:initial-distance screen)) ; the start distance between fingers
(println (:distance screen)) ; the end distance between fingers
entities))
; 2D physics contact (for play-clj.g2d-physics)
; Tip: use first-entity and second-entity to get the entities that are contacting
(defscreen my-screen
; two bodies began to touch
:on-begin-contact
(fn [screen entities]
(println (:contact screen)) ; the Contact - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/physics/box2d/Contact.html
entities)
; two bodies ceased to touch
:on-end-contact
(fn [screen entities]
(println (:contact screen)) ; the Contact - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/physics/box2d/Contact.html
entities)
; called between each use of `step!` before the collision is processed
:on-pre-solve
(fn [screen entities]
(println (:contact screen)) ; the Contact - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/physics/box2d/Contact.html
(println (:impulse screen)) ; the ContactImpulse - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/physics/box2d/ContactImpulse.html
entities)
; called between each use of `step!` after the collision is processed
:on-post-solve
(fn [screen entities]
(println (:contact screen)) ; the Contact - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/physics/box2d/Contact.html
(println (:old-manifold screen)) ; the Manifold - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/physics/box2d/Manifold.html
entities))
; 3D physics contact (for play-clj.g3d-physics)
; Tip: use first-entity and second-entity to get the entities that are contacting
(defscreen my-screen
; two bodies began to touch
:on-begin-contact
(fn [screen entities]
(println (:first-body screen)) ; the first btCollisionObject - http://bulletphysics.org/Bullet/BulletFull/classbtCollisionObject.html
(println (:second-body screen)) ; the second btCollisionObject - http://bulletphysics.org/Bullet/BulletFull/classbtCollisionObject.html
entities)
; two bodies ceased to touch
:on-end-contact
(fn [screen entities]
(println (:first-body screen)) ; the first btCollisionObject - http://bulletphysics.org/Bullet/BulletFull/classbtCollisionObject.html
(println (:second-body screen)) ; the second btCollisionObject - http://bulletphysics.org/Bullet/BulletFull/classbtCollisionObject.html
entities))
; ui input functions (for play-clj.ui)
(defscreen my-screen
; the ui entity was clicked or changed
:on-ui-changed
(fn [screen entities]
(println (:event screen)) ; the ChangeListener.ChangeEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/utils/ChangeListener.ChangeEvent.html
(println (:actor screen)) ; the Actor - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/Actor.html
entities)
; the finger/mouse moved over the ui entity
:on-ui-enter
(fn [screen entities]
(println (:event screen)) ; the InputEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/InputEvent.html
(println (:actor screen)) ; the Actor - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/Actor.html
(println (:input-x screen)) ; the x position of the finger/mouse
(println (:input-y screen)) ; the y position of the finger/mouse
(println (:pointer screen)) ; the pointer for the event
entities)
; the finger/mouse moved out of the ui entity
:on-ui-exit
(fn [screen entities]
(println (:event screen)) ; the InputEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/InputEvent.html
(println (:actor screen)) ; the Actor - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/Actor.html
(println (:input-x screen)) ; the x position of the finger/mouse
(println (:input-y screen)) ; the y position of the finger/mouse
(println (:pointer screen)) ; the pointer for the event
entities)
; the finger/mouse went down on the ui entity
:on-ui-touch-down
(fn [screen entities]
(println (:event screen)) ; the InputEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/InputEvent.html
(println (:input-x screen)) ; the x position of the finger/mouse
(println (:input-y screen)) ; the y position of the finger/mouse
(println (:pointer screen)) ; the pointer for the event
(println (:button screen)) ; the mouse button that was pressed (see button-code)
entities)
; the finger/mouse moved anywhere
:on-ui-touch-dragged
(fn [screen entities]
(println (:event screen)) ; the InputEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/InputEvent.html
(println (:input-x screen)) ; the x position of the finger/mouse
(println (:input-y screen)) ; the y position of the finger/mouse
(println (:pointer screen)) ; the pointer for the event
entities)
; the finger/mouse went up anywhere
:on-ui-touch-up
(fn [screen entities]
(println (:event screen)) ; the InputEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/InputEvent.html
(println (:input-x screen)) ; the x position of the finger/mouse
(println (:input-y screen)) ; the y position of the finger/mouse
(println (:pointer screen)) ; the pointer for the event
(println (:button screen)) ; the mouse button that was released (see button-code)
entities))
; ui drag functions (for play-clj.ui)
(defscreen my-screen
:on-ui-drag
(fn [screen entities]
(println (:event screen)) ; the InputEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/InputEvent.html
(println (:input-x screen)) ; the x position of the finger/mouse
(println (:input-y screen)) ; the y position of the finger/mouse
(println (:pointer screen)) ; the pointer for the event
entities)
:on-ui-drag-start
(fn [screen entities]
(println (:event screen)) ; the InputEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/InputEvent.html
(println (:input-x screen)) ; the x position of the finger/mouse
(println (:input-y screen)) ; the y position of the finger/mouse
(println (:pointer screen)) ; the pointer for the event
entities)
:on-ui-drag-stop
(fn [screen entities]
(println (:event screen)) ; the InputEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/InputEvent.html
(println (:input-x screen)) ; the x position of the finger/mouse
(println (:input-y screen)) ; the y position of the finger/mouse
(println (:pointer screen)) ; the pointer for the event
entities))
; ui focus functions (for play-clj.ui)
(defscreen my-screen
:on-ui-keyboard-focus-changed
(fn [screen entities]
(println (:event screen)) ; the FocusListener.FocusEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/utils/FocusListener.FocusEvent.html
(println (:actor screen)) ; the Actor - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/Actor.html
(println (:focused? screen)) ; whether it is focused
entities)
:on-ui-scroll-focus-changed
(fn [screen entities]
(println (:event screen)) ; the FocusListener.FocusEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/utils/FocusListener.FocusEvent.html
(println (:actor screen)) ; the Actor - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/Actor.html
(println (:focused? screen)) ; whether it is focused
entities))
; ui gesture functions (for play-clj.ui)
(defscreen my-screen
; the user dragged a finger over the screen and lifted it
:on-ui-fling
(fn [screen entities]
(println (:event screen)) ; the InputEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/InputEvent.html
(println (:velocity-x screen)) ; the x-axis velocity (s)
(println (:velocity-y screen)) ; the y-axis velocity (s)
(println (:button screen)) ; the mouse button that was pressed (see button-code)
entities)
; the user pressed
:on-ui-long-press
(fn [screen entities]
(println (:actor screen)) ; the Actor - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/Actor.html
(println (:input-x screen)) ; the x position of the finger
(println (:input-y screen)) ; the y position of the finger
entities)
; the user dragged a finger over the screen
:on-ui-pan
(fn [screen entities]
(println (:event screen)) ; the InputEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/InputEvent.html
(println (:input-x screen)) ; the x position of the finger
(println (:input-y screen)) ; the y position of the finger
(println (:delta-x screen)) ; the x-axis distance moved
(println (:delta-y screen)) ; the y-axis distance moved
entities)
; the user is no longer panning
:on-ui-pan-stop
(fn [screen entities]
(println (:event screen)) ; the InputEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/InputEvent.html
(println (:input-x screen)) ; the x position of the finger
(println (:input-y screen)) ; the y position of the finger
(println (:pointer screen)) ; the pointer for the event
(println (:button screen)) ; the mouse button that was pressed (see button-code)
entities)
; the user performed a pinch zoom gesture
:on-ui-pinch
(fn [screen entities]
(println (:event screen)) ; the InputEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/InputEvent.html
(println (:initial-pointer-1 screen)) ; the start position of finger 1 (see the x and y functions)
(println (:initial-pointer-2 screen)) ; the start position of finger 2 (see the x and y functions)
(println (:pointer-1 screen)) ; the end position of finger 1 (see the x and y functions)
(println (:pointer-2 screen)) ; the end position of finger 2 (see the x and y functions)
entities)
; the user tapped
:on-ui-tap
(fn [screen entities]
(println (:event screen)) ; the InputEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/InputEvent.html
(println (:input-x screen)) ; the x position of the finger
(println (:input-y screen)) ; the y position of the finger
(println (:count screen)) ; the number of taps
(println (:button screen)) ; the mouse button that was pressed (see button-code)
entities)
; the user performed a pinch zoom gesture
:on-ui-zoom
(fn [screen entities]
(println (:event screen)) ; the InputEvent - http://libgdx.badlogicgames.com/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/InputEvent.html
(println (:initial-distance screen)) ; the start distance between fingers
(println (:distance screen)) ; the end distance between fingers
entities))"
[n & options]
(let [s (format "Unexpected value in (defscreen %s). You need to give it
keywords and functions in pairs."
(str n))]
(assert (even? (count options)) s)
(assert (= 0 (count (remove keyword? (keys (apply hash-map options))))) s))
`(let [fn-syms# (->> (for [[k# v#] ~(apply hash-map options)]
[k# (intern *ns* (symbol (str '~n "-" (name k#))) v#)])
(into {}))
map-sym# (symbol (str '~n "-map"))
screen# (deref (or (resolve map-sym#)
(intern *ns* map-sym# (atom {}))))
entities-sym# (symbol (str '~n "-entities"))
entities# (deref (or (resolve entities-sym#)
(intern *ns* entities-sym# (atom []))))]
(def ~n (defscreen* screen# entities# fn-syms#))))
(defn defgame*
[{:keys [on-create]}]
(proxy [Game] []
(create []
(when on-create
(on-create this)))))
(defmacro defgame
"Defines a game. This should only be called once."
[n & {:keys [] :as options}]
`(defonce ~n (defgame* ~options)))
(defn set-screen!
"Creates and displays a screen for the `game-object`, using one or more
`screen-objects` in the order they were provided.
(set-screen! my-game main-screen text-screen)"
[^Game game-object & screen-objects]
(doseq [screen screen-objects]
(assert (every? #(fn? (get screen %))
[:show :render :hide :pause :resize :resume])
"Attempted to set an invalid screen."))
(let [run-fn! (fn [k & args]
(doseq [screen screen-objects]
(apply (get screen k) args)))]
(when-let [old-screen (.getScreen game-object)]
(.dispose old-screen))
(on-gl
(.setScreen game-object
(reify Screen
(show [this]
(input! :set-input-processor (InputMultiplexer.))
(run-fn! :show)
(doseq [{:keys [screen]} screen-objects]
(doseq [[_ listener] (:input-listeners @screen)]
(add-input! listener))))
(render [this d] (run-fn! :render d))
(hide [this] (run-fn! :hide))
(pause [this] (run-fn! :pause))
(resize [this w h] (run-fn! :resize w h))
(resume [this] (run-fn! :resume))
(dispose [this]))))))
(defn set-screen-wrapper!
"Sets a function that wraps around all screen functions, allowing you to
handle errors and perform other custom actions each time they run.
; default behavior
(set-screen-wrapper! (fn [screen-atom screen-fn]
(screen-fn)))
; if there is an error, print it out and switch to a blank screen
; (this is useful because it makes error recovery easier in a REPL)
(set-screen-wrapper! (fn [screen-atom screen-fn]
(try (screen-fn)
(catch Exception e
(.printStackTrace e)
(set-screen! my-game blank-screen)))))"
[wrapper-fn]
(intern 'play-clj.core 'wrapper wrapper-fn))
(defn update!
"Runs the equivalent of `(swap! screen-atom assoc ...)`, where `screen-atom`
is the atom storing the screen map behind the scenes. Returns the updated
`screen` map.
(update! screen :renderer (stage))"
[screen & args]
(doto (apply (:update-fn! screen) assoc args)
update-screen!))
(defn screen!
"Runs a function defined in another screen. You may optionally pass a series
of key-value pairs, which will be given to the function via its screen map.
(screen! my-other-screen :on-show)
(screen! my-other-screen :on-change-color :color :blue)"
[screen-object fn-name & options]
(let [execute-fn! (:execute-fn! screen-object)
screen-fn (-> screen-object :options (get fn-name))]
(apply execute-fn! screen-fn options)
nil))