Teknologian jakaminen

Tetriksen pelaaminen käsin (3) - pelin ydinmoduulisuunnittelu

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Tetriksen pelaaminen käsin - pelin ydinmoduulisuunnittelu

Aloita peli

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();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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?

Pelin aloituslogiikka


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);
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

Analysoidaan sitä rivi riviltä:

  1. Saadastatusmuuttuva,statusvartenGamePelin 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.

  2. Jos peli on käynnissä, palaa suoraan;

  3. Jos peli pysäytetään ja peli on ohi, niinStagesuorita nollaus jacanvasSuorita yleinen uudelleenpiirtäminen.

  4. 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;

  5. Aseta pelin tila 游戏中(RUNNING), sisäinen tila tickCount = 0;

  6. siirtääcanvasSuorita 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.

  7. Käynnistä ajastin, ajastimen aika kuluu tämän.nopeus,speedTulevaisuudessa harkitsemme sen yhdistämistä pelitasoon (ei vielä tuettu).

  • Jos tickCount == 0, käynnistä vaiheen rastitoiminto ja tarkista, päättyykö se välittömästi liipaisun jälkeen;
  • Käynnistä kankaan päivitystoiminto
  • tickCount kasvaa automaattisesti ja nollataan, jos >= tickMaxCount täyttyy;

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.

Stage rasti

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();
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • Määritä ensin, onko peli päättynyt vai onko käynnissä puhdistustoiminto.

  • joscurrentJos tyhjä, se tarkoittaa, että peli ladataan ensimmäistä kertaa ja alustetaan erikseen.currentjanext

  • Selvitä, saavuttaako peli lopputilanteen, elicurrentjapoints 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, elihandleCleartoteutumista.

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);
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

Kuten yllä olevasta koodista voidaan nähdä, koko prosessi on jaettu neljään vaiheeseen:

  1. Kopioi uusi pointClone, joka sisältää nykyiset ja nykyiset pisteet.

  2. Tunnista pisteet Kloonaa rivi riviltä ja merkitse, onko koko rivi täytetty;

  3. 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ä.

  4. Siivoustyöt.Tämä vaihe on suoritettava riippumatta siitä, suoritetaanko tyhjennystoiminto Assign pointsClone tothis.points, suorita samaan aikaancurrentjanextvaihtaa.

kiertää

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();
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Katso seuraavaksiStagetoteutumista

class Stage {
  rotate(): boolean {
    if (!this.current) {
      return false;
    }
    const canChange = this.current.canRotate(this.points);
    if (canChange) {
      this.current.rotate();
    }
    return false;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • tuomari ensincurrentOnko se olemassa, jos sitä ei ole, se palautetaan suoraan;

  • siirtääcurrent/canRotateMenetelmä tarkistaaksesi, voidaanko nykyistä sijaintia kiertää, jos se voidaan valita, kutsu kiertomenetelmä.

Mennään pidemmälle ja katsotaanBlock/canRotatejarotatemenetelmä.

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;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

Katsotaanpa ensincanRotatetoteutumista.

  • 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;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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.muttagetCenterIndexpalata-1

  • Hae muutokset -taulukko Matriisin pituus edustaa kiertojen määrää.KutenIBlockmääritellään seuraavasti:

    class IBlock extends Block {
      currentChangeIndex: number = -1;
      getChanges(): number[] {
        return [
          Math.PI / 2,
          0 - Math.PI / 2
        ];
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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:

    // 初始状态
    // 口田口口
    
    // 第一次旋转
    //  口
    //  田
    //  口
    //  口
    
    // 第二次旋转
    // 口田口口
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    PS: Huomaa tässä, että koordinaattiakseli on vasemmalta oikealle ja ylhäältä alas.

  • Rotaatioarvioinnissa arviointikriteerit ovat:

    1. Kierretyt koordinaattipisteet eivät saa ylittää koko pelin rajoja;
    2. Kierretyt koordinaattipisteet eivät voi täyttää täytetyn neliön pisteitä.

    Siksi näemme, että niitä onisValidjanewPoints.everytuomio.

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;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

Yllä olevan kuvauksen kauttarotateLogiikka on helppo ymmärtää.

  • SaadacenterIndexjachanges,TahtoacurrentChangeIndexSuorita 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)

liikkua

Liike tarkoittaa lohkon liikuttamista neljään suuntaan.Katsotaanpa sen toteutusta

class Game {
  move(direction: Direction) {
    this.stage.move(direction);
    this.canvas.update();
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Niiden joukossa Suunta on määritelty seuraavasti:

type Direction = 'up' | 'down' | 'left' | 'right'
  • 1

Katso lisääStageToteutus:

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;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

Katso lisääcanMovejamovetoteutumista.

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;
      }
    });
  };
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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, katsotaanmovetoteutumista.

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;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

Muokkaa suoraan koordinaattipisteen arvoa.

yhteenveto

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.