こんにちは、ソラニと申します。
星々の幻境でフォーリンバトラーズというマルチ対戦ゲームを作っています。
一見シンプルに見えるゲームですが…実は、サーバー負荷対策をめちゃくちゃ頑張って作っています!
というのも、実は星々の幻境ではサーバーの負荷制限が非常に厳しく設定されています。
ちょっと入り組んだ実装をするとすぐ上限に達してしまい、サーバー側からステージを強制終了されてしまう事がしばしばあります。
![]()
幻境のサーバーを借りてゲームを動かすので、無尽蔵にサーバーのリソースを使われては困るためこのような制限が敷かれているのだと思いますが…普通に作っているだけで上限に達してしまうこともあるので、結構困ってしまうこともあります。
![]()
今回は幻境の制作におけるサーバー負荷対策の手法を、私が実際に使った例を交えながら解説していきます。
![]()
負荷の主な原因について
負荷対策を行うには、まず負荷の原因について知る必要があります。
サーバー負荷とクライアント負荷について
まず、負荷の種類には大きくわけてサーバー負荷とクライアント負荷の2種類があります。
サーバー負荷
サーバー負荷とは、星々の幻境が動作しているクラウドサーバー側の負荷のことです。
基本的にサーバーノードグラフで実装したものは全てサーバー負荷になると考えてもらって大丈夫です。
HoYoVerse側で何らかの基準で上限が設定されており、その上限を超えるとサーバー側からステージが強制終了されてしまいます。
今回はこちらの対策をメインに解説していきます。
クライアント負荷
クライアント負荷とは、幻境を遊ぶプレイヤー側の端末(PC、スマホ、PS5等…)側の負荷のことです。
クライアント負荷は主にオブジェクトの数やエフェクトの数によって上がっていきます。
特にエフェクトや創造物は負荷が高く、密集して配置するとすぐに警告が表示されます。
また、意外と環境設定の星空の負荷も高いので注意が必要です。
クライアント負荷が高いと、スペックが低い端末で処理落ちが発生したりします。
また、予想される負荷に応じて端末によってはプレイ前に警告表示が出ることもあります。
サーバー負荷と違ってステージが強制終了されることはない(端末によっては負荷で原神本体ごと落ちるようなことはあるかも)ので、サーバー負荷と比べてそこまで神経質になることは無いかなと思いますが、幅広いプレイヤーに快適にプレイしてもらうにはなるべく低めに抑えておきたいです。
負荷の原因を調べる方法
ここでは負荷の原因について調べる方法を解説します。
負荷検出機能について
幻境エディターには負荷検出の機能が備わっています。
星々の箱庭からウィンドウ->負荷検出を開き、「負荷テストを開始」にチェックを入れてテストプレイを行うと、負荷検出を行うことができます。

負荷検出の利用方法
…ですが、この機能はサーバー負荷の検出には何の役にも立ちません!!
![]()
というのも、この機能ではサーバー負荷の原因となっているノードグラフを特定することはできません。

サーバー負荷の検出には役に立たない
負荷上限に達して強制終了された時にも、このようなログが表示されるだけでどのノードグラフの負荷が高いかを知ることはできません。
一方、クライアント側の負荷についてはこのように詳細に表示されます。クライアント側の負荷の原因を特定したい場合には結構役立つと思います。

クライアント側の負荷詳細の表示
ノードグラフの負荷を検査する方法
サーバー側の負荷の詳細を調べるには、「日誌」の機能を使う必要があります。
星々の箱庭からウィンドウ->日誌を開き、右上の「ノードグラフ稼働時の負荷を検査」にチェックを入れます。
そして、左側の一覧から負荷の詳細を確認したいノードグラフを選択し、チェックを入れて「決定」を押します。

ノードグラフの負荷検出について
この状態でテストプレイを開始すると、選択したノードグラフが稼働した際にログが記録され、負荷の詳細が確認できるようになります。

各ノードごとの負荷数値を見ることができる
各ノードでどれくらいの負荷が掛かっているかが数値化され見ることができるため、どこの処理が重いかを調べることができます。
負荷検査の注意点について
負荷検査をオンにすると、通常のテストプレイよりも負荷が大幅に高まります!
そのため、負荷上限に近い状態で負荷検査を行おうとするとすぐに強制終了してしまい、有効な検査結果が得られないということもよくあります。
負荷検査を行う際は以下のような点に気をつけましょう。
- 負荷検出の対象となるノードグラフの数を最小限にする
- 負荷の高そうな処理だけを予め単体のノードグラフに抽出しておく
こういった事情があるため、検出を行う前からある程度原因が特定できていないとそもそも役に立たない事が多いです…。![]()
サーバー負荷の主な原因について
サーバー負荷において、特に強制終了に繋がりやすい注意すべきポイントを解説します。
オブジェクト生成はめちゃくちゃ重い
恐らく最も強制終了の原因に繋がりやすいのが、ノードグラフによるオブジェクト生成です。具体的に言うと、一般的な計算系のノードよりも50~100倍くらい処理が重いです。
![]()
短時間に大量のオブジェクトを生成すると簡単に処理上限に達してしまいます。

プレハブ作成は50~200程度と、通常のノードに比べて負荷が極めて高い
ちなみに、オブジェクトの削除についても生成と同じくらい重いです。
ループ処理は重い
意外なのが、ループ処理はそれ自体が結構重いです。
ループ内の処理内容にかかわらず、ループを行う事自体が負荷に繋がります。同じ処理内容でも、ループを使わずにノードを複製して繋げたほうが負荷が小さくなったりします。

ループ本体自体に処理負荷がある
ちなみに、ループ処理には負荷上限とは別にループ回数に対して暗黙的に制限が掛けられています。
![]()
負荷上限に達していなくても、一定回数以上のループが行われると処理が強制的に中断されるようになっています。
その場合、ゲームは強制終了されずログにも特にエラーが表示されないため、一見すると原因が非常に分かり辛いです…。
なぜかループの途中で処理が中断されてしまう場合は、この仕様を疑ってみましょう。
ステージ開始直後は結構重い
意外と盲点なのが、ステージ開始直後は結構重いです。
負荷検出で見ることはできないですが、ステージ開始時にはステージに配置されているオブジェクトやプレイヤーが一斉に生成されることになるため、サーバーにかなりの負荷が掛かっています。
それに加えてノードグラフで重たい処理を行ってしまうと、開始直後に落ちてしまう事もあります。
その他負荷が高めな処理について
上記に上げた内容の次点として、辞書やリスト、構造体の操作などは通常の計算ノードよりも少しだけ負荷が高いです(おおよそ5~10倍程度)。
とはいえ、これらが強制終了に繋がるほどの負荷を生み出すことは稀だと思うので、そこまで気にしなくても大丈夫だと思います。(むしろこれらを使わず冗長な実装をした方がかえって負荷が高まる可能性もあるため)
テストプレイの時間帯によっても負荷上限は変わる
厄介な点として、実はテストプレイの時間帯によっても負荷上限は結構変わります。
10回以上試して大丈夫だった処理が、別の時間で試してみると8割以上の確率で落ちるようになっていたなんてことすらあります。
この仕様のため、負荷対策をしたとしても本当にどのタイミングでプレイしても大丈夫かどうかというのは確証が持ち辛いです…。
具体的な数値の目安も無いため、雰囲気で対策を行うしか無いというのが現状です。
![]()
できれば異なる時間帯で何度かテストプレイしてみることをオススメします。
負荷対策のテクニック
それでは、ここからは負荷対策を行うための実践的なテクニックを紹介していきます。
重たい処理はタイマーを使って小分けにする
最も汎用的かつ効果的なのは、重たい処理をタイマーを使って分割して処理するという事です。
重たい処理というとほとんどの場合はオブジェクト生成処理だと思いますが、大量のオブジェクトを生成するような時はタイマーを使って小分けにし、負荷を分散しましょう。
例えば100個のオブジェクトを生成するような時は、0.1秒ごとに5個ずつ生成するのを20回繰り返す…といったようなイメージです。
フォーリンバトラーズでの具体例
フォーリンバトラーズではゲーム開始時にステージの足場を生成しており、最大で12x12=144個ものオブジェクトを生成する必要がありました。
これを一気に生成しようとすると当然負荷上限に達してしまうため、0.1秒ごとに5個ずつ足場を生成し、完了時に生成完了の信号を送るようにしました。

ステージ生成を行うノードグラフ
生成完了した数をノードグラフ変数に記録しておくことで、次回のタイマー起動時に続きから生成できるようになっています。
実装のポイントとして、タイマーはループせずに処理が完了してから都度次のタイマーを起動するのがオススメです。
タイマーをループで起動してしまうと、生成処理が終わる前に次の生成処理が始まってしまう場合があります。これではせっかくタイマーを使って処理を分散した意味が無くなってしまいます。
また、負荷対策とは直接関係無いですが、処理に時間がかかる場合は進捗バーによるUI表示をつけることもオススメします。
何も表示されずに待たされているとプレイヤーからすると不安になってしまうため、適切に情報を表示してあげることはユーザー体験の向上に繋がります。

ステージ生成中は進捗バーを表示する
ステージ開始時の重い処理を避ける
先程解説した通り、ステージ開始直後はサーバー負荷が高い状態です。
そのため、重い処理を行う場合はステージ開始から2~3秒経ってから開始するのがオススメです。
フォーリンバトラーズでの具体例
フォーリンバトラーズでは、ステージの生成処理をステージ開始から2.5秒後に始めるようにしています。

エンティティ作成時にタイマーを起動
なるべく早く開始したほうがその分待ち時間も少なくなりますが、自分が何度か試していた感じだと2秒未満だと時間帯によっては負荷上限に達してしまう事がありました。
恐らくステージに初期配置されているオブジェクトの量などにもよりますが、余裕を持って2~3秒程度待ってから開始する事をオススメします。
ループ処理のループ回数を最小限にする
ループ処理を行う場合は、ループの回数がなるべく少なくなるようなアルゴリズムを考えてみましょう。
といってもこればかりは作りたいものにもよるので、必ずしも可能だとは限らないですが…
自分の作ったゲームでの実装を例に解説してみます。
フォーリンバトラーズでの具体例
フォーリンバトラーズでは、攻撃時にキャラクターの前方一列にある足場を破壊するという処理があります。
これを実装するのには実は結構苦労しました。
最初に考えていた方法は、攻撃時に足場オブジェクト全てを走査し、キャラの位置と向きから破壊対象になるかどうかを判定するというものでした。
しかし、この場合足場オブジェクトの数だけ毎回ループ処理を行うことになります。デフォルトの8x8サイズでも64個のオブジェクトに対してループ処理を行うことになるため、簡単に負荷上限に到達してしまいました。
![]()
そこで、ステージ開始時に予め足場オブジェクトと座標の関係を辞書に登録しておき、攻撃時のプレイヤー座標と向きから破壊対象となるオブジェクトを必要な数だけ取り出すような実装に変更しました。
まず、ステージのサイズが決まっていれば、以下のように2次元の座標を整数のIDに変換することができます。
これによってキーを座標情報に、値を足場オブジェクトのエンティティにした辞書を作成しておきます。

座標と辞書キー用のIDの対応関係
次に、プレイヤーの位置から足場座標を計算することを考えます。
これは原点の座標と足場のサイズが分かっていれば計算で求めることができます。

プレイヤー座標の求め方
次に、プレイヤーの向きから破壊対象を決めます。
プレイヤーの向きは単位ベクトルで表し、xの正方向なら(1,0)、zの負方向なら(0,-1)というふうに表すようにします。
プレイヤーの向きは「エンティティの回転を取得」で取得することができ、3Dベクトルとして表現されています。
この3DベクトルのYがプレイヤーの向きを0~360の角度で表しています。これを極座標の変換の要領で方向ベクトルに変換することができます。

回転から方向ベクトルに変換する処理
プレイヤー座標と向きが取得できれば、破壊対象は簡単なループ処理で取得することができます。
プレイヤー座標に対して向きを加算していき、ステージの端に到達するまで処理を行えば良いだけです。

最小限のループ処理で破壊対象のオブジェクトを取得
これによって、ループの回数を64回から最大7回までに減らすことができ、負荷を大幅に減らす事ができました。
![]()
このようなアルゴリズムの実装はあくまで一例に過ぎませんが、効率の良いアルゴリズムの実装のためのヒントになれば幸いです。

