Назад | Учебник TypeScript | Вперёд
Наша игра почти готова. Осталось добавить три вещи:
- Первоначально фон рисуется белым цветом, но при сдвиге шариков он закрашивается чёрным.
- Нужно добавить подсчёт очков.
- По окончании игры нужно добавить перезапуск.
Первая проблема решается очень легко. Добавим ещё одну стадию в перечисление Situation, во время которого закрасим весь экран чёрным цветом.
1 2 3 4 5 6 |
enum Situation { CLEAR_SCREEN, GAME, ANIMATION, SHOW_SCORE } |
Очистка canvas в “Engine.ts”:
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 |
private onPaint():void { let ctx:CanvasRenderingContext2D = <CanvasRenderingContext2D>this.canvas .getContext("2d"); switch (this.situation) { case Situation.CLEAR_SCREEN: this.clearScreen(ctx); break; case Situation.GAME: this.paintGame(ctx); break; } } private clearScreen(ctx:CanvasRenderingContext2D):void { ctx.fillStyle = Engine.EMPTY_COLOR; ctx.fillRect(0, 0, Engine.BOARD_WIDTH * Engine.TILE_WIDTH, Engine.BOARD_HEIGHT * Engine.TILE_HEIGHT); } private paintGame(ctx:CanvasRenderingContext2D):void { for (let x:number = 0; x < Engine.BOARD_WIDTH; x++) { for (let y:number = 0; y < Engine.BOARD_HEIGHT; y++) { switch (this.board.getTileState(x, y)) { case TileState.EMPTY: this.drawRect(ctx, x, y, Engine.EMPTY_COLOR); break; case TileState.RED: this.drawCircle(ctx, x, y, "#ba4747"); break; case TileState.GREEN: this.drawCircle(ctx, x, y, "#356637"); break; case TileState.BLUE: this.drawCircle(ctx, x, y, "#678beb"); break; case TileState.YELLOW: this.drawCircle(ctx, x, y, "#bebb70"); break; } } } } |
Как видите, мы просто добавили проверку текущей ситуации и блок очистки поля для situation == Situation.CLEAR_SCREEN. Также мы вынесли код цвета для игрового поля с пустой ячейкой в константу Engine.EMPTY_COLOR:
1 2 3 4 5 |
class Engine { ... public static EMPTY_COLOR:string = "#404040"; ... } |
При инициализации движка в Engine мы теперь инициализируем situation значением Situation.CLEAR_SCREEN для очистки игрового поля перед началом игры:
1 2 3 4 5 6 7 8 |
class Engine { ... public constructor() { this.situation = Situation.CLEAR_SCREEN; ... } ... } |
Нам также нужно модифицировать логику в onStep, чтобы после очистки поля начать игру:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Engine { ... private onStep():void { if (this.situation == Situation.ANIMATION) { this.processGravity(); } this.onPaint(); if (this.situation == Situation.CLEAR_SCREEN) { this.situation = Situation.GAME; } let engine = this; setTimeout(function () { engine.onStep(); }, Engine.TICK_MILLISECONDS); if (console) console.log("endOnStep"); } ... } |
Отлично. Первый пункт из списка готов. Теперь нам нужно реализовать подсчёт очков при схлопывании шариков одинакового цвета. Мы будем давать по одному очку за каждый схлопнувшийся шарик и умножать на количество схлопнувшихся одновременно шариков.
Метод removeSameBalls возвращает количество удалённых шариков, так что нам достаточно легко подсчитать количество очков по описанной выше формуле.
Для начала добавим переменную, в которой будем хранить количество набранных игроком очков:
1 2 3 4 5 |
class Engine { ... private score:number; ... } |
Инициализируем его в конструкторе:
1 2 3 4 5 6 |
class Engine { ... public constructor() { this.score = 0; ... } |
Собственно сам подсчёт в методе onClick:
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 { ... 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); if (this.board.getTileState(tileX, tileY) != TileState.EMPTY) { let removedCount = this.removeSameBalls(tileX, tileY); if (removedCount > 1) { this.situation = Situation.ANIMATION; this.score += removedCount * removedCount; } } break; } } ... } |
Отобразим количество очков в нижней части canvas:
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 |
class Engine { ... private onPaint():void { let ctx:CanvasRenderingContext2D = <CanvasRenderingContext2D>this.canvas .getContext("2d"); switch (this.situation) { case Situation.CLEAR_SCREEN: this.clearScreen(ctx); break; case Situation.GAME: this.paintGame(ctx); break; } this.paintStatus(ctx); } private paintStatus(ctx:CanvasRenderingContext2D):void { ctx.fillStyle = Engine.EMPTY_COLOR; ctx.fillRect(0, Engine.TILE_HEIGHT * Engine.BOARD_HEIGHT, Engine.TILE_WIDTH * Engine.BOARD_WIDTH, Engine.TILE_WIDTH * Engine.BOARD_WIDTH + Engine.TILE_HEIGHT - 3) ctx.font="20px Courier New"; ctx.strokeStyle="#0000ff"; ctx.fillStyle="#ffff00"; let scoreString:string = "00000000" + this.score; scoreString = scoreString.slice( scoreString.length - 8, scoreString.length); ctx.fillText("score: " + scoreString, 0, Engine.TILE_HEIGHT * Engine.BOARD_HEIGHT + Engine.TILE_HEIGHT - 3); } ... } |
Теперь нужно добавить проверку, остались ли на игровом поле шарики, которые можно схлопнуть, и если таких нет, то перезапускать игру. Но перед этим отобразим игроку его результат в Situation.END_GAME:
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 |
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); if (this.board.getTileState(tileX, tileY) != TileState.EMPTY) { let removedCount = this.removeSameBalls(tileX, tileY); if (removedCount > 1) { this.situation = Situation.ANIMATION; this.score += removedCount * removedCount; } if (!this.checkEndGame()) { this.situation = Situation.END_GAME; } } break; case Situation.END_GAME: this.situation = Situation.CLEAR_SCREEN; break; } } private checkEndGame():boolean { for (let n:number = 0; n < Engine.BOARD_WIDTH - 1; n++) { for (let m:number = 0; m < Engine.BOARD_HEIGHT - 1; m++) { let tileState:TileState = this.board.getTileState(n, m); if ((tileState != TileState.EMPTY) && ((tileState == this.board.getTileState(n + 1, m)) || (tileState == this.board.getTileState(n, m + 1)))) { return true; } } } return false; } ... } |
В Situation добавим новый элемент перечисления Situation.END_GAME:
1 2 3 4 5 6 7 |
enum Situation { CLEAR_SCREEN, GAME, ANIMATION, SHOW_SCORE, END_GAME } |
По окончании игры нужно отобразить пользователю его результат:
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 |
class Engine { ... private onPaint():void { let ctx:CanvasRenderingContext2D = <CanvasRenderingContext2D>this.canvas .getContext("2d"); switch (this.situation) { case Situation.CLEAR_SCREEN: this.clearScreen(ctx); break; case Situation.GAME: this.paintGame(ctx); break; case Situation.END_GAME: this.paintEndGame(ctx); break; } this.paintStatus(ctx); } private paintEndGame(ctx:CanvasRenderingContext2D):void { ctx.fillStyle="#404040"; ctx.fillRect(10, Engine.TILE_HEIGHT * (Engine.BOARD_HEIGHT / 2 - 1), Engine.TILE_WIDTH * Engine.BOARD_WIDTH - 20, Engine.TILE_HEIGHT * 2); ctx.fillStyle="#ffff00"; ctx.font="40px Courier New"; var gameOverString ="GAME OVER"; ctx.fillText(gameOverString, Engine.TILE_WIDTH * (Engine.BOARD_WIDTH/ 2) - ctx.measureText(gameOverString) .width / 2, Engine.TILE_HEIGHT * (Engine.BOARD_HEIGHT/ 2)); var yourScore = "score: " + this.score; ctx.fillText(yourScore, Engine.TILE_WIDTH * (Engine.BOARD_WIDTH/ 2) - ctx.measureText(yourScore).width / 2, Engine.TILE_HEIGHT * (Engine.BOARD_HEIGHT / 2 + 1)); } … } |
Для перезапуска игры выделим отдельный метод initGame из конструктора:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class Engine { ... private situation:Situation = Situation.CLEAR_SCREEN; ... public constructor() { this.canvas = <HTMLCanvasElement>document .getElementById("tscolorballscanvas"); let engine = this; setTimeout(function() {engine.onStep();}, Engine.TICK_MILLISECONDS); this.canvas.addEventListener("mousedown", function(event) { engine.onClick(event) }); } private initGame():void { this.score = 0; this.situation = Situation.CLEAR_SCREEN; this.board = new Board(Engine.BOARD_WIDTH, Engine.BOARD_HEIGHT); this.situation = Situation.GAME; } ... } |
Сам метод initGame будем вызывать на situation == Situation.CLEAR_SCREEN (заодно перенесём в initGame присвоение нового значения situation):
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 { ... private initGame():void { this.score = 0; this.situation = Situation.CLEAR_SCREEN; this.board = new Board(Engine.BOARD_WIDTH, Engine.BOARD_HEIGHT); this.situation = Situation.GAME; } private onStep():void { if (this.situation == Situation.ANIMATION) { this.processGravity(); } this.onPaint(); if (this.situation == Situation.CLEAR_SCREEN) { this.initGame(); } let engine = this; setTimeout(function () { engine.onStep(); }, Engine.TICK_MILLISECONDS); if (console) console.log("endOnStep"); } ... } |
Отлично игра готова. Небольшой штрих: будем сохранять в localStorage лучший результат. Для этого в Engine добавим новое поле highScore:
1 2 3 4 5 |
class Engine { ... private highScore:number; ... } |
Считываем значение наилучшего результата при инициализации 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 |
class Engine { ... public static HIGHSCORE_KEY:string = "ru.urvanov.tscolorballs.highScore"; ... public constructor() { this.canvas = <HTMLCanvasElement>document .getElementById("tscolorballscanvas"); let engine = this; setTimeout(function() {engine.onStep();}, Engine.TICK_MILLISECONDS); this.canvas.addEventListener("mousedown", function(event) { engine.onClick(event) }); this.readHighScoreFromStorage(); } private readHighScoreFromStorage():void { try { if (window.localStorage) { this.highScore = window.localStorage[Engine.HIGHSCORE_KEY]; } if (this.highScore === undefined) { this.highScore = 0; } } catch (ex) { this.highScore = 0;; } } private saveHighScoreToStorage():void { try { window.localStorage[Engine.HIGHSCORE_KEY] = this.highScore; } catch (ex) { console.error(ex); } } ... } |
В обработчике onClick сохраняем результат как лучший, если он лучше предыдущего лучшего:
1 2 3 4 5 6 7 |
if (!this.checkEndGame()) { this.situation = Situation.END_GAME; if (this.score > this.highScore) { this.highScore = this.score; this.saveHighScoreToStorage(); } } |
Отображаем лучший результат в paintStatus:
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 |
class Engine { ... private paintStatus(ctx:CanvasRenderingContext2D):void { ctx.fillStyle = Engine.EMPTY_COLOR; ctx.fillRect(0, Engine.TILE_HEIGHT * Engine.BOARD_HEIGHT, Engine.TILE_WIDTH * Engine.BOARD_WIDTH, Engine.TILE_WIDTH * Engine.BOARD_WIDTH + Engine.TILE_HEIGHT - 3) ctx.font="20px Courier New"; ctx.strokeStyle="#0000ff"; ctx.fillStyle="#ffff00"; let scoreString:string = "00000000" + this.score; scoreString = scoreString.slice( scoreString.length - 8, scoreString.length); ctx.fillText("score: " + scoreString, 0, Engine.TILE_HEIGHT * Engine.BOARD_HEIGHT + Engine.TILE_HEIGHT - 3); scoreString = "00000000" + this.highScore; scoreString= scoreString.slice( scoreString.length - 8, scoreString.length); ctx.fillText("high : " + scoreString, 0, Engine.TILE_HEIGHT * Engine.BOARD_HEIGHT + Engine.TILE_HEIGHT *1.5 - 3); } ... } |
Скомпилированный результат я выложил в раздел проектов. Игра доступна по ссылке. Исходники на GitHub.
Назад | Учебник TypeScript | Вперёд
Спасибо за учебник.
В checkEndGame баг забрался, не проверяет нижний и правый ряд board.
Проверю, спасибо. Только, к сожалению, не прямо на этой неделе.
Я проверил, но бага не нашёл. У меня всё нормально отрабатывает. Там, я думаю, может быть недопонимание из-за того, что мы проходимся по верхним левым строчкам и пытаемся найти совпадения по цвету с координатами x + 1, y + 1. В итоге в основном цикле последнюю строчку и столбец проходить не нужно.