LoginSignup
7
0

More than 3 years have passed since last update.

Rust + Entity Component System で仕様変更に強いゲーム設計 その3−1 〜 コンポーネントの設計

Last updated at Posted at 2020-01-09

目次

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

前回まで

前回は、プレイヤーキャラを動かすためのコンポーネントと、それを扱うシステムを定義して、プレイヤーキャラを動かせるようにするところまでを実装しました。
ただ、扱うエンティティが1つしかなかったので、エンティティを管理する仕組みはありませんでした。

今回は、エンティティを管理する仕組みを実装し、敵キャラを表示して、動かすところまでをやりたいと思います。

今回つくったもの

game.gif

緑がプレイヤーキャラ、赤が敵キャラです。
敵キャラは、一定距離までプレイヤーが近づくと追いかけてくるようにしました。

今回つくったソースは、こちらです。

gitpodでソースを見たり動かすことができるようになっています。
詳しくは その2 を参照してください。

エンティティとコンポーネント

ECSにおけるエンティティとは、プレイヤーキャラや敵キャラなど、ゲーム内のオブジェクトを表す言葉です。
そしてコンポーネントは、細分化された1つ1つ機能を実現するためのデータの事でした。

例えば、前回作ったプレイヤーキャラのエンティティは、Input、Velocity、Position といったコンポーネントを持ちます。

今回作る敵キャラは、自動で動くため、Inputは必要ありません。その代わり今回は MoveTargetというコンポーネントを持つことにし、敵キャラはMoveTargetに向かって自動で移動する、という処理にしたいと思います。

ゲーム内にプレイヤーキャラ1体、敵キャラ1体がいた場合

  • Input 1個
  • MoveTarget 1個
  • Velocity 2個
  • Position 2個

となります。これらはそれぞれ Vec で持つことにしますが、例えばVelocityを更新しようとした場合、それがInputに紐付いている(プレイヤーキャラ)のか、MoveTargetに紐付いている(敵キャラ)のかを判別できなければなりません。

そこで、各コンポーネントには、エンティティIDを持つことにします。
IDが同じコンポーネントは、同じエンティティ、ということになります。

コンポーネントの実装

コンポーネントをどう実装するか、いろいろ検討しましたが、結局下記のようになりました。

今回の設計では、できるだけランタイムコストは払わず、「コンパイル時に決められることはコンパイル時に決める」という方針で行きます。

抽象度や汎用性は下がってしまいますが、その方が、コンパイラによるチェックの恩恵を最大限に受けられますし、思わぬランタイムエラーに悩まされる可能性も減るからです。

アプリケーションのレイヤでは、その方が良い、というのが筆者の考えです。

  • Component
pub(crate) type EntityID = u32;

pub(crate) struct Component<T> {
    entity_id: EntityID,
    inner: T,
}

impl<T> Component<T> {
    pub fn new(entity_id: EntityID, inner: T) -> Self {
        Self {
            entity_id: entity_id,
            inner: inner,
        }
    }
    pub fn entity_id(&self) -> EntityID {
        self.entity_id
    }
    pub fn inner(&self) -> &T {
        &self.inner
    }
    pub fn inner_mut(&mut self) -> &mut T {
        &mut self.inner
    }
}

Component は、内部にエンティティIDと、実際に扱うデータが入ります。
内部データにアクセスするための、 inner() inner_mut() も作りました。

Deref を使いたくなるところですが、アンチパターンらしいのでやめました。
その代わり、後述するイテレータが、直接内部データへの参照を返してくれるようになっています。

つづいて、コンポーネントをまとめて保持する、 ComponentContainer です。
名前が長いので、 CContainer というエイリアス名も作りました。

  • ComponentContainer

pub(crate) type CContainer<T> = ComponentContainer<T>;

#[derive(Default)]
pub(crate) struct ComponentContainer<T> {
    map: HashMap<EntityID, usize>,
    vec: Vec<Component<T>>,
}

impl<T> ComponentContainer<T> {
    pub fn push(&mut self, entity_id: EntityID, item: T) {
        self.vec.push(Component::<T>::new(entity_id, item));
        self.map.insert(entity_id, self.vec.len() - 1);
    }
    pub fn get(&self, entity_id: EntityID) -> Option<&T> {
        let index = self.map.get(&entity_id)?;
        Some(self.vec[*index].inner())
    }
    pub fn get_mut(&mut self, entity_id: EntityID) -> Option<&mut T> {
        let index = self.map.get(&entity_id)?;
        Some(self.vec[*index].inner_mut())
    }
    pub fn iter(&self) -> ComponentIter<T> {
        ComponentIter {
            iter: self.vec.iter(),
        }
    }
    pub fn iter_mut(&mut self) -> ComponentIterMut<T> {
        ComponentIterMut {
            iter: self.vec.iter_mut(),
        }
    }
}

ComponentContainer 内部では、実際のコンポーネントを入れる Vec と、エンティティIDをキーとしてコンポーネントを参照するための HashMap をおいてあります。
(この設計だと、コンポーネントを削除するときに面倒・・・! あとで考えます)

iter() iter_mut() は専用のイテレータ ComponentIter と ComponentIterMut を返します。
エンティティIDとコンポーネントの中身をタプルで返してくれるイテレータです。

  • ComponentIter, ComponentIterMut

pub(crate) struct ComponentIter<'a, T>
where
    T: 'a,
{
    iter: std::slice::Iter<'a, Component<T>>,
}
impl<'a, T> Iterator for ComponentIter<'a, T> {
    type Item = (EntityID, &'a T);
    fn next(&mut self) -> Option<Self::Item> {
        let next = self.iter.next()?;
        Some((next.entity_id(), next.inner()))
    }
}
impl<'a, T> ComponentIter<'a, T> {
    pub fn zip_entity<U>(self, other: &'a CContainer<U>) -> ZipEntity<'a, T, U> {
        ZipEntity {
            base: self,
            other: other,
        }
    }
    pub fn zip_entity2<U, V>(
        self,
        other1: &'a CContainer<U>,
        other2: &'a CContainer<V>,
    ) -> ZipEntity2<'a, T, U, V> {
        ZipEntity2 {
            base: self,
            other1: other1,
            other2: other2,
        }
    }
}
pub(crate) struct ComponentIterMut<'a, T>
where
    T: 'a,
{
    iter: std::slice::IterMut<'a, Component<T>>,
}
impl<'a, T> Iterator for ComponentIterMut<'a, T> {
    type Item = (EntityID, &'a mut T);
    fn next(&mut self) -> Option<Self::Item> {
        let next = self.iter.next()?;
        Some((next.entity_id(), next.inner_mut()))
    }
}
impl<'a, T> ComponentIterMut<'a, T> {
    pub fn zip_entity<U>(self, other: &'a CContainer<U>) -> ZipEntityMut<'a, T, U> {
        ZipEntityMut {
            base: self,
            other: other,
        }
    }
    pub fn zip_entity2<U, V>(
        self,
        other1: &'a CContainer<U>,
        other2: &'a CContainer<V>,
    ) -> ZipEntity2Mut<'a, T, U, V> {
        ZipEntity2Mut {
            base: self,
            other1: other1,
            other2: other2,
        }
    }
}

ComponentIter ComponentIterMut に、 zip_entity() というメソッドを作りました。
これは、他の ComponentContainer を受け取って、 ZipEntity というイテレータを返します。
ZipEntity は、同じエンティティIDを持つコンポーネントを順に返してくれるイテレータです。

同種のものとして、 ZipEntityMut, ZipEntity2, ZipEntity2Mut があります。
ZipEntity については、前述のソースコードを参照してください。

長くなってしまったので、次の投稿で、システムの設計をしていきます。

7
0
1

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