我的新書AI 職場超神助手:ChatGPT 與生成式 AI 一鍵搞定工作難題的教材投影片已製作完成
歡迎各位有需要的教師和博碩文化索取教材

[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/


17年資歷女工程師,專精於動畫、影像辨識以及即時串流程式開發。經常組織活動,邀請優秀的女性分享她們的技術專長,並在眾多場合分享自己的技術知識,也活躍於非營利組織,辦理活動來支持特殊兒及其家庭。期待用技術改變世界。

如果你認同我或想支持我的努力,歡迎請我喝一杯咖啡!讓我更有動力分享知識!