minhas informações de contato
Correspondência[email protected]
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
De acordo com o design anterior, precisamos dos elementos do jogo necessários para iniciar o jogo. Vamos dar o exemplo de rodar o Tetris no console para explicar.
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();
Vamos analisar linha por linha:
A introdução dos pacotes é dividida em tetris-core e tetris-console. Esta é a divisão dos pacotes. Os componentes do pacote principal são colocados no tetris-core, e a implementação específica é colocada no tetris-console.
Inicialização de tema, canvas e controlador;
Inicialização de fábrica e dimensão;
Para inicialização do jogo, use os objetos canvas, factory, canvas e dimension previamente inicializados;
O jogo chama o método start.
A seguir, vamos dar uma olhada no que o start faz?
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);
}
}
Vamos analisar linha por linha:
Obtivermosstatus
variável,status
paraGame
A representação interna do estado do jogo, respectivamente准备就绪(READY)
、游戏中(RUNNING)
、暂停(PAUSE)
、停止(STOP)
、游戏结束(OVER)
. A diferença entre parar e terminar o jogo é que o primeiro interrompe ativamente o jogo, enquanto o último aciona a lógica de final do jogo e faz com que o jogo termine.
Se o jogo estiver em andamento, retorne diretamente;
Se o jogo for interrompido e o jogo terminar, entãoStage
faça um reset ecanvas
Execute um redesenho geral.
Se o jogo continuar durante a preparação, significa que o jogo acabou de concluir a inicialização e nunca foi iniciado. Chame o controlador para vincular eventos e desenhar a tela pela primeira vez;
Defina o estado do jogo para 游戏中(RUNNING)
, estado interno tickCount = 0;
transferircanvas
Execute uma atualização parcial imediatamente. A principal atualização aqui é que o status mudou, fazendo com que o status do jogo precise ser renderizado novamente;
Inicie o cronômetro, o tempo do cronômetro passa assim.velocidade,speed
No futuro, consideraremos combiná-lo com o nível do jogo (ainda não suportado).
A razão pela qual o mecanismo tickCount foi introduzido é principalmente para garantir a frequência de atualização da tela. Geralmente, a taxa de atualização da tela é maior que a velocidade do stage.tick. Se os dois forem consistentes, a interface do jogo pode não ser suave.
Como pode ser visto no código acima, a lógica central do jogo éstage.tick
, sua implementação interna é a seguinte:
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();
}
}
}
Primeiro determine se o jogo terminou ou se uma operação de limpeza está sendo executada.
securrent
Se estiver vazio, significa que o jogo foi carregado pela primeira vez e inicializado separadamente.current
enext
。
Determine se o jogo atinge a condição final, ou sejacurrent
epoints
Há sobreposição. Se houver uma sobreposição, o jogo será marcado como encerrado.
Determine se a corrente atual pode se mover para baixo. Se ela puder se mover para baixo, mova-a um espaço para baixo. Caso contrário, verifique se ela pode ser eliminada.
A seguir, veremos como detectar a eliminação, ou sejahandleClear
realização.
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 pode ser visto no código acima, todo o processo é dividido em quatro etapas:
Copie um novo pointsClone, incluindo os pontos atuais e atuais.
Detectar pontosClone linha por linha e marque se a linha inteira está preenchida;
Exclua linha por linha de acordo com o conteúdo marcado gerado em 2. Observe que a operação de exclusão é executada de cima para baixo. Ao excluir uma linha, uma linha em branco é adicionada de cima.
Trabalho de limpeza.Esta etapa precisa ser executada independentemente de a operação de limpeza ser executada. Assign pointsClone to.this.points
, complete ao mesmo tempocurrent
enext
trocar.
O que está acontecendo com a rotação dos blocos?
Todos os comportamentos de rotação são acionados pela chamada do método game.rotate, incluindo eventos definidos pelo controlador, chamadas externas, etc.;
A lógica implementada no Game é a seguinte:
class Game {
rotate() {
this.stage.rotate();
this.canvas.update();
}
}
Assista a seguirStage
realização
class Stage {
rotate(): boolean {
if (!this.current) {
return false;
}
const canChange = this.current.canRotate(this.points);
if (canChange) {
this.current.rotate();
}
return false;
}
}
julgar primeirocurrent
Se existir, se não existir, será devolvido diretamente;
transferircurrent
decanRotate
Método para verificar se a posição atual pode ser girada, se puder ser selecionada, chame o método de rotação para girar;
Vamos mais longe e vejaBlock
decanRotate
erotate
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;
}
}
Vamos olhar primeirocanRotate
realização.
Obtenha centerIndex, centerIndex é o índice do ponto central de rotação. Cada gráfico é diferente, como o IBlock, que é definido da seguinte forma:
class IBlock extends Block {
getCenterIndex(): number {
return 1;
}
}
Ou seja, o ponto central de rotação é o segundo nó.como口口口口
, o segundo ponto central口田口口
。
Além disso, ao projetar este bloco, também consideramos que alguns blocos não podem ser girados, como o OBlock, que não pode ser selecionado.masgetCenterIndex
retornar-1
。
Obtenha a matriz de alterações. A matriz é definida como o ângulo da rotação atual. O comprimento da matriz representa o número de rotações.comoIBlock
é definido da seguinte forma:
class IBlock extends Block {
currentChangeIndex: number = -1;
getChanges(): number[] {
return [
Math.PI / 2,
0 - Math.PI / 2
];
}
}
Ou seja, a primeira rotação é o estado inicial Math.PI/2 (ou seja, 90 graus), e a segunda rotação é -Math.PI/2 da primeira rotação (ou seja, -90 graus). do seguinte modo:
// 初始状态
// 口田口口
// 第一次旋转
// 口
// 田
// 口
// 口
// 第二次旋转
// 口田口口
PS: Observe aqui que o eixo de coordenadas é da esquerda para a direita e de cima para baixo.
Para julgamento de rotação, os critérios de julgamento são:
Portanto, vemos que existemisValid
enewPoints.every
julgamento.
Vamos ver a seguirBlock.rotate
,do seguinte modo:
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;
}
}
Através da descrição acima,rotate
A lógica é fácil de entender.
ObtivermoscenterIndex
echanges
,VaicurrentChangeIndex
Execute um incremento cíclico e aponte o Bloco para as novas coordenadas.
emcurrentChangeIndex
O valor inicial é -1, o que significa que a rotação atual está em andamento, e se for maior ou igual a 0, significa que o índice + 1 está selecionado. (Por favor, pense com cuidado aqui, porque o índice do array começa em 0)
Movimento significa mover o Bloco em quatro direções.Vamos ver sua implementação
class Game {
move(direction: Direction) {
this.stage.move(direction);
this.canvas.update();
}
}
Entre eles, a Direção é definida da seguinte forma:
type Direction = 'up' | 'down' | 'left' | 'right';
Olhar mais longeStage
Implementação 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;
}
}
Olhar mais longecanMove
emove
realização.
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;
}
});
};
}
Vamos traduzi-lo brevemente da seguinte forma:
Para subir, todos os pontos do eixo y devem ser maiores que 0 (ou seja, maiores ou iguais a 1) e os pontos após o movimento devem ser pontos vazios;
Desloque para a esquerda, todos os pontos do eixo x devem ser maiores que 0 (ou seja, maiores ou iguais a 1) e os pontos após o movimento devem ser pontos vazios;
Deslocando para a direita, todos os pontos do eixo x devem ser menores que o comprimento do eixo da coordenada x -1 (ou seja, menores ou iguais a xSize - 2) e os pontos após o movimento devem ser pontos vazios;
Para mover para baixo, todos os pontos do eixo y devem ser menores que o comprimento do eixo da coordenada y -1 (ou seja, menores ou iguais a ySize - 2) e os pontos após o movimento devem ser pontos vazios.
Depois de atender às condições de movimento, vejamosmove
realização.
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 diretamente o valor do ponto de coordenadas.
Este capítulo descreve três comportamentos importantes do jogo: limpar, girar e mover. Os três cooperam entre si para completar o jogo. No próximo capítulo compartilharemos a renderização da interface e o controle de operação do jogo.