Назад | Учебник TypeScript | Вперёд
Напишем немного логики для нашей игры. При клике на какой-нибудь шарик он у нас должен схлопываться с шариками того-же цвета.
Для начала добавим логику обработки клика мышкой. Но перед этим произведём небольшой рефакторинг: вынесем canvas в свойства engine:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Engine { ... private canvas:HTMLCanvasElement; public constructor() { this.board = new Board(Engine.BOARD_WIDTH, Engine.BOARD_HEIGHT); this.canvas = <HTMLCanvasElement>document .getElementById("tscolorballscanvas"); let engine = this; setTimeout(function() {engine.onStep();}, Engine.TICK_MILLISECONDS); } ... } |
В обработчике onPaint теперь будем использовать это созданное поле:
1 2 3 |
private onPaint():void { let ctx = this.canvas.getContext("2d"); ... |
В конструктор Engine добавим подписку на событие mousedown, обрабатывающее нажатие кнопки мышки:
1 2 |
this.canvas.addEventListener("mousedown", function(event) { engine.onClick(event) }); |
Затем опишем сам обработчик onClick:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Engine { ... private onClick(event:MouseEvent):void { let canvasPosition = this.getElementPosition(this.canvas); let mouseX = event.pageX - canvasPosition.x; let mouseY = event.pageY - canvasPosition.y; let tileX = Math.floor(mouseX / Engine.TILE_WIDTH); let tileY = Math.floor(mouseY / Engine.TILE_HEIGHT); alert(tileX + " " + tileY); } ... } |
Здесь мы просто вычисляем ячейку в Board, на которую приходится клик мышкой. Метод getElementPosition я взял из своей прошлой игры html5lines, но переделал на TypeScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Engine { ... // Get X and Y position of the elm (from: vishalsays.wordpress.com) private getElementPosition(elm):Point { var x = elm.offsetLeft; // set x to elm's offsetLeft var y = elm.offsetTop; // set y to elm's offsetTop elm = elm.offsetParent; // set elm to its offsetParent // use while loop to check if elm is null // if not then add current elm's offsetLeft to x // offsetTop to y and set elm to its offsetParent while(elm != null) { x = parseInt(x) + parseInt(elm.offsetLeft); y = parseInt(y) + parseInt(elm.offsetTop); elm = elm.offsetParent; } // returns an object with "xp" (Left), "=yp" (Top) position return new Point(x, y); } ... } |
Внутри getElementPosition мы используем класс Point, поэтому нам придётся создать новый файл “Point.ts”:
1 2 3 4 5 6 7 8 9 |
class Point { public x:number; public y:number; constructor(x:number, y:number) { this.x = x; this.y = y; } } |
Если сейчас мы соберём проект командой tsc, то при клике мышкой на ячейку должно выходить сообщение с координатами по ширине и высоте ячейки в Board.
Добавим теперь перечисление Situation в новом файле “Situation.ts”. Оно будет хранить текущее состояние игры, на текущий момент это два возможных значения:
1 2 3 4 |
enum Situation { GAME, SHOW_SCORE } |
Добавим новое поле в “Engine.ts” и проинициализируем его в конструкторе:
1 2 3 4 5 6 7 8 |
class Engine { ... private situation:Situation; public constructor() { this.situation = Situation.GAME; ... } |
В обработчике клика добавим проверку текущей ситуации:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Engine { ... private onClick(event:MouseEvent):void { let canvasPosition = this.getElementPosition(this.canvas); let mouseX = event.pageX - canvasPosition.x; let mouseY = event.pageY - canvasPosition.y; switch (this.situation) { case Situation.GAME: let tileX = Math.floor(mouseX / Engine.TILE_WIDTH); let tileY = Math.floor(mouseY / Engine.TILE_HEIGHT); alert(tileX + " " + tileY); break; } } ... } |
Если вы сейчас соберёте проект и откроете “tscolorballs.html”, то по клику на шарик у вас должно появляться диалоговое окно с координатами клика.
Добавим выбор шариков одного цвета и их удаление с игровой доски. Для этого заменим наш код с alert на:
1 2 3 4 5 6 7 8 9 10 |
case Situation.GAME: let tileX = Math.floor(mouseX / Engine.TILE_WIDTH); let tileY = Math.floor(mouseY / Engine.TILE_HEIGHT); if (this.board.getTileState(tileX, tileY) != TileState.EMPTY) { let removedCount = this.removeSameBalls(tileX, tileY); if (removedCount > 0) { this.situation = Situation.ANIMATION; } } break; |
Мы просто вызываем метод removeSameBalls(tileX, tileY), который сейчас напишем, и смотрим количество удалённых шаров с поля. Если какие-нибудь шары были удалены, то мы переключаем ситуацию игры в ANIMATION, где будет происходить сдвиг шаров. Эту ситуацию нужно добавить в перечисление Situation в “Situation.ts”:
1 2 3 4 5 |
enum Situation { GAME, ANIMATION, SHOW_SCORE } |
Метод removeSameBalls в классе Engine будет содержать алгоритм по определению близлежащих шаров одного цвета и их удалению:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
private removeSameBalls(tileX: number, tileY: number):number { let a:ProcessState[][] = new Array<Array<ProcessState>>(); for (let n:number = 0; n < Engine.BOARD_WIDTH; n++) { a[n] = new Array<ProcessState>(Engine.BOARD_HEIGHT); for (let m:number = 0; m < Engine.BOARD_HEIGHT; m++) { a[n][m] = ProcessState.READY; } } let pointsToProcess:Point[] = new Array<Point>(); let toRemove:Point[] = new Array<Point>(); pointsToProcess.push(new Point(tileX, tileY)); let tileState:TileState = this.board.getTileState(tileX, tileY); while (pointsToProcess.length > 0) { let point = pointsToProcess.pop(); a[point.x][point.y] = ProcessState.PROCESSED; toRemove.push(point); if (point.x > 0) { this.pushIfSameColorAndNotProcessed( pointsToProcess, a, new Point(point.x - 1, point.y), tileState); } if (point.x < Engine.BOARD_WIDTH - 1) { this.pushIfSameColorAndNotProcessed( pointsToProcess, a, new Point(point.x + 1, point.y), tileState); } if (point.y > 0) { this.pushIfSameColorAndNotProcessed( pointsToProcess, a, new Point(point.x, point.y - 1), tileState); } if (point.y < Engine.BOARD_HEIGHT - 1) { this.pushIfSameColorAndNotProcessed( pointsToProcess, a, new Point(point.x, point.y + 1), tileState); } } if (toRemove.length > 1) { for (let n = 0; n < toRemove.length; n++) { let point:Point = toRemove[n]; this.board.setTileState(point.x, point.y, TileState.EMPTY); } } return toRemove.length; } private pushIfSameColorAndNotProcessed(pointsToProcess:Point[], a: ProcessState[][], point:Point, tileState:TileState): void { if ((this.board.getTileState(point.x, point.y) == tileState) && (a[point.x][point.y] == ProcessState.READY)) { pointsToProcess.push(point); } } |
Я даже не знаю, стоит ли здесь что-то описывать. Тут по TypeScript почти ничего нет. Одна логика. Смысл в том, что мы используем массив pointsToProcess в качестве стека, содержащего позиции, которые нам нужно ещё обработать. Для каждой из позиций мы смотрим цвет шара в ней, если цвет совпадает с удаляемым, то мы в двумерном массиве a помечаем эту позицию как ProcessState.PROCESSED, добавляем координату в toRemove (чтобы позднее за один цикл очистить все ячейки поля, которые нужно). Массив a используется для того, чтобы мы не обрабатывали несколько раз одни и те же точки, то есть, чтобы не зациклились.
Если координата добавлена в toRemove, то для неё смотрятся шары слева, справа, сверху и снизу. Они обрабатываются аналогично.
Нам также нужно добавить файл “ProcessState.ts” с новым перечислением:
1 2 3 4 |
enum ProcessState { READY, PROCESSED } |
Нам также нужно добавить новый метод setTileState в класс Board, так как мы используем его в алгоритме удаления:
1 2 3 |
public setTileState(x:number, y:number, tileState:TileState):void { this.tiles[x][y] = tileState; } |
Если вы сейчас соберёте и запустите проект, то у вас по клике на группе шаров одинакового цвета они должны пропадать, а их освобождённое место закрашиваться чёрным цветом.
Обычно в подобных играх после удаления с поля группы шаров остальные падают вниз, заполняя пустое пространство. Для этого в методе onStep нужно добавить обработку ситуации ANIMATION, на которую мы переключили наше свойство situation у Engine в обработчике клика.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Engine { ... private onStep():void { if (this.situation == Situation.ANIMATION) { this.processGravity(); } this.onPaint(); let engine = this; setTimeout(function () { engine.onStep(); }, Engine.TICK_MILLISECONDS); if (console) console.log("endOnStep"); } ... } |
Сам метод processGravity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
class Engine { ... private processGravity():void { let movesCount:number = 0; for (let x = 0; x < Engine.BOARD_WIDTH; x++) { for (let y = 0; y < Engine.BOARD_HEIGHT - 1; y++) { let tileState:TileState = this.board.getTileState(x, y); if ((tileState != TileState.EMPTY) && (this.board.getTileState(x, y + 1) == TileState.EMPTY)) { this.board.setTileState(x, y + 1, tileState); this.board.setTileState(x, y, TileState.EMPTY); movesCount++; } } } for (let x = 1; x < Engine.BOARD_WIDTH; x++) { let lastLineY:number = Engine.BOARD_HEIGHT - 1; let lastLineTileState:TileState = this.board.getTileState(x, lastLineY); if ((lastLineTileState != TileState.EMPTY) && (this.board.getTileState(x - 1, lastLineY) == TileState.EMPTY)) { for (let y = 0; y < Engine.BOARD_HEIGHT; y++ ) { let tileState = this.board.getTileState(x, y); this.board.setTileState(x - 1, y, tileState); this.board.setTileState(x, y, TileState.EMPTY); movesCount++; } } for (let y = 0; y < Engine.BOARD_HEIGHT; y++) { } } if (movesCount == 0) { this.situation = Situation.GAME; } } ... } |
Тут тоже довольно просто, если вы писали код до этого. Мы просто проходим в цикле по ячейкам поля, проверяем, что под каждой ячейкой. Если там пусто, то очищаем ячейку, а ячейке ниже присваиваем шар с цветом, который был в текущей ячейке. Затем аналогичным образом сдвигаем шары влево.
Соберите проект. Уже должно быть вполне играбельно. Только условия победы и поражения нет. Если этот проект вызывает у вас сложности, то рекомендую вам немного почитать про алгоритмы и структуры данных. У меня на сайте пока таких статьей почти нет, но когда-нибудь, возможно добавлю.