Quantcast
Channel: Clojure – Compound Theory
Viewing all articles
Browse latest Browse all 9

Mini – Game Dev Diary #1

$
0
0

I’ve been having lots of fun this break getting back into writing a top down racing game that I had originally started (way) earlier in the year, so I thought I would start writing a little dev diary on it, to aid me in keeping up my momentum in developing it.  It’s still very much in the prototype phase, but I’m starting to see real things come out of it.

The basic gist of the game is:

  • Top down racing game, very much inspired by the early Micro Machines PC game of my childhood years (hence the code name Mini for the game).
  • I want the steering and handling to be “drifty” and very arcade like – basically not technical and lots of fun to play.
  • I have this feeling of wanting a lot of “bounce” between artefacts in the game. For example, if you hit a wall, you don’t stop, you ricochet off it. We’ll see how this pans out in actual game play though.

I have a few more ideas on top of this, but this gives you a feel for what I am going for.

I wanted to write more Clojure, so I ended up picking this as my language of choice, and then using libGDX as my game development framework, and Box2d as my physics engine. Clojure is awesome, and libGDX is a great framework, but in retrospect, I’ve been wondering if it would have been faster to write this in something like Unity instead.  That being said, I’m being productive, and I do enjoy writing Clojure, so I’ll continue the current course for now (When I started, Arcadia Unity didn’t exist either).

I also chose to use Brute, my entity component framework, as the other main library to write my game with. So far, I’ve been very happy with it, and I’ve been able to add any features I needed quite easily to the library.

The first thing I did (and what took the longest), was to write my own wrapper around libGDX to use with Clojure. It would have been far faster to use play-clj, which I have used in the past, but I had found it previously had issues with clojure.tools.namespace and having a user namespace you could reset your state with, as in the Clojure Reloaded Workflow.  I probably should have spent more time trying to get play-clj to work better with a reloaded workflow, because it took me at least about three months of my spare time (and an entire CampJS weekend) to get my wrapper for libGDX to be in a place that I was genuinely happy with.

For the car and the steering I went with a super simple approach. There are a whole load of articles on how to simulate a top down car in Box2d, but I didn’t want a simulation, I want something fun and arcadey, and also something I could implement easily. Therefore, my car is just a rectangle, which gets pushed from the back when accelerating, pushed from the front when braking and pushed from the top left and right when turning.

This was quick and easy to do, however it gives you a very “floaty” feel to your vehicle as you drive (or you could see it as I was really getting an extra helping of the drift I was looking for). If you have ever played the original Asteroids, you know exactly the movement I’m talking about, so I had a new problem to solve.  I quickly surmised that what I needed to do was fake the auto correction you get when driving a car when you stop turning, let go of the steering wheel but keep accelerating, but I was quite unsure on how to get this to happen. After some fruitless Googling and several way too complicated solutions, I realised I could simply reduce the Car’s angular velocity if the up arrow was depressed (car accelerating), but neither the left or right key was depressed, and this has seemed to work really, really well.

I dropped in some sample wall blocks to drive around, and tweaked the numbers until I was happy with how the steering felt.

You can’t really see the auto correction on the steering working in the video, but here is the code that powers it:

(defrecord Car [acceleration deceleration max-velocity turn-force autocorrect])

(defn create-car
  "Creates a car!. Pass in the entity system, the entity we are making a car, and the image you want to use"
  [es system physics entity x y sprite-image]
  (let [sprite (->Sprite system x y sprite-image)
        physic (->Physic (p/load-body-from-sprite physics BodyDef$BodyType/DynamicBody sprite 0.5 0.5))]
    (-> es
        (e/add-entity entity)
        (e/add-component entity (->Car 120 -20 100 15 3))
        (e/add-component entity sprite)
        (e/add-component entity physic))))

(defn- accelerate-linearly
  [^Sprite sprite ^Body body direction force]
  (when-not (= direction :none)
    (let [f (.getWorldVector body (Vector2. 0 force))
          pos (.getWorldPoint body (Vector2. 0.0 (-> sprite .getHeight (/ 2) (* -1))))]
      (.applyForce body f pos true))))

(defn- autocorrect-steering
  [^Body body turn-direction autocorrect]
  (when (= turn-direction :none)
    (let [velocity (.getAngularVelocity body)]
      (.setAngularVelocity body (/ velocity autocorrect)))
    ))

(defn- apply-turning-force
  [^Sprite sprite ^Body body direction turn-force]
  (when-not (= direction :none)
    (let [f (Vector2. (if (= direction :right)
                        turn-force
                        (* turn-force -1)) 0)
          f (.getWorldVector body f)
          pos (.getWorldPoint body (Vector2. 0 (-> sprite .getHeight (/ 2))))]
      (.applyForce body f pos true))))

(defn accelerate-car
  "Accelerate a car.
  linear-direction is either :acceleration or :deceleration
  turn-direction is :left, :right or :none"
  [es entity linear-direction turn-direction]
  (let [car (e/get-component es entity Car)
        body (:body (e/get-component es entity Physic))
        sprite (:sprite (e/get-component es entity Sprite))
        turn-force (:turn-force car)]
    (when (< (-> body .getLinearVelocity .len) (:max-velocity car))
      (accelerate-linearly sprite body linear-direction (linear-direction car))
      (apply-turning-force sprite body turn-direction turn-force)
      (autocorrect-steering body turn-direction (:autocorrect car)))))

The input system calls accelerate-car directly with various inputs, depending on what arrow keys are pressed. Many of the magic numbers that determine how the Car operates are set on the Car component itself, so I can have different models of cars down the line that can have different handling and acceleration.

Finally, I needed the camera to always have the car be in the centre of the screen. This would mean I could have tracks that are bigger than the display and also goes back to the feel of that original Micro Machines game.  This was remarkably easier than I had anticipated.  I created a Cam component and attached it to my player car. From there it was just a matter of updating the current Camera with the centre position of the Sprite that has the Cam component, and everything worked perfectly.

The code is as follows:

;; pretty much just a label to say, hey track this thing with a cam.
(defrecord Cam [])

(defn process-one-game-tick
  "Move the cam to follow the player marked with one"
  [{:keys [cam]} es delta]
  (doseq [entity (e/get-all-entities-with-component es Cam)]
    (let [sprite (:sprite (e/get-component es entity Sprite))
          center-position (-> sprite .getBoundingRectangle (.getCenter (Vector2.)))
          cam-position (.position cam)
          z (.z cam-position)]
      (.set cam-position center-position z)
      (.update cam)))
  es)

What I found quite surprising, was that by changing the camera to follow the car, I was no longer happy with how the Car’s handling felt. It felt kind of sluggish now, even through I hadn’t changed any of the values I had previously set.  I’ll leave it alone for the moment, and came back to it once I have some more elements to the game in place.

Coming up next, I want to lay out a simple track I can drive around, and then I can work which features I want to prioritise from there.


Viewing all articles
Browse latest Browse all 9

Trending Articles