informasi kontak saya
Surat[email protected]
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
Berdasarkan desain sebelumnya, kita memerlukan elemen permainan yang diperlukan untuk memulai permainan. Mari kita ambil contoh menjalankan Tetris di konsol untuk menjelaskannya.
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();
Mari kita analisa baris demi baris:
Pengenalan paket dibagi menjadi tetris-core dan tetris-console. Ini adalah pembagian paket. Komponen paket inti ditempatkan di tetris-core, dan implementasi spesifik ditempatkan di konsol tetris.
Inisialisasi tema, kanvas, dan pengontrol;
Inisialisasi pabrik dan dimensi;
Untuk inisialisasi game, gunakan objek kanvas, pabrik, kanvas, dan dimensi yang telah diinisialisasi sebelumnya;
Permainan memanggil metode awal.
Selanjutnya, mari kita lihat apa yang dilakukan 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);
}
}
Mari kita analisa baris demi baris:
Memperolehstatus
variabel,status
untukGame
Representasi internal dari status permainan, masing-masing准备就绪(READY)
、游戏中(RUNNING)
、暂停(PAUSE)
、停止(STOP)
、游戏结束(OVER)
. Perbedaan antara stop dan game end adalah yang pertama secara aktif menghentikan permainan, sedangkan yang kedua memicu logika akhir permainan dan menyebabkan permainan berakhir.
Jika permainan sedang berlangsung, langsung kembali;
Jika permainan dihentikan dan permainan berakhir, makaStage
melakukan reset dancanvas
Lakukan penggambaran ulang secara keseluruhan.
Jika permainan dilanjutkan saat persiapan, berarti permainan baru saja menyelesaikan inisialisasi dan belum pernah dimulai. Panggil pengontrol untuk mengikat acara dan menggambar kanvas untuk pertama kalinya;
Atur status permainan ke 游戏中(RUNNING)
, keadaan internal tickCount = 0;
transfercanvas
Segera lakukan update sebagian. Update utama disini adalah status telah berubah sehingga menyebabkan status game perlu dirender ulang;
Mulai pengatur waktu, waktu pengatur waktu melewati ini. Kecepatan,speed
Di masa mendatang, kami akan mempertimbangkan untuk mencocokkannya dengan level game (belum didukung).
Alasan mengapa mekanisme tickCount diperkenalkan terutama untuk memastikan frekuensi pembaruan kanvas. Umumnya, kecepatan refresh layar lebih tinggi daripada kecepatan stage.tick. Jika keduanya konsisten, antarmuka game mungkin tidak mulus.
Seperti yang dapat dilihat dari kode di atas, logika inti dari permainan ini adalahstage.tick
, implementasi internalnya adalah sebagai berikut:
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();
}
}
}
Pertama-tama tentukan apakah permainan telah selesai atau operasi pembersihan sedang dilakukan.
jikacurrent
Jika kosong, berarti game dimuat pertama kali dan diinisialisasi secara terpisah.current
Dannext
。
Tentukan apakah permainan mencapai kondisi akhir, yaitucurrent
Danpoints
Ada tumpang tindih. Jika terjadi tumpang tindih, permainan ditandai selesai.
Tentukan apakah arus saat ini dapat bergerak ke bawah. Jika dapat bergerak ke bawah, pindahkan satu spasi ke bawah. Jika tidak, periksa apakah dapat dihilangkan.
Selanjutnya kita melihat cara mendeteksi eliminasi, yaituhandleClear
realisasi.
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);
}
}
}
Seperti yang dapat dilihat dari kode di atas, keseluruhan proses dibagi menjadi empat langkah:
Salin poin baruKlon, termasuk poin saat ini dan saat ini.
Deteksi titikKloning baris demi baris, dan tandai jika seluruh baris terisi;
Hapus baris demi baris sesuai dengan konten yang ditandai yang dihasilkan di 2. Perhatikan bahwa operasi penghapusan dilakukan dari atas ke bawah. Saat menghapus sebuah baris, baris kosong ditambahkan dari atas.
Membersihkan pekerjaan.Langkah ini perlu dilakukan terlepas dari apakah operasi kliring dilakukanthis.points
, selesaikan secara bersamaancurrent
Dannext
mengalihkan.
Apa yang terjadi dengan perputaran balok?
Semua perilaku rotasi dipicu dengan memanggil metode game.rotate, termasuk peristiwa yang ditentukan oleh pengontrol, panggilan eksternal, dll.;
Logika yang diterapkan dalam Game adalah sebagai berikut:
class Game {
rotate() {
this.stage.rotate();
this.canvas.update();
}
}
Tonton selanjutnyaStage
realisasi
class Stage {
rotate(): boolean {
if (!this.current) {
return false;
}
const canChange = this.current.canRotate(this.points);
if (canChange) {
this.current.rotate();
}
return false;
}
}
menilai terlebih dahulucurrent
Apakah ada, jika tidak ada akan langsung dikembalikan;
transfercurrent
daricanRotate
Metode untuk memeriksa apakah posisi saat ini dapat diputar; jika dapat dipilih, panggil metode rotasi untuk memutar.
Mari kita melangkah lebih jauh dan melihatBlock
daricanRotate
Danrotate
metode.
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;
}
}
Mari kita lihat dulucanRotate
realisasi.
Dapatkan centerIndex, centerIndex adalah indeks titik pusat rotasi. Setiap grafik berbeda, seperti IBlock, yang didefinisikan sebagai berikut:
class IBlock extends Block {
getCenterIndex(): number {
return 1;
}
}
Artinya, titik pusat rotasi adalah node kedua.menyukai口口口口
, titik pusat kedua口田口口
。
Selain itu, saat mendesain blok ini, kami juga mempertimbangkan beberapa blok yang tidak dapat diputar, seperti OBlock yang tidak dapat dipilih.TetapigetCenterIndex
kembali-1
。
Dapatkan perubahan array. Array didefinisikan sebagai sudut rotasi saat ini. Panjang array mewakili jumlah rotasi.menyukaiIBlock
didefinisikan sebagai berikut:
class IBlock extends Block {
currentChangeIndex: number = -1;
getChanges(): number[] {
return [
Math.PI / 2,
0 - Math.PI / 2
];
}
}
Artinya, putaran pertama adalah keadaan awal Math.PI / 2 (yaitu 90 derajat), dan putaran kedua adalah -Math.PI / 2 dari putaran pertama (yaitu -90 derajat). sebagai berikut:
// 初始状态
// 口田口口
// 第一次旋转
// 口
// 田
// 口
// 口
// 第二次旋转
// 口田口口
PS: Perlu diperhatikan disini sumbu koordinatnya dari kiri ke kanan dan atas ke bawah.
Untuk penilaian rotasi, kriteria penilaiannya adalah:
Oleh karena itu, kami melihat adaisValid
DannewPoints.every
pertimbangan.
Mari kita lihat selanjutnyaBlock.rotate
,sebagai berikut:
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;
}
}
Melalui uraian di atas,rotate
Logikanya mudah dimengerti.
MemperolehcenterIndex
Danchanges
,AkancurrentChangeIndex
Lakukan kenaikan siklik dan arahkan Blok ke koordinat baru.
di dalamcurrentChangeIndex
Nilai awalnya -1 berarti perputaran arus sedang berlangsung, dan jika lebih besar atau sama dengan 0 berarti dipilih indeks + 1. (Harap pikirkan baik-baik di sini, karena indeks array dimulai dari 0)
Gerakan berarti menggerakkan Balok ke empat arah.Mari kita lihat implementasinya
class Game {
move(direction: Direction) {
this.stage.move(direction);
this.canvas.update();
}
}
Diantaranya, Arah didefinisikan sebagai berikut:
type Direction = 'up' | 'down' | 'left' | 'right';
Lihat lebih jauhStage
Implementasi dari:
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;
}
}
Lihat lebih jauhcanMove
Danmove
realisasi.
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;
}
});
};
}
Mari kita terjemahkan secara singkat sebagai berikut:
Untuk berpindah ke atas, semua titik sumbu y harus lebih besar dari 0 (yaitu, lebih besar dari atau sama dengan 1), dan titik setelah perpindahan harus berupa titik kosong;
Bergeser ke kiri, semua titik sumbu x harus lebih besar dari 0 (yaitu lebih besar atau sama dengan 1), dan titik setelah perpindahan harus berupa titik kosong;
Geser ke kanan, semua titik sumbu x harus kurang dari panjang sumbu koordinat x -1 (yaitu kurang dari atau sama dengan xSize - 2), dan titik setelah perpindahan harus berupa titik kosong;
Untuk bergerak ke bawah, semua titik sumbu y harus lebih kecil dari panjang sumbu koordinat y -1 (yaitu, kurang dari atau sama dengan Ukuran y - 2), dan titik setelah pergerakan harus berupa titik kosong.
Setelah memenuhi kondisi pergerakan, mari kita lihatmove
realisasi.
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;
}
}
Ubah langsung nilai titik koordinat.
Bab ini menjelaskan tiga perilaku penting permainan: membersihkan, memutar, dan memindahkan. Ketiganya bekerja sama satu sama lain untuk menyelesaikan permainan. Di bab berikutnya kami akan membagikan rendering antarmuka dan kontrol operasi game.