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.

No comments:

Post a Comment