-
So I've added a function called Snake that's going to
be the bulk of the snake game.Outside of it, I've set the DOT_SIZE
and WIDTH and HEIGHT of canvas, as absolutes (so I can easily play
with it later.
Within the constructor, I've set width and height in
"dot" units (e.g. this.width = WIDTH / DOT_SIZE
) and set
locals (this.x
and this.y
) to half the width and
height.
Now I'll try to grab the canvas and draw a "dot" at [x, y].
-
Ok, dot drawn. Basically
ctx = document.querySelector('#snake').getContext('2d');, ctx.beginPath(); ctx.arc(...); ctx.fill();
-
Now I'll try to grab keyboard events.
Let's see: I've made a basic KeyboardListener prototype that
pretends it's an EventEmitter and listens to arrows and
WASD. Let's add it to the snake.
-
No, wait, I've added a repeat interval thingy to the
KeyboardListener. Now if you press and hold a key, it'll wait for
half a sec, then trigger the "move" event again and repeat every
200 milliseconds.
-
I've added a startTicks
method to Snake
.
It will run a setInterva
every second (actually every
MOVE_INTERVAL millis) and try to move our dot in that direction.
The moving part is done by a move
method. Oh well.
The same gor a GROW_INTERVAL
. Now, the cool thing,
is that it works - I can now move the snake, it'll get redrawn. I
am still not clearing screens on ticks, I wanna get the sizing
thing figured out first.
The only problem is I've replaced up-down with left-right so
let's remap thise this.x, this.y
bits.
Oh, I'm speeding it up for development purposes.
- Oh, nice:
- But I'm done for the night.
- Except that the towners said nice things about this, so now I
can't quit!
-
Now I need to make the snake not just leave the black trail,
but rather grow in size. I'll try +1 for every tick.
How to do that? I have two ideas. And it appears the townies
suggest the easier one. So, the easy idea is to just keep a
list of the tail coordinates in an array. As in, head would be
[x, y]
and the tail is [ [x, y - 1], [x, y - 2], [x + 1, y - 2] ]
etc etc. It could grow a bit, but shouldn't be too hard. And
it's going to be easier to get the matches (as in, did we crash
into something).
The other idea I've had is to keep tail as a bunch of pointers:
['down', 'left', 'left', 'up'...]
but that would be a
nightmare to calculate redraws on changing directions.
selfsame says he went with the second
version here.
Kudos!
So let's build the easy version.
-
So how it's supposed to work? E.g.
- Head is at X, Y, say,
[5, 7]
. Going up.
- Tail has size 3.E.g.
[[5, 6], [5, 5], [4, 5]
- We move head one more up.
[5, 8]
.
-
We move the first tail-part to where the head was.
- We move the next part to where the one before was.
- Go on until we get them all moved.
- If size of the snake is greater than current (we grew on
last tick), add one more part to where the last tail part was.
So, something like:
tail.unshift(head);
if (tail.length > size - 1) {
tail.pop();
}
Woot! Woot!
Ok, some glitches, but still!
So, next: collision detection (so we don't drive over ourselves
and remove that glitch when wall-crossing.
- But before that, I've added
freeze
and
unfreeze
methods and bound to a button-click so we can
stop and inspect the state when needed.
- Thought I'd add that I'm trying to add all the
properties of the Snake class in the Snake constructor. It probably
doesn't matter,it's not like I'll have a huge number of these all at once anyway.
Now, about that collision detection. I need to be able to detect,
on the "move" call, if the tile I'm stepping into occupied or not.
Now, off the top of my head, I can think of two ways. One is to
check if [x, y]
is already within the snake. If yes,
bummer, if no, all ok. The thing is, I might be adding other
objects on the game board later.
So the other idea is that the game board itself holds a list of
the objects on it, and it exposes an API to check if a tile is
ocupied or not.
But since the "game board" right now is a canvas and it doesn't
even have a state or anything, I'll just go with the first idea.
-
Wow! That was unexpectedly long. First, collision detection was
easy. We now have a snake.isOccupied(x, y)
which
returns true or false. So our move()
call will first
check if the new position is occupied before moving the snake. If
yes, we call a this.collide();
and bail.
Now that collide function was interesting. I'm painting a red dot
over the intended (occupied) spot, with the same this.dot
but it doesn't work! So I find out you need to context.closePath()
if you're painting with arc()
.
But still no luck! Even when I manually draw another, red dot,
wherever, it's still all gray. Only when I, after the game was over,
manually grabbed that context and snake and drew a dot (snake.dot(x, y, color)
did I get a dot. Why? Then I realized - in my ticker, I call
this.move
then this.redraw
. So I was
overwriting my shiny new red dot!
Anyway, now we have collision detection.
Next up, score!
- (a while later...)
So, score!
First, some usability fixes - add a "reset/restart game" button.
I think we can just forget the current snake and snake = new Snake();
. That should cleanly reset everything.
Next, score. Every time a snake moves would be a bit too much.
But maybe an ultra-silent, fast ticker, that gives one point every
10 or so milliseconds? Just to see the counter growing? Let's try.
Also, every time to grow a tail part, add X to score. Now, how
much? Let's say just surviving for a second would give you, I don't
know, 5 points? And a tail part growing gives you 5 more?
Those should be simple, one is a global (snake-global) interval,
activated on first time move, and frozen when the game is frozen.
The other is triggered on the "grow" interval.
Also, display and refresh? Let's just add a simple div and
put the number there and update on every update.
Ok, well. snake = new Snake(); works perfectly. Now I don't
have to reload to restart the game.
Displaying score every 10 miliseconds is not a problem. But it
feels like the score is growing way too fast. So maybe only have
1 score per second. And 5 per tail growth.
-
So the new numbers work way better. We have a bug, we don't resume
on unfreeze, but otherwise scoring is much better now. I want to
abstract the UI display away though, so that that happens maybe with
a redraw?
-
Also, I've noticed that every time there's a formula or something,
first I write the number (e.g.
this.score = this.score + 5 * some_calculation
).
But right after that, I delete it and put it a CAPS constant, e.g. SCORE_PER_SECOND in this case.
Not sure why is that. I have a fear of magic numbers, I guess.
- Bug fixed, it was a stray
this.paused = true;
.