OpenXOPS技術解説書

OpenXOPS Technical Document
最終更新:2022/10/12


はじめにお読みください

このドキュメントは、OpenXOPSのソースコードを参照する方々のために用意された、ソースコードの解読を補助するための説明文書(参考資料)です。

最低限、以下のような知識がある前提で解説します。
 ・C言語に関する一通りの知識
 ・C++に関する基礎的な知識(クラス記述など)
 ・XOPSに関する知識(マップ・ミッションが作れる程度)

所々で「デザインパターン」という表現が出てきますが、そもそも書いている人間・[−_−;](みかん)が正しく理解している自信がありません。デザインパターンを完全に把握していなくても読んで理解することは可能です。解釈が間違えていたら教えてください。

このドキュメント上で表記される関数は、引数表記や戻り値を省略しています。各関数やクラスの使い方は、別途ドキュメントやソースコードを参照してください。

ドキュメント内の全ての画像は、クリックすると拡大されます。

このドキュメントは基本的に無保証です。参考資料として自己責任で利用してください。これらを利用したことにより何かしらの損失が発生しても、一切の責任を負いません。また、改良により仕様が変更された際、ドキュメントが更新されず情報が古くなっている可能性があることにも留意してください。

なお、このドキュメントは事前予告なく随時変更・編集されます。原則として古い情報に辿ることはできないため、必要に応じてバックアップ(保存)してください。本ドキュメントを引用する場合、最上部にある更新日時も合わせて記載されることをお勧めします。

このドキュメントの著作権は [−_−;](みかん) にあります。


コード記述、命名規則

座標の取り扱い

3Dの座標は、ゲーム内のシステム(計算や描画)は、全て左手系座標です。殆どが X・Y・Z の順で記述され、XZ平面が水平、Y軸が高さです。回転角は球面座標系の考え方に近く、2次元として横軸(水平)・縦軸(垂直)として扱っており、それぞれ横軸:rx 縦軸:ryとして扱っています。
2D座標は、Windows系の2D描画と同様に、左上が原点としX軸・Y軸で表されます。

ゲーム内における3D空間の基本スケール(倍率)は、公式のX operations TOOLS、非公式のXPE+やXopsAddonCreatorでの表示座標と同様に 1.0倍 です。(nine-two氏いわく、本家XOPSでは 1/10 のスケールを使用しているようです。)

コーディング規則

字下げスタイルは、原則としてK&Rのスタイルですが、ブロックの最後の中括弧「}」の後ろは必ず改行する、ストロヴストルップ・スタイルです。
C/C++では、関数やif/else・do/whileが1行に収まる場合 中括弧は不要ですが、記述を明確にするためあえて中括弧を使っています。よって以下のような記述がゴロゴロ出てきます。
 if( x == 0 ){ a = 1; }
 else{ a = -1; }
 if( y > 0 ){ b = 0; }

各命名には原則として英単語を使用していますが、英単語の使い方が誤っている可能性があります。余りにも酷い場合は正しい英語をご指摘ください。

関数名には、原則として 動詞+名詞 の順で組み合わせて命名しています。
動詞には概ね以下のような単語を使っています。使い分けは適当で深い意味はありません。
処理内容命名
初期化Create・Init
読み込むLoad
メインの処理Process・RunFrame
描画処理Draw・Render
解放Destroy・Cleanup
また、オブジェクトや値を取得する関数には Get〜 、設定する関数には Set〜 と命名されています。

関数のプロトタイプ宣言は、変数名も含めて記述しています。

変数宣言は、各関数の上部にまとめて書く傾向があります。(例外あり)
整数値は、1byteで収まる範囲であっても可読性を優先しint型を用いています。有効・無効に代表されるような 0・1 しか取り扱わない場合、bool型とfalse・tureを積極的に使っています。少数値は全て float 型に統一し、値の後ろには全て f を付けています。(例:3.14f)

条件文の判定は、勘違いを防ぐため明確化し省略していません。丁寧に「!= NULL」や「!= 0」と書いています。

プリプロセッサ(特に #if)を活用し、ヘッダーファイルの多重インクルードを防止しているほか、レイヤーごとにインクルードを制限しています。 (レイヤーについては「レイヤー分類」を参照)

ソースコード内のコメントは、一部情報が古い(プログラムの変更が反映されていない)場合や、根本的に誤っている場合も考えられますので注意してください。また、記述上「クラス」と「オブジェクト」の記載を混同している箇所もあります。
プログラム記述を中心に確認し、コメントは参考程度に留めてください。

本家XOPSとのプログラムの違い

本家XOPSとOpenXOPSは、実行時の動作とファイルシステムなどはもちろん、addonなどの各種データも高い互換性の実現を目指していますが、内部のアルゴリズム・データ構造が完全に一致している保証はありません。
すなわち、本家XOPSのプログラムを開発された nine-two氏 と、全く同じアルゴリズム・データ構造であるとは限りません。


コンフィグレーション

OpenXOPSには、ゲームの動作や仕様を変更するための様々なコンフィグレーションオプションが備わっています。本項では、OpenXOPSをコンパイルする際に開発者によって決定される、コンパイル時の設定について述べます。
(なお、各操作のキー割り当てのようなユーザー(ゲームプレイヤー)が自ら設定できるオプションについては、後述の「ファイル制御」を参照してください。)

OpenXOPSコンパイル時のコンフィグレーションは、全て #define 構文で設定されており、ソースコード以外の外部ファイルでは設定されていません。また、単一のファイルでは管理されておらず、各ソースコードのヘッダーファイルに分散しています。

例えば、グラフィックのレンダラの選択(DirectX または OpenGL)は d3dgraphics.h ファイル内、ウインドウ解像度は main.h ファイル内、ゲーム内に表記されるバージョン情報は main.h 内で定義されています。必要に応じてヘッダーファイル内を検索し、目的の設定項目を探してください。


レイヤー分類

OpenXOPSの設計構造を読み解く上で、最も最初に理解するべきはレイヤー分類です。ゲームをプレイする上ではレイヤーを意識することはありませんが、OpenXOPSのソースコードを読んだり手を加える場合、このレイヤーの階層を一度理解したほうが、全体の構成を理解しやすいと思います。

OpenXOPSは、各クラスなどゲームを構成する部分を大きく3段階に分けて、設計し開発されています。構造上の「階層」と解釈しても構いません。レイヤーの分類は、概ね以下の通りです。


▲各クラスのレイヤー分類

ただし、ソフトウェア工学で言う「多層アーキテクチャ」ではないため、高レイヤーが(中間レイヤーを経由せず)直接低レイヤーを呼び出す場合もあります。

OpenXOPSのソースコードに触れる最初のうちは、レイヤー分類を意識したほうが解読しやすいと思います。一方で読み慣れてしまえば、前述の通りヘッダーファイルの多重インクルード制限の確認や、後述するOpenXOPSを他のプラットフォームに移植する際の目安程度にしか使わないかもしれません。


状態遷移

ゲームの各画面を‘状態’(ステート)として管理しています。

次のような画面・状態があります。
 ・オープニング画面
 ・メニュー画面
 ・ブリーフィング画面
 ・メイン画面
 ・結果表示(リザルト)画面
加えて、各状態は「初期化・準備」「実行中」「終了・廃棄」に分けられます。

「初期化・準備」と「終了・廃棄」は、それぞれ状態開始・終了時に1度だけ呼び出されます。「実行中」は、その状態の間は連続して呼び出されます。

全体の状態遷移を図に表すと、以下のようになります。一見複雑に見えますが、ゲームの画面遷移を思い出しながら辿れば難しくありません。


▲メインゲーム画面でのデバック表示

本家XOPSをやり込んでいるユーザーや、図を読み込んだ方ならばお気づきの通り、どの画面状態でも[Esc]キーを押せばメニュー画面になります。ゲームを終了する場合、一度メニュー画面に移行してから、再度[Esc]キーを押す必要があります。
ただし、ウインドウ右上の[×]を押すなど強制的にプログラムを停止した場合は、この限りではありません。ゲーム終了前に常にメニュー画面が表示されるとは限らないので注意が必要です。

状態遷移は StateMachineクラス で管理されます。
ゲーム起動時は STATE_CREATE_OPENING (オープニングの初期化・準備)に設定されています。各画面状態の時にメンバー関数を実行することで、自動的に適した状態に遷移します。
ただし、初期化や終了処理が完了した時点で自前で同クラスのメンバー関数を呼び出し、次の状態へ移行しなくてはいけません。初期化や終了処理が完了しても同クラスのメンバー関数を呼び忘れると、半永久的に初期化・終了処理が呼ばれ続けます。
オブジェクトは1個だけ作成する前提で設計しています。オブジェクトを複数作成しないでください。


画面関係

オープニング、メニュー、ブリーフィング、メインゲーム、リザルト(結果表示)の各画面は、3D/2D描画とサウンドを含めた構成と2D描画のみに限定された構成に分けることができます。
(3D/2D描画+サウンド=オープニング・メニュー・メインゲーム、2D描画のみ=ブリーフィング・リザルト)
両者の画面構成は明確に区別しており、それぞれ D3Dsceneクラス と D2Dsceneクラス を継承して作られます。

デザインパターンで表現すれば TemplateMethod パターン です。

初期化処理は Create()関数 に、解放処理は Destroy() に記述します。
毎フレームごとに Input()関数・Process()関数・Render3D()関数・Render2D()関数が呼び出されますが、別途オプション設定で‘フレームスキップ’が有効な場合、 Render3D()関数 と Render2D()関数 は 2フレームごとに呼び出されます。
前述の4つの関数の使い分けは、以下の通りです。
関数名用途
Input()関数入力処理をします
Process()関数フレーム内のメインの処理をします
Sound()関数サウンド関係の処理をします
Render3D()関数主に3D関係の描画を行います (D3Dsceneクラスのみ)
Render2D()関数主に2D関係の描画を行います
上記の使い分けは、厳守しなくてもコンパイルおよび実行できますが、ソースコードの可読性を維持するため処理を分けて記述することを強く推奨します。

OpenXOPSでは、毎フレームを呼び出す最小間隔は30.0msです。よってフレームレートは約33.33fps(Frames Per Second)です。
(武器のリロード時間や連射間隔、手榴弾の起爆時間、AIの時間待ち管理など)ゲーム内で時間・タイミングを管理する処理は、ほぼ全てフレーム数を数えて対応しています。人が移動・ジャンプする速度計算も約33.33fpsを前提に調整しています。
理論上は呼び出し間隔を16.0msにすることで62.5fpsを実現できますが、それら処理を全て書き換える必要があるため、fps変更はお勧めしません。

メインゲーム画面では開発用にデバック表示を行うことができ、標準でいくつかのパラメーターが表示されます。


▲メインゲーム画面でのデバック表示例

表示される各項目は以下の通りです。
frameゲーム(ミッション)開始時からのフレーム数です。
timeframeから換算されるプレイ時間です。 これは人間が認識しやすいように表示しているだけであり、OpenXOPS内部では(分・秒単位は)使われていません。
cameraカメラ(視点)の座標や向きです。
human[x]プレイヤーが操作する人自体の座標・向き・HPです。 []内の数字はデータ番号で、データ上で「何番目の人か」を表します。裏技で人を変えると、[]内の番号も変わることが確認できます。
Input・Object
・AI・Event
・Render
順に 入力処理・オブジェクト処理・AI処理・イベント処理・描画処理 に費やした計算時間(ミリ秒)です。ただし、PCによっては10ms刻みになるなど正確とは言えず、目安程度にしか使えません。


グラフィック

グラフィックス処理は、ゲームの3D描画と2D描画を行う部分であり、視覚的な影響が大きい部分です。

本家XOPSでは DirectX8.1 が使用されていますが、OpenXOPSでは DirectX9.0c と OpenGL1.1 を使用しています。(コンパイル時に d3dgraphics.h の GRAPHIC_ENGINE 定数で選択します。)
描画パイプラインは固定パイプラインを使用していますが、DirectX版のみオプションでシェーダーも使用できます。当然、一般的な‘ゲームエンジン’の類は一切使用していません。

3D描画と2D描画は、関数などが明確に分けられているため、使い分ける必要があります。また3D描画においても、ブロックデータ(.bd1)とモデルデータ(.x)も区別しています。

ブロックデータは、頂点データに加えて設定されたテクスチャも一括で読み込みます。
逆にモデルデータは、データに含まれるテクスチャファイル指定を無視し頂点データのみ読み込みます。よって、モデルデータでは頂点データとテクスチャをそれぞれ分けて読み込む必要があります。
またモデルデータを描画する際には、頂点データとテクスチャ(1枚)を別々に組み合わせて指定する必要があります。すなわち、モデルデータ1個に対して、テクスチャは1枚しか使えません。

仕様上、モデルデータやテクスチャを読み込める最大数が設定されています。d3dgraphics.h の MAX_MODEL 定数と MAX_TEXTURE 定数です。
読み込める最大数に達すると、それ以上読み込みを試みてもエラーになります。必要に応じて定数を変更して対応してください。

OpenXOPSでは、一般的なアニメーション・モーション再生技術を使用していません。本家XOPSと同じく人の歩きや走りのモーションは、静止したモデルデータを高速に切り替えることで実現しています。(古典的なパラパラマンガをイメージしてください。)
足のモーション再生を滑らかに行うため、モーフィングを実施しています。モデルデータの各頂点座標に対して単純な線形補間で中間データを生成し、予め用意されたデータと比べて倍のフレーム数を用意しています。

OpenXOPSでは、モデルデータに対して LOD(Level Of Detail)に相当する技術は使用していません。遠くのオブジェクトをローポリゴンで描画する処理は実装されていないため、描画範囲であればカメラの距離に関わらず、常に同じクオリティでポリゴンを描画します。(本家XOPSも同様です。)

OpenXOPSの全てのエフェクトは、四角形状のポリゴンにテクスチャを貼り、ビルボードとして表示しています。
パーティクル(particle)ではありません。 (エフェクトの使用場面については「オブジェクトの関係と制御」を参照)

描画の際にアルファテストを利用することによって、完全透明なアルファ値を持つテクスチャを利用した場合、そのピクセルは描画がスキップされます。これは本家XOPSの 0.97ft にあたる機能です。

本家XOPSのオプション設定にある BRIGHTNESS(画面の明るさ) は、画面全体に半透明の板を貼ることで実現しています。設定に合わせ、白い半透明の板を描画することで疑似的に画面を明るくしています。'0'では板を描画せずに原色のまま表示します。 (おそらく本家XOPSも同様の処理を実施しています。)

フォグは、本家XOPSの0.975tで採用されたピクセルフォグです。
テクスチャフィルタリングは、ミップマップを使用しています。ただし、テクスチャフォントデータについては、ミップマップレベルを制限して可視性を上げています。

基本的な使い方は、起動時に InitD3D()関数 で初期化した後、毎フレームごとに
 ・StartRender()
 ・ 〜様々な描画関数の実行〜
 ・ScreenBrightness()
 ・EndRender()
の手順通り行うだけです。
オブジェクトは1個だけ作成する前提で設計しています。オブジェクトを複数作成しないでください。

DirectX9.0c 特有の情報

D3DXを利用していますが、DXUT(DirectX ユーティリティ ライブラリ)は使用していません。

2D描画はある程度の描画処理速度を実現するため、ID3DXSpriteインターフェース を利用は極力避けており、可能な限り3D上に直接ポリゴンを配置して、擬似的に2Dに見せる手法で処理しています。 ※一部例外あり
ただし、全体的に3D描画を前提に特化しているため、2D描画を行う際はその都度(描画関数ごとに)2D描画用設定を行い実際に2D描画を行ってから、3D描画設定に戻しています。理論上、2D描画は高速な処理とは言えません。

ブロックデータを格納するメモリー先として、GPUメモリー(VRAM)とメインメモリーが選択できます。
OpenXOPSはゲーム中にマップが一切変形しないため、GPUメモリーを利用することで高速化を図っています。一方で、ゲームプレイ中にマップを変形させる場合は、メインメモリーを使用した方が効率的です。
変更は、ソースコード内の定数で行えます。

OpenGL1.1 特有の情報

WindowsAPIに同梱されているヘッダーファイルを用いています。DirectXと異なり別途SDKは必要ありません。

極力、補助・拡張ライブラリは避ける方針の元、設計しています。GLU(OpenGL Utility Library)を使用していますが、GLUT(OpenGL Utility Toolkit)は一切利用していません。
テクスチャファイルの読み込み処理は、以下の汎用ライブラリを使用する想定です。
 ・libjpeg 9a
 ・zlib 1.2.8
 ・libpng 1.6.16

モデルファイルやテクスチャの読み込み処理は、独自で実装しています。使用できるフォーマット(≒拡張子)に限りがあるほか、特殊なオプション設定を用いたファイルには対応できていません。

OpenGLによる実装は試験的に行われているものです。OpenGL自体も勉強中であり、不完全な実装になっています。問題のあるコードが使われている場合は、ご指摘ください。

ポリゴン数の概算

OpenXOPSにTENNKUU氏が作られた本家XOPS向けのグラフィックデータを用いた場合、毎フレームのポリゴン数は概算で以下の通りです。
マップのブロック1個 12ポリゴン
小物 約10ポリゴン
武器 約50ポリゴン
人  約250ポリゴン

マップ 12ポリゴン×160個 = 1920ポリゴン
人   250ポリゴン×96人 = 24000ポリゴン
武器  50ポリゴン×100個 = 5000ポリゴン
小物  10ポリゴン×40個 = 400ポリゴン
合計 31320ポリゴン
上記概算に加えて銃弾やエフェクト、2Dの表示にもポリゴンが必要です。また、プレイするミッションの人数や武器数などに影響される上、使用するデータによっても大幅に増減します。


入力、サウンド

プレイヤーからの入力処理やサウンド再生は、以下の手段を利用しています。
・マウスとキーボード入力WinAPI または DirectInput
・サウンド再生本家XOPS付属の ezds.dll または DirectSound

入力処理のインターフェースは WinAPI と DirectInput がそれぞれ用意されており、ソースコード内の定数を変更することで切り替え可能です。標準では WinAPI のコードが有効にされています。
WinAPIやDirectInputの詳しい仕様説明は割愛します。必要に応じて各種文献で調べてください。

サウンド周りも 本家XOPS付属のezds.dll と DirectSound がそれぞれ用意されており、ソースコード内の定数で選択できます。標準では ezds.dll のコードが有効にされています。
両者とも音源に向きの概念はなく、音は360度広がります。また、ドップラー効果は再現されていません。

ezds.dll は、nine-two氏が作成したサウンド再生DLLで、本家XOPSで使用されているものです。
サウンドの再生は、カメラ(視点)と音源の距離に応じて再生音量は変化しますが、左右のパン・バランスは変わりません。つまり常にモノラル再生のような状況で、3D空間の角度(左右どちらから聞こえるか)は考慮されません。
ezds.dllの仕様は本家側で正式に公開されていませんが、以下の解析資料を参考にしてください。
 OpenXOPS公式サイト http://openxops.net/
  ⇒ 開発・技術資料 ⇒ ファイル解析情報 ⇒ ezds.dllファイル解析資料

DirectSoundは、DirectXの一部として提供されているAPIです。3Dサウンドを用いたステレオ再生を行えます。OpenXOPSでは実装されていませんが、ドップラー効果の再現やエフェクト(特殊効果)を加えることも可能です。一方で、本家XOPSと音の聞こえ方が大きく異なる点に注意してください。
DirectSoundの詳しい仕様説明は割愛します。必要に応じて各種文献で調べてください。

起動時にInitD3Dinput()を呼び出した後、原則として毎フレームごとにGetInputState()を呼び出してください。実際に座標やキー入力状況は、別途用意された関数で実行します。
オブジェクトは1個だけ作成する前提で設計しています。オブジェクトを複数作成しないでください。


ファイル制御

ファイルの読み込み部分は、C言語標準関数群(stdio.h)を用いています。書き込み処理はありません。非同期ファイル読み込みもサポートしていません。

ブロックデータ(.bd1)やポイントデータ(.pd1)のフォーマットは本家側で公表されていませんが、以下の解析資料を参考にしてください。
 OpenXOPS公式サイト http://openxops.net/
  ⇒ 開発・技術資料 ⇒ ファイル解析情報 ⇒ BD1ファイル解析情報、PD1ファイル解析情報

ミッション設定ファイル(.mif)のフォーマットも、以下を参考にしてください。
 OpenXOPS公式サイト http://openxops.net/
  ⇒ 開発・技術資料 ⇒ ファイル解析情報 ⇒ MIFファイル解析情報

設定ファイル(canfig.dat)のフォーマットは、以下の解析資料を参考にしてください。
 OpenXOPS公式サイト http://openxops.net/
  ⇒ 開発・技術資料 ⇒ ファイル解析情報 ⇒ config.datファイル解析情報

ファイル自体の読み込み処理はDataInterfaceクラスとその派生クラス、およびConfigクラスで行います。ファイル参照後のデータの読み取りは、各クラスのメンバー関数を利用してください。
オブジェクトは、1個作成すれば十分です。

Windows環境以外も考慮し、ファイルパス内の'\'を'/'へ置き換えて処理する機能も有しています。通常は無効化されていますが、main.h内の PATH_DELIMITER_SLASH 定数を有効にすることで機能します。


AI

プレイヤー以外の敵や味方を制御するのがAI(NPC)です。ポイントデータのAIパス設定・配置に合わせた移動や、敵発見時の攻撃などを行います。

ゲームのAI制御は全て AIcontrolクラス に集約されています。このクラスを全て置き換えることで、より高度なAI制御がされるゲームを実現できます。

AIcontrolクラス で作成した1つのオブジェクトで、1人分のオブジェクトを管理します。(96人いれば、96個のAIオブジェクトを作成する必要があります。)
それぞれのオブジェクトに対して初期化関数を実行した後、それぞれに処理系関数を実行することによってAIが制御されます。

AIの状態遷移

AIは状態遷移を組み合せて動作しています。
大きく「攻撃中」「警戒中」「平常時」の3つに分けられます。さらに平常時には、設定されたAIパスに応じて様々な状態があります。なお各状態に関係なく、武器のリロードや廃棄が行われます。
図で表すと以下の通りです。


▲AIの状態遷移

原則として、平常時に敵を発見した場合、一度警戒状態に移行した後、改めて攻撃状態に移行します。攻撃終了時も、一度警戒状態に移行し、改めて平常時に戻ります。
ただし、「優先的な走り」を行っている場合に限り、警戒状態に入ることなく、攻撃状態と平常時は直接設定されます。

操作対象の人(自分自身)が死亡した場合は、次のフレームから処理を行いません。

各状態の詳細

「攻撃中」「警戒中」「平常時」各モードの詳細について解説します。

「平常時」は、AI初期化時に設定される最も基本的な状態です。上の図に記してある通り、さらに細かい状態を保持しており、実際にAIパス設定に合わせて移動や待機、特定人物を追尾などを行います。
移動処理に加えて、敵が見えないか・発砲音が聞こえないか・見方の死体がないかなどの基本的なチェックも行っています。

「警戒中」は、周囲の状況を入念に確認します。‘入念に確認する’という行為は、「平常時」の待機状態の際に使用する周囲を見回す処理に対して、左右に回転しやすく設定した上で、敵が見えるかどうかの判定範囲を広げているだけです。
常に内部でカウントダウンをしており、カウントが 0 になると「平常時」に戻ります。一定の条件かつランダムに、カウントに関わらず強制的に警戒処理を終わらせています。

「攻撃中」は、主に敵への攻撃を行うことを目的とした状態です。厳密には、武器を持っていない場合は手を上げる処理も実施します。またゾンビの場合は敵に対して近づいて行き、近寄った時点でダメージを与えます。自身がゾンビである場合を除き、この攻撃中は前後左右にランダムに移動します。
敵への距離に応じて「近距離攻撃」と「遠距離攻撃」を切り替えていますが、敵を照準に合わせたと判定する角度や、弾切れの際にリロードするか武器を持ち替えるかの判定優先度 などを変えている程度です。
当然、攻撃対象が死亡すれば攻撃を終了します。敵の姿が見えているかどうか(=敵の頭までブロックで遮っていないか)は、ランダムなタイミングで判定しています。判定に意図的なタイムラグを設けることで敵の姿が見えなくなった直後でも、敵が生きている間は壁に向かって発砲する動作を実現するためです。

AIスキル・AIレベル

X operations TOOLSの説明書でも明らかにされている通り、各人物に割り当てられるAIにはレベルが設定されています。基本的な処理は全てのAIで共通であり、各処理の判定基準値を変えることでAIレベルを再現しています。

各レベルの設定パラメーターは4つあります。具体的に以下の通りです。
名前説明
エイミング能力目標に対して適切に銃を向ける能力。
許容誤差目標を狙う際に、目標を捉えていると認識する範囲・角度。
攻撃性目標に照準が合っている時に、弾を発射する(連射)確率。
発見能力敵を見つけたと判定する距離。

AIに設定されたレベルは、弱い(D)・普通(C)・少し強い(B)・強い(A)・最強(S)の5種類です。各人物のAIレベルについては、X operations TOOLSの説明書をご覧ください。また、各レベルに対する4つのパラメーターの設定値については、直接ソースコードを参照してください。

4つの設定パラメーターの存在は、nine-two氏から直接教えて頂きました。本家XOPSと同様です。
原則として各パラメータは高い方がAIは強くなりますが、nine-two氏いわく『戦闘では銃を乱射すれば勝てるというわけではないので、攻撃性というパラメータは単に高ければ強いとは限らない。』だそうです。

デバック表示

メインゲーム画面において、AI関連のデバック情報を表示することができます。


▲メインゲーム画面でのAI情報デバック表示例

[ID:xx]は人のデータ番号です。()内の数字は、それぞれ人の座標とHPです。
Modeの文字は「攻撃中」「警戒中」「平常時」を表します。TargetEnemyIDは攻撃対象の人のデータ番号で、攻撃中以外は"-1"になります。AIlevelはAIレベルの設定値です。
PointPathはAIが読み込んでいる移動パスの値、MovePosは移動先の座標です。移動先座標に高さの概念はありません。
なお、攻撃中は攻撃対象の人を赤い線で囲んで表示します。


設定パラメータ

設定パラメータとは、人・武器・小物の設定データを管理する機能です。これらには、標準ミッションのミッション名などの情報、および各AIレベルの設定値も含みます。

ka-navi氏(ユーザーの1人)が開発・公開した非公式ソフトウェア X operations Mod Supporter(XMS・旧:X operations Weapon Cracker(XWC))での設定項目を管理していると考えて間違いありません。

人・武器・小物の追加や、標準ミッションの増減・変更などにも、ある程度柔軟に対応できます。ただし、最大数を指定する定数や、その他関連定数の設定を忘れないでください。必要に応じてモデルデータやテクスチャを読み込める最大数も変更してください。(グラフィック参照)

基本的には本家XOPSの設定値通り(XMSで読み込める通り)ですが、武器の設定は一部変更・拡張しています。
例えば、切り替える武器対象の設定を各武器単位で個別に持っているため、GLOCK18 SEMI・FULLの切り替えに限らず、様々な武器を持ち替えるように設定できます。2種類の武器を切り替えるだけでなく、3種類以上の武器を順番に切り替えることも可能です。
また、発射する弾数も各武器単位で個別に設定でき、M1のようなショットガンも複数作成することができます。

全ての設定パラメータは、ParameterInfoクラス に格納されています。設定値を読みだす場合、クラスのメンバー関数を用いてください。
オブジェクトは、当然1個作成すれば十分です。


オブジェクトの関係と制御

オブジェクト単体

ゲーム内で同時に複数存在し使用する物体については‘オブジェクト’として管理します。
具体的には以下のようなデータです。
 ・人 (生存・死亡問わず)
 ・マップに落ちている武器
 ・小物 (破壊されていない状態)
 ・発射された弾丸
 ・投げられた手榴弾
 ・エフェクト (詳細は後述)
ブロックデータによるマップは、常に1つのみ表示・使用されるためオブジェクトとして扱っていません。

エフェクト(effect)とは、ゲームのグラフィックに対して飾りとして描画される特殊効果のことで、血・煙(手榴弾の爆風など)・薬莢などです。 (グラフィックについては「グラフィック」を参照)

オブジェクトは、それぞれ計算処理に加えて描画も行います。
各種オブジェクトのインターフェースはある程度統一されており、SetPosData()関数で座標を設定し、SetParamData()関数でパラメーター(HPや攻撃力など)を設定します。Get〜()関数で逆に取得できます。
毎フレームごとに、ProcessObject()関数で座標などを計算し、Render()関数でオブジェクトを描画します。
各オブジェクトには‘描画フラグ’を持っており、このフラグを有効にしないと計算や描画が有効になりません。フラグの読み出しには GetEnableFlag()関数 を、設定には SetEnableFlag()関数 を使用します。

必要に応じて各オブジェクトごとに当たり判定も実施しています。当たり判定の詳細は別途「当たり判定」を参照してください。

オブジェクトにおける座標の中心点は、概ねオブジェクト(≒モデル)の中心に存在することが多いですが、人は足元が中心点になります。

各オブジェクトは、複数作成する必要があります。例えば、人オブジェクトは人数に合わせて96個、武器オブジェクトは200個必要です。

複数のオブジェクトの管理

多数のオブジェクトを効率的に管理するため、 ObjectManager と呼ばれるクラスがあります。
例えば、人と(所持させる)武器の双方の初期化が必要な人を追加する処理や、弾丸に対して人や小物との複雑な当たり判定を実行する処理などを、 ObjectManager を利用することにより簡単に実行できます。

デザインパターンで言うと Facadeパターン です。

ObjectManager は、手順が複雑で行数が多くなりがちな処理を行うのみであり、各処理は比較的単純です。ただし、人による武器の発砲のみ処理がやや複雑なので、以下の通り擬似的な簡易シーケンス図を提示します。


▲ObjectManager による銃弾の発射シーケンス

オブジェクト単体と異なり、ObjectManager は1個だけ作成する前提で設計しています。ObjectManager を複数作成しないでください。


当たり判定

当たり判定(衝突判定)とは、ゲーム空間において物体同士が衝突するか判定するプログラムを指します。なお、三次元空間での当たり判定について、基本的な知識(使わない物も含めて AABB、OBB、レイ、球体同士 など)がある前提で解説します。

ゲーム内のオブジェクト(表示)は複雑な形状をしていますが、当然ながら厳密な判定は実施しておらず、四角い箱や球体のような単純な形状で判定処理をしています。よって、実際に表示されているオブジェクトの形状と判定処理では異なります。
荒い当たり判定によるプレイヤーへの違和感は殆ど生じません。ゲームを処理落ちさせてまで、過剰なほど厳格・厳密な判定を実施する意味はありません。

当たり判定は、大きく分けて「マップとオブジェクト」と「オブジェクト同士」の2通りに分けることができます。「.bd1ファイル」と「.bd1ファイル以外」と分けて解釈しても問題ありません。
マップとオブジェクトは、ブロックにより構成された移動しないマップに対して、移動するオブジェクトを判定する処理です。例えば、マップと人、マップと(誰も所持していない)置かれた武器などです。AIが敵の発見を行う際にも、敵までにブロックが存在しないか(=敵が見えるか)判定に用います。
オブジェクト同士は、両対象が複雑な形状を持ち、お互いに移動している物体に対する判定です。例えば、人同士、人と銃弾などです。

マップとオブジェクト

最初に、マップとオブジェクトの当たり判定について説明します。
マップとの当たり判定を実施する上で最大のポイントは、ブロックの外側か内側か(埋まっているか)を判定する処理です。ブロックは6個の面で構成されていますが、判定を行う点に対して、6面全てが裏面ならばそのブロックに埋まっていることになり、1面以上表面があれば埋まっていないことになります。
 ※1個のブロックに対して、常に1面以上裏面があります。6面とも表面であることは考えられません。


▲ブロックの表側と裏側の判定

このブロックの外側と内側を判定する処理を利用し、移動によるブロックへの判定を行います。
まず、現時点の座標を基準に、各ブロックの表面を向いている面を記憶します。(この時点で表面がないブロックがある場合、その基準点はマップに埋まっています。) 次に移動先の座標を基準に、改めて各ブロック面の表裏を判定します。新たに内側になったブロックを見つけたら、移動前の情報と比較し、外側⇒内側に変化した面に衝突したと判定します。
ただし、この手法では極端に薄いブロックなどに対して正しく判定されず、貫通する可能性があります。弾丸や手榴弾などには利用できません。
この方法は [−_−;](みかん) が生み出した方法ではなく、nine-two氏が発見した手法です。本家XOPSでも同様の処理を実装しているようです。


▲移動によるブロックへの判定

nine-two氏が公開している「X operations TOOLS」の説明書に、
空間の中心から周囲に均等にブロックが分布している状態の方が、実行時に処理速度が速くなります。
また出来るだけブロックが空間の中心の軸を跨がないようにするとより早く処理されます。

引用:X operations TOOLS 1st edition 〜 マップの作り方 〜
と書かれている通り、原点を通るX軸とZ軸を基準として空間分割を実施しています。(nine-two氏に確認済み)
X・Z平面で原点を中心に4分割し、その4グループ内に収まるブロックとグループを跨ぐブロックの、計5グループに分けて分類し、当たり判定実行時に計算対象のブロックを削減しています。

前述の通り、銃弾や手榴弾など薄いブロックにも正確な判定が必要な場合、レイ(Ray・光線)を用いて判定しています。マップと人との当たり判定の場合でも、ブロックまでの正確な距離を求めるため、レイを使用している場合もあります。

オブジェクト同士

(前述の通り)オブジェクト同士の判定には、実際に描画される形状に関わらず、できるだけ単純な形状で当たり判定を実施しています。具体的な形状は以下の通りです。

対象形状
人と人垂直な円柱(筒)
人と武器
(マップ上に置いてある武器)
距離(球体)
銃弾と人レイと円柱(筒)
銃弾と小物レイと球体
手榴弾爆発と人距離(球体)
手榴弾爆発と小物距離(球体)

手榴弾爆発の当たり判定は少々複雑で、爆発時に人・小物とマップの当たり判定を両方行った上で、距離が近い方を有効にしています。人や小物よりマップの方が距離が近ければ、マップが障害物になっていると見なし、後ろの人や小物にダメージは与えません。
人が手榴弾の爆風でダメージを受けるポイントは2点あり、それぞれ頭と足元です。爆発時点と頭・足元の距離に応じて、それぞれ別にダメージ算出を行い、最終的に2点分のダメージを加算します。なお、どんなに近距離で(腰・腕付近が無防備で)も頭と足元が隠れていれば、手榴弾のダメージは受けません。

当然ながら全ての判定を対象に、距離やAABB同士の判定を用いて計算対象を荒削りしています。


音源の管理

ゲーム内で発生する音は、プレイヤーに対してサウンドを再生する以外に、AI側も利用します。例えば発砲音に反応し周囲を警戒するといった行為が挙げられます。
一方で、常に「プレイヤーに聞こえる音 = AIが利用する音」とは限りません。足音(走る音)は、サウンドは再生されずプレイヤーには分かりませんが、AIは感知しており、敵の足音が近づくと周囲を警戒します。また手榴弾がマップを跳ね返る音は、プレイヤーには音として再生されますが、AIには認知されません。

OpenXOPS上で存在する全ての音源の機能を表にまとめると、以下のようになります。
種類AI再生識別音量敵味方継続
発砲音××
小物着弾音×××
マップ着弾音×××
被弾音××××
手榴弾バウンド音×××--×
手榴弾爆発音××××
弾移動音××
足音×××
 AI:音源をAIが利用するか
 再生:プレイヤーに対してサウンドを再生するか
 識別:音源ごとにさらに種類が細分化されるか
 音量:音源ごとに音量が変化するか
 敵味方:音源に敵味方の区別があるか
 継続:継続して鳴るか (1回のアクションで1回しか発生しない音なら ×)

ゲーム中に発生する様々な効果音を「音源」として一括で管理するのが、SoundManager です。
SoundManager は、ゲーム内で毎フレーム発生する全ての効果音を一元管理します。SoundManager が管理する音源の情報に応じて、プレイヤーに対してサウンドを再生したり、AIに情報を渡します。

SoundManager の音源情報は、主に以下のような情報を保持しています。
 ・音の種類
 ・3D空間での位置情報
 ・音量
 ・チーム番号 (敵味方の判定用)

カメラの座標を与えてサウンド再生関数 PlayWorldSound() を実行すると、空間内の音源情報を元にサウンドを再生します。また、AIの座標を GetWorldSound()関数 で与えることで、AIが検知すべき音を自動的に決定して返します。
ただし、プレイヤーに対して再生される音は最新フレームによる情報ですが、AI向けに検知する情報は前回のフレームの情報です。AIの音源に対する反応が、1フレーム分遅れることに注意してください。

SoundManager は1個だけ作成する前提で設計しています。SoundManager を複数作成しないでください。
また追加する音源情報は毎フレーム初期化されるため、音源が有効な間は毎フレーム追加する必要があります。全ての音源追加処理が完了した時点で、毎回(毎フレーム)PlayWorldSound()関数を呼び出し再生してください。


イベント

イベントの制御は EventControl クラスに集約されています。ポイントデータを読み出しながら、現在実行中のイベントポイントを状態遷移により管理し、ミッション成功・失敗やメッセージ表示を判定します。
無限ループに陥ることがないよう、1フレームあたりイベントポイントは6個までしか処理されません。フレームあたりの処理数は定数により変更可能です。

EventControl クラスは1つ分のイベント処理を行います。3つ同時に並行処理させるには EventControl オブジェクトを3個作り、それぞれ処理させる必要があります。ミッション開始時に Reset() 関数を実行の上、毎フレーム ProcessEventPoint() 関数を呼び出してください。


リソース管理

OpenXOPSは、テクスチャやサウンドファイルといったデータ(=リソース)は、全て合わせると80個を超えます。
それらリソースを管理するのが ResourceManager です。ResourceManagerはテクスチャとサウンドファイルのファイル名を設定しているほか、読み込み処理を実行し、必要に応じて他のクラスにリソースを渡した上で、終了時に廃棄処理を行います。

デザインパターンに当てはめると Facadeパターン です。人のテクスチャ(最大10種類)の読み込みを管理するような部分は Flyweightパターン です。

読み込み実行関数は、人や武器などの分類に加え、モデルデータ・テクスチャ・サウンドファイルといったデータ形式でも細分化されています。これはリソースの数やサイズが膨大になった際に、ロード画面の実装が容易になるように設計しているためです。


バグの再現

本家XOPSは、公式の説明書やX operations TOOLSのマニュアルには記載していない、(本来の仕様から外れた)明らかにバグと思われる挙動があります。一方で、一部の上級者が作るaddonの中には、本家XOPSのバグを意図的に利用しているものもあります。
OpenXOPSでは本家XOPSのバグを明確に再現することで、本家XOPS向けのaddonに対するOpenXOPSでの再現性向上を目指しています。

本家XOPSの「謎人間」は典型例です。(謎人間に関する詳細な説明は割愛します。) OpenXOPSでは本家XOPSの謎人間に暫定的に対応し、バグによる挙動を再現することで、謎人間を利用したaddonでも正しく動作する可能性を高めています。

謎人間関係を含め、OpenXOPSにて暫定および正式対応している挙動は、以下の通りです。 (順不同)

※上記記載は、本家XOPS向けのaddon制作に利用することを推奨するものではありません。
 また、これらはOpenXOPSプロジェクト独自の見解であり、本家XOPSの‘正規の仕様’ではありません。
 ご迷惑になりますので、nine-two氏への問合せは絶対にしないようにお願いします。
※「公式の裏技」については「バグ」として扱っていません。


コンソール機能

標準で有効になっている機能の中で、(本家XOPSにはない)OpenXOPS独自機能のひとつがコンソール機能です。
ゲーム画面上での利用方法については別途説明書などを参考にしてください。ここではコンソール機能の技術的な手法や注意点を説明します。

コンソール機能は、全てメイン画面のクラス(maingameクラス)に記述されています。
同クラスのCreate()関数で初期化後、InputConsole()関数で入力処理、ProcessConsole()関数でコマンドの解読や実行、RenderConsole()関数で画面上に描画、同クラスのDestroy()関数で解放しています。
既に表示済みの情報(過去に入力した文字や、処理結果などの表示)は InfoConsoleData 変数、現在入力中の情報は InputConsoleData 変数に格納されます。現フレーム中に[Enter]キーで決定したコマンドは、NewCommand 変数に格納されます。

コンソール処理において、最も中心的な役割を担っているのがProcessConsole()関数です。NewCommand 変数に格納された文字列を、C言語標準関数のstrcmp()関数でコマンドの文字を比較し、条件分岐によってコマンドを実行しています。

前述の通り、OpenXOPSのコンソール機能は本家XOPS(オフライン版)には存在しない機能です。objectmanager.h の ENABLE_DEBUGCONSOLE 定数をコメント化することで、コンソール機能自体を無効にできます。無効化するとゲーム画面からコンソール画面が呼び出せなくなるほか、EXEファイルのサイズがわずかに小さくなります。


デバックログ機能

コンソール機能と同様に、標準で有効になっているOpenXOPS独自の機能です。利用方法については別途説明書などを参考にしてください。

機能は全て DebugLog クラスで完結しており、動作原理は非常に単純です。MakeLog()関数でログファイルを初期化した後、WriteLog()関数が呼ばれる度にログファイルにHTMLを追記します。
WriteLog()関数でログに追記する前に、MakeLog()関数でファイルを初期化する必要があります。OpenXOPS各所からWriteLog()関数が呼び出されていますが、ログファイルの出力の有無をMakeLog()関数呼び出しの有無で切り替えることで、ログを出力しない場合WriteLog()関数を意図的に失敗させています。
出力されるログは英語固定です。タイムスタンプは自動的に発行されます。

WriteLog()関数でログを出力(追記)する際、「出力モード」を選択する必要があります。(情報・初期化・読み込み・解放・成功・失敗)
[初期化]か[読み込み]が使用された場合、次に[成功]で呼びださないと初期化・読み込みがエラーになったと解釈し、自動的に[失敗]としてログに追記されます。明示的に[失敗]を呼び出すこともできます。

コンソール機能と同様に、main.h の ENABLE_DEBUGLOG 定数をコメント化することで、コンソール機能自体を無効にできます。


その他

OpenXOPS全体で乱数の生成を、別関数(random())にしています。ランダムな動作を行う際に、乱数に偏りが少ないアルゴリズムを採用したり、逆にTASなどを行うために乱数調整がしやすいよう変更する事も可能です。 (あまり詳しくないので良く分かりませんが。)

OpenXOPSは、当初から汎用ゲームエンジンの開発は想定されていません。OpenXOPSを改造する場合、現行のOpenXOPSを構成しているクラスを継承して構築するより、OpenXOPSのクラス自体を書き換えていくのが賢明です。


改造のヒント

簡単な改造

画面解像度の変更や移動速度の変更など、予め設定された固定値の変更に代表されるような単純な変更ならば、#define で定義された定数を変更することによって対応可能な場合があります。

一例として、addonの最大読み込み数は、main.h の 50〜60行目付近にある
 #define MAX_ADDONLIST 128
を変更することで対応できます。例えば最大数・上限を 512個 に変更する場合は、
 #define MAX_ADDONLIST 512
と書き換えるのみで、概ね変更完了になります。

ブロック数やポイント数の読み込み数上限も、別途ファイルの定数を書き換えることで対応可能です。

OpenXOPSの改造作業に着手する前に、前例のような単純な定数宣言の変更で対応できないかどうか、よく検討するべきです。
必要に応じてその都度全ファイルを検索して探しても構いませんが、常日頃から(?)OpenXOPSソースコードの特に定数宣言の部分を眺めておくと良いかもしれません。

※固定値を変更した際は、十分な動作検証を行ってください設定値によっては予期せぬ不具合が発生する可能性があります。

他のプラットフォームへの移植

OpenXOPSは、他のプラットフォームへの移植性も考慮して設計・開発されました。ほとんどのクラスや記述は他のプラットフォームに流用できます。

移植性がなく流用できない部分は「レイヤー」で例えると、概ね低レイヤーに集中しています。具体的に置き換える必要がある部分については、移植作業を行いながら確認してください。

例えば、低レイヤーを中心に書きかえることで、Linuxなどの他のプラットファームに移植できる可能性があります。PCに限らずスマートフォンやゲーム機など、他のデバイスにも流用できるかもしれません。(要検証)


Copyright (C) 2014-2022 [−_−;](みかん). All rights reserved.