LoginSignup
25
14

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-16

本記事は「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. ゲームを描画するための画面(ウィンドウ)を表示できる
  2. 画像を読み込むことができる
  3. 読み込んだ画像を画面の任意の位置に描画することができる
  4. プレイヤーのキー入力をゲームに反映することができる

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

前提

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

下準備

プロジェクトの作成

最初に

$ cargo new --bin piston-test

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

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

Cargo.toml
[dependencies]
piston_window = "0.116.0"

を追加します。

表示する画像の準備

表示する画像はいらすとやさんより、下記の「ソリに乗ったサンタとトナカイ」のイラストをお借りしました。

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

コードの全体像

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);
                }
            }
            _ => {}
        }
    }
}

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

$ cargo run

で実行します。

piston_santa.gif

あなたの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の中にはLoopInputCustomの3つのイベントが定義されており、さらにLoopの中にはRenderAfterRenderUpdateIdleの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)の機能を挙げていきます。

おわりに

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

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

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

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

参考資料


  1. 使用したツールチェインはx86_64-pc-windows-msvcです。 

  2. 「図形を描画する」「文字を描画する」「BGMや効果音を鳴らす」なども紹介したかったのですが、記事が長くなってしまうため本記事では割愛し、最後にリンクを貼るだけに留めたいと思います。 

  3. アドベントカレンダーなので申し訳程度のクリスマス要素 

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

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

25
14
0

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
25
14