Posted on

[30- 相關工具] 手機遠程測試

Android系統的手機設備偵錯

在電腦上的偵錯工具首推的就是 Chrome 開發者工具,但是由於若我們想要做手機遊戲,不同的設備有可能會有不同的狀況,我們會需要在不同手機上去做除錯。如果是Android系統的手機,開啟Enable USB Debugging之後,用USB線連至電腦,則可以藉著使用chrome去做遠端偵錯。
偵錯步驟如下:

  • 選擇 Settings > Developer Options > Enable USB Debugging以啟用設備上的開發者選項(要如何開啟請參考此篇文章:Set up a device for development
  • 使用一個 Google 帳戶登錄到 Chrome
  • 打開Chrome 開發者工具(在Chrome選單中選擇 更多工具 > 開發者工具)
  • 在 DevTools 中,點擊 Main Menu 主菜單,然後選擇 More tools > Remote devices。
  • 在 DevTools 中,點擊 Settings 標籤
  • 確保已啓用 Discover USB devices。
  • 使用 USB 將 Android 設備直接連接到電腦上
  • 在左測的remote target選擇要偵錯的設備

iOS的手機設備偵錯

首先針對要偵錯的 iDevice 接上 Mac 並且進入 iOS 設定中的「Safari」選項,在「進階」選單中開啟「網頁檢閱器」,iPhone 畫面如下:

接著我們開啟 MacOS 中的 Safari,並且啟用「開發」選項,如下圖:

當我們的 iOS 開啟網頁時,就可以在 Mac 中的 Safari「開發」功能表中看到連上線的 iDevice,直接選取正在開啟的頁面就可以進行 Debug 囉,畫面如下:

Debug的畫面如下:

利用 weinre 遠端 Debug 網頁元素

官網位置:https://people.apache.org/~pmuellr/weinre/docs/latest/Home.html

weinre 的全名是 WEb INspector REmote,顧名思義是一個遠端的網頁檢視器。在技術上 weinre 其實是一個以 node.js 為基礎的 Http Server,利用了 Web 即時通訊的技巧,將某個已經掛上 Target JavaScript 的 Browser ,透過背景將 DOM 資訊傳遞到 Debugging Tools 中。Debugging Tools 也是由 Web 進行設計,連接後雙方可以即時傳遞一些命令讓我們即時看見反饋,是一個很聰明的做法。而且沒有太多的環境限制,基本上可以執行 JavaScript 的瀏覽器都可以使用。

1. 安裝方式:可以直接由網址下載安裝,也可以透過npm來安裝
透過npm
sudo npm -g install weinre
直接透過網址安裝
sudo npm -g install http://example.com/path/to/apache-cordova-weinre-X.Y.Z-bin.tar.gz

2. 啟動服務
weinre –boundHost 10.0.0.13

接著在電腦內打開網址 http://10.0.0.13:8080,會看到如下的畫面:

3. 設定要被觀察的程式
在程式內加入下面的script

<script src="http://10.0.0.13:8081/target/target-script-min.js"></script>

接著重新整理就可以看到我們在手機設備上開啟的網頁的資訊,點選該連結就可以偵錯該程式

4. 偵錯畫面如下

參考資料

Posted on 1 Comment

[29- 相關工具] 效能評估工具



遊戲效能評估方式

FPS是指影格速率,是用於測量顯示張數的量度。測量單位為「每秒顯示張數」(Frame per Second,FPS)或「赫茲」,一般來說FPS用於描述影片、電子繪圖或遊戲每秒播放多少影格。一般說來,顯示器的張數為60Hz,因此,若超過60的fps容易會有畫面撕裂的狀況產生。而60FPS的遊戲體驗會相較30FPS的遊戲體驗來得更順暢。

PixiJS的ticker的預設FPS為60,可以自行藉由ticker.speed的值來調整遊戲的FPS。

// Scales ticker.deltaTime to what would be
// the equivalent of approximately 120 FPS
ticker.speed = 2;

顯示遊戲現在的fps數值

這邊介紹兩款不同的畫面fps顯示工具,FPSMeter和stats.js

FPSMeter

官網:http://darsa.in/fpsmeter/
使用樣子:

使用方法:

//anchor指要把fps顯示的方框放在那邊,如果沒有傳值進去,則會放至document.body
//options是指相關的設定(請參見:https://github.com/darsain/fpsmeter/wiki/Options)
var meter = new FPSMeter([ anchor ] [, options ]);

若一個遊戲有多個不同的fps設定,則可以在該ticker去設定要顯示的fps是那一個ticker的

// Function that renders one frame
function render() {
	// ... rendering happens here ...
	meter.tick();
}

若要知道每一次render圖像時要花的時間,則使用下列方法:

// Function that renders one frame
function render() {
	meter.tickStart();
	// ... rendering happens here ...
	meter.tick();
}

stats.js

官網:https://github.com/mrdoob/stats.js/
使用樣子:

使用方法:

var stats = new Stats();
stats.showPanel( 1 ); // 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild( stats.dom );

function animate() {

	stats.begin();

	// monitored code goes here

	stats.end();

	requestAnimationFrame( animate );

}

requestAnimationFrame( animate );

這個工具可以同時顯示較多的數值,我們初始顯示的數值雖然只能四選一(0: fps, 1: ms, 2: mb, 3+: custom)
但是只要在顯示fps的方塊用滑鼠點擊,亦可以切換顯示不同的panel。



Chrome Debug Tool

在電腦上測試時最好的效能監測工具還是屬Chrome Debug Tool,切到Performance頁籤,在要監測時按下左上方的錄影按鈕,在結束時按下停止,就會看到如下的畫面:

我們可以看到,最上方有fps的部份,以上圖為例,我們可以觀察到,在遊戲重新loading時的fps最低,這的同時,也是cpu最高的時期,在frame的部份,還可以看到遊戲在每個時間點時的遊戲畫面截圖,幫助我們更了解這個時間軸的每個時刻遊戲畫面是如何呈現。

在2500ms的地方,有一條紅色的線,代表當時是被debug tool判定效能較差的時刻。我們點一下上方的時間軸,可以選擇下方要顯示的資料的範圍。

這時候我們可以看到最上方有一條Timer Fired的任務花費最多時間,這時候大家可能會想什麼ansync.js根本不是我寫的呀,但是其實這個工具上面顯示的名稱是只會顯示一整串序列最初被呼叫的函數名稱。例如在pixi裡的event handler是循序被執行並不會同步執行的,這時如果有許多listener監聽某個事件,其中有一個在聽到該事件後會執行一個很耗效能的動作,在這個performace工具裡會顯示的名稱為該動作emit事件的動作。

因此我們較難直接從這個列表得知造成效能低落的原因,這時,我們可以點選某列我們感興趣的執行動作,這時下方會出現一個面板,我們可以從這個面板得知這個工作項目的細節。

由上圖可得知,這個耗效能的動作,是由於GameScene.draw要繪製整個遊戲場景所造成的。

除了Call-Tree可以讓我們看到這整個工作項目是怎麼被呼叫的,Summary裡可以看到這個動作較花效能的是script、painting還是rendering

參考資料

Posted on

[28- 相關工具] PixiJS devtools

開發人員工具

Chrome 開發者工具是網頁開發者常在使用的偵錯工具,可以檢查下載的檔案、html的dom元素檢視及編輯、調整css等…

開發工具打開方式:
1. 功能表 -> 工具 -> 開發人員工具
2. 直接按 F12 叫出來
3. 在網頁任何位置按右鍵,選擇”檢查元素”,就可以看到原始碼。

工具有許多的面版在最上列,左邊也可以直接選擇要找的html元素的位置

上圖即為偵錯面板的圖示。

關於這個工具更多的資訊請見此:Chrome 開發者工具

PixiJS devtools

一般的debug tools在一般網頁上就十分夠用了,但是對於PIXI來說,因為其繪圖元件都放在Canvas裡,對於畫面上的偵錯較不容易。

因此推薦大家可以安裝下面這個擴充應用程式:

安裝完成後,如果這個網頁的頁面內有使用pixi,則會多顯示這個pixi的tab如下圖

在左邊我們可以看到場景上所有的繪圖元件以及其屬性

當我們點選元件的名稱時,場景上會有框框將這個元件的位置框出來

當我們有物件放到場景上卻沒有看到時,用這個工具可以很方便的查找是否該物件有存在在場景上只是被其他物件壓住,亦或是根本沒有加進場景裡。
也可以從物件的順序看到圖層的相對關係,越下面的代表越上面的圖片。

面版的左上方的Reconnect按鈕則是用來刷新元件資訊,元件列表的更新需要手動按這個鈕才能夠更新到最新。

屬性面版的部份,則是我認為最好用的功能,我們可以直接去檢視現在這個物件的某些屬性是否正確。
例如x、y、visible,並且也可以動態調整。

因為現在Pixi JS的開發工具還沒有專屬的IDE,在開發遊戲時,對位是相對較麻煩的
這個工具因為可以所見即所得,並且動態調整x、y時可以直接在畫面上反應出來,在對位上是十分方便的一個工具。

在調整數值時,可以直接用上、下鍵來增加或減少數字,也可以直接在畫面中馬上看到物件的移動,若是直接輸入數字,較容易游標會有跳掉的狀況。

參考資料

Posted on

[27- Pixi教學] PixiParticles

Particles System

遊戲經常透過粒子系統製作各種視覺效果,例如火焰、煙霧、下雨、沙塵、爆炸等效果,並不容易使用一般的動畫工具製作。通常粒子系統在三維空間中的位置與運動是由發射器控制的,發射器可以設定粒子生成速度(即單位時間粒子生成的數目)、粒子初始速度向量(例如什麼時候向什麼方向運動)、粒子壽命(經過多長時間粒子湮滅)、粒子顏色、在粒子生命周期中的變化以及其它參數等等。經由這些參數,來產生不同的特效效果。

下圖為一個火燄特效的範例圖:

PixiParticles

官網網址如下:PixiParticles
Pixi Particles是一款供Pixi JS去使用的Particles功能,其官網對其功能的介紹如下:

A particle system library for the PixiJS library. Also, we created an interactive particle editor to design and preview custom particle emitters which utilitze PixiParticles.

在官網的下方有範例可供參考,基本使用上會去用Emitter來設定config到特效裡,方法為:

new PIXI.particles.Emitter(particleParent, particleImages, config)

簡單的使用範例如下:

// Create a new emitter
var emitter = new PIXI.particles.Emitter(

    // The PIXI.Container to put the emitter in
    // if using blend modes, it's important to put this
    // on top of a bitmap, and not use the root stage Container
    container,

    // The collection of particle images to use
    [PIXI.Texture.fromImage('image.jpg')],

    // Emitter configuration, edit this to change the look
    // of the emitter
    {
        alpha: {
            list: [
                {
                    value: 0.8,
                    time: 0
                },
                {
                    value: 0.1,
                    time: 1
                }
            ],
            isStepped: false
        },
        scale: {
            list: [
                {
                    value: 1,
                    time: 0
                },
                {
                    value: 0.3,
                    time: 1
                }
            ],
            isStepped: false
        },
        color: {
            list: [
                {
                    value: "fb1010",
                    time: 0
                },
                {
                    value: "f5b830",
                    time: 1
                }
            ],
            isStepped: false
        },
        speed: {
            list: [
                {
                    value: 200,
                    time: 0
                },
                {
                    value: 100,
                    time: 1
                }
            ],
            isStepped: false
        },
        startRotation: {
            min: 0,
            max: 360
        },
        rotationSpeed: {
            min: 0,
            max: 0
        },
        lifetime: {
            min: 0.5,
            max: 0.5
        },
        frequency: 0.008,
        spawnChance: 1,
        particlesPerWave: 1,
        emitterLifetime: 0.31,
        maxParticles: 1000,
        pos: {
            x: 0,
            y: 0
        },
        addAtBack: false,
        spawnType: "circle",
        spawnCircle: {
            x: 0,
            y: 0,
            r: 10
        }
    }
);

// Calculate the current time
var elapsed = Date.now();

// Update function every frame
var update = function(){

    // Update the next frame
    requestAnimationFrame(update);

    var now = Date.now();

    // The emitter requires the elapsed
    // number of seconds since the last update
    emitter.update((now - elapsed) * 0.001);
    elapsed = now;

    // Should re-render the PIXI Stage
    // renderer.render(stage);
};

// Start emitting
emitter.emit = true;

// Start the update
update();

Pixi Particles Editor

粒子特效的config通常需要一個編輯軟體讓美術來調整相關數值,PixiParticles的編輯器在官網有提供,其網址為:https://pixijs.io/pixi-particles-editor/#pixieDust

Posted on

[26- Pixi教學] 實作所有遊戲功能

還未完成的功能

前一篇的成果和我們的mockup相比

還有時間、生命和FB按鈕還未完成。

今天我們要將這些功能全部實作完成。

時間倒數

新增Clock.ts,內容如下

import Container = PIXI.Container;
import { Loader } from "../core/Loader";
import { reloadTimes } from "./GameBoard";
import { eventEmitter } from "../Main";
import { GameFlowEvent } from "../core/Event";

export class Clock extends Container {
    private starList = [];
    private remainTimes:number = 480;
    private remainText:PIXI.Text;
    private clockInterval:any;
    constructor() {
        super();
        this.x = 18;
        this.y = 17;

        this.addChild(PIXI.Sprite.from(Loader.resources['Button'].textures['Clock']));

        eventEmitter.on(GameFlowEvent.CreateNewGameRequest, ()=>{
            this.remainTimes = 480;
            this.remainText.text = "8:00";
        });
        this.remainText = new PIXI.Text("8:00", {
            fontWeight: 'bold',
            fontSize: 20,
            fontFamily: 'Arial',
            fill: '#75C6ED',
            align: 'center',
            stroke: '#FFFFFF',
            strokeThickness: 6
        });
        this.remainText.x = 36;
        this.addChild(this.remainText);
        this.clockInterval = setInterval(this.updateTime.bind(this), 1000);
    }

    public updateTime(){
        this.remainTimes --;
        if(this.remainTimes == 0){
            clearInterval(this.clockInterval);
            eventEmitter.emit(GameFlowEvent.GameEndWithTimeout);
        }
        this.remainText.text = Math.floor(this.remainTimes/60)+':'+((this.remainTimes%60 < 10) ? "0":"")+this.remainTimes%60;
    }
}&#91;/code&#93;
<h3>重整次數限制</h3>
新增<code>Stars.ts</code>,內容如下:
[code lang="js"]import Container = PIXI.Container;
import { Loader } from "../core/Loader";
import { reloadTimes } from "./GameBoard";
import { eventEmitter } from "../Main";
import { GameFlowEvent } from "../core/Event";

export class Stars extends Container {
    private starList = [];
    constructor() {
        super();
        this.x = 20;
        this.y = 78;
        this.updateStarStatus();
        eventEmitter.on(GameFlowEvent.ReloadBoardRequest, this.updateStarStatus.bind(this));
        eventEmitter.on(GameFlowEvent.BoardNeedReload, this.updateStarStatus.bind(this));
        eventEmitter.on(GameFlowEvent.CreateNewGameRequest, this.updateStarStatus.bind(this));
    }

    updateStarStatus = ()=>{
        this.removeChildren();
        for(var i =0;i<3;i++){
            let star:any;
            if(i<reloadTimes){
                star = PIXI.Sprite.from(Loader.resources&#91;'Button'&#93;.textures&#91;'Star_Full'&#93;);
            }else{
                star = PIXI.Sprite.from(Loader.resources&#91;'Button'&#93;.textures&#91;'Star_Empty'&#93;);
            }
            star.x = i*33;
            this.starList.push(star);
            this.addChild(star);
        }
    }
}&#91;/code&#93;
<h3>FB按鈕實作</h3>
新增<code>FBBtn.ts</code>,內容如下:
[code lang="js"]import { ButtonBase } from "./ButtonBase";
import { SoundMgr } from "../core/SoundMgr";

export class FBBtn extends ButtonBase {
    constructor() {
        super('Button','FB',50,410);
    }
    public trigger(){
        window.open(' https://www.facebook.com/claire0318 ', 'Claire Chang');
        SoundMgr.play("About");
    }
}



將元件放進場景

在GameScene.ts新增下面程式碼:

        application.stage.addChild(new FBBtn());
        application.stage.addChild(new Stars());
        application.stage.addChild(new Clock());

今日成果

最終遊戲畫面:

所有commit紀錄請見GITHUB:https://github.com/cochiachang/ironman2018
今日成果demo:http://claire-chang.com/ironman2018/1110/
今日檔案下載:ironman20181110

Posted on

[25- Pixi教學] 遊戲開始、結束與過關畫面

遊戲關卡概念

每個遊戲一般都會需要關卡的概念,也就是過關後可以再重新進行遊戲,並且需要有關卡結局畫面。
今天我們就是要製作這樣的一個過關或遊戲結束的畫面,如下圖:

在pixi內使用文字

在pixi4.0之後可以直接利用pixi來做出很漂亮的文字效果,以下為官網的範例

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

var basicText = new PIXI.Text('Basic text in pixi');
basicText.x = 30;
basicText.y = 90;

app.stage.addChild(basicText);

var style = new PIXI.TextStyle({
    fontFamily: 'Arial',
    fontSize: 36,
    fontStyle: 'italic',
    fontWeight: 'bold',
    fill: ['#ffffff', '#00ff99'], // gradient
    stroke: '#4a1850',
    strokeThickness: 5,
    dropShadow: true,
    dropShadowColor: '#000000',
    dropShadowBlur: 4,
    dropShadowAngle: Math.PI / 6,
    dropShadowDistance: 6,
    wordWrap: true,
    wordWrapWidth: 440
});

var richText = new PIXI.Text('Rich text with a lot of options and across multiple lines', style);
richText.x = 30;
richText.y = 180;

app.stage.addChild(richText);

成果如下:

現在更支援Bitmap font,請參見官網的demo

var app = new PIXI.Application();
document.body.appendChild(app.view);

// // Load them google fonts before starting...!
window.WebFontConfig = {
    google: {
        families: ['Snippet', 'Arvo:700italic', 'Podkova:700']
    },

    active: function() {
        // do something
        init();
    }
};

// include the web-font loader script
/* jshint ignore:start */
(function() {
    var wf = document.createElement('script');
    wf.src = ('https:' === document.location.protocol ? 'https' : 'http') +
        '://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js';
    wf.type = 'text/javascript';
    wf.async = 'true';
    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(wf, s);
})();
/* jshint ignore:end */

function init()
{
    PIXI.loader
        .add('desyrel', 'required/assets/desyrel.xml')
        .load(onAssetsLoaded);

    function onAssetsLoaded() {
        var bitmapFontText = new PIXI.extras.BitmapText('bitmap fonts are\n now supported!', { font: '35px Desyrel', align: 'right' });

        bitmapFontText.x = app.screen.width - bitmapFontText.textWidth - 20;
        bitmapFontText.y = 20;

        app.stage.addChild(bitmapFontText);
    }
}

顯示成果如下:

新增遊戲關卡畫面

新增檔案GameRoundEnd.ts,內容如下:

import Container = PIXI.Container;
import { eventEmitter } from "../Main";
import { GameFlowEvent } from "../core/Event";

export class GameRoundEnd extends Container {
    private text:PIXI.Text;
    constructor() {
        super();
        this.interactive = true;
        this.visible = false;
        eventEmitter.on(GameFlowEvent.GameEndWithTimeout, ()=>{
            this.text.text = "Time is up!";
            this.text.x = 260;
            this.text.y = 200;
            this.visible = true;
        });
        eventEmitter.on(GameFlowEvent.GameEndWithNoPath, ()=>{
            this.text.text = "Game over";
            this.text.x = 260;
            this.text.y = 200;
            this.visible = true;
        });
        eventEmitter.on(GameFlowEvent.GamePass, ()=>{
            this.text.text = "Congratulations! \nYou passed!";
            this.text.x = 210;
            this.text.y = 200;
            this.visible = true;
        });
        //黑底
        let gt = new PIXI.Graphics();
        gt.beginFill(0x000000, 0.9);
        gt.drawRect(0,0,860,540);
        gt.endFill();
        this.addChild(gt);
        //文字
        this.text = new PIXI.Text("Congratulations! \nYou passed!", {
            fontWeight: 'bold',
            fontSize: 60,
            fontFamily: 'Arial',
            fill: '#ff0000',
            align: 'center',
            stroke: '#FFFFFF',
            strokeThickness: 3
        });
        this.addChild(this.text);
        //再玩一次按鈕
        let btn = new PIXI.Graphics();
        btn.beginFill(0x75C7ED);
        btn.drawRoundedRect(700,480,115,35,10);
        btn.endFill();
        btn.buttonMode = true;
        btn.interactive = true;
        btn.on("mouseup", this.trigger.bind(this));
        btn.on("touchend", this.trigger.bind(this));
        this.addChild(btn);
        let newGame = new PIXI.Text("New Game", {
            fontWeight: 'bold',
            fontSize: 20,
            fontFamily: 'Arial',
            fill: '#75C6ED',
            align: 'center',
            stroke: '#FFFFFF',
            strokeThickness: 6
        });
        newGame.x = 705;
        newGame.y = 483;
        this.addChild(newGame);
    }
    public trigger(){
        eventEmitter.emit(GameFlowEvent.CreateNewGameRequest);
        this.visible = false;
    }
}

並修改GameBoard.ts內iconClickHandler的內容如下

let iconClickHandler = ()=>{
    this.cancelTips();
    if (this.selected) {
        let selectCorrect = false;
        this.select2 = new Point(x, y);
        this.iconSelected(this.select2);
        setTimeout(()=>{
            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.pathHistory.push(path);
                        this.valueHistory.push(board.getValue(this.select1));
                        LinkedLine.instance.drawPath(path);
                        this.clearIcon(this.select1);
                        this.clearIcon(this.select2);
                        eventEmitter.emit(GameFlowEvent.LinkedLineSuccess);
                        selectCorrect = true;
                        //判斷還有沒有路走
                        if(board.gameRoundEnd()){
                            eventEmitter.emit(GameFlowEvent.GamePass);
                        }else if(board.getFirstExistPath() == null){
                            if(reloadTimes > 0){
                                this.reloadBoard();
                                eventEmitter.emit(GameFlowEvent.BoardNeedReload);
                            }else{
                                eventEmitter.emit(GameFlowEvent.GameEndWithNoPath);
                            }
                        }
                    }
                }
            }
            if(selectCorrect){
                SoundMgr.play('Sound_select_crrect');
            }else{
                SoundMgr.play('Sound_select_error');
                this.iconUnSelected(this.select1);
                this.iconUnSelected(this.select2);
            }
            this.selected = false;
        },0);

    } else {
        this.select1 = new Point(x, y);
        this.iconSelected(this.select1);
        this.selected = true;
        SoundMgr.play('Sound_select_1');

    }
};

今日成果

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

Posted on

[24- Pixi教學] 提示、重整按鈕功能實作

實作提示按鈕

新增檔案TipBtn.ts,內容如下:

import { ButtonBase } from "./ButtonBase";
import { eventEmitter } from "../Main";
import { GameFlowEvent } from "../core/Event";

export class TipBtn extends ButtonBase {
    constructor() {
        super('Button','Tip',50,287);
    }

    public trigger(){
        eventEmitter.emit(GameFlowEvent.TipsRequest);
    }
}

在GameBoard.ts裡的constructor監聽TipsRequest事件

eventEmitter.on(GameFlowEvent.TipsRequest,this.showTips.bind(this));

並新增所需要的方法

    private tipsPath:Path;
    showTips = ()=>{
        this.tipsPath = board.getFirstExistPath();
        let icon1 = this.getChildByName('icon_'+this.tipsPath.point1.x+"_"+this.tipsPath.point1.y) as GameIcon;
        icon1.select();//為可連線的方塊增加紅框提示玩家

        let icon2 = this.getChildByName('icon_'+this.tipsPath.point2.x+"_"+this.tipsPath.point2.y) as GameIcon;
        icon2.select();
        SoundMgr.play('Tips');
    }

因為若玩家此時選擇了其他的方塊,我們需要把提示的框消除掉以避免混淆

    createIcon = (id, x, y)=>{
        let icon = new GameIcon(id,x,y);
        this.addChild(icon);
        let iconClickHandler = ()=>{
            this.cancelTips();//在這時將提示的框消除
            if (this.selected) {
                //...
            }
        }
    }

並實作消除提示紅框的功能

    cancelTips=()=>{
        if(this.tipsPath == null){
            return;
        }
        let icon1 = this.getChildByName('icon_'+this.tipsPath.point1.x+"_"+this.tipsPath.point1.y) as GameIcon;
        if(icon1) icon1.unSelect();

        let icon2 = this.getChildByName('icon_'+this.tipsPath.point2.x+"_"+this.tipsPath.point2.y) as GameIcon;
        if(icon2) icon2.unSelect();
    }

實作重整按鈕

新增檔案ReloadBtn.ts,內容如下:

import { ButtonBase } from "./ButtonBase";
import { eventEmitter } from "../Main";
import { GameFlowEvent } from "../core/Event";
import { reloadTimes } from "./GameBoard";

export class ReloadBtn extends ButtonBase {
    constructor() {
        super('Button','Reflash',50,230);
        eventEmitter.on(GameFlowEvent.GameRoundStart,(()=>{
            this.enable = true;
        }).bind(this))
    }
    public trigger(){
        if(reloadTimes > 0){
            eventEmitter.emit(GameFlowEvent.ReloadBoardRequest);
        }
        if(reloadTimes == 0){
            this.enable = false;
        }
    }
}

在GameBoard.ts裡的constructor監聽ReloadBoardRequest事件

eventEmitter.on(GameFlowEvent.ReloadBoardRequest, this.reloadBoard.bind(this));

並實作所需的方法

    reloadBoard = ()=>{
        this.reloadTimes--;
        do{
            board.rearrangeBoard();
        }while(board.getFirstExistPath() == null)
        this.drawBoardIcon();
        SoundMgr.play('ReloadBoard');
    }

今日成果

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

Posted on

[23- Pixi教學] 復原按鈕功能實作

堆疊與佇列

堆疊 (Stack)

  • 加入(push)與刪除(pop)於同一端
  • 具有後進先出(LIFO, Last-in-First-out)或先進後出(FILO, First-in-Last-out)性質的有序串列
  • 例子:疊盤子、發牌、走迷宮

佇列 (Queue)

  • 加入(enqueue)與刪除(dequeue)於不同端(front & rear)
  • 先進先出(FIFO, First-in-First-out)
  • 例子:排隊買票、坐公車

實作復原功能

復原功能比較像是走迷宮,一定是先從最後一步開始復原,因此這邊我們採用堆疊的方式。堆疊最簡單的實作方式就是使用array,並用pop以及push來塞入、取出資料。

首先先創建一個類別名為RevertBtn.ts

import { ButtonBase } from "./ButtonBase";
import { eventEmitter } from "../Main";
import { GameFlowEvent } from "../core/Event";

export class RevertBtn extends ButtonBase {

    constructor() {
        super('Button','Revert',50,345);
    }
    public trigger(){
        eventEmitter.emit(GameFlowEvent.RevertBackRequest);
    }
}

接著在GameScene.ts加入RevertBtn這個按鈕

export class GameScene {

    public static draw(){
        //加入背景
        application.stage.addChild(PIXI.Sprite.from(Loader.resources["background"].texture));
        //加入按鈕
        application.stage.addChild(new SoundBtn());
        application.stage.addChild(new RevertBtn());
        //加入連連看牌面
        application.stage.addChild(new GameBoard());
        application.stage.addChild(LinkedLine.instance);
        //角色動畫
        application.stage.addChild(new Character());
    }
}

然後去GameBoard.ts註冊GameFlowEvent.RevertBackRequest這個事件

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

        eventEmitter.on(GameFlowEvent.RevertBackRequest,this.revertBoard.bind(this));
    }

並新增revertBoard的方法

    revertBoard = ()=>{
        let value = this.valueHistory.pop();
        let path = this.pathHistory.pop();
        if(value != null && path != null){
            board.board[path.point1.x][path.point1.y] = value;
            board.board[path.point2.x][path.point2.y] = value;

            this.drawBoardIcon();
            SoundMgr.play('Back');
        }
    }ack');
        }
    }

然後在每次成功連線時,將成功連線的方塊的值和位置存進陣列裡

createIcon = (id, x, y)=>{
        let icon = new GameIcon(id,x,y);
        this.addChild(icon);
        let iconClickHandler = ()=>{
            if (this.selected) {
                let selectCorrect = false;
                this.select2 = new Point(x, y);
                this.iconSelected(this.select2);
                setTimeout(()=>{
                    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.pathHistory.push(path);//存入選擇的點
                                this.valueHistory.push(board.getValue(this.select1));//存入消掉的符號id
                                LinkedLine.instance.drawPath(path);
                                this.clearIcon(this.select1);
                                this.clearIcon(this.select2);
                                eventEmitter.emit(GameFlowEvent.LinkedLineSuccess);
                                selectCorrect = true;
                                //判斷還有沒有路走
                                if(board.gameRoundEnd()){
                                    alert("恭喜完成遊戲!");
                                    this.createNewGame();
                                }else if(board.getFirstExistPath() == null){
                                    this.reloadTimes--;
                                    board.rearrangeBoard();
                                }
                            }
                        }
                    }
                    if(selectCorrect){
                        SoundMgr.play('Sound_select_crrect');
                    }else{
                        SoundMgr.play('Sound_select_error');
                        this.iconUnSelected(this.select1);
                        this.iconUnSelected(this.select2);
                    }
                    this.selected = false;
                },0);

            } 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);
    }

今日成果

線上展示:http://claire-chang.com/ironman2018/1107
今日成果下載: ironman20181107

Posted on Leave a comment

[22- Pixi教學] 按鈕動態- Tween

GreenSock

下面是GSAP官網對這個產品的說明:

Think of GSAP as the Swiss Army Knife of javascript animation…but better. It animates anything JavaScript can touch (CSS properties, canvas library objects, SVG, generic objects, whatever) and it solves countless browser inconsistencies, all with blazing speed (up to 20x faster than jQuery), including automatic GPU-acceleration of transforms.

因為GSAP可以使用GPU加速,因此在執行速度效能上會勝過jQuery,也因為它是很單純的去漸變改變某個屬性,所以我們可以很容易的使用在任何我們可操控的屬性上去製作動畫效果,是一個十分強大好用的工具。

要嘗試動畫效果的樣式和程式寫法,官網有提供GSDevTools可供我們使用,可以更容易的編輯我們要的動畫樣式

TweenLite

TweenLiteTweenMax是當我們只有一個Tween時可使用的,例如:把一個物件從左邊移到右邊。

TweenLite.to(".grey", 1, {x: 700, rotation: 360, delay:3, id:"grey"})

而這兩個類別的差異在於TweenMax提供更多的參數和方法可供我們使用

TimelineLite

TimelineLiteTimelineMax則是當我們要做一系列的Tween時可使用的,例如:某按鈕按下去要先放大再縮小再彈回來。

var tl = new TimelineMax({repeat:2, repeatDelay:1});
tl.add( TweenLite.to(element, 1, {scale:0.9}) );
tl.add( TweenLite.to(element, 1, {scale:1.1}) );
tl.add( TweenLite.to(element, 1, {scale:1}) );

而TimelineLite與TimelineMax的差異也是TimelineMax提供更多方法和參數可以使用。

為按鈕加Tween效果

首先我們要在專案裡加入使用gsap
npm install gsap
以及@types/gsap
npm install –save @types/gsap
設定在build的時後要複製TweenLite.min.js及TimelineMax.min.js到libs裡(gulpfile.js)

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',
            'node_modules/howler/dist/howler.core.min.js',
            'node_modules/gsap/src/minified/TweenLite.min.js',
            'node_modules/gsap/src/minified/TimelineMax.min.js '
        ])
        .pipe(gulp.dest("build/lib"));
});  

並在index.js裡加上

  <script src="lib/TweenLite.min.js"></script>
  <script src="lib/TimelineMax.min.js"></script>

接著修改ButtonBase.ts的內容如下:

import Sprite = PIXI.Sprite;
import {Loader} from "../core/Loader";
//add gasp
declare const TweenLite;
declare const TimelineMax;

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(){
    }
    private _enable:boolean = true;
    public set enable(v:boolean){
        this.interactive = v;
        this.buttonMode = v;
        this.alpha = v ? 1:0.5;
    }
    //滑鼠按下時會先縮小
    public mouseDownEffect():void{
        let animTweenTimeline = new TimelineMax();
        animTweenTimeline.add(new TweenLite(this, 0.2,
            {
                "scaleX": 0.9,
                "scaleY": 0.9
            }));
        animTweenTimeline.play();
    }
    //滑鼠移開時先放大再縮小
    public mouseUpEffect():void{
        let animTweenTimeline = new TimelineMax();
        animTweenTimeline.add(new TweenLite(this, 0.1,
            {
                "scaleX": 1.1,
                "scaleY": 1.1
            }));
        animTweenTimeline.add(new TweenLite(this, 0.1,
            {
                "scaleX": 1,
                "scaleY": 1
            }));
        animTweenTimeline.play();
    }
    set scaleX(v:number){
        this.scale.x = v;
    }
    set scaleY(v:number){
        this.scale.y = v;
    }
    public mouseOutEffect():void{
        this.scale.set(1,1);
    }
}

今日成果

線上成果展示:http://claire-chang.com/ironman2018/1106/
今日成果下載:ironman20181106

Posted on Leave a comment

[21- Pixi教學] 連線效果實作-Graphics

連連看連線及選擇題示

在進行遊戲時,清楚的玩家操作說明及互動是很重要的遊戲要素。因此,像一般的連連看遊戲,都會在玩家選擇圖示之後,把玩家所選擇的圖示做效果,提示玩家已經選擇了某個符號。並且在玩家選擇了第二個符號且連線成功時,會顯示所經過的路徑,並畫出一條線來消除這兩個符號,如下圖:

在這一篇裡,我們就要實做這樣的功能。

Graphics

這邊是官網使用Graphics的一個使用範例:Graphics
這是API說明:http://pixijs.download/release/docs/PIXI.Graphics.html
下面是畫線的一個簡單範例:

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

var graphics = new PIXI.Graphics();

// set a line style
graphics.lineStyle(4, 0xffd900, 1);

// draw a shape
graphics.moveTo(50,50);
graphics.lineTo(250, 50);
graphics.lineTo(250, 250);
graphics.endFill();

app.stage.addChild(graphics);

成果如下:

而這是畫矩型的一個簡單範例:

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

var graphics = new PIXI.Graphics();

// draw a rounded rectangle
graphics.lineStyle(2, 0xFF00FF, 1);
graphics.beginFill(0xFF00BB, 0);
graphics.drawRoundedRect(150, 450, 300, 100, 1);
graphics.endFill();

app.stage.addChild(graphics);

成果如下:

為方塊加上選取效果

在過去,我們在產生方塊是直接new一個Sprite並加入場景,而現在方塊要能夠有被選取、取消選取的功能,因此我們將方塊拉出成為一個獨立的類別GameIcon
其內容如下:

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

export class GameIcon extends Sprite{
    constructor(id,x,y) {
        super();
        this.texture = Loader.resources['Icon'].textures['icon_' + id];
        this.name = 'icon_' + x + "_" + y;//方便可以從父層更容易的取出這個方塊
        this.width = this.height = 45;
        this.x = (this.width + 20) * x + 22.5;
        this.y = (this.width + 6) * y + 22.5;
        this.anchor.set(0.5);//縮放時可以以中間為中心點
        this.buttonMode = true;
        this.interactive = true;
    }

    //選擇時,繪製邊框,顏色為紅色
    select = ()=>{
        let gt = new PIXI.Graphics();
        gt.lineStyle(3,0xFF0000,1);
        gt.drawRect(-3-22.5,-3-22.5,51,51);
        gt.endFill();
        this.addChild(gt);
    }

    //取消選擇時,將邊框拿掉
    unSelect = ()=>{
        this.removeChildren();
    }
}

接著我們在GameBoard.ts裡,撰寫兩個方法iconSelectediconUnSelected如下:

    iconSelected = (point:Point)=>{
        //根據在GameIcon設定的name來取得正確位置上的方塊
        let icon = this.getChildByName('icon_'+point.x+"_"+point.y) as GameIcon;
        icon.select();
    };

    iconUnSelected = (point:Point)=>{
        let icon = this.getChildByName('icon_'+point.x+"_"+point.y) as GameIcon;
        icon.unSelect();
    };

然後改寫GameBoard.ts裡的createIcon方法

    createIcon = (id, x, y)=>{
        let icon = new GameIcon(id,x,y);//id為要顯示的圖片編號,x,y為位置
        this.addChild(icon);
        let iconClickHandler = ()=>{
            if (this.selected) {
                let selectCorrect = false;
                this.select2 = new Point(x, y);
                this.iconSelected(this.select2);//將方塊加上紅框
                setTimeout(()=>{//為了避免第二個方塊都還沒有繪製到邊框就被取消掉,因此在此增加setTimeout
                    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.clearIcon(this.select1);
                                this.clearIcon(this.select2);
                                eventEmitter.emit(GameFlowEvent.LinkedLineSuccess);
                                selectCorrect = true;
                                //判斷還有沒有路走
                                if(board.gameRoundEnd()){
                                    alert("恭喜完成遊戲!");
                                    this.createNewGame();
                                }else if(board.getFirstExistPath() == null){
                                    this.reloadTimes--;
                                    board.rearrangeBoard();
                                }
                            }
                        }
                    }
                    if(selectCorrect){
                        SoundMgr.play('Sound_select_crrect');
                    }else{
                        SoundMgr.play('Sound_select_error');
                        //不能消除,取消紅框
                        this.iconUnSelected(this.select1);
                        this.iconUnSelected(this.select2);
                    }
                    this.selected = false;
                },0);

            } 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);
    }

為消除加上連線路徑

當成功消除兩個方塊時,應該要有剛剛連線的路徑,這樣使用者才能夠確定連線的方式是正確的,現在我們要為遊戲加上這個功能。

我希望能夠在盤面的上方加上一層圖層,能夠繪製剛剛成功消除的方塊的連線路徑。之前我們在[6 – 遊戲邏輯] 連線消除程式撰寫的地方,所撰寫的Path類別若呼叫canLinkInLine()結果反回為true的話,同時亦會把所經的路徑的點塞入path_Detail這個陣列裡面。

所以這個類別的主要職責,應該是要能夠把輸入的path裡的路徑畫出來。為了要使這個圖層能更方便的在各個地方被取用,我使用了singleton方法來建立這個物件,這樣所有的類別都可以利用LinkedLine.instance來取得這個元件惟一的實體。

下面為LinkedLine.ts的資料

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

export class LinkedLine extends Container {

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

    //將這個類別設定為singleton類別
    private static _instance:LinkedLine;
    public static get instance():LinkedLine{
        if(this._instance == null){
            this._instance = new LinkedLine();
        }
        return this._instance;
    }

    //輸入一個path物件,藉由paths.path_Detail來畫出連線
    public drawPath(paths:Path){
        this.removeChildren();
        let point = paths.path_Detail.pop();//取出第一個點
        let gt = new PIXI.Graphics();
        gt.lineStyle(5, 0xff0000);
        let start = this.getPositionFromPoint(point);
        gt.moveTo(start.x,start.y);//先移到第一個點的位置
        do{
            point = paths.path_Detail.pop();//取出後面的點
            let line = this.getPositionFromPoint(point);
            gt.lineTo(line.x,line.y);//繪製連線
        }while(paths.path_Detail.length > 0);

        this.addChild(gt);

        //設定連線會在500毫秒後自動消失
        setTimeout(()=>{this.removeChildren();},500);
    }
    //把遊戲盤面的x,y系統轉化為畫面上實際的坐標系統
    public getPositionFromPoint(point:Point){
        let x = (45 + 20) * point.x + 22.5;
        let y = (45 + 6) * point.y + 22.5;
        if(y < 0){
            y = -5;
        }
        if(y > 502){
            y = 510;
        }
        return new Point(x, y);
    }
}

接著在GameBoard.ts裡連線成功時加上這行來繪製連線

LinkedLine.instance.drawPath(path);

並在GameScene.ts裡加上LinkedLine元件

application.stage.addChild(LinkedLine.instance);

今日成果

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