Wednesday, January 13, 2016

Phaser tutorial: Using Phaser signals

 





Previous Phaser tutorials:
Phaser tutorial: Breaking the (z-order) law!
Phaser tutorial: Phaser and Spriter skeletal animation
Phaser tutorial: DronShooter - simple game in Typescript - Part 3
Phaser tutorial: DronShooter - simple game in Typescript - Part 2
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


 In many situations you need some mechanism how to notify one object in your game with something, that happen in another object of your game. There are several levels how to solve this from tight coupling of objects to loose coupling. If you also want to make parts of your game reusable, then we can say, there are several levels from bad solution to good one from this point of view.
 As this is Phaser tutorial, we will not reinvent the wheel, but describe mechanism, that is available in this engine - Phaser.Signal. But first, I will show some bad solutions as this will help to show advantages of signals later in contrast.


Worst case

 Imagine you have class Game, that counts score and you also have some class ScoreCounter responsible for displaying it:

            class Game {
                private _score: number;

                public get score(): number {
                    return this._score;
                }
            }

            class ScoreCounter {
                private _game: Game;

                public update() {
                    var actualScore = this._game.score;
                        :
                }
            }

 Class game keeps score amount. ScoreCounter is keeping reference to Game object and has some update method, which is executed every frame. When it is executed, then it asks game for current score and does something with it. It is inefficient as update is called every frame and also classes are tightly coupled. You can not take your ScoreCounter and put it simply into another game unless it has class Game with score property.


Bad case

 What if  we change it in way, that Game class will call some method in ScoreCounter when score is changed?

            class Game {
                private _score: number;
                private _scoreCounter: ScoreCounter;

                public addScore(points: number): void {
                    this._score.+= points;
                    this._scoreCounter.update(this._score);
                }
            }

            class ScoreCounter {
                public update(actualScore: number) {
                        :
                }
            }

 This is better. ScoreCounter's update method does not need to be executed regularly. Game class is keeping reference to ScoreCounter and when score is changed inside game, it calls update method of ScoreCounter. In this case, ScoreCounter can be in some kind of library which you can reuse for other game. But, what if we want to notify other class than ScoreCounter with minimum effort?


Listener

 Now, we will define interface IScoreListener. And every class that implements it can be notified of score change. Reference to listener is stored in Game class in _scoreListener variable. In our case it is again ScoreCounter that implements IScoreListener, but it can be any class. It just has to have onScoreChange method implemented.

            class Game {
                private _score: number;
                private _scoreListener: IScoreListener;

                public addScore(points: number): void {
                    this._score += points;
                    this._scoreListener.onScoreChange(this._score);
                }
            }

            interface IScoreListener {
                onScoreChange(points: number): void;
            }

            class ScoreCounter implements IScoreListener {
                public onScoreChange(actualScore: number): void {
                        :
                }
            }

 If interface definition and ScoreCounter are in your library class then it is reusable. Game does not know anything about ScoreCounter. It just knows about "some"  IScoreListener implementer. In your game you can then create other classes that implements this listener like MonsterCountingScore and if it implements IScoreListener, you will get monster that will get notified on score change (for example some boss inflating its size as you are hitting it and getting score for it). Just do not forget to change reference in Game class from ScoreCounter implementing IScoreListener to MonsterCountingScore implementing IScoreListener.
 We are getting close, but in current solution we can have only one listener. What if there is more of them - you want to notify ScoreCounter as well as monster.
 Solution is to create some list or array of IScoreListeners and call them all in for-loop. You would also need some add / remove mechanism how to add new listeners and remove old ones (like when monster is killed). Additional features like testing whether some listener is already on list would be handy. What about passing parameters, one-time listeners, ... If you are using Phaser than all of this is already implemented for you in Phaser.Signal class.


Phaser.Signal

 Signals are Phaser built in event dispatching mechanism supporting notification of one or more listeners.
 Let's start with simple example:

            class Game {
                public onScoreChange: Phaser.Signal = new Phaser.Signal();
                private _score;

                public addScore(points: number): void {
                    this._score += points;
                    this.onScoreChange.dispatch(this._score);
                }
            }

            class ScoreCounter {
                public setScore(actualScore: number): void {
                        :
                }
            }

 In this example all listeners stored in onScoreChange Phaser Signal will be notified with current score. Notification is fired with call to dispatch method. But in the beginning this list of listeners is empty. You have to fill it with couples method-context first.
 In example with interface we did not need context as listener reference was storing object. On the other hand that object had to implement specific interface. With Signals we can use any method, but we also have to provide correct context.
 Initialization for above example can look like this somewhere in your code:

            var game = new Game();
            var scoreCounter = new ScoreCounter();

            game.onScoreChange.add(scoreCounter.setScore, scoreCounter);



Phaser.Signal class features

 Phaser.Signal class has many features. Following I will describe most of them (for all methods see documentation):


add(listener: Function, listenerContext?: any, priority?: number, ...args: any[]): Phaser.SignalBinding;
addOnce(listener: Function, listenerContext?: any, priority?: number, ...args: any[]): Phaser.SignalBinding;

 These two methods adds listener to list. In second case event for this listener will be dispatched only once and then the listener will be removed from list.
 First two parameters are method and context you want to call. With next argument you can assign priority to your listeners in case you need some of them to be called earlier than others. Then, you can add any number of arguments that are specific for listener and that will be called to it with dispatch method. I will show example in moment.
 It also checks, whether listener is already on list to prevent multiple adding.


dispatch(...params: any[]): void;

 dispatch method fires notification process. You can add any number of parameters to it. In our original example we sent actual score value through it.

 Now, as we have some parameters on add(Once) method and dispatch method, let's see small example how it works:

            var signal = new Phaser.Signal();

            // listener 1
            signal.add(function () {
                console.log("listener 1");
                for (var i = 0; i < arguments.length; i++) {
                    console.log("argument " + i + ": " + arguments[i]);
                }
            }, this, 0, "listener - 1", "listener - 2");

            // listener 2
            signal.add(function () {
                console.log("listener 2");
                for (var i = 0; i < arguments.length; i++) {
                    console.log("argument " + i + ": " + arguments[i]);
                }
            }, this);

            //dispatch
            signal.dispatch("dispatch - 1", "dispatch - 2");

 Here we added two listeners to signal class instance. These listeners are anonymous functions and only thing they do is to output all parameters passed to it in console window. First listener is added with two extra parameters (strings): "listener - 1" and "listener - 2". In the end we call dispatch with parameters "dispatch - 1" and "dispatch - 2". Here is console output if you run the code:

listener 1
argument 0: dispatch - 1 
argument 1: dispatch - 2 
argument 2: listener - 1 
argument 3: listener - 2 
listener 2
argument 0: dispatch - 1 
argument 1: dispatch - 2

 As you can see first parameters our listeners got are those from dispatch call. After that follow listener parameters.


has(listener: Function, context?: any): boolean;

 has method returns true or false and allows you to test, whether Signal has listener in argument already attached.


remove(listener: Function, context?: any): Function;
removeAll(context?: any): void;

 With remove and removeAll you can remove particular listener from list while removeAll clears whole list. Method removeAll has optional context parameter, that allows you to remove all listeners, but only for given context.


dispose(): void;

 With dispose, you can clear list of listeners as well as all internal references to external objects. After this call you cannot use signal anymore and it is ready to be garbage collected.


Conclusion

 Hope this tutorial helped you to understand Phaser.Signals.







No comments:

Post a Comment