目次
その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
をクリック
さらに、こんな通知が出るので、 Open Browser
をクリック
すると、ゲーム画面が開きます。
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につづく・・・