こちらは、2017年9月28日に公開された以下のドキュメントを翻訳したものになります。
Bannermen, a Classic RTS Game Using Lockstep with Photon and Unreal Engine
この寄稿は、Pathos Interactiveの共同設立者およびソフトウェア設計者であるChristoffer Anderssonによるものです。Pathos Interactiveはスウェーデンのメンダールに本社を置くゲームスタジオで、2015年に設立されました。同社のタイトルであるBannermanは、Kickstarterを使用して資金調達をおこなっています。
本ゲームについて
Bannermenは従来のRTSゲームの刷新を目指すゲームで、シングルプレイヤーキャンペーンのほか複数のマルチプレイヤーモードがあります。プレイヤーの主なタスクには基地構築、資源管理、敵軍との戦闘などがあり、さまざまな地図や任務が装備されています。このゲームには動的環境があり、この環境ではプレイヤーは複数の方法で環境とのインタラクションすることが可能です。一例として、プレイヤーは地図上の宗教スポットを管理することで、様々な自然の力を制御できるようになります。ユーザーのレベル環境に応じて、利用できる動的環境は異なります。弊社の目標は、戦略に富んださらに面白い戦闘を作ってユーザーの興味を引き、ユーザーが思いのままにゲームを操縦しているという感覚を増すことです。もし弊社のゲームにご興味がある場合には、ぜひ弊社のKickstarterをご参照ください。
完全同期を選択
Bannermenの最大の要件は、最新のネットワーキングソリューションをサポートすることでした。多くのプレイヤーは、ホスティングやポート転送、ネットワーク帯域幅の心配がないシームレスな体験を期待しています。Photonのネットワーキングモデルは、こうした要件に最適でした。帯域幅を低減しつつ、多くのユニットをサポートすることを目指していた弊社は、Bannermenに完全同期モデルを実装する選択しました。「完全同期」という言葉になじみがない方に説明しておきますが、「完全同期」とは基本的にすべてのクライアント間ですべてのフレームが同期されることを意味します。さらに、プレイヤーからの入力を送信するだけで、ネットワーク帯域幅の利用は最低限に抑えられます。
Bannermenは、大変優れたUnreal Engine 4で作成されています。唯一の問題点は、Unityとは異なりすぐに使用できる完全同期へのサポートが装備されていない点です。このため、弊社はC++ SDKを使用して完全同期を直接実装しました。
同期を維持
ゲームに完全同期を実装する際にはゲームの構造全体に影響するため、設計プロセスの手順1つ1つを考慮する必要があります。すべてのクライアント間で全フレームを同期する必要があるため、入力を同期するとともに各フレームを予測しなければなりません。ゲームのロジックが関わるすべての点において、予測が必要です。クライアントが特定のフレームの入力を受信した場合、任意の数のフレームの後にゲーム状態を同期し続ける必要があります。この処理が正常に行なわれなければ非同期という重大な問題が発生し、ゲームを同期するか、もしくは終了しなければなりません。完全同期が正しく実装されていれば、こうした問題は決して起こりません。
残念ながら非同期は様々な要因から発生しますので、開発の際には十分に注意してください。たとえば、浮動小数点や倍精度浮動小数点を使用する場合には異なるプラットフォーム間、またはコンピューター間でさえも予測が不可能な場合があります。マルチスレッドの場合には、予測がさらに困難になります。マルチスレッドのコードは、常にすべてのクライアント間で同じゲーム状態にする必要があります。
入力の評価
弊社がBannermenの開発で直面した問題の1つに、2人のプレイヤーが同じフレーム内でリソースに入力したい場合がありました。リソースの余地がプレイヤーもう1人分のみの場合には、すべてのクライアント間を予測したうえで1人のプレイヤーを選択しなければなりません。非常にまれにしか発生しない小さな問題のように思えるかもしれませんが、このような状況が発生した場合に予測が実装されていなければゲームは非同期となってしまいます。すべてのクライアントに対して常に同じ順序で入力を評価すればよいかもしれませんが、それだけでは十分ではありません。また、プレイヤーに優先順位をつけたり、不公平な優先権を与えることは避けるべきです。Bannermenでは、「リソースへの入力」というアクションを二段階に分けることにしました。まず、すべてのプレイヤーを入力の候補者として登録します。その後、プレイヤーの一意のネットワークIDにもとづく予測ランダムアルゴリズムを使用してプレイヤーを選択しました。この方法を使用すると、一定の期間で考えればすべてのプレイヤーに対して公平な機会が与えられます。ただし、プレイヤーはこうしたアルゴリズムが使用されていることは全く気づきません。
固定小数点演算
浮動小数点の問題を回避するため、弊社はBannermenで固定小数点演算用に独自の演算ライブラリを記載しました。そして固定小数点演算を使用したうえで、独自のナビゲーションメッシュ、経路探索、衝突システムなどを実装しました。弊社はUnreal Engineのメインのスレッドと並行して、別個のシミュレーションスレッドを実行しています。このスレッドは、実際のすべてのゲームロジックをシミュレーションするためのものです。こうして、弊社はゲーム状態のすべてのスレッドをUnreal Engineのメインのスレッドと同期しています。結果的にあたかも全くゲームロジックのない状態か、または非常に軽い負荷のロジックによってUnreal Engineを稼動しているかのように、大きなFPSを保ちつつ、多くのグラフィック効果を使用できるようになります。
Unreal Engineでの完全同期サポート実装を開始する際には、既存の固定小数点演算ライブラリの実装、または使用を推奨します。以下は、固定小数点演算ライブラリの使用を開始する際の非常に基本的な実装です:
#pragma once
#include "FixedPoint.generated.h"
USTRUCT()
struct FFixedPoint
{
GENERATED_USTRUCT_BODY()
FFixedPoint();
FFixedPoint(uint32 inValue);
FFixedPoint(const FFixedPoint &otherFixedPoint);
FFixedPoint& operator=(int32 intValue);
FFixedPoint& operator=(const FFixedPoint &otherFixedPoint);
FFixedPoint operator+(const FFixedPoint &otherFixedPoint) const;
FFixedPoint operator-(const FFixedPoint &otherFixedPoint) const;
FFixedPoint operator*(const FFixedPoint &otherFixedPoint) const;
FFixedPoint operator/(const FFixedPoint &otherFixedPoint) const;
static FFixedPoint createFromInt(int32 value);
static FFixedPoint createFromFloat(float value);
// use UPROPERTY so unreal engine can save the value!
UPROPERTY()
uint32 value;
static const int32 FractionBits = 8;
static const int32 FirstIntegerBitSet = 1 << FractionBits;
};
#include "YourHeader.h"
#include "FixedPoint.h"
FFixedPoint::FFixedPoint()
: value { 0 }
{
}
FFixedPoint::FFixedPoint(uint32 inValue)
: value { inValue }
{
}
FFixedPoint::FFixedPoint(const FFixedPoint &otherFixedPoint)
{
value = otherFixedPoint.value;
}
FFixedPoint& FFixedPoint::operator=(const FFixedPoint &otherFixedPoint)
{
value = otherFixedPoint.value;
return *this;
}
FFixedPoint& FFixedPoint::operator=(int32 intValue)
{
value = intValue << FractionBits;
return *this;
}
FFixedPoint FFixedPoint::operator+(const FFixedPoint &otherFixedPoint) const
{
int32 result = value + otherFixedPoint.value;
return FFixedPoint(result);
}
FFixedPoint FFixedPoint::operator-(const FFixedPoint &otherFixedPoint) const
{
int32 result = value - otherFixedPoint.value;
return FFixedPoint(result);
}
FFixedPoint FFixedPoint::operator*(const FFixedPoint &otherFixedPoint) const
{
int32 result = value * otherFixedPoint.value;
// rounding of last bit, can be removed for performance
result = result + ((result & 1 << (FractionBits - 1)) << 1);
result = result >> FractionBits;
return FFixedPoint(result);
}
FFixedPoint FFixedPoint::operator/(const FFixedPoint &otherFixedPoint) const
{
int32 result = (value << FractionBits) / otherFixedPoint.value;
return FFixedPoint(result);
}
FFixedPoint FFixedPoint::createFromInt(int32 value)
{
int32 newValue = value << FractionBits;
return FFixedPoint(newValue);
}
FFixedPoint FFixedPoint::createFromFloat(float value)
{
int32 newValue = value * FirstIntegerBitSet;
return FFixedPoint(newValue);
}
重要な箇所は、乗算と除算です。一般的に2つの24.8固定小数点を掛け合わせると(すなわち整数部が24ビット、小数部が8ビット)、結果は48.16のように思えます。結果を24.8の形式にしたい場合には、オーバーフローのように整数部の上部を単純に無視します。そして、この場合には右に8ビットシフトし、結果的に小数部の最下部8ビットを切り捨てます。興味のある方は、こちらの記事に詳細が記載されていますのでご覧ください。
https://www.codeproject.com/Articles/37636/Fixed-Point-Class または
http://x86asm.net/articles/fixed-point-arithmetic-and-tricks/
浮動小数点演算による成果物をゲームロジックに使用することは推奨しません。しかし、エディタから値を開始してから保存すると便利です。この方法を利用すればより多くの演算子や関数を実装でき、また設計図への反映も容易になります(Unreal Engineのビジュアルスクリプト)。
最後に説明するのは、デバッグの際に理解しやすいフォーマットで固定小数点数の値を参照する方法です。「natvis」ファイルを利用して、値を浮動小数点数または倍精度浮動小数点数に視覚化することで処理しやすくする方法を推奨します。「natvis」ファイルが無い場合、「natvis」ファイルが有る場合の例を以下に示します。
“natvis”ファイルなし:
“natvis”ファイルあり:
“natvis” ファイル:
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
<Type Name="FFixedPoint">
<DisplayString>{{{(double)value / FirstIntegerBitSet}}}</DisplayString>
</Type>
</AutoVisualizer>
Visual Studio 2017では、「natvis」ファイルの置き場所は“C:\Users\<username>\Documents\Visual Studio 2017\Visualizers”などです。この置き場所は、ユーザーのセットアップによって異なります。
結論として、Unreal Engineで完全同期を実装するには多くの作業が必要でした。しかし、一旦完了してしまえば、Photonの強みをすべて活用することができます。さらに、Unreal Engineのみで実行していた初期のプロトタイプに比べ、完全同期を実装した設計によってパフォーマンスが飛躍的に向上しました。ネットワーク帯域幅は劇的に低減し、また弊社のシミュレーションスレッドがUnreal Engineのメインスレッドと並行して作動しているため、より高度な作業にリソースを割くことが可能になりました。結果的に、完全同期モデルと組み合わせたPhotonインテグレーションはBannermenとUnreal Engineに最適だとわかりました。
ぜひ、上記の実装例を活用してください!
Christoffer Andersson
Pathos Interactive 共同設立者およびソフトウェア設計者
コメント
0件のコメント
サインインしてコメントを残してください。