LoginSignup
10
10

More than 3 years have passed since last update.

Rust + Entity Component System で仕様変更に強いゲーム設計 その2 〜 キャラの移動

Last updated at Posted at 2019-12-27

目次

その1 〜 序文
【イマココ】その2 〜 キャラの移動
その3−1 〜 コンポーネントの設計
その3−2 〜 システムの設計
その3−3 〜 メイン部分
その4−1 〜 剣を表示
その4−2 〜 アニメーションコンポーネント
その4-3 〜 アニメーションを動かす
その5-1~ あたり判定
その5-2~ やられアニメーション
その6 〜 これまでの振り返り

はじめに

前回の記事では、RustとECSの簡単な紹介をしました。
今回から、実際にRust言語を使い、ECSの思想に則って、ゲームを作ってみたいと思います。

なお、RustのECSライブラリもgithubで公開されてたりしますが、今回は、ECSの仕組みの理解とその有用性の研究のためにやっているので、自前でECSシステムを構築していきます。
あまり抽象化や汎用化を意識せず、今回作るゲームに合わせたシステムで設計していきます。

環境は、webassembly が使えるゲームエンジンの quicksilver を、クラウドIDEの gitpod 上で動かして開発を進めます。

この構成だと、ブラウザ上で開発からテストまでできます。とっても便利!

今回は第一段階として、キーボード入力「A」(左)、「D」(右)、「W」(上)、「S」(下)を検出して、画面上のキャラを動かす、というところまで、作ってみたいと思います。

今回つくったソースとテスト環境

今回作ったソースは、こちらで閲覧&テストできます。(要githubアカウント)
https://gitpod.io/#https://github.com/mas-yo/rust-ecs-game/tree/step-2

上記リンクをクリックすると、gitpodのワークスペースが立ち上がります。
その後、ライブラリのインストールが始まります。(初回のみ)
15分ほどかかります。

それが終わったら、画面下のコンソールから

$ cargo web start

とすると、ビルドが走ったあと、webサーバーが立ち上がります。

サーバーが立ち上がると、右下に、こんな通知が出るので、 Expose をクリック
step1-1.png

さらに、こんな通知が出るので、 Open Browser をクリック
step1-2.png

すると、ゲーム画面が開きます。

step1-3.png

ADWSキーで、黄緑色の円(これが操作キャラだと思ってください)が動くことが確認できると思います。

ソースだけみたい、という方は、こちらからどうぞ。
https://github.com/mas-yo/rust-ecs-game/tree/step-2

ECSによる設計

では、今回行った作業を順に紹介します。

コンポーネント

ECSの考え方で行くと、下記の4つのコンポーネントが必要になりそうです。

  • 入力状態を保持する Input コンポーネント
  • キャラの速度を保持する Velocity コンポーネント
  • キャラの位置を保持する Position コンポーネント
  • キャラの描画に必要な情報を保持する CharacterView コンポーネント

これらを、structとして定義してみます。

use quicksilver::geom::Vector;

#[derive(Default)]
pub(crate) struct Input {
    pub left: bool,
    pub right: bool,
    pub up: bool,
    pub down: bool,
}

pub(crate) type Velocity = Vector;

pub(crate) type Position = Vector;

#[derive(Default)]
pub(crate) struct CharacterView {
    pub position: Vector,
    pub dir: f32,          //キャラの向き(未使用)
    pub radius: f32,
}

システム

これらのコンポーネントを扱うシステムは、下記の4つになりそうです。

  • Input の情報を元に、Verocity を更新するシステム
  • Verocity の情報を元に、Position を更新するシステム
  • Position の情報を元に、CharacterView を更新するシステム
  • CharacterView の情報を元に、実際に画面に描画するシステム

とりあえずシンプルに、単なる関数として作ってみました。

use crate::components::*;
use quicksilver::prelude::*;

pub(crate) fn update_velocity(input: &Input, velocity: &mut Velocity) {
    velocity.x = 0f32;
    velocity.y = 0f32;
    if input.left {
        velocity.x = -1f32;
    }
    if input.right {
        velocity.x = 1f32;
    }
    if input.up {
        velocity.y = -1f32;
    }
    if input.down {
        velocity.y = 1f32;
    }
}

pub(crate) fn update_position(velocity: &Velocity, position: &mut Position) {
    position.x += velocity.x;
    position.y += velocity.y;
}

pub(crate) fn update_character_view(position: &Position, view: &mut CharacterView) {
    view.position = *position;
    view.dir = 0f32;
    view.radius = 10f32;
}

pub(crate) fn update_window(view: &CharacterView, window: &mut Window) {
    window.draw(
        &Circle::new((view.position.x, view.position.y), view.radius),
        Col(Color::GREEN),
    );
}

各関数は、自分が関わっているコンポーネントのみに依存していることがポイントです。

この様にしておくことで、例えば描画周りを quicksilver でなく htmlのcanvasにしたいと
思ったら、canvas用のupdate_window を用意して、そこだけを差し替えれば良い、ということになります。

main.rs

大元となる main.rs を忘れていました。
Game 構造体をつくり、そこに各コンポーネントを置きます。

そして、quicksilverのStateを下記の様に実装しました。
キーボードイベントをInputコンポーネントに反映するのと、各システムの関数を順に呼び出しています。

use quicksilver::prelude::*;

mod components;
mod systems;
use components::*;
use systems::*;

#[derive(Default)]
struct Game {
    input: Input,
    position: Position,
    velocity: Velocity,
    character_view: CharacterView,
}

impl State for Game {
    fn new() -> Result<Game> {
        Ok(Self{position: Position{x: 50f32, y:50f32}, ..Default::default()})
    }

    /// Will happen at a fixed rate of 60 ticks per second under ideal conditions. Under non-ideal conditions,
    /// the game loop will do its best to still call the update at about 60 TPS.
    ///
    /// By default it does nothing
    fn update(&mut self, _window: &mut Window) -> Result<()> {
        update_velocity(&self.input, &mut self.velocity);
        update_position(&self.velocity, &mut self.position);
        update_character_view(&self.position, &mut self.character_view);
        Ok(())
    }
    /// Process an incoming event
    ///
    /// By default it does nothing
    fn event(&mut self, event: &Event, _: &mut Window) -> Result<()> {
        match event {
            Event::Key(key, state) => {
                let mut pressed = false;
                if *state == ButtonState::Pressed {
                    pressed = true;
                } else if *state == ButtonState::Released {
                    pressed = false;
                }
                match key {
                    Key::A => {
                        self.input.left = pressed;
                    }
                    Key::D => {
                        self.input.right = pressed;
                    }
                    Key::W => {
                        self.input.up = pressed;
                    }
                    Key::S => {
                        self.input.down = pressed;
                    }
                    _ => {}
                }
            }
            _ => {}
        }
        Ok(())
    }

    fn draw(&mut self, window: &mut Window) -> Result<()> {
        window.clear(Color::WHITE)?;
        update_window(&self.character_view, window);
        Ok(())
    }
}

fn main() {
    run::<Game>("Game", Vector::new(800, 600), Settings::default());
}

エンティティは・・?

今回は、エンティティが1つ(プレイヤーキャラ)しか無いので、エンティティを考慮する必要がありませんでした。
次回から、エンティティの管理の部分も作ってみたいと思います。

その3につづく・・・

10
10
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
10
10