IZ*ONE remember Z 運営チームのクライアントエンジニアです。
この記事では、複数種類のイベントを短期間で開発し、
さらにそれぞれのイベントを実装したソースコードの可読性や運用コスト低下により重視した
IZ*ONE remember Zのイベント開発設計について解説いたします。
IZ*ONE remember Zでは、短期スパンで複数タイプのイベントを開催する運用を行なっております。
また、開発期間も短いという側面もあったため、それぞれのイベントの開発期間を可能な限り少なくし
複数あるイベントタイプに対して、それぞれが干渉しないような設計をする必要がありました。
- pvrイベント # 個人ランキングイベント
- tvrイベント # チームランキングイベント
- dvrイベント # 個人ランキング&リアルイベント
- prdイベント # センターキャラクター毎のランキング&リアルイベント
- mcpイベント # 専用チャレンジイベント
目次
ユーザーデータ構造体
複数存在するイベントタイプのユーザーデータは、
それぞれをモジュールとして管理することで、それぞれが干渉しない設計になります。
User # UserData Root
- |-- Event # Event UserData Root
- |-- Pvr # Ranking Event UserData Module
- |-- Mcp # Campaign Event UserData Module
- |-- Tvr # Ranking with Guild Event UserData Module
- |--...
Event構造にはイベント固有のユニークId(eventId)を所持しており、このIdはのちのAPI設計で使用します。
API設計
各種APIはイベントタイプをAPI名に含んでおり、それぞれエンドポイントが違いますが、
クライアントサイドではイベントAPIは汎用的なものも複数あり(イベントショップAPIなど)
エンドポイントをeventIdで自動解釈するように設計しております。
API Requestクラス設計
各種APIをコールする際にクライアントサイドでは、
APIのレスポンスデータとポストデータを明記する、APIRequestクラスを使用します。
例えばevent/pvr/shop/item/buy
という、エンドポイントをもつAPIをコールする場合、
EventPvrShopItemBuyRequest
というクラスを用いてコールします。
この時、エンドポイントをクラス内では明記せず、クラス名(EventPvrShopItemBuyRequest
)から自動的にAPI名を作成します。
上記の方法を用いることで、Requestクラス名とコールされるAPIが明確に外部から判断できるため、
必要なAPIRequestクラスはどれを使用すれば良いかが実装者以外でも簡単に判断することができました。
Event API Factory
上の話でもあった通り、イベント専用APIはそれぞれのタイプによってエンドポイントが違い、
例えば、pvrタイプのイベントであれば、必ずevent/pvr/*
というようになります。
しかし、pvrタイプでもgvrタイプのイベントでも使用されるようなAPIもあり(むしろほとんどが別イベントタイプでも使用されるものばかり)、
毎回イベントタイプを追加されるたびにエンドポイントを分けてクラスを作成するのは無駄な作業と判断しました。
そこで、各種イベントAPIでもコモン的なものに関しては専用のフォルダにイベントタイプの記述を抜いてクラスを作成し、
そのクラス名から現在存在する全てのイベントタイプに合わせたAPIRequestクラスをGeneratorで
スクリプトを自動生成するというメソッドをとりました。
また自動生成したスクリプトをeventIdによって、動的に判断して生成するスクリプトも必要です。
このスクリプトをAPIRequestクラスを動的に判断して生成するクラスとして、EventAPIFactory
として生成します。
- EventAPIFactory # 各種イベントリクエストクラスをeventIdを元に動的に判断してインスタンスを生成
- Event*ShopItemBuyRequest # EventShopItemBuyRequestクラスを元に生成された、全てのイベントタイプに応じたクラス
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// DON'T EDIT, SINCE THIS IS GENERATED AUTOMATICALLY. namespace API { public static class EventAPIFactory { public static T CreateRequest<T>(int eventId) where T : class, IAPIRequest, new() { var type = MasterDataAccessor.eventManager.GetType(eventId); (略) if (typeof(T).Equals(typeof(EventShopItemBuyRequest))) { switch (type) { case EnumList.EventType.Pvr: return AppliAPIManager.CreateRequest<EventPvrShopItemBuyRequest>() as T; case EnumList.EventType.Tvr: return AppliAPIManager.CreateRequest<EventTvrShopItemBuyRequest>() as T; case EnumList.EventType.Mcp: return AppliAPIManager.CreateRequest<EventMcpShopItemBuyRequest>() as T; case EnumList.EventType.Prd: return AppliAPIManager.CreateRequest<EventPrdShopItemBuyRequest>() as T; case EnumList.EventType.Dvr: return AppliAPIManager.CreateRequest<EventDvrShopItemBuyRequest>() as T; } } (略) return AppliAPIManager.CreateRequest<T>(); } } } |
各種自動生成されたスクリプトはpartial
クラスにすることで、拡張性を持たせます。
1 2 3 4 5 |
// DON'T EDIT, SINCE THIS IS GENERATED AUTOMATICALLY. namespace API { public partial class EventPvrShopItemBuyRequest : EventShopItemBuyRequest { } } |
サーバーレスポンスからユーザーデータを更新
ここで一つ問題が発生します。
それは、こちらからAPIごとにイベントタイプを判断する必要がなくなったため、
サーバーからのレスポンスデータに格納された、それぞれのイベントタイプごとのモジュールに格納された
ユーザーデータの更新の際に、どのイベントタイプのモジュールがレスポンスデータとして返ってきているかが、
外部からわからなくなっていることです。
# クライアント側
EventShopBuyRequestをeventIdと同時に送信
-> eventIdからpvrタイプと動的に判断して、EventPvrShopBuyRequestをインスタンス化する(外部からはわからない)
-> EventPvrShopBuyRequestインスタンスからevent/pvr/shop/buyというAPIをコールする
# サーバー側
event/pvr/shop/buyのロジックから、レスポンスデータを作成する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
{ "d": { "Event": { "Pvr": { "User": { "Item": [ { "id": 1, "itemId": 13002, "number": 25 } ], "Shop": { "Item": { "Counter": [ { "id": 1, "shopItemId": 2, "times": 1 } ] } } }, "eventId": 1 } }, }, } |
# クライアント側
-> サーバーからレスポンスを受け取る
-> Event以下のどのモジュールがレスポンスデータに入っているかが外から見てわからない!
そこでサーバーとクライアントの双方の約束事として、Event構造の中にeventId
をもち
そのIdからイベントタイプを割り出し、特定のモジュールを更新することにしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
namespace Logic { [Serializable] public class Event { (略) public void UpdateModule(Logic.Event.EventModule module) { switch (MasterDataAccessor.eventManager.GetType(module.eventId)) { case EnumList.EventType.Pvr: Pvr.Update(module as Logic.Event.EventPvr); break; case EnumList.EventType.Tvr: Tvr.Update(module as Logic.Event.EventTvr); break; case EnumList.EventType.Mcp: Mcp.Update(module as Logic.Event.EventMcp); break; case EnumList.EventType.Prd: Prd.Update(module as Logic.Event.EventPrd); break; case EnumList.EventType.Dvr: Dvr.Update(module as Logic.Event.EventDvr); break; } } (略) } } |
このようなAPI設計にすることで、イベントタイプ固有の処理以外で
イベントタイプについて外部からわざわざ指定する必要がなくなったため、
イベントタイプがどんなに増えても、既存のソースコードを人間の手で更新する必要がない設計になります。
イベント専用シーンをイベント毎にprefab化
IZ*ONE remember Zで開催されるイベントは例え同じイベントタイプであったとしても、
イベントトップシーンやイベントストーリーシーンは毎回シーンのレイアウトなど見た目が変わる仕様でした。
これらの対応を行うため、イベント専用シーンを全てイベント毎にprefab化しました。
prefab化をそれぞれ行うデメリットとして、何かロジックや共通画面レイアウトに調整があると
影響する全てのprefabを調整しなければならないことですが、
これらの問題はUnityのVariantPrefab機能を使用することで解決します。
- EventPvrBaseContainer
- |-- pvr1001
- |-- pvr1002
- |-- pvr1003
- |-- EventTvrBaseContainer # EventPvrBaseContainerを編集すると自動更新される
- | |-- tvr2001
- | |-- tvr2002
- | |-- tvr2003
- | |-- EventDvrBaseContainer # EventTvr/PvrBaseContainerを編集すると自動更新される
- | |-- dvr5001
- | |-- dvr5002
- | |-- dvr5003
- |-- EventPrdBaseContainer # EventPvrBaseContainerを編集すると自動更新される
- |-- prd4001
- |-- prd4002
- |-- prd4003
- EventMcpBaseContainer
- |-- mcp3001
- |-- mcp3002
- |-- mcp3003
また個別作成されたprefabは外部から呼び出す際にはこちらもAPI設計と同じように
外部からそのイベントがどのイベントタイプなのか、を判断しなくても
動的に判断してprefabを生成する設計としております。
全体設計思想について
まとめになります。
以上のようなイベント開発設計にすることで、少なくともクライアントサイドの開発は格段に成果物を早く、
そしてエンバグを可能な限り抑えて開発を行うことができました。
イベントに限った話ではありませんが、全体設計思想として、
絶対にタイプ判定を追加実装で各所に書かない
ということを念頭に、クライアントサイドでは設計しております。
ここでいうタイプとは、運用していく中でどんどん追加されるものであり、
今回の例でいうとイベントタイプがそれに当たります。
例えば、
Pvrイベントだったら、〜する
Tvrイベントだったら、〜する
というロジックを各クラスで記載した場合、
長年運用することで、
Pvrイベントでやっていることが実装者以外が見てわからない
Pvrイベントで書いたロジックをタイプ判定の追加漏れでTvrイベントには実装されていなかった
Prdだけのロジックを実装するのに、影響のないはずのPvrでエンバグが発生した
などといった自体がかなり高い確率で発生します。
上記のような事態を避けるために、IZ*ONE remember Zでのイベント設計では、
イベントタイプの判定はほぼ全て自動生成ツールによって生成されたスクリプトで判断され、
タイプ毎にロジックをクラス化して、他のイベントタイプに全く干渉せず、
後から見て、そのイベントタイプでどんなロジックになっているのかを
実装者以外が見て簡単に判断できるようにしております。
以上が、IZ*ONE remember Zで実装されているイベントの設計思想になります。