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.
Why snake?
- Using a known game I don't need to focus on the design.
- 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:
- A level, I use a grid that is 30 units wide and calculate the height to keep a unit square.
- A snake, where each body part is 1 unit large. It moves either UP, RIGHT, DOWN or LEFT.
- 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.