あけましておめでとうございます!!
今年もよろしくおねがいします!
今年は子年で、蛇関係ないですが、スネークゲームを作ります。
Rustに入門したい
ゲームを作りながらRustを勉強してみたいんです。
海外の方がYoutubeでRust向けゲームエンジンであるpiston
を用いて、
スネークゲームを作ってる動画を見つけたので、
翻訳しながら、自分が勉強したことをまとめていこうと思います。
Making a Snake Game in Rust
日本語訳チュートリアルとして、貢献したいな、と思っています。
この記事での完成形は、こんな感じ。この記事では、スネークが動くところまで実装します。
ここまでのソースコードはGithubに置いておきます。

Rust初心者が書く記事ですので、
間違ったことを書いてしまっていた場合はご指摘いただけると幸いです。
よろしくおねがいします。
200行のRustでスネークゲームを作られた方も。
記事を書き終えてから、ぜひソースコードを眺めて理解したいです。
Rustの開発環境の準備はとても簡単なので省略します。
記事も書かれているので参考に。Rustのアップデート方法もここにお世話になりました。
rustup で Rust コンパイラーを簡単インストール
開発環境
- Manjaro Linux 18.1.5
- Visual Studio Code 1.14.1
- rustup 1.12.1
- rustc 1.40.0
- cargo 1.40.0
Rustのアップデートは以下のコマンドを叩きます。
$ rustup update
プロジェクトの構築
PistonDevelopers/Piston-Tutorialsにもプロジェクトの構築手順が記載されています。
こちらも適宜参考にします。
プロジェクトを作成します。
--bin
オプションの他にも、--lib
オプションがあります。
-
--bin
: ビルドした際に、実行可能ファイルを作成する場合 -
--lib
: ビルドした際に、他のRustパッケージから利用できるライブラリファイルを作成する場合
というように使い分け、どちらも指定しなければ、--bin
が使用されます。
$ cargo new snake_game --bin
次に依存関係のインストールを行う。
Setting Up The Projectに書かれているdependencies
をコピペする。
[package]
name = "snake_game"
version = "0.1.0"
authors = ["Hiroya_W <mail@address>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# ここから下の部分
piston = "0.49.0"
piston2d-graphics = "0.35.0"
pistoncore-glutin_window = "0.63.0"
piston2d-opengl_graphics = "0.69.0"
コマンドからビルドする。
これにより、すべての依存関係が取得され、コンパイルされます。
$ cd snake_game
$ cargo build
ドキュメントを作成する。--open
オプションをつければ、ブラウザでドキュメントが開く。
あとで使うらしい。
$ cargo doc --open
いざ
ソースコードは全てmain.rs
に書いていきます。
外部クレートの読み込み(OLD)
クレートとは、他の言語における「ライブラリ」やパッケージと同じ意味です。
先程の依存関係のインストールでは、クレートがインストールされます。
このクレートを使用することをRust 2015では記述する必要がありました。
ので、こう書きます。
extern crate glutin_window;
extern crate graphics;
extern crate opengl_graphics;
extern crate piston;
fn main() {
println!("Hello, world!");
}
現在はRust 2018がリリースされ、それを利用しています。
Rust 2018からは書く必要が無くなったようです。
extern crate
が不要になり、use
文で他クレートのシンボルを直接インポートできるようになります。
消しちゃいましょう。
以下のようにuse
文を用いて他の名前空間にあるプログラムの要素をインポートします。
use glutin_window::GlutinWindow;
use opengl_graphics::{GlGraphics, OpenGL};
use piston::event_loop::*;
use piston::input::*;
use piston::window::WindowSettings;
この時、アスタリスクには注意する必要があります。
アスタリスクを用いることで、私達の名前空間にその名前空間の全てを読み込むことになります。
使用している関数や変数がどこから読み込んだものなのか見失ってしまったり、意図しない関数や変数を読み込んでしまう原因になります。
当時は、サンプルプログラムがアスタリスクを使用して記述されていたようです。現在は使わない記述がされているので、そのように修正します。
しなければならない、ではなく、した方がいい、という部分ですね。
外部クレートの読み込み(NEW)
結局のところ、外部クレートを読み込む部分の記述は以下のようになります。
サンプルプログラムのように、as
を使えば、自分で分かりやすい名前を付けて
読み込めるようですが、あえてここでは使わずに進めます。
以降、本記事では、記載するコードブロックが長くなってしまうため、use
文の記述を省略します。
use glutin_window::GlutinWindow;
use opengl_graphics::{GlGraphics, OpenGL};
use piston::event_loop::{EventSettings, Events};
use piston::input::{RenderArgs, RenderEvent, UpdateArgs, UpdateEvent};
use piston::window::WindowSettings;
アプリの設計のお話
画像は参考元動画から使用しています。
アプリの一般的なデザインは次の通りです。
ゲームを表すBlobデータまたは構造体があります。
この構造は、ウィンドウに描画する「レンダリング」メソッドを持っています。

キー入力やスネークの当たり判定など、ゲーム内の全てのものはレンダリングとは別に発生します。
それらは、このBlobデータを更新するだけです。


もちろん、その時Blobデータはレンダリングするように伝えられるので、スネークが歩き回るアニメーションを見ることができます。

ウィンドウの作成
最初のステップはウィンドウを作成することです。
main関数では、opengl
変数とGlutinWindow
を作成します。
fn main() {
let opengl = OpenGL::V3_2;
let mut window : GlutinWindow = WindowSettings::new(?,?); // この使い方は...?
}
サンプルコードを見れば、この静的メソッドの使い方がわかります。
しかし、そうしない場合はどうすればいいのでしょうか?
ドキュメントを見てみる
プロジェクトの作成時に作成したドキュメントを使用し、
WindowSetting
を検索してみます。
出てきました!
見てみると、まず、title
はジェネリック型のT
を持ち、
String
型に変換できる必要があることが分かります。
また、ジェネリック型のS
を持つsize
もあり、Size
型に変換できる必要があります。
わかった?
もちろん、わかりません。Size
型とは何でしょうか?
Size
をクリックしてみれば、分かります。
つまり、Trait Implementations
に書かれている4つのうち、どれかの形で記述すれば良いのです。
コードを書く
ウィンドウのタイトル、幅と高さを記述し、続くメソッドはサンプルを参考に記述していきます。
fn main() {
let opengl = OpenGL::V3_2;
let mut window: GlutinWindow = WindowSettings::new("Snake Game", [200, 200])
.graphics_api(opengl)
.exit_on_esc(true)
.build()
.unwrap();
}
実行してみる
以下のコマンドでビルドしてから実行することができます。
$ cargo run
今の状態では、ウィンドウが開くと、すぐに閉じるという動作をします。
なぜなら、プログラムのmain関数の最後まで実行されたため、プログラムが終了するからです。
イベントループ
コードを書く
もう少しサンプルからコピペします。
動画じゃコピーパスタって言ってて面白かった。
もちろん、常に責任を持ってコピペをするようにしてください。
fn main() {
let opengl = OpenGL::V3_2;
let mut window: GlutinWindow = WindowSettings::new("Snake Game", [200, 200])
.graphics_api(opengl)
.exit_on_esc(true)
.build()
.unwrap();
/* ---------------------------
ここから下
--------------------------- */
let mut events = Events::new(EventSettings::new());
while let Some(e) = events.next(&mut window) { // ここは何をしているのでしょうか?
if let Some(args) = e.render_args() {
// app.render(&args);
}
}
}
さて、event.next
は何をするのでしょうか?
ドキュメントを見てみる
event_loop::Events
を検索します。
見つかりました!探すのが上手になりましたね!
Option<Event>
のEvent
をクリックして見てみると、
Event
は列挙型(Enum)
であり、Input
,Loop
,Custom
の異なる表現があることが分かります。
Trait Implementations
には、全て異なるイベントのトレイトが書かれています。
これらは全て、Event
列挙体にメソッドを追加します。
ここでは、基本的に、受け取ったイベントの型を確認することができます。
今知りたいRenderEvent
トレイトのrender_args
メソッドは、
オプション型Option<RenderArgs>
の型を返す、ということが分かります。
オプション型は値があれば、Some
で値を包み、無いときはNone
が使われます。
if let文を眺めてみる
次にif let
文によるパターンマッチを眺めてみます。
let mut events = Events::new(EventSettings::new());
while let Some(e) = events.next(&mut window) {
if let Some(args) = e.render_args() { // ここのif let文を眺める
// app.render(&args);
}
}
このif let
文が意味するのは、「Event
がRenderEvent
である時、何か処理をする」ということです。
Event
がRenderEvent
でない時、render_args
メソッドはNone
返します。
つまり、if let
文でのパターンマッチに失敗するので、処理は行われません。
App構造体を作る
App
という構造体を作り、GlGraphics
を持たせます。
GlGraphics
は、ウィンドウ内に物を描画する役割を持ちます。
続いて、構造体にrender
メソッドを追加します。
// App構造体
struct App{
gl : GlGraphics,
}
// App構造体のメソッドの定義
impl App {
fn render(&mut self, arg: &RenderArgs) {}
}
fn main() {
//...
}
RustはPythonのように動き、render
メソッドの最初の引数self
は
メソッドを呼び出すインスタンス(レシーバともいう)が渡されます。
また、Rustなので、メソッドがインスタンスを取得する方法を指定できます。
-
self
: メソッドを呼び出すインスタンスの所有権がメソッドに移動する -
&self
: メソッドを呼び出すインスタンスをイミュータブルな参照として使用する -
&mut self
: メソッドを呼び出すインスタンスをミュータブルな参照として使用する
今回のrender
メソッドでは、&mut self
として使用します。
なぜなら、画面への描画はミュータブルである必要があるからです。
また、render
はRenderArgs
イベントへの参照も行います。
色を作る
緑色の背景がほしいので、色を作ります。
色はRed
,Green
,Blue
,Opacity
の4つの値を持ち、
値の範囲は0.0~1.0でなければなりません。
impl App {
fn render(&mut self, arg: &RenderArgs) {
use graphics;
let GREEN: [f32; 4] = [0.0, 1.0, 0.0, 1.0];
}
}
描画する部分
グラフィックスレンダラーを用いて描画させます。
そのためには、レンダラーイベントから取得できるviewport
とクロージャが必要です。
クロージャはdraw
メソッドにより呼び出される無名関数として働き、
コンテキストと自分自身を2つの引数として渡します。
graphics
ライブラリのclear
メソッドを使用して、ウィンドウの色を指定できます。
impl App {
fn render(&mut self, args: &RenderArgs) {
use graphics;
let GREEN: [f32; 4] = [0.0, 1.0, 0.0, 1.0];
// 緑色の背景を描画させる
self.gl.draw(args.viewport(), |_c, gl| {
graphics::clear(GREEN, gl);
});
}
}
App構造体を使う
main関数で作成したApp
構造体を初期化し、render
メソッドを呼び出します。
fn main() {
let opengl = OpenGL::V3_2;
let mut window: GlutinWindow = WindowSettings::new("Snake Game", [200, 200])
.graphics_api(opengl)
.exit_on_esc(true)
.build()
.unwrap();
// 初期化
let mut app = App {
gl: GlGraphics::new(opengl),
};
let mut events = Events::new(EventSettings::new());
while let Some(e) = events.next(&mut window) {
if let Some(args) = e.render_args() {
// メソッド呼び出し
app.render(&args);
}
}
}
実行してみる
緑色のウィンドウが描画されれば成功です!

ウィンドウタイトルはスクリーンショットを撮る関係上消えてしまってますが、
ちゃんとSnake Game
と表示されています。
これを、スネークゲームにしていきます。
スネークゲームを作る
スネーク構造体を追加する
App
構造体に、snake
フィールドを作る
struct App {
gl: GlGraphics,
snake: Snake,
}
そして、とても簡単なSnake
構造体を作ります。
Snake
構造体はX座標
とY座標
を持ち、render
メソッドを持たせます。
render
メソッドには、GlGraphics
を引数に渡すことでrender
イベントだけでなく、自分自身を描画できるようにします。
struct Snake {
pos_x: i32,
pos_y: i32,
}
impl Snake {
fn render(&self, gl: &mut GlGraphics, args: &RenderArgs) {
use graphics;
let RED: [f32; 4] = [1.0, 0.0, 0.0, 1.0];
// 正方形を作る
let square = graphics::rectangle::square(self.pos_x as f64, self.pos_y as f64, 20_f64);
// 自分自身を描画する
gl.draw(args.viewport(), |c, gl| {
let transform = c.transform;
graphics::rectangle(RED, square, transform, gl);
});
}
}
正方形を描画するコードは、App
の render
メソッドと非常に似ていて、App
のrender
メソッドでウィンドウを緑1色にclear
した後、このメソッドを呼び出す必要があります。
impl App {
fn render(&mut self, args: &RenderArgs) {
use graphics;
let GREEN: [f32; 4] = [0.0, 1.0, 0.0, 1.0];
self.gl.draw(args.viewport(), |_c, gl| {
graphics::clear(GREEN, gl);
});
// スネークの描画
self.snake.render(&mut self.gl, args);
}
}
App
構造体のインスタンスで、Snake
を初期化することを忘れないように。
let mut app = App {
gl: GlGraphics::new(opengl),
// 初期化
snake: Snake {
pos_x: 50,
pos_y: 100,
},
};
ひとまず実行してみる

赤色の正方形が表示されれば成功です!
これで、基本的なスネークゲームを作るための必要なものが揃いました。
他のイベント
キー入力をする
キーボードイベントは別の種類のEvent
ですが、RenderEvent
と同じ方法で取得できます。
ゲームを更新する
ゲームを更新するためのUpdateEvent
もあります。
例えば、スネークが移動するとか。
グリッドで移動するようにする
スネークは、1つの正方形ではなく、単なる正方形が連なったものです。

スネークに方向の情報を持たせることにします。
enum Direction {
Right,
Left,
Up,
Down,
}
ゲームをグリッドとして表示するようにスネークを修正します。
struct Snake {
pos_x: i32,
pos_y: i32,
// 方向を持たせる
dir: Direction,
}
let mut app = App {
gl: GlGraphics::new(opengl),
snake: Snake {
pos_x: 0,
pos_y: 0,
// 初期化
dir: Direction::Right,
},
};
render
メソッドを修正し、グリッドに従って移動するようにします。
impl Snake {
fn render(&self, gl: &mut GlGraphics, args: &RenderArgs) {
use graphics;
let RED: [f32; 4] = [1.0, 0.0, 0.0, 1.0];
let square =
// 移動量を調整
graphics::rectangle::square((self.pos_x * 20) as f64, (self.pos_y * 20) as f64, 20_f64);
gl.draw(args.viewport(), |c, gl| {
let transform = c.transform;
graphics::rectangle(RED, square, transform, gl);
});
}
}
今、ウィンドウサイズは200x200
としています(単位はpixel)。
1マスのサイズは20x20
とするなら、10x10
のグリッドに区切ることができます。
この時、スネークの場所をグリッドの座標を用いて表すと、
- 左上 pos_x : 0, pos_y : 0
- 右下 pos_x : 9, pos_x : 9
と表せます。
では、UpdateEvent
でスネークが移動するように、App
構造体にupdate
メソッドを追加します。
while let Some(e) = events.next(&mut window) {
// RenderEvent
if let Some(args) = e.render_args() {
app.render(&args);
}
// UpdateEventの時、updateメソッドを呼び出す
if let Some(args) = e.update_args(){
app.update();
}
}
App
構造体のupdate
メソッドで、Snake
のupdate
メソッドを呼び出すようにします。
impl App {
fn render(&mut self, args: &RenderArgs) {
// ...
}
// updateメソッドの実装
fn update(&mut self){
self.snake.update();
}
}
Snake
のupdate
メソッドでは、スネークが向いている方向に移動するようにします。
パターンマッチを使って座標を変更します。
impl Snake {
fn render(&self, gl: &mut GlGraphics, args: &RenderArgs) {
// ...
}
fn update(&mut self) {
match self.dir {
Direction::Left => self.pos_x -= 1,
Direction::Right => self.pos_x += 1,
Direction::Up => self.pos_y -= 1,
Direction::Down => self.pos_y += 1,
}
}
}
とりあえず実行してみる
ものすごい速さで右側に移動するのが見えたでしょうか...。
イベントの更新回数を調節する
ups
メソッドを使って1秒あたり8回更新されるようにします。
EventLoop
の名前空間を読み込んで、
use piston::event_loop::{EventSettings, Events, EventLoop};
ups
メソッドを呼び出します。
let mut events = Events::new(EventSettings::new()).ups(8);
これで実行してみれば、いい感じに移動するようになっていると思います。
キー入力で方向を切り替える
キー入力のイベントを使用するために、名前空間を読み込みます。
use piston::input::{
keyboard::Key, Button, ButtonEvent, ButtonState, RenderArgs, RenderEvent, UpdateArgs,
UpdateEvent,
};
キー入力があった時に、pressed
メソッドを呼び出すようにします。
// ButtonEvent
if let Some(args) = e.button_args() {
// "キーを押した"っていう状態になって発火したイベントだったら
if args.state == ButtonState::Press{
app.pressed(&args.button);
}
}
pressed
メソッドを実装します。
last_direction
に所有権を渡すわけではないので、clone
で値をコピーします。
入力されたキーをパターンマッチで判断し、更にパターンに条件(ガード)をつけます。
こうすることで、スネークの方向転換に制限をかけます。
impl App {
fn render(&mut self, args: &RenderArgs) {
// ...
}
fn update(&mut self) {
// ...
}
fn pressed(&mut self, btn: &Button) {
// 値はcloneする
let last_direction = self.snake.dir.clone();
self.snake.dir = match btn {
&Button::Keyboard(Key::Up) if last_direction != Direction::Down => Direction::Up,
&Button::Keyboard(Key::Down) if last_direction != Direction::Up => Direction::Down,
&Button::Keyboard(Key::Left) if last_direction != Direction::Right => Direction::Left,
&Button::Keyboard(Key::Right) if last_direction != Direction::Left => Direction::Right,
_ => last_direction,
}
}
}
でも、これにはバグがあり、コンパイラはDirection
を比較しようとするとエラーを吐きます。
そもそも、それらが等しいという概念を実装していないからです。
# [derive(Clone, PartialEq)]
enum Direction {
Right,
Left,
Up,
Down,
}
derive
で自動的に実装できます。とても簡単でした...。
ついてくる尻尾を追加する
ついてくる尻尾を追加します。
そのために、標準ライブラリのLinkedList
を使用します。
use std::collections::LinkedList;
各尻尾のX座標,Y座標をペアとして持つLinkedList
にスネークを修正していきます。
struct Snake {
body: LinkedList<(i32,i32)>,
dir: Direction,
}
そうすると、スネークの移動と、レンダリングの部分のコードを書き変えなければなりません。
移動するには、新しいスネークの頭、もしくは、リストの先頭をクローンします。
先程と同様の理由で、clone
しなければならないことに注意してください。
方向に基づいて頭の位置を更新し、LinkedList
の先頭に追加し、最後の要素を取り除きます。
移動させるというよりかは、進行方向に新しい頭を置き、尻尾を1個消す、みたいなイメージ。
impl Snake {
fn render(&self, gl: &mut GlGraphics, args: &RenderArgs) {
// ...
}
fn update(&mut self) {
let mut new_head = (*self.body.front().expect("Snake has no body")).clone();
match self.dir {
Direction::Left => new_head.0 -= 1,
Direction::Right => new_head.0 += 1,
Direction::Up => new_head.1-= 1,
Direction::Down => new_head.1 += 1,
}
self.body.push_front(new_head);
self.body.pop_back().unwrap();
}
}
これをレンダリングしていきます。
レンダリングは各尻尾の部分に対して行いますが、本質的には全て同じことを行います。
-
まず、
Snake.body
をiter
を使ってイテレータを回します。
それぞれの要素において、map
を使って、座標のペアをx
とy
の変数にマッピングして
正方形を作成したものをベクトル型に格納していきます。 -
次に、作成したベクトル型の
squares
に対して、into_iter
を使って、
イテレータを回し、それぞれの正方形を描画していきます。
イテレータには3種類あり、
-
iter(&self)
: 各要素を&T
型で返すイテレータ -
iter_mut(&mut self)
: 各要素を&mut T
型で返すイテレータ -
into_iter(self)
: 各要素をT
型で返すイテレータ
into_iter
メソッドは引数がself
で、コレクションの所有権が移動します。
なので、一度呼ぶとそのコレクションにはアクセスできなくなります。
参照の場合は、iter
メソッド、ですね。
impl Snake {
fn render(&self, gl: &mut GlGraphics, args: &RenderArgs) {
use graphics;
let RED: [f32; 4] = [1.0, 0.0, 0.0, 1.0];
// 正方形を作成したものをベクトル型に格納
let squares: Vec<graphics::types::Rectangle> = self
.body
.iter()
.map(|&(x, y)| graphics::rectangle::square((x * 20) as f64, (y * 20) as f64, 20_f64))
.collect();
gl.draw(args.viewport(), |c, gl| {
let transform = c.transform;
// それぞれの正方形を描画
squares
.into_iter()
.for_each(|square| graphics::rectangle(RED, square, transform, gl));
});
}
fn update(&mut self) {
// ...
}
}
main関数のsnake
インスタンスでの初期化も修正しなければなりません。
from_iter
メソッドを使って、ベクトル型からLinkedList
を作成します。
FromIterator
をインポートします。
use std::iter::FromIterator;
スネークの初期位置は、(0,0)
と``(0,1)`となります。
let mut app = App {
gl: GlGraphics::new(opengl),
snake: Snake {
body: LinkedList::from_iter((vec![(0, 0), (0, 1)]).into_iter()),
dir: Direction::Right,
},
};
実行してみる

静止画でわかりにくくてごめんなさい。
いつかGIF画像に置き換えます。
こんな感じに、キー入力で移動、尻尾がついてくるところまで実装できました。
一旦ここまで
Youtubeでの解説はここまでで終わりでした。
ので、この記事も一旦ここまで、としたいと思います。
まだ移動までしか出来てなくて、ここからゲームらしさが追加されていくことになるので楽しみですね。
完成形のソースコードへのリンクはYoutubeの概要欄に記載されているので、次回はそれを参考に完成させたいと思います。
ここまで書いてみて、Rust特有の仕様、書き方を体験しつつ、ゲーム制作ができるいい題材だったのではないかな、と感じています。