How to make Slither.io with JavaScript: Part 5 - Snake Eyes

7/23/17

This is the fifth 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 4 if you need to.

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

Eye Concepts

In this part of our tutorial series we will be adding eyes to the snake heads. Now let's go over what we want the eyes to do.

Firstly, we need white circles for the back parts of the eyes. These white circles will be locked to the snake heads using a lock constraint. Next, we need black circles for the front parts of the eyes. These black circles will need to stay within the white circles, just so that they can move around like eye pupils. We will keep these black circles near the centers of the white circles using distance constraints, meaning that they can only move a certain distance away from their centers. To make these black circles look towards the mouse pointer, we will apply small forces to them in the direction of the mouse from the head.

Organization

As far as organization, we will make two new classes, Eye and EyePair. The Eye class will handle constraints for a single eye to the head, and the EyePair class will handle positioning of two eyes.

As with other parts of the snakes, the eyes will have to be scaled and destroyed. There will need to be methods for this in the eye classes that can be called from the snake class controlling the eyes.

Eye

Let's start coding. Take a look at eye.js. First we create the Eye function:

Eye = function(game, head, scale) {
    this.game = game;
    this.head = head;
    this.scale = scale;
    this.eyeGroup = this.game.add.group();
    this.collisionGroup = this.game.physics.p2.createCollisionGroup();
    this.debug = false;

    //constraints that will hold the circles in place
    //the lock will hold the white circle on the head, and the distance
    //constraint (dist) will keep the black circle within the white one
    this.lock = null;
    this.dist = null;

    //initialize the circle sprites
    this.whiteCircle = this.game.add.sprite(
        this.head.body.x, this.head.body.y, "eye-white"
    );
    this.whiteCircle = this.initCircle(this.whiteCircle);

    this.blackCircle = this.game.add.sprite(
        this.whiteCircle.body.x, this.whiteCircle.body.y, "eye-black"
    );
    this.blackCircle = this.initCircle(this.blackCircle);
    this.blackCircle.body.mass = 0.01;
}

We create some groups, then we create properties for the lock and distance constraints, though we do not actually create the constraints yet. We create the black and white circles, then call initCircle on them to add some basic physics properties to these circle sprites. If you look in the asset folder, you will see the new circle images that we are using.

Now let's look at initCircle in the Eye prototype:

initCircle: function(circle) {
    circle.scale.setTo(this.scale);
    this.game.physics.p2.enable(circle, this.debug);
    circle.body.clearShapes();
    //give the circle a circular physics body
    circle.body.addCircle(circle.width*0.5);
    circle.body.setCollisionGroup(this.collisionGroup);
    circle.body.collides([]);
    this.eyeGroup.add(circle);
    return circle;
}

Here we simply give the circles circular physics bodies, remove collisions for these bodies, and add the circles to a group. Now we need a way to add the constraints.

Constraints

Let's look at the updateConstraints method, which should be called at least once to add the constraints:

updateConstraints: function(offset) {
    //change where the lock constraint of the white circle
    //is if it already exists
    if (this.lock) {
        this.lock.localOffsetB = [
            this.game.physics.p2.pxmi(offset[0]),
            this.game.physics.p2.pxmi(Math.abs(offset[1]))
        ];
    }
    //create a lock constraint if it doesn't already exist
    else {
        this.lock = this.game.physics.p2.createLockConstraint(
            this.whiteCircle.body, this.head.body, offset, 0
        );
    }

    //change the distance of the distance constraint for
    //the black circle if it exists already
    if (this.dist) {
        this.dist.distance = this.game.physics.p2.pxm(this.whiteCircle.width*0.25);
    }
    //create a distance constraint if it doesn't exist already
    else {
        this.dist = this.game.physics.p2.createDistanceConstraint(
            this.blackCircle.body, this.whiteCircle.body, this.whiteCircle.width*0.25
        );
    }
}

This method take's a parameter offset which is an Array in the form [x,y] telling where to offset the eye from the center of the head, placing a lock constraint in that position. Why do we check whether constraints exist or not? If they already exist, we are probably changing the scale of the eye, so we need to simply change where the eye is offset and how far the black circle can go within the white circle.

Scale

Next we have the scale method:

setScale: function(scale) {
    this.scale = scale;
    for (var i = 0 ; i < this.eyeGroup.children.length ; i++) {
        var circle = this.eyeGroup.children[i];
        circle.scale.setTo(this.scale);
        //change the radii of the circle bodies using pure p2 physics
        circle.body.data.shapes[0].radius = this.game.physics.p2.pxm(circle.width*0.5);
    }

}

We iterate through the circles, scaling up their sprites, then scaling up their physics bodies.

Update

We need an update method to move the black part of the eyes based on mouse position. Let's take a look:

update: function() {
    var mousePosX = this.game.input.activePointer.worldX;
    var mousePosY = this.game.input.activePointer.worldY;
    var headX = this.head.body.x;
    var headY = this.head.body.y;
    var angle = Math.atan2(mousePosY-headY, mousePosX-headX);
    var force = 300;
    //move the black circle of the eye towards the mouse
    this.blackCircle.body.moveRight(force*Math.cos(angle));
    this.blackCircle.body.moveDown(force*Math.sin(angle));
}

Here we find the angle of the line formed by the center of the snake head and the mouse position. Then we apply a force on the black circle towards the mouse. I ensured a specific force because a force that is too small would make the eyes lazy and a force that is too large could cause physics glitches.

Destroy

Finally, we have a destroy method:

destroy: function() {
    this.whiteCircle.destroy();
    this.blackCircle.destroy();
    this.game.physics.p2.removeConstraint(this.lock);
    this.game.physics.p2.removeConstraint(this.dist);
}

We must make sure to destroy the constraints!

EyePair

Now take a look at eyePair.js. This class will create two eyes, then act as a driver for them that calls their methods:

EyePair = function(game, head, scale) {
    this.game = game;
    this.head = head;
    this.scale = scale;
    this.eyes = [];

    this.debug = false;

    //create two eyes
    var offset = this.getOffset();
    this.leftEye = new Eye(this.game, this.head, this.scale);
    this.leftEye.updateConstraints([-offset.x, -offset.y]);
    this.eyes.push(this.leftEye);

    this.rightEye = new Eye(this.game, this.head, this.scale);
    this.rightEye.updateConstraints([offset.x, -offset.y]);
    this.eyes.push(this.rightEye);
}

We create two eyes, then call their updateConstraints methods using offsets obtained from a method called getOffset. Let's take a look at it now in the EyePair prototype:

getOffset: function() {
    var xDim = this.head.width*0.25;
    var yDim = this.head.width*.125;
    return {x: xDim, y: yDim};
}

We position the eyes relative to the width of the snake head. So if the head is scaled up, the width of the head is larger, and therefore the offset will be different.

Finally, we have methods that call the eyes' methods for scaling, updating, and destruction:

/**
 * Set the scale of the eyes
 * @param  {Number} scale new scale
 */
setScale: function(scale) {
    this.leftEye.setScale(scale);
    this.rightEye.setScale(scale);
    //update constraints to place them at the right offset
    var offset = this.getOffset();
    this.leftEye.updateConstraints([-offset.x, -offset.y]);
    this.rightEye.updateConstraints([offset.x, -offset.y]);
},
/**
 * Call from snake update loop
 */
update: function() {
    for (var i = 0 ; i < this.eyes.length ; i++) {
        this.eyes[i].update();
    }
},
/**
 * Destroy this eye pair
 */
destroy: function() {
    this.leftEye.destroy();
    this.rightEye.destroy();
}

Snake

Of course, we need to still implement this EyePair within the Snake class. Open snake.js. First we initialize an EyePair object:

this.eyes = new EyePair(this.game, this.head, this.scale);

In the snake's update method, we need to call the eyes update method:

this.eyes.update();

We scale the eyes in the snake scale method:

this.eyes.setScale(scale);

and we destroy the eyes in the snake destroy method:

this.eyes.destroy();

And that's what it takes to give a snake eyes! In Part 6 we will add shadows under the snakes.

Until next time,

Loonride

Up Next

Subscribe

  • No spam
  • Update information
  • Free newsletters

Invalid Email

Subscribed

Try Again Later