Inverse Kinematics for Worms/Snakes

Hi again,

If you read our previous post you’ll be familiar with our goal, turning a Ludum Dare hack in to a production ready game. As I mentioned in that post, we decided to pull it down and start from scratch, and this series will detail the process and any bits that might be of interest to anyone (or someone, possibly you!)

The previous post had code, but didn’t really discuss it due to time constraints, so let’s rectify that by starting with the player (all code examples are in Javascript, but hopefully you can migrate it to your chosen language). Incidentally, the code for the worm was based on examples in HTML5 Animation, chapter 14 (awesome book!).

Make me a worm

To start with, we’ll create a separate segment object to store things like coordinates, size etc.:

function Segment (width, height, color) {
    this.x = 0;
    this.y = 0;
    this.width = width;
    this.height = height;
    this.radius = (width + height) * 0.5;
    this.vx = 0;
    this.vy = 0;
    this.rotation = 0;
    this.color = color || "red";
};

The segment object has a single method used to find the end of the segment for updating the movement later on (this code is from the above book).

Segment.prototype.getPin = function() {
   return {
       x: this.x + Math.cos(this.rotation) * this.radius,
       y: this.y + Math.sin(this.rotation) * this.radius
   }
};

We also have a player object representing the complete worm, which will contain all the segments and will be responsible for updating/drawing them.

function Player(){
    this.isAlive = true;

    // Key detection
    this.right = false;
    this.left = false;
    this.up = false;
    this.down = false;

    this.maxthrust = 5;
    this.thrust = this.maxthrust;

    this.x = Engine.canvas.width * 0.5;
    this.y = Engine.canvas.height * 0.5;

    this.segments = [];
};

We can now create our worm from its component parts with a little function we like to call ‘createWorm’. We generate the worm in reverse so that each segment is drawn on top of the other (when you start using sprites for the segments it looks better to have the head overlapping the others).

Player.prototype.createWorm = function(num_segments, width, height) {
    this.segments = [];
    this.num_segments = parseInt(num_segments) || 10;

    var segment,
        i = this.num_segments;

    while(i--) {
        segment = new Segment(width, height);
        segment.id = i;
        this.segments.push(segment);
    }
};

We now have an array of segments to abuse. Let’s make them move about…

Wiggle wiggle…

First we need some methods to detect keys. The controls are setup for WASD or arrow keys, with the space bar reserved for some ‘action’ (whatever that may be – originally it was used to surface).  This next snippet is pretty standard Javascript key detection code, but we present it for completeness.  For each key we set a movement flag that we can detect later to set animations etc.:

Player.prototype.keydown = function (event) {
    switch (event.keyCode) {
        // WASD
        case 37: //left
            this.left = true;
            break;
        case 39: //right
            this.right = true;
            break;
        case 38: //up
            this.up = true;
            break;
        case 40: //down
            this.down = true;
            break;
        // Arrow keys
        case 65: //left
            this.left = true;
            break;
        case 68: //right
            this.right = true;
            break;
        case 87: //up
            this.up = true;
            break;
        case 83: //down
            this.down = true;
            break;
        case 32: //action - SPACE
            this.action = true;
            break;
    }
};
	
Player.prototype.keyup = function (event) {
    switch (event.keyCode) {
        //  WASD
        case 37: //left
            this.left = false;
            break;
        case 39: //right
            this.right = false;
            break;
        case 38: //up
            this.up = false;
            break;
        case 40: //down
            this.down = false;
            break;
        // arrow keys
        case 65: //left
            this.left = false;
            break;
        case 68: //right
            this.right = false;
            break;
        case 87: //up
            this.up = false;
            break;
        case 83: //down
            this.down = false;
            break;
        case 32: //Action
            this.action = false;
            break;
    }
};

Right, so here is the main bit that will be needed to get a nice set of segments moving around together. The behaviour required can be thought of as ‘dragging’, implying we will update each segment in relation to some other one. The players x and y coordinates are updated with the thrust value based on the relevant key (up key), and then an update is performed over all segments in relation to the one preceding it (the one actually being controlled by the player is the head = segment 0).

Player.prototype.update = function(dt, segment) {
    // Check for up key for forward movement
    if(this.up) {
        this.thrust = this.maxthrust;
    } else {
        this.thrust = 0;
    }

    this.x += this.thrust * dt;
    this.y += this.thrust * dt;

    // move the head
    this.drag(this.segments[0], this.x, this.y);

    // move the other segments
    var i, segment;
    for(i = 1; i < this.segments.length; i ++) {
        // move the current segment in relation to the previous one
        segment = this.segments[i];
        this.drag(
            segment, 
            this.segments[i-1].x, 
            this.segments[i-1].y
        );
    }
};

The drag method is very simple, we first find the difference between the current segment position and the previous one, obtain its anchor point with .getPin(), set the rotation of the segment by calculating the angle between the two segments and then set the new position of the current segment:

Player.prototype.drag = function(segment, newX, newY) {
    var dx = newX - segment.x,
        dy = newY - segment.y;
        // get anchor point of current segment
        w = segment.getPin().x - segment.x,
        h = segment.getPin().y - segment.y;
    // updates the segment rotation used as part of the getPin() calculation.
    segment.rotation = Math.atan2(dy, dx);
    segment.x = newX - w,
    segment.y = newY - h
};

OK, looks like we’re done here.  Finally, we can write a nice simple draw method to see our worm come alive!

Seeing worms

Since we’re not doing anything fancy at the moment (though we have plans for graphics later), we’ll simply draw the segments as circles. we loop through the segments array as before and draw them to screen,…. done:

Player.prototype.draw = function(dt, context) {
    var i, segment;
    for(i = this.segments.length - 1; i >= 0; i --) {
        segment = this.segments[i];
        context.fillStyle = segment.color;
        context.beginPath();

        context.arc(
            segment.x,
            segment.y,
            segment.radius, 
            0, 
            2 * Math.PI
        );
        context.fill();
    }
};

We’re finished! Hopefully that’ll be enough to get you going with a worm (or snake – I’ll try not to show any bias), we left out the main game update and draw methods that would call the player methods, so you’ll need to add this player object to whatever framework you use for that.

We’ll leave you now, but in the next post we’ll be talking about the world map and some current ideas, hint: tile.width = 64, tile.height = 32. 😛

Caveats

– The example code is based on our current development game code so may contain bugs, incorrect assumptions, or just plain hacks. We’ve tried to reference any good sources we have come across in our reading.

Advertisements

One thought on “Inverse Kinematics for Worms/Snakes

  1. Pingback: Isometric Heaven | Lazyeels

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s