2013-12-18

Mouse control

Latest by now it starts getting obvious to me that pasting code here isn't the best way to go. So from now on, the code will be shown as GitHub Gists which seem to be pretty awesome.

This post is going to be a short one anyway; it's been quite busy lately so I haven't had too much time to play around with this. On the good news, I have used Clojure a bit outside this project and I hope that in future my code starts becoming more and more Clojurish instead of trying to write functional code without actually writing functional code...

First of all, the character data map was modified so that all movement information is now stored in a separate map as shown below. Makes the top-level map a bit more structured. I also added couple of helper functions to convert between degrees and radians:


Next up is a function that creates and returns such new movement map based on a mouse click (or touchscreen touch). The calculation is in theory very simple: just get the direction of the movement by taking atan from the delta coordinates. The complexity comes from the fact that atan doesn't return values on the range of 0 to 360; we have to decide the final direction based on the relation of the touch point and our current location.


The get-movement function is then extended to check for a touch event, convert the touch coordinates to the camera's coordinate system, and call for the previous get-movement-from-touch function to update the movement struct. In addition, we now use a target-matches-location? helper function (shown above) to decide whether we've reached our destination or not.


Also the movement function is updated to use the new map structure:


Okay, that's it for now. I know that I'm not explaining things in a lot of detail here; hopefully there's more time in the future (assuming that someone is actually sometimes reading these posts.. :) ). Next up is hopefully the already-mentioned A* for the movements. At the moment the guy just walks towards the target until the target is reached or the dude hits a wall. Not the best possible solution.

2013-10-19

Simple collision detection

Having a character that walks around is pretty boring if there's nothing to interact with. The code from the previous posts allow the character to walk over the walls and continue off the screen to infinity (or at least until an overflow occurs..).

So now I'll show how to define simple collision zones in Tiled and parse them for the character with Clojure. I've created a new tile set and a new map file for this because the walls just by themselves were fairly dull.


Note how this time the tile image makes more sense, the wall tiles are drawn together and there are some additional elements that can be scattered around the map (don't ask what they are supposed to be, I'm still not putting much effort on the looks..).

Next, after creating a map of some kind with the walls and obstacles, we need to create a new object layer in Tiled. It should be named something reasonable, because the name is later used to access the layer data from the code. Then the only thing left to do is to draw some rectangular objects on the layer as shown below.


I named the wall objects "wall" but that doesn't really matter. The names of individual objects are not used in the code (at least not yet). It might be a bit hard to see from the screenshot, but each area that is not plain floor (the walls and the "crate" textures) is covered with a rectangular object. A thing to note is that these rectangles don't care about the tile borders, they can be drawn and placed with pixel resolution.

Then let's dive into the code. The important changes to the map.clj file are shown below:

(defn extract-obstacles [tilemap scale]
  (let [objects (.getObjects (.get (.getLayers tilemap) "obstacles"))
        rectangles (seq (.getByType objects RectangleMapObject))]
    (for [rect-obj rectangles]
      (let [rectangle (.getRectangle rect-obj)
            x-scaled (* (. rectangle x) scale)
            y-scaled (* (. rectangle y) scale)
            width-scaled (* (. rectangle width) scale)
            height-scaled (* (. rectangle height) scale)]
        (set! (. rectangle x) x-scaled)
        (set! (. rectangle y) y-scaled)
        (set! (. rectangle width) width-scaled)
        (set! (. rectangle height) height-scaled)
        rectangle))))

(defn create [map-name scale]
  (let [tilemap (.load (TmxMapLoader.) map-name)]
    (hash-map :tilemap tilemap
              :renderer (OrthogonalTiledMapRenderer. tilemap (float scale))
              :obstacles (extract-obstacles tilemap scale)
              :scale scale)))

(defn obstacles [mapmap]
  (mapmap :obstacles))

We have made a new function called extract-obstacles. In the first let we define objects as being a group of objects that are contained within the map layer "obstacles". So the name given to the layer in Tiled can be directly used in code to get the layer contents. Awesome! Note that the call structure to get the objects is a bit cumbersome: first we need to get the layers, then we need to separately get the specific obstacle layer (why not have a single "getLayer" call in the main map object?) and only then we can ask for the objects. Well anyway, after we have the objects we want to only deal with the rectangular ones (although at the moment we don't even have any others) so we define rectangles as a list of all the RectangleMapObjects of the layer.

Next we loop through all the objects from rectangles and get the matching Rectangle objects. An important thing to note here is that these rectangles have their sizes as pixels. One additional change that I made before starting with the collisions was to change the single size unit of the game from a single pixel to a single tile. This is why the scale value is added to the function arguments and this is also why we have to scale all the obstacle rectangles to match the tile units.

So now we have the obstacles extracted from the .tmx data and stored into the map data structure as correctly scaled Rectangle objects. Then we need to do something to the character to make it care about the obstacles:

(def CHANGE-WALK-MODE-DIST 1)
(def PI 3.14159)
(def DEG2RAD (/ PI 180))

(defn create [x y width height texture-base scale]
  (hash-map :texture (hash-map :still (Texture. (.internal (Gdx/files) (str texture-base "_still.png")))
                               :walk1 (Texture. (.internal (Gdx/files) (str texture-base "_walk1.png")))
                               :walk2 (Texture. (.internal (Gdx/files) (str texture-base "_walk2.png"))))
            :area (Rectangle. (- x (/ width 2)) (- y (/ height 2)) width height)
            :walk-state 1
            :walk-state-dist 0
            :x x
            :y y
            :width width
            :height height
            :speed 0
            :direction 0
            :scale scale))

(defn move [character direction speed delta-time obstacles]
  (let [newdir (if (>= direction 0)
                 direction
                 (character :direction))
        distance (* speed delta-time)
        dx (* (math/sin (* direction DEG2RAD)) (- 0 distance))
        dy (* (math/cos (* direction DEG2RAD)) distance)
        newx (+ (character :x) dx)
        newy (+ (character :y) dy)
        new-walk-dist (if (>= speed 0)
                        (+ distance (character :walk-state-dist))
                        0)
        new-walk-state (if (> new-walk-dist CHANGE-WALK-MODE-DIST)
                         (+ (mod (character :walk-state) 2) 1)
                         (character :walk-state))
        area (character :area)
        scale (character :scale)]
    
    (set! (. area x) (- newx (/ (character :width) 2)))
    (set! (. area y) (- newy (/ (character :height) 2)))
    (if (some (fn [obstacle] (.overlaps obstacle area)) obstacles)
      (assoc character :speed 0 :direction newdir :walk-state-dist 0)
      (assoc character :speed speed :direction newdir :x newx :y newy
           :walk-state new-walk-state
           :walk-state-dist (mod new-walk-dist CHANGE-WALK-MODE-DIST)))))


(defn render [character ^SpriteBatch batch]
  (let [speed (character :speed)
        texture (if (= speed 0)
                  ((character :texture) :still)
                  (if (= (character :walk-state) 1)
                    ((character :texture) :walk1)
                    ((character :texture) :walk2)))
        texture-width (.getWidth texture)
        texture-height (.getHeight texture)
        x-middle (character :x)
        y-middle (character :y)
        x (- x-middle (/ texture-width 2))
        y (- y-middle (/ texture-height 2))
        direction (character :direction)
        scale (character :scale)]
    (.draw batch texture x y (/ texture-width 2) (/ texture-height 2)
      texture-width texture-height scale scale direction 0 0

      texture-width texture-height false false)))


The create function has now two new elements, the scale and a Rectangle called area matching the character's current position and size. In the move function, we first update this area to match the proposed new coordinates and then check if an obstacle exists (the some function) that overlaps with the area of the character. If there is an overlap, the move is not accepted and the character is made to stop. The rendering function is almost the same as before, although we've added the scale value to the drawing method call.

That's it. Now all we need to do is to change the main code a little to pass the obstacles from the map to the character:

(def SCALE (/ 1 32.0))

...

(def main-screen
  (proxy [Screen] []
    (show [] )
    (render [delta]

      ...

        (def world (assoc world :character
                          (character/move
                            (world :character) direction speed
                            (.getDeltaTime Gdx/graphics)
                            (tilemap/obstacles (world :tilemap))))))

...

(defn -create [this]
  (.setScreen this main-screen)
  (def ortocamera (OrthographicCamera.))
  (.setToOrtho ortocamera false 20 20)
  (def batch (SpriteBatch.))
  (def world (hash-map
               :tilemap (tilemap/create (str asset-path "simple_room.tmx") SCALE)
               :character (character/create 10 10 2 2 (str asset-path "character") SCALE))))

Not much has changed except for adding the scale into the map and character creation and adding the call for the obstacles as a parameter for the character's move function. In the screenshot below, I'm pressing the left arrow button as hard as I can but the guy just stays put!


So that's that. Next I'm planning to add some mouse controls and path finding algorithms to control the character without a keyboard. The goal is to be able to run the game and control the little guy on Android as well without needing to somehow plug a keyboard into my poor phone.

2013-10-18

Adding a character

Now that we have a map, it's time to add a character as well. At first I wanted to just be able to render the character on top of the map and move it with the arrow keys. No collision detection or anything fancy like that is included yet.

First I created some very simple textures for the character: one for when the character is standing still and two that alternate when the character walks. The image below shows all the three different textures together. The game of course uses separate png files for each, and those can be found from the github project (https://github.com/jvnn/space-ventures/tree/master/android/assets). Keep in mind the requirement that texture dimensions are multiples of two (16, 32, 64, ... pixels). The still texture is 64x32 pixels while the two walking ones are 64x64.


Next we need some code to create and render our lovely round-headed character. As was the case with the map (see the previous post), I wanted to avoid storing any state info in the character file, so all the required data is stored in a hash-map which is created when the character is created and must then be passed to all subsequent functions that do something (move, render ...) to the character. This code is a bit more complex than the map loading one, so I'll show it part by part.

(ns com.space-ventures.character
  (:require [clojure.algo.generic.math-functions :as math])
  (:import [com.badlogic.gdx Gdx]
           [com.badlogic.gdx.graphics Texture]
           [com.badlogic.gdx.graphics.g2d SpriteBatch]
           [com.badlogic.gdx.math Rectangle]))

(defn create [x y width height texture-base]
  (hash-map :texture (hash-map :still (Texture. (.internal (Gdx/files) (str texture-base "_still.png")))
                               :walk1 (Texture. (.internal (Gdx/files) (str texture-base "_walk1.png")))
                               :walk2 (Texture. (.internal (Gdx/files) (str texture-base "_walk2.png"))))
            :walk-state 1
            :walk-state-dist 0
            :x x
            :y y
            :speed 0
            :direction 0))

The create function is responsible for constructing the character map, which means creating the textures (which live in a separate map in order to have a little more hierarchy and order) and setting all other parameters to default values. The parameter :walk-state stores the information about which walking texture is currently in use while :walk-state-dist counts the distance walked (in pixels) while using that texture.

(def CHANGE-WALK-MODE-DIST 40)

(defn move [character direction speed delta-time]
  (let [newdir (if (>= direction 0)
                 direction
                 (character :direction))
        deg2rad (/ 3.14159 180)
        distance (* speed delta-time)
        newx (- (character :x) (* (math/sin (* direction deg2rad))
                                  distance))
        newy (+ (character :y) (* (math/cos (* direction deg2rad))
                                  distance))
        new-walk-dist (if (>= speed 0)
                        (+ distance (character :walk-state-dist))
                        0)
        new-walk-state (if (> new-walk-dist CHANGE-WALK-MODE-DIST)
                         (+ (mod (character :walk-state) 2) 1)
                         (character :walk-state))]
    
    
    (assoc character :speed speed :direction newdir :x newx :y newy
           :walk-state new-walk-state :walk-state-dist (mod new-walk-dist CHANGE-WALK-MODE-DIST))))

The move function is used to calculate and store the new position, speed and direction of the character. It looks quite messy at first, but in the end the functionality is quite simple:

  1. If the received direction is less than 0, there was no change in direction and we use the old value. Otherwise the received value is stored.
  2. deg2rad is used to convert degrees to radians needed for the sin/cos functions later.
  3. The distance travelled is calculated as speed * delta-time.
  4. The new x coordinate is calculated by adding to the old value the x component ( sin(direction) ) of the total movement.
  5. Same is done for the y coordinate, this time using cos(direction).
  6. If we have some speed, the distance travelled since last rendering is added to the total walking distance covered using the texture defined by walk-state. If the character is stopped, the distance gets cleared.
  7. If the character has travelled long enough with the current texture, it's time to swap the used texture.
In the end the new values are stored into a new version of the state map.

(defn render [character ^SpriteBatch batch]
  (let [speed (character :speed)
        texture (if (= speed 0)
                  ((character :texture) :still)
                  (if (= (character :walk-state) 1)
                    ((character :texture) :walk1)
                    ((character :texture) :walk2)))
        texture-width (.getWidth texture)
        texture-height (.getHeight texture)
        x-middle (character :x)
        y-middle (character :y)
        x (- x-middle (/ texture-width 2))
        y (- y-middle (/ texture-height 2))
        direction (character :direction)]
    (.draw batch texture x y (/ texture-width 2) (/ texture-height 2)
      texture-width texture-height 1 1 direction 0 0
      texture-width texture-height false false)))


(defn dispose [character]
  (doseq [texture (vals (character :texture))]
    (.dispose texture)))

What is left is rendering the character in the position defined in the move function and facing the given direction. But rotating a texture turned out to require quite a monster of a method. The draw method for SpriteBatch that also handles rotation requires the following arguments: the texture to be drawn, x- and y-positions, the x and y for the rotation origin (relative to the texture), width and height of the texture, scale values for x and y, the rotation of the texture, the start x and y coordinates for the region of the texture that we want to draw, the width and height of the region, and finally whether we want the texture to be flipped along the x or y axis. Whoah. But ones that monster gets called, our character appears on the screen.

The last function simply disposes the textures once we don't need them any more.

What is left is the main code that calls the character (and map) functions to make it all happen:

(ns com.space-ventures.core
  (:require [com.space-ventures.map :as tilemap]
            [com.space-ventures.character :as character])
  (:import [com.badlogic.gdx Game Gdx Graphics Screen Input$Keys]
           [com.badlogic.gdx.graphics GL10 Color OrthographicCamera]
           [com.badlogic.gdx.graphics.g2d SpriteBatch]
           [com.badlogic.gdx.utils TimeUtils]))

(def asset-path "../android/assets/")

(def ortocamera nil)
(def batch nil)
(def world nil)

(def main-screen
  (proxy [Screen] []
    (show [] )
    (render [delta]
      (.glClearColor (Gdx/gl) 0 0 0.2 1)
      (.glClear (Gdx/gl) GL10/GL_COLOR_BUFFER_BIT)
      
      (.update ortocamera)
      (tilemap/render (world :tilemap) ortocamera)
      
      (let [direction (cond
                        (.isKeyPressed Gdx/input Input$Keys/UP) 0
                        (.isKeyPressed Gdx/input Input$Keys/RIGHT) 270
                        (.isKeyPressed Gdx/input Input$Keys/DOWN) 180
                        (.isKeyPressed Gdx/input Input$Keys/LEFT) 90
                        :else -1)
            speed (cond
                    (.isKeyPressed Gdx/input Input$Keys/UP) 200
                    (.isKeyPressed Gdx/input Input$Keys/RIGHT) 200
                    (.isKeyPressed Gdx/input Input$Keys/DOWN) 200
                    (.isKeyPressed Gdx/input Input$Keys/LEFT) 200
                    :else 0)]
        (def world (assoc world :character
                          (character/move
                            (world :character)
                            direction
                            speed
                            (.getDeltaTime Gdx/graphics)))))
      
      (.setProjectionMatrix batch (. ortocamera combined))
      (.begin batch)
      (character/render (world :character) batch)
      (.end batch))
    
    (dispose[]
      (tilemap/dispose (world :tilemap))
      (character/dispose (world :character))
      (.dispose batch))
    (hide [])
    (pause [])
    (resize [w h])
    (resume [])))


(gen-class
  :name com.space-ventures.core.Game
  :extends com.badlogic.gdx.Game)

(defn -resize [this w h]
  (println "resized to" w "x" h))

(defn -create [this]
  (.setScreen this main-screen)
  (def ortocamera (OrthographicCamera.))
  (.setToOrtho ortocamera false 640 640)
  (def batch (SpriteBatch.))
  (def world (hash-map
               :tilemap (tilemap/create
                          (str asset-path "simple_walled_map.tmx"))
               :character (character/create
                            320 320 64 64
                            (str asset-path "character")))))


In the main code, the hash-maps returned by the map and character functions are stored in (yet another) hash-map called world. Lacking a better (more functional?) approach, I'm always redefining the world whenever something changes. I have a doubt that this is not a good way to do things, so I will try to come up with a better solution at some point.

Otherwise nothing so special happens here. The map and the character are created in the create function and, after deciding on the new direction and speed of the character based on user input, both are rendered in the render function. And this is how it looks like:


Unfortunately the picture doesn't show how beautifully the two walking textures alternate, so you just have to either trust me on this one or check out the project from github and see for yourself!

Loading a simple map

Alright, enough with the raindrops. Now that the tutorial is more or less successfully done, it's time to concentrate on something else. In this post I will explain how to create a simple tile-based map with Tiled map editor (http://www.mapeditor.org/) and how to load and render it with Clojure and libgdx.

Note: I've created a github project for my experiments, so the source code and used pictures are available at https://github.com/jvnn/space-ventures.

I won't concentrate here on how to use Tiled because it's easy enough and there's tons of material out there to learn from (such as http://gamedev.tutsplus.com/tutorials/level-design/introduction-to-tiled-map-editor/). What wasn't clear to me right away (because I didn't bother reading through the tutorial) was how to give the editor something to work with. Tiled is a tile-based editor (as the name somewhat suggests) and thus it needs to have a tile set. The tiles are then used to draw the map. I ended up creating a very simple set of walls and corner tiles with Inkscape:


The picture above has the size of 512x32 pixels and is divided into 32x32-pixel tiles by Tiled. One might wonder why I have so many equally gray tiles included. Well, libgdx only accepts textures whose dimensions are a multiple of two. So creating just nine tiles (which would otherwise be enough for four corners, four walls and a floor tile) isn't going to cut it.

Once we have the tile set imported, we can draw a simple map with darker walls and a light gray floor. I didn't put much effort into making it fancy, this will do for now... The map can then be saved as a .tmx file that will be parsed by libgdx. 


So now we have a map. Then we need to dive into the awfully complex code that handles parsing the map file:

(ns com.space-ventures.map
  (:import [com.badlogic.gdx.maps.tiled TiledMap TmxMapLoader]
           [com.badlogic.gdx.maps.tiled.renderers OrthogonalTiledMapRenderer]))

(defn load-map [map-name]
  (let [tilemap (.load (TmxMapLoader.) map-name)]
    (hash-map :tilemap tilemap
              :renderer (OrthogonalTiledMapRenderer. tilemap))))

(defn render-map [mapmap camera]
  (.setView (mapmap :renderer) camera)
  (.render (mapmap :renderer)))


(defn dispose [mapmap]
  (.dispose (mapmap :tilemap))

  (.dispose (mapmap :renderer)))


Yep, that's it. I created a new file called map.clj and defined two functions, one for loading the map in the beginning and another one that renders it. The map information is stored in a hash-map that is created in load-map and later passed to the renderer. I'm still trying to learn the ways of the force, i.e. functional programming, so I'm not sure if this is the right way to do things but at least no state information is stored in the module itself. Otherwise I don't know much more to say about the code, it's so very simple! Loading the map only requires the name of the .tmx file (map-name) and the resulting map is then fed into the constructor of the map renderer. In the rendering function, the renderer is told to use the view of the camera and then simply told to render the thing. (Oh, sorry for the variable name, I gave up trying to create a better name for a map (hash-map) that contains a map...)

I will show how these functions are called from within the main code in the next post where I describe how to add a simple character into the "game".

2013-10-13

The libgdx Simple Game Demo in Clojure

Hello world! As a first post, I'd like to share my Clojure implementation of the simple game demo found here:
https://github.com/libgdx/libgdx/wiki/A-simple-game

I'm using Nightcode as my editor (https://github.com/oakes/Nightcode) and it gave me a nice template where to start. I'm not going through all the set up and directory structures of the program here, using nightcode makes it all very easy. The only thing I changed for the launcher code (desktop_launcher.clj) was the resolution of the program.

So without any further blaa blaa, here's the main code (for now, I'll paste it without fancy formatting. Have to check what SyntaxHighlighter has to offer later). I left out the sounds though, because I didn't feel like registering to the page from where I would have had to download them...

(ns com.demo-game.core
  (:import [com.badlogic.gdx Game Gdx Graphics Screen Input$Keys]
           [com.badlogic.gdx.graphics GL10 Color Texture OrthographicCamera]
           [com.badlogic.gdx.graphics.g2d BitmapFont SpriteBatch]
           [com.badlogic.gdx.math Rectangle Vector3 MathUtils]
           [com.badlogic.gdx.scenes.scene2d Stage]
           [com.badlogic.gdx.scenes.scene2d.ui Label Label$LabelStyle]
           [com.badlogic.gdx.utils Array TimeUtils]))

(declare ^Stage stage)


(def asset_path "../android/assets/")


(def droplet-img nil)

(def bucket-img nil)
(def ortocamera nil)
(def batch nil)
(def bucket nil)
(def droplet nil)
(def raindrops (Array.))
(def last-drop nil)

(defn spawn-raindrop []

  (let [raindrop (Rectangle.)]
    (set! (. raindrop x) (.nextInt MathUtils/random (- 800 64)))
    (set! (. raindrop y) 480)
    (set! (. raindrop width) 64)
    (set! (. raindrop height) 64)
    (.add raindrops raindrop)
    (def last-drop (TimeUtils/nanoTime))))

(def main-screen

  (proxy [Screen] []
    (show [] )
    (render [delta]
      (.glClearColor (Gdx/gl) 0 0 0.2 1)
      (.glClear (Gdx/gl) GL10/GL_COLOR_BUFFER_BIT)
      
      (if (> (- (TimeUtils/nanoTime) last-drop) 1000000000)
        (spawn-raindrop))
      
      (if (.isKeyPressed Gdx/input Input$Keys/LEFT)
        (set! (. bucket x)
              (- (. bucket x)(* 200 (.getDeltaTime Gdx/graphics)))))
      (if (.isKeyPressed Gdx/input Input$Keys/RIGHT)
        (set! (. bucket x)
              (+ (. bucket x) (* 200 (.getDeltaTime Gdx/graphics)))))  
      
      (if (.isTouched Gdx/input)
        (let [touch-pos (Vector3.)]
          ; convert the touch coordinates (from input) into the
          ; coordinate system of the camera
          (.set touch-pos (.getX Gdx/input) (.getY Gdx/input) 0)
          (.unproject ortocamera touch-pos)
          (set! (. bucket x) (- (. touch-pos x) (/ 64 2)))))
      
      (let [iter (.iterator raindrops)]
        (while (.hasNext iter)
          (let [rdrop (.next iter)]
            (set! (. rdrop y)
                  (- (. rdrop y) (* 200 (.getDeltaTime Gdx/graphics))))
            (if (< (+ (. rdrop y) 64) 0)
              (.remove iter))
            (if (.overlaps rdrop bucket)
              (.remove iter)))))
      
      (if (< (. bucket x) 0)
        (set! (. bucket x) 0))
      (if (> (. bucket x) (- 800 64))
        (set! (. bucket x) (- 800 64)))
      
      (.update ortocamera)
      (.setProjectionMatrix batch (. ortocamera combined))
      (.begin batch)
      (.draw batch bucket-img (. bucket x) (. bucket y))
      
      (doseq [rdrop (seq raindrops)]
        (.draw batch droplet-img (. rdrop x) (. rdrop y)))
      (.end batch))
    
    (dispose[]
      (.dispose droplet-img)
      (.dispose bucket-img)
      (.dispose batch))
    (hide [])
    (pause [])
    (resize [w h])
    (resume [])))


(gen-class

  :name com.demo-game.core.Game
  :extends com.badlogic.gdx.Game)

(defn -resize [this w h]

  (println "resized to" w "x" h))

(defn -create [this]

  (.setScreen this main-screen)
  (def droplet-img
    (Texture. (.internal (Gdx/files) (str asset_path "droplet.png"))))
  (def bucket-img
    (Texture. (.internal (Gdx/files) (str asset_path "bucket.png"))))
  (def ortocamera (OrthographicCamera.))
  (.setToOrtho ortocamera false 800 480)
  (def batch (SpriteBatch.))
  (def bucket (Rectangle.))
  (set! (. bucket x) (- (/ 800 2) (/ 64 2)))
  (set! (. bucket y) 20)
  (set! (. bucket width) 64)
  (set! (. bucket height) 64)
  (spawn-raindrop))