Tutorials

Making a Simple Game - Part 4

You can find the full project here. If you haven't see Part 1, Part 2 and Part 3 read them first.

The Football

The football is the center of attention in our Keepy Up game. It responds to player input, it responds to the environment (well, gravity), it makes sounds. It's probably the most complicated part of the game. Fortunately, we're going to explain all the bits to your as simply as we can.

ball.js

var Ball = pc.createScript('ball');

Ball.attributes.add('gravity', {
    type: 'number',
    default: -9.8,
    description: 'The value of gravity to use'
});

Ball.attributes.add('defaultTap', {
    type: 'number',
    default: 5,
    description: 'Speed to set the ball to when it is tapped'
});

Ball.attributes.add('impactEffect', {
    type: 'entity',
    description: 'The particle effect to trigger when the ball is tapped'
});

Ball.attributes.add('ballMinimum', {
    type: 'number',
    default: -6,
    description: 'When ball goes below minimum y value game over is triggered'
});

Ball.attributes.add('speedMult', {
    type: 'number',
    default: 4,
    description: 'Multiplier to apply to X speed when tap is off center'
});

Ball.attributes.add('angMult', {
    type: 'number',
    default: -6,
    description: 'Multiplier to apply to angular speed when tap is off center'
});

Ball.tmp = new pc.Vec3();

// initialize code called once per entity
Ball.prototype.initialize = function() {
    this.paused = true;

    // Get the "Game" Entity and start listening for events
    this.game = this.app.root.findByName("Game");

    this.app.on("game:start", this.unpause, this);
    this.app.on("game:gameover", this.pause, this);
    this.app.on("game:reset", this.reset, this);

    // Initialize properties
    this._vel = new pc.Vec3(0, 0, 0);
    this._acc = new pc.Vec3(0, this.gravity, 0);
    this._angSpeed = 0;

    // Store the initial position and rotation for reseting
    this._origin = this.entity.getLocalPosition().clone();
    this._rotation = this.entity.getLocalRotation().clone();
};

// update code called every frame
Ball.prototype.update = function(dt) {
    // Don't update when paused
    if (this.paused) {
        this.entity.rotate(0, 30*dt, 0);
        return;
    }

    var p = this.entity.getLocalPosition();
    var tmp = Ball.tmp;

    // integrate the velocity in a temporary variable
    tmp.copy(this._acc).scale(dt);
    this._vel.add(tmp);

    // integrate the position in a temporary variable
    tmp.copy(this._vel).scale(dt);
    p.add(tmp);

    // update position
    this.entity.setLocalPosition(p);

    // rotate by angular speed
    this.entity.rotate(0, 0, this._angSpeed);

    // check for game over condition
    if (p.y < this.ballMinimum) {
        this.game.script.game.gameOver();
    }
};

/*
 * Called by the input handler to tap the ball up in the air
 * dx is the tap distance from centre of ball in x
 * dy is the tap distance from centre of ball in y
 */
Ball.prototype.tap = function (dx, dy) {
    // Update velocity and spin based on position of tap
    this._vel.set(this.speedMult * dx, this.defaultTap, 0);
    this._angSpeed += this.angMult * dx;

    // calculate the position of the tap in world space
    var tmp = Ball.tmp;
    tmp.copy(this.entity.getLocalPosition());
    tmp.x -= dx;
    tmp.y -= dy;

    // trigger particle effect to tap position, facing away from the center of the ball
    this.impactEffect.setLocalPosition(tmp);
    this.impactEffect.particlesystem.reset();
    this.impactEffect.particlesystem.play();
    this.impactEffect.lookAt(this.entity.getPosition());

    // play audio
    this.entity.sound.play("bounce");

    // increment the score by 1
    this.game.script.game.addScore(1);
};

// Pause the ball update when not playing the game
Ball.prototype.unpause = function () {
    this.paused = false;

    // start game with a tap
    this.tap(0, 0);
};

// Resume ball updating
Ball.prototype.pause = function () {
    this.paused = true;
};

// Reset the ball to initial values
Ball.prototype.reset = function () {
    this.entity.setLocalPosition(this._origin);
    this.entity.setLocalRotation(this._rotation);
    this._vel.set(0,0,0);
    this._acc.set(0, this.gravity, 0);
    this._angSpeed = 0;
};

Script Attributes

The first thing you'll notice at the top of the script are a set of script attributes that we've defined. Defining script attributes lets you expose values from your script into the editor. There are three very good reasons to do this.

Script Attributes

First, it lets you use the same script for many different Entities with different values. For example, you could have a script attribute which sets a color, and in the editor create a red, blue and green version of a entity just by modifying the script attribute.

Second, you can quickly and easily tune the behavior of scripts. When you modify a script attribute (or indeed any property from the editor) the changes are made instantly to any instance of the game that you have launched from the editor. So for example in the case of the ballMinimum property we define here, you can launch the game and test what the value of ballMinimum should be to allow the ball to drop off the bottom of the screen without ever having to reload the game. Test the game, modify the value, test the game.

This is known as "iteration speed". The faster you can modify and test your game, the quicker you can get it developed!

For the ball, we define script attributes that let us tweak a number of game play properties like the gravity, the impulse applied when the ball is tapped. These attributes let us very quickly tune the game to our liking.

Third, the script attribute is a great way to link a script to an Entity or an Asset in your scene. For example, the ball script needs to trigger a particle effect when it is tapped. The particle effect is on another Entity in our scene. We define a script attribute called impactEffect of type entity and in the Editor we link this to the entity with our particle effect. Our script now has a reference to the entity and we are free to modify this entity or change to another entity without breaking our code.

The Physics Simulation

For those of you with some basic vector maths knowledge this update() loop of the ball should be simple, but for everyone else we'll explain a little about simulating a ball in a video game.

A simple way to simulate something in video game is to give that object an acceleration, a velocity and a position. Every time step (or frame) the acceleration (which the rate of change velocity) changes the velocity and the velocity (which is the rate of change of position) changes the position. Then you draw your object at the new position.

You can influence the position of your object in one of three ways.

In our simulation we have a constant acceleration due to gravity, when you tap the ball we apply an instant change in velocity and when you reset the game we teleport the ball back to it's starting position.

Simulating

The update loop does this:

(Change in Velocity) = (Acceleration) * (Time since last frame)

(New Velocity) = (Old Velocity) + (Change in Velocity)

(Change in Position) = (New Velocity) * (Time since last frame)

(New Position) = (Old Position) + (Change in Position)

In code this looks like this:

var p = this.entity.getLocalPosition();

// integrate the velocity in a temporary variable
tmp.copy(this._acc).scale(dt);
this._vel.add(tmp);

// integrate the position in a temporary variable
tmp.copy(this._vel).scale(dt);
p.add(tmp);

// update position
this.entity.setLocalPosition(p);

You will note that we use temporary vector tmp to store intermediate values. It's important not to create a new vector every frame for this. Also notice that we have to call setLocalPosition to apply the updated position.

Finally, for a nice effect we add rotate the ball by the angular speed value using entity.rotate(). This isn't very physically accurate, but it looks nice.

Responding to input

You may remember from Part 2 that the input.js script checked to see if an input has hit the ball and if so it calls the tap() method. The tap() method defined above applies a direct change to the velocity and the angular speed of the ball. We use a couple of our script attributes this.speedMult and this.angMult to multiple the new velocity and angular speed to match our expectations of the gameplay.

We also use the tap method to trigger a particle dust cloud at the point of impact and play a sound effect. We'll talk about particle and sounds in Part 5.

Summary

The ball script runs a simply physical simulation to make the ball fall under gravity and respond to taps. It also listens for game events to know when to pause and reset. Finally, it interacts with some other systems to show particle effects and play sounds.