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!

No comments:

Post a Comment