Posted on Leave a comment

[19- Pixi教學] 連連看盤面實作

PIXI顯示元件介紹

PixiJS裡較常使用的顯示元件有:PIXI.ContainerSpriteAnimatedSprite

PIXI.Container在官網上的解釋如下:

A Container represents a collection of display objects. It is the base class of all display objects that act as a container for other objects.

簡而言之Container就是一個可以放其他顯示元件的一個容器,像SpriteAnimatedSprite等也都是繼承自Container的顯示元件,因此也都可以再在裡面放其他的顯示元件。
下面是一個簡單的使用範例:

let container = new PIXI.Container();
container.addChild(sprite);

Sprite是可以放單張的靜態圖檔元件。

The Sprite object is the base for all textured objects that are rendered to the screen

下面是一個簡單的使用範例:

PIXI.loader.add("assets/spritesheet.json").load(setup);

function setup() {
  let sheet = PIXI.loader.resources["assets/spritesheet.json"].spritesheet;
  let sprite = new PIXI.Sprite(sheet.textures["image.png"]);
  ...
}

AnimatedSprite則可以把多張連續的圖檔播放成連續的動畫。

An AnimatedSprite is a simple way to display an animation depicted by a list of textures.

下面是一個簡單的使用範例:

PIXI.loader.add("assets/spritesheet.json").load(setup);

function setup() {
  let sheet = PIXI.loader.resources["assets/spritesheet.json"].spritesheet;
  animatedSprite = new PIXI.extras.AnimatedSprite(sheet.animations["image_sequence"]);
  ...
}

連連看邏輯程式

之前在第一部份我們完成的程式碼在此:ironman20181022

在這一篇我們要將第一篇所寫完的連線邏輯套入pixiJS版本的連連看遊戲之中。

搬移邏輯部份的檔案

在邏輯程式的部份,PathBoardDirection都可以直接移來專案內使用。
這些程式碼的邏輯解釋在前幾篇的系列文中都有詳細的說明。

新增Path.ts內容如下:

import Point = PIXI.Point;
import {Board} from "./Board";
import {Direction} from "./Direction";

export class Path {
    public point1:Point;
    public point2: Point;
    readonly board: Board;
    public path_Detail:Array<point>;

    constructor(point1: Point, point2: Point, board: Board) {
        this.point1 = point1;
        this.point2 = point2;
        this.board = board;
    }

    public canLinkInLine(): boolean {


        //從上面消
        let point1UP = this.board.getNearByPointByDirection(this.point1, Direction.UP);
        let point2UP = this.board.getNearByPointByDirection(this.point2, Direction.UP);
        {
            let min = Math.max(point1UP.x,point2UP.x);
            let max = Math.min(this.point1.x, this.point2.x);
            for (var i = max;i>=min;i--){
                if (!this.board.hasMiddleValue(new Point(i, this.point1.y), new Point(i, this.point2.y))){
                    this.path_Detail = [this.point1,new Point(i, this.point1.y),new Point(i, this.point2.y),this.point2];
                    return true;
                }
            }
        }
        //從下面消
        let point1DOWN = this.board.getNearByPointByDirection(this.point1, Direction.DOWN);
        let point2DOWN = this.board.getNearByPointByDirection(this.point2, Direction.DOWN);
        {
            let max = Math.min(point1DOWN.x,point2DOWN.x);
            let min = Math.max(this.point1.x, this.point2.x);
            for (var i = min;i<=max;i++){
                if (!this.board.hasMiddleValue(new Point(i, this.point1.y), new Point(i, this.point2.y))){
                    this.path_Detail = &#91;this.point1,new Point(i, this.point1.y),new Point(i, this.point2.y),this.point2&#93;;
                    return true;
                }
            }
        }

        //從左邊消
        let point1LEFT = this.board.getNearByPointByDirection(this.point1, Direction.LEFT);
        let point2LEFT = this.board.getNearByPointByDirection(this.point2, Direction.LEFT);
        {
            let min = Math.max(point1LEFT.y,point2LEFT.y);
            let max = Math.min(this.point1.y, this.point2.y);
            for (var i = max;i>=min;i--) {
                if (!this.board.hasMiddleValue(new Point(this.point1.x, i), new Point(this.point2.x, i))) {
                    this.path_Detail = [this.point1, new Point(this.point1.x, i), new Point(this.point2.x, i), this.point2];
                    return true;
                }
            }
        }

        //從右邊消
        let point1RIGHT = this.board.getNearByPointByDirection(this.point1, Direction.RIGHT);
        let point2RIGHT = this.board.getNearByPointByDirection(this.point2, Direction.RIGHT);
        {
            let max = Math.min(point1RIGHT.y,point2RIGHT.y);
            let min = Math.max(this.point1.y, this.point2.y);
            for (var i = min;i<=max;i++) {
                if (!this.board.hasMiddleValue(new Point(this.point1.x, i), new Point(this.point2.x, i))) {
                    this.path_Detail = &#91;this.point1, new Point(this.point1.x, i), new Point(this.point2.x, i), this.point2&#93;;
                    return true;
                }
            }
        }
        //左右連消
        if (this.point1.y != this.point2.y){
            let leftPoint = (this.point1.y < this.point2.y) ? this.point1:this.point2;
            let rightPoint = (this.point1.y >= this.point2.y) ? this.point1:this.point2;
            let leftPointRIGHT = this.board.getNearByPointByDirection(leftPoint, Direction.RIGHT);
            let rightPointLEFT = this.board.getNearByPointByDirection(rightPoint, Direction.LEFT);
            if (leftPointRIGHT.y != leftPoint.y && rightPointLEFT.y != rightPoint.y){
                for (var i = rightPointLEFT.y; i <= leftPointRIGHT.y; i++) {
                    if (!this.board.hasMiddleValue(new Point(leftPoint.x, i), new Point(rightPoint.x, i))) {
                        this.path_Detail = &#91;leftPoint, new Point(leftPoint.x, i), new Point(rightPoint.x, i), rightPoint&#93;;
                        return true;
                    }
                }
            }
        }

        //上下連消
        if (this.point1.x != this.point2.x){
            let upPoint = (this.point1.x < this.point2.x) ? this.point1:this.point2;
            let downPoint = (this.point1.x >= this.point2.x) ? this.point1:this.point2;
            let upPointDOWN = this.board.getNearByPointByDirection(upPoint, Direction.DOWN);
            let downPointUP = this.board.getNearByPointByDirection(downPoint, Direction.UP);

            if (upPointDOWN.x != upPoint.x && downPointUP.x != downPoint.x){
                for (var i = downPointUP.x; i <= upPointDOWN.x; i++) {
                    if (!this.board.hasMiddleValue(new Point(i, upPoint.y), new Point(i, downPoint.y))) {
                        this.path_Detail = &#91;upPoint, new Point(i, upPoint.y), new Point(i, downPoint.y), downPoint&#93;;
                        return true;
                    }
                }
            }
        }

        return false;
    }
}&#91;/code&#93;

新增<code>Board.ts</code>,內容如下:
[code lang="js"]import {Path} from "./Path";
import Point = PIXI.Point;
import {Direction} from "./Direction";

export class Board {
    public board: Array<array<number>>;

    constructor() {
        let content = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24];
        //產生初始局面
        let length = 10;
        let data = content.concat(content).concat(content).concat(content).sort((a, b) => (Math.random() > .5) ? 1 : 0);
        this.board = []
        for (var i = 0;i<length;i++){
            this.board.push(data.slice(i*length, (i+1)*length))
        }
    }

    public gameRoundEnd():boolean{
        for (var i =0;i<this.board.length;i++){
            for (var j = 0; j<this.board&#91;i&#93;.length;j++){
                if(this.board&#91;i&#93;&#91;j&#93; != null){
                    return false;
                }
            }
        }
        return true;
    }

    public getFirstExistPath():Path{
        var searchedValue = &#91;&#93;;
        for (var i =0;i<this.board.length;i++){
            for (var j = 0; j<this.board&#91;i&#93;.length;j++){
                let value = this.board&#91;i&#93;&#91;j&#93;;
                if(value!= null && searchedValue.indexOf(value) == -1){
                    searchedValue.push(value);
                    let positionsArr = this.getPositionByValue(value);
                    let permutationsArr = this.getPairNumPermutations(positionsArr.length);
                    for(var k = 0;k<permutationsArr.length;k++){
                        let v = permutationsArr&#91;k&#93;;
                        let path = new Path(positionsArr&#91;v&#91;0&#93;&#93;, positionsArr&#91;v&#91;1&#93;&#93;,this);
                        if(path.canLinkInLine()){
                            return path;
                        }
                    }
                }
            }
        }
        return null;
    }

    private getAllValueInBoard(){
        let values = &#91;&#93;;
        for (var i =0;i<this.board.length;i++){
            for (var j = 0; j<this.board&#91;i&#93;.length;j++){
                if(this.board&#91;i&#93;&#91;j&#93; != null){
                    values.push(this.board&#91;i&#93;&#91;j&#93;);
                }
            }
        }
        return values;
    }

    public rearrangeBoard(){
        let values = this.getAllValueInBoard().sort((a, b) => (Math.random() > .5) ? 1 : 0);
        for (var i =0;i<this.board.length;i++){
            for (var j = 0; j<this.board&#91;i&#93;.length;j++){
                if(this.board&#91;i&#93;&#91;j&#93; != null){
                    this.board&#91;i&#93;&#91;j&#93; = values.pop();
                }
            }
        }
    }

    private pairNumPermutations = {};
    /**
     * 取得輸入的index中,2個2個一組的所有可能排列組合
     */
    public getPairNumPermutations(num:number){
        if(this.pairNumPermutations&#91;num&#93; != null){
            return this.pairNumPermutations&#91;num&#93;;
        }
        let data = &#91;&#93;;
        for(var i = 0; i <num;i++){
            for(var j = 0; j <num;j++){
                if(i != j && i <= j){
                    data.push(&#91;i,j&#93;);
                }
            }
        }
        this.pairNumPermutations&#91;num&#93; = data;
        return data;
    }

    public getPositionByValue(value:number):Array<point>{
        let arr = new Array<point>();
        for (var i =0;i<this.board.length;i++){
            for (var j = 0; j<this.board&#91;i&#93;.length;j++){
                if (this.board&#91;i&#93;&#91;j&#93; == value){
                    arr.push(new Point(i, j));
                }
            }
        }
        return arr;
    }

    public getNearByPointByDirection(point: Point, direction: string): Point {
        let nearByPoint: Point = new Point(point.x, point.y);
        switch (direction) {
            case Direction.UP:
                for (var i = point.x-1; i >= 0; i--) {
                    if (this.board[i][point.y] == null) {
                        nearByPoint.x = i;
                    } else {
                        break;
                    }
                }
                if (nearByPoint.x == 0) {
                    nearByPoint.x = -1;
                }
                break;
            case Direction.DOWN:
                let maxLengthDOWN = this.board.length;
                for (var i = point.x+1; i < maxLengthDOWN; i++) {
                    if (this.board&#91;i&#93;&#91;point.y&#93; == null) {
                        nearByPoint.x = i;
                    } else {
                        break;
                    }
                }
                if (nearByPoint.x == maxLengthDOWN - 1) {
                    nearByPoint.x = maxLengthDOWN;
                }
                break;
            case Direction.RIGHT:
                let maxLengthRIGHT = this.board&#91;0&#93;.length;
                for (var i = point.y+1; i < maxLengthRIGHT; i++) {
                    if (this.board&#91;point.x&#93;&#91;i&#93; == null) {
                        nearByPoint.y = i;
                    } else {
                        break;
                    }
                }
                if (nearByPoint.y == maxLengthRIGHT - 1) {
                    nearByPoint.y = maxLengthRIGHT;
                }
                break;
            case Direction.LEFT:
                for (var i = point.y-1; i >= 0; i--) {
                    if (this.board[point.x][i] == null) {
                        nearByPoint.y = i;
                    } else {
                        break;
                    }
                }
                if (nearByPoint.y == 0) {
                    nearByPoint.y = -1;
                }
                break;
        }
        return nearByPoint;
    }

    public canFindPath(a: Point, b: Point, direction:string): boolean {
        return this.hasMiddleValue(a ,b);
    }
    public hasMiddleValue(a: Point, b: Point): boolean {
        let arr = [];
        if (a.x == b.x) {
            if (a.x == -1 || a.x == this.board.length) return false;
            let max = Math.max(a.y, b.y);
            let min = Math.min(a.y, b.y);
            for (var i = min + 1; i < max; i++) {
                if (this.board&#91;a.x&#93;&#91;i&#93; != null) {
                    return true;
                }
            }
            return false;
        } else if (a.y == b.y) {
            if (a.y == -1 || a.y == this.board&#91;0&#93;.length) return false;
            let max = Math.max(a.x, b.x);
            let min = Math.min(a.x, b.x);
            for (var i = min + 1; i < max; i++) {
                if (this.board&#91;i&#93;&#91;a.y&#93; != null) {
                    return true;
                }
            }
            return false;
        } else {
            return true;
        }
    }

    public hasSameValue(point1: Point, point2: Point): boolean {
        return this.board&#91;point1.x&#93;&#91;point1.y&#93; == this.board&#91;point2.x&#93;&#91;point2.y&#93;;
    }

    public clearPoint(point: Point) {
        this.board&#91;point.x&#93;&#91;point.y&#93; = null;
        point = null;
    }
}&#91;/code&#93;

新增<code>Direction.ts</code>,內容如下:
[code lang="js"]export class Direction {
    public static UP: string = "up";
    public static DOWN: string = "down";
    public static RIGHT: string = "right";
    public static LEFT: string = "left";
}

需要另外撰寫的部份

第一部份邏輯的介面我們是使用angularJS來做呈現,而現在的專案則需要使用pixiJS,因此呈現部份的程式碼需要重新撰寫。

首先我們先創立一個檔案名為GameBoard.ts,要放置所有的圖示。

import Container = PIXI.Container;

export class GameBoard extends Container{
}

然後在GameScene.ts加入這個元件:

//加入連連看牌面
application.stage.addChild(new GameBoard());




實際撰寫GameBoard的邏輯

完整的程式碼內容如下:

import Container = PIXI.Container;
import {Board} from "../core/Board";
import {Loader} from "../core/Loader";
import Point = PIXI.Point;
import {Path} from "../core/Path";
import {SoundMgr} from "../core/SoundMgr";


export let board:Board;

export class GameBoard extends Container{

    private select1 = new Point(-1, -1);
    private select2 = new Point(-1, -1);
    private selected = false;
    private msgArr = [];
    private reloadTimes = 3;
    private selectedBorder:PIXI.Graphics;

    constructor() {
        super();
        this.createNewGame();
        this.x = 175;
        this.y = 20;
    }

    createNewGame = ()=>{
        this.removeChildren();
        this.select1 = new Point(-1, -1);
        this.select2 = new Point(-1, -1);
        this.selected = false;
        this.msgArr = [];
        this.reloadTimes = 3;
        board = new Board();
        for (var i =0;i<board.board.length;i++){
            for (var j = 0; j<board.board&#91;i&#93;.length;j++){
                this.createIcon(board.board&#91;i&#93;&#91;j&#93;, i, j);
            }
        }
    };

    clearIcon = (point:Point)=>{
        this.removeChild(this.getChildByName('icon_'+point.x+"_"+point.y));
        board.clearPoint(point);
        this.removeChild(this.selectedBorder);
    };

    IconSelected = (point:Point)=>{
    };

    IconUnSelected = (point:Point)=>{
    };

    createIcon = (id, x, y)=>{
        let icon = PIXI.Sprite.from(Loader.resources['Icon'].textures['icon_' + id]);
        icon.name = 'icon_'+x+"_"+y;
        icon.width = icon.height = 45;
        icon.x = (icon.width + 20) * x + 22.5;
        icon.y = (icon.width + 6) * y + 22.5;
        icon.anchor.set(0.5);
        icon.buttonMode = true;
        icon.interactive = true;
        this.addChild(icon);
        let iconClickHandler = ()=>{
            if (this.selected) {
                let selectCorrect = false;
                this.select2 = new Point(x, y);
                if (board.hasSameValue(this.select1, this.select2)) {
                    if (! (this.select1.x == x && this.select1.y == y) ) {
                        let path = new Path(this.select1, this.select2, board);
                        if(path.canLinkInLine()){
                            this.msgArr.push(path);
                            selectCorrect = true;
                            //判斷還有沒有路走
                            if(board.gameRoundEnd()){
                                alert("恭喜完成遊戲!");
                                this.createNewGame();
                            }else if(board.getFirstExistPath() == null){
                                this.reloadTimes--;
                                board.rearrangeBoard();
                            }
                        }
                    }
                }
                if(selectCorrect){
                    this.clearIcon(this.select1);
                    this.clearIcon(this.select2);
                    SoundMgr.play('Sound_select_crrect');
                }else{
                    SoundMgr.play('Sound_select_error');
                    this.IconUnSelected(this.select1);
                }
                this.selected = false;
            } else {
                this.select1 = new Point(x, y);
                this.IconSelected(this.select1);
                this.selected = true;
                SoundMgr.play('Sound_select_1');

            }
        };
        icon.on("click", iconClickHandler);
        icon.on("tap", iconClickHandler);
    }
}

今日成果

今天終於有個連連看遊戲的樣子了!

程式碼下載:ironman20181103
線上demo:http://claire-chang.com/ironman2018/1103/

Posted on Leave a comment

[18- Pixi教學] 按鈕製作

開啟按鈕效果

官網點擊效果範例:https://pixijs.io/examples/#/basics/click.js
預設sprite是無法被點擊到的,因為要偵測這些事件會讓sprite更耗資源,因此如果我們想要讓一個sprite可以被點擊,則要特別對元件設定:

// Opt-in to interactivity
sprite.interactive = true;
// Shows hand cursor
sprite.buttonMode = true;
// Pointers normalize touch and mouse
sprite.on('pointerdown', onClick);
sprite.on('click', onClick); // mouse-only
sprite.on('tap', onClick); // touch-only

在連連看專案加入按鈕

ButtonBase

因為連連看裡面有許多個按鈕,每個按鈕雖然功能不同,但是會有許多相似的設定,因此將按鈕抽出一個ButtonBase的父類別
ButtonBase.ts的內容如下:

import Sprite = PIXI.Sprite;
import {Loader} from "../core/Loader";

export class ButtonBase extends Sprite{

    constructor(_id:string, textureID:string, _x:number, _y:number) {
        super();
        this.texture = Loader.resources[_id].textures[textureID];
        this.interactive = true;
        this.buttonMode = true;
        this.x = _x;
        this.y = _y;
        this.anchor.set(0.5);

        //按下效果
        this.on("mousedown", this.mouseDownEffect.bind(this));
        this.on("mouseup", this.mouseUpEffect.bind(this));
        this.on("mouseout", this.mouseOutEffect.bind(this));
        this.on("touchstart", this.mouseDownEffect.bind(this));
        this.on("touchend", this.mouseUpEffect.bind(this));

        this.on("mouseup", this.trigger.bind(this));
        this.on("touchend", this.trigger.bind(this));
    }
    public trigger(){
    }
    public mouseDownEffect():void{
    }
    public mouseUpEffect():void{
    }
    public mouseOutEffect():void{
    }
}

SoundBtn

新增一個檔案SoundBtn.ts,製作聲音開/關按鈕,內容如下:

import {ButtonBase} from "./ButtonBase";
import {Loader} from "../core/Loader";
import {SoundMgr} from "../core/SoundMgr";


export class SoundBtn extends ButtonBase {
    private isMute: boolean = false;
    constructor() {
        super('Button','Sound_On',50,170);
        this.updateImage();

    }
    public trigger(){
        this.isMute = !this.isMute;
        SoundMgr.mute(this.isMute);
        this.updateImage();
    }
    updateImage = ()=>{
        if (this.isMute){
            this.texture = this.texture = Loader.resources['Button'].textures['Sound_Off'];
        } else{
            this.texture = this.texture = Loader.resources['Button'].textures['Sound_On'];
        }
    }

}

GameScene

因為除了聲音按鈕外,要設定整個場景還需要加入非常多東西,因此我新增了一個類別叫GameScene,主要在將所有場景上的物件加進場景裡
GameScene.ts其內容如下

import {Loader} from "../core/Loader";
import {application} from "../Main";
import {SoundBtn} from "./SoundBtn";

export class GameScene {

    public static draw(){
        //加入背景
        application.stage.addChild(PIXI.Sprite.from(Loader.resources["background"].texture));
        //加入按鈕
        application.stage.addChild(new SoundBtn());
    }
}



實作靜音功能

在SoundMgr.ts加上靜音功能

    static isMute: boolean = false;
    public static mute(value:boolean):void {
        this.isMute = value;
        if (this.isMute) {
            //禁聲
            Howler.mute(true);
        } else {
            //出聲
            Howler.mute(false);
        }
    }

今日成果


線上demo:http://claire-chang.com/ironman2018/1102/
檔案下載:ironman20181102

Posted on Leave a comment

[17- Pixi教學] 音樂音效設定



PixiJS Sound

PixiJS本身有開發可供使用的PIXI.sound

並且官網上有很詳細的使用範例以及API手冊:PixiJS Sound

PixiJS Sound最大的優點是它可以與pixi的素材共用同一個Loader,下載mp3的資源檔案。並支援相當多種的音樂格式,也可以將多個音效壓縮在一個mp3內,再根據要開始與結束的秒數去切成多個聲音資源。在一般的狀況下,PixiJS Sound應為搭配PixiJS首選可使用的音樂函式庫

PixiJS Sound於TypeScript

不過很可惜的,PixiJS Sound現在在typescript上的支援度尚不是那麼的好。PixiJS Sound並沒有推出針對type script專案的函式庫,在開發上會帶給使用typescript的人帶來一些困擾。因此我選擇了另一款很受歡迎的聲音函式庫howler

下面是howler.js一些簡單的使用範例

var sound = new Howl({
  src: ['sound.webm', 'sound.mp3']
});

// Clear listener after first call.
sound.once('load', function(){
  sound.play();
});

// Fires when the sound finishes playing.
sound.on('end', function(){
  console.log('Finished!');
});
var sound = new Howl({
  src: ['sound.webm', 'sound.mp3', 'sound.wav'],
  autoplay: true,
  loop: true,
  volume: 0.5,
  onend: function() {
    console.log('Finished!');
  }
});



在連連看專案內使用Howler

ResourcesList.ts裡新增要下載的mp3檔案

public static sound:Array<resources> = [
        new Resources('Sound_bg','assets/bg.mp3'),
        new Resources('Sound_level_pass','assets/level_pass.mp3'),
        new Resources('Sound_select_1','assets/select_1.mp3'),
        new Resources('Sound_select_crrect','assets/select_crrect.mp3'),
        new Resources('Sound_select_error','assets/select_error.mp3')
    ];

新增SoundMgr.ts內容如下:

import {ResourcesList} from "./ResourcesList";

export class SoundMgr {
    private static soundList:Array<soundInfo> = new Array<soundInfo>();
    public static load(){
        ResourcesList.sound.forEach(element => {
            let info = new SoundInfo(element.id, element.path);
            this.soundList.push(info);
        });
    }
    public static play(id, loop = false){
        this.soundList.forEach(element => {
            if (element.soundID == id){
                element.sound.loop(loop);
                element.sound.play();
            }
        });
    }
}
class SoundInfo{
    public soundID:string;
    public path:string;
    public sound:Howl;
    constructor(_id:string, url:string) {
        this.soundID = _id;
        this.path = url;
        this.load();
    }
    public load(){
        this.sound = new Howl({src: this.path});
    }
}

Main.ts加入

            //播放背景音樂
            SoundMgr.play('Sound_bg',true);

並呼叫load()來下載音檔

SoundMgr.load();

需要注意的是,因為howler的loader與pixi本身的是兩個不同的loader,所以像這樣的寫法,很有可能剛進遊戲音效會還沒有載入完成,等到載完後才會播放聲音。如果希望一定要剛進遊戲就有聲音,那就需要把拿掉loading page的判斷另外加上音效是否載入完成的判斷。

今日成果

專案下載:ironman20181101
線上demo:http://claire-chang.com/ironman2018/1101/

Posted on Leave a comment

[16- Pixi教學] 與網頁互動-控制loading page



加入loading page

上一篇的成果在載入檔案時畫面就會停在那邊,一般的遊戲通常會有一個下載中的畫面,告知使用者現在下載了幾%,還差幾%還未下載完成。
因此今日我們就要來製作這個loading page

首先,在index.html裡加上一個div

<div id="gameContainer">
  <canvas id="gameCanvas"></canvas>
  <div id="loadingPage">Loading...</div>
</div>

並在style.css裡設定

#gameCanvas{
    position:absolute;
}
#loadingPage{
    min-width: 100%;
    min-height: 100%;
    position:absolute;
    left:0px;
    top: 0px;
    z-index:1;
    background-color: #000000;
    color: #fff;
    text-align: center;
    vertical-align: middle;
    line-height: 100vh;
}
#loadingPercent{
    position:absolute;
    min-width: 100%;
    min-height: 100%;
    top:calc(50%);
}

這樣我們就會有一個很簡單的loading畫面如下圖

更新loading進度

直接用jquery去操控剛剛所設定的div裡面的文字顯示,來更新現在的下載進度:

this.loader.onProgress.add((e) => {
    jQuery("#loadingPage").html("Loading..." + Math.floor(e.progress) + "%");
});

整個Loader.ts修改後的程式碼如下:

import { ResourcesList } from "./ResourcesList";
import {eventEmitter} from "../Main";
import {CoreEvent} from "./Event";
import math = PIXI.core.math;

export class Loader{
    private static loader:PIXI.loaders.Loader;
    private static failedFiles:Array<string> = [];
    private static completedFiles:Array<string> = [];
    public static resources:PIXI.loaders.Resource;

    public static load(){
        this.loader = new PIXI.loaders.Loader();
        ResourcesList.img.forEach(element => {
            this.loader.add(element.id, element.path);
        });
        this.loader.load((loader, resources) => {
            this.resources = resources;
        });
        //可取得下載進度
        this.loader.onProgress.add((e) => {
            jQuery("#loadingPage").html("Loading..." + Math.floor(e.progress) + "%");
        });
        //載入檔案錯誤時
        this.loader.onError.add((t, e, r) => {
            this.failedFiles.push(r.name);//載入失敗的檔案列表
        });
        //每個檔案載入時都會呼叫
        this.loader.onLoad.add((e, t) => {
            this.completedFiles.push(t.name);//載入成功的檔案列表
        });
        //全部下載完成後
        this.loader.onComplete.add(() => {
            if (this.failedFiles.length == 0){
                //全部的檔案都下載成功
            } else{
                jQuery("#loadingPage").html("Loading...failed: could not load "+ this.failedFiles);
            }
        });
    }
}

EventEmitter介紹

PIXI.utils.EventEmitter使用EventEmitter3,在pixiJSv4之後已直接被整理到pixi裡了。
這個套件可以讓我們在js裡很方便的使用事件,在動畫處理上,事件的監聽與發送可以讓我們在處理動畫上更加輕鬆,也可以降低這些元件間的相依性。

下面是EventEmitter3上的一個簡單使用範例

var EE = new EventEmitter()
  , context = { foo: 'bar' };

function emitted() {
  console.log(this === context); // true
}

EE.once('event-name', emitted, context);//context為傳入函數的參數
EE.on('another-event', emitted, context);
EE.removeListener('another-event', emitted, context);

如果我們希望整個遊戲共用一個EventEmitter,可以將產生的EventEmitter做export:

export let eventEmitter:EventEmitter;

要使用這個物件可以直接import進來

import {eventEmitter} from "../Main";

發送下載完成事件

Loader.ts加入下面的程式碼:

 //全部下載完成後
        this.loader.onComplete.add(() => {
            if (this.failedFiles.length == 0){
                eventEmitter.emit(CoreEvent.AssetsLoadComplete);
            } else{
                jQuery("#loadingPage").html("Loading...failed: could not load "+ this.failedFiles);
            }
        });

接著在Main.ts裡設定下載完後要做的事情:

//設定共用的事件傳遞元件
        eventEmitter = new EventEmitter();
        eventEmitter.on(CoreEvent.AssetsLoadComplete,()=>{
            //隱藏loading page
            jQuery("#loadingPage").hide();
            //加入背景
            var background = PIXI.Sprite.from(Loader.resources["background"].texture);
            application.stage.addChild(background);
        });

上面需要注意,因為我們在Loader.loader.load()裡將下載後的resource存在一個變數裡,所以我們可以直接使用

Loader.resources["background"].texture

來取得某一個resource id下的texture。

今日成果

線上展示:http://claire-chang.com/ironman2018/1031/
成果檔案下載:ironman20181031

Posted on Leave a comment

[15- Pixi教學] 載入素材

用PIXI.Sprite來載入圖檔

在PIXI.Sprite裡有一個fromImage(‘URL’)的方法可以直接讓我們將某個圖檔載入Sprite裡面,如:

var bunny = PIXI.Sprite.fromImage('required/assets/basics/bunny.png')

但這只能針對單張圖檔,而不能處理SpriteSheet的狀況。

並且當我們在做一個遊戲專案時,通常會需要先載完所有的遊戲素材,這樣才不會在進遊戲時看到未載完圖檔的空白而產生錯誤或破圖。
因此較多的時候,我們都會使用PIXI.loaders去下載並監聽下載進度,製作一個loading bar。
並在素材載入完成後才進入遊戲。


PIXI.loaders

下面為一個PIXI.loaders的簡單使用範例

const loader = new PIXI.loaders.Loader();
loader.add('bunny', 'data/bunny.png')
      .add('spaceship', 'assets/spritesheet.json');
loader.load((loader, resources) => {
   // resources.bunny
   // resources.spaceship
});

PIXI.loaders是從resource-loader擴展的。
下面為resource-loader的GitHub上的一個簡單使用範例:

// ctor
const loader = new Loader();

loader
    // Chainable `add` to enqueue a resource
    .add(name, url, options)

    // Chainable `pre` to add a middleware that runs for each resource, *before* loading that resource.
    // This is useful to implement custom caching modules (using filesystem, indexeddb, memory, etc).
    .pre(cachingMiddleware)

    // Chainable `use` to add a middleware that runs for each resource, *after* loading that resource.
    // This is useful to implement custom parsing modules (like spritesheet parsers, spine parser, etc).
    .use(parsingMiddleware)

    // The `load` method loads the queue of resources, and calls the passed in callback called once all
    // resources have loaded.
    .load((loader, resources) => {
        // resources is an object where the key is the name of the resource loaded and the value is the resource object.
        // They have a couple default properties:
        // - `url`: The URL that the resource was loaded from
        // - `error`: The error that happened when trying to load (if any)
        // - `data`: The raw data that was loaded
        // also may contain other properties based on the middleware that runs.
    });

// throughout the process multiple signals can be dispatched.
loader.onProgress.add((event) => {}); // called once per loaded/errored file
loader.onError.add((target,event,error) => {}); // called once per errored file
loader.onLoad.add((target) => {}); // called once per loaded file
loader.onComplete.add(() => {}); // called once when the queued resources all load.

從範例可以看出,我們可以用onProgressonErroronLoadonComplete
來取得現在的下載進度,並且根據所傳入的參數來更新loading page要顯示的資訊。



開始載入連連看圖檔

新增一個檔案名為ResourcesList.ts,用來設定要載入的圖片並設定一個識別id給它。
其內容如下:

class Resources{
    public id:string;
    public path:string;

    constructor(id, path) {
        this.id = id;
        this.path = path;
    }
}
export class ResourcesList{
    public static img = [
        new Resources('bunny','assets/bunny.png'),
        new Resources('background','assets/background.png'),
        new Resources('Button','assets/Button.json'),
        new Resources('Character_Idle','assets/Character_Idle.json'),
        new Resources('Character_Jump','assets/Character_Jump.json'),
        new Resources('Character_Laugh','assets/Character_Laugh.json'),
        new Resources('Icon','assets/Icon.json')
    ];
}

接著新增Loader.ts檔案,用來載入並監控下載進度。

import { ResourcesList } from "./ResourcesList";
import {CoreEvent} from "./Event";
import math = PIXI.core.math;

export class Loader{
    private static loader:PIXI.loaders.Loader;
    private static failedFiles:Array<string> = [];
    private static completedFiles:Array<string> = [];
    public static resources:PIXI.loaders.Resource;

    public static load(){
        this.loader = new PIXI.loaders.Loader();
        ResourcesList.img.forEach(element => {
            this.loader.add(element.id, element.path);
        });
        this.loader.load((loader, resources) => {
            this.resources = resources;
        });
        //可取得下載進度
        this.loader.onProgress.add((event) => {
            console.log("onProgress: ",event);
        });
        //載入檔案錯誤時
        this.loader.onError.add((target, event, error) => {
            this.failedFiles.push(error.name);
            console.log("onError: ",error);
        });
        //每個檔案載入時都會呼叫
        this.loader.onLoad.add((event, target) => {
            this.completedFiles.push(target.name);
            console.log("onLoad: ",target);
        });
        //全部下載完成後
        this.loader.onComplete.add(() => {
            if (this.failedFiles.length == 0){
                console.log("all file completed");
            } else{
                console.log("Loading...failed: could not load "+ this.failedFiles);
            }
        });
    }
}

並在Main.ts裡加上

        //載入素材
        Loader.load();



觀察事件觸發時機

從console視窗可以看到我們在Loader內所下的log被觸發的時機

從上圖可知:

  • onProgress:每一段時間都會更新現在下載的進度
  • onError:有載入失敗的圖檔會呼叫這個func,然後繼續執行後面的圖檔的載入動作
  • onLoad:每一個檔案下載完成都會呼叫這個function,載入一個spritesheet的json檔案,會呼叫兩次onLoad,一次是json載入,一次是載入這個json裡所指定要使用到的png檔案
  • onComplete:全部載入完成後會呼叫,要注意即使中間有檔案載入失敗也會呼叫此function



今日成果

線上展示:http://claire-chang.com/ironman2018/1030/
今日檔案下載:ironman20181030

Posted on Leave a comment

[14- Pixi教學] PIXI場景設定



載入所使用的Library

在開發遊戲時我們會使用到PixiJS、SystemJS、和JQuery,因此我們會需要將這幾個library加進html裡。
首先我們新增一個index.html的入口檔案,內容如下:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>Test</title>
  <link rel="stylesheet" type="text/css" href="assets/style.css">
  <script src="lib/system.js"></script>
  <script src="lib/jquery.min.js"></script>
  <script src="lib/pixi.min.js"></script>
</head>
<body>
</body>
</html>

打開這個html後,會看到找不到檔案的訊息。
這時我們在gulpfile.js裡增加一個task名為libs,將我們所需的檔案copy到目標資料夾
其內容如下:

gulp.task('libs', () => {
    return gulp
        .src([
            'node_modules/jquery/dist/jquery.min.js',
            'node_modules/jquery/dist/jquery.min.map',
            'node_modules/systemjs/dist/system-polyfills.js',
            'node_modules/systemjs/dist/system-polyfills.js.map',
            'node_modules/systemjs/dist/system.js',
            'node_modules/systemjs/dist/system.js.map',
            'node_modules/pixi.js/dist/pixi.min.js',
            'node_modules/pixi.js/dist/pixi.min.js.map'
        ])
        .pipe(gulp.dest("build/lib"));
});

這樣子就會在每次編譯時自動複製專案在使用的library進入build/lib裡。

設定場景

要設定一個PixiJS的遊戲場景,這個部份可以先參考關網的範例:Basics
範例裡創建場景的方式如下:

var app = new PIXI.Application(800, 600, {backgroundColor : 0x1099bb});
document.body.appendChild(app.view);

PIXI.Application可以建立一個PIXI的canvas遊戲畫面,其相關API說明請見:PIXI.Application
這個類別有兩種建構子,可以達到相似的功能:

在PixiJS v4以後,官方將所有相關參數都整入options物件內,這樣使用上可以僅傳入需要自行設定的值就好。而要先傳入寬高的則是為了兼容舊版寫法。

new PIXI.Application(960, 540, {width:100, backgroundColor : 0x000000, view: gameCanvasContext});

若兩邊都傳入相同的屬性,會以optionst傳入的值為主。如上面程式碼產生的場景的width會是100。

PIXI.Application可用參數

在創建這個類別時,可以傳入的options參數如下:

Name Type Default Description
autoStart boolean true 過去
width number 800 renderers view的寬
height number 600 renderers view的高
view HTMLCanvasElement 遊戲場景要放置的canvas目標,如果沒有傳入,則會新建一個canvas做為回傳
transparent boolean false 是否支援透明可看到底下物件
antialias boolean false 防鋸齒
preserveDrawingBuffer boolean false 是否支援toDataUrl
resolution number 1 解析度
forceCanvas boolean false 即使存在webGL引擎也不使用
backgroundColor number 0x000000 背景顏色
clearBeforeRender boolean true 在繪圖前先清除canvas上所有元素
roundPixels boolean false 使用Math.floor()來處理所有圖像
forceFXAA boolean false 強制使用FXAA,速度會較快,但可能效果較WebGL差
legacy boolean false 確保能與舊裝置相容
powerPreference string 設定要傳給webGL的參數,對雙顯卡的設備而言可提高效能。
sharedTicker boolean false 可以有共用的PIXI.ticker.shared物件
sharedLoader boolean false 可以有共用的PIXI.loaders.shared物件

於專案中建立場景

接下來,我們在index.html新增一個canvas物件,自行建立一個canvas物件的好處是我們可以較方便的在html內設定canvas的位置

<body>
<div id="gameContainer">
  <canvas id="gameCanvas"></canvas>
</div>
</body>

接著建立Main.ts,其內容如下:

import Application = PIXI.Application;

//將application設定為export,這樣可以在專案的任何地方取到這個元件
export let application:Application
/**
 * 主要的 client application.
 *
 */
export class Main {

    public initGame() {
       //設定場景
       let gameCanvasContext = (< HTMLCanvasElement >jQuery("#gameCanvas")[0]);
       application = new PIXI.Application(960, 540, {width:100,backgroundColor : 0x000000, view: gameCanvasContext});

       //貼一張圖片
       var bunny = PIXI.Sprite.fromImage('assets/bunny.png');
       bunny.x = application.screen.width / 2;
       bunny.y = application.screen.height / 2;
       application.stage.addChild(bunny);
    }
}

接著因為我們使用SystemJS,所以必需用SystemJS的方式來啟動遊戲,在index.html加上下面的javascript script

    var game;
    $(function () {
        //設定要載入的js的位置與副檔名
        SystemJS.config({
            baseURL: "/",
            packages: {
                "/": { defaultExtension: "js" }
            }
        });
        //載入Main.js,載完後執行裡面的某個func
        SystemJS.import('Main').then(function (m) {
            m.Main.prototype.initGame();
        });
    });

這時後打開瀏覽器就可以看到下面的畫面

隨螢幕大小改變尺寸

我們可以看到上面的成果裡,右下方有許多的空白,如果沒有特別對Canvas做縮放,Canvas會一直是相同尺寸。這樣就沒辦法在各個平台、尺寸很順利的進行遊戲,因此要自動偵測並縮放canvas以及場景尺寸。

Main.ts加上onResize()

public onResize() {
    var w = window.innerWidth;
    var h = window.innerHeight;
    var scale = Math.min(w/860,h/540);
    application.view.style.left = (w-scale*860)/2 + "px";
    application.view.style.top = (h-scale*540)/2 + "px";
    application.view.style.width = scale*860 + "px";
    application.view.style.height = scale*540 + "px";
}

並在initGame()新增下面兩行

       //設定遊戲大小隨視窗大小改變
       this.onResize();
       window.onresize = this.onResize; 

這樣遊戲畫面就會永遠置中且自動縮放至適合螢幕大小。



今日成果

原始檔請於此下載:ironman20181029
線上展示:http://claire-chang.com/ironman2018/1029/

Posted on Leave a comment

[13- 遊戲製作] 素材處理

PixiJS能使用的素材

承上一篇文章,我們排好的UI介面,其最終的產出是一個fla檔案,但是這樣的檔案pixiJS是無法使用的。
因此我們必需要整理素材並匯出pixiJS可以使用的素材。

那麼,pixiJS可以使用那些素材呢?最基本的是單張的圖檔,但是因為HTTP/1.1在載入檔案是每個檔案都需額外處理request和response header,會讓下載的速度變慢。並且GPU在處理圖檔上,因GPU擅長矩陣處理,而同一張圖去做截取亦為一種矩陣處理,可以使用到GPU加速的優勢。因此在製作遊戲時,我們偏好使用SpriteSheet。SpriteSheet指的是很多張圖檔被整併到一大張圖檔裡。

如下圖這個動畫:

是由10張圖所構成的,那麼SpriteSheet會將這10張圖打包成一個png檔案。

並會產生一個json檔案,來設定每一個畫面要截取的範圍,其檔案內容如下:
{“frames”: {
“0”:
{
“frame”: {“x”:0,”y”:0,”w”:109,”h”:74},
“rotated”: false,
“trimmed”: true,
“spriteSourceSize”: {“x”:1,”y”:0,”w”:110,”h”:74},
“sourceSize”: {“w”:110,”h”:74}
}
//…
“meta”: {
“app”: “Adobe Animate”,
“version”: “15.1.1.13”,
“image”: “Character_Laugh.png”,
“format”: “RGBA8888”,
“size”: {“w”:256,”h”:512},
“scale”: “1”
}
}
在json裡的”0″,是代表這張圖片的id,之後我們要在pixi去取某個spritesheet的某一張圖,會需要使用到這個id來辨視我們要取的圖。在adobe animate裡,動畫的id他會自動幫我們從0~N這樣以流水號編下去。

另外,在pixiJS裡,某些設備沒辦法處理比4096更大的圖片。
有關於這部份的瀏覽器支援,請見:WebGL Stats

由上圖可看出現在尚有許多設備無法支援4096以上的尺寸的圖檔,這點在包圖時要注意一下

除了以上單張圖檔和SpriteSheet之外,PixiJS還可以使用spine的圖檔,不過由於spine的圖檔編輯軟體要價不斐,因此這次的小作品就先跳過了!

從Adobe Animate匯出動畫連續圖

要從Adobe Animate匯出圖檔,首先要先打開元件庫,然後點擊該動畫元件,按下右鍵,最下方會有一個Generate Sprite Sheet。

接著會跳出一個設定頁面,我們選擇Algorithm為Basic,Data format為JSON,然後按下Export就可以了。

從Adobe Animate匯出圖片集

如果要匯出多個圖檔至一個圖片集,則在元件庫先整理過元件的名稱,預設adobe animate會以元件庫的名稱做為在json內的識別id。
接著一次選擇多個要匯出的圖檔,點右鍵,選擇Generate Sprite Sheet

左邊可以看到將要輸出的sprite sheet裡會有那些圖檔,圖檔的id為何,選項的部份與動畫匯出相同。

音效下載

我是在這邊尋找適合的音效:小森平的免費下載音效

最後打包好的素材集

請按此下載:assets

Posted on Leave a comment

[12- 遊戲製作] 介面設計

連連看遊戲需求

我所規劃的連連看應要有下面的功能:

  • 時間限制:需要在8分鐘內完成遊戲,否則就算是沒有過關。
  • 重整牌面的限制:最多只能重整牌面3次,每次耗1點生命,生命值耗光遊戲就結束了。
  • 音樂、音效需能開或關
  • 重新整理牌面:可以手動洗牌
  • 提示:提示可連線的路徑
  • 復原:還原已消除的圖案
  • 可連到我的FB

其版面配置如下:

尋找免費素材

有了mockup之後,身為一個不會美術的程式設計師,鐵人賽又沒有美術可以幫忙,最重要的就是去尋找免費素材。
這時候就要大推UNITY的Unity Asset Store

很快的我選擇了Free Platform Game Assets這款免費素材來做我連連看的基本素材,首先先登入網站,然後選擇

接著要在電腦裡安裝unity,安裝好之後,再按

接著就會在unity內開啟這個網頁畫面

按下import後,這包素材就會被下載至你剛剛所創的unity專案下的資料夾裡,按右鍵就可以在一般的資料夾找到所下載的素材。

製作遊戲介面

我是使用Adobe Animate來做畫面的繪製。Adobe Animate(前稱Adobe Flash Professional,Macromedia Flash,FutureSplash Animator)是由Adobe Systems開發的多媒體創作和電腦動畫程式。可用於設計向量圖形和動畫,並發布到電視節目,網路影片,網站,網路應用程式,大型網際網路應用程式和電子遊戲中。

這是我所製作完成的遊戲畫面:

原始檔請在此下載:ui.fla

Posted on Leave a comment

[11- 遊戲製作] 使用模組介紹

什麼是NPM

下面是官網上對於NPM的說明

Use npm to install, share, and distribute code; manage dependencies in your projects; and share & receive feedback with others.

簡而言之,NPM是一個套件管理的工具,我們可以使用npm來建立、分享、發佈模組,並且於其平台上分享、接收其他人對模組的反饋。

NPM創造了node的生態圈,我們可以在其官網上搜尋、下載、安裝、使用、並管理我們所需要的模組。

模組的選擇

一般而言,在選擇npm的模組時,要去注意一下這個模組的開發者是否有持續在更新、維護模組內容。並從單周下載量,來評估這個套件的普及度,一般很多人在用的套件會較可信賴。
由於node的生態圈現在還算是個戰國時代,有許多在上面分享的模組很有可能過了一陣子發現了bug,套件的開發者卻已經沒在維護了,我們還得要去研究套件本身是否有某些程式碼會造成這bug,會讓我們在開發時的成本大大增加。

上圖中的weekly download、last publish以及下圖中的Dependents(有多少專案使用這個模組)都是我們要去觀察一個套件的指標。

另外網路上相關教學、使用手冊完整度、網路論壇討論度、是否適合我們的專案,也是我們可拿來做選擇模組的考量點。

我所選擇的套件 – 環境與部署相關

  • run-sequence:可讓gulp的task循序被執行。
        var gulp = require('gulp');
        var runSequence = require("run-sequence");
        gulp.task("build-web", function () {
        runSequence(
            'firstJob',
            'secondJob',
            ['otherJob1', 'otherJob2'],
            function (error) {
                if (error) {
                    console.log(error.message);
                } else {
                    console.log('success');
                }
            }
        );
    });
  • del:可以去刪除本機的檔案。
    var gulp = require('gulp');
    const del = require("del");
    gulp.task('clean', (cb) => {
        return del(["build"], cb);
    });
  • gulp:可以編寫complier時要做的動作的腳本
    var gulp = require('gulp');
    const del = require("del");
    gulp.task('job1', () => {
        //some task write here
    });
  • gulp-typescript:可以使用gulp來呼叫TypeScript的API去將typescript轉為js
    	var ts = require('gulp-typescript');
            var gulp = require('gulp');
            var tsProject = ts.createProject('tsconfig.json');
    	gulp.task('build',  function() {
    	    return tsResult.js.pipe(gulp.dest('build'));
    	});
  • gulp-sourcemaps:可以產生一份xxx.ts.map的檔案,我們可以在debug時連到原始的ts檔案去做偵錯。
  • browser-sync:可以同步瀏覽器,當有檔案被更新時瀏覽器會自動重新整理。
    var gulp = require('gulp');
    const browserSync = require('browser-sync');
    gulp.task('launch-web', ['build-web'], function () {
        browserSync({
            open: true,
            port: 8001,
            files: ["./build/**/*.{html,htm,css,js,json}"],
            server: {
                "baseDir": "./build"
            }
        });
    });

我所選擇的套件 – 開發用Library

  • systemjs:這個套件能夠用模組化的方式來加載檔案,我們只需要在開頭打上下面的程式碼,systemJS就會自動去下載相對路徑的js檔案。
    import {Loader} from "../core/Loader";

    但是若要使用systemJS,則必需在html裡面輸入下面的程式碼,去設定要載入的路徑以及載入檔案的附檔名。

    var game;
    $(function () {
        SystemJS.config({
            baseURL: "",
            packages: {
                "/": { defaultExtension: "js" }
            }
        });
        //載入第一個要執行的檔案
        SystemJS.import('Main').then(function (m) {
            m.Main.prototype.initGame();
        });
    });
  • pixi.js:本系列文章最主要要使用的2D遊戲引擎
  • jquery:大家應該都很熟悉,可以更方便的與html的DOM元件做互動。在開發網頁遊戲時,常常會有某些GUI元件,若使用html來做會更加容易,如線上使用者列表、聊天視窗等…。這時候我們通常會在網頁上重疊兩個div元件,下面那一層放canvas,上面則疊上一層html div元件。如loading page時常是用這樣的方式來製作。妥善的利用canvas與html間不同的優勢,會讓遊戲開發上更加的容易。
  • typescript:TypeScript程式物件導向的特性能讓我們在寫TypeScript的時候,有像是寫強型別的語言一樣輕鬆自在,IDE也可以幫忙檢查基本的錯誤。
  • gsap:一個很好用的tween工具,官網GreenSock裡有非常詳細的使用說明,手冊的範例非常的清楚,在這邊也能夠直接預覽特效的樣子。
Posted on Leave a comment

[10- 遊戲製作] PixiJS介紹

關於PixiJS


下面文字是官網裡對於 Pixi.js 特性的說法:

Pixi.js is a 2D webGL renderer with a seamless canvas fallback that enables it to work across all modern browsers both desktop and mobile. Helping Goodboy and hopefully you achieve the big fat goal of “Build once, play everywhere

他們強調因為Pixi.js支援WebGL的關係,使得Pixi.js的效能表現大大超過過往使用svg做網頁遊戲的效能。詳細比較及實驗數據請參見:突破 D3.js 的速度極限 — 2D WebGL 與 PIXI.js

Pixi.js是一個基於Javascript的2D繪圖引擎,主要是做網頁的2D遊戲,可兼容多種瀏覽器(所支援的瀏覽器版本請參見:What browsers are supported?),但即便是不支援WebGL的瀏覽器,PixiJS也可以正常運行,只是效能會變得差很多,因為沒辦法享受到GPU加速的優勢。

由於Pixi.js的主要開發者過去曾經寫過flash,若是曾經開發過flash的developer,在學習pixi.js時,會感到格外親切。

在Pixi.js裡主要的單張圖片使用的class名稱為Sprite,而動畫則為MovieClip。其他如addChild()removeChildren()play()gotoAndStop()gotoAndPlay()等API的名稱都與flash的語法十分相近。過往的flash developer很容易便能夠透過IDE裡auto complete的功能去找到想要使用的功能。

Pixi.js只有提供關於使用WebGL的圖像處理API以及動畫控制元件,並不一定要我們遵循特定的方式(如:TypeScript、粒子系統)等去做開發。這也是它一個優點,我們可以很自由的去依據自己專案的需求去選擇我們要使用的配套方案。

相關技術 – Phaser


Phaser是一個基於Pixi.js去開發的遊戲框架,雖然Phaser是使用較舊版本的PixiJS為基準,並且我們難以自行將其所使用的PixiJS更新到最新版,而新版的PixiJS提供了更多的功能以及更好的效能。
但是Phaser可以讓我們在開發遊戲時更能夠遵循一致的規範,並且它為我們把許多好用的遊戲開發功能都整合進去,例如粒子系統等。

Phaser額外為Pixi添加了下面的內容:

  • 物理系統的選擇(arcade或full body)
  • 一個遊戲世界和一個可以平移它的鏡頭
  • Tilemap支持
  • 粒子系統
  • 聲音支持
  • 更高級的輸入處理
  • 鍵盤和遊戲手柄輸入
  • Scale Manager可以處理遊戲/場景調整大小+全營幕支持
  • 為Tween物件增加Tween Manager, 使其與核心時間一致
  • 素材載入管理
  • 遊戲狀態管理
  • 遊戲時鐘+自定義計時器+計時器事件

Source:Decide Pixi.js or Phaser

相關技術 – ThreeJS

three.js是另一個可以使用WebGL的3D Javascript遊戲引擎。主要與pixiJS不同的是它是一個3D引擎,而pixiJS主要強項則是產生2D的遊戲。ThreeJS可以讓我們用它創建各種三維場景,包括了攝影機、光影、材質等各種對象。

ThreeJS可以與pixiJS做結合,不過由於3D遊戲是由一個攝影機去做投影,因此投影出來的多個物件會在同一層裡。2D的畫面會永遠在3D物件之上(或之下)而無法交疊出現(以同一個Canvas來說)

這裡是一個將PixiJS結合ThreeJS的範例:https://codepen.io/NEWREBEL/pen/OObLyE

學習PixiJS的好用資源

  • PixiJS API Documentation:裡面有詳細的API手冊,搭配example去學習pixi可以讓上手速度更快速。
  • PixiJS Examples:大部份我們要用PixiJS開發遊戲會需要用到的技術都有很簡單的範例在裡面。
    比較特別的是,Example Code的地方是可以動態修改,然後按上面重整的圖示,就可以即時看到你所修改的東西呈現的樣子,可以讓我們很方便的做測試。
  • Pixi.js官方論壇:我們公司使用PixiJS開發web game已經兩年多了,在這之間遇到難解的怪問題時,這個論壇是一個很好的平台。pixi的主要開發者在論壇裡算是很活躍也很樂於回答我們的問題。他們也會在解惑時提到很多他們建議的開發方式。
  • Pixi教程:一個基於官方英文教學文件的中文翻譯

使用PixiJS開發的作品

這篇介紹的很詳細:[PixiJS – Day-03] 使用 PixiJS 製作的網站,也許並不少

相關資料