LoginSignup
5
2

More than 3 years have passed since last update.

Rust + Entity Component System で仕様変更に強いゲーム設計 その3−2 〜 システムの設計

Last updated at Posted at 2020-01-10

目次

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

システム

3−1に引き続き、実装を進めます。
Component ComponentContainer ができたので、次に System を作ってみましょう。

その2で作った(ECSの)システムは、単なる関数として、

fn update_velocity(velocity: &mut Velocity, input: &Input) {}

この様な形になっていました。
よく見ると、この関数名の velocity と、引数の &mut Velocity は、表現として冗長な気がしますね。
例えば、あとから、 Velocity の名前を変えてしまった場合、 update_velocity の方も変えることになり、面倒なことになりそうです。

そこで、同じ関数名で引数が違うだけで、別の機能を表現できる様にしました。
Rustでは関数のオーバーロードがありませんが、下記の様な形で実現できます。

pub(crate) trait SystemInterface {
    type Update;
    type Refer;
}
pub(crate) trait SystemProcess: SystemInterface {
    fn process(update: &mut Self::Update, _ref: &Self::Refer);
}

pub(crate) struct System<U, R> {
    phantom: PhantomData<(U, R)>,
}

impl<U, R> SystemInterface for System<U, R> {
    type Update = U;
    type Refer = R;
}

そして、update_velocity に相当する関数は、下記の様に書きます。

impl SystemProcess for System<CContainer<Velocity>, CContainer<Input>> {
    fn process(velocities: &mut Self::Update, inputs: &Self::Refer) {
        velocities
            .iter_mut()
            .zip_entity(inputs)
            .for_each(|(velocity, input)| {
                velocity.x = 0f32;
                velocity.y = 0f32;
                if input.left {
                    velocity.x = -2f32;
                }
                if input.right {
                    velocity.x = 2f32;
                }
                if input.up {
                    velocity.y = -2f32;
                }
                if input.down {
                    velocity.y = 2f32;
                }
            });
    }
}

こうしておくと、呼び出し側では、

System::process(&mut velocities, &inputs);

の様な形で呼び出せます。
関数名は process で共通となり、引数の型によって機能が変わるようになっています。

System<CContainer<Velocity>, CContainer<Input>>::process の中身は実装は、同じエンティティIDを持つ Velocity Input に対して、Inputの情報をVelocityに反映させるようになっています。

敵の動きの実装

では、今回(その3)のゴールである、敵の動きを実装してみます。
まずは、敵の Velocity です。

impl SystemProcess
    for System<CContainer<Velocity>, (&CContainer<Position>, &CContainer<MoveTarget>)>
{
    fn process(velocities: &mut Self::Update, pos_tgt: &Self::Refer) {
        velocities
            .iter_mut()
            .zip_entity2(pos_tgt.0, pos_tgt.1)
            .for_each(|(vel, pos, target)| {
                let mut tmp = Vector::default();
                tmp.x = target.x - pos.x;
                tmp.y = target.y - pos.y;
                vel.x = tmp.x / 50f32;
                vel.y = tmp.y / 50f32;
            });
    }
}

敵の Velocity は、自分の PositionMoveTarget との差分から計算する必要があります。
よって、System::Refer(Velocityを更新するために参照するデータ)の型は、

(&CContainer<Position>, &CContainer<MoveTarget>)

このようなタプルとなりました。
上記ソース内の、zip_entity2() は、2つの ComponentContainer を受け取って、同じエンティティIDのコンポーネントを順に返してくれるイテレータです。

敵かどうかの判定

では、 MoveTarget 自体は、どのように値を更新したら良いでしょうか?

MoveTarget を決めるためには、他のキャラを見て、これが敵だと認識できなければなりません。
そこで、Team というコンポーネントを導入することにしました。

pub(crate) struct Team {
    team_id: u32,
}

プレイヤーキャラ、敵キャラは、 Team を持っていて、チームIDが違うもの同士は敵、という意味です。

MoveTarget は、他のすべてのキャラのうち、自分から一定距離の範囲にいて、自分とは違うチームのキャラの、位置に設定される、という処理にしました。

impl SystemProcess for System<CContainer<MoveTarget>, (&CContainer<Team>, &CContainer<Position>)> {
    fn process(move_targets: &mut Self::Update, team_pos: &Self::Refer) {
        let (teams, positions) = team_pos;
        move_targets
            .iter_mut()
            .zip_entity2(teams, positions)
            .for_each(|(target, self_team, self_pos)| {
                teams
                    .iter()
                    .filter(|(_, team)| team.team_id() != self_team.team_id())
                    .for_each(|(entity_id, _)| {
                        if let Some(pos) = CContainer::<Position>::get(positions, entity_id) {
                            let distance = pos.distance((self_pos.x, self_pos.y));
                            if distance < 100f32 {
                                target.x = pos.x;
                                target.y = pos.y;
                            } else {
                                target.x = self_pos.x;
                                target.y = self_pos.y;
                            }
                        }
                    });
            });
    }
}

ここまでで、敵の動きの実装は完了です。

次回、3−3で、メインの処理を実装します。

5
2
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
5
2