效果演示

本示例涉及的主要知识点:

  1. eframe 跨平台配置
  2. egui 的基本用法
  3. Rust 语言条件编译
  4. egui 基本控件用法
  5. egui 布局
  6. egui 图形绘制技术
  7. 键盘事件响应
  8. 按钮事件响应
  9. 嵌入字体

部分源码如下:

use crate::pages::gameview::settings::{
    BACKGROUND_COLOR, BLOCK_COLOR, BLOCK_SIZE, GRID_HEIGHT, GRID_WIDTH,
};
use crate::pages::gameview::shape;
use eframe::egui::{Rect, Ui, Vec2};
use getrandom;

pub struct Tetris {
    grid: Vec<Vec<u8>>,
    current_tetromino: shape::Tetromino,
    current_shape: [[u8; 4]; 4], // Store the current shape directly
    position: (i32, i32),
}

impl Tetris {
    pub fn new() -> Self {
        let tetromino = shape::Tetromino::T;
        Self {
            grid: vec![vec![0; GRID_WIDTH]; GRID_HEIGHT],
            current_tetromino: tetromino,
            current_shape: tetromino.shape(),
            position: (0, 3),
        }
    }

    pub fn update(&mut self) {
        self.position.0 += 1;
        if self.check_collision() {
            self.position.0 -= 1;
            self.place_tetromino();
            self.clear_lines();
            self.spawn_new_tetromino();
        }
    }

    fn check_collision(&self) -> bool {
        for y in 0..4 {
            for x in 0..4 {
                if self.current_shape[y][x] == 1 {
                    let grid_x = self.position.1 + x as i32;
                    let grid_y = self.position.0 + y as i32;

                    // Check if the tetromino is out of bounds or collides with existing blocks
                    if grid_x < 0
                        || grid_x >= GRID_WIDTH as i32
                        || grid_y >= GRID_HEIGHT as i32
                        || (grid_y >= 0 && self.grid[grid_y as usize][grid_x as usize] == 1)
                    {
                        return true;
                    }
                }
            }
        }
        false
    }

    fn place_tetromino(&mut self) {
        for y in 0..4 {
            for x in 0..4 {
                if self.current_shape[y][x] == 1 {
                    let grid_x = self.position.1 + x as i32;
                    let grid_y = self.position.0 + y as i32;
                    if grid_y >= 0 && grid_x >= 0 {
                        self.grid[grid_y as usize][grid_x as usize] = 1;
                    }
                }
            }
        }
    }

    fn clear_lines(&mut self) {
        self.grid.retain(|row| row.iter().any(|&cell| cell == 0));
        while self.grid.len() < GRID_HEIGHT {
            self.grid.insert(0, vec![0; GRID_WIDTH]);
        }
    }

    fn spawn_new_tetromino(&mut self) {
        let shapes = [
            shape::Tetromino::I,
            shape::Tetromino::O,
            shape::Tetromino::T,
            shape::Tetromino::S,
            shape::Tetromino::Z,
            shape::Tetromino::J,
            shape::Tetromino::L,
        ];

        let mut buf = [0u8; 1];
        getrandom::fill(&mut buf).expect("Failed to generate random number");
        let index = (buf[0] as usize) % shapes.len();

        self.current_tetromino = shapes[index];
        self.current_shape = self.current_tetromino.shape();
        self.position = (0, 3);
    }

    pub fn render(&self, ui: &mut Ui) {
        let rect = ui.available_rect_before_wrap();
        let center = rect.center();
        let grid_size = Vec2::new(
            GRID_WIDTH as f32 * (BLOCK_SIZE + 2.0),
            GRID_HEIGHT as f32 * (BLOCK_SIZE + 2.0),
        );
        let mut top_left = center - grid_size / 2.0;
        top_left.y -= 35.0;

        // Draw the grid
        for y in 0..GRID_HEIGHT {
            for x in 0..GRID_WIDTH {
                let color = if self.grid[y][x] == 1 {
                    BLOCK_COLOR
                } else {
                    BACKGROUND_COLOR
                };
                let block_rect = Rect::from_min_size(
                    top_left
                        + Vec2::new(x as f32 * (BLOCK_SIZE + 2.0), y as f32 * (BLOCK_SIZE + 2.0)),
                    Vec2::splat(BLOCK_SIZE),
                );
                ui.painter().rect_filled(block_rect, 0.0, color);
            }
        }

        // Draw the current moving tetromino
        for y in 0..4 {
            for x in 0..4 {
                if self.current_shape[y][x] == 1 {
                    let grid_x = self.position.1 + x as i32;
                    let grid_y = self.position.0 + y as i32;
                    if grid_y >= 0
                        && grid_x >= 0
                        && grid_x < GRID_WIDTH as i32
                        && grid_y < GRID_HEIGHT as i32
                    {
                        let block_rect = Rect::from_min_size(
                            top_left
                                + Vec2::new(
                                    grid_x as f32 * (BLOCK_SIZE + 2.0),
                                    grid_y as f32 * (BLOCK_SIZE + 2.0),
                                ),
                            Vec2::splat(BLOCK_SIZE),
                        );
                        ui.painter().rect_filled(block_rect, 0.0, BLOCK_COLOR);
                    }
                }
            }
        }
    }

    pub fn move_horizontal(&mut self, direction: i32) {
        self.position.1 += direction;
        if self.check_collision() {
            self.position.1 -= direction; // Revert if collision occurs
        }
    }

    pub fn rotate(&mut self) {
        let mut new_shape = [[0; 4]; 4];

        // Rotate the shape 90 degrees clockwise
        for y in 0..4 {
            for x in 0..4 {
                new_shape[x][3 - y] = self.current_shape[y][x];
            }
        }

        // Check for collision after rotation
        let original_shape = self.current_shape;
        self.current_shape = new_shape;
        if self.check_collision() {
            self.current_shape = original_shape; // Revert if collision occurs
        }
    }
}