How to make Slither.io with JavaScript: Part 7 - Food & Conclusion

8/20/17

This is the seventh part of our tutorial series on creating Slither.io with JavaScript and Phaser! Take a look at Part 1 if you are just starting with this series, or jump back to Part 6 if you need to.

Look at the example and also look at the source code for this part.

Food Concepts

In this final part of our series, we will go over adding food that snakes can eat. Since this is our final part, the demo is the completed version!

Our food sprites will be tiny hexagons. These sprites will have circular physics bodies. When a food sprite collides with a snake head, a constraint will be created that makes it look like the food is gravitating in towards the head center. Once the food is at the center of the snake's head, it will be destroyed, and the snake will increase in size. The hexagon image will be white so that we can make food of any color, as you can see with hex.png.

Food

Take a look at the Food function in food.js:

Food = function(game, x, y) {
    this.game = game;
    this.debug = false;
    this.sprite = this.game.add.sprite(x, y, 'food');
    this.sprite.tint = 0xff0000;

    this.game.physics.p2.enable(this.sprite, this.debug);
    this.sprite.body.clearShapes();
    this.sprite.body.addCircle(this.sprite.width * 0.5);
    //set callback for when something hits the food
    this.sprite.body.onBeginContact.add(this.onBeginContact, this);

    this.sprite.food = this;

    this.head = null;
    this.constraint = null;
}

Here we are making a food sprite, giving it a circular physics body, setting a callback for when this body begins contact with a snake head, and finally creating properties head and constraint which will be used when the food touches a head.

Beginning Contact

Let's see the callback for when the food touches something:

onBeginContact: function(phaserBody, p2Body) {
    if (phaserBody && phaserBody.sprite.name == "head" && this.constraint === null) {
        this.sprite.body.collides([]);
        //Create constraint between the food and the snake head that
        //it collided with. The food is then brought to the center of
        //the head sprite
        this.constraint = this.game.physics.p2.createRevoluteConstraint(
            this.sprite.body, [0,0], phaserBody, [0,0]
        );
        this.head = phaserBody.sprite;
        this.head.snake.food.push(this);
    }
}

First we check if it is a snake head that the food has collided with. If it is, we then turn off all collisions for this food. We actually create a revolute constraint, which pulls the food in towards the snake head that it has touched. We set the head property to the snake head that the food has collided with, and we push this food to a food Array in the snake that I will explain in a moment.

Update

Look at the update method:

update: function() {
    //once the food reaches the center of the snake head, destroy it and
    //increment the size of the snake
    if (this.head && Math.round(this.head.body.x) == Math.round(this.sprite.body.x) &&
    Math.round(this.head.body.y) == Math.round(this.sprite.body.y)) {
        this.head.snake.incrementSize();
        this.destroy();
    }
}

Here we check if the food is approximately at the center of the head. When it is, we increment the snake size and destroy this food.

Destroy

As with everything else in our game, the food needs a destroy method:

destroy: function() {
    if (this.head) {
        this.game.physics.p2.removeConstraint(this.constraint);
        this.sprite.destroy();
        this.head.snake.food.splice(this.head.snake.food.indexOf(this), 1);
        this.head = null;
    }
}

We are only destroying it though once it has collided with a head. Then we remove the constraint, destroy the sprite, and remove the food from that food Array in the snake that I mentioned earlier.

Snake Food Array

In the Snake class, I have added the line:

this.food = [];

This array will contain any food that the snake is currently consuming, meaning food that it has collided with but has not destroyed yet. Why do we need this? If the snake is suddenly destroyed, all the food attached to it needs to be destroyed as well, because the food is constrained to the snake. Not handling this will result in errors. So let's add this to the snake destroy method:

//destroy food that is constrained to the snake head
for (var i = this.food.length - 1 ; i >= 0 ; i--) {
    this.food[i].destroy();
}

Adding Food to the Game

Finally, we need to actually add food and make sure it collides correctly. Take a look at game.js. We will add some groups and food in the create state:

this.foodGroup = this.game.add.group();
this.snakeHeadCollisionGroup = this.game.physics.p2.createCollisionGroup();
this.foodCollisionGroup = this.game.physics.p2.createCollisionGroup();

//add food randomly
for (var i = 0 ; i < 100 ; i++) {
    this.initFood(Util.randomInt(-width, width), Util.randomInt(-height, height));
}

The last few lines call the initFood method at some random positions. Take a look at this method:

initFood: function(x, y) {
    var f = new Food(this.game, x, y);
    f.sprite.body.setCollisionGroup(this.foodCollisionGroup);
    this.foodGroup.add(f.sprite);
    f.sprite.body.collides([this.snakeHeadCollisionGroup]);
    return f;
}

We add the food to some groups, and then we make sure it is only colliding with snake heads. After we add our snakes in the create method, we need to give them their own collision groups:

//initialize snake groups and collision
for (var i = 0 ; i < this.game.snakes.length ; i++) {
    var snake = this.game.snakes[i];
    snake.head.body.setCollisionGroup(this.snakeHeadCollisionGroup);
    snake.head.body.collides([this.foodCollisionGroup]);
    //callback for when a snake is destroyed
    snake.addDestroyedCallback(this.snakeDestroyed, this);
}

We set the snake heads to collide with the food, then we add a destruction callback.

Dropping Food on Destruction

Lastly, the most important part of our game: snakes dropping food when they die! Let's see our callback method in game.js when a snake is destroyed:

snakeDestroyed: function(snake) {
    //place food where snake was destroyed
    for (var i = 0 ; i < snake.headPath.length ;
    i += Math.round(snake.headPath.length / snake.snakeLength) * 2) {
        this.initFood(
            snake.headPath[i].x + Util.randomInt(-10,10),
            snake.headPath[i].y + Util.randomInt(-10,10)
        );
    }
}

We drop some food randomly near the snake's path, iterating through its path so that the food is spaced out evenly. And with that, our game is complete!

Conclusion

Congratulations! You are at the end of your Slither.io journey. Now its time to make your own IO game, or to add on to this one. Let me know if you want to see another series like this one. I will consider writing a tutorial on making this game multiplayer if there is enough interest.

This series would not have been possible without support from Phaser. Please take a moment to follow me on Twitter and give the source code of this tutorial series a star on Github!

Until next time,

Loonride

Up Next

Subscribe

  • No spam
  • Update information
  • Free newsletters

Invalid Email

Subscribed

Try Again Later