Help us understand the problem. What is going on with this article?

Rust + Entity Component System で仕様変更に強いゲーム設計 その5―1~ あたり判定

目次

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

前回は、アニメーションの仕組みと、それを入力に従って再生する機能を実装しました。
今回は、当たり判定と、やられアニメーションを実装してみたいと思います。

今回つくったもの

今回つくったものはこちら。

attack.gif

だいぶゲームらしくなってきました。
昔の2Dアクションゲームでは良くあった、プレイヤーの攻撃は、武器だけにあり、相手の攻撃は、体そのもの(つまり敵に触っただけでダメージを受ける)という実装になっています。
敵を2体にしてみました。

ソースはこちらにあります。
https://github.com/mas-yo/rust-ecs-game/tree/step-5

それではまず、あたり判定の部分を実装してみましょう。

あたり判定コンポーネント

あたり判定用のコンポーネントとして、
SwordCollider :武器(剣)コライダ(プレイヤーキャラ用)
BodyWeaponCollider :体を武器とするコライダ(敵キャラ用)
BodyDefenseCollider :体のコライダ(攻撃を受ける側、プレイヤー、敵共通)
を用意します。

#[derive(Default)]
pub(crate) struct SwordCollider {
    pub active: bool,  //武器を振ってるときだけtrueにする
    pub line: quicksilver::geom::Line,
}
impl SwordCollider {
    pub fn is_collided(&self, body: &BodyDefenseCollider) -> bool {
        if self.active == false {
            false
        } else {
            body.circle.overlaps(&self.line)
        }
    }
}

#[derive(Default)]
pub(crate) struct BodyWeaponCollider {
    pub circle: quicksilver::geom::Circle,
}

impl BodyWeaponCollider {
    pub fn is_collided(&self, body: &BodyDefenseCollider) -> bool {
        body.circle.overlaps(&self.circle)
    }
}

#[derive(Default)]
pub(crate) struct BodyDefenseCollider {
    pub hit: bool,  //攻撃を受けたときにtrueにする
    pub circle: quicksilver::geom::Circle,
}

それぞれ、quicksilver::geomが提供するCircle, Lineなどの形状structを利用しています。

なお、私があたり判定の実装を始めた当初は、下記の様な汎用的な設計を試しましたが、やってみたらロジックを共通化できる便利な感じにはならなかった事と、具体的な名前のコンポーネント名の方が、用途が明確に示されていて良いと思ったので、結局上記の様な形になりました。

良くなかった設計
struct Collider<T> where T:IsCollided //IsCollidedはコリジョン判定ができることを意味する

システム

さて、これらの当たり判定コンポーネントを扱うシステムを作ってみます。

//SwordCollider キャラの位置とアニメーションの状態から位置を決定、攻撃アニメ中だけactiveにする
impl SystemProcess
    for System<
        CContainer<SwordCollider>,
        (&CContainer<CharacterView>, &CContainer<CharacterAnimator>),
    >
{
    fn process(sword_colliders: &mut Self::Update, (views, animators): &Self::Refer) {
        sword_colliders
            .iter_mut()
            .zip_entity2(views, animators)
            .for_each(|(_, collider, view, animator)| {
                let dir = view.direction + view.weapon_direction;
                collider.line.a = view.position;
                collider.line.b.x = view.position.x + dir.cos() * view.radius * 1.8f32;
                collider.line.b.y = view.position.y + dir.sin() * view.radius * 1.8f32;

                collider.active = false;
                if let Some(id) = animator.playing_id() {
                    if id == CharacterAnimID::Attack {
                        collider.active = true;
                    }
                }
            });
    }
}

//BodyWeaponCollider キャラの位置と半径をそのままコピー
impl SystemProcess for System<CContainer<BodyWeaponCollider>, CContainer<CharacterView>> {
    fn process(body_weapon_colliders: &mut Self::Update, views: &Self::Refer) {
        body_weapon_colliders
            .iter_mut()
            .zip_entity(views)
            .for_each(|(_, collider, view)| {
                collider.circle.pos = view.position;
                collider.circle.radius = view.radius;
            });
    }
}

//BodyDefenseCollider 位置の更新とともに、あたり判定も行う
impl SystemProcess
    for System<
        CContainer<BodyDefenseCollider>,
        (
            &CContainer<CharacterView>,
            &CContainer<SwordCollider>,
            &CContainer<BodyWeaponCollider>,
            &CContainer<Team>,
        ),
    >
{
    fn process(
        body_defenses: &mut Self::Update,
        (character_views, sword_colliders, body_weapon_colliders, teams): &Self::Refer,
    ) {
        body_defenses
            .iter_mut()
            .zip_entity2(character_views, teams)
            .for_each(|(defense_entity_id, body_defense, view, defense_team)| {
                body_defense.hit = false;
                body_defense.circle.pos = view.position;
                body_defense.circle.radius = view.radius;

                sword_colliders.iter().zip_entity(teams).for_each(
                    |(sword_entity_id, sword_collider, sword_team)| {
                        if defense_entity_id == sword_entity_id {
                            return;
                        }
                        if defense_team.team_id() == sword_team.team_id() {
                            return;
                        }
                        if sword_collider.is_collided(body_defense) {
                            body_defense.hit = true;
                        }
                    },
                );

                body_weapon_colliders.iter().zip_entity(teams).for_each(|(weapon_entity_id, weapon_collider, weapon_team)|{
                    if defense_entity_id == weapon_entity_id {
                        return;
                    }
                    if defense_team.team_id() == weapon_team.team_id() {
                        return;
                    }
                    if weapon_collider.is_collided(body_defense) {
                        body_defense.hit = true;
                    }
                });
            });
    }
}

コライダーは、Circle, Lineなど、CharacterViewと似たようなデータになっているので、無駄に見えるかも知れませんが、実際のゲーム制作では、CharacterViewはスプライトや3Dモデルになる想定しています。それぞれのコンポーネントの目的にあったデータを持っている事に留意してください。

また、今まで ZipEntity ZipEntity2 は、イテレーション時に EntityIDを返すようにはなっていなかったのですが、今回ヒットを取るかどうかの判定に必要になったので、EntityIDも返すように修正しました。

次回は、やられアニメーションを実装してみたいと思います。

mas-yo
オンラインゲームのサーバーをC++で作っています。 Rust勉強中。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away