目次
その1 〜 序文
その2 〜 キャラの移動
【イマココ】その3−1 〜 コンポーネントの設計
その3−2 〜 システムの設計
その3−3 〜 メイン部分
その4−1 〜 剣を表示
その4−2 〜 アニメーションコンポーネント
その4-3 〜 アニメーションを動かす
その5-1~ あたり判定
その5-2~ やられアニメーション
その6 〜 これまでの振り返り
前回まで
前回は、プレイヤーキャラを動かすためのコンポーネントと、それを扱うシステムを定義して、プレイヤーキャラを動かせるようにするところまでを実装しました。
ただ、扱うエンティティが1つしかなかったので、エンティティを管理する仕組みはありませんでした。
今回は、エンティティを管理する仕組みを実装し、敵キャラを表示して、動かすところまでをやりたいと思います。
今回つくったもの
緑がプレイヤーキャラ、赤が敵キャラです。
敵キャラは、一定距離までプレイヤーが近づくと追いかけてくるようにしました。
今回つくったソースは、こちらです。
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
については、前述のソースコードを参照してください。
長くなってしまったので、次の投稿で、システムの設計をしていきます。