はじめに
前回は、テトリスのゲーム部分の実装を行いました。
今回は、winit と tiny-skia による描画をあわせて、ゲームを完成させましょう。
実装の全体像は以下のリポジトリを参照してください。
ウインドウの変更
前々回のウインドウ作成処理は以下のようなものでした。
fn main() { let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_inner_size(winit::dpi::LogicalSize::new(500, 300)) .with_title("winit") .build(&event_loop).unwrap(); let window = std::rc::Rc::new(window); let context = softbuffer::Context::new(window.clone()).unwrap(); let mut surface = softbuffer::Surface::new(&context, window.clone()).unwrap(); event_loop.set_control_flow(ControlFlow::Wait); let _ = event_loop.run(move |event, elwt| { match event { Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => elwt.exit(), Event::AboutToWait => window.request_redraw(), Event::WindowEvent { window_id, event: WindowEvent::RedrawRequested } if window_id == window.id() => { // 描画処理 }, _ => () } }); }
最初にウインドウのサイズとタイトルを変更しておきましょう。
テトリスの盤面に合わせたサイズとし、タイトルも Tetris に変更しておきます。
let window = WindowBuilder::new() .with_inner_size(winit::dpi::LogicalSize::new(200, 440)) .with_title("Tetris") .build(&event_loop).unwrap();
次にコントロールフローを ControlFlow::Wait
から ControlFlow::Poll
に変更します。
event_loop.set_control_flow(ControlFlow::Poll);
ControlFlow::Poll
は、OSがイベントをディスパッチしていなくても、イベントループを継続的に実行します。一方、ControlFlow::Wait
は、処理可能なイベントがない場合にイベントループを一時停止します。ゲームなどのアプリケーションでは、ControlFlow::Poll
を選択します。
コントロールフローの変更に加えて、テトリスのインスタンスも追加しておきましょう。
event_loop.set_control_flow(ControlFlow::Poll); let mut game: Tetris = Tetris::new(); // ...
続いて、キー入力を処理します。
キーイベントの捕捉
winit のイベントは以下のような定義となっています。
pub enum Event<T: 'static> { NewEvents(StartCause), WindowEvent { window_id: WindowId, event: WindowEvent, }, DeviceEvent { device_id: DeviceId, event: DeviceEvent, }, UserEvent(T), Suspended, Resumed, AboutToWait, LoopExiting, MemoryWarning, }
同じ名前で紛らわしいですが、WindowEvent
の中の event: WindowEvent
は以下の定義となっており、この内の KeyboardInput
でキー入力を処理することができます。
pub enum WindowEvent { ActivationTokenDone { serial: AsyncRequestSerial, token: ActivationToken, }, Resized(PhysicalSize<u32>), Moved(PhysicalPosition<i32>), CloseRequested, Destroyed, DroppedFile(PathBuf), HoveredFile(PathBuf), HoveredFileCancelled, Focused(bool), KeyboardInput { device_id: DeviceId, event: KeyEvent, is_synthetic: bool, }, // 略 }
Event::WindowEvent
の WindowEvent::KeyboardInput
をパターンマッチに追加します。
match event { Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => elwt.exit(), Event::WindowEvent { event: WindowEvent::KeyboardInput {event, .. }, .. } if event.state.is_pressed() => { match event.logical_key { Named(NamedKey::ArrowRight) => game.key_pressed(Key::RIGHT), Named(NamedKey::ArrowLeft) => game.key_pressed(Key::LEFT), Named(NamedKey::ArrowDown) => game.key_pressed(Key::DOWN), Named(NamedKey::ArrowUp) => game.key_pressed(Key::UP), Named(NamedKey::Space) => game.key_pressed(Key::SP), Named(NamedKey::Escape) => game.rerun(), _ => game.key_pressed(Key::OTHER), }; window.request_redraw(); }, Event::AboutToWait => { // ...
event.logical_key
の内容に応じて、ゲームのインスタンスのメソッドをコールするだけです。
ここでは、常に window.request_redraw();
で再描画をリクエストしていますが、必要に応じて再描画を行う方が良いでしょう。が、ここでは簡単のため、そのままにします。
続いて AboutToWait
の内容を変更します。
前回まででは、再描画を要求するのみでしたが、ここでゲームを進めるために game.tick()
を呼び出すことにします。
Event::AboutToWait => { if !game.stopped { game.tick(); window.set_title(format!("Tetris:{}", game.score).as_str()); window.request_redraw(); } },
加えて、ゲームスコアをタイトルに設定し、再描画をリクエストします。
ゲームが終了していた場合は、何も処理を行いません。
ゲームの描画
ゲームの描画は再描画要求に応じて、以下のように書きます。
Event::WindowEvent { window_id, event: WindowEvent::RedrawRequested } if window_id == window.id() => { let (width, height) = { let size = window.inner_size(); (size.width, size.height) }; surface.resize( core::num::NonZeroU32::new(width).unwrap(), core::num::NonZeroU32::new(height).unwrap(), ).unwrap(); let mut pixmap = Pixmap::new(width, height).unwrap(); game.draw(&mut pixmap); let mut buffer = surface.buffer_mut().unwrap(); for index in 0..(width * height) as usize { buffer[index] = pixmap.data()[index * 4 + 2] as u32 | (pixmap.data()[index * 4 + 1] as u32) << 8 | (pixmap.data()[index * 4 + 0] as u32) << 16; } buffer.present().unwrap(); },
前々回に見た通り、pixmap
にグラフィックス書き込みを行い、バッファに反映する流れは同様です。
ゲームの描画は、game.draw(&mut pixmap);
として委譲しています。
実装は以下のようになります。
impl Tetris { // ... fn draw(&self, pixmap: &mut Pixmap) { for y in 0..BOARD_HEIGHT { for x in 0..BOARD_WIDTH { Tetris::draw_square( pixmap, x * UNIT_SIZE, y * UNIT_SIZE, self.shape_at(x, BOARD_HEIGHT - y - 1)); } } for i in 0..4 { let (x, y) = self.current.point(i); Tetris::draw_square( pixmap, x * UNIT_SIZE, (BOARD_HEIGHT - y - 1) * UNIT_SIZE, self.current.obj.kind); } }
最初に盤面のブロックを書き込み、その後、落下ブロックを書き込んでいます。
draw_square()
では種類に応じて色違いの四角形を描画しているだけです。
impl Tetris { // ... fn draw_square(pixmap: &mut Pixmap, x: i16, y: i16, kind: Tetromino) { if kind == Tetromino::X { return; } let rect = Rect::from_xywh( (x + 1) as f32, (y + 1) as f32, (UNIT_SIZE - 2) as f32, (UNIT_SIZE - 2) as f32, ).unwrap(); let path = PathBuilder::from_rect(rect); let mut paint = Paint::default(); let (r ,g, b) = kind.color(); paint.set_color_rgba8(r, g, b, 255); pixmap.fill_path( &path, &paint, FillRule::EvenOdd, Transform::identity(), None, ); } }
ゲームの実行
以上で実装は完了です。
モジュール分割なども行わない、雑な実装になりますが、まとめると以下のコードになります。
use winit::event::{ Event, WindowEvent }; use winit::event_loop::{ ControlFlow, EventLoop }; use winit::window::WindowBuilder; use tiny_skia::{ FillRule, Paint, PathBuilder, Pixmap, Rect, Transform }; use winit::keyboard::{ Key::Named, NamedKey }; use std::time::{ Duration, SystemTime }; fn main() { let event_loop = EventLoop::new().unwrap(); let window = WindowBuilder::new() .with_inner_size(winit::dpi::LogicalSize::new(200, 440)) .with_title("Tetris") .build(&event_loop).unwrap(); let window = std::rc::Rc::new(window); let context = softbuffer::Context::new(window.clone()).unwrap(); let mut surface = softbuffer::Surface::new(&context, window.clone()).unwrap(); event_loop.set_control_flow(ControlFlow::Poll); let mut game: Tetris = Tetris::new(); let _ = event_loop.run(move |event, elwt| { match event { Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => elwt.exit(), Event::WindowEvent { event: WindowEvent::KeyboardInput {event, .. }, .. } if event.state.is_pressed() => { match event.logical_key { Named(NamedKey::ArrowRight) => game.key_pressed(Key::RIGHT), Named(NamedKey::ArrowLeft) => game.key_pressed(Key::LEFT), Named(NamedKey::ArrowDown) => game.key_pressed(Key::DOWN), Named(NamedKey::ArrowUp) => game.key_pressed(Key::UP), Named(NamedKey::Space) => game.key_pressed(Key::SP), Named(NamedKey::Escape) => game.rerun(), _ => game.key_pressed(Key::OTHER), }; window.request_redraw(); }, Event::AboutToWait => { if !game.stopped { game.tick(); window.set_title(format!("Tetris:{}", game.score).as_str()); window.request_redraw(); } }, Event::WindowEvent { window_id, event: WindowEvent::RedrawRequested } if window_id == window.id() => { let (width, height) = { let size = window.inner_size(); (size.width, size.height) }; surface.resize( core::num::NonZeroU32::new(width).unwrap(), core::num::NonZeroU32::new(height).unwrap(), ).unwrap(); let mut pixmap = Pixmap::new(width, height).unwrap(); game.draw(&mut pixmap); let mut buffer = surface.buffer_mut().unwrap(); for index in 0..(width * height) as usize { buffer[index] = pixmap.data()[index * 4 + 2] as u32 | (pixmap.data()[index * 4 + 1] as u32) << 8 | (pixmap.data()[index * 4 + 0] as u32) << 16; } buffer.present().unwrap(); }, _ => () } }); } /// Tetromino is a geometric shape composed of four squares, connected orthogonally. #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum Tetromino { S, Z, I, T, O, J, L, X, } impl Tetromino { fn rand() -> Tetromino { match rand::random::<u16>() % 7 { 0 => Tetromino::S, 1 => Tetromino::Z, 2 => Tetromino::I, 3 => Tetromino::T, 4 => Tetromino::O, 5 => Tetromino::J, 6 => Tetromino::L, _ => Tetromino::X, } } fn points(&self) -> [[i16; 2]; 4] { match self { Tetromino::S => [[ 0, -1], [0, 0], [-1, 0], [-1, 1]], Tetromino::Z => [[ 0, -1], [0, 0], [ 1, 0], [ 1, 1]], Tetromino::I => [[ 0, -1], [0, 0], [ 0, 1], [ 0, 2]], Tetromino::T => [[-1, 0], [0, 0], [ 1, 0], [ 0, 1]], Tetromino::O => [[ 0, 0], [1, 0], [ 0, 1], [ 1, 1]], Tetromino::J => [[-1, -1], [0, -1], [ 0, 0], [ 0, 1]], Tetromino::L => [[ 1, -1], [0, -1], [ 0, 0], [ 0, 1]], Tetromino::X => [[ 0, 0], [0, 0], [ 0, 0], [ 0, 0]], } } fn is_rotatable(&self) -> bool { !(matches!(self, Tetromino::O) || matches!(self, Tetromino::X)) } fn color(&self) -> (u8, u8, u8) { match self { Tetromino::S => (204, 102, 102), Tetromino::Z => (102, 204, 102), Tetromino::I => (104, 102, 204), Tetromino::T => (204, 204, 102), Tetromino::O => (204, 102, 204), Tetromino::J => (204, 204, 204), Tetromino::L => (218, 170, 0), _ => ( 0, 0, 0) } } } /// A Tetromino block. #[derive(Copy, Clone, Debug)] struct Block { kind: Tetromino, points: [[i16; 2]; 4], } impl Block { fn rand() -> Block { Block::block(Tetromino::rand()) } fn block(t: Tetromino) -> Block { Block { kind: t, points: t.points() } } fn rotate_left(&self) -> Block { if self.kind.is_rotatable() { let mut points: [[i16; 2]; 4] = [[0; 2]; 4]; for i in 0..4 { points[i] = [self.points[i][1], -self.points[i][0]]; } Block { points, ..*self } } else { *self } } fn rotate_right(&self) -> Block { if self.kind.is_rotatable() { let mut points: [[i16; 2]; 4] = [[0; 2]; 4]; for i in 0..4 { points[i] = [-self.points[i][1], self.points[i][0]]; } Block { points, ..*self } } else { *self } } fn min_y(&self) -> i16 { self.points.iter().min_by_key(|p| p[1]).unwrap()[1] } } #[derive(Debug)] struct FallingBlock { x: i16, y: i16, obj: Block, } impl FallingBlock { fn new() -> Self { let obj = Block::rand(); FallingBlock { x: BOARD_WIDTH / 2, y: BOARD_HEIGHT - 1 + obj.min_y(), obj, } } fn empty() -> Self { FallingBlock { x: 0, y: 0, obj: Block::block(Tetromino::X) } } fn down(&self) -> FallingBlock { FallingBlock { y: self.y - 1, ..*self } } fn left(&self) -> FallingBlock { FallingBlock { x: self.x - 1, ..*self } } fn right(&self) -> FallingBlock { FallingBlock { x: self.x + 1, ..*self } } fn rotate_left(&self) -> FallingBlock { FallingBlock { obj: self.obj.rotate_left(), ..*self } } fn rotate_right(&self) -> FallingBlock { FallingBlock { obj: self.obj.rotate_right(), ..*self } } fn is_empty(&self) -> bool { self.obj.kind == Tetromino::X } fn point(&self, i: usize) -> (i16, i16) { (self.x + self.obj.points[i][0], self.y - self.obj.points[i][1]) } } /// Type of key. enum Key { LEFT, RIGHT, UP, DOWN, SP, OTHER, } const UNIT_SIZE: i16 = 20; const BOARD_WIDTH: i16 = 10; const BOARD_HEIGHT: i16 = 22; const BOARD_LEN: usize = BOARD_WIDTH as usize * BOARD_HEIGHT as usize; /// Game of tetris. struct Tetris { board: [Tetromino; BOARD_LEN], current: FallingBlock, stopped: bool, time: SystemTime, score: u32, } impl Tetris { fn new() -> Self { Tetris { board: [Tetromino::X; BOARD_LEN], current: FallingBlock::empty(), stopped: false, time: SystemTime::now(), score: 0, } } fn rerun(&mut self) { self.board = [Tetromino::X; BOARD_LEN]; self.current = FallingBlock::empty(); self.stopped = false; self.time = SystemTime::now(); self.score = 0; } fn tick(&mut self) { if self.current.is_empty() { self.put_block(); } else if self.time.elapsed().unwrap() > Duration::from_millis((1000 - self.score) as u64) { self.down(); self.time = SystemTime::now(); } } fn key_pressed(&mut self, key: Key) { if self.stopped || self.current.is_empty() { return; } match key { Key::LEFT => { self.try_move(self.current.left()); }, Key::RIGHT => { self.try_move(self.current.right()); }, Key::UP => { self.try_move(self.current.rotate_right()); }, Key::DOWN => { self.try_move(self.current.rotate_left()); }, Key::OTHER => { self.down(); }, Key::SP => { self.drop_down(); }, }; } fn down(&mut self) { if !self.try_move(self.current.down()) { self.block_dropped(); } } fn drop_down(&mut self) { while self.current.y > 0 { if !self.try_move(self.current.down()) { break; } } self.block_dropped(); } fn block_dropped(&mut self) { for i in 0..4 { let (x, y) = self.current.point(i); let index = (y * BOARD_WIDTH + x) as usize; self.board[index] = self.current.obj.kind; } self.remove_complete_lines(); if self.current.is_empty() { self.put_block(); } } fn put_block(&mut self) { self.stopped = !self.try_move(FallingBlock::new()); } fn try_move(&mut self, block: FallingBlock) -> bool { for i in 0..4 { let (x, y) = block.point(i); if x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT { return false } if self.shape_at(x, y) != Tetromino::X { return false } } self.current = block; true } fn remove_complete_lines(&mut self) { let mut line_count = 0; for i in (0..BOARD_HEIGHT).rev() { let mut complete = true; for x in 0.. BOARD_WIDTH { if self.shape_at(x, i) == Tetromino::X { complete = false; break } } if complete { line_count += 1; for y in i..BOARD_HEIGHT - 1 { for x in 0..BOARD_WIDTH { self.board[(y * BOARD_WIDTH + x) as usize] = self.shape_at(x, y + 1); } } } } self.score += line_count * line_count; self.current = FallingBlock::empty(); } fn shape_at(&self, x: i16, y: i16) -> Tetromino { self.board[(y * BOARD_WIDTH + x) as usize] } fn draw(&self, pixmap: &mut Pixmap) { for y in 0..BOARD_HEIGHT { for x in 0..BOARD_WIDTH { Tetris::draw_square( pixmap, x * UNIT_SIZE, y * UNIT_SIZE, self.shape_at(x, BOARD_HEIGHT - y - 1)); } } for i in 0..4 { let (x, y) = self.current.point(i); Tetris::draw_square( pixmap, x * UNIT_SIZE, (BOARD_HEIGHT - y - 1) * UNIT_SIZE, self.current.obj.kind); } } fn draw_square(pixmap: &mut Pixmap, x: i16, y: i16, kind: Tetromino) { if kind == Tetromino::X { return; } let rect = Rect::from_xywh( (x + 1) as f32, (y + 1) as f32, (UNIT_SIZE - 2) as f32, (UNIT_SIZE - 2) as f32, ).unwrap(); let path = PathBuilder::from_rect(rect); let mut paint = Paint::default(); let (r ,g, b) = kind.color(); paint.set_color_rgba8(r, g, b, 255); pixmap.fill_path( &path, &paint, FillRule::EvenOdd, Transform::identity(), None, ); } }
実行してみましょう。
cargo run
うまく動いていそうですね。
700 点を超えるのは、なかなか難しいです。
まとめ
Rust の GUI 周りを調べていたところ、なぜかテトリスができあがりました。
不思議です。