For over a year I worked on an HTML5 game project as a hobby that I haven’t released (yet?). It’s called Chain of Heroes, and it’s like Snake meets Diablo. I’m not here to talk about my game though, I want to talk about how the snake moves. You probably have already heard of the game Snake, it looks like this:
One of the things I don’t like about traditional Snake is that movement is tied to a grid. That means that every time the snake moves, it moves by one width / height of a body segment. e.g. in this picture, the snake moves the distance of a green circle. Instead of that, I wanted it to be possible to move by fractions of a body segment so that movement appeared much more fluid. It was very difficult for me to figure out how to do this and I didn’t get it right on the first attempt. In this article, I’m going to explain how I pulled it off.
First, I want to give credit where credit is due. Most of this advice came from someone who took the time to help me on reddit and the original thread is here.
Lets talk about each segment as if it were a class. These would be the fields in it:
Imagine the game has just started and the snake is going from left to right. In this case, the currentDirection would be “right”, the currentPosition holds the location of each segment and the list of pivots would be empty. A Pivot is a class with a Point and a Direction , and it represents a position where the snake should change direction from its current course. For example, if the snake is going from left to right and the player presses “down”, that will add a pivot to each segment that says, “move down when you get to position [x=10, y=0]”. Then, each time movement is calculated, the list of each segment’s pivots are checked to see if they’re on a pivot and if they are, the currentDirection is changed to the pivot’s direction and then that pivot is removed from the list.
Hopefully that makes sense so far, but this is kind of hand wavy and has nothing to do with grid-less movement yet. If you tried to use this logic with grid-less movement, it would be a mess: Lets say you want to move 100 pixels per second but you want to update the snake’s position 7 times per second. 7 doesn’t divide evenly into 100. That means each update, your segments will be on positions that are fractional numbers with precision loss.
This causes a problem because if you let a pivot occur anywhere (eg: [x: 105.323, y: 520.1202]), then what are the odds that a segment will land on that exact location? The moons would have to align perfectly. This is bad because if a segment misses its pivot, it will disconnect from the rest of the snake and continue moving as if a train car disconnected from the rest of a train. You need to put some restrictions on movement to avoid this problem.
The first thing you need is a movement accumulator. This accumulator should be a Double. When this accumulator reaches 1, it means you move each segment 1 pixel. If it’s less than 1, you don’t move the snake at all. If the accumulator is 3.5, you move each segment by 1 pixel 3 times. Each time you move by one pixel, you check to see if you’re on a pivot and if you are, you change direction appropriately for that segment. This fixes multiple issues:
- The pivot’s position will always be in whole numbers
- The current position of the segment will always be whole numbers
- By moving at most 1 pixel at a time, you can’t “jump” over a pivot and have a segment disconnect from the rest of the train
If you have remainders in your movement accumulator (e.g. 0.235 left over), you keep that remainder for the next update.
Here’s some code to show how this whole thing works. This method is called every update:
root.unit.moveSegments = (gameState, nextDir) ->
frontSegment = gameState.segments
speed = gameState.speed
gameState.movementAccumulator += speed
while gameState.movementAccumulator >= 1
for segment in gameState.segments
segment.pos = getNewPositionFromMovement(segment, segment.direction)
if frontSegment.direction isnt nextDir #if direction changed, then add a pivot
for segment, i in gameState.segments
addPivot(segment, root.unit.createPivot(frontSegment.pos, nextDir)) if i isnt 0 #add pivot to each segment unless it's the front
frontSegment.direction = nextDir