iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 19
3
Software Development

遊戲之美 - 連連看經典遊戲開發系列 第 19

[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 {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 : -1);
        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[i].length;j++){
                if(this.board[i][j] != null){
                    return false;
                }
            }
        }
        return true;
    }

    public getFirstExistPath():Path{
        var searchedValue = [];
        for (var i =0;i<this.board.length;i++){
            for (var j = 0; j<this.board[i].length;j++){
                let value = this.board[i][j];
                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[k];
                        let path = new Path(positionsArr[v[0]], positionsArr[v[1]],this);
                        if(path.canLinkInLine()){
                            return path;
                        }
                    }
                }
            }
        }
        return null;
    }

    private getAllValueInBoard(){
        let values = [];
        for (var i =0;i<this.board.length;i++){
            for (var j = 0; j<this.board[i].length;j++){
                if(this.board[i][j] != null){
                    values.push(this.board[i][j]);
                }
            }
        }
        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[i].length;j++){
                if(this.board[i][j] != null){
                    this.board[i][j] = values.pop();
                }
            }
        }
    }

    private pairNumPermutations = {};
    /**
     * 取得輸入的index中,2個2個一組的所有可能排列組合
     */
    public getPairNumPermutations(num:number){
        if(this.pairNumPermutations[num] != null){
            return this.pairNumPermutations[num];
        }
        let data = [];
        for(var i = 0; i <num;i++){
            for(var j = 0; j <num;j++){
                if(i != j && i <= j){
                    data.push([i,j]);
                }
            }
        }
        this.pairNumPermutations[num] = 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[i].length;j++){
                if (this.board[i][j] == 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[i][point.y] == null) {
                        nearByPoint.x = i;
                    } else {
                        break;
                    }
                }
                if (nearByPoint.x == maxLengthDOWN - 1) {
                    nearByPoint.x = maxLengthDOWN;
                }
                break;
            case Direction.RIGHT:
                let maxLengthRIGHT = this.board[0].length;
                for (var i = point.y+1; i < maxLengthRIGHT; i++) {
                    if (this.board[point.x][i] == 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[a.x][i] != null) {
                    return true;
                }
            }
            return false;
        } else if (a.y == b.y) {
            if (a.y == -1 || a.y == this.board[0].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[i][a.y] != null) {
                    return true;
                }
            }
            return false;
        } else {
            return true;
        }
    }

    public hasSameValue(point1: Point, point2: Point): boolean {
        return this.board[point1.x][point1.y] == this.board[point2.x][point2.y];
    }

    public getValue(point: Point): number {
        return this.board[point.x][point.y];
    }

    public clearPoint(point: Point) {
        this.board[point.x][point.y] = null;
        point = null;
    }
}

新增Board.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>;
    public value:number;

    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 = [this.point1,new Point(i, this.point1.y),new Point(i, this.point2.y),this.point2];
                    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 = [this.point1, new Point(this.point1.x, i), new Point(this.point2.x, i), this.point2];
                    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);
            leftPointRIGHT.y = (leftPointRIGHT.y < rightPoint.y) ? leftPointRIGHT.y : rightPoint.y;
            rightPointLEFT.y = (rightPointLEFT.y > leftPoint.y) ? rightPointLEFT.y : leftPoint.y;
            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 = [leftPoint, new Point(leftPoint.x, i), new Point(rightPoint.x, i), rightPoint];
                        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);
            upPointDOWN.x = (upPointDOWN.x < downPoint.x) ? upPointDOWN.x : downPoint.x;
            downPointUP.x = (downPointUP.x > upPoint.x) ? downPointUP.x : upPoint.x;

            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 = [upPoint, new Point(i, upPoint.y), new Point(i, downPoint.y), downPoint];
                        return true;
                    }
                }
            }
        }

        return false;
    }
}

新增Direction.ts,內容如下:

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[i].length;j++){
                this.createIcon(board.board[i][j], 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/


上一篇
[18- Pixi教學] 按鈕製作
下一篇
[20- Pixi教學] 連連看公仔實作- 逐格動畫
系列文
遊戲之美 - 連連看經典遊戲開發31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
SunAllen
iT邦研究生 1 級 ‧ 2018-11-03 07:52:03

/images/emoticon/emoticon07.gif

我要留言

立即登入留言