Posted on Leave a comment

[20- Pixi教學] 連連看公仔實作- 逐格動畫

動畫物件

在一般的2D遊戲中,動畫可以用2D骨骼動畫製作(如:Spine)、粒子系統(如:Particles)、或者使用Tween(如:gsap)以及逐格動畫來製作。

下面為這四種動畫的介紹:

  • 骨骼動畫:針對角色動畫的骨架做設定,並且經由改變骨骼元件方向及變形來讓角色做出如行走、跳躍等動態動作。使用骨骼動畫可以大大降低角色所占的圖像空間,尤其是在2D的遊戲裡,這是不可或缺的技術。
  • 粒子系統:有許多編輯器可以編輯出粒子效果的設定檔,並藉由粒子系統的程式來做出特效。
  • Tween:針對一個物件的某個屬性去設定一段時間將該屬性從a數值變成b數值。可以用在物件位移(更動x、y屬性)、放大縮小(更動scale)、旋轉(更動rotate)等效果上。也可以更改自定義屬性,如跑分效果(將分數慢慢加上去)。
    下面是官網上的一個效果範例:
  • 逐格動畫:逐格動畫是最簡單原始的動畫類型,由許多張的動態圖組合成為一幅動畫。逐格動畫的動畫製作能夠最精緻、最多樣化,但是也需要花最多力氣在繪製圖像,並且會有很大的檔案大小。其原理與一般電影、gif檔相同。

逐格動畫

PIXI.extras.AnimatedSprite是在Pixi.JS裡處理動畫的類別,官網對此類別的說明如下:

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

也就是可以用來播放由多張連續的圖檔組成的逐格動畫,在前面的[13- 遊戲製作] 素材處理的部份,已經有分享過該如何將連續圖檔打包成所需的格式,在這一章裡,則會在遊戲裡來實際使用這些連續圖檔。

角色動畫需求

在遊戲中,我們希望角色動畫可以在不同的時間播放不同的動畫,讓角色與遊戲能夠更有互動感,播放時機與動畫的設計如下:

    • 待機動畫:播放Character_Idle.json
      消除成功時:播放Character_Laugh.json
      按下提示時:播放Character_Jump.json
  • 角色動畫程式撰寫

    1. 產生動畫元件: 傳入要產生的動畫名稱,然後設定動畫完成後要呼叫的動作。

        createAnim(id:string, onComplete:any){
            let anim = Loader.resources[id].textures;
            let textures = [];
            for(var index in anim) {
                textures.push(anim[index]);
            }
            var character = new PIXI.extras.AnimatedSprite(textures);
            character.play();
            character.animationSpeed = 0.25;
            character.loop = false;
            character.onComplete = onComplete;
            this.addChild(character);
            return character;
        }

    2. 產生動畫並設定handler:因為動畫若播到一半被中斷會很突兀,這邊一律是等到動畫播完後,才會依狀態判斷下一個要播的動畫

        private shouldPlayTarget:string = 'idle';
        constructor(){
            super();
            //每次動畫完成之後,都要判斷下一個要播放的動畫為何
            this.idle = this.createAnim('Character_Idle', this.playAnim.bind(this));
            this.jump = this.createAnim('Character_Jump', this.playAnim.bind(this));
            this.laugh = this.createAnim('Character_Laugh', this.playAnim.bind(this));
    
            eventEmitter.on(GameFlowEvent.LinkedLineSuccess, ()=>{
                this.shouldPlayTarget = 'laugh';//設定下一個要播的動畫
            });
            eventEmitter.on(GameFlowEvent.TipsRequest, ()=>{
                this.shouldPlayTarget = 'jump';
            });
        }
        //依據shouldPlayTarget的值來判斷現在要播的動畫
        //如果沒有特殊要播的動畫的話,則一律播放待機動畫
        playAnim(){
            this.idle.visible = false;
            this.laugh.visible = false;
            this.jump.visible = false;
            this[this.shouldPlayTarget].visible = true;
            this[this.shouldPlayTarget].gotoAndPlay(0);
            this.shouldPlayTarget = 'idle';
        }

    Container特性

    在上面,我們是直接產生三個動畫物件,在需要使用時才會把visible設為true並且播放,因此要小心是否會造成效能問題,在研究了一下Container.js的原始碼,可以發現如果當元件的visible為false時,是不會去render這個物件的,也不會去處理其相關子元件的畫面變更。


    Source:Container.js

    在經過實測後,的確將畫面上不會被看到的元件的visible設為false後,能夠提升畫面的fps,因此這也是開發上一個可以多加利用的小技巧。

    另外,filter的使用,會花費較多的效能,需要謹慎使用。而mask下的物件,即便因為mask而無法看到,仍舊會一直被render,因此即便是已經在mask下因此畫面上無法看見的元件,依然建議要把可試範圍外的物件的visible設為false,以提升遊戲效能。

    今日成果


    線上demo:http://claire-chang.com/ironman2018/1104/
    今日成果下載:ironman1104

    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裡有非常詳細的使用說明,手冊的範例非常的清楚,在這邊也能夠直接預覽特效的樣子。