Saturday, May 9, 2015

Phaser tutorial: DronShooter - simple game in Typescript - Part 2

  





Previous Phaser tutorials:
Phaser tutorial: adding 9-patch image support to Phaser
Phaser tutorial: DronShooter - simple game in Typescript - Part 1
Phaser tutorial: custom easing functions for tweening and easing functions with parameters
Phaser tutorial: sprites and custom properties for atlas frames
Phaser tutorial: manage different screen sizes
Phaser tutorial: How to wrap bitmap text


 This is second part of short tutorial series on creating simple Phaser shooter game with Typescript. In Part 1 we ended with drone sprite hanging in the air above postapocalyptic city. In this part we will make our dron move along curve and we will also animate it little to make it alive.
 In the end of Part 2 our result will look like this:



Wiggle tween function


 Look at dron moving above city for a while. It makes quite a complex, but smooth move (reload page for another movement). It is thanks to our custom tweening function we will create. Let"s call it "wiggle".
 With tweens you can change some value to another in given time period with given function. In Phaser you can tween anything. Most often you will use it for changing something, which gives you visual feedback like position, scale or alpha. But you are not restricted to this, as you can tween for example score value to make it change from old value to new value gradually.

 Tween is created in two steps. First you have to create new tween and set its target. Target is object, which has some properties that will be tweened. In second step you set tween parameters as end value of the property, duration and easing function. Simple tween for moving dron from current x position to position 200 in 1 second (1000 milliseconds) linearly would look like this:

        var tween: Phaser.Tween = this.game.add.tween(dron);
        tween.to({ x: 200}, 1000, Phaser.Easing.Linear.None, true);

 There are also other possible parameters as delay before tween starts, whether it should repeat and how many times and so on. It is best to look into source code at tween\Tween.js.
 Phaser comes with several easing functions you can use out of the box. Common thing for all of them is, that it takes 1 parameter between 0 and 1 (easing progress - normalized tween duration) and returns value between 0 and 1. When property is tweened its current value is calculated like: old_value + easing_progress * (new_value - old_value). For example Cubic.In easing function has this shape:


 As said: in time 0 it returns value 0 and in the end of duration normalised to 0-1 it returns 1.

 For dron movement we will tweak it little. We will start with 0 and also end with 0. So there will not be any change in position in the end. If we moved from 0 to 0 (or current position to the same position) linearly, we would not see any movement. So we have to design our wiggle function that interesting things are taking place while tween runs.

 Imagine standard sin and cos functions for one period (2 * PI):


 Sin function is starting and ending with 0, while cos function starts and ends with 1. If we multiply them, we can be sure it will still start with zero and end with zero. Still not to much interesting:


 Let's now change number of periods for sin and cos independently. 2 periods for sin and 5 periods for cos:


 And multiply them...


 ... and we get quite interesting pattern for movement. We will start and end in the same x position, but during tween run, we will oscillate around this position in a way that may look random, but will be nice and smooth. Let's do the same for y position but with different periods for sin and cos (for example 7 and 3). The final path the dron will follow looks like this:


 This is how our wiggle function will look - the same function will be used for tweening x and y position.

function wiggle(aProgress: number, aPeriod1: number, aPeriod2: number): number {
    var current1: number = aProgress * Math.PI * 2 * aPeriod1;
    var current2: number = aProgress * Math.PI * 2 * aPeriod2;

    return Math.sin(current1) * Math.cos(current2);
}

 Place this piece of code just above window.onload() function. Also notice, that this function has 3 parameters instead of just one. We will have to find way how to call it from tween (I also wrote separate article on this topic - custom easing functions for tweening and easing functions with parameters )


Dron class


 First, change create function from Part 1 to this:

    create() {
        // background
        this.add.image(0, 0, "BG");

        // set physiscs to P2 physics engin
        this.game.physics.startSystem(Phaser.Physics.P2JS);

        // dron sprite
        var dron: Dron = new Dron(this.game, 320, 100, "Atlas", "dron1");
        // physics
        this.game.physics.enable(dron, Phaser.Physics.P2JS);
        dron.body.kinematic = true;
        // setup drton
        dron.setUp();
        // add dron to world group
        this.world.add(dron);
    }

 We are starting P2 physics system. Then we are creating instance of Dron class we will write soon. Then we enable physics for it and mark it as kinematic. Kinematic physical bodies are not affected with forces. After this we call dron's method setUp(), which will randomize behavior of our dron. In the last line we are adding this object into scene graph. World is just root Phaser.Group in scene graph.

 Now we will start Dron class. It will extend Phaser.Sprite, so we will have everything this class can do at our disposal and we will add some new functionality to it. Begin with setting anchor:

class Dron extends Phaser.Sprite {

    // -------------------------------------------------------------------------
    public setUp() {
        this.anchor.setTo(0.5, 0.5);

 Next, we will randomize position, range the dron will move in and also periods for sin and cos for both x and y direction. We also set our movement tweens:

        // random position
        this.reset(this.game.rnd.between(40, 600), this.game.rnd.between(60, 150));

        // random movement range
        var range: number = this.game.rnd.between(60, 120);
        // random duration of complete move
        var duration: number = this.game.rnd.between(30000, 50000);
        // random parameters for wiggle easing function
        var xPeriod1: number = this.game.rnd.between(2, 13);
        var xPeriod2: number = this.game.rnd.between(2, 13);
        var yPeriod1: number = this.game.rnd.between(2, 13);
        var yPeriod2: number = this.game.rnd.between(2, 13);

        // set tweens for horizontal and vertical movement
        var xTween = this.game.add.tween(this.body)
        xTween.to({ x: this.position.x + range }, duration, function (aProgress: number) {
            return wiggle(aProgress, xPeriod1, xPeriod2);
        }, true, 0, -1);

        var yTween = this.game.add.tween(this.body)
        yTween.to({ y: this.position.y + range }, duration, function (aProgress: number) {
            return wiggle(aProgress, yPeriod1, yPeriod2);
        }, true, 0, -1);

 From code, you can see there are two tweens. If we had sprite without physics, we would tween sprite's x and y (target for tween would be "this"). But, as we have physics body enabled on this sprite, we have to tween properties of "this.body".
 In tweens we bypassed requirement for easing function to have only one parameter. We nested call to our wiggle function into anonymous easing function.

 To make dron more alive we add some animations to it:

        // define animations
        this.animations.add("anim", ["dron1", "dron2"], this.game.rnd.between(2, 5), true);
        this.animations.add("explosion", Phaser.Animation.generateFrameNames("explosion", 1, 6, "", 3));

        // play first animation as default
        this.play("anim");
    }

 We call basic animation just "anim" and as it is simple animation with only two frames, we put the array with frames directly into code. Animation is blinking center of dron - we also randomize framerate for each created dron, so drones on screen will not blink synchronously. Second animation is longer - it takes 6 frames and it is explosion of dron. Here is shown another way how to create animation. Its name is explosion and instead of putting all 6 frame names into array we are generating this array with Phaser.Animation.generateFrameNames() method. It takes several parameters: prefix, first frame index, last frame index, suffix and zero padding. It returns array, in our case: ["explosion001", "explosion002", ..., "explosion006"].
 In the very end we start animation "anim" to play.

 Now we end our Dron class with explode function. We will use this function in next parts when we resolve collisions between drons and missiles:

    // -------------------------------------------------------------------------
    public explode() {
        // remove movement tweens
        this.game.tweens.removeFrom(this.body);
        // explode dron and kill it on complete
        this.play("explosion", 8, false, true);
    }
}

 Important is, we are stopping movement by removing all tweens from target, which is this.body. and starting explosion animation.



End of Part 2


 Now, your dron from Part 1 is nicely moving above the city. In Part 3 we will add cannon, missiles and keyboard controls. Here is complete code after this part:


// -------------------------------------------------------------------------
// -------------------------------------------------------------------------
// -------------------------------------------------------------------------
class Game extends Phaser.Game {
    // -------------------------------------------------------------------------
    constructor() {
        // init game
        super(640, 400, Phaser.CANVAS, "content", State);
    }
}

// -------------------------------------------------------------------------
// -------------------------------------------------------------------------
// -------------------------------------------------------------------------
class State extends Phaser.State {

    // -------------------------------------------------------------------------
    preload() {
        // background image
        this.game.load.image("BG", "bg.jpg");
        // load sprite images in atlas
        this.game.load.atlas("Atlas", "atlas.png", "atlas.json");
    }

    // -------------------------------------------------------------------------
    create() {
        // background
        this.add.image(0, 0, "BG");

        // set physiscs to P2 physics engin
        this.game.physics.startSystem(Phaser.Physics.P2JS);

        // dron sprite
        var dron: Dron = new Dron(this.game, 320, 100, "Atlas", "dron1");
        // physics
        this.game.physics.enable(dron, Phaser.Physics.P2JS);
        dron.body.kinematic = true;
        // setup drton
        dron.setUp();
        // add dron to world group
        this.world.add(dron);
    }
}

// -------------------------------------------------------------------------
// -------------------------------------------------------------------------
// -------------------------------------------------------------------------
class Dron extends Phaser.Sprite {

    // -------------------------------------------------------------------------
    public setUp() {
        this.anchor.setTo(0.5, 0.5);

        // random position
        this.reset(this.game.rnd.between(40, 600), this.game.rnd.between(60, 150));

        // random movement range
        var range: number = this.game.rnd.between(60, 120);
        // random duration of complete move
        var duration: number = this.game.rnd.between(30000, 50000);
        // random parameters for wiggle easing function
        var xPeriod1: number = this.game.rnd.between(2, 13);
        var xPeriod2: number = this.game.rnd.between(2, 13);
        var yPeriod1: number = this.game.rnd.between(2, 13);
        var yPeriod2: number = this.game.rnd.between(2, 13);

        // set tweens for horizontal and vertical movement
        var xTween = this.game.add.tween(this.body)
        xTween.to({ x: this.position.x + range }, duration, function (aProgress: number) {
            return wiggle(aProgress, xPeriod1, xPeriod2);
        }, true, 0, -1);

        var yTween = this.game.add.tween(this.body)
        yTween.to({ y: this.position.y + range }, duration, function (aProgress: number) {
            return wiggle(aProgress, yPeriod1, yPeriod2);
        }, true, 0, -1);

        // define animations
        this.animations.add("anim", ["dron1", "dron2"], this.game.rnd.between(2, 5), true);
        this.animations.add("explosion", Phaser.Animation.generateFrameNames("explosion", 1, 6, "", 3));

        // play first animation as default
        this.play("anim");
    }

    // -------------------------------------------------------------------------
    public explode() {
        // remove movement tweens
        this.game.tweens.removeFrom(this.body);
        // explode dron and kill it on complete
        this.play("explosion", 8, false, true);
    }
}

// -------------------------------------------------------------------------
// -------------------------------------------------------------------------
// -------------------------------------------------------------------------
function wiggle(aProgress: number, aPeriod1: number, aPeriod2: number): number {
    var current1: number = aProgress * Math.PI * 2 * aPeriod1;
    var current2: number = aProgress * Math.PI * 2 * aPeriod2;

    return Math.sin(current1) * Math.cos(current2);
}

// -------------------------------------------------------------------------
// -------------------------------------------------------------------------
// -------------------------------------------------------------------------
window.onload = () => {
    new Game();
};