LoginSignup
48
34

More than 5 years have passed since last update.

Rustでテトリス

Last updated at Posted at 2017-04-08

テトリスとは

降り続くいくつかの四角を操作し、その地を満たすように並べていくと
自然と四角は消え、積もる四角は下へと落ちていく
それを繰り返せば、日頃の煩悩は薄れ、さらに続ければ悟りが開かれよう
それすなわちテトリスと言う

...ナンノコッチャ...

というわけで皆さんご存知テトリスです

以前はターミナルで動くテトリスをRubyで作りました

tetris.gif

今回は少しリッチにOpenGLで描写するテトリスをRustでやります
source:github

コード

まずは描写とは分離されたテトリスのロジック本体

lib.rs

lib.rs
extern crate rand;
use rand::distributions;
use rand::distributions::IndependentSample;

use std::f32;

struct Tetris

操作するブロックとブロックを配置するフィールドを状態として持ちます
structは基本let mutされて使うことを前提とします

lib.rs
pub struct Tetris {
  pub block: Block,
  pub field: [[Color; 10]; 20],
}

struct Block

ブロックはひとかたまり同じ色として扱います
それぞれのブロックのサイズが違うので配列ではなくベクターを使い表現します

lib.rs
pub struct Block {
  pub color: Color,
  pub blocks: Vec<(i32,i32)>,
}

enum Color

色は黒色をフィールド上ではブロック未配置とします
OptionのNoneをそれとして扱った方がよかったかもね
そのままではOpenGLの色情報として扱えないのであとで変換してあげます

lib.rs
#[derive(PartialEq, Copy, Clone)]
pub enum Color {
  Black, Red, Green, Blue, Yellow, Cyan, Magenta, White
}

enum Control

ブロック操作コマンド

lib.rs
#[derive(PartialEq)]
pub enum Control {
  Down, Left, Right, Rotate
}

const COLORS, BLOCKS

降ってくるブロック色と形と
ちなみにconstではVec::new()などのアロケーションは使えないようです、当たり前か

lib.rs
const COLORS: &'static [Color] = &[
  Color::Red,
  Color::Green,
  Color::Blue, 
  Color::Yellow, 
  Color::Cyan, 
  Color::Magenta,
];

const BLOCKS: &'static [&'static [(i32,i32)]] = &[ 
  &[(0,0),(0,1),(1,0),(1,1)],
  &[(0,0),(0,1),(0,2),(1,1),(2,1)],
  &[(0,0),(0,1),(0,2),(0,3)],
  &[(0,0),(0,1),(0,2),(0,3),(1,3)],
  &[(0,0),(0,1),(0,2),(0,3),(1,0)],
  &[(0,0),(0,1),(1,1),(1,2)],
  &[(1,0),(1,1),(0,1),(0,2)],
  &[(0,1),(0,1),(0,2),(1,1)],
];

impl Block, fn new, rand, rotate, down, left, right

まずはブロックの操作や初期化を実装
回転はもうちょっとなんとかなりそう

lib.rs
impl Block {
  pub fn new(c: Color, b: Vec<(i32,i32)>) -> Block {
    return Block {
      color: c,
      blocks: b
    };
  }

  pub fn rand() -> Block {
    let mut rng = rand::thread_rng();
    let blocks_range = distributions::Range::new(0, BLOCKS.len());
    let colors_range = distributions::Range::new(0, COLORS.len());
    return Block {
      color: COLORS[colors_range.ind_sample(&mut rng)],
      blocks: BLOCKS[blocks_range.ind_sample(&mut rng)].to_vec()
    };
  }

  fn down(&mut self) {
    for c in self.blocks.iter_mut() {
      c.0 += 1;
    }
  }

  fn left(&mut self) {
    for c in self.blocks.iter_mut() {
      c.1 -= 1;
    }
  }

  fn right(&mut self) {
    for c in self.blocks.iter_mut() {
      c.1 += 1;
    }
  }

  fn rotate(&mut self) {
    let r: f32 = f32::consts::PI / 2.0;
    let cy: f32 = 
      (self.blocks.iter().map(|i| i.0).sum::<i32>() as f32) / (self.blocks.len() as f32);
    let cx: f32 = 
      (self.blocks.iter().map(|i| i.1).sum::<i32>() as f32) / (self.blocks.len() as f32);

    for c in self.blocks.iter_mut() {
      let (y, x) = *c;
      let y = f32::from(y as i16);
      let x = f32::from(x as i16);
      *c = (
        (cy + (x - cx) * r.sin() + (y - cy) * r.cos()).round() as i32,
        (cx + (x - cx) * r.cos() - (y - cy) * r.sin()).round() as i32
      );
    }
  }
}

impl Tetris, fn new, control, delete, fall

fn controlがブロックがフィールド上で移動可能どうか、可能なら移動させる
fn deleteは行が満ちていれば消し、フィールド上のブロックを落下させる
fn fallが現在操作中のブロックを1マス下へ落とす
もう落とせなければブロックをフィールドへ書き込み、新しいブロックを設定する

lib.rs
impl Tetris {
  pub fn new() -> Tetris {
    return Tetris {
      block: Block::rand(),
      field: [[Color::Black; 10]; 20],
    };
  }

  pub fn control(&mut self, op: Control) {
    let pre = self.block.blocks.clone();
    match op {
      Control::Down => self.block.down(),
      Control::Left => self.block.left(),
      Control::Right => self.block.right(),
      Control::Rotate => self.block.rotate()
    }

    let ly = self.field.len() as i32;
    let lx = self.field[0].len() as i32;
    let exists = self.block.blocks.iter().all(|&(y,x)| {
      return 0 <= y && y < ly && 0 <= x && x < lx 
        && (self.field[y as usize][x as usize] == Color::Black);
    });

    if !exists {
      self.block.blocks = pre;
    }
  }

  pub fn delete(&mut self) {
    for y in 0 .. self.field.len() {
      if self.field[y].iter().all(|c| *c != Color::Black) {
        for x in 0 .. self.field[y].len() {
          let mut yy = y;
          for yyy in (0 .. y - 1).rev() {
            self.field[yy][x] = self.field[yyy][x];
            yy -= 1;
          }
        }
      }
    }
  }

  pub fn fall(&mut self) {
    let blocks = self.block.blocks.clone();
    self.control(Control::Down);

    let mut not_moved = true;
    let len = self.block.blocks.len();
    for i in 0 .. len {
      if self.block.blocks[i] != blocks[i] {
        not_moved = false; 
        break;
      }
    }

    if not_moved {
      {
        let ref bs = self.block.blocks;
        for &(y,x) in bs {
          self.field[y as usize][x as usize] = self.block.color;
        }
      }
      self.block = Block::rand();
      self.delete();
    }
  }
}

main.rs

続いて、描写部分
gliumクレートを使います

main.rs
#[macro_use]
extern crate glium;
use glium::{DisplayBuild, Surface, Program};
use glium::{glutin, index, vertex};

extern crate tetris;
use tetris::{Tetris, Color};

use std::f32;
use std::thread;
use std::time;
use std::sync::{Arc, Mutex};

struct Vertex

gliumでは自前でVertexなstructを用意しなければなりません
が、マクロがトレイトを実装してくれるので楽チンです

main.rs
#[derive(Copy, Clone)]
struct Vertex {
  pos: [f32; 2],
  color: [f32; 4],
}
implement_vertex!(Vertex, pos, color);

const VERTEX_SHADER, FRAGMENT_SHADER

シェーダーも必須です
バーテックス経由でテトリスのフィールド情報を渡しています
ここもうちょっと上手いことできないかな

main.rs
const VERTEX_SHADER: &'static str = r#"
#version 400

in vec2 pos;
in vec4 color;
out vec4 v_color;

void main() {
  v_color = color;
  gl_Position = vec4(pos, 0, 1);
}
"#;

const FRAGMENT_SHADER: &'static str = r#"
#version 400

in vec4 v_color;
out vec4 f_color;

void main() {
  f_color = v_color;
}
"#;

fn color_to_rgba

RustのenumをOpenGLの色vec4へ変換

main.rs
pub fn color_to_rgba(c: Color) -> [f32; 4] {
  match c {
    Color::Black   => [0.0, 0.0, 0.0, 0.0],
    Color::Red     => [0.5, 0.0, 0.0, 0.5],
    Color::Green   => [0.0, 0.5, 0.0, 0.5],
    Color::Blue    => [0.0, 0.0, 0.5, 0.5],
    Color::Yellow  => [0.5, 0.5, 0.0, 0.5],
    Color::Cyan    => [0.0, 0.5, 0.5, 0.5],
    Color::Magenta => [0.5, 0.0, 0.5, 0.5],
    Color::White   => [0.5, 0.5, 0.5, 0.5]
  }
}

fn tetris_to_vertexs

テトリスの状態をバーテックスシェーダーへ渡す情報へ変換
本当はスケールなりトランスレートなりで表示位置変えたいのだけど
シェーダーだけでどうやるのかわからなかった
glScaleとかglTranslateとかをgliumが提供していないから、nalgebraの行列関数使えってことなのかな
デフォルトの-1 .. 1の間でどうにか表現、めんどかった

main.rs
fn tetris_to_vertexs(tetris: &Tetris) -> Vec<Vertex> {
  let mut vs = vec!();
  let mut y: f32 = 0.80;
  let mut iy: usize = 0;

  while y >= -0.885 {
    let mut x: f32 = -0.375;
    let mut ix: usize = 0;

    while x <= 0.46  {
      if tetris.block.blocks.iter().any(|&(yy,xx)| iy as i32 == yy && ix as i32 == xx) {
        vs.push(Vertex { 
          pos: [x, y], 
          color: color_to_rgba(tetris.block.color)
        });
      }
      else {
        vs.push(Vertex { 
          pos: [x, y], 
          color: color_to_rgba(tetris.field[iy][ix])
        });
      }

      x += 0.085;
      ix += 1;
    }

    y -= 0.085;
    iy += 1;
  }
  return vs;
}

fn main

やっとメイン
スレッドでタイマーを回し、メインスレッドでキー入力と表示を捌く
バーテックスとインデックスとシェーダーとユニフォームとパラメーターを用意してドロー!

実は排他制御が甘いせいか、mutのゴリ押しのせいか、表示がたまに崩れます
解決策がわかりませんでした😇

main.rs
fn main() {
  let display = glutin::WindowBuilder::new()
    .with_dimensions(600, 600).build_glium().unwrap();
  println!("{:?}", display. get_framebuffer_dimensions());

  let index = index::NoIndices(index::PrimitiveType::Points);
  let uniform = uniform!();
  let param = glium::DrawParameters { 
    point_size: Some(26.0),
    .. Default::default()
  };

  let program = match Program::from_source(&display, VERTEX_SHADER, FRAGMENT_SHADER, None) {
    Ok(p) => p,
    Err(e) => {
      println!("{}", e);
      return;
    }
  };

  let mutex_main = Arc::new(Mutex::new(Tetris::new()));
  let mutex_timer = mutex_main.clone();

  thread::spawn(move || {
    loop {
      thread::sleep(time::Duration::from_millis(1000));
      let mut t = mutex_timer.lock().unwrap();
      t.fall();
    }
  });

  loop {
    let mut t = mutex_main.lock().unwrap();

    'event: for e in display.poll_events() {
      match e {
        glutin::Event::KeyboardInput(
          glutin::ElementState::Pressed, _, Some(keycode)
        ) => {
          //println!("{:?}", e);
          match keycode {
            glutin::VirtualKeyCode::Up => {
              t.control(tetris::Control::Rotate);
            },

            glutin::VirtualKeyCode::Down => {
              t.fall();
            },

            glutin::VirtualKeyCode::Left => {
              t.control(tetris::Control::Left);
            },

            glutin::VirtualKeyCode::Right => {
              t.control(tetris::Control::Right);
            },

            glutin::VirtualKeyCode::Q => {
              return
            },

            _ => {}
          }

          break 'event;
        },

        _ => break 'event
      }
    }

    let mut frame = display.draw();

    let vertex_buffer = vertex::VertexBuffer::new(
       &display, &tetris_to_vertexs(&t) 
     ).unwrap();

    frame.clear_color(0.0, 0.0, 0.0 ,0.0);
    frame.draw(&vertex_buffer, &index, &program, &uniform, &param).unwrap();
    frame.finish().unwrap();
  } 
}

こいつ! 動くぞ!!

tetris.gif
(あぁ 表示崩れてる)

結び

ようやくRustと仲良く慣れた気がする
RcやRefCell、ArcやMutexなど
まだまだ理解が足りないところが多いが楽しくRust書けるようになってきたので良しとしよう

48
34
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48
34