Posted on

Linux用pm2來管理伺服器

pm2介紹

pm2 是一個管理 Node.js process 的工具,它可以讓我們用簡單的指令啟動 Node.js cluster 並最大化使用我們的 CPU,可以利用它來簡化很多node應用管理的繁瑣任務,如性能監控、自動重啟、負載均衡等,而且使用非常簡單。

安裝

使用npm安裝
npm install pm2 -g

目錄介紹

pm2安裝好後,會自動創建下面目錄。
$HOME/.pm2 將包含所有PM2相關文件
$HOME/.pm2/logs 將包含所有應用程序日誌
$HOME/.pm2/pids 將包含所有應用程序pids
$HOME/.pm2/pm2.log PM2記錄
$HOME/.pm2/pm2.pid PM2 pid
$HOME/.pm2/rpc.sock 遠程命令的套接字文件
$HOME/.pm2/pub.sock 可發布事件的套接字文件
$HOME/.pm2/conf.js PM2配置

啟動程序

pm2 start後面加上node.js 起始檔案
pm2 start app.js
使用叢集(cluster)的方式來起始檔案
pm2 start -i 4 –name server app.js

刪除Process

根據id, name刪除process
pm2 delete { id or name }

停止Process

根據id, name停止process
pm2 stop { id or name }

重新啟動Process

根據id, name重新啟動process
pm2 restart { id or name }

監控

列出目前所有已啟動的process並顯示他們目前的狀態, 按下ctrl+c 可以離開monitor
pm2 monit

利用叢集增加效能

請參考此篇文章:使用 pm2 啟動 Node.js cluster 以提升效能
截取作者實測結果報告:

可以發現開多個 process 處理的時候,消化 1000 個 request 的時間從 17.4 秒降到 9.9 秒,而每個 request 的回應時間也從 1668ms 降到 930ms,整體的速度大概快了兩倍(我的電腦是四核心,如果配備更好的話會快更多)

參考資料

Posted on

在Linux與Windows間傳送檔案

PSCP介紹

PSCP 是一套使用命令提示列的軟體,是PuTTY相關可選擇使用的軟體。它提供 SCP client 的功能。當我們僅需要將一個或少數檔案從 pc 端 upload 到 server 端時,這套軟體就提供非常安全的方法,使得所傳送的內容不會被其他人給竊聽。倘若遠端有提供 SSH2 建議還是使用 PSFTP 會比較好。

檔案下載:pscp.exe
下載原站:Download PuTTY: latest release (0.72)

linux傳送檔案到windows

下載單一檔案: /claire/test.txt為linux檔案位置,/tmp為windows要下載到的位置

pscp claire@8.8.8.8:/claire/test.txt /tmp

下載整個資料夾: /claire/為linux檔案位置,/tmp為windows要下載到的位置

pscp -r claire@8.8.8.8:/claire /tmp

windows傳送檔案到linux

pscp c:3.xls root@xxx.xxx.xxx.xxx:/home/uploads

代表將於Windows之C槽下的一123.xls檔案,
傳送至IP為xxx.xxx.xxx.xxx的Linux中的/home/uploads資料夾下。

參考資料

Posted on

PieTTY : 免費的SSH及Telnet client

軟體介紹

軟體下載:pietty0400b14.exe
官方網站:PieTTY:A terminal forked from PuTTY by piaip

PuTTY 是個小巧方便的 Telnet/SSH 安全遠端連線程式, 但用於非英語系文字時有非常多的問題, 而且它對於初學者來說過於複雜的使用界面也為人詬病已久。 PieTTY (舊稱 pputty) 則是源自於 PuTTY ,修正與完整支援亞洲等多國語系字元、 並在使用界面上大幅改進、易學易用的版本。

連線至Linux伺服器


按下『連線』後,就會出現等待登入與輸入帳/密資料的畫面,輸入帳密後就可以連線到遠端的linux電腦了。

如果想更改程式的編碼,可以選擇『選項 > 字元編碼 > 想顯示的編碼』,這樣就可以正常顯示各種不同編碼的文字

更多的連線方式

如果想要嘗試更多的連線方式,可以至此文章有更詳盡的介紹:
鳥哥的 Linux 私房菜 第十一章、遠端連線伺服器SSH / XDMCP / VNC / RDP

參考資料

Posted on

Windows Services的設置

如果我們想要把一個EXE檔設定為開機自動執行,就需要把這個程式設定為service

開啟services管理畫面

打開搜尋,輸入

services.msc

就會看到以下畫面

就可以看到現有的所有service列表和狀態了

新增exe檔案為服務

使用sc create並設定呼叫程式的方式,如下

sc create MyProgramName binpath= “C:\MyFolder\MyProgram.exe” type= own start= auto

刪除現有的服務

使用以下的語法,可移除現有的服務

sc delete ServiceName

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