τα στοιχεία επικοινωνίας μου
Ταχυδρομείο[email protected]
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
Σύμφωνα με το προηγούμενο σχέδιο, χρειαζόμαστε τα απαραίτητα στοιχεία του παιχνιδιού για να ξεκινήσουμε το παιχνίδι. Ας πάρουμε το παράδειγμα εκτέλεσης του Tetris στην κονσόλα.
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-console.
Αρχικοποίηση θέματος, καμβά και ελεγκτή.
Αρχικοποίηση εργοστασίου και διάστασης.
Για την προετοιμασία του παιχνιδιού, χρησιμοποιήστε τα αντικείμενα καμβά, εργοστασιακά, καμβά και διαστάσεων αρχικοποιημένα προηγουμένως.
Το παιχνίδι καλεί τη μέθοδο έναρξης.
Στη συνέχεια, ας ρίξουμε μια ματιά στο τι κάνει η αρχή;
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
, η εσωτερική του εφαρμογή έχει ως εξής:
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, συμπεριλαμβανομένων των τρεχόντων και των τρεχόντων σημείων.
Ανίχνευση σημείων Κλωνοποίηση γραμμή προς γραμμή και επισήμανση εάν ολόκληρη η γραμμή είναι γεμάτη.
Διαγράψτε γραμμή προς γραμμή σύμφωνα με το επισημασμένο περιεχόμενο που δημιουργήθηκε στο 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
Εκτελέστε μια κυκλική αύξηση και στρέψτε το Block στις νέες συντεταγμένες.
σεcurrentChangeIndex
Η αρχική τιμή είναι -1, που σημαίνει ότι η τρέχουσα περιστροφή είναι σε εξέλιξη και εάν είναι μεγαλύτερη ή ίση με 0, σημαίνει ότι έχει επιλεγεί ο δείκτης + 1. (Σκεφτείτε προσεκτικά εδώ, γιατί ο δείκτης του πίνακα ξεκινά από το 0)
Κίνηση σημαίνει μετακίνηση του Μπλοκ προς τέσσερις κατευθύνσεις.Ας δούμε την εφαρμογή του
class Game {
move(direction: Direction) {
this.stage.move(direction);
this.canvas.update();
}
}
Μεταξύ αυτών, η Κατεύθυνση ορίζεται ως εξής:
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;
}
}
Τροποποιήστε απευθείας την τιμή του σημείου συντεταγμένων.
Αυτό το κεφάλαιο περιγράφει τρεις σημαντικές συμπεριφορές του παιχνιδιού: εκκαθάριση, περιστροφή και μετακίνηση. Οι τρεις τους συνεργάζονται μεταξύ τους για να ολοκληρώσουν το παιχνίδι. Στο επόμενο κεφάλαιο θα μοιραστούμε την απόδοση της διεπαφής και τον έλεγχο λειτουργίας του παιχνιδιού.