le mie informazioni di contatto
Posta[email protected]
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
Secondo il progetto precedente, per avviare il gioco abbiamo bisogno degli elementi necessari. Per spiegarlo, prendiamo l'esempio dell'esecuzione di Tetris sulla console.
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();
Analizziamolo riga per riga:
L'introduzione dei pacchetti è divisa in tetris-core e tetris-console. Questa è la divisione dei pacchetti. I componenti del pacchetto principale sono inseriti in tetris-core e l'implementazione specifica è inserita in tetris-console.
Inizializzazione di tema, tela e controller;
Inizializzazione di fabbrica e dimensione;
Per l'inizializzazione del gioco, utilizzare gli oggetti canvas, factory, canvas e dimensione precedentemente inizializzati;
Il gioco chiama il metodo di avvio.
Quindi, diamo un'occhiata a cosa fa l'inizio?
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);
}
}
Analizziamolo riga per riga:
Ottenerestatus
variabile,status
perGame
La rappresentazione interna dello stato del gioco, rispettivamente准备就绪(READY)
、游戏中(RUNNING)
、暂停(PAUSE)
、停止(STOP)
、游戏结束(OVER)
. La differenza tra stop e fine del gioco è che il primo ferma attivamente il gioco, mentre il secondo innesca la logica finale del gioco e ne provoca la fine.
Se il gioco è in corso, torna direttamente;
Se il gioco viene interrotto e il gioco finisce, alloraStage
eseguire un ripristino ecanvas
Eseguire un ridisegno generale.
Se il gioco continua durante la preparazione, significa che il gioco ha appena completato l'inizializzazione e non è mai iniziato. Chiama il controller per associare gli eventi e disegnare la tela per la prima volta;
Imposta lo stato del gioco su 游戏中(RUNNING)
, stato interno tickCount = 0;
trasferimentocanvas
Esegui immediatamente un aggiornamento parziale L'aggiornamento principale qui è che lo stato è cambiato, rendendo necessario il rendering dello stato del gioco;
Avvia il timer, il tempo del timer passa a questa velocità,speed
In futuro valuteremo l'abbinamento con il livello del gioco (non ancora supportato).
Il motivo per cui viene introdotto il meccanismo tickCount è principalmente quello di garantire la frequenza di aggiornamento della tela. In generale, la frequenza di aggiornamento dello schermo è superiore alla velocità di stage.tick. Se i due sono coerenti, l'interfaccia di gioco potrebbe non essere fluida.
Come si può vedere dal codice sopra, la logica fondamentale del gioco èstage.tick
, la sua implementazione interna è la seguente:
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();
}
}
}
Per prima cosa determina se il gioco è finito o se è in corso un'operazione di pulizia.
Securrent
Se vuoto significa che il gioco viene caricato per la prima volta e inizializzato separatamente.current
Enext
。
Determina se il gioco raggiunge la condizione finale, cioècurrent
Epoints
C'è sovrapposizione. Se c'è una sovrapposizione, la partita viene considerata finita.
Determina se la corrente attuale può spostarsi verso il basso. Se può spostarsi verso il basso, spostala di uno spazio verso il basso. Altrimenti controlla se può essere eliminata.
Successivamente esamineremo come rilevare l'eliminazionehandleClear
realizzazione.
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);
}
}
}
Come si può vedere dal codice sopra, l'intero processo è diviso in quattro passaggi:
Copia un nuovo clone di punti, inclusi i punti attuali e attuali.
Rileva punti, clona riga per riga e contrassegna se l'intera riga è riempita;
Elimina riga per riga in base al contenuto contrassegnato generato in 2. Tieni presente che l'operazione di eliminazione viene eseguita dall'alto verso il basso Quando si elimina una riga, viene aggiunta una riga vuota dall'alto.
Lavoro di pulizia.Questo passaggio deve essere eseguito indipendentemente dal fatto che venga eseguita l'operazione di cancellazione. Assegna puntiClone athis.points
, completare allo stesso tempocurrent
Enext
interruttore.
Cosa succede con la rotazione dei blocchi?
Tutti i comportamenti di rotazione vengono attivati chiamando il metodo game.rotate, inclusi gli eventi definiti dal controller, le chiamate esterne, ecc.;
La logica implementata nel gioco è la seguente:
class Game {
rotate() {
this.stage.rotate();
this.canvas.update();
}
}
Guarda dopoStage
realizzazione
class Stage {
rotate(): boolean {
if (!this.current) {
return false;
}
const canChange = this.current.canRotate(this.points);
if (canChange) {
this.current.rotate();
}
return false;
}
}
giudicare primacurrent
Se esiste, se non esiste verrà restituito direttamente;
trasferimentocurrent
DicanRotate
Metodo per verificare se la posizione corrente può essere ruotata; se può essere selezionata, chiamare il metodo di rotazione per ruotare.
Andiamo oltre e vediamoBlock
DicanRotate
Erotate
metodo.
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;
}
}
Diamo un'occhiata primacanRotate
realizzazione.
Ottieni centerIndex, centerIndex è l'indice del punto centrale di rotazione. Ogni elemento grafico è diverso, come IBlock, che è definito come segue:
class IBlock extends Block {
getCenterIndex(): number {
return 1;
}
}
Cioè, il punto centrale di rotazione è il secondo nodo.Piace口口口口
, il secondo punto centrale口田口口
。
Inoltre, durante la progettazione di questo blocco, abbiamo considerato anche che alcuni blocchi non possono essere ruotati, come OBlock, che non può essere selezionato.MagetCenterIndex
ritorno-1
。
Ottieni l'array delle modifiche. L'array è definito come l'angolo della rotazione corrente. La lunghezza dell'array rappresenta il numero di rotazioni. Il contenuto dell'array rappresenta l'angolo di questa rotazione rispetto all'ultima rotazione.PiaceIBlock
è definito come segue:
class IBlock extends Block {
currentChangeIndex: number = -1;
getChanges(): number[] {
return [
Math.PI / 2,
0 - Math.PI / 2
];
}
}
Cioè, la prima rotazione è lo stato iniziale Math.PI / 2 (ovvero 90 gradi) e la seconda rotazione è -Math.PI / 2 della prima rotazione (ovvero -90 gradi). come segue:
// 初始状态
// 口田口口
// 第一次旋转
// 口
// 田
// 口
// 口
// 第二次旋转
// 口田口口
PS: Tieni presente che l'asse delle coordinate va da sinistra a destra e dall'alto verso il basso.
Per il giudizio a rotazione i criteri di giudizio sono:
Quindi vediamo che ci sonoisValid
EnewPoints.every
giudizio.
Vediamo dopoBlock.rotate
,come segue:
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;
}
}
Attraverso la descrizione di cui sopra,rotate
La logica è facile da capire.
OttenerecenterIndex
Echanges
,VolerecurrentChangeIndex
Eseguire un incremento ciclico e puntare il Blocco sulle nuove coordinate.
IncurrentChangeIndex
Il valore iniziale è -1, significa che è in corso la rotazione corrente, e se è maggiore o uguale a 0 significa che è selezionato indice + 1. (Per favore pensa attentamente qui, perché l'indice dell'array inizia da 0)
Movimento significa muovere il Blocco in quattro direzioni.Vediamo la sua realizzazione
class Game {
move(direction: Direction) {
this.stage.move(direction);
this.canvas.update();
}
}
Tra questi, la Direzione è definita come segue:
type Direction = 'up' | 'down' | 'left' | 'right';
Guarda oltreStage
Implementazione di:
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;
}
}
Guarda oltrecanMove
Emove
realizzazione.
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;
}
});
};
}
Traduciamolo brevemente così:
Per spostarsi verso l'alto, tutti i punti dell'asse y devono essere maggiori di 0 (ovvero maggiori o uguali a 1) e i punti dopo lo spostamento devono essere punti vuoti;
Spostandosi a sinistra, tutti i punti dell'asse x devono essere maggiori di 0 (ovvero maggiori o uguali a 1) e i punti dopo lo spostamento devono essere punti vuoti;
Spostandosi a destra, tutti i punti dell'asse x devono essere inferiori alla lunghezza dell'asse delle coordinate x -1 (ovvero, inferiori o uguali a xSize - 2) e i punti dopo lo spostamento devono essere punti vuoti;
Per spostarsi verso il basso, tutti i punti dell'asse y devono essere inferiori alla lunghezza dell'asse delle coordinate y -1 (ovvero, inferiori o uguali a ySize - 2) e i punti dopo il movimento devono essere punti vuoti.
Dopo aver soddisfatto le condizioni di movimento, diamo un'occhiatamove
realizzazione.
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;
}
}
Modificare direttamente il valore del punto delle coordinate.
Questo capitolo descrive tre importanti comportamenti del gioco: pulizia, rotazione e movimento. I tre collaborano tra loro per completare il gioco. Nel prossimo capitolo condivideremo il rendering dell'interfaccia e il controllo operativo del gioco.