Jump to navigation

iameven.com - Snake

- Even Alander

I made Snake. As a focus for this year I really want to make games, and if I keep things simple (stupid) enough, I can maybe complete several.

Play Snake

Why snake?

  1. Using a known game I don't need to focus on the design.
  2. Snake only has one moving actor, that can only collide with itself.

Previous attempts at making games I also kept it simple. Maybe too simple. I made tic-tac-toe, and then a level editor for Sokoban. The problem was going from these sort of action/react type games to those with a continuous loop. Not sure why I struggle with that, it's strictly not very complicated. Then again, a barrier that's been crossed always seems lower after crossing it.

The goal was to make a game with some animation and real time controls. In snake the player has to react to the game, so this is a good fit.

The base game

Simplifying things, Snake mainly consist of three parts:

  1. A level, I use a grid that is 30 units wide and calculate the height to keep a unit square.
  2. A snake, where each body part is 1 unit large. It moves either UP, RIGHT, DOWN or LEFT.
  3. An apple, that the snake collects to grow.

The grid starts at [0,0] in the top left corner. The x value covers left to right, and the y value top to bottom.

I decided to store the snake body as an array, that way I could could unshift (add a new part at the start) and pop (remove the last part) to make the snake move. The direction is stored when the player clicks the arrow keys, it defaults to a random direction. If the direction is UP or DOWN I subtract or add 1 to the y value, if LEFT or RIGHT I subtract or add 1 to the x value. I then test that new position to see if it is where the apple currently is, and if not, I iterate over the snake array to see if there is a collision. When I find an apple, it is as simple as not using pop, making the array grow with 1 new part. A collision ends the game and if nothing happens I remove the last item in the snake array.

Frame rate

Using the "new" hotness in JavaScript that is requestAnimationFrame() I should get a refresh rate of around 60 per second. That of course depends on how much I try to do every frame, but this game shouldn't be a problem. This is good when it comes to animation, but at this point I don't have any.

Meaning that if I do the calculation above every frame the snake will be off screen in a quarter second if it starts in the center (half a second from one side to the other going LEFT => RIGHT or RIGHT => LEFT).

Which doesn't leave any time to react, much less control the snake.

I see two ways to solve this. Either by setting a timeout before calling the update function, or count the time passed and subtract from a cool down number. Since I need to know how much time has passed to get smooth animations I went with the second option. In code it sort of looks like this:

var snake = new Snake();
var time;
var render = () => {
    var now = new Date().getTime();
    var dt = now - (time || now);
    time = now;
    snake.update(dt);
    snake.render();
}

I get the current time, subtract that from the previously recorded time and get milliseconds since last update. Feeding that into the snakes update function I use a variable called wait which is initially set to cool down (around 300 ms) and when this has counted down to > 0 I reset wait to cool down and do the update:

snake.update(dt) {
    this.wait = this.wait - dt;

    if (this.wait > 0) {
        return;
    }

    this.wait = this.cooldown;
    // do the update
}

Animations

I could probably have made the snake a different way and done proper collision detection and moved the snake with every tick. But the solution I came up with have the snake move in units so I had to fill in the gaps.

This turned out to be more complicated than what I had anticipated. Mainly due to me having a hard time understanding how the math I wrote affected the rendering. Here is how I ended up rendering the snake:

// I store all the variables in the top of the function.
// since I have no strong opinion on this I follow the eslint rules.
// see http://eslint.org/docs/rules/vars-on-top.html
var i;
// I used the plain this.wait variable to begin with but that
// iterates the percentage from 100 => 0, it's easier to math
// when it counts up.
I double the percentage so it goes from
// 0 => 200%.
This made calculating the offset work for me.
var percentage = (this.cooldown - this.wait) / this.cooldown * 2;
// Every bodypart come from one direction and goes another direction
// so I do different calculations before 50% of the time is reached.
var entering = this.wait > this.cooldown / 2;
// The offsets are a position from the center of the unit
// that changes with the percentage above.
// In other words, they make the snake appear to move.
var offsetX;
var offsetY;

for (i = 0; i < this.snake.length; i++) {
    // reset offsets
    offsetX = 0;
    offsetY = 0;

    // this was were I had my main struggles.
    // That is after making sure I had stored a from and to
    // direction which I also managed to flip at one point
    // making for some really confusing results.
    if (entering) {
        switch (this.snake[i].from) {
        case UP:
            offsetY = this.radius * percentage - this.radius;
            break;
        case DOWN:
            offsetY = this.radius * percentage * -1 + this.radius;
            break;
        case LEFT:
            offsetX = this.radius * percentage - this.radius;
            break;
        case RIGHT:
            offsetX = this.radius * percentage * -1 + this.radius;
            break;
        }
    } else {
        // if it is the head it doesn't have a to direction and
        // have to follow the input direction
        switch (this.snake[i].to || this.direction) {
        case UP:
            offsetY = this.radius * percentage * -1 + this.radius;
            break;
        case DOWN:
            offsetY = this.radius * percentage - this.radius;
            break;
        case LEFT:
            offsetX = this.radius * percentage * -1 + this.radius;
            break;
        case RIGHT:
            offsetX = this.radius * percentage - this.radius;
            break;
        }
    }

    // draw the body part as a circle
    this.c.beginPath();
    this.c.arc(
        this.snake[i].x * this.radius * 2 + offsetX + this.radius,
        this.snake[i].y * this.radius * 2 + offsetY + this.radius,
        this.radius,
        0,
        Math.PI * 2);
    this.c.closePath();
    this.c.fill();
}

As you can see in the code, the math is not very complex but it still manages to confuse me. Since it works I just went with it. For every offset I get a value between 0% and 200% of this.radius. this.radius is half a unit, so this.radius * percentage get the pixel offset of one unit. I then keep or flip the number and add or subtract the radius so everything is based on the center of the unit and not an edge. I do see that I could have simplified the code some as the same calculation repeats it self, but I stopped coding when it worked.

Done

As I've achieved my goal, I think I'm done with this project. There are of course more things I could do and even planned to do. But I also want to try new challenges and I want to make my own games.

Feel free to Play Snake.

And read the code.

  1. iameven.com
  2. : Nostalgia driven web design
  3. : Lynx
  4. : The ssstraight story
  5. : Tetris
  6. : Snake
  7. : Post install
  8. : Re-building this website
  9. : Post stats 2014
  10. : Optimizing for git
  11. : Digging through old files
  12. : Arduino Uno
  13. : Building this website
  14. : Getting myself a logo
  15. : Warm Echo
  16. : Recent events
  17. : WTF, Spotify?
  18. : Numusic and Nuart
  19. : U don't simply uplay
  20. : Playing with time lapse
  21. : WZ
  22. : Fine Day
  23. : Restoring iameven
  24. : Ableton push
  25. : Ghost - Light weight Wordpress
  26. : Everything on the net should be dated
  27. : Euro Offshore container
  28. : Niels Juels street
  29. : Kverneland Næringspark
  30. : Hiking
  31. : Fume Tests
  32. : More Lexi pictures, in Sandveparken
  33. : Trustbuddy
  34. : Lexi visiting the beach
  35. : A Bit Weird
  36. : Weekend trip to Sirdal
  37. : Old sketches
  38. : HÃ¥vard's post stats
  39. : Rant about airports
  40. : The Good Old Days
  41. : Beetle
  42. : 206
  43. : Phone sketches
  44. : Noroff 3DDA
  45. : Panorama pictures, Sola and Stavanger