本記事は「Develop fun!」を体現する Works Human Intelligence Advent Calendar 2020の17日目の記事です。
はじめに
本記事では、Rust用ゲームエンジンであるPistonの入門編として、簡単なゲームを作れる程度に基本的な機能を紹介していきます。
Pistonは3D等にも対応したゲームエンジンですが、今回はPistonの公式ラッパーライブラリであるpiston_window
を使用した2Dゲームを作成するための機能を紹介していきます。また、コードのシンプルさを優先するため、main.rs
内にすべてのコードを書いていきます。
記事作成に使用した環境はrustc 1.48.0 (7eac88abb 2020-11-16)
およびpiston_window 0.116.0
です1。
本記事のゴール
最初に述べたとおり、Pistonを使って「簡単なゲームを作れる程度に基本的な機能」が実装できることがゴールとなります。
本記事では、「簡単なゲームを作れる程度に基本的な機能」として、下記の機能を定義します2。
- ゲームを描画するための画面(ウィンドウ)を表示できる
- 画像を読み込むことができる
- 読み込んだ画像を画面の任意の位置に描画することができる
- プレイヤーのキー入力をゲームに反映することができる
そして上記1.~4.を満たす最も簡単な例題として、**「画面上にサンタさんの画像が表示され、矢印キーでそのサンタさんが移動する」**というものを作成します3。
前提
事前にrustc
(コンパイラ)およびcargo
(ビルドツール/パッケージマネージャ)をインストールして使用可能にしてあるものとします。
まだインストールしていない方はrustup等でインストールしてください。
下準備
プロジェクトの作成
最初に
$ cargo new --bin piston-test
などで新規プロジェクトを作ります。
プロジェクトが作成されたら、プロジェクトのCargo.toml
に
[dependencies]
piston_window = "0.116.0"
を追加します。
表示する画像の準備
表示する画像はいらすとやさんより、下記の「ソリに乗ったサンタとトナカイ」のイラストをお借りしました。
ファイル名はchristmas_santa_sori.png
、大きさは800x394でした。
ここではプロジェクトディレクトリ直下にimg
ディレクトリを作成し、その中に配置(i.e. piston-test/img/christmas_santa_sori.png
)します。
コードの全体像
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);
}
}
_ => {}
}
}
}
このようなコードを書いてやり、
$ cargo run
で実行します。
あなたのPC画面にサンタさんが来てくれたら成功です。上下左右の矢印キーでサンタさんを動かすことができます。
コード各部の解説
上記のコードの中から、Pistonの機能に関わる部分について抜粋してちょっとした解説等を入れていきます。
ウィンドウの表示
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
のメソッドチェーンで指定できるオプションについてはこちらを参照してください。
//メインループ
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つのイベントが定義されており、さらにLoop
の中にはRender
、AfterRender
、Update
、Idle
の4つが定義されています。
match
でこれらに分岐してやり、その中で各イベントに対する動作を書いていきます。
画像を表示する
画像の読み込み
メインループが始まる前に
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
で読み込みます。
画像の描画
//レンダリング
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
の中身は下記のとおりです。
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
には、例えば回転などここで使わなかった機能も色々あります。詳しくはこちらを参照してください。
キー入力で画像を動かす
Event::Input(i, _) => {
//入力関係
if let Input::Button(key) = i {
arrow_keys.set(&key);
}
}
Event::Input
は様々なイベントが発生したときに分岐し、引数としてInput
およびOption<u32>
が渡されます。(後者の引数にはタイムスタンプが格納されますが、今回は使いません。)
前者には「キーなどが押された」「マウスカーソルが動かされた」「ウィンドウにフォーカスがあたった/外された」「ウィンドウがリサイズされた」「ウィンドウが閉じられた」などのイベントが実装されており、今回はキーボードからの入力を受け取りたいので、Input::Button
イベントが発生したときに渡されるButtonArgs
を受け取ります。
///`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
- 図形を描画したい
- キーボード以外にもマウスやゲームパッドから入力を受け付けたい:piston_window::Button
- この構造体の中に
Mouse
やController
も定義されています。
- この構造体の中に
- BGMや効果音を鳴らしたい: music
- 3Dを描画したい:piston_window::PistonWindow.draw_3d
- ランダムな数値を使いたい:rand4
おわりに
本記事は私が所属している企業である、株式会社Works Human Intelligenceの企業アドベントカレンダー用に作成されました。アドベントカレンダーのテーマが"Develop Fun!"とのことでしたので、企業アドベントカレンダーとはいいつつ普段の業務5とは全く関係ない、私の趣味全開の記事を書かせていただきました。
Unityなどの個人でも使いやすく高機能なゲームエンジン・プラットフォームが普及した現在において、わざわざゲーム開発にRust + Pistonを選ぶ理由は薄いかもしれません。
しかし、「Rustって言語がアツいらしいけど、Rustを学ぶインセンティブがないんだよなあ~」「Rustで何か作ってみたいけど、特に作りたいものがあるわけではないんだよなあ~」といった「Rustに興味があるけど、学ぶ目的(作りたいもの)が無いから躊躇してしまう/長続きしない」人にとっては、「自分が作りたいゲームを作ってみる」というのは背中を押してくれる強力な目的になってくれるのではないでしょうか。
そういった方々にとって、この記事が「Rustを楽しみながら習得する」「Rustで楽しみながら開発する」ことの一助になれば幸いです。
参考資料
- PistonDevelopers/piston_window: The official Piston convenience window wrapper for the Piston game engine
- piston_window - Rust
- Rustのpiston_windowのループをmatch式で処理する最新の書き方
-
使用したツールチェインは
x86_64-pc-windows-msvc
です。 ↩ -
「図形を描画する」「文字を描画する」「BGMや効果音を鳴らす」なども紹介したかったのですが、記事が長くなってしまうため本記事では割愛し、最後にリンクを貼るだけに留めたいと思います。 ↩
-
アドベントカレンダーなので申し訳程度のクリスマス要素↩ -
rand
クレートはPistonの機能ではありませんが、ゲーム制作ではほぼ必須なので。 ↩ -
普段の業務ではフロントエンド中心にフルスタックエンジニアをやっています。最近ではTypeScript + Node.js + ElectronでWindowsアプリケーションを作ったりしています。 ↩