2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
Selon la conception précédente, nous avons besoin des éléments nécessaires du jeu pour démarrer le jeu. Prenons l'exemple de l'exécution de Tetris sur la console pour expliquer.
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();
Analysons-le ligne par ligne :
L'introduction des packages est divisée en tetris-core et tetris-console. Il s'agit de la division des packages. Les composants du package principal sont placés dans tetris-core et l'implémentation spécifique est placée dans tetris-console.
Initialisation du thème, du canevas et du contrôleur ;
Initialisation de l'usine et des dimensions ;
Pour l'initialisation du jeu, utilisez les objets canevas, usine, canevas et dimension précédemment initialisés ;
Le jeu appelle la méthode start.
Voyons ensuite ce que fait 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);
}
}
Analysons-le ligne par ligne :
Obtenirstatus
variable,status
pourGame
La représentation interne de l'état du jeu, respectivement准备就绪(READY)
、游戏中(RUNNING)
、暂停(PAUSE)
、停止(STOP)
、游戏结束(OVER)
. La différence entre l'arrêt et la fin du jeu est que le premier arrête activement le jeu, tandis que le second déclenche la logique de fin du jeu et provoque la fin du jeu.
Si le jeu est en cours, revenez directement ;
Si le jeu est arrêté et terminé, alorsStage
effectuer une réinitialisation etcanvas
Effectuez un redessinage global.
Si le jeu continue pendant la préparation, cela signifie que le jeu vient de terminer son initialisation et n'a jamais démarré. Appelez le contrôleur pour lier les événements et dessiner le canevas pour la première fois ;
Définissez l'état du jeu sur 游戏中(RUNNING)
, état interne tickCount = 0 ;
transfertcanvas
Effectuez immédiatement une mise à jour partielle. La mise à jour principale ici est que le statut a changé, ce qui nécessite un nouveau rendu du statut du jeu ;
Démarrez la minuterie, le temps de la minuterie dépasse cette vitesse,speed
À l'avenir, nous envisagerons de le faire correspondre au niveau du jeu (pas encore pris en charge).
La raison pour laquelle le mécanisme tickCount est introduit est principalement pour garantir la fréquence de mise à jour du canevas. Généralement, le taux de rafraîchissement de l'écran est supérieur à la vitesse de stage.tick. Si les deux sont cohérents, l'interface de jeu peut ne pas être fluide.
Comme le montre le code ci-dessus, la logique de base du jeu eststage.tick
, sa mise en œuvre interne est la suivante :
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();
}
}
}
Déterminez d’abord si le jeu est terminé ou si une opération de nettoyage est en cours.
sicurrent
S'il est vide, cela signifie que le jeu est chargé pour la première fois et initialisé séparément.current
etnext
。
Déterminer si le jeu atteint la condition de fin, c'est-à-direcurrent
etpoints
Il y a un chevauchement. En cas de chevauchement, la partie est marquée comme terminée.
Déterminez si le courant actuel peut descendre. S'il peut descendre, déplacez-le d'un espace vers le bas. Sinon, vérifiez s'il peut être éliminé.
Nous verrons ensuite comment détecter l'élimination, c'est-à-direhandleClear
la concrétisation.
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);
}
}
}
Comme le montre le code ci-dessus, l’ensemble du processus est divisé en quatre étapes :
Copiez un nouveau pointsClone, y compris les points actuels et actuels.
Détecter les pointsCloner ligne par ligne et marquer si la ligne entière est remplie ;
Supprimer ligne par ligne en fonction du contenu marqué généré en 2. Notez que l'opération de suppression s'effectue de haut en bas. Lors de la suppression d'une ligne, une ligne vide est ajoutée par le haut.
Travaux de nettoyage.Cette étape doit être effectuée indépendamment du fait que l'opération de compensation soit effectuée ou non.this.points
, terminer en même tempscurrent
etnext
changer.
Que se passe-t-il avec la rotation des blocs ?
Tous les comportements de rotation sont déclenchés par l'appel de la méthode game.rotate, y compris les événements définis par le contrôleur, les appels externes, etc. ;
La logique implémentée dans Game est la suivante :
class Game {
rotate() {
this.stage.rotate();
this.canvas.update();
}
}
Regarder ensuiteStage
La concrétisation
class Stage {
rotate(): boolean {
if (!this.current) {
return false;
}
const canChange = this.current.canRotate(this.points);
if (canChange) {
this.current.rotate();
}
return false;
}
}
juger d'abordcurrent
Qu'il existe, s'il n'existe pas, il sera restitué directement ;
transfertcurrent
decanRotate
Méthode pour vérifier si la position actuelle peut être pivotée ; si elle peut être sélectionnée, appelez la méthode de rotation pour faire pivoter.
Allons plus loin et voyonsBlock
decanRotate
etrotate
méthode.
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;
}
}
Regardons d'abordcanRotate
la concrétisation.
Obtenez centerIndex, centerIndex est l'index du point central de rotation. Chaque graphique est différent, comme IBlock, qui est défini comme suit :
class IBlock extends Block {
getCenterIndex(): number {
return 1;
}
}
Autrement dit, le point central de rotation est le deuxième nœud.comme口口口口
, le deuxième point central口田口口
。
De plus, lors de la conception de ce bloc, nous avons également considéré que certains blocs ne peuvent pas pivoter, comme OBlock, qui ne peut pas être sélectionné.maisgetCenterIndex
retour-1
。
Récupère le tableau des modifications. Le tableau est défini comme l'angle de la rotation actuelle. La longueur du tableau représente le nombre de rotations. Le contenu du tableau représente l'angle de cette rotation par rapport à la dernière rotation.commeIBlock
est défini comme suit :
class IBlock extends Block {
currentChangeIndex: number = -1;
getChanges(): number[] {
return [
Math.PI / 2,
0 - Math.PI / 2
];
}
}
C'est-à-dire que la première rotation est l'état initial Math.PI/2 (c'est-à-dire 90 degrés), et la deuxième rotation est -Math.PI/2 de la première rotation (c'est-à-dire -90 degrés). comme suit:
// 初始状态
// 口田口口
// 第一次旋转
// 口
// 田
// 口
// 口
// 第二次旋转
// 口田口口
PS : Veuillez noter ici que l'axe des coordonnées est de gauche à droite et de haut en bas.
Pour le jugement de rotation, les critères de jugement sont :
On voit donc qu'il y aisValid
etnewPoints.every
jugement.
Voyons ensuiteBlock.rotate
,comme suit:
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;
}
}
Grâce à la description ci-dessus,rotate
La logique est facile à comprendre.
ObtenircenterIndex
etchanges
,VolontécurrentChangeIndex
Effectuez un incrément cyclique et pointez le bloc vers les nouvelles coordonnées.
danscurrentChangeIndex
La valeur initiale est -1, ce qui signifie que la rotation en cours est en cours, et si elle est supérieure ou égale à 0, cela signifie que l'index + 1 est sélectionné. (Veuillez bien réfléchir ici, car l'index du tableau commence à 0)
Le mouvement signifie déplacer le bloc dans quatre directions.Voyons sa mise en œuvre
class Game {
move(direction: Direction) {
this.stage.move(direction);
this.canvas.update();
}
}
Parmi eux, la Direction est définie comme suit :
type Direction = 'up' | 'down' | 'left' | 'right';
Regardez plus loinStage
Implémentation 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;
}
}
Regardez plus loincanMove
etmove
la concrétisation.
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;
}
});
};
}
Traduisons-le brièvement comme suit :
Pour monter, tous les points de l'axe y doivent être supérieurs à 0 (c'est-à-dire supérieurs ou égaux à 1) et les points après le déplacement doivent être des points vides ;
Décalage vers la gauche, tous les points de l'axe x doivent être supérieurs à 0 (c'est-à-dire supérieurs ou égaux à 1) et les points après le déplacement doivent être des points vides ;
Décalage vers la droite, tous les points de l'axe x doivent être inférieurs à la longueur de l'axe des coordonnées x -1 (c'est-à-dire inférieur ou égal à xSize - 2) et les points après le déplacement doivent être des points vides ;
Pour descendre, tous les points de l'axe y doivent être inférieurs à la longueur de l'axe des coordonnées y -1 (c'est-à-dire inférieur ou égal à ySize - 2) et les points après le mouvement doivent être des points vides.
Après avoir satisfait aux conditions de mouvement, regardonsmove
la concrétisation.
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;
}
}
Modifiez directement la valeur du point de coordonnées.
Ce chapitre décrit trois comportements importants du jeu : dégager, faire pivoter et se déplacer. Les trois coopèrent pour terminer le jeu. Dans le prochain chapitre, nous partagerons le rendu de l'interface et le contrôle du fonctionnement du jeu.