こんにちは、トイロジック2年目プログラマーのMKです。本記事では、『Warlander』の開発で実装した、汎用アイテムモデルについて振り返り、一部の実装についてご紹介したいと思います。
汎用アイテムモデルとは
まず、汎用アイテムモデル
とは何かを説明します。汎用アイテムモデルとは、アウトゲーム上でアイテムを表示する際に使用するモデルのことを呼んでいます。様々な画面でアイテムモデルを表示する必要があり、各画面で表示、そして制御の実装を組むのが無駄という話から、実装することとなりました。
実際にインゲームでモデルとして表示される装備アイテムやスキル書アイテム、モーションアイテムが存在しています。使用箇所としては、シーズンパスやショップ、リザルト報酬画面など、アウトゲームの様々な箇所で登場します。
汎用アイテムモデルの種類を大きく分けると図のようになります。
メインの枠に囲まれているものが、アイテムそのもののモデルで、サブの枠に囲まれているものが、状況に応じてアイテムの補足として表示するモデルとなります。
アクターモデルでは、デフォルトアクター
と呼ばれる初期装備のアクターに、表示させたいアイテムを装備させて表示させています。
主な実装内容
主な実装内容としては、
- 3Dモデル(アクターモデル)制御
- 2Dモデル制御
- エフェクト
- 配置情報の設定
となります。
簡単に紹介すると、3Dモデルはデフォルトアクターの制御周り。2Dモデルはアイテムに適したテクスチャの適用とスキル書とタレント石アイテムの経験値ゲージの実装。エフェクトは表示制御。配置情報の設定では、デザイナさんが設定するためのパラメータ構造の作成・適用を実装しています。
スキルゲージ
スキルゲージでは、スキル書とタレント石というアイテムを獲得した時にアニメーションさせる、経験値ゲージを実装しました。ゲージアニメーションを管理するということで、各メニュー側で、キー入力によるゲージアニメーションのスキップ処理や、ゲージアニメーションの終了待ちが必要になり、スキップ処理やスキップ可能のフラグ管理、アニメーションの終了判定などもメニュー側に渡せるよう実装しました。
また、ショップやクエスト報酬画面で必要となった、アイテムを獲得したらどこまで進むのかという予測線表示も実装し、メニュー側からどちらのモードで表示するのか切り替えられるようにしています。
クラス設計の見直し
開発後半になるにつれて、「これも汎用アイテムモデルとして表示してほしい!」という要望が増えていきました。甘い設計によりif文,Switch文でコードがぐちゃぐちゃ、バグが発生してもどこで起きているのか…。そこでクラス設計を見直すことにしました。
アイテムの種類毎にDrawer
という実装クラスを用意し、必要な機能を実装することで細分化しました。また、基本クラスのList
を用意し、自社ライブラリが用意する機能を用いて、表示するDrawer
のみインスタンス化させ、List
にPushすることで、メモリ削減もしています。クラス設計を見直すことにより、コードの可読性も高まり、バグが生じた際に原因を追いやすくなりました。また、柔軟性が高まったことにより、表示するアイテム種がさらに追加されても、問題が起きることがなくなりました。
// Drawerの基本クラス
class ADrawer
{
public:
// 汎用的な関数を仮想関数で用意
virtual ~ADrawer() = default;
virtual void Finalize() {}
virtual void Update(float delta_sec) {}
virtual UpdatePosition(const Matrix& cam_mat, const ItemModelPosInfo& pos_info) {}
virtual void Flash(float flash_sec = -1.f) {}
virtual void SetEnableFlash(bool flag) {}
// with_animは2DDrawer用
virtual void Show(bool with_anim = false) {}
virtual void Hide(bool with_anim = false) {}
virtual bool IsCreatedModel() const { return true; }
};
// アクター用Drawer
class ActorDrawer : public ADrawer
{
// 初期化時に必要な情報を保持
struct SetupInfo
{
// アイテム情報
Item item_;
// 再生するアクションコマンド
string acName_;
// 表示関連
ItemModelPosInfo posInfo_;
SceneType sceneType_;
};
public:
ActorDrawer(const SetupInfo& info)
{
Setup(info);
}
void Setup(const SetupInfo& info)
{
setupInfo_ = info;
actor_ = CreateChara();
actor_->SetAction(info.acName_);
...
}
void Show(bool with_anim) override
{
auto parts = setupInfo_.item_.GetArmorPartsType();
if (parts.isValid())
{
// リムフラッシュ
actor_->StartPartsRimFlash();
}
else
{
// 対象が無かったら全フラッシュを停止
actor_->StopPartsRimFlash();
}
// 表示
actor_->SetVisible(true);
}
void Update(float delta_sec) override
{
// カラー更新
UpdateModelColor(delta_sec);
// アクションコマンド更新
UpdateAc(delta_sec);
// パーツの表示制御更新
UpdateEnablePartsVisible();
}
...
private:
void UpdateModelColor(float delta_sec);
void UpdateAc(float delta_sec);
void UpdateEnablePartsVisible();
...
private:
SetupInfo setupInfo_;
Actor* actor_;
};
// 3Dモデル用Drawer
class ModelDrawer : public ADrawer
{
...
};
// 2Dモデル用Drawer
class Image2DDrawer : public ADrawer
{
private:
bool IsFinishGaugeAnim();
...
};
// 汎用アイテムモデルクラス
class DrawerManager
{
public:
void Show(bool with_anim)
{
for(auto& drawer : drawers_)
drawer.Show(with_anim);
}
void Hide(bool With_anim)();
void Flash();
// 2DDrawerのみ定義している処理
bool IsFinishGaugeAnim()
{
if(img2dDrawer_)
return img2dDrawer_.IsFinishGaugeAnim();
return true;
}
// メニュー側で呼ぶ
// 表示したいアイテムをPUSH
void SetupItemModel(Item item, SceneType scene)
{
// 直近で設定したアイテムモデルの一部情報保持
item_ = item;
sceneType_ = scene;
// アイテムの種類によって、Drawerの生成を分岐
Switch(item_.GetItemModelType())
{
case Actor:
{
// 初期化情報の設定
ActorDrawer::SetupInfo setup_info;
setup_info.item_ = item_;
setup_info.posInfo_ = item_.GetModelPosInfo();
setup_info.sceneType_ = sceneType_;
// ActorDrawerの生成
actorDrawer_ = ActorDrawer(setup_info);
// 表示Drawerのリストに追加
drawers_.push_back(&actorDrawer_);
}
break;
case ...
...
default
...
}
}
private:
// アイテムモデルの更新処理
void Update(float delta_sec)
{
for(auto& drawer : drawers_)
{
drawer.Update(delta_sec);
}
UpdateModelPosInfo();
...
}
// アイテムモデルの位置情報更新
void UpdateModelPosInfo()
{
for(auto& drawer : drawers_)
drawer.UpdateModelPosInfo()
}
private:
// 表示するアイテム情報
Item item_;
SceneType sceneType_;
ActorDrawer actorDrawer_;
ModelDrawer modelDrawer_;
Image2DDrawer img2dDrawer_;
...
// Drawer管理List
List<ADrawer> drawers_;
}
運用方法
ここまで実装内容やクラス構造について説明してきましたが、実際に画面上にアイテムモデルを表示する際にはこのクラスだけでは完結せず、表示したいメニュー画面での運用が必要となります。そこで、汎用アイテムモデルが実際どのように運用されているのかを説明したいと思います。
- 汎用アイテムモデルを表示したいメニュー側で
DrawerManager
を宣言します。 - 汎用アイテムモデルの2Dモデルとして表示するウィジェットは、メニュー側で生成する必要があるため、コンストラクタでウィジェットを作成します。
- 各メニューで表示したいアイテムをSetup関数で
DrawerManager
に登録し、使用したい機能をDrawerManager
から呼び出します。 - 各モデルの破棄を安全に行うために、メニューのデストラクタで、アイテムモデルの終了処理を明示的に呼んであげます。
// 表示したいメニュー
class HogeMenu
{
public:
HogeMenu()
{
// ウィジェットの生成
CreateWidget(drawerManager_.GetWidget());
}
~HogeMenu()
{
// アイテムモデルの終了処理(破棄など)
drawerManager_.Finalize()
}
void Update()
{
// アイテムモデルの設定
drawerManager_.SetupItemModel(item_, sceneType_);
// 状況に応じて、DrawerManagerの各種機能の使用
...
}
...
private:
// 汎用アイテムモデル関連
DrawerManager drawerManager_;
Item item_;
SceneType sceneType_;
...
};
といったものが一連の流れとなります。
最後に
いかがでしたでしょうか。今回は、汎用アイテムモデルの実装について、一部ご紹介させていただきました。
汎用アイテムモデルの実装を通して、先を見据えた拡張性のある設計や、ユーザビリティの高い関数・パラメータ構造の実装など、まだまだ改善しなくてはならない点がたくさんあり、今回の経験を活かして今後の開発に携わりたいと思います。