目次
その1 〜 序文
その2 〜 キャラの移動
その3−1 〜 コンポーネントの設計
その3−2 〜 システムの設計
その3−3 〜 メイン部分
その4−1 〜 剣を表示
【イマココ】その4−2 〜 アニメーションコンポーネント
その4-3 〜 アニメーションを動かす
その5-1~ あたり判定
その5-2~ やられアニメーション
その6 〜 これまでの振り返り
前回は、アニメーションを再生する準備のため、キャラの向きを示す線(=剣)を表示するところまで実装しました。
今回から、いよいよアニメーションの実装に入ります。
まず、アニメーションを制御するコンポーネントとしてAnimator
を作ります。
//K:アニメーションを識別するためのID(キー)
//V:1フレーム分のデータを示す型
#[derive(Default)]
pub(crate) struct Animator<K, V>
where
K: Hash + Eq,
{
playing_id: Option<K>, //現在再生中のアニメーションID 再生していないならNone
current_frame: usize, //現在何フレーム目か
animations: HashMap<K, Animation<V>>, //アニメーションデータ
}
impl<K, V> Animator<K, V>
where
K: Hash + Eq + Copy,
{
//再生
pub fn play(&mut self, animation_id: K) {
if self.animations.contains_key(&animation_id) {
self.playing_id = Some(animation_id);
self.current_frame = 0;
}
}
//アニメーションが終わっているか
pub fn is_end(&self) -> bool {
if let Some(id) = self.playing_id {
let anim = self.animations.get(&id).unwrap();
return !anim.looped && self.current_frame == anim.values.len();
}
return false;
}
//フレーム更新(毎フレーム呼ばれる想定)
pub fn update(&mut self) {
if let Some(id) = self.playing_id {
if let Some(anim) = self.animations.get(&id) {
self.current_frame += 1;
if anim.values.len() <= self.current_frame && anim.looped {
self.current_frame = 0;
}
}
}
}
//アニメーションの登録
pub fn register(&mut self, id: K, anim: Animation<V>) {
self.animations.insert(id, anim);
}
//現在のアニメーションの値
pub fn value(&self) -> Option<&V> {
let id = self.playing_id?;
let anim = self.animations.get(&id)?;
anim.values.get(self.current_frame)
}
}
//アニメーション1つ分のデータ
//T: 1フレーム分のデータを示す型
#[derive(Default)]
pub(crate) struct Animation<T> {
looped: bool, //ループアニメかどうか
values: Vec<T>, //アニメーションデータ
}
impl<T> Animation<T> {
pub fn new(looped: bool, values: Vec<T>) -> Self {
Self { looped, values }
}
}
Animator
の中には、複数のアニメーションデータが入っており、それをIDで切り替えて再生する想定です。
今回(その4)では、待機アニメーションと攻撃アニメーションを登録しておいて、デフォルトでは待機アニメ、攻撃時は攻撃アニメを再生する、ということになります。
Animator
はECSの「コンポーネント」として使います。
一般にコンポーネントは単なるデータで、機能を持たないということになっていると思いますが、コンポーネント内部だけで完結する機能は、コンポーネントのメソッドとして実装して良いと判断しました。
アニメーション1フレーム分のデータは、下記の様になります。
#[derive(Default, Clone)]
pub(crate) struct CharacterAnimFrame {
pub radius_scale: f32, //円のスケール
pub weapon_direction: f32, //武器の向き
}
この値が時間とともに変化し、実際の描画にアニメーションが適用されるわけですね。
アニメーションのキーとなる値は、enum
にしました。
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
pub(crate) enum CharacterAnimID {
Wait, //待機
Attack, //攻撃
}
今回使うAnimator
の別名も定義しておきます。
type CharacterAnimator = Animator<CharacterAnimID, CharacterAnimFrame>;
ここまでで、アニメーション周りのコンポーネントの準備は完了です。
続いて、Animator
自体を更新するシステムを実装しましょう。
impl<K, V> SystemProcess for System<CContainer<Animator<K, V>>, ()>
where
K: Hash + Eq + Copy,
{
fn process(animators: &mut Self::Update, _: &Self::Refer) {
animators.iter_mut().for_each(|(_, a)| a.update());
}
}
単純にupdate()
を呼ぶだけです。
次に、Animator
から現在の値を取り出し、CharacterView
に反映させるシステムです。
impl SystemProcess for System<CContainer<CharacterView>, CContainer<CharacterAnimator>> {
fn process(views: &mut Self::Update, animators: &Self::Refer) {
views
.iter_mut()
.zip_entity(animators)
.for_each(|(view, animator)| {
if let Some(val) = animator.value() {
view.radius_scale = val.radius_scale;
view.weapon_direction = val.weapon_direction;
}
});
}
}
radius_scale
とweapon_direction
をコピーしているだけです。
これで、アニメーションを再生する仕組みができました。
さて、アニメーションを制御するのに、もう1つ、仕組みが必要です。
それは、「どういう条件でどのアニメーションを再生するか」を決める仕組みです。
これについては、また次の投稿で、説明します。