моя контактная информация
Почтамезофия@protonmail.com
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
Согласно предыдущему дизайну, для запуска игры нам нужны необходимые элементы. Давайте для объяснения возьмем пример запуска Тетриса на консоли.
import { ConsoleCanvas, ConsoleController, ConsoleColorTheme, Color } from '@shushanfx/tetris-console';
import { Dimension, ColorFactory, Game } from '@shushanfx/tetris-core';
const theme = new ConsoleColorTheme();
const canvas = new ConsoleCanvas(theme);
const controller = new ConsoleController();
const dimension = new Dimension(10, 20);
const factory = new ColorFactory(dimension, [
Color.red,
Color.green,
Color.yellow,
Color.blue,
Color.magenta,
Color.cyan,
]);
const game = new Game({ dimension, canvas, factory, controller });
game.start();
Давайте проанализируем это построчно:
Внедрение пакетов делится на тетрис-ядро и тетрис-консоль. Это разделение пакетов: компоненты основного пакета размещаются в тетрис-консоли, а конкретная реализация — в тетрис-консоли.
Инициализация темы, холста и контроллера;
Инициализация фабрики и размеров;
Для инициализации игры используйте ранее инициализированные объекты Canvas, Factory, Canvas и Dimension;
Игра вызывает метод start.
Далее, давайте посмотрим, что делает start?
class Game {
start() {
const { status } = this;
if (status === GameStatus.RUNNING) {
return ;
}
if (status === GameStatus.OVER
|| status === GameStatus.STOP) {
this.stage.reset();
this.canvas.render();
} else if (status === GameStatus.READY) {
this.controller?.bind();
this.canvas.render();
}
this.status = GameStatus.RUNNING;
this.tickCount = 0;
this.canvas.update();
// @ts-ignore
this.tickTimer = setInterval(() => {
if (this.tickCount == 0) {
// 处理向下
this.stage.tick();
this.checkIsOver();
}
this.canvas.update();
this.tickCount++;
if (this.tickCount >= this.tickMaxCount) {
this.tickCount = 0;
}
}, this.speed);
}
}
Разберем построчно:
Получатьstatus
переменная,status
дляGame
Внутреннее представление состояния игры соответственно准备就绪(READY)
、游戏中(RUNNING)
、暂停(PAUSE)
、停止(STOP)
、游戏结束(OVER)
. Разница между остановкой и завершением игры заключается в том, что первая активно останавливает игру, а вторая запускает логику завершения игры и приводит к ее завершению.
Если игра продолжается, вернитесь сразу;
Если игра остановлена и игра окончена, тоStage
выполнить сброс иcanvas
Выполните общую перерисовку.
Если игра продолжается во время подготовки, это означает, что игра только что завершила инициализацию и еще не запускалась. Вызовите контроллер, чтобы привязать события и впервые нарисовать холст;
Установите состояние игры на 游戏中(RUNNING)
, внутреннее состояние TicketCount = 0;
передачаcanvas
Немедленно выполните частичное обновление. Основное обновление здесь заключается в том, что статус изменился, что привело к необходимости повторного отображения статуса игры;
Запустите таймер, время таймера проходит на этой скорости,speed
В будущем мы рассмотрим возможность согласования его с уровнем игры (пока не поддерживается).
Причина, по которой введен механизмickCount, заключается главным образом в обеспечении частоты обновления холста. Как правило, частота обновления экрана выше, чем скорость stage.tick. Если эти два параметра согласованы, интерфейс игры может быть не плавным.
Как видно из приведенного выше кода, основная логика игры такова:stage.tick
, его внутренняя реализация следующая:
class Stage {
tick(): void {
if (this.isOver || this.clearTimers.length > 0) {
return;
}
// 首次加载,current为空
if (!this.current) {
this.next = this.factory.randomBlock();
this.toTop(this.next);
this.current = this.factory.randomBlock();
this.toTop(this.current);
return ;
}
const isOver = this.current.points.some((point) => {
return !this.points[point.y][point.x].isEmpty;
});
if (isOver) {
this.isOver = true;
return;
}
const canMove = this.current.canMove('down', this.points);
if (canMove) {
this.current.move('down');
} else {
this.handleClear();
}
}
}
Сначала определите, закончилась ли игра или выполняется операция очистки.
еслиcurrent
Если пусто, это означает, что игра загружается впервые и инициализируется отдельно.current
иnext
。
Определите, достигает ли игра конечного состояния, то естьcurrent
иpoints
Есть перекрытие. Если есть совпадение, игра помечается как оконченная.
Определите, может ли текущий ток двигаться вниз. Если он может двигаться вниз, переместите его на одну позицию вниз. В противном случае проверьте, можно ли его устранить.
Далее мы рассмотрим, как обнаружить устранение, то естьhandleClear
реализация.
class Stage {
private handleClear() {
if (!this.current) {
return;
}
// 1. 复制新的points
const pointsClone: Point[][] = this.points.map((row) => row.map((point) => point.clone()));
this.current.points.forEach((point) => {
pointsClone[point.y][point.x] = point.clone();
});
// 2. 检查是否有消除的行
const cleanRows: number[] = [];
for(let i = 0; i < pointsClone.length; i ++) {
const row = pointsClone[i];
const isFull = row.every((point) => {
return !point.isEmpty
});
if (isFull) {
cleanRows.push(i);
}
}
// 3. 对行进行消除
if (cleanRows.length > 0) {
this.startClear(pointsClone, cleanRows, () => {
// 处理计算分数
this.score += this.getScore(cleanRows.length);
// 处理消除和下落
cleanRows.forEach((rowIndex) => {
for(let i = rowIndex; i >= 0; i--) {
if (i === 0) {
pointsClone[0] = Array.from({ length: this.dimension.xSize }, () => new Point(-1, -1));
} else {
pointsClone[i] = pointsClone[i - 1];
}
}
});
// 4. 扫尾工作,变量赋值
this.points = pointsClone;
this.current = this.next;
this.next = this.factory.randomBlock();
this.toTop(this.next);
});
} else {
// 4. 扫尾工作,变量赋值
this.points = pointsClone;
this.current = this.next;
this.next = this.factory.randomBlock();
this.toTop(this.next);
}
}
}
Как видно из приведенного выше кода, весь процесс разделен на четыре этапа:
Скопируйте новый клон точек, включая текущие и текущие точки.
Обнаруживать точки, клонировать построчно и отмечать, заполнена ли вся строка;
Удалить построчно в соответствии с отмеченным содержимым, созданным в 2. Обратите внимание, что операция удаления выполняется сверху вниз. При удалении строки сверху добавляется пустая строка.
Уборочные работы.Этот шаг необходимо выполнить независимо от того, выполняется ли операция очистки.this.points
, завершить одновременноcurrent
иnext
выключатель.
Что происходит с вращением блоков?
Все варианты поведения вращения запускаются вызовом метода game.rotate, включая события, определенные контроллером, внешние вызовы и т. д.;
Логика, реализованная в Game, следующая:
class Game {
rotate() {
this.stage.rotate();
this.canvas.update();
}
}
Смотреть дальшеStage
Реализация
class Stage {
rotate(): boolean {
if (!this.current) {
return false;
}
const canChange = this.current.canRotate(this.points);
if (canChange) {
this.current.rotate();
}
return false;
}
}
судите первымcurrent
Существует ли он, если он не существует, он будет возвращен напрямую;
передачаcurrent
изcanRotate
Метод проверки того, можно ли повернуть текущую позицию; если ее можно выбрать, вызовите метод вращения для поворота.
Пойдем дальше и посмотримBlock
изcanRotate
иrotate
метод.
class Block {
canRotate(points: Point[][]): boolean {
const centerIndex = this.getCenterIndex();
if (centerIndex === -1) {
return false;
}
const changes = this.getChanges();
if (changes.length === 0) {
return false;
}
const nextChange = changes[(this.currentChangeIndex + 1) % changes.length];
const newPoints = this.changePoints(this.points, this.points[centerIndex], nextChange);
const isValid = Block.isValid(newPoints, this.dimension);
if (isValid) {
return newPoints.every((point) => {
return points[point.y][point.x].isEmpty;
});
}
return isValid;
}
}
Давайте сначала посмотримcanRotate
реализация.
Получите centerIndex, centerIndex — это индекс центральной точки вращения. Каждый графический элемент различен, например IBlock, который определяется следующим образом:
class IBlock extends Block {
getCenterIndex(): number {
return 1;
}
}
То есть центральной точкой вращения является второй узел.нравиться口口口口
, вторая центральная точка口田口口
。
Кроме того, при разработке этого блока мы также учли, что некоторые блоки нельзя вращать, например OBlock, который нельзя выбрать.ноgetCenterIndex
возвращаться-1
。
Получите массив изменений. Массив определяется как угол текущего поворота. Длина массива представляет собой количество поворотов. Содержимое массива представляет собой угол этого поворота относительно последнего поворота.нравитьсяIBlock
определяется следующим образом:
class IBlock extends Block {
currentChangeIndex: number = -1;
getChanges(): number[] {
return [
Math.PI / 2,
0 - Math.PI / 2
];
}
}
То есть первый поворот — это начальное состояние Math.PI/2 (то есть 90 градусов), а второй поворот — это -Math.PI/2 первого поворота (то есть -90 градусов). следующее:
// 初始状态
// 口田口口
// 第一次旋转
// 口
// 田
// 口
// 口
// 第二次旋转
// 口田口口
PS: Обратите внимание, что ось координат идет слева направо и сверху вниз.
Для оценки ротации критериями оценки являются:
Таким образом, мы видим, что существуютisValid
иnewPoints.every
суждение.
посмотрим дальшеBlock.rotate
,следующее:
class Block {
rotate() {
const centerIndex = this.getCenterIndex();
if (centerIndex === -1) {
return false;
}
const changes = this.getChanges();
if (changes.length === 0) {
return false;
}
const nextChange = changes[(this.currentChangeIndex + 1) % changes.length];
const newPoints = this.changePoints(this.points, this.points[centerIndex], nextChange);
const isValid = Block.isValid(newPoints, this.dimension);
if (isValid) {
this.currentChangeIndex = (this.currentChangeIndex + 1) % changes.length;
this.points = newPoints;
}
return isValid;
}
}
Благодаря приведенному выше описанию,rotate
Логику легко понять.
ПолучатьcenterIndex
иchanges
,ВоляcurrentChangeIndex
Выполните циклическое приращение и наведите блок на новые координаты.
вcurrentChangeIndex
Начальное значение равно -1, что означает, что идет текущий поворот, а если оно больше или равно 0, это означает, что выбран индекс +1. (Пожалуйста, подумайте здесь внимательно, потому что индекс массива начинается с 0)
Движение означает перемещение Блока в четырех направлениях.Посмотрим его реализацию
class Game {
move(direction: Direction) {
this.stage.move(direction);
this.canvas.update();
}
}
Среди них Направление определяется следующим образом:
type Direction = 'up' | 'down' | 'left' | 'right';
Смотри дальшеStage
Реализация:
class Stage {
move(direction: Direction) {
if (!this.current) {
return false;
}
const canMove = this.current.canMove(direction, this.points);
if (canMove) {
this.current.move(direction);
}
return canMove;
}
}
Смотри дальшеcanMove
иmove
реализация.
class Block {
canMove(direction: Direction, points: Point[][]): boolean {
return this.points.every((point) => {
switch (direction) {
case 'up':
return point.y > 0 && points[point.y - 1][point.x].isEmpty;
case 'down':
return point.y < this.dimension.ySize - 1 && points[point.y + 1][point.x].isEmpty;
case 'left':
return point.x > 0 && points[point.y][point.x - 1].isEmpty;
case 'right':
return point.x < this.dimension.xSize - 1 && points[point.y][point.x + 1].isEmpty;
}
});
};
}
Коротко переведем это так:
Для перемещения вверх все точки оси Y должны быть больше 0 (то есть больше или равны 1), а точки после перемещения должны быть пустыми точками;
Сдвиг влево, все точки оси X должны быть больше 0 (то есть больше или равны 1), а точки после перемещения должны быть пустыми точками;
Сдвиг вправо, все точки оси X должны быть меньше длины оси координат X -1 (то есть меньше или равны xSize - 2), а точки после перемещения должны быть пустыми точками;
Для перемещения вниз все точки оси Y должны быть меньше длины оси координат Y -1 (то есть меньше или равны ySize - 2), а точки после перемещения должны быть пустыми точками.
После выполнения условий движения посмотрим наmove
реализация.
class Block {
move(direction: Direction): boolean {
switch (direction) {
case 'up':
this.points.forEach((point) => { point.y = point.y - 1})
break;
case 'down':
this.points.forEach((point) => { point.y = point.y + 1})
break;
case 'left':
this.points.forEach((point) => { point.x = point.x - 1})
break;
case 'right':
this.points.forEach((point) => { point.x = point.x + 1})
break;
}
return true;
}
}
Непосредственно измените значение координатной точки.
В этой главе описываются три важных режима игры: очистка, вращение и перемещение. Все трое сотрудничают друг с другом, чтобы завершить игру. В следующей главе мы поделимся рендерингом интерфейса и управлением работой игры.