Snake - iameven.com

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.

Post install Tetris