テトリスとは
降り続くいくつかの四角を操作し、その地を満たすように並べていくと
自然と四角は消え、積もる四角は下へと落ちていく
それを繰り返せば、日頃の煩悩は薄れ、さらに続ければ悟りが開かれよう
それすなわちテトリスと言う
...ナンノコッチャ...
というわけで皆さんご存知テトリスです
今回は少しリッチにOpenGLで描写するテトリスをRustでやります
source:github
コード
まずは描写とは分離されたテトリスのロジック本体
lib.rs
extern crate rand;
use rand::distributions;
use rand::distributions::IndependentSample;
use std::f32;
struct Tetris
操作するブロックとブロックを配置するフィールドを状態として持ちます
structは基本let mutされて使うことを前提とします
pub struct Tetris {
pub block: Block,
pub field: [[Color; 10]; 20],
}
struct Block
ブロックはひとかたまり同じ色として扱います
それぞれのブロックのサイズが違うので配列ではなくベクターを使い表現します
pub struct Block {
pub color: Color,
pub blocks: Vec<(i32,i32)>,
}
enum Color
色は黒色をフィールド上ではブロック未配置とします
OptionのNoneをそれとして扱った方がよかったかもね
そのままではOpenGLの色情報として扱えないのであとで変換してあげます
#[derive(PartialEq, Copy, Clone)]
pub enum Color {
Black, Red, Green, Blue, Yellow, Cyan, Magenta, White
}
enum Control
ブロック操作コマンド
#[derive(PartialEq)]
pub enum Control {
Down, Left, Right, Rotate
}
const COLORS, BLOCKS
降ってくるブロック色と形と
ちなみにconstではVec::new()などのアロケーションは使えないようです、当たり前か
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
まずはブロックの操作や初期化を実装
回転はもうちょっとなんとかなりそう
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マス下へ落とす
もう落とせなければブロックをフィールドへ書き込み、新しいブロックを設定する
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クレートを使います
#[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を用意しなければなりません
が、マクロがトレイトを実装してくれるので楽チンです
#[derive(Copy, Clone)]
struct Vertex {
pos: [f32; 2],
color: [f32; 4],
}
implement_vertex!(Vertex, pos, color);
const VERTEX_SHADER, FRAGMENT_SHADER
シェーダーも必須です
バーテックス経由でテトリスのフィールド情報を渡しています
ここもうちょっと上手いことできないかな
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へ変換
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の間でどうにか表現、めんどかった
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のゴリ押しのせいか、表示がたまに崩れます
解決策がわかりませんでした😇
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, ¶m).unwrap();
frame.finish().unwrap();
}
}
こいつ! 動くぞ!!
結び
ようやくRustと仲良く慣れた気がする
RcやRefCell、ArcやMutexなど
まだまだ理解が足りないところが多いが楽しくRust書けるようになってきたので良しとしよう