Обмен технологиями

Игра в тетрис вручную (3) — дизайн основного модуля игры

2024-07-12

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

Игра в тетрис вручную — дизайн основного модуля игры

Начать игру

Согласно предыдущему дизайну, для запуска игры нам нужны необходимые элементы. Давайте для объяснения возьмем пример запуска Тетриса на консоли.

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

Давайте проанализируем это построчно:

  • Внедрение пакетов делится на тетрис-ядро и тетрис-консоль. Это разделение пакетов: компоненты основного пакета размещаются в тетрис-консоли, а конкретная реализация — в тетрис-консоли.

  • Инициализация темы, холста и контроллера;

  • Инициализация фабрики и размеров;

  • Для инициализации игры используйте ранее инициализированные объекты Canvas, Factory, Canvas и Dimension;

  • Игра вызывает метод start.

Далее, давайте посмотрим, что делает start?

Логика Game.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);
  }
}

  • 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

Разберем построчно:

  1. Получатьstatusпеременная,statusдляGameВнутреннее представление состояния игры соответственно准备就绪(READY)游戏中(RUNNING)暂停(PAUSE)停止(STOP)游戏结束(OVER) . Разница между остановкой и завершением игры заключается в том, что первая активно останавливает игру, а вторая запускает логику завершения игры и приводит к ее завершению.

  2. Если игра продолжается, вернитесь сразу;

  3. Если игра остановлена ​​и игра окончена, тоStageвыполнить сброс иcanvasВыполните общую перерисовку.

  4. Если игра продолжается во время подготовки, это означает, что игра только что завершила инициализацию и еще не запускалась. Вызовите контроллер, чтобы привязать события и впервые нарисовать холст;

  5. Установите состояние игры на 游戏中(RUNNING), внутреннее состояние TicketCount = 0;

  6. передачаcanvasНемедленно выполните частичное обновление. Основное обновление здесь заключается в том, что статус изменился, что привело к необходимости повторного отображения статуса игры;

  7. Запустите таймер, время таймера проходит на этой скорости,speedВ будущем мы рассмотрим возможность согласования его с уровнем игры (пока не поддерживается).

  • Если TicketCount == 0, запускает действие тика этапа и проверяет, заканчивается ли оно сразу после запуска;
  • Запустить операцию обновления холста
  • TicketCount увеличивается автоматически и сбрасывается, если >= TicketMaxCount удовлетворен;

Причина, по которой введен механизмickCount, заключается главным образом в обеспечении частоты обновления холста. Как правило, частота обновления экрана выше, чем скорость stage.tick. Если эти два параметра согласованы, интерфейс игры может быть не плавным.

Этап тика

Как видно из приведенного выше кода, основная логика игры такова:stage.tick, его внутренняя реализация следующая:

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
  • Сначала определите, закончилась ли игра или выполняется операция очистки.

  • еслиcurrentЕсли пусто, это означает, что игра загружается впервые и инициализируется отдельно.currentиnext

  • Определите, достигает ли игра конечного состояния, то естьcurrentиpoints Есть перекрытие. Если есть совпадение, игра помечается как оконченная.

  • Определите, может ли текущий ток двигаться вниз. Если он может двигаться вниз, переместите его на одну позицию вниз. В противном случае проверьте, можно ли его устранить.

Далее мы рассмотрим, как обнаружить устранение, то естьhandleClearреализация.

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

Как видно из приведенного выше кода, весь процесс разделен на четыре этапа:

  1. Скопируйте новый клон точек, включая текущие и текущие точки.

  2. Обнаруживать точки, клонировать построчно и отмечать, заполнена ли вся строка;

  3. Удалить построчно в соответствии с отмеченным содержимым, созданным в 2. Обратите внимание, что операция удаления выполняется сверху вниз. При удалении строки сверху добавляется пустая строка.

  4. Уборочные работы.Этот шаг необходимо выполнить независимо от того, выполняется ли операция очистки.this.points, завершить одновременноcurrentиnextвыключатель.

вращать

Что происходит с вращением блоков?

Все варианты поведения вращения запускаются вызовом метода game.rotate, включая события, определенные контроллером, внешние вызовы и т. д.;

Логика, реализованная в Game, следующая:

class Game {
  rotate() {
    this.stage.rotate();  
    this.canvas.update();
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Смотреть дальшеStageРеализация

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
  • судите первымcurrentСуществует ли он, если он не существует, он будет возвращен напрямую;

  • передачаcurrentизcanRotateМетод проверки того, можно ли повернуть текущую позицию; если ее можно выбрать, вызовите метод вращения для поворота.

Пойдем дальше и посмотримBlockизcanRotateиrotateметод.

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

Давайте сначала посмотримcanRotateреализация.

  • Получите centerIndex, centerIndex — это индекс центральной точки вращения. Каждый графический элемент различен, например IBlock, который определяется следующим образом:

    class IBlock extends Block {
      getCenterIndex(): number {
        return 1;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    То есть центральной точкой вращения является второй узел.нравиться口口口口, вторая центральная точка口田口口

    Кроме того, при разработке этого блока мы также учли, что некоторые блоки нельзя вращать, например OBlock, который нельзя выбрать.ноgetCenterIndexвозвращаться-1

  • Получите массив изменений. Массив определяется как угол текущего поворота. Длина массива представляет собой количество поворотов. Содержимое массива представляет собой угол этого поворота относительно последнего поворота.нравитьсяIBlockопределяется следующим образом:

    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

    То есть первый поворот — это начальное состояние Math.PI/2 (то есть 90 градусов), а второй поворот — это -Math.PI/2 первого поворота (то есть -90 градусов). следующее:

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

    PS: Обратите внимание, что ось координат идет слева направо и сверху вниз.

  • Для оценки ротации критериями оценки являются:

    1. Повернутые точки координат не могут выходить за границы всей игры;
    2. Повернутые точки координат не могут занимать точки заполненного квадрата.

    Таким образом, мы видим, что существуютisValidиnewPoints.everyсуждение.

посмотрим дальшеBlock.rotate,следующее:

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

Благодаря приведенному выше описанию,rotateЛогику легко понять.

  • ПолучатьcenterIndexиchanges,ВоляcurrentChangeIndexВыполните циклическое приращение и наведите блок на новые координаты.

  • вcurrentChangeIndex Начальное значение равно -1, что означает, что идет текущий поворот, а если оно больше или равно 0, это означает, что выбран индекс +1. (Пожалуйста, подумайте здесь внимательно, потому что индекс массива начинается с 0)

двигаться

Движение означает перемещение Блока в четырех направлениях.Посмотрим его реализацию

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

Среди них Направление определяется следующим образом:

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

Смотри дальшеStageРеализация:

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

Смотри дальшеcanMoveиmoveреализация.

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

Коротко переведем это так:

  • Для перемещения вверх все точки оси Y должны быть больше 0 (то есть больше или равны 1), а точки после перемещения должны быть пустыми точками;

  • Сдвиг влево, все точки оси X должны быть больше 0 (то есть больше или равны 1), а точки после перемещения должны быть пустыми точками;

  • Сдвиг вправо, все точки оси X должны быть меньше длины оси координат X -1 (то есть меньше или равны xSize - 2), а точки после перемещения должны быть пустыми точками;

  • Для перемещения вниз все точки оси Y должны быть меньше длины оси координат Y -1 (то есть меньше или равны ySize - 2), а точки после перемещения должны быть пустыми точками.

После выполнения условий движения посмотрим наmoveреализация.

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

Непосредственно измените значение координатной точки.

краткое содержание

В этой главе описываются три важных режима игры: очистка, вращение и перемещение. Все трое сотрудничают друг с другом, чтобы завершить игру. В следующей главе мы поделимся рендерингом интерфейса и управлением работой игры.