0.想定する読者
- DxLibで3Dゲームを作っている
- pmd/pmxのキャラクターでアニメーションをしたい
- アニメーションのクラス分けに困っている
- キャラクターごとにアニメーションを分けたい
1.はじめに
DxLibのアニメーションの取り扱いは結構難しいです。
個人的に、これは3Dゲームを作る上での1つの大きな課題だと思います。
メモリの負荷を抑えるために、なるべく1つのアニメーションだけアタッチしていたいし、何よりも、なるべく簡単な操作でアニメーションを切り替えたいですね。
...そういったことで半月くらい試行錯誤していましたが、自分の中でかなり納得のいくAnimatorクラスができたので、記事にまとめて共有できれば良いなと思います。
2.今回の狙い
- アニメーションの切り替えを楽にしたい!
- templateって便利だよな~というお話
3.Animatorクラスの作成
宣言と実装、共にすべてヘッダーに書いていきます。(理由は後で分かります)
#pragma once
class Animator
{
public:
/// <summary>
/// アニメーション番号順<para/>
/// </summary>
enum class PlayerAnimation : int
{
None = -1,
Wait = 0,
Walk,
Attack,
Jump,
};
private:
PlayerAnimation attachAnime; //再生中のアニメーション名
int attachAnimeIndex; //アニメーションのアタッチされた番号
float animeTimer; //アニメーションのタイマー
float animeStopTime; //アニメーションの終了タイマー
bool isLoopAnime; //ループするアニメーションか否か
public:
//コンストラクタ
Animator()
{
attachAnime = static_cast<PlayerAnimation>(-1);
attachAnimeIndex = -1;
animeStopTime = 0.0f;
animeTimer = 0.0f;
isLoopAnime = false;
}
//デストラクタ
~Animator()
{
MV1DetachAnim(modelHandle, attachAnimeIndex);
}
/// <summary>
/// アニメーションのアタッチ
/// </summary>
/// <param name="modelHandle">モデルのハンドル</param>
/// <param name="anime">animation enum</param>
/// <param name="isLoop">ループ指定</param>
void SetAnime(const int &modelHandle, const PlayerAnimation &anime, const bool &isLoop)
{
//現在再生中のアニメーションを指定されても再設定しない
if (anime == attachAnime) return;
int anim = static_cast<int>(anime);
MV1DetachAnim(modelHandle, attachAnimeIndex);
attachAnimeIndex = MV1AttachAnim(modelHandle, anim, -1, FALSE);
attachAnime = anime;
animeStopTime = MV1GetAttachAnimTotalTime(modelHandle, attachAnimeIndex);
animeTimer = 0.0f;
isLoopAnime = isLoop;
}
/// <summary>
/// 指定されたアニメーションが再生中だったら削除
/// </summary>
/// <param name="modelHandle">モデルのハンドル</param>
/// <param name="anime">animation enum</param>
void RemoveAnime(const int &modelHandle, const PlayerAnimation &anime)
{
if (attachAnimeIndex == -1) return;
if (anime != attachAnime) return;
MV1DetachAnim(modelHandle, attachAnimeIndex);
attachAnime = static_cast<PlayerAnimation>(-1);
attachAnimeIndex = -1;
animeStopTime = 0.0f;
animeTimer = 0.0f;
isLoopAnime = false;
}
/// <summary>
/// アニメーションの再生
/// </summary>
/// <param name="modelHandle">モデルのハンドル</param>
void PlayAnime(const int &modelHandle)
{
if (attachAnimeIndex == -1) return;
if (animeStopTime <= animeTimer)
{
if (isLoopAnime)
{ //ループアニメーションの場合
animeTimer = 0.0f;
} else
{
RemoveAnime(modelHandle);
}
}
MV1SetAttachAnimTime(modelHandle, attachAnimeIndex, animeTimer);
animeTimer += 0.5f;
}
const PlayerAnimation getNowAnime() const { return attachAnime; };
const bool isEndAnimation(const PlayerAnimation &anime) const
{
if (anime == attachAnime) return animeStopTime <= animeTimer;
else return false;
};
};
基本的な要素は実装できましたね。
変数宣言はコメントの通りです。
コンストラクタとデストラクタも普通ですね。
enumとしてPlayerAnimationというものを作ります。
これをアニメーションの管理番号として振るようにします。
コンストラクタのアニメーションの管理番号は-1を指定します。
そのため、PlayerAnimationにも-1の数値を持つものをNoneと名前を付けて入れておきます。
それ以降は移動、ジャンプ、攻撃、回避などの付けたいモーションの名前を追加していけば良いです。
アニメーションの再生速度を変更したい場合はPlayAnime関数の最後辺りにある、
animeTimer += 0.5f;
の右辺の値を変えてください。
クラス内の関数にはほぼ全てにモデルのハンドルを引数で貰うようにします。
モデルを作る際に使うMV1LoadModel関数の戻り値がモデルのハンドルなので、モデルを作る際は大切に保管しておきましょう。
DxLib標準の関数については以下を参照してください。
4.汎用性を高めてみよう
さて、これを更に汎用性を高めたいという魂胆です。
今はPlayerAnimationというenumにしか対応していません。
つまり、自機のアニメーションしか割り当てることができないのです。
自機だけでなく、ペットや敵やボスキャラなど、いろんなキャラクターにアニメーションをアタッチしたいときに、キャラクター1人1人に合わせたアニメーション管理クラスを作るのは手間がかかります。
そこで、templateという技術を使います。
#pragma once
template<typename T> //任意のアニメーションenum
class Animator
{
private:
T attachAnime; //再生中のアニメーション名
int attachAnimeIndex; //アニメーションのアタッチされた番号
float animeTimer; //アニメーションのタイマー
float animeStopTime; //アニメーションの終了タイマー
bool isLoopAnime; //ループするアニメーションか否か
public:
//コンストラクタ
Animator()
{
attachAnime = static_cast<T>(-1);
attachAnimeIndex = -1;
animeStopTime = 0.0f;
animeTimer = 0.0f;
isLoopAnime = false;
}
//デストラクタ
~Animator()
{
MV1DetachAnim(modelHandle, attachAnimeIndex);
}
/// <summary>
/// アニメーションのアタッチ
/// </summary>
/// <param name="modelHandle">モデルのハンドル</param>
/// <param name="anime">animation enum</param>
/// <param name="isLoop">ループ指定</param>
void SetAnime(const int &modelHandle, const T &anime, const bool &isLoop)
{
//現在再生中のアニメーションを指定されても再設定しない
if (anime == attachAnime) return;
int anim = static_cast<int>(anime);
MV1DetachAnim(modelHandle, attachAnimeIndex);
attachAnimeIndex = MV1AttachAnim(modelHandle, anim, -1, FALSE);
attachAnime = anime;
animeStopTime = MV1GetAttachAnimTotalTime(modelHandle, attachAnimeIndex);
animeTimer = 0.0f;
isLoopAnime = isLoop;
}
/// <summary>
/// 指定されたアニメーションが再生中だったら削除
/// </summary>
/// <param name="modelHandle">モデルのハンドル</param>
/// <param name="anime">animation enum</param>
void RemoveAnime(const int &modelHandle, const T &anime)
{
if (attachAnimeIndex == -1) return;
if (anime != attachAnime) return;
MV1DetachAnim(modelHandle, attachAnimeIndex);
attachAnime = static_cast<T>(-1);
attachAnimeIndex = -1;
animeStopTime = 0.0f;
animeTimer = 0.0f;
isLoopAnime = false;
}
/// <summary>
/// アニメーションの再生
/// </summary>
/// <param name="modelHandle">モデルのハンドル</param>
void PlayAnime(const int &modelHandle)
{
if (attachAnimeIndex == -1) return;
if (animeStopTime <= animeTimer)
{
if (isLoopAnime)
{ //ループアニメーションの場合
animeTimer = 0.0f;
} else
{
RemoveAnime(modelHandle);
}
}
MV1SetAttachAnimTime(modelHandle, attachAnimeIndex, animeTimer);
animeTimer += 0.5f;
}
const T getNowAnime() const { return attachAnime; };
const bool isEndAnimation(const T &anime) const
{
if (anime == attachAnime) return animeStopTime <= animeTimer;
else return false;
};
};
これでどんなアニメーションenumにも対応するようになりました。
templateクラスは宣言と実装を分けると色々と面倒なので、見づらいですがヘッダーに全てまとめちゃいました。
(上手く分けられる人は分けちゃっても良いと思います。筆者は技術不足でできませんでした...)
PlayerAnimationなどのenumは新しいヘッダーを作ってその中に入れるなどして対処します。
他にアニメーションを作りたければ、新しくenumを作ってみましょう。
ただし、先頭は必ず「None = -1」にしてください。
それ以降は自由です。
//別のヘッダーに記述する。
//ここでは、名前はCharacterAnimation.hとした。
#pragma once
/// <summary>
/// アニメーション番号順<para/>
/// </summary>
enum class PlayerAnimation : int
{
None = -1,
Wait = 0,
Walk,
Attack,
Jump,
};
enum class DogAnimation : int
{
None = -1,
Wait = 0,
Walk,
Run,
Osuwari,
Ote,
};
enum class BossAnimation : int
{
None = -1,
Wait = 0,
FireAttack,
LightningAttack,
};
このように、Templateを使えばクラスの汎用性をぐっと高めることができます。
5.ゲームメインに組み込もう
早速ゲームメインに記述していきます。
スマートポインタなど普通に使っていますが、ご了承ください。
//愚直に実行しても一切動きません、あくまでもイメージです。
#include "CharacterAnimation.h"
#include "Animator.h"
#include "Model3D.h"
#include "DxLib.h"
#include <memory>
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd)
{
// DxLibの初期化処理
if (DxLib_Init() == -1)
{
//初期化に失敗
return -1;
}
//宣言
std::unique_ptr<Model3D> model; //キャラのモデル
std::unique_ptr<Animator<PlayerAnimation>> animator; //アニメーター
//作成
model = std::make_unique<Model3D>("素材/character/Otokonoko.pmd", Model3D::CenterState::Middle); //男の子のモデルを作成
animator = std::make_unique<Animator<PlayerAnimation>>(); //PlayerAnimation列挙型を使用したアニメーターの作成
//再生アニメーションの設定
anim->SetAnime(model->getModelHandle(), PlayerAnimation::Walking, true); //歩行モーションの設定(ループ再生)
//ゲームループ
while (ProcessMessage() == 0 && ClearDrawScreen() == 0)
{
//アニメーションの再生
anim->PlayAnime(model->getModelHandle());
//モデル描画
model->Draw();
//画面更新
ScreenFlip();
//escで終了
if (CheckHitKey(KEY_INPUT_ESCAPE) || GetWindowUserCloseFlag())
{
DxLib_End();
return 0;
}
}
}
Model3Dクラスは、作成したモデルを管理するためのクラスだと思ってください。
モデルとアニメーションをそれぞれ作成して、アニメーションのアタッチをSetAnimeでしています。
そして、ゲームループ内でアニメーションの再生とモデルの描画を行っています。
「アニメーションの再生が終わったら別のアニメーションを再生したい!」という時は
isEndAnimation関数でアニメーション再生の終了フラグを取得して、trueの場合にSetAnime関数をすればOKです。
6.完成!
これでアニメーションの管理が簡単になりました。
自分のお好みでアニメーションをカスタマイズして好きなキャラクターを動かしてあげましょう。
それでは、良いDxLibライフを(^^)/