Mi informacion de contacto
Correo[email protected]
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
Según el diseño anterior, necesitamos los elementos necesarios del juego para iniciarlo. Tomemos el ejemplo de ejecutar Tetris en la consola para explicarlo.
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();
Analicémoslo línea por línea:
La introducción de paquetes se divide en tetris-core y tetris-console. Esta es la división de paquetes. Los componentes del paquete principal se colocan en tetris-core y la implementación específica se coloca en tetris-console.
Inicialización de tema, lienzo y controlador;
Inicialización de fábrica y dimensión;
Para la inicialización del juego, utilice los objetos de lienzo, fábrica, lienzo y dimensión previamente inicializados;
El juego llama al método de inicio.
A continuación, echemos un vistazo a lo que significa el inicio.
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);
}
}
Analicémoslo línea por línea:
Obtenerstatus
variable,status
paraGame
La representación interna del estado del juego, respectivamente.准备就绪(READY)
、游戏中(RUNNING)
、暂停(PAUSE)
、停止(STOP)
、游戏结束(OVER)
. La diferencia entre detener y finalizar el juego es que el primero detiene activamente el juego, mientras que el segundo activa la lógica de finalización del juego y hace que el juego termine.
Si el juego está en curso, regresa directamente;
Si el juego se detiene y termina, entoncesStage
realizar un reinicio ycanvas
Realice un rediseño general.
Si el juego continúa mientras se prepara, significa que acaba de completar la inicialización y nunca comenzó. Llame al controlador para vincular eventos y dibujar el lienzo por primera vez;
Establece el estado del juego en 游戏中(RUNNING)
, estado interno tickCount = 0;
transferircanvas
Realice una actualización parcial de inmediato. La actualización principal aquí es que el estado ha cambiado, lo que hace que sea necesario volver a representar el estado del juego;
Inicie el cronómetro, el tiempo del cronómetro pasa a esta velocidad,speed
En el futuro, consideraremos combinarlo con el nivel del juego (aún no es compatible).
La razón por la que se introduce el mecanismo tickCount es principalmente para garantizar la frecuencia de actualización del lienzo. Generalmente, la frecuencia de actualización de la pantalla es mayor que la velocidad de stage.tick. Si las dos son consistentes, es posible que la interfaz del juego no sea fluida.
Como se puede ver en el código anterior, la lógica central del juego esstage.tick
, su implementación interna es la siguiente:
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();
}
}
}
Primero determine si el juego ha terminado o si se está realizando una operación de limpieza.
sicurrent
Si está vacío, significa que el juego se carga por primera vez y se inicializa por separado.current
ynext
。
Determinar si el juego alcanza la condición final, es decircurrent
ypoints
Hay superposición. Si hay una superposición, el juego se marca como terminado.
Determine si la corriente actual puede moverse hacia abajo. Si puede moverse hacia abajo, muévala un espacio hacia abajo. De lo contrario, verifique si se puede eliminar.
A continuación veremos cómo detectar la eliminación, es decirhandleClear
realización.
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);
}
}
}
Como puede verse en el código anterior, todo el proceso se divide en cuatro pasos:
Copie un nuevo clon de puntos, incluidos los puntos actuales y actuales.
Detectar puntosClonar línea por línea y marcar si toda la línea está llena;
Eliminar línea por línea según el contenido marcado generado en 2. Tenga en cuenta que la operación de eliminación se realiza de arriba a abajo. Al eliminar una fila, se agrega una fila en blanco desde la parte superior.
Trabajo de limpieza.Este paso debe realizarse independientemente de si se realiza la operación de limpieza. Asignar puntosClonar a.this.points
, completo al mismo tiempocurrent
ynext
cambiar.
¿Qué pasa con la rotación de los bloques?
Todos los comportamientos de rotación se activan llamando al método game.rotate, incluidos los eventos definidos por el controlador, llamadas externas, etc.;
La lógica implementada en Game es la siguiente:
class Game {
rotate() {
this.stage.rotate();
this.canvas.update();
}
}
Ver siguienteStage
realización
class Stage {
rotate(): boolean {
if (!this.current) {
return false;
}
const canChange = this.current.canRotate(this.points);
if (canChange) {
this.current.rotate();
}
return false;
}
}
juez primerocurrent
Si existe, si no existe, se devolverá directamente;
transferircurrent
decanRotate
Método para verificar si la posición actual se puede rotar; si se puede seleccionar, llame al método de rotación para rotar.
Vayamos más lejos y veamosBlock
decanRotate
yrotate
método.
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;
}
}
miremos primerocanRotate
realización.
Obtenga centerIndex, centerIndex es el índice del punto central de rotación. Cada gráfico es diferente, como por ejemplo IBlock, que se define de la siguiente manera:
class IBlock extends Block {
getCenterIndex(): number {
return 1;
}
}
Es decir, el punto central de rotación es el segundo nodo.como口口口口
, el segundo punto central口田口口
。
Además, al diseñar este bloque, también consideramos que algunos bloques no se pueden rotar, como OBlock, que no se puede seleccionar.perogetCenterIndex
devolver-1
。
Obtenga la matriz de cambios. La matriz se define como el ángulo de la rotación actual. La longitud de la matriz representa el número de rotaciones. El contenido de la matriz representa el ángulo de esta rotación en relación con la última rotación.comoIBlock
se define de la siguiente manera:
class IBlock extends Block {
currentChangeIndex: number = -1;
getChanges(): number[] {
return [
Math.PI / 2,
0 - Math.PI / 2
];
}
}
Es decir, la primera rotación es el estado inicial Math.PI/2 (es decir, 90 grados), y la segunda rotación es -Math.PI/2 de la primera rotación (es decir, -90 grados). como sigue:
// 初始状态
// 口田口口
// 第一次旋转
// 口
// 田
// 口
// 口
// 第二次旋转
// 口田口口
PD: tenga en cuenta aquí que el eje de coordenadas es de izquierda a derecha y de arriba a abajo.
Para el juicio de rotación, los criterios de juicio son:
Por lo tanto, vemos que hayisValid
ynewPoints.every
juicio.
veamos a continuaciónBlock.rotate
,como sigue:
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;
}
}
A través de la descripción anterior,rotate
La lógica es fácil de entender.
ObtenercenterIndex
ychanges
,VoluntadcurrentChangeIndex
Realice un incremento cíclico y apunte el bloque a las nuevas coordenadas.
encurrentChangeIndex
El valor inicial es -1, lo que significa que la rotación actual está en progreso, y si es mayor o igual a 0, significa que se selecciona el índice + 1. (Piense detenidamente aquí, porque el índice de la matriz comienza desde 0)
Movimiento significa mover el Bloque en cuatro direcciones.Veamos su implementación.
class Game {
move(direction: Direction) {
this.stage.move(direction);
this.canvas.update();
}
}
Entre ellos, Dirección se define de la siguiente manera:
type Direction = 'up' | 'down' | 'left' | 'right';
Mira más alláStage
Implementación de:
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;
}
}
Mira más allácanMove
ymove
realización.
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;
}
});
};
}
Traducámoslo brevemente de la siguiente manera:
Para subir, todos los puntos del eje y deben ser mayores que 0 (es decir, mayores o iguales a 1) y los puntos después del movimiento deben ser puntos vacíos;
Desplazarse hacia la izquierda, todos los puntos del eje x deben ser mayores que 0 (es decir, mayores o iguales a 1) y los puntos después del movimiento deben ser puntos vacíos;
Desplazarse hacia la derecha, todos los puntos del eje x deben ser menores que la longitud del eje de coordenadas x -1 (es decir, menor o igual que xSize - 2), y los puntos después del movimiento deben ser puntos vacíos;
Para moverse hacia abajo, todos los puntos del eje y deben ser menores que la longitud del eje de coordenadas y -1 (es decir, menor o igual que ySize - 2), y los puntos después del movimiento deben ser puntos vacíos.
Después de cumplir las condiciones de movimiento, veamosmove
realización.
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;
}
}
Modifique directamente el valor del punto de coordenadas.
Este capítulo describe tres comportamientos importantes del juego: limpiar, rotar y moverse. Los tres cooperan entre sí para completar el juego. En el próximo capítulo compartiremos la representación de la interfaz y el control de operación del juego.