Κοινή χρήση τεχνολογίας

Παίζοντας Tetris με το χέρι (3) - σχεδιασμός βασικής μονάδας του παιχνιδιού

2024-07-12

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

Παίζοντας Tetris με το χέρι - σχεδιασμός βασικής μονάδας του παιχνιδιού

Ξεκίνα το παιχνίδι

Σύμφωνα με το προηγούμενο σχέδιο, χρειαζόμαστε τα απαραίτητα στοιχεία του παιχνιδιού για να ξεκινήσουμε το παιχνίδι. Ας πάρουμε το παράδειγμα εκτέλεσης του 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();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

Ας το αναλύσουμε γραμμή προς γραμμή:

  • Η εισαγωγή των πακέτων χωρίζεται σε 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);
  }
}

  • 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), εσωτερική κατάσταση tickCount = 0;

  6. ΜΕΤΑΦΟΡΑcanvasΕκτελέστε μια μερική ενημέρωση αμέσως Η κύρια ενημέρωση εδώ είναι ότι η κατάσταση έχει αλλάξει, με αποτέλεσμα η κατάσταση του παιχνιδιού να πρέπει να αποδοθεί ξανά.

  7. Ξεκινήστε το χρονόμετρο, ο χρόνος του χρονοδιακόπτη περνάει αυτή την ταχύτητα,speedΣτο μέλλον, θα εξετάσουμε το ενδεχόμενο να το αντιστοιχίσουμε με το επίπεδο παιχνιδιού (δεν υποστηρίζεται ακόμη).

  • Εάν tickCount == 0, ενεργοποιήστε την ενέργεια tick ενός σταδίου και ελέγξτε αν τελειώνει αμέσως μετά την ενεργοποίηση.
  • Ενεργοποιήστε τη λειτουργία ενημέρωσης του καμβά
  • Το tickCount αυξάνεται αυτόματα και επαναρυθμίζεται εάν ικανοποιηθεί >= tickMaxCount.

Ο λόγος για τον οποίο εισάγεται ο μηχανισμός 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();
    }
  }
}
  • 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. Αντιγράψτε ένα νέο pointsClone, συμπεριλαμβανομένων των τρεχόντων και των τρεχόντων σημείων.

  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

    ΥΓ: Σημειώστε εδώ ότι ο άξονας συντεταγμένων είναι από αριστερά προς τα δεξιά και από πάνω προς τα κάτω.

  • Για την εκ περιτροπής κρίση, τα κριτήρια κρίσης είναι:

    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Εκτελέστε μια κυκλική αύξηση και στρέψτε το Block στις νέες συντεταγμένες.

  • σε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

Τροποποιήστε απευθείας την τιμή του σημείου συντεταγμένων.

περίληψη

Αυτό το κεφάλαιο περιγράφει τρεις σημαντικές συμπεριφορές του παιχνιδιού: εκκαθάριση, περιστροφή και μετακίνηση. Οι τρεις τους συνεργάζονται μεταξύ τους για να ολοκληρώσουν το παιχνίδι. Στο επόμενο κεφάλαιο θα μοιραστούμε την απόδοση της διεπαφής και τον έλεγχο λειτουργίας του παιχνιδιού.