PixiJS裡較常使用的顯示元件有:PIXI.Container、Sprite、AnimatedSprite 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就是一個可以放其他顯示元件的一個容器,像Sprite、AnimatedSprite等也都是繼承自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版本的連連看遊戲之中。
在邏輯程式的部份,Path
、Board
、Direction
都可以直接移來專案內使用。
這些程式碼的邏輯解釋在前幾篇的系列文中都有詳細的說明。
新增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());
完整的程式碼內容如下:
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/