Rustでゲームを作る（入門編） ～Rust用ゲームエンジンPistonの基本的な使い方～

本記事は[「Develop fun!」を体現する Works Human Intelligence Advent Calendar 2020](https://qiita.com/advent-calendar/2020/whi)の17日目の記事です。

## はじめに

本記事では、Rust用ゲームエンジンである[Piston](https://www.piston.rs/)の入門編として、簡単なゲームを作れる程度に基本的な機能を紹介していきます。

Pistonは3D等にも対応したゲームエンジンですが、今回はPistonの公式ラッパーライブラリである`piston_window`を使用した2Dゲームを作成するための機能を紹介していきます。また、コードのシンプルさを優先するため、`main.rs`内にすべてのコードを書いていきます。

記事作成に使用した環境は`rustc 1.48.0 (7eac88abb 2020-11-16)`および`piston_window 0.116.0`です[^toolchain]。

[^toolchain]: 使用したツールチェインは`x86_64-pc-windows-msvc`です。

### 本記事のゴール

最初に述べたとおり、Pistonを使って「簡単なゲームを作れる程度に基本的な機能」が実装できることがゴールとなります。

本記事では、「簡単なゲームを作れる程度に基本的な機能」として、下記の機能を定義します[^omit]。

1. ゲームを描画するための画面（ウィンドウ）を表示できる
2. 画像を読み込むことができる
3. 読み込んだ画像を画面の任意の位置に描画することができる
4. プレイヤーのキー入力をゲームに反映することができる

[^omit]: 「図形を描画する」「文字を描画する」「BGMや効果音を鳴らす」なども紹介したかったのですが、記事が長くなってしまうため本記事では割愛し、[最後にリンクを貼るだけ](#その他に知っておくと便利な機能)に留めたいと思います。

そして上記1.～4.を満たす最も簡単な例題として、**「画面上にサンタさんの画像が表示され、矢印キーでそのサンタさんが移動する」**というものを作成します[^santa]。

[^santa]: ~~アドベントカレンダーなので申し訳程度のクリスマス要素~~

## 前提

事前に`rustc`（コンパイラ）および`cargo`（ビルドツール／パッケージマネージャ）をインストールして使用可能にしてあるものとします。
まだインストールしていない方は[rustup](https://rustup.rs/)等でインストールしてください。

## 下準備

### プロジェクトの作成

最初に

```bash
$ cargo new --bin piston-test
```

などで新規プロジェクトを作ります。

プロジェクトが作成されたら、プロジェクトの`Cargo.toml`に

```toml:Cargo.toml
[dependencies]
piston_window = "0.116.0"
```

を追加します。

### 表示する画像の準備

表示する画像は[いらすとや](https://www.irasutoya.com/)さんより、下記の[「ソリに乗ったサンタとトナカイ」](https://www.irasutoya.com/2012/10/blog-post_24.html)のイラストをお借りしました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627744/e4833f11-6920-fded-dc1d-ac3bfe8a3340.png" width=30% />

ファイル名は`christmas_santa_sori.png`、大きさは800x394でした。
ここではプロジェクトディレクトリ直下に`img`ディレクトリを作成し、その中に配置（i.e. `piston-test/img/christmas_santa_sori.png`）します。

## コードの全体像

```rust:src/main.rs
extern crate piston_window;

use piston_window::*;

///ウィンドウタイトル
const WINDOW_TITLE: &str = "PistonTest";
///画面サイズ
const WINDOW_SIZE: Size = Size {
    width: 640.0,
    height: 480.0,
};

/// サンタさん
struct Santa {
    texture: G2dTexture,
    size: Size,
    scale: f64,
    pos: Position,
}
impl Santa {
    ///サンタさんが画面にやってくるよ
    fn draw(&self, c: Context, g: &mut G2d) {
        //描画位置とサイズをセット
        //指定した描画位置は画像の左上になるので、中央に配置する
        let transform = c
            .transform
            .trans(
                self.pos.x as f64 - self.screen_size().width / 2.0,
                self.pos.y as f64 - self.screen_size().height / 2.0,
            )
            .scale(self.scale, self.scale);
        //描画
        image(&self.texture, transform, g);
    }

    ///サンタさんが動くよ
    ///
    ///本来なら`move`という関数名にすべきだが、Rustでは`move`は予約語なので使えない
    fn sled(&mut self, key: &ArrowKeysState) {
        if key.up {
            if self.pos.y > (self.screen_size().height / 2.0) as i32 {
                self.pos.y -= Santa::SPEED;
            } else {
                self.pos.y = (self.screen_size().height / 2.0) as i32;
            }
        }
        if key.down {
            if self.pos.y < (WINDOW_SIZE.height - self.screen_size().height / 2.0) as i32 {
                self.pos.y += Santa::SPEED;
            } else {
                self.pos.y = (WINDOW_SIZE.height - self.screen_size().height / 2.0) as i32;
            }
        }
        if key.left {
            if self.pos.x > (self.screen_size().width / 2.0) as i32 {
                self.pos.x -= Santa::SPEED;
            } else {
                self.pos.x = (self.screen_size().width / 2.0) as i32;
            }
        }
        if key.right {
            if self.pos.x < (WINDOW_SIZE.width - self.screen_size().width / 2.0) as i32 {
                self.pos.x += Santa::SPEED;
            } else {
                self.pos.x = (WINDOW_SIZE.width - self.screen_size().width / 2.0) as i32;
            }
        }
    }

    ///サンタさんのソリの速さ
    const SPEED: i32 = 2;

    ///サンタさんの画面上のサイズ
    fn screen_size(&self) -> Size {
        return Size {
            width: self.size.width * self.scale,
            height: self.size.height * self.scale,
        };
    }
}

/// 矢印キーが押されているかの状態
struct ArrowKeysState {
    up: bool,
    down: bool,
    left: bool,
    right: bool,
}
impl ArrowKeysState {
    fn new() -> ArrowKeysState {
        return ArrowKeysState {
            up: false,
            down: false,
            left: false,
            right: false,
        };
    }
    ///`PistonWindow::ButtonArgs`から状態をセットする
    fn set(&mut self, key: &ButtonArgs) {
        match key.button {
            Button::Keyboard(Key::Up) => {
                self.up = if key.state == ButtonState::Press {
                    true
                } else {
                    false
                };
            }
            Button::Keyboard(Key::Down) => {
                self.down = if key.state == ButtonState::Press {
                    true
                } else {
                    false
                };
            }
            Button::Keyboard(Key::Left) => {
                self.left = if key.state == ButtonState::Press {
                    true
                } else {
                    false
                };
            }
            Button::Keyboard(Key::Right) => {
                self.right = if key.state == ButtonState::Press {
                    true
                } else {
                    false
                };
            }
            _ => {}
        }
    }
}

fn main() {
    let mut window: PistonWindow = WindowSettings::new(WINDOW_TITLE, WINDOW_SIZE)
        .exit_on_esc(true) //Escキーを押したら終了する
        .vsync(true) //垂直同期を有効にする
        .resizable(false) //ウィンドウのリサイズをさせない
        .samples(4) //アンチエイリアスのサンプル数
        .build()
        .unwrap_or_else(|e| panic!("Failed to build PistonWindow: {}", e));
    window.events.set_max_fps(60); //描画の最大FPS
    window.events.set_ups(60); //1秒間に何回アップデートするか

    let mut santa = Santa {
        texture: Texture::from_path(
            &mut window.create_texture_context(),
            "img/christmas_santa_sori.png",
            Flip::None,
            &TextureSettings::new(),
        )
        .unwrap(),
        size: Size {
            width: 800.0,
            height: 394.0,
        },
        scale: 0.2,
        pos: Position {
            x: (WINDOW_SIZE.width / 2.0) as i32,
            y: (WINDOW_SIZE.height / 2.0) as i32,
        },
    };

    let mut arrow_keys = ArrowKeysState::new();

    //メインループ
    while let Some(e) = window.next() {
        match e {
            Event::Loop(Loop::Render(_)) => {
                //レンダリング
                window.draw_2d(&e, |c, g, _| {
                    //画面を黒でクリア
                    clear([0.0, 0.0, 0.0, 1.0], g);
                    //サンタさんを描画
                    santa.draw(c, g);
                });
            }
            Event::Loop(Loop::Update(_)) => {
                //アップデート
                santa.sled(&arrow_keys);
            }
            Event::Input(i, _) => {
		        //入力関係
                if let Input::Button(key) = i {
                    arrow_keys.set(&key);
                }
            }
            _ => {}
        }
    }
}

```

このようなコードを書いてやり、

```bash
$ cargo run
```

で実行します。

![piston_santa.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627744/89f1e730-58c8-993e-176e-6008eda3b42c.gif)

あなたのPC画面にサンタさんが来てくれたら成功です。上下左右の矢印キーでサンタさんを動かすことができます。

## コード各部の解説

上記のコードの中から、Pistonの機能に関わる部分について抜粋してちょっとした解説等を入れていきます。

### ウィンドウの表示

```rust
let mut window: PistonWindow = WindowSettings::new(WINDOW_TITLE, WINDOW_SIZE)
	.exit_on_esc(true) //Escキーを押したら終了する
	.vsync(true) //垂直同期を有効にする
	.resizable(false) //ウィンドウのリサイズをさせない
	.samples(4) //アンチエイリアスのサンプル数
	.build()
	.unwrap_or_else(|e| panic!("Failed to build PistonWindow: {}", e));
window.events.set_max_fps(60); //描画の最大FPS
window.events.set_ups(60); //1秒間に何回アップデートするか
```

こちらでウィンドウのインスタンスを作成します。

`WindowSettings`のメソッドチェーンで指定できるオプションについては[こちら](https://docs.piston.rs/piston_window/piston_window/struct.WindowSettings.html)を参照してください。

```rust
//メインループ
while let Some(e) = window.next() {
	match e {
		Event::Loop(Loop::Render(_)) => {
			//レンダリング
		}
		Event::Loop(Loop::Update(_)) => {
			//アップデート
		}
		Event::Input(i, _) => {
			//入力関係
		}
		_ => {}
	}
}
```

こちらのメインループ内にコードを書いていくことで、ウィンドウ内にゲームを作成していきます。メインループの`window.next`からは`Option<piston_window::Event>`が取得できます。

`piston_window::Event`の中には`Loop`、`Input`、`Custom`の3つのイベントが[定義されており](https://docs.piston.rs/piston_window/piston_window/enum.Event.html)、さらに`Loop`の中には`Render`、`AfterRender`、`Update`、`Idle`の4つが[定義されています](https://docs.piston.rs/piston_window/piston_window/enum.Loop.html)。

`match`でこれらに分岐してやり、その中で各イベントに対する動作を書いていきます。

### 画像を表示する

#### 画像の読み込み

メインループが始まる前に

```rust
let mut santa = Santa {
	texture: Texture::from_path(
		&mut window.create_texture_context(),
		"img/christmas_santa_sori.png",
		Flip::None,
		&TextureSettings::new(),
	)
}
```

のように`Texture::from_path`で読み込みます。

#### 画像の描画

```rust
//レンダリング
window.draw_2d(&e, |c, g, _| {
	//画面を黒でクリア
	clear([0.0, 0.0, 0.0, 1.0], g);
	//サンタさんを描画
	santa.draw(c, g);
});
```

基本的には`draw_2d`関数に引数として渡される無名関数の中で描画していきます。この無名関数に渡される引数はそれぞれ

- `c`: `piston_window::Context`（コンテキスト、ものすごく大雑把に言えば座標空間のようなもの）
- `g`: `piston_window::G2d`（2D用のグラフィック周り諸々）
- `_`: `gfx_device_gl::Device`（OpenGLデバイス、今回のコードでは使用しません）

となっています。`Santa::draw`の中身は下記のとおりです。

```rust
fn draw(&self, c: Context, g: &mut G2d) {
	//描画位置とサイズをセット
	//指定した描画位置は画像の左上になるので、中央に配置する
	let transform = c
		.transform
		.trans(
			self.pos.x as f64 - self.screen_size().width / 2.0,
			self.pos.y as f64 - self.screen_size().height / 2.0,
		)
		.scale(self.scale, self.scale);
	    //描画
        image(&self.texture, transform, g);
}
```

画像は`image`関数などで描画します。

#### 指定した位置に描画する

`c.transform`のメソッドチェーンで位置やサイズをセットできます。

`transform`には、例えば回転などここで使わなかった機能も色々あります。詳しくは[こちら](http://docs.piston.rs/piston_window/piston_window/trait.Transformed.html)を参照してください。

### キー入力で画像を動かす

```rust
Event::Input(i, _) => {
    //入力関係
	if let Input::Button(key) = i {
		arrow_keys.set(&key);
	}
}
```

`Event::Input`は様々なイベントが発生したときに分岐し、引数として`Input`および`Option<u32>`が渡されます。（後者の引数にはタイムスタンプが格納されますが、今回は使いません。）

前者には「キーなどが押された」「マウスカーソルが動かされた」「ウィンドウにフォーカスがあたった／外された」「ウィンドウがリサイズされた」「ウィンドウが閉じられた」などのイベントが[実装されており](https://docs.piston.rs/piston_window/piston_window/enum.Input.html)、今回はキーボードからの入力を受け取りたいので、`Input::Button`イベントが発生したときに渡される`ButtonArgs`を受け取ります。

```rust
///`PistonWindow::ButtonArgs`から状態をセットする
fn set(&mut self, key: &ButtonArgs) {
	match key.button {
		Button::Keyboard(Key::Up) => {
			self.up = if key.state == ButtonState::Press {
				true
			} else {
				false
			};
		}
		Button::Keyboard(Key::Down) => {
			self.down = if key.state == ButtonState::Press {
				true
			} else {
				false
			};
		}
		Button::Keyboard(Key::Left) => {
			self.left = if key.state == ButtonState::Press {
				true
			} else {
				false
			};
		}
		Button::Keyboard(Key::Right) => {
			self.right = if key.state == ButtonState::Press {
				true
			} else {
				false
			};
		}
		_ => {}
	}
}
```

キーボードなどのキー／ボタンが押された時（`ButtonState::Press`）／離された時（`ButtonState::Press`）にイベントが発生するため、「現在押されているか否か」を判定するためにはこちらで状態管理してやる必要があります。

状態が格納された変数（本記事では`ArrowKeysState`構造体）をゲーム内で取得したい所（本記事では`Santa::sled`関数）で参照することで、プレイヤーのキー入力をゲームに反映させることが可能となります。

以上で、[本記事のゴール](#本記事のゴール)で示した4つの「簡単なゲームを作れる程度に基本的な機能」が実装できました。

## その他に知っておくと便利な機能

本記事では紹介できなかったものの、ゲームを作る上で知ってると何かと便利なPiston（主に`piston_window`）の機能を挙げていきます。

- 文字を描画したい：[piston_window::text](https://docs.rs/piston_window/0.116.0/piston_window/fn.text.html)
- 図形を描画したい
    - 弧：[piston_window::circle_arc](https://docs.rs/piston_window/0.116.0/piston_window/fn.circle_arc.html)
    - 円/楕円：[piston_window::ellipse](https://docs.rs/piston_window/0.116.0/piston_window/fn.ellipse.html) もしくは [piston_window::ellipse_from_to](https://docs.rs/piston_window/0.116.0/piston_window/fn.ellipse_from_to.html)
    - 線：[piston_window::line](https://docs.rs/piston_window/0.116.0/piston_window/fn.line.html) もしくは [piston_window::line_from_to](https://docs.rs/piston_window/0.116.0/piston_window/fn.line_from_to.html)
    - 四角形：[piston_window::rectangle](https://docs.rs/piston_window/0.116.0/piston_window/fn.rectangle.html) もしくは [piston_window::rectangle_from_to](https://docs.rs/piston_window/0.116.0/piston_window/fn.rectangle_from_to.html)
- キーボード以外にもマウスやゲームパッドから入力を受け付けたい：[piston_window::Button](https://docs.rs/piston_window/0.116.0/piston_window/enum.Button.html) 
    - この構造体の中に`Mouse`や`Controller`も定義されています。
- BGMや効果音を鳴らしたい: [music](https://docs.rs/piston-music/0.26.0/music/)
- 3Dを描画したい：[piston_window::PistonWindow.draw_3d](https://docs.rs/piston_window/0.116.0/piston_window/struct.PistonWindow.html#method.draw_3d)
- ランダムな数値を使いたい：[rand](https://docs.rs/rand/0.7.3/rand/)[^rand]

[^rand]: `rand`クレートはPistonの機能ではありませんが、ゲーム制作ではほぼ必須なので。

## おわりに

本記事は私が所属している企業である、[株式会社Works Human Intelligence](https://www.works-hi.co.jp/)の企業アドベントカレンダー用に作成されました。アドベントカレンダーのテーマが"Develop Fun!"とのことでしたので、企業アドベントカレンダーとはいいつつ普段の業務[^myjob]とは全く関係ない、私の趣味全開の記事を書かせていただきました。

[^myjob]: 普段の業務ではフロントエンド中心にフルスタックエンジニアをやっています。最近ではTypeScript + Node.js + ElectronでWindowsアプリケーションを作ったりしています。

Unityなどの個人でも使いやすく高機能なゲームエンジン・プラットフォームが普及した現在において、わざわざゲーム開発にRust + Pistonを選ぶ理由は薄いかもしれません。

しかし、「Rustって言語がアツいらしいけど、Rustを学ぶインセンティブがないんだよなあ～」「Rustで何か作ってみたいけど、特に作りたいものがあるわけではないんだよなあ～」といった「Rustに興味があるけど、学ぶ目的（作りたいもの）が無いから躊躇してしまう／長続きしない」人にとっては、「自分が作りたいゲームを作ってみる」というのは背中を押してくれる強力な目的になってくれるのではないでしょうか。

そういった方々にとって、この記事が「Rustを楽しみながら習得する」「Rustで楽しみながら開発する」ことの一助になれば幸いです。

## 参考資料
- [PistonDevelopers/piston_window: The official Piston convenience window wrapper for the Piston game engine](https://github.com/PistonDevelopers/piston_window)
- [piston_window - Rust](http://docs.piston.rs/piston_window/piston_window/index.html)
- [Rustのpiston_windowのループをmatch式で処理する最新の書き方](https://qiita.com/glaceef/items/1bc5377d3b5334bf95ad)