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

## 目次

[その１ 〜 序文](https://qiita.com/mas-yo/items/e7d1e247e5d4a9f47e92)
[その２ 〜 キャラの移動](https://qiita.com/mas-yo/items/207244f04dd442cb567c)
[その３−１ 〜 コンポーネントの設計](https://qiita.com/mas-yo/items/e1f89bea5f84b7041462)
[その３−２ 〜 システムの設計](https://qiita.com/mas-yo/items/ca97fe6ed961d77be715)
[その３−３ 〜 メイン部分](https://qiita.com/mas-yo/items/9f0459dd942a874c6273)
[その４−１ 〜 剣を表示](https://qiita.com/mas-yo/items/daae411935d138d85c95)
[その４−２ 〜 アニメーションコンポーネント](https://qiita.com/mas-yo/items/678d775e4caf5e14abe7)
[その４－３ 〜 アニメーションを動かす](https://qiita.com/mas-yo/items/3db0a920cb76c75bca51)
【イマココ】[その５－１～ あたり判定](https://qiita.com/mas-yo/items/598ed52f5c08a0375ead)
[その５－２～ やられアニメーション](https://qiita.com/mas-yo/items/cf017ac1ebdd7ce25d86)
[その６ 〜 これまでの振り返り](https://qiita.com/mas-yo/items/22ead1dc226da4df54e2)


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

## 今回つくったもの

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

![attack.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227302/e9dc5cff-2aa0-1208-f752-ac7fc250b847.gif)

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

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


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

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

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

```Rust
#[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を利用しています。

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

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

## システム

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

```Rust

//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も返すように修正しました。


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