내 연락처 정보
우편메소피아@프로톤메일.com
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();
한 줄씩 분석해 보겠습니다.
패키지 도입은 tetris-core와 tetris-console로 나누어집니다. 이는 핵심 패키지의 구성 요소가 tetris-core에 배치되고, 구체적인 구현이 tetris-console에 배치됩니다.
테마, 캔버스, 컨트롤러 초기화
팩토리 및 차원 초기화
게임 초기화에는 앞서 초기화한 캔버스, 팩토리, 캔버스, 차원 개체를 사용합니다.
게임은 시작 메소드를 호출합니다.
다음으로 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);
}
}
한 줄씩 분석해 보겠습니다.
얻다status
변하기 쉬운,status
~을 위한Game
게임 상태의 내부 표현准备就绪(READY)
、游戏中(RUNNING)
、暂停(PAUSE)
、停止(STOP)
、游戏结束(OVER)
. 중지와 게임 종료의 차이점은 전자는 게임을 적극적으로 중지하는 반면, 후자는 게임의 종료 논리를 트리거하여 게임을 종료시킨다는 점입니다.
게임이 진행 중이면 바로 돌아가세요.
게임이 중단되고 게임이 종료되면Stage
재설정을 수행하고canvas
전체 다시 그리기를 수행합니다.
준비하는 동안에도 게임이 계속된다면, 게임이 초기화만 완료되어 아직 시작되지 않았다는 의미입니다. 컨트롤러를 호출하여 이벤트를 바인딩하고 처음으로 캔버스를 그립니다.
게임 상태를 다음으로 설정합니다. 游戏中(RUNNING)
, 내부 상태 TickCount = 0;
옮기다canvas
즉시 부분 업데이트를 수행하십시오. 여기서 주요 업데이트는 상태가 변경되어 게임 상태를 다시 렌더링해야 한다는 것입니다.
타이머를 시작하면 타이머 시간이 이 속도를 초과합니다.speed
향후에는 게임 레벨에 맞춰서 고려해볼 예정입니다(아직 지원되지 않음).
TickCount 메커니즘이 도입되는 이유는 주로 캔버스의 업데이트 빈도를 보장하기 위한 것입니다. 일반적으로 화면 새로 고침 빈도가 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();
}
}
}
먼저 게임이 끝났는지, 아니면 정리 작업이 진행 중인지 확인하세요.
만약에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);
}
}
}
위의 코드에서 볼 수 있듯이 전체 프로세스는 네 단계로 나뉩니다.
현재 포인트와 현재 포인트를 포함하여 새 pointsClone을 복사합니다.
pointsClone을 한 줄씩 감지하고 전체 줄이 채워졌는지 표시합니다.
2에서 생성된 마킹된 내용에 따라 한 줄씩 삭제합니다. 삭제 작업은 위에서 아래로 수행되므로 행을 삭제하면 위에서부터 빈 행이 추가됩니다.
청소 작업.이 단계는 지우기 작업 수행 여부에 관계없이 수행되어야 합니다.this.points
, 동시에 완료current
그리고next
스위치.
블록의 회전에 무슨 일이 일어나고 있나요?
컨트롤러에 의해 정의된 이벤트, 외부 호출 등을 포함하여 모든 회전 동작은 game.rotate 메서드를 호출하여 트리거됩니다.
Game에 구현된 로직은 다음과 같습니다.
class Game {
rotate() {
this.stage.rotate();
this.canvas.update();
}
}
다음 시청Stage
실현
class Stage {
rotate(): boolean {
if (!this.current) {
return false;
}
const canChange = this.current.canRotate(this.points);
if (canChange) {
this.current.rotate();
}
return false;
}
}
먼저 판단하다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;
}
}
먼저 살펴보자canRotate
실현.
centerIndex를 얻으세요. centerIndex는 회전 중심점의 인덱스입니다. 다음과 같이 정의된 IBlock과 같이 각 그래픽은 다릅니다.
class IBlock extends Block {
getCenterIndex(): number {
return 1;
}
}
즉, 회전 중심점이 두 번째 노드가 됩니다.좋다口口口口
, 두 번째 중심점口田口口
。
또한, 이 블록을 설계할 때 선택할 수 없는 OBlock과 같이 일부 블록은 회전할 수 없다는 점도 고려했습니다.하지만getCenterIndex
반품-1
。
변경 사항 배열을 가져옵니다. 배열은 현재 회전 각도로 정의됩니다. 배열의 길이는 회전 수를 나타냅니다. 배열의 내용은 마지막 회전을 기준으로 한 회전 각도를 나타냅니다.좋다IBlock
다음과 같이 정의됩니다:
class IBlock extends Block {
currentChangeIndex: number = -1;
getChanges(): number[] {
return [
Math.PI / 2,
0 - Math.PI / 2
];
}
}
즉, 첫 번째 회전은 초기 상태 Math.PI/2(즉, 90도)이고, 두 번째 회전은 첫 번째 회전의 -Math.PI/2(즉, -90도)입니다. 다음과 같이:
// 初始状态
// 口田口口
// 第一次旋转
// 口
// 田
// 口
// 口
// 第二次旋转
// 口田口口
추신: 여기서 좌표축은 왼쪽에서 오른쪽, 위에서 아래라는 점에 유의하세요.
회전 판정의 판정 기준은 다음과 같습니다.
그러므로 우리는 다음과 같은 것이 있음을 알 수 있습니다.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;
}
}
위의 설명을 통해,rotate
논리는 이해하기 쉽습니다.
얻다centerIndex
그리고changes
,할 것이다currentChangeIndex
주기적 증분을 수행하고 블록이 새 좌표를 가리키도록 합니다.
~에currentChangeIndex
초기값은 -1로 현재 회전이 진행 중임을 의미하고, 0보다 크거나 같으면 인덱스 + 1이 선택된다는 의미이다. (여기서 배열의 인덱스는 0부터 시작하므로 잘 생각해주세요)
이동이란 블록을 네 방향으로 움직이는 것을 의미합니다.구현을 살펴보자
class Game {
move(direction: Direction) {
this.stage.move(direction);
this.canvas.update();
}
}
그 중 방향(Direction)은 다음과 같이 정의됩니다.
type Direction = 'up' | 'down' | 'left' | 'right';
더 자세히 살펴보세요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;
}
}
더 자세히 살펴보세요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;
}
});
};
}
간단히 번역해 보면 다음과 같습니다.
위로 이동하려면 모든 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;
}
}
좌표점의 값을 직접 수정합니다.
이 장에서는 게임의 세 가지 중요한 동작인 지우기, 회전 및 이동을 설명합니다. 세 사람이 서로 협력하여 게임을 완성합니다. 다음 장에서는 게임의 인터페이스 렌더링과 작동 제어를 공유하겠습니다.