2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
Edellisen suunnitelman mukaan tarvitsemme pelin aloittamiseen tarvittavat elementit. Otetaan esimerkkinä Tetriksen käyttäminen konsolilla.
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();
Analysoidaan sitä rivi riviltä:
Pakettien käyttöönotto on jaettu tetris-core- ja tetris-konsoliin. Tämä on pakettien jako.
Teeman, kankaan ja ohjaimen alustus;
Tehtaan ja mittasuhteiden alustaminen;
Käytä pelin alustamiseen aiemmin alustettua kangas-, tehdas-, kangas- ja mittaobjekteja;
Peli kutsuu aloitusmenetelmää.
Katsotaan seuraavaksi, mitä aloitus tekee?
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);
}
}
Analysoidaan sitä rivi riviltä:
Saadastatus
muuttuva,status
vartenGame
Pelin tilan sisäinen esitys准备就绪(READY)
、游戏中(RUNNING)
、暂停(PAUSE)
、停止(STOP)
、游戏结束(OVER)
. Ero pysäytyksen ja pelin lopun välillä on, että edellinen pysäyttää pelin aktiivisesti, kun taas jälkimmäinen laukaisee pelin loppulogiikan ja saa pelin päättymään.
Jos peli on käynnissä, palaa suoraan;
Jos peli pysäytetään ja peli on ohi, niinStage
suorita nollaus jacanvas
Suorita yleinen uudelleenpiirtäminen.
Jos peli jatkuu valmistelun aikana, se tarkoittaa, että peli on juuri saanut alustuksen valmiiksi eikä ole koskaan alkanut. Soita ohjaimelle sitoaksesi tapahtumat ja piirtääksesi kankaan ensimmäistä kertaa;
Aseta pelin tila 游戏中(RUNNING)
, sisäinen tila tickCount = 0;
siirtääcanvas
Suorita osittainen päivitys välittömästi. Tärkein päivitys tässä on, että tila on muuttunut, minkä vuoksi pelin tila on renderöitävä uudelleen.
Käynnistä ajastin, ajastimen aika kuluu tämän.nopeus,speed
Tulevaisuudessa harkitsemme sen yhdistämistä pelitasoon (ei vielä tuettu).
Syy, miksi tickCount-mekanismi otetaan käyttöön, on pääasiassa kankaan päivitystiheyden varmistaminen. Yleensä näytön virkistystaajuus on korkeampi kuin stage.tick-nopeus.
Kuten yllä olevasta koodista voidaan nähdä, pelin ydinlogiikka onstage.tick
, sen sisäinen toteutus on seuraava:
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();
}
}
}
Määritä ensin, onko peli päättynyt vai onko käynnissä puhdistustoiminto.
joscurrent
Jos tyhjä, se tarkoittaa, että peli ladataan ensimmäistä kertaa ja alustetaan erikseen.current
janext
。
Selvitä, saavuttaako peli lopputilanteen, elicurrent
japoints
On päällekkäisyyttä. Jos päällekkäisyyksiä esiintyy, peli merkitään päättyneeksi.
Selvitä, voiko virta liikkua alaspäin. Muussa tapauksessa tarkista, voidaanko se poistaa.
Seuraavaksi tarkastellaan, kuinka havaita eliminaatio, elihandleClear
toteutumista.
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);
}
}
}
Kuten yllä olevasta koodista voidaan nähdä, koko prosessi on jaettu neljään vaiheeseen:
Kopioi uusi pointClone, joka sisältää nykyiset ja nykyiset pisteet.
Tunnista pisteet Kloonaa rivi riviltä ja merkitse, onko koko rivi täytetty;
Poista rivi riviltä kohdassa 2 luodun merkityn sisällön mukaan. Huomaa, että poistotoiminto suoritetaan ylhäältä alas Kun poistat rivin, tyhjä rivi lisätään ylhäältä.
Siivoustyöt.Tämä vaihe on suoritettava riippumatta siitä, suoritetaanko tyhjennystoiminto Assign pointsClone tothis.points
, suorita samaan aikaancurrent
janext
vaihtaa.
Mitä lohkojen pyörimisessä tapahtuu?
Kaikki kiertokäyttäytymiset käynnistyvät kutsumalla game.rotate-menetelmä, mukaan lukien ohjaimen määrittämät tapahtumat, ulkoiset kutsut jne.;
Peliin toteutettu logiikka on seuraava:
class Game {
rotate() {
this.stage.rotate();
this.canvas.update();
}
}
Katso seuraavaksiStage
toteutumista
class Stage {
rotate(): boolean {
if (!this.current) {
return false;
}
const canChange = this.current.canRotate(this.points);
if (canChange) {
this.current.rotate();
}
return false;
}
}
tuomari ensincurrent
Onko se olemassa, jos sitä ei ole, se palautetaan suoraan;
siirtääcurrent
/canRotate
Menetelmä tarkistaaksesi, voidaanko nykyistä sijaintia kiertää, jos se voidaan valita, kutsu kiertomenetelmä.
Mennään pidemmälle ja katsotaanBlock
/canRotate
jarotate
menetelmä.
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;
}
}
Katsotaanpa ensincanRotate
toteutumista.
Hanki centerIndex, centerIndex on kiertoliikkeen keskipisteen indeksi. Jokainen grafiikka on erilainen, kuten IBlock, joka määritellään seuraavasti:
class IBlock extends Block {
getCenterIndex(): number {
return 1;
}
}
Toisin sanoen kiertokeskipiste on toinen solmu.Kuten口口口口
, toinen keskipiste口田口口
。
Lisäksi tätä lohkoa suunnitellessamme huomioimme myös sen, että joitain lohkoja ei voi kiertää, kuten OBlock, jota ei voi valita.muttagetCenterIndex
palata-1
。
Hae muutokset -taulukko Matriisin pituus edustaa kiertojen määrää.KutenIBlock
määritellään seuraavasti:
class IBlock extends Block {
currentChangeIndex: number = -1;
getChanges(): number[] {
return [
Math.PI / 2,
0 - Math.PI / 2
];
}
}
Toisin sanoen ensimmäinen kierto on alkutila Math.PI / 2 (eli 90 astetta), ja toinen kierto on -Math.PI / 2 ensimmäisestä kierrosta (eli -90 astetta). seuraavasti:
// 初始状态
// 口田口口
// 第一次旋转
// 口
// 田
// 口
// 口
// 第二次旋转
// 口田口口
PS: Huomaa tässä, että koordinaattiakseli on vasemmalta oikealle ja ylhäältä alas.
Rotaatioarvioinnissa arviointikriteerit ovat:
Siksi näemme, että niitä onisValid
janewPoints.every
tuomio.
Katsotaan seuraavaksiBlock.rotate
,seuraavasti:
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;
}
}
Yllä olevan kuvauksen kauttarotate
Logiikka on helppo ymmärtää.
SaadacenterIndex
jachanges
,TahtoacurrentChangeIndex
Suorita syklinen lisäys ja osoita lohko uusiin koordinaatteihin.
sisääncurrentChangeIndex
Alkuarvo on -1, mikä tarkoittaa, että nykyinen kierto on käynnissä, ja jos se on suurempi tai yhtä suuri kuin 0, se tarkoittaa, että indeksi + 1 on valittuna. (Ajattele tarkkaan, koska taulukon indeksi alkaa nollasta)
Liike tarkoittaa lohkon liikuttamista neljään suuntaan.Katsotaanpa sen toteutusta
class Game {
move(direction: Direction) {
this.stage.move(direction);
this.canvas.update();
}
}
Niiden joukossa Suunta on määritelty seuraavasti:
type Direction = 'up' | 'down' | 'left' | 'right';
Katso lisääStage
Toteutus:
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;
}
}
Katso lisääcanMove
jamove
toteutumista.
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;
}
});
};
}
Käännetään se lyhyesti seuraavasti:
Ylöspäin siirtymiseksi kaikkien y-akselin pisteiden on oltava suurempia kuin 0 (eli suurempia tai yhtä suuria kuin 1), ja liikkeen jälkeisten pisteiden on oltava tyhjiä pisteitä;
Siirrä vasemmalle, kaikkien x-akselin pisteiden on oltava suurempia kuin 0 (eli suurempia tai yhtä suuria kuin 1), ja siirron jälkeisten pisteiden on oltava tyhjiä pisteitä;
Siirrä oikealle, kaikkien x-akselin pisteiden on oltava pienempiä kuin x-koordinaatin akselin pituus -1 (eli pienempi tai yhtä suuri kuin xSize - 2), ja liikkeen jälkeisten pisteiden on oltava tyhjiä pisteitä;
Alaspäin siirtymistä varten kaikkien y-akselin pisteiden on oltava pienempiä kuin y-koordinaatin akselin pituus -1 (eli pienempiä tai yhtä suuria kuin ySize - 2), ja liikkeen jälkeisten pisteiden on oltava tyhjiä pisteitä.
Kun liikeehdot on täytetty, katsotaanmove
toteutumista.
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;
}
}
Muokkaa suoraan koordinaattipisteen arvoa.
Tässä luvussa kuvataan kolme tärkeää pelin käyttäytymistä: tyhjennys, pyöriminen ja liikkuminen. He kolme tekevät yhteistyötä keskenään saadakseen pelin loppuun. Seuraavassa luvussa jaamme pelin käyttöliittymän renderöinnin ja toiminnan ohjauksen.