Compare commits

25 Commits

Author SHA1 Message Date
Tony Cannon
7ddadef854 Merge pull request #46 from seasidelab/fix-dev-guide
Fix Japanese developer guide
2019-11-07 09:39:58 -08:00
BUGMAN
08d9f489bc Unify section name. 2019-11-08 02:22:56 +09:00
BUGMAN
6e02f97848 Adjust Japanese documentation. 2019-11-08 02:06:40 +09:00
BUGMAN
5d4772c9b1 Replaced Japanese document words with appropriate ones. 2019-11-07 22:28:38 +09:00
BUGMAN
f1493d7449 Restore lost back quotes. 2019-11-05 22:13:25 +09:00
Tony Cannon
b13e16a5a5 Merge pull request #44 from Shugyousha/linkjareadme
readme: add link to Japanese README
2019-11-04 07:03:55 -08:00
Silvan Jegen
1dfb6c2a1c readme: add link to Japanese README 2019-11-04 10:18:19 +01:00
Tony Cannon
7513f9d1a6 Merge pull request #43 from JakeTheJake74/patch-1
Update README.ja.md
2019-10-31 17:50:36 -07:00
JakeTheJake74
831c91d454 Update README.ja.md 2019-10-31 20:19:21 -04:00
Tony Cannon
b98f725036 Merge pull request #41 from seasidelab/minimize-doc-diff
Minimize documentation differences.
2019-10-31 08:33:57 -07:00
BUGMAN
8ed9bb9ab3 Fixed broken link in Japanese documentation. 2019-10-31 00:01:31 +09:00
BUGMAN
699dd85414 Minor corrections to Japanese documentation. 2019-10-30 23:51:33 +09:00
BUGMAN
fa906c3e63 Apply latest "Sample Application" of root directory. 2019-10-30 23:48:01 +09:00
BUGMAN
899f41445d Apply latest "What GGPO?" of doc directory. 2019-10-30 23:34:58 +09:00
BUGMAN
493e87c9ce Convert full-width symbols to half-width. 2019-10-30 22:24:14 +09:00
BUGMAN
e0fae946b5 Minimize documentation differences. (en <=> ja) 2019-10-30 22:23:54 +09:00
Tony Cannon
e4e11d8e3e Merge pull request #40 from pond3r/dev/compile_warnings
Clean up compilation
2019-10-29 07:20:58 -07:00
Tony Cannon
c3f933f3b0 fix remaing compile errors for release build (thanks ci!) 2019-10-27 16:26:57 -07:00
Tony Cannon
49fe5c19ec remove remaining compile errors 2019-10-27 16:24:02 -07:00
Tony Cannon
ef37947edc Merge branch 'master' into dev/compile_warnings 2019-10-27 15:51:58 -07:00
Tony Cannon
98d299480d work in progress to remove compile errors 2019-10-27 15:51:26 -07:00
Tony Cannon
4c3aa4e099 work in progress to remove all warnings 2019-10-27 15:50:54 -07:00
Tony Cannon
881d218bfd Merge pull request #30 from erebuswolf/master
Fix crash on escape keypress for vector war
2019-10-27 15:49:57 -07:00
Tony Cannon
e6e4cc76b2 Merge pull request #39 from pond3r/dev/crossplatform
Merge latest dev/crossplatform into master
2019-10-27 14:03:16 -07:00
Jesse Fish
36365b12e4 Fix crash on escape key for vector war
Vector war example was crashing on escape key press due to both deleting the renderer without setting a value to NULL and calling the init function with a not initialized pointer. Now the program closes gracefully on escape keypress.
2019-10-24 16:51:39 -07:00
37 changed files with 323 additions and 291 deletions

View File

@@ -1,19 +1,19 @@
![](doc/images/ggpo_header.png)
## GGPOとは
&nbsp; _[![Appveyor build status](https://img.shields.io/appveyor/ci/pond3r/ggpo/master.svg?logo=appveyor)](https://ci.appveyor.com/project/pond3r/ggpo/branch/master)_
従来の技術はプレイヤーの入力に遅延を織り込んで通信を行っており、その結果反応が遅く、ラグを感じるプレイ感になっていました。ロールバックネットワーキングはプレイヤーの入力を即座に送信するため、入力予測と投機的実行を行うことにより、遅延ゼロの回線環境をもたらせるのです。ロールバックがあれば、タイミングや相手の動きや効果音に対しての反応、指が覚えている入力、これらオフラインで行えた内容が、そのままオンラインでも行えます。GGPOネットワーキングSDKは、ロールバックネットワーキングを新作や発売されているゲームに極力簡単に組み込めるよう作られています。
## GGPOとは
これまでのGGPOについてさらに知りたい方は、http://ggpo.net/をご覧ください
従来の技術はプレイヤーの入力に遅延を織り込んで通信を行っており、その結果反応が遅く、ラグを感じるプレイ感になっていました。ロールバックネットワーキングは入力予測と投機的実行を行って、プレイヤーの入力を即座に送信するため、遅延を感じさせないネット環境をもたらします。ロールバックがあれば、タイミングや相手の動きや効果音に対する反応、指が覚えている入力、これらオフラインで行えた内容が、そのままオンラインでも行えます。GGPOネットワーキングSDKは、ロールバックネットワーキングを新作や発売されているゲームに極力簡単に組み込めるよう作られています
これまでのGGPOについてさらに知りたい方は、http://ja.ggpo.net/ をご覧ください。
このリポジトリにコードやドキュメント、SDKのサンプルアプリケーションが収められています。
## ビルド
GGPOのビルドは現在のところWindowsのみになりますが、他プラットフォームへのポートも現在行っています。
### Windows
Windowsのビルドは[Visual Studio 2019](https://visualstudio.microsoft.com/downloads/)と[CMake](https://cmake.org/download/)が必要になります。ご使用の前に、どちらもインストールされていることをご確認ください。またインストール時、パスにCMakeを追加してください。
@@ -23,17 +23,15 @@ Windowsのビルドは[Visual Studio 2019](https://visualstudio.microsoft.com/do
好みにあわせて`cmake-gui`で実行も出来ます。
## サンプルアプリケーション
ソースディレクトリ内のVector Warアプリケーションでは、2つのクライアントを同期するGGPOが搭載されています。コマンドライン引数は
ソースディレクトリ内のVector Warには、GGPOを使った2つのクライアントを同期する単純なアプリケーションが含まれています。コマンドライン引数は以下の通りです。
```
vectorwar.exe <localport> <num players> ('local' | <remote ip>:<remote port>) for each player
```
2~4プレイヤーゲームの始め方についての例は、binディレクトリにある.cmdファイルをご覧ください。
24プレイヤーでのゲーム開始方法の例については、binディレクトリにある.cmdファイルを参照してください。
## ライセンス

View File

@@ -2,17 +2,22 @@
&nbsp; _[![Appveyor build status](https://img.shields.io/appveyor/ci/pond3r/ggpo/master.svg?logo=appveyor)](https://ci.appveyor.com/project/pond3r/ggpo/branch/master)_
(日本語ドキュメントは[こちら](README.ja.md))
## What's GGPO?
Traditional techniques account for network transmission time by adding delay to a players input, resulting in a sluggish, laggy game-feel. Rollback networking uses input prediction and speculative execution to send player inputs to the game immediately, providing the illusion of a zero-latency network. Using rollback, the same timings, reactions visual and audio queues, and muscle memory your players build up playing offline translate directly online. The GGPO networking SDK is designed to make incorporating rollback networking into new and existing games as easy as possible.
Traditional techniques account for network transmission time by adding delay to a players input, resulting in a sluggish, laggy game-feel. Rollback networking uses input prediction and speculative execution to send player inputs to the game immediately, providing the illusion of a zero-latency network. Using rollback, the same timings, reactions, visual and audio queues, and muscle memory your players build up playing offline will translate directly online. The GGPO networking SDK is designed to make incorporating rollback networking into new and existing games as easy as possible.
For more information about the history of GGPO, check out http://ggpo.net/
This repository contains the code, documentation, and sample applications for the SDK.
## Building
Building GGPO is currently only available on Windows, however efforts are being made to port it to other platforms.
### Windows
### Windows
Windows builds requires both [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/) and [CMake](https://cmake.org/download/). Make sure you've installed both before starting. Make sure to add CMake to your path when installing.
- Run the `build_windows.cmd` in the root directory of the SDK to generate the Visual Studio 2019 solution files.
@@ -21,6 +26,7 @@ Windows builds requires both [Visual Studio 2019](https://visualstudio.microsoft
You can also run the `cmake-gui` tool if you prefer.
## Sample Application
The Vector War application in the source directory contains a simple application which uses GGPO to synchronize the two clients. The command line arguments are:
```
@@ -30,5 +36,5 @@ vectorwar.exe <localport> <num players> ('local' | <remote ip>:<remote port>)
See the .cmd files in the bin directory for examples on how to start 2, 3, and 4 player games.
## Licensing
GGPO is available under The MIT License. This means GGPO is free for commercial and non-commercial use. Attribution is not required, but appreciated.
GGPO is available under The MIT License. This means GGPO is free for commercial and non-commercial use. Attribution is not required, but appreciated.

View File

@@ -1,40 +1,37 @@
# GGPO開発者向けガイド
# GGPO開発者ガイド
GGPOネットワークライブラリ開発者向けガイドは、アプリにGGPOネットワークライブラリを実装する開発者向けに用意されたテキストです。
GGPOネットワークライブラリ開発者ガイドは、アプリケーションにGGPOネットワークライブラリを実装する開発者向けに用意されたドキュメントです。
## ゲームステートと入力
ゲームは数多くの変化するパートがあると思います。GGPOはこれら2つだけに依存します。
ゲームは数多くの変化するパートがあると思います。GGPOは次の2つだけに依存します。
- **ゲームステート**はゲーム内で現在のステートすべてを描写します。シューティングゲームの場合、画面上にある自機と敵機の位置、ショットや敵弾の位置、敵機の残り体力、現在のスコアなどになります。
- **ゲームステート**はゲームでの全ての状態を表します。シューティングゲームの場合、画面上にある自機と敵機の位置、ショットや敵弾の位置、敵機の体力、現在のスコアなどになります。
- **ゲーム入力**はゲームステートを変更する一連のものを指します。プレイヤーが操作したジョイスティックやボタンも間違いなく含みますが、目に見えない入力も含みます。例えば、ゲーム内で何かを計算するためにその日の時刻を使用したならば、フレームの最初になるその日の時刻も、また入力になります。
ゲームエンジンの中にはゲームステートでも入力でもないものが多数あります。例えばオーディオやビデオレンダラーはゲームの結果に影響を及ぼさないので、ゲームステートではありません。ゲームに影響を及ぼさないような、特別効果を生成する特別効果エンジンがあれば、ゲームステートから除外することができます。
- **ゲーム入力**はゲームステートを変更する一連のものを指します。言うまでもなく、プレイヤーが操作したジョイスティックやボタンの押下が含まれますが、入力以外のものも含みます。例えば、現在時刻を使って何かを計算した場合、フレームを開始した時の時刻も入力になります。
ゲームエンジンにはゲームステートでも入力でもないものが他にもたくさんあります。例えばオーディオやビデオレンダラーはゲームの結果に影響を与えないため、ゲームステートではありません。ゲームに影響を与えない特殊効果を生成する特殊効果エンジンがあったとしたら、それもゲームステートから除外できます。
## 同期にステートと入力を使用する
GGPOを使用したゲームで遊ぶ各プレイヤーは、現在遊んでいるゲームの完全なコピーを持っています。両プレイヤーが同じゲーム内容遊べるよう、GGPOは両プレイヤーにあるゲームステートのコピーを同期し続ける必要があります。プレイヤー間で各フレームごとにゲームステート全コピーを送信することは大きな負になってしまいます。代わりにGGPOはお互いの入力を送信し、互いのゲームを一つ一つ進めています。この機能のために、ゲームエンジン3つの条件を満たさねばなりません
- ゲームシミュレーションが完全に決定性(deterministic)であること。与えられたゲームステートと入力があった場合、1フレームゲームステートを進めた際に全プレイヤーのゲームステートが全く同一の結果にならなければいけません。
- ゲームステートが完全にカプセル化(encapsulated)されていること、そして直列化が可能(serializable)であること。
- ゲームエンジンがそのフレーム時のゲーム内容をレンダリングすることなく、ロード、セーブ、フレームのシミュレーションができること。これはロールバック実行の際に使用します。
GGPOを使たゲームで遊ぶ各プレイヤーは、プレイしているゲームの完全なコピーを持っています。両プレイヤーが同じゲーム内容遊べるよう、保持しているゲームステートのコピーを同期し続ける必要があります。フレームが進む度にプレイヤー間でゲームステート全コピーを送信するは大きな負になます。代わりにGGPOはお互いの入力を送信し、各プレイヤーのゲームを進めます。これが機能するには、ゲームエンジン3つの条件を満たしている必要があります
- ゲームのシミュレーションは完全に決定的でなければなりません。つまり、特定のゲームステートと入力があった時に、ゲームステートを1フレーム進めると全プレイヤーのゲームステートが同じにならなければいけません。
- ゲームステートが完全にカプセル化され、シリアライズが可能であること。
- ゲームエンジンはそのフレームのゲーム内容をレンダリングすることなく、復元、保存、フレームのシミュレーションができなくてはなりません。これはロールバックを実装するために使用されます。
## プログラミングガイド
以下のセクションはあなたのアプリをGGPOにポーティング、移植する一連の流れを紹介します。GGPO APIの詳細な説明は、以下のGGPO参照セクションをご覧ください。
のセクションはあなたのアプリケーションをGGPO上で動作させるための一連の流れを紹介しています。GGPO APIの詳細な説明については、以下のGGPOリファレンスセクションを参照してください。
### GGPOとの繋ぎ込み
### GGPOとのインターフェイス
GGPOは新旧のゲームエンジンと簡単にインターフェイスできるよう設計されています。GPOSessionCallbacksのフックを介してアプリに呼び出すことにより、大分のロールバックの実装を行います。
GGPOは新規および既存のゲームエンジンと簡単に繋ぎ込みができるよう設計されています。`GGPOSessionCallbacks`フックを介してアプリケーションを呼び出すことにより、ほとんどのロールバックの実装を行います。
### GGPOSessionオブジェクトの生成
`GGPOSession`オブジェクトはGGPOフレームワークへのインターフェスです。ローカル、IPアドレス、対戦したいプレイヤーをバインドするためのポートを渡す`ggponet_start_session`ファンクションを使用して作成します。またセッションが1プレイヤー側、2プレイヤー側いずれの場合でも、ゲームステートを管理するコールバック関数で満たされた`GGPOSessionCallbacks`オブジェクト渡す必要があります。全GGPOSessionCallbackファンクションは実行をしなければなりません。詳細は以下を参照してください。例として、ポート8001にバインドしている別プレイヤーと同じホストで新しいセッションをスタートする場合、次のようになります:
`GGPOSession`オブジェクトはGGPOフレームワークへのインターフェスです。ローカルのポートと、対戦したいプレイヤーのIPアドレスとポートを`ggponet_start_session`関数を渡して作成します。またゲームステートを管理するコールバック関数で満たされた`GGPOSessionCallbacks`オブジェクトと、このセッションで遊ぶプレイヤーの数を渡す必要があります。全ての`GGPOSessionCallback`関数を実装しなければなりません。詳細は以下を参照してください。
例えば、ポート8001にバインドされた別のプレイヤーと同じホストで新しいセッションを開始する場合、次のようになります。
```
GGPOSession ggpo;
@@ -58,17 +55,18 @@ GGPOは新旧のゲームエンジンと簡単にインターフェイスでき
8001); // our local udp port
```
`GGPOSession`オブジェクトは1つのゲームセッションだけに用いられるべきです。 別の相手と接続する必要がある場合、`ggpo_close_session`を使用して既存のオブジェクトを閉じ、新たに以下のものを始めてください:
`GGPOSession`オブジェクトは単一のゲームセッションだけに使われるべきです。別の相手と接続する必要がある場合、`ggpo_close_session`を使用して既存のオブジェクトを閉じ、新しいオブジェクトを開始します。
```
/* Close the current session and start a new one */
ggpo_close_session(ggpo);
```
### プレイヤーロケーションの送信
### プレイヤーの場所を送信する
GGPOSessionオブジェクトを作成した時、ゲームに参加しているプレイヤーの数を渡しましたが、どのようにコンタクトをするかが実際には記述されていません。そのためには、それぞれのプレイヤーを記述する`GGPOPlayer`オブジェクトを渡して、`ggpo_add_player`呼び出します。以下は2プレイヤーで遊ぶ時に`ggpo_add_player`を使用する際の例になります:
GGPOSessionオブジェクトを作成した時、ゲームに参加しているプレイヤーの数を渡しましたが、実際にそれらを連携をする方法について説明していませんでした。これを行うには、各プレイヤーを表す`GGPOPlayer`オブジェクトを`ggpo_add_player`関数に渡して呼び出します。次の例は、2人用のゲームでの`ggpo_add_player`の使い方です。
```
GGPOPlayer p1, p2;
@@ -87,18 +85,19 @@ result = ggpo_add_player(ggpo, &p2, &player_handles[1]);
### ローカルと遠隔プレイヤーによる入力の同期
入力の同期は各ゲームフレームの最初に行われます。各ローカルプレイヤー`ggpo_add_local_input`の呼び出しと、 遠隔プレイヤーの入力を取得するために`ggpo_synchronize_input`の呼び出しをすることで行われます。`ggpo_synchronize_inputs`の戻り値を必ず確認するようにしてください。`GGPO_OK`以外の値を返す場合、ゲームステートを次に進めるべきではありません。わずかなあいだ、遠隔プレイヤーからパケットを受信していなかったり、内部で行われる予測の限界に達することがあるために、こういったことは頻繁に発生します。
入力の同期は各ゲームフレームの最初に行われます。各ローカルプレイヤーに対する`ggpo_add_local_input`の呼び出しと、遠隔プレイヤーの入力を取得する`ggpo_synchronize_input`の呼び出しによって行われます。
`ggpo_synchronize_inputs`の戻り値は必ず確認するようにしてください。`GGPO_OK`以外の値が返ってきた場合、ゲームステートを進めないでください。これは通常、GGPOがしばらくの間、遠隔プレイヤーからパケットを受信せず、内部の予測制限に達したことで発生します。
例えば、ローカルゲームのコードがのよう場合:
例えば、ローカルゲームのコードがのようになっている場合
```
GameInputs &p1, &p2;
GetControllerInputs(0, &p1); /* read p1s controller inputs */
GetControllerInputs(1, &p2); /* read p2s controller inputs */
GetControllerInputs(0, &p1); /* read p1's controller inputs */
GetControllerInputs(1, &p2); /* read p2's controller inputs */
AdvanceGameState(&p1, &p2, &gamestate); /* send p1 and p2 to the game */
```
以下のように変更するべきです:
のように変更する必要があります。
```
GameInputs p[2];
@@ -122,12 +121,11 @@ result = ggpo_add_player(ggpo, &p2, &player_handles[1]);
}
```
ロールバック中に発生したものでも、フレームごとに`ggpo_synchronize_inputs`を呼び出す必要があります。 ゲームステートを進めるためには、ローカルコントローラーから得られた値を読むのではなく、常に`ggpo_synchronize_inputs`から返ってくる値を使用してください。ロールバック中`ggpo_synchronize_inputs`前のフレームに用いられた値とともに、`ggpo_add_local_input`に渡された値を置き換えます。 また、ロールバックの影響を緩和するためにローカルプレイヤー向けの入力遅延を加えた場合、 フレーム遅延が終えるまで`ggpo_add_local_input`に渡され入力は`ggpo_synchronize_inputs`に返されません。
ロールバック中に発生したものも含め、全てのフレーム`ggpo_synchronize_inputs`を呼び出す必要があります。ゲームステートを進めるためには、ローカルコントローラーから得られた値を読むのではなく、常に`ggpo_synchronize_inputs`から返された値を使用してください。ロールバック中`ggpo_synchronize_inputs``ggpo_add_local_input`に渡された値を前のフレームに使われた値に置き換えます。また、ロールバックの影響を緩和するためにローカルプレイヤー向けの入力遅延を加えた場合、`ggpo_add_local_input`に渡され入力はフレーム遅延が終わるまで`ggpo_synchronize_inputs`に返されません。
### 保存、復元、解放コールバックの実装
### セーブ、ロード、およびフリーコールバックの実装
定期的にゲームステートをセーブ、リストア(復元)するために、GGPOは`load_game_state``save_game_state`コールバックを使用します。`save_game_state`関数はゲームの現在のステートを復元し、`buffer`出力パラメーター内に戻るために、十分な情報を含んだバッファを作成する必要があります。`load_game_state`ファンクションは以前にセーブしたバッファからゲームステートを復元します。例として:
GGPOはゲームステートを定期的に保存または復元するために、`load_game_state``save_game_state`コールバックを使用します。`save_game_state`関数はゲームの現在のステートを復元し、それを`buffer`出力パラメーターで返すのに十分な情報を含むバッファーを作成する必要があります。`load_game_state`関数は以前に保存したバッファーからゲームステートを復元します。例えば、
```
struct GameState gamestate; // Suppose the authoritative value of our game's state is in here.
@@ -153,7 +151,7 @@ ggpo_load_game_state_callback(unsigned char *buffer, int len)
}
```
これ以上必要がなくなった時は、GGPOがあなたの`save_game_state`コールバックにある割り当てたメモリを破棄するために、`free_buffer`コールバックを呼び出します。
不要になった、GGPO`free_buffer`コールバックを呼び出して、`save_game_state`コールバック割り当てたメモリを解放します。
```
void __cdecl
@@ -165,56 +163,49 @@ ggpo_free_buffer(void *buffer)
### 残っているコールバックの実装
先ほど挙げたように、`GGPOSessionCallbacks`構造にはオプショナルコールバックありません。 最低で`return true`である必要がありますが、残っているコールバックは即座に実行される必要ありません。詳しい情報はggponet.hのコメントをご覧ください。
前述のように、`GGPOSessionCallbacks`構造にはオプション扱いのコールバックありません。これらは少なくと`return true`である必要がありますが、残りのコールバックは必ずしもすぐに実装する必要ありません。詳細については`ggponet.h`のコメントを参照してください。
### ggpo_advance_frameとggpo_idle関数の呼び出し
### GGPOアドバンスとアイドル関数の呼び出し
いよいよ終わりに近づいてきています。大丈夫、お約束します。最後のステップはゲームステートが1フレームごとに進んだことを終えたら、毎回GGPOに通知することです。1フレームを終えた後、次のフレームを始める前に`ggpo_advance_frame`を呼び出すだけです。
GGPOは内部記録のパケットを送受信するために、一定の時間が必要になります。GGPOに許可したミリ秒単位で、最低でもフレームごとに1回は`ggpo_idle function`を呼び出す必要があります。
いよいよ終わりに近づいてきました。大丈夫、お約束します。最後のステップはゲームステートを1フレーム進める度にGGPOへ通知することです。1フレームを終えた後、次のフレームを開始する前に`ggpo_advance_frame`を呼び出すだけです。
GGPOは内部記録を行うパケットを送受信するために、一定の時間が必要になります。GGPOに許可したミリ秒単位で、最低でもフレームごとに1回は`ggpo_idle`関数を呼び出す必要があります。
## アプリケーションのチューニング: フレーム遅延 vs 投機的実行
遅延を感じさせないようにするために、GGPOはフレーム遅延と投機的実行の両方を用います。アプリ開発者が遅延入力のフレーム数はどの程度にするか、については選択ができます。もしゲームのフレーム数よりパケットの送信に時間がかか場合、GGPO投機的実行を用いて残りの遅延を感じさせないようにします。もし希望すれば、ゲーム中でもこの数字を変更できます。フレーム遅延の適切な値はゲームによって依存します。以下は役に立つヒントです
GGPOは遅延を感じさせないようにするために、フレーム遅延と投機的実行の両方を使用します。これは、アプリケーション開発者が入力を遅延させるフレーム数を選択できるようにすることで実現します。もしゲームのフレーム数よりパケットの送信に時間がかかった場合、GGPO投機的実行を使って残りの遅延を隠します。この数値は、必要に応じてゲーム中でも調整することができます。フレーム遅延の適切な値はゲームに大きく依存します。役に立つヒントをいくつか紹介しましょう
まずはゲームを遊ぶ感覚に影響を与えない範囲で、フレーム遅延を出来るだけく設定するようにしてみてください。例えば格闘ゲームであればドット単位の精度を要する操作、寸分狂いもないタイミング、極めて素早く動かすアーケードコントローラーといった要素があります。こういったゲームの場合、中級者の大半は2フレームの遅延に気き、ベテランプレイヤーであれば1フレームの遅延に気くこともあります。かたや、厳密なタイミング操作を要求しないボードゲームやパズルゲームなら、45のフレーム遅延であればユーザーが遅延に気づき始める前に上手くゲームを進められるかもしれません。
フレーム遅延を高く設定するもう一つの理由は、ロールバック中に発生しうるグリッチ(不具合)を排除することにあります。ロールバックが長くなればなるほど、間違った予測フレームを一時的に表示したことで、本来ないシーンを継ぎ接ぎした様子が表示される可能性が高くなるからです。例えば、ユーザーがボタンを押した瞬間にちょうど2フレーム分全画面がフラッシュする仕様のゲームがあったとします。フレーム遅延を1に設定し、パケット送信に4フレームかかったとすると、この場合はロールバックは約3フレーム分(4 - 1 = 3)になります。フラッシュがロールバックの最初のフレームに発生した場合、2フレームのフラッシュはロールバックで消えてしまい、遠隔で遊ぶプレイヤーはフラッシュの演出が見えなくなってしまいます。この場合、さらに高いフレーム遅延値を設定するか、ロールバック発生後までフラッシュを遅らせるようビデオレンダラーを再設計するか、のどちらかになります。
まずはゲームを遊ぶ感覚に影響を与えない範囲で、フレーム遅延を出来るだけ大きく設定してみてください。例えば格闘ゲームでドット単位の精度、寸分違わぬタイミング、非常に正確なアーケードコントローラーの操作が必要となります。このタイプのゲームでは、ほとんどの中級プレイヤーは2フレームの遅延に気き、上級プレイヤーであれば1フレームの遅延に気くこともあります。一方、厳密な操作を必要としないボードゲームやパズルゲームであれば、45のフレーム遅延を設定すればユーザーが気付く前に上手くゲームを進められるかもしれません。
フレーム遅延を大きく設定するもうひとつの理由は、ロールバック中に発生し得るグリッチ(不具合)を排除することにあります。ロールバックが長くなればなるほど、間違った予測フレームを一時的に実行したことによって生じた、本来存在しないシーンを継ぎ接ぎした様子が表示される可能性が高くなります。例えば、ユーザーがボタンを押した瞬間に2フレームの画面フラッシュが起きるゲームがあったとします。フレーム遅延を1に設定し、パケット送信に4フレームかかった場合、ロールバックは約3フレーム分(4 - 1 = 3)になります。フラッシュがロールバックの最初のフレームで発生した場合、2フレームのフラッシュはロールバックによって完全に消失してしまい、遠隔で遊ぶプレイヤーはフラッシュ演出を見ることができなくなります。この場合、さらに大きなフレーム遅延値を設定するか、ロールバック発生後までフラッシュを遅らせるようビデオレンダラーを再設計するのが良いでしょう。
## サンプルアプリケーション
ソースディレクトリ内のVector Warアプリケーションでは、2つのクライアントを同期するGGPOが搭載されています。コマンドライン引数は
ソースディレクトリ内のVector Warには、GGPOを使った2つのクライアントを同期する単純なアプリケーションが含まれています。コマンドライン引数は以下の通りです。
```
vectorwar.exe <localport> <num players> ('local' | <remote ip>:<remote port>) for each player
```
2~4プレイヤーゲームの始め方についての例は、binディレクトリにある.batファイルをご覧ください。
24プレイヤーでのゲーム開始方法の例については、binディレクトリにある.cmdファイルを参照してください。
## ベストプラクティスとトラブルシューティング
以下はGGPOへアプリを移植する際に検討したい、推薦できるベストプラクティスの一覧です。最初の段階からゲームを始めなくとも、ここで推奨している多くは簡単にまねることができます。多くのアプリケーションは推薦している以下の手法に準拠しています。
以下はアプリケーションをGGPO上で動作させる際に検討したいベストプラクティスの一覧です。これら推奨事項は、まだゲームを作り初めていない段階でも簡単に理解できます。多くのアプリケーションは既にほとんどの推奨事項を満たしています。
### ゲームステートを非ゲームステートから分離する
### 非ゲームステートとゲームステートを分ける
GGPOは定期的にゲームステート全体の保存と復元を要求します。ほとんどのゲームにおいて、保存が必要なステートはゲーム全体のごく一部です。通常、ビデオやオーディオレンダラー、テーブルの検索、テクスチャー、サウンドデータ、コードセグメントは、フレームごとに不変であるか、ゲームステートの計算には影響しません。これらを保存または復元する必要はありません。
GGPOは定期的にゲームにおける全ステートのセーブとロードを要求します。多くのゲームにとって、セーブが必要となるステートはゲーム全体のなかでも小さな要素にあたります。ビデオやオーディオレンダラー、テーブルの検索、テクスチャ―サウンドデータ、コードセグメントは大半の場合、フレームからフレームまで一定か、ゲームステートの計算には関与しません。これらはセーブや復元する必要がありません
できるだけゲーム以外の状態をゲームステートから分離する必要があります。例えば、全ゲームステートをC言語の構造体にカプセル化することを考えるかもしれません。これは、ゲームステートであるものとそうでないものが明確に区別され、保存と復元のコールバック実装が簡単になります(詳細についてはリファレンスガイドを参照してください)
ゲームステートから非ゲームステートを出来る限り分けましょう。例えば、全ゲームステートをC構造体へカプセル化を考えるかもしれません。この二つは、ゲームステートであるもの、そうでないもの、セーブやロード呼び出しの実行に必要でないものをはっきりと分けてくれます(さらなる情報はリファレンスガイドをご覧ください)。
### ゲームステートを進める際の固定時間を定義する
GGPOは、フレームごとにアプリケーションのロールバックとシングルステップ実行を必要とすることがあります。もしゲームステートを可変ティックレートで進めている場合、実行は困難になります。レンダーループがそうでない場合でも、フレームごとに固定時間単位でゲームステートを進めるようにしてください。
### ゲームステートを進める際の固定のタイムクオンタムを定義する
### ゲームループ内にあるレンダリングからゲームステートの更新を分離する
GGPOは時折、ロールバックやフレームごとにアプリに対してシングルステップ実行をする必要があります。もしあなたのゲームステートが変数のチックレートで進めているのであれば難しい実行です。レンダーループがそうでなくとも、ゲームステートの進行はフレームにつき固定のタイムクオンタムでするようにしてください
### ゲームの一連の流れをレンダリングするところから、ゲームステートのアップデートを分離する
GGPはロールバック中、事前フレームコールバックを何度も呼び出します。ロールバックが終わるまで、ロールバック中に発生したエフェクトやサウンドは遅らせる必要があります。これはレンダーステートからゲームステートを分離することで簡単に行うことができます。そうできたら、以下のようになるでしょう。
GGPOはロールバック中に、advance frameコールバックを何度も呼び出します。ロールバック中に発生するエフェクトやサウンドはロールバックが完了するまで先延ばしする必要があります。これはゲームステートとレンダーステートを分離することで最も簡単に実現できます。分離が出来たら、ゲームループは次のようになるでしょう
```
Bool finished = FALSE;
@@ -230,34 +221,29 @@ GGPはロールバック中、事前フレームコールバックを何度も
while (!finished);
```
言い換えると、ゲームステートは入力のみで決定され、レンダリングコードは現在のゲームステートによって動作をします。レンダリングなしで一連の入力をもとに簡単にゲームステートを進める方法であるべきです。
言い換えると、ゲームステートは入力のみで決定され、レンダリングは現在のゲームステートによって実行される必要があります。また、レンダリングせずに一連の入力をにゲームステートを簡単に進める方法が必要です。
### ゲームステートの進行が決定的であることを確認する
### ゲームステートの進行は決定性であること
ゲームステートを特定したら、必ず次のゲームステートはゲーム入力のみで計算されるようにしてください。ゲームステートと入力がすべて正しく特定できたなら、このことは自然と発生しますが、時として手が込む作業です。以下は見落としやすい内容です。
ゲームステートを特定したら、次のゲームステートが入力のみから計算されることを確認します。これは、全てのゲームステートと入力を正しく識別できていれば自然とそうなりますが、時には注意が必要です。見落とされがちなことをいくつか紹介します。
#### 乱数ジェネレーターに気を付ける
次のゲームステートを計算するうえで、多くのゲームは乱数を使用します。もし乱数を使っていたら、それが両プレイヤーがフレーム0の時に乱数ジェネレーターのシードが同一であること、そしてあなたのゲームステート内に乱数ジェネレーターのステート含まれていること、といった完全に決定性であることを必ず確認してください。この両方を確認すれば、GGPOが特定のフレームロールバックが何回も必要になろうとも、そのフレームで生成された乱数は必ず同一になります。
次のゲームステートを計算するうえで、多くのゲームは乱数を使用します。もし乱数を使う場合、それらが完全に決定的であること、乱数ジェネレーターのシードが両プレイヤーの0フレーム目で同じであること、乱数ジェネレーターの状態がゲームステート含まれていることを確認してください。これらのことが行われていれば、特定のフレームに対して生成される乱数は、GGPOがのフレームロールバックする回数に関係なく、常に同じ値になります。
#### 外部の時刻情報(壁時計時間)に気を付ける
#### 外部時間ソース(壁時計時刻)に気を付ける
ゲームステートの計算に現在時刻を使う場合は注意してください。ゲームに影響を与えたり、別のゲームステートに導く可能性があります(例: 乱数ジェネレーターのシードにタイマーを使う)。2台のコンピューターまたはゲームコンソールの時刻が同期することはほとんどないため、ゲームステートの計算に時刻を使用すると同期のトラブルに繋がります。ゲームステートに時刻を使うのを止めるか、プレイヤーの現在時刻をフレームへの入力の一部として含め、常にその時刻を使って計算を行う必要があります。
ゲームステートの計算にその日の現在時刻を用いる場合は注意してください。このことでゲームに影響を与える、または他のゲームステートを導く可能性がありま(例: タイマーを乱数ジェネレーターのシードに用いる)。2つのコンピューターもしくはゲームコンソールの時刻は、ほぼ同期することはなく、ゲームステート計算に時刻を使用すると同期の問題につながります。ゲームステートに時刻の使用する、またはフレームに入力する一部として、プレイヤーの1人のために現在時刻を含める、そして計算に時刻を常に用いるといったことは排除するべきです
ゲームステート以外の計算に外部の時刻情報を使う分には問題ありません(例: 画面上のエフェクト時間の計算やオーディオサンプルの減衰など)
非ゲームステート計算における外部時刻ソースの使用は構いません(例: 画面上の効果時間を計算、またはオーディオサンプルの減衰)。
### ダングリングポインターに気を付ける
ゲームステートに動的に割り当てられたメモリが含まれる場合、データの保存や復元の際に十分に気を付けながらポインターの再配置を行ってください。これを緩和するひとつの方法は、ポインターの代わりにベースとオフセットを使って割り当てられたメモリを参照することです。これにより再配置が必要なポインターの数を大幅に減らすことができます。
### ダングリングリファレンスに気を付ける
### 静的変数や隠れたステートに気を付ける
ゲームステートが動的に割り当てられたメモリを含んでいる場合、データをセーブ/ロードする際には、ポインタをリベースするセーブとロードファンクションに十分気を付けてください。緩和をする一つの方法として、ポインタの代わりに割り当てられたメモリを参照するため、ベースとオフセットを使用します。これでリベースが必要なポインタの数が大幅に削減できます
### 静的変数、またはほかの隠れたステートに気を付ける
あなたのゲームで用いられている言語は、全ステートを追跡するのを難しくさせている特徴があるかもしれません。Cにある静的自動変数はこの挙動の一例にあたります。すべてのロケーションを追跡して、セーブできるフォームへ変換する必要があります。例として、この2つを比較してください。まずはこちらから:
ゲームが記述されている言語には、全てのステートの追跡を困難にさせる機能があるかもしれません。C言語の静的自動変数はこの動作の一例です。該当する全ての箇所を探し出し、保存可能な形式に変換する必要があります。例えば、以下を見比べてください
```
// This will totally get you into trouble.
@@ -268,7 +254,7 @@ GGPはロールバック中、事前フレームコールバックを何度も
}
```
にこちらを:
のように書き換えます。
```
// If you must, this is better
static int global_counter = 0; /* move counter to a global */
@@ -288,15 +274,14 @@ GGPはロールバック中、事前フレームコールバックを何度も
}
```
### GGPO同期テスト機能をたくさん使用しましょう
### GGPO同期テスト機能をたくさん使ましょう
GGPOにあなたのアプリを移植したら、リークがありやすいゲームステートの可能性がある同期問題、この追跡ができる`ggpo_start_synctest`関数を使ことができます。
あなたのアプリケーションがGGPO上で動作するようになったら、`ggpo_start_synctest`関数を使ってゲームステートの漏れによる同期問題を追跡することができます。
この同期テストセッションは特別に作られもので、シミュレーション決定性内にあるエラーを探すために設計されたシングルプレヤーセッションです。同期テストセッション実行すると、GGPOはあなたのゲーム内でフレームごとに1フレームのロールバックを行います。初回に行った時のフレームステートとロールバック中に行ったステートを比較して、異なっていればエラーが浮上します。ゲーム実行中に`ggpo_log`関数を使用した場合、エラーを追跡するために初期フレームのログとロールバックフレームのログをdiffすることができます。
ゲームコードを書き込んでいる時に開発システムで継続的に同期テストを実行することで、非同期を発生するバグをすぐに見つけることができます。
この同期テストセッションは、シミュレーション決定論におけるエラーを探すために設計された特別なシングルプレヤーセッションです。同期テストセッション実行すると、GGPOは全てのフレームに対して1フレームのロールバックを行います。フレームが最初に実行されたときのステートとロールバック中に実行されたステートを比較し、それらが異なっていた場合はエラーを発生させます。ゲーム実行中に`ggpo_log`関数を使用すると、初回フレームのログとロールバックフレームのログを比較してエラーを追跡することができます。
ゲームコードを書いている時に開発システム上で同期テストを継続的に実行することで、同期ズレの原因となったバグをすぐに見つけることができます。
## さらに詳しく知りたい方は
この文章はGGPOの基本的な特徴を紹介しています。さらに知りたい方は、ggponet.hヘッダーにあるコメント、そしてコードを直接読むことをお勧めします。それではみなさん頑張ってください
このドキュメントではGGPOの基本的な機能について紹介しました。さらに知りたい方は、`ggponet.h`ヘッダーにあるコメント、そしてコードを直接読むことをお勧めします。それではみなさん頑張ってください!

View File

@@ -10,11 +10,11 @@ Your game probably has many moving parts. GGPO only depends on these two:
- **Game Inputs** are the set of things which modify the game state. These obviously include the joystick and button presses done by the player, but can include other non-obvious inputs as well. For example, if your game uses the current time of day to calculate something in the game, the current time of day at the beginning of a frame is also an input.
There are many other things in your game engine that are neither game state nor inputs. For example, your audio and video renderers are not game state since they dont have an effect on the outcome of the game. If you have a special effects engine thats generating effects that do not have an impact on the game, they can be excluded from the game state as well.
There are many other things in your game engine that are neither game state nor inputs. For example, your audio and video renderers are not game state since they don't have an effect on the outcome of the game. If you have a special effects engine that's generating effects that do not have an impact on the game, they can be excluded from the game state as well.
## Using State and Inputs for Synchronization
Each player in a GGPO networked game has a complete copy of your game running. GGPO needs to keep both copies of the game state in sync to ensure that both players are experiencing the same game. It would be much too expensive to send an entire copy of the game state between players every frame. Instead GGPO sends the players inputs to each other and has each player step the game forward. In order for this to work, your game engine must meet three criteria:
Each player in a GGPO networked game has a complete copy of your game running. GGPO needs to keep both copies of the game state in sync to ensure that both players are experiencing the same game. It would be much too expensive to send an entire copy of the game state between players every frame. Instead GGPO sends the players' inputs to each other and has each player step the game forward. In order for this to work, your game engine must meet three criteria:
- The game simulation must be fully deterministic. That is, for any given game state and inputs, advancing the game state by exactly 1 frame must result in identical game states for all players.
- The game state must be fully encapsulated and serializable.
@@ -30,7 +30,7 @@ GGPO is designed to be easy to interface with new and existing game engines. It
### Creating the GGPOSession Object
The `GGPOSession` object is your interface to the GGPO framework. Create one with the `ggponet_start_session` function passing the port to bind to locally and the IP address and port of the player youd like to play against. You should also pass in a `GGPOSessionCallbacks` object filled in with your games callback functions for managing game state and whether this session is for player 1 or player 2. All `GGPOSessionCallback` functions must be implemented. See the reference for more details.
The `GGPOSession` object is your interface to the GGPO framework. Create one with the `ggponet_start_session` function passing the port to bind to locally and the IP address and port of the player you'd like to play against. You should also pass in a `GGPOSessionCallbacks` object filled in with your game's callback functions for managing game state and whether this session is for player 1 or player 2. All `GGPOSessionCallback` functions must be implemented. See the reference for more details.
For example, to start a new session on the same host with another player bound to port 8001, you would do:
```
@@ -65,6 +65,7 @@ The `GGPOSession` object should only be used for a single game session. If you
```
### Sending Player Locations
When you created the GGPOSession object passed in the number of players participating in the game, but didn't actually describe how to contact them. To do so, call the `ggpo_add_player` function with a `GGPOPlayer` object describing each player. The following example show how you might use ggpo_add_player in a 2 player game:
```
@@ -83,6 +84,7 @@ result = ggpo_add_player(ggpo, &p2, &player_handles[1]);
```
### Synchronizing Local and Remote Inputs
Input synchronization happens at the top of each game frame. This is done by calling `ggpo_add_local_input` for each local player and `ggpo_synchronize_input` to fetch the inputs for remote players.
Be sure to check the return value of `ggpo_synchronize_inputs`. If it returns a value other than `GGPO_OK`, you should not advance your game state. This usually happens because GGPO has not received packets from the remote player in a while and has reached its internal prediction limit.
@@ -90,8 +92,8 @@ For example, if your code looks like this currently for a local game:
```
GameInputs &p1, &p2;
GetControllerInputs(0, &p1); /* read p1s controller inputs */
GetControllerInputs(1, &p2); /* read p2s controller inputs */
GetControllerInputs(0, &p1); /* read p1's controller inputs */
GetControllerInputs(1, &p2); /* read p2's controller inputs */
AdvanceGameState(&p1, &p2, &gamestate); /* send p1 and p2 to the game */
```
@@ -122,6 +124,7 @@ You should change it to read as follows:
You should call `ggpo_synchronize_inputs` every frame, even those that happen during a rollback. Make sure you always use the values returned from `ggpo_synchronize_inputs` rather than the values you've read from the local controllers to advance your game state. During a rollback `ggpo_synchronize_inputs` will replace the values passed into `ggpo_add_local_input` with the values used for previous frames. Also, if you've manually added input delay for the local player to smooth out the effect of rollbacks, the inputs you pass into `ggpo_add_local_input` won't actually be returned in `ggpo_synchronize_inputs` until after the frame delay.
### Implementing your save, load, and free Callbacks
GGPO will use the `load_game_state` and `save_game_state` callbacks to periodically save and restore the state of your game. The `save_game_state` function should create a buffer containing enough information to restore the current state of the game and return it in the `buffer` out parameter. The `load_game_state` function should restore the game state from a previously saved buffer. For example:
```
@@ -159,42 +162,50 @@ ggpo_free_buffer(void *buffer)
```
### Implementing Remaining Callbacks
As mentioned previously, there are no optional callbacks in the `GGPOSessionCallbacks` structure. They all need to at least `return true`, but the remaining callbacks do not necessarily need to be implemented right away. See the comments in `ggponet.h` for more information.
### Calling the GGPO Advance and Idle Functions
We're almost done. Promise. The last step is notify GGPO every time your gamestate finishes advancing by one frame. Just call `ggpo_advance_frame` after youve finished one frame but before youve started the next.
GGPO also needs some amount of time to send and receive packets do its own internal bookkeeping. At least once per-frame you should call the `ggpo_idle` function with the number of milliseconds youre allowing GGPO to spend.
We're almost done. Promise. The last step is notify GGPO every time your gamestate finishes advancing by one frame. Just call `ggpo_advance_frame` after you've finished one frame but before you've started the next.
GGPO also needs some amount of time to send and receive packets do its own internal bookkeeping. At least once per-frame you should call the `ggpo_idle` function with the number of milliseconds you're allowing GGPO to spend.
## Tuning Your Application: Frame Delay vs. Speculative Execution
GGPO uses both frame delay and speculative execution to hide latency. It does so by allowing the application developer the choice of how many frames that theyd like to delay input by. If it takes more time to transmit a packet than the number of frames specified by the game, GGPO will use speculative execution to hide the remaining latency. This number can be tuned by the application mid-game if you so desire. Choosing a proper value for the frame delay depends very much on your game. Here are some helpful hints.
GGPO uses both frame delay and speculative execution to hide latency. It does so by allowing the application developer the choice of how many frames that they'd like to delay input by. If it takes more time to transmit a packet than the number of frames specified by the game, GGPO will use speculative execution to hide the remaining latency. This number can be tuned by the application mid-game if you so desire. Choosing a proper value for the frame delay depends very much on your game. Here are some helpful hints.
In general you should try to make your frame delay as high as possible without affecting the qualitative experience of the game. For example, a fighting game requires pixel perfect accuracy, excellent timing, and extremely tightly controlled joystick motions. For this type of game, any frame delay larger than 1 can be noticed by most intermediate players, and expert players may even notice a single frame of delay. On the other hand, board games or puzzle games which do not have very strict timing requirements may get away with setting the frame latency as high as 4 or 5 before users begin to notice.
Another reason to set the frame delay high is to eliminate the glitching that can occur during a rollback. The longer the rollback, the more likely the user is to notice the discontinuities caused by temporarily executing the incorrect prediction frames. For example, suppose your game has a feature where the entire screen will flash for exactly 2 frames immediately after the user presses a button. Suppose further that youve chosen a value of 1 for the frame latency and the time to transmit a packet is 4 frames. In this case, a rollback is likely to be around 3 frames (4 1 = 3). If the flash occurs on the first frame of the rollback, your 2-second flash will be entirely consumed by the rollback, and the remote player will never get to see it! In this case, youre better off either specifying a higher frame latency value or redesigning your video renderer to delay the flash until after the rollback occurs.
Another reason to set the frame delay high is to eliminate the glitching that can occur during a rollback. The longer the rollback, the more likely the user is to notice the discontinuities caused by temporarily executing the incorrect prediction frames. For example, suppose your game has a feature where the entire screen will flash for exactly 2 frames immediately after the user presses a button. Suppose further that you've chosen a value of 1 for the frame latency and the time to transmit a packet is 4 frames. In this case, a rollback is likely to be around 3 frames (4 1 = 3). If the flash occurs on the first frame of the rollback, your 2-second flash will be entirely consumed by the rollback, and the remote player will never get to see it! In this case, you're better off either specifying a higher frame latency value or redesigning your video renderer to delay the flash until after the rollback occurs.
## Sample Application
The Vector War application in the source directory contains a simple application which uses GGPO to synchronize the two clients. The command line arguments are:
```
vectorwar.exe <localport> <num players> ('local' | <remote ip>:<remote port>) for each player
```
See the .bat files in the bin directory for examples on how to start 2, 3, and 4 player games.
See the .cmd files in the bin directory for examples on how to start 2, 3, and 4 player games.
## Best Practices and Troubleshooting
Below is a list of recommended best practices you should consider while porting your application to GGPO. Many of these recommendations are easy to follow even if youre not starting a game from scratch. Most applications will already conform to most of the recommendations below.
Below is a list of recommended best practices you should consider while porting your application to GGPO. Many of these recommendations are easy to follow even if you're not starting a game from scratch. Most applications will already conform to most of the recommendations below.
### Isolate Game State from Non-Game State
GGPO will periodically request that you save and load the entire state of your game. For most games the state that needs to be saved is a tiny fraction of the entire game. Usually the video and audio renderers, look up tables, textures, sound data and your code segments are either constant from frame to frame or not involved in the calculation of game state. These do not need to be saved or restored.
You should isolate non-game state from the game state as much as possible. For example, you may consider encapsulating all your game state into a single C structure. This both clearly delineates what is game state and was is not and makes it trivial to implement the save and load callbacks (see the Reference Guide for more information).
### Define a Fixed Time Quanta for Advancing Your Game State
GGPO will occasionally need to rollback and single-step your application frame by frame. This is difficult to do if your game state advances by a variable tick rate. You should try to make your game state advanced by a fixed time quanta per frame, even if your render loop does not.
### Separate Updating Game State from Rendering in Your Game Loop
GGPO will call your advance frame callback many times during a rollback. Any effects or sounds which are genearted during the rollback need to be deferred until after the rollback is finished. This is most easily accomplished by separating your game state from your render state. When youre finished, your game loop may look something like this:
GGPO will call your advance frame callback many times during a rollback. Any effects or sounds which are genearted during the rollback need to be deferred until after the rollback is finished. This is most easily accomplished by separating your game state from your render state. When you're finished, your game loop may look something like this:
```
Bool finished = FALSE;
@@ -213,20 +224,25 @@ GGPO will call your advance frame callback many times during a rollback. Any ef
In other words, your game state should be determined solely by the inputs, your rendering code should be driven by the current game state, and you should have a way to easily advance the game state forward using a set of inputs without rendering.
### Make Sure Your Game State Advances Deterministically
Once you have your game state identified, make sure the next game state is computed solely from your game inputs. This should happen naturally if you have correctly identified all the game state and inputs, but it can be tricky sometimes. Here are some things which are easy to overlook:
#### Beware of Random Number Generators
Many games use random numbers in the computing of the next game state. If you use one, you must ensure that they are fully deterministic, that the seed for the random number generator is same at frame 0 for both players, and that the state of the random number generator is included in your game state. Doing both of these will ensure that the random numbers which get generated for a particular frame are always the same, regardless of how many times GGPO needs to rollback to that frame.
#### Beware of External Time Sources (aka. Wall clock time)
Be careful if you use the current time of day in your game state calculation. This may be used for an effect on the game or to derive other game state (e.g. using the timer as a seed to the random number generator). The time on two computers or game consoles is almost never in sync and using time in your game state calculations can lead to synchronization issues. You should either eliminate the use of time in your game state or include the current time for one of the players as part of the input to a frame and always use that time in your calculations.
The use of external time sources in non-gamestate calculations is fine (e.g. computing the duration of effects on screen, or the attenuation of audio samples).
### Beware of Dangling References
If your game state contains any dynamically allocated memory be very careful in your save and load functions to rebase your pointers as you save and load your data. One way to mitigate this is to use a base and offset to reference allocated memory instead of a pointer. This can greatly reduce the number of pointers you need to rebase.
### Beware of Static Variables or Other Hidden State
The language your game is written in may have features which make it difficult to track down all your state. Static automatic variables in C are an example of this behavior. You need to track down all these locations and convert them to a form which can be saved. For example, compare:
```
@@ -259,11 +275,13 @@ To:
```
### Use the GGPO SyncTest Feature. A Lot.
Once youve ported your application to GGPO, you can use the `ggpo_start_synctest` function to help track down synchronization issues which may be the result of leaky game state.
Once you've ported your application to GGPO, you can use the `ggpo_start_synctest` function to help track down synchronization issues which may be the result of leaky game state.
The sync test session is a special, single player session which is designed to find errors in your simulation's determinism. When running in a synctest session, GGPO will execute a 1 frame rollback for every frame of your game. It compares the state of the frame when it was executed the first time to the state executed during the rollback, and raises an error if they differ. If you used the `ggpo_log` function during your game's execution, you can diff the log of the initial frame vs the log of the rollback frame to track down errors.
By running synctest on developer systems continuously when writing game code, you can identify desync causing bugs immediately after they're introduced.
## Where to Go from Here
This document describes the most basic features of GGPO. To learn more, I recommend starting with reading the comments in the `ggponet.h` header and just diving into the code. Good luck!

View File

@@ -4,15 +4,13 @@
従来の技術はプレイヤーの入力に遅延を織り込んで通信を行っており、その結果反応が遅く、ラグを感じるプレイ感になっていました。ロールバックネットワーキングは入力予測と投機的実行を行って、プレイヤーの入力を即座に送信するため、遅延を感じさせないネット環境をもたらします。ロールバックがあれば、タイミングや相手の動きや効果音に対する反応、指が覚えている入力、これらオフラインで行えた内容が、そのままオンラインでも行えます。GGPOネットワーキングSDKは、ロールバックネットワーキングを新作や発売されているゲームに極力簡単に組み込めるよう作られています。
# 仕組み
ロールバックネットワーキングは決定的P2Pエンジンに統合できるよう設計されています。完全に決定的なエンジンなら、同じ入力をした場合にゲームは必ず同じ内容のプログラム再生をします。その内容を実現する一つの方法としては、ネットワーク上の全プレイヤーと入力のやりとりをする方法があげられますが、これは全プレイヤーがピアから入力を全て受け取った時にのみゲームプレイロジックが1フレームだけ実行される形になります。この方法ではゲーム内でキャラの動きがぎくしゃくし、反応の悪いゲーム内容になりがちです。ネットワークを介して入力を受け取る時間が長くなるほど、ゲーム展開も遅くなってしまいます。
## 入力遅延を用いたネットワーキング
### 理論上は…
### 理論上は…
下の図を見てください。2つのクライアントが遅延0msの理想的なネットワークで同期されている図になっています。1プレイヤー側の入力が青、2プレイヤー側の入力は赤、ネットワーク層は緑です。黒の矢印は入力がシステム内で送信され、ゲームステートが推移する流れを表します。各フレームは破線で区切られています。図は1プレイヤー側から見たものになっていますが、2プレイヤー側も全く同じ手順になっています。
@@ -20,7 +18,6 @@
1プレイヤーの入力は、ネットワーク層によって2プレイヤーの入力とマージされ、ゲームエンジンに送信されます。エンジンはその入力を用いて現在のフレームのゲームステートを変更します。2プレイヤー側も同様に自分と1プレイヤーの入力をマージしてゲームエンジンに送信します。プレイヤーの入力に応じたロジックを適用し、過去のフレームのゲームステートを変更しながら、フレームごとにゲームが進行します。1プレイヤーと2プレイヤー両方が同じゲームステートで、それぞれのエンジンに送信される入力が同じなので、両プレイヤーのゲームステートは毎フレーム同期されたままになります。
### 実際は…
理想的なネットワークの例では、パケットがネットワークを介して即時に送信されるものとされていますが、現実はそう甘くはありません。一般的なブロードバンド接続では、プレイヤー間の距離や回線の品質に応じて、パケットの送信に5150msかかります。ゲームが1秒間に60フレームで実行されるとするならば、遅延は19フレーム相当になります。
@@ -31,7 +28,6 @@
この例では、パケットの送信に3フレームかかります。2プレイヤーによって遠隔から送信された入力は、1フレーム目に1プレイヤー側に届かず、3フレーム後になるまでゲーム機に届きません。1プレイヤー側のゲームエンジンは入力を受信するまでゲームを進めることができないので、1フレーム目を3フレーム遅延せざるを得なくなります。続きのフレームも同様に3フレームの遅延が発生します。ネットワーク層は両プレイヤー間で送信されるパケットの最長転送時間だけ、マージされた入力を遅延せざるを得なくなります。理想的なネットワーク環境を除いては、大半のゲームジャンルにおいてこのラグはプレイ感に大きく影響を与えることとなります。
## ロールバックネットワーキングで入力遅延を取り除く
### 投機的実行
@@ -44,15 +40,14 @@ GGPOは遠隔のプレイヤーから入力が届くのを待つ代わりに、
GGPOの予測が完璧であれば、オンラインで遊ぶユーザー体験はオフラインと同一のものになります。もちろん、未来を予測することは誰にもできませんGGPOも2プレイヤーの入力を間違って予測することがあります。上の図をもう一度見てください。もしGGPOが1フレーム目に2プレイヤーに間違った入力を送信したらどうなるでしょうか。1プレイヤー側に表示される2プレイヤーの入力は、2プレイヤー側で表示されるものと異なってしまいます。両サイドのゲームは同期を失い、プレイヤーは違ったゲーム画面を見ながら相手の動きに反応することになります。同期のズレは、1プレイヤー側が2プレイヤーの正しい入力が届く4フレーム目まで検出することができませんが、それでは遅すぎます。
そういうことから、GGPOの手法は「投機的実行(speculative execution)」と呼ばれます。遊んでいるプレイヤーがその時に見ているものは正しいかもしれませんが、そうでないこともあります。GGPOが遠隔プレイヤーの入力を誤って予測した場合、次のフレームへ進める前にエラーを修正する必要があります。次の例では、その方法を説明します。
### 投機的実行エラーをロールバックで修正する
GGPOは遠隔プレイヤーの入力を間違って予測する度に、ロールバックを使ってクライアントを再同期します。「ロールバック」という単語は、ステートを巻き戻し、プレイヤーの入力に関する、より正しく新しい情報を元に結果を予測する過程を指します。前のセクションでは、遠隔の入力1における予測したフレームが間違っていたらどうなるか、ということについて考えました。それでは、GGPOがエラーを修正する過程を見てみましょう。
![](images/overview_image5.png)
GGPOは遠隔の入力を受信したら、その都度前回のフレームで予測した品質をチェックします。先程触れたように、GGPOは4フレーム目まで2プレイヤー側の入力が届きません。4フレーム目で、GGPOは以前に予測した入力とネットワークから受信した入力が一致しないことに気付きます。両サイドのゲームを再同期するため、GGPOは3フレーム分の誤った入力によって発生したダメージや間違いを取り消す必要があります。誤って予測した入力を送信する前のフレームまで戻るよう、ゲームエンジンに要求します(つまり過去のステートまで「ロールバック」します)。以前のステートを復元したら、GGPOはエンジンに正しい入力で1フレーム進めるよう要求します。このフレームは水色で示しています。ゲームエンジンはこのフレームをユーザーに見えない形で出来る限り素早く進める必要があります。例えば、ビデオレンダラーはこのフレームを画面に描写するべきではありません。オーディオレンダラーは原則、音声を生成し続けるべきですが、ロールバックが終わるまでレンダーすべきではなく、サンプルが生成されたフレームを引いた現在のフレームであるnフレームでサンプルがスタートする必要があります。エンジンがGGPOがエラーを見つける前のフレームまで到達したら、GGPOはロールバックモードを止め、ゲームを通常どおり進めることを許可します。図の5フレームと6フレーム目はGGPOの予測が正しく行われた場合を示しています。ゲームステートが正しいので、ロールバックをする理由はありません。
GGPOは遠隔の入力を受信したら、その都度前回のフレームで予測した品質をチェックします。先程触れたように、GGPOは4フレーム目まで2プレイヤー側の入力が届きません。4フレーム目で、GGPOは以前に予測した入力とネットワークから受信した入力が一致しないことに気付きます。両サイドのゲームを再同期するため、GGPOは3フレーム分の誤った入力によって発生したダメージや間違いを取り消す必要があります。誤って予測した入力を送信する前のフレームまで戻るよう、ゲームエンジンに要求します(つまり過去のステートまで「ロールバック」します)。以前のステートを復元したら、GGPOはエンジンに正しい入力で1フレーム進めるよう要求します。このフレームは水色で示しています。ゲームエンジンはこのフレームをユーザーに見えない形で出来る限り素早く進める必要があります。例えば、ビデオレンダラーはこのフレームを画面に描写するべきではありません。オーディオレンダラーは原則、音声を生成し続けるべきですが、ロールバックが終わるまでレンダーすべきではなく、サンプルが生成されたフレームを引いた現在のフレームであるnフレームでサンプルがスタートする必要があります。
エンジンがGGPOがエラーを見つける前のフレームまで到達したら、GGPOはロールバックモードを止め、ゲームを通常どおり進めることを許可します。図の5フレームと6フレーム目はGGPOの予測が正しく行われた場合を示しています。ゲームステートが正しいので、ロールバックをする理由はありません。
# コード構造
@@ -60,42 +55,34 @@ GGPOは遠隔の入力を受信したら、その都度前回のフレームで
![](images/overview_image4.png)
## GGPOインタフェース(GGPO Interface)
GGPOインターフェスはP2Pと同期テストバックエンド間の詳細な実装を抽象化しています。適切なバックエンドは`ggpo_start_session``ggpo_start_synctest`エントリーポイントを呼び出した時に、自動的に生成されます。
GGPOインターフェスはP2Pと同期テストバックエンド間の詳細な実装を抽象化しています。適切なバックエンドは`ggpo_start_session``ggpo_start_synctest`エントリーポイントを呼び出した時に、自動的に生成されます。
## P2Pバックエンド(P2P Backend)
P2Pバックエンドはプレイヤー間でゲームを調整します。`ggpo_start_session` APIの呼び出しによって生成されます。大きな情報の処理の大半は含まれているヘルパークラスによって行われます。
## ポーリングオブジェクト(Poll Object)
(図にはありません)ポーリングオブジェクトはコード内で他のオブジェクトによって用いられる登録方式です。待機可能なオブジェクトが準備できたときに通知とタイマーを送信します。例としてUDPバックエンドは新たなパケットが到着したときに、通知を受信するためポーリングオブジェクトを使用します。
## 同期オブジェクト(Sync Object)
同期オブジェクトはゲームステートのnフレームを追跡するために用いられます。埋め込まれた予測(prediction)オブジェクトが予測エラーを通知された時、同期バックエンドがより正確なステートまでゲームを巻き戻し、予測エラーを修正するためシングルステップ処理を進めます。
## 入力キューオブジェクト(Input Queue Object)
入力キューオブジェクトはローカル、または遠隔プレイヤー用に受信した全入力を追跡します。所持していない入力を要求された場合、入力キューは次の入力を予測し、後の情報を追跡します。そうすることで同期オブジェクトは予測が誤った場合にどこまでロールバックすればよいのか分かります。リクエストがあった場合、入力キューはフレーム遅延も実行します。
## UDPプロトコルオブジェクト(UDP Protocol Object)
UDPプロトコルオブジェクトは両プレイヤー間の同期と入力交換プロトコルを扱います。また、ゲーム入力の圧縮と信頼できるUDP層も実装しています。各UDPプロトコルオブジェクトにはTimeSyncオブジェクトが含まれ、プレイヤー間の時間のずれを推測するために利用しています。
## UDPオブジェクト(UDP Object)
UDPオブジェクトは単純なUDPパケットの送受信を行います。他のプラットフォームへの移植を簡単にするため、UDPプロトコルから切り離されています。
## 同期テストバックエンド(Sync Test Backend)
(図にはありません)同期テストバックエンドは、P2Pバックエンドがアプリのセーブステートと決定的に機能上実行していることを確認するときに同じ同期オブジェクトを使用します。同期テストの使用に関する詳しい情報は、開発者向けガイドを参照してください。
(図にはありません)同期テストバックエンドは、P2Pバックエンドがアプリのセーブステートと決定的に機能上実行していることを確認するときに同じ同期オブジェクトを使用します。同期テストの使用に関する詳しい情報は、開発者ガイドを参照してください。

View File

@@ -1,4 +1,4 @@
# What Is GGPO?
# What's GGPO?
Created in 2009, the GGPO networking SDK pioneered the use of rollback networking in peer-to-peer games. It's designed specifically to hide network latency in fast paced, twitch style games which require very precise inputs and frame perfect execution.
@@ -12,42 +12,42 @@ Rollback networking is designed to be integrated into a fully deterministic peer
### In Theory...
Take a look at the diagram below. It shows how 2 clients are kept synchronized in an ideal network with 0 milliseconds of latency. Player 1s inputs and game state are shown in blue, player 2s inputs are shown in red, and the network layer is shown in green. The black arrows indicate how inputs move through the system and transitions from one game state to the next. Each frame is separated by a horizontal, dashed line. Although the diagram only shows what happens from the perspective of player 1, the game on player 2s end goes through the exact same steps.
Take a look at the diagram below. It shows how 2 clients are kept synchronized in an ideal network with 0 milliseconds of latency. Player 1's inputs and game state are shown in blue, player 2's inputs are shown in red, and the network layer is shown in green. The black arrows indicate how inputs move through the system and transitions from one game state to the next. Each frame is separated by a horizontal, dashed line. Although the diagram only shows what happens from the perspective of player 1, the game on player 2's end goes through the exact same steps.
![](images/overview_image1.png)
The inputs for player 1 are merged with the inputs from player 2 by the network layer before sending them to the game engine. The engine modifies the game state for the current frame using those inputs. Player 2 does the same thing: merging player 1s inputs with his own before sending the combined inputs to the game engine. The game proceeds in this manner every frame, modifying the previous frame's game state by applying logic according to the value of the the player inputs. Since player 1 and player 2 both began with the same game state and the inputs they send to their respective engines are the same, the game states of the two players will remain synchronized on every frame.
The inputs for player 1 are merged with the inputs from player 2 by the network layer before sending them to the game engine. The engine modifies the game state for the current frame using those inputs. Player 2 does the same thing: merging player 1's inputs with his own before sending the combined inputs to the game engine. The game proceeds in this manner every frame, modifying the previous frame's game state by applying logic according to the value of the the player inputs. Since player 1 and player 2 both began with the same game state and the inputs they send to their respective engines are the same, the game states of the two players will remain synchronized on every frame.
### In Practice..
The Ideal Network example assumes that packets are transmitted over the network instantaneously. Reality isnt quite so rosy. Typical broadband connections take anywhere between 5 and 150 milliseconds to transmit a packet, depending on the distance between the players and the quality of the infrastructure where the players live. That could be anywhere between 1 and 9 frames if your game runs at 60 frames per seconds.
The Ideal Network example assumes that packets are transmitted over the network instantaneously. Reality isn't quite so rosy. Typical broadband connections take anywhere between 5 and 150 milliseconds to transmit a packet, depending on the distance between the players and the quality of the infrastructure where the players live. That could be anywhere between 1 and 9 frames if your game runs at 60 frames per seconds.
Since the game cannot process the frame until it has received the inputs from both players, it must apply 1 to 9 frames of delay, or lag, on each players inputs. Lets modify the previous diagram to take latency into account:
Since the game cannot process the frame until it has received the inputs from both players, it must apply 1 to 9 frames of delay, or "lag", on each player's inputs. Let's modify the previous diagram to take latency into account:
![](images/overview_image3.png)
In this example it takes 3 frames to transmit a packet. This means the remote inputs sent by player 2 at frame 1 dont arrive at player 1s game console until 3 frames later. The game engine for player 1 cannot advance until it receives the input, so its forced to delay the frame 1 for 3 frames. All subsequent frames are delayed by 3 frames as well. The network layer is generally forced to delay all merged inputs by the maximum one way transit time of the packets sent between the two players. This lag is enough to substantially affect the quality of the game play experience for many game types in all but the most ideal networking conditions.
In this example it takes 3 frames to transmit a packet. This means the remote inputs sent by player 2 at frame 1 don't arrive at player 1's game console until 3 frames later. The game engine for player 1 cannot advance until it receives the input, so it's forced to delay the frame 1 for 3 frames. All subsequent frames are delayed by 3 frames as well. The network layer is generally forced to delay all merged inputs by the maximum one way transit time of the packets sent between the two players. This lag is enough to substantially affect the quality of the game play experience for many game types in all but the most ideal networking conditions.
## Removing Input Delay with Rollback Networking
### Speculative Execution
GGPO prevents the input lag by hiding the latency required to send a packet using speculative execution. Lets see another diagram:
GGPO prevents the input lag by hiding the latency required to send a packet using speculative execution. Let's see another diagram:
![](images/overview_image2.png)
Instead of waiting for the input to arrive from the remote player, GGPO predicts what the other player is likely to do based on past inputs. It combines the predicted input with player 1s local input and immediately passes the merged inputs to your game engine so it can proceed executing the next frame, even though you have not yet received the packet containing the inputs from the other player.
If GGPOs prediction were perfect, the user experience playing online would be identical to playing offline. Of course, no one can predict the future! GGPO will occasionally incorrectly predict player 2s inputs. Take another look at the diagram above. What happens if GGPO sent the wrong inputs for player 2 at frame 1? The inputs for player 2 would be different on player 1s game than in player 2s. The two games will lose synchronization and the players will be left interacting with different versions of reality. The synchronization loss cannot possibly be discovered until frame 4 when player 1 receives the correct inputs for player 2, but by then its too late.
This is why GGPOs method is called speculative execution. What the current player sees at the current frame may be correct, but it may not be. When GGPO incorrectly predicts the inputs for the remote player, it needs to correct that error before proceeding on to the next frame. The next example explains how that happens.
Instead of waiting for the input to arrive from the remote player, GGPO predicts what the other player is likely to do based on past inputs. It combines the predicted input with player 1's local input and immediately passes the merged inputs to your game engine so it can proceed executing the next frame, even though you have not yet received the packet containing the inputs from the other player.
If GGPO's prediction were perfect, the user experience playing online would be identical to playing offline. Of course, no one can predict the future! GGPO will occasionally incorrectly predict player 2's inputs. Take another look at the diagram above. What happens if GGPO sent the wrong inputs for player 2 at frame 1? The inputs for player 2 would be different on player 1's game than in player 2's. The two games will lose synchronization and the players will be left interacting with different versions of reality. The synchronization loss cannot possibly be discovered until frame 4 when player 1 receives the correct inputs for player 2, but by then it's too late.
This is why GGPO's method is called "speculative execution". What the current player sees at the current frame may be correct, but it may not be. When GGPO incorrectly predicts the inputs for the remote player, it needs to correct that error before proceeding on to the next frame. The next example explains how that happens.
### Correcting Speculative Execution Errors with Rollbacks
GGPO uses rollbacks to resynchronize the clients whenever it incorrectly predicts what the remote player will do. The term "rollback" refers to the process of rewinding state and predicting new outcomes based on new, more correct information about a player's input. In the previous section we wondered what would happen if the predicted frame for remote input 1 was incorrect. Lets see how GGPO corrects the error:
GGPO uses rollbacks to resynchronize the clients whenever it incorrectly predicts what the remote player will do. The term "rollback" refers to the process of rewinding state and predicting new outcomes based on new, more correct information about a player's input. In the previous section we wondered what would happen if the predicted frame for remote input 1 was incorrect. Let's see how GGPO corrects the error:
![](images/overview_image5.png)
GGPO checks the quality of its prediction for previous frames every time it receives a remote input. As mentioned earlier, GGPO doesnt receive the inputs for player 2s first frame until player 1s fourth. At frame 4, GGPO notices that the inputs received from the network do not match the predicted inputs sent earlier. To resynchronize the two games, GGPO needs to undo the damage caused by running the game with incorrect inputs for 3 frames. It does this by asking the game engine to go back in time to a frame before the erroneously speculated inputs were sent (i.e. to "rollback" to a previous state). Once the previous state has been restored, GGPO asks the engine to move forward one frame at a time with the corrected input stream. These frames are shown in light blue. Your game engine should advance through these frames as quickly as possible with no visible effect to the user. For example, your video renderer should not draw these frames to the screen. Your audio renderer should ideally continue to generate audio, but it should not be rendered until after the rollback, at which point samples should start playing n frames in, where n is the current frame minus the frame where the sample was generated.
Once your engine reaches the frame it was on before GGPO discovered the error, GGPO drops out of rollback mode and allows the game to proceed as normal. Frames 5 and 6 in the diagram show what happens when GGPO predicts correctly. Since the game state is correct, theres no reason to rollback.
GGPO checks the quality of its prediction for previous frames every time it receives a remote input. As mentioned earlier, GGPO doesn't receive the inputs for player 2's first frame until player 1's fourth. At frame 4, GGPO notices that the inputs received from the network do not match the predicted inputs sent earlier. To resynchronize the two games, GGPO needs to undo the damage caused by running the game with incorrect inputs for 3 frames. It does this by asking the game engine to go back in time to a frame before the erroneously speculated inputs were sent (i.e. to "rollback" to a previous state). Once the previous state has been restored, GGPO asks the engine to move forward one frame at a time with the corrected input stream. These frames are shown in light blue. Your game engine should advance through these frames as quickly as possible with no visible effect to the user. For example, your video renderer should not draw these frames to the screen. Your audio renderer should ideally continue to generate audio, but it should not be rendered until after the rollback, at which point samples should start playing n frames in, where n is the current frame minus the frame where the sample was generated.
Once your engine reaches the frame it was on before GGPO discovered the error, GGPO drops out of rollback mode and allows the game to proceed as normal. Frames 5 and 6 in the diagram show what happens when GGPO predicts correctly. Since the game state is correct, there's no reason to rollback.
# Code Structure
@@ -81,8 +81,8 @@ The UDP protocol object handles the synchronization and input exchange protocols
## UDP Object
The UDP object is simply a dumb UDP packet sender/receiver. Its divorced from UDP protocol to ease ports to other platforms.
The UDP object is simply a dumb UDP packet sender/receiver. It's divorced from UDP protocol to ease ports to other platforms.
## Sync Test Backend
(not pictured) The Sync Test backend uses the same Sync object as the P2P backend to verify your applications save state and stepping functionality execute deterministically. For more information on sync test uses, consult the Developer Guide.
(not pictured) The Sync Test backend uses the same Sync object as the P2P backend to verify your application's save state and stepping functionality execute deterministically. For more information on sync test uses, consult the Developer Guide.

View File

@@ -11,11 +11,14 @@ target_include_directories(GGPO PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/lib/ggpo>
)
if(WIN32 AND BUILD_SHARED_LIBS)
# Link to Multimedia API and Winsocks during a shared build.
target_link_libraries(GGPO PUBLIC winmm.lib ws2_32.lib)
add_definitions(-DGGPO_SHARED_LIB)
add_definitions(-DGGPO_SDK_EXPORT)
if(WIN32)
target_compile_options(GGPO PRIVATE "/W4" "/WX")
if(BUILD_SHARED_LIBS)
# Link to Multimedia API and Winsocks during a shared build.
target_link_libraries(GGPO PUBLIC winmm.lib ws2_32.lib)
add_definitions(-DGGPO_SHARED_LIB)
add_definitions(-DGGPO_SDK_EXPORT)
endif()
endif()
set_target_properties(GGPO PROPERTIES VERSION ${PROJECT_VERSION})

View File

@@ -30,6 +30,13 @@ if(UNIX)
)
endif()
if(WIN32)
set(GGPO_LIB_SRC_NOFILTER
${GGPO_LIB_SRC_NOFILTER}
"lib/ggpo/platform_windows.cpp"
)
endif()
set(GGPO_LIB_INC_NETWORK
"lib/ggpo/network/udp.h"
"lib/ggpo/network/udp_msg.h"

View File

@@ -14,3 +14,4 @@ add_definitions(-D_UNICODE -DUNICODE)
# Link against GGPO, winmm (Windows Multimedia API), and ws2_32 (Winsock).
target_link_libraries(VectorWar LINK_PUBLIC GGPO winmm.lib ws2_32.lib)
target_compile_options(VectorWar PRIVATE "/W4" "/WX")

View File

@@ -89,13 +89,13 @@ void GameState::ParseShipInputs(int inputs, int i, double *heading, double *thru
*fire = inputs & INPUT_FIRE;
}
void GameState::MoveShip(int i, double heading, double thrust, int fire)
void GameState::MoveShip(int which, double heading, double thrust, int fire)
{
Ship *ship = _ships + i;
Ship *ship = _ships + which;
ggpo_log(ggpo, "calculation of new ship coordinates: (thrust:%.4f heading:%.4f).\n", thrust, heading);
ship->heading = heading;
ship->heading = (int)heading;
if (ship->cooldown == 0) {
if (fire) {

View File

@@ -52,11 +52,11 @@ GDIRenderer::Draw(GameState &gs, NonGameState &ngs)
SetTextColor(hdc, _shipColors[i]);
SelectObject(hdc, _shipPens[i]);
DrawShip(hdc, i, gs);
DrawConnectState(hdc, gs._ships[i], ngs.players[i], _shipColors[i]);
DrawConnectState(hdc, gs._ships[i], ngs.players[i]);
}
SetTextAlign(hdc, TA_BOTTOM | TA_CENTER);
TextOutA(hdc, (_rc.left + _rc.right) / 2, _rc.bottom - 32, _status, strlen(_status));
TextOutA(hdc, (_rc.left + _rc.right) / 2, _rc.bottom - 32, _status, (int)strlen(_status));
SetTextColor(hdc, RGB(192, 192, 192));
RenderChecksum(hdc, 40, ngs.periodic);
@@ -71,15 +71,15 @@ void
GDIRenderer::RenderChecksum(HDC hdc, int y, NonGameState::ChecksumInfo &info)
{
char checksum[128];
sprintf(checksum, "Frame: %04d Checksum: %08x", info.framenumber, info.checksum);
TextOutA(hdc, (_rc.left + _rc.right) / 2, _rc.top + y, checksum, strlen(checksum));
sprintf_s(checksum, ARRAYSIZE(checksum), "Frame: %04d Checksum: %08x", info.framenumber, info.checksum);
TextOutA(hdc, (_rc.left + _rc.right) / 2, _rc.top + y, checksum, (int)strlen(checksum));
}
void
GDIRenderer::SetStatusText(const char *text)
{
strcpy(_status, text);
strcpy_s(_status, text);
}
void
@@ -110,7 +110,7 @@ GDIRenderer::DrawShip(HDC hdc, int which, GameState &gs)
int i;
for (i = 0; i < ARRAY_SIZE(shape); i++) {
int newx, newy;
double newx, newy;
double cost, sint, theta;
theta = (double)ship->heading * PI / 180;
@@ -120,27 +120,27 @@ GDIRenderer::DrawShip(HDC hdc, int which, GameState &gs)
newx = shape[i].x * cost - shape[i].y * sint;
newy = shape[i].x * sint + shape[i].y * cost;
shape[i].x = newx + ship->position.x;
shape[i].y = newy + ship->position.y;
shape[i].x = (LONG)(newx + ship->position.x);
shape[i].y = (LONG)(newy + ship->position.y);
}
Polyline(hdc, shape, ARRAY_SIZE(shape));
for (int i = 0; i < MAX_BULLETS; i++) {
for (i = 0; i < MAX_BULLETS; i++) {
if (ship->bullets[i].active) {
bullet.left = ship->bullets[i].position.x - 1;
bullet.right = ship->bullets[i].position.x + 1;
bullet.top = ship->bullets[i].position.y - 1;
bullet.bottom = ship->bullets[i].position.y + 1;
bullet.left = (LONG)ship->bullets[i].position.x - 1;
bullet.right = (LONG)ship->bullets[i].position.x + 1;
bullet.top = (LONG)ship->bullets[i].position.y - 1;
bullet.bottom = (LONG)ship->bullets[i].position.y + 1;
FillRect(hdc, &bullet, _bulletBrush);
}
}
SetTextAlign(hdc, alignments[which]);
sprintf(buf, "Hits: %d", ship->score);
TextOutA(hdc, text_offsets[which].x, text_offsets[which].y, buf, strlen(buf));
sprintf_s(buf, ARRAYSIZE(buf), "Hits: %d", ship->score);
TextOutA(hdc, text_offsets[which].x, text_offsets[which].y, buf, (int)strlen(buf));
}
void
GDIRenderer::DrawConnectState(HDC hdc, Ship &ship, PlayerConnectionInfo &info, COLORREF color)
GDIRenderer::DrawConnectState(HDC hdc, Ship &ship, PlayerConnectionInfo &info)
{
char status[64];
static const char *statusStrings[] = {
@@ -154,34 +154,34 @@ GDIRenderer::DrawConnectState(HDC hdc, Ship &ship, PlayerConnectionInfo &info, C
*status = '\0';
switch (info.state) {
case Connecting:
sprintf(status, (info.type == GGPO_PLAYERTYPE_LOCAL) ? "Local Player" : "Connecting...");
sprintf_s(status, ARRAYSIZE(status), (info.type == GGPO_PLAYERTYPE_LOCAL) ? "Local Player" : "Connecting...");
break;
case Synchronizing:
progress = info.connect_progress;
sprintf(status, (info.type == GGPO_PLAYERTYPE_LOCAL) ? "Local Player" : "Synchronizing...");
sprintf_s(status, ARRAYSIZE(status), (info.type == GGPO_PLAYERTYPE_LOCAL) ? "Local Player" : "Synchronizing...");
break;
case Disconnected:
sprintf(status, "Disconnected");
sprintf_s(status, ARRAYSIZE(status), "Disconnected");
break;
case Disconnecting:
sprintf(status, "Waiting for player...");
sprintf_s(status, ARRAYSIZE(status), "Waiting for player...");
progress = (timeGetTime() - info.disconnect_start) * 100 / info.disconnect_timeout;
break;
}
if (*status) {
SetTextAlign(hdc, TA_TOP | TA_CENTER);
TextOutA(hdc, ship.position.x, ship.position.y + PROGRESS_TEXT_OFFSET, status, strlen(status));
TextOutA(hdc, (int)ship.position.x, (int)ship.position.y + PROGRESS_TEXT_OFFSET, status, (int)strlen(status));
}
if (progress >= 0) {
HBRUSH bar = (HBRUSH)(info.state == Synchronizing ? GetStockObject(WHITE_BRUSH) : _redBrush);
RECT rc = { ship.position.x - (PROGRESS_BAR_WIDTH / 2),
ship.position.y + PROGRESS_BAR_TOP_OFFSET,
ship.position.x + (PROGRESS_BAR_WIDTH / 2),
ship.position.y + PROGRESS_BAR_TOP_OFFSET + PROGRESS_BAR_HEIGHT };
RECT rc = { (LONG)(ship.position.x - (PROGRESS_BAR_WIDTH / 2)),
(LONG)(ship.position.y + PROGRESS_BAR_TOP_OFFSET),
(LONG)(ship.position.x + (PROGRESS_BAR_WIDTH / 2)),
(LONG)(ship.position.y + PROGRESS_BAR_TOP_OFFSET + PROGRESS_BAR_HEIGHT) };
FrameRect(hdc, &rc, (HBRUSH)GetStockObject(GRAY_BRUSH));
rc.right = rc.left + min(100, progress) * PROGRESS_BAR_WIDTH / 100;
@@ -192,7 +192,7 @@ GDIRenderer::DrawConnectState(HDC hdc, Ship &ship, PlayerConnectionInfo &info, C
void
GDIRenderer::CreateGDIFont(HDC hdc)
GDIRenderer::CreateGDIFont(HDC)
{
_font = CreateFont(-12,
0, // Width Of Font

View File

@@ -21,7 +21,7 @@ public:
protected:
void RenderChecksum(HDC hdc, int y, NonGameState::ChecksumInfo &info);
void DrawShip(HDC hdc, int which, GameState &gamestate);
void DrawConnectState(HDC hdc, Ship &ship, PlayerConnectionInfo &info, COLORREF color);
void DrawConnectState(HDC hdc, Ship &ship, PlayerConnectionInfo &info);
void CreateGDIFont(HDC hdc);
HFONT _font;

View File

@@ -90,7 +90,7 @@ draw_fairness_graph_control(LPDRAWITEMSTRUCT di)
}
static INT_PTR CALLBACK
ggpo_perfmon_dlgproc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
ggpo_perfmon_dlgproc(HWND hwndDlg, UINT uMsg, WPARAM, LPARAM lParam)
{
switch (uMsg) {
@@ -114,7 +114,7 @@ ggpo_perfmon_dlgproc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
case WM_INITDIALOG:
{
char pid[64];
sprintf(pid, "%d", GetCurrentProcessId());
snprintf(pid, ARRAYSIZE(pid), "%d", GetCurrentProcessId());
SetWindowTextA(GetDlgItem(hwndDlg, IDC_PID), pid);
return TRUE;
}
@@ -159,7 +159,7 @@ ggpoutil_perfmon_toggle()
void
ggpoutil_perfmon_update(GGPOSession *ggpo, GGPOPlayerHandle players[], int num_players)
{
GGPONetworkStats stats;
GGPONetworkStats stats = { 0 };
int i;
_num_players = num_players;
@@ -219,11 +219,11 @@ ggpoutil_perfmon_update(GGPOSession *ggpo, GGPOPlayerHandle players[], int num_p
char fLocal[128], fRemote[128], fBandwidth[128];
char msLag[128], frameLag[128];
sprintf(msLag, "%d ms", stats.network.ping);
sprintf(frameLag, "%.1f frames", stats.network.ping ? stats.network.ping * 60.0 / 1000 : 0);
sprintf(fBandwidth, "%.2f kilobytes/sec", stats.network.kbps_sent / 8.0);
sprintf(fLocal, "%d frames", stats.timesync.local_frames_behind);
sprintf(fRemote, "%d frames", stats.timesync.remote_frames_behind);
sprintf_s(msLag, ARRAYSIZE(msLag), "%d ms", stats.network.ping);
sprintf_s(frameLag, ARRAYSIZE(frameLag), "%.1f frames", stats.network.ping ? stats.network.ping * 60.0 / 1000 : 0);
sprintf_s(fBandwidth, ARRAYSIZE(fBandwidth), "%.2f kilobytes/sec", stats.network.kbps_sent / 8.0);
sprintf_s(fLocal, ARRAYSIZE(fLocal), "%d frames", stats.timesync.local_frames_behind);
sprintf_s(fRemote, ARRAYSIZE(fRemote), "%d frames", stats.timesync.remote_frames_behind);
SetWindowTextA(GetDlgItem(_dialog, IDC_NETWORK_LAG), msLag);
SetWindowTextA(GetDlgItem(_dialog, IDC_FRAME_LAG), frameLag);
SetWindowTextA(GetDlgItem(_dialog, IDC_BANDWIDTH), fBandwidth);

View File

@@ -6,9 +6,6 @@
#include "vectorwar.h"
#include "ggpo_perfmon.h"
int local_port, num_players, num_spectators;
GGPOPlayer *players;
LRESULT CALLBACK
MainWindowProc(HWND hwnd,
UINT uMsg,
@@ -23,7 +20,7 @@ MainWindowProc(HWND hwnd,
ggpoutil_perfmon_toggle();
} else if (wParam == VK_ESCAPE) {
VectorWar_Exit();
VectorWar_Init(hwnd, local_port, num_players, players, num_spectators);
PostQuitMessage(0);
} else if (wParam >= VK_F1 && wParam <= VK_F12) {
VectorWar_DisconnectPlayer((int)(wParam - VK_F1));
}
@@ -95,18 +92,18 @@ Syntax()
MessageBox(NULL,
L"Syntax: vectorwar.exe <local port> <num players> ('local' | <remote ip>:<remote port>)*\n",
L"Could not start", MB_OK);
exit(1);
}
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
_In_opt_ HINSTANCE,
_In_ LPWSTR,
_In_ int)
{
HWND hwnd = CreateMainWindow(hInstance);
int offset = 1, local_player = 0;
WSADATA wd = { 0 };
wchar_t wide_ip_buffer[128];
unsigned int wide_ip_buffer_size = (unsigned int)ARRAYSIZE(wide_ip_buffer);
WSAStartup(MAKEWORD(2, 2), &wd);
POINT window_offsets[] = {
@@ -124,18 +121,20 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
Syntax();
return 1;
}
local_port = _wtoi(__wargv[offset++]);
num_players = _wtoi(__wargv[offset++]);
unsigned short local_port = (unsigned short)_wtoi(__wargv[offset++]);
int num_players = _wtoi(__wargv[offset++]);
if (num_players < 0 || __argc < offset + num_players) {
Syntax();
return 1;
}
if (wcscmp(__wargv[offset], L"spectate") == 0) {
char host_ip[128];
int host_port;
if (swscanf(__wargv[offset+1], L"%[^:]:%d", wide_ip_buffer, &host_port) != 2) {
unsigned short host_port;
if (swscanf_s(__wargv[offset+1], L"%[^:]:%hu", wide_ip_buffer, wide_ip_buffer_size, &host_port) != 2) {
Syntax();
return 1;
}
wcstombs(host_ip, wide_ip_buffer, sizeof(host_ip));
wcstombs_s(nullptr, host_ip, ARRAYSIZE(host_ip), wide_ip_buffer, _TRUNCATE);
VectorWar_InitSpectator(hwnd, local_port, num_players, host_ip, host_port);
} else {
GGPOPlayer players[GGPO_MAX_SPECTATORS + GGPO_MAX_PLAYERS];
@@ -153,21 +152,21 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
}
players[i].type = GGPO_PLAYERTYPE_REMOTE;
if (swscanf(arg, L"%[^:]:%hd", wide_ip_buffer, &players[i].u.remote.port) != 2) {
if (swscanf_s(arg, L"%[^:]:%hd", wide_ip_buffer, wide_ip_buffer_size, &players[i].u.remote.port) != 2) {
Syntax();
return 1;
}
wcstombs(players[i].u.remote.ip_address, wide_ip_buffer, sizeof(players[i].u.remote.ip_address));
wcstombs_s(nullptr, players[i].u.remote.ip_address, ARRAYSIZE(players[i].u.remote.ip_address), wide_ip_buffer, _TRUNCATE);
}
// these are spectators...
num_spectators = 0;
int num_spectators = 0;
while (offset < __argc) {
players[i].type = GGPO_PLAYERTYPE_SPECTATOR;
if (swscanf(__wargv[offset++], L"%[^:]:%hd", wide_ip_buffer, &players[i].u.remote.port) != 2) {
if (swscanf_s(__wargv[offset++], L"%[^:]:%hd", wide_ip_buffer, wide_ip_buffer_size, &players[i].u.remote.port) != 2) {
Syntax();
return 1;
}
wcstombs(players[i].u.remote.ip_address, wide_ip_buffer, sizeof(players[i].u.remote.ip_address));
wcstombs_s(nullptr, players[i].u.remote.ip_address, ARRAYSIZE(players[i].u.remote.ip_address), wide_ip_buffer, _TRUNCATE);
i++;
num_spectators++;
}
@@ -180,7 +179,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
}
RunMainLoop(hwnd);
VectorWar_Exit();
delete [] players;
WSACleanup();
DestroyWindow(hwnd);
return 0;

View File

@@ -47,10 +47,10 @@ struct NonGameState {
}
}
void SetDisconnectTimeout(GGPOPlayerHandle handle, int now, int timeout) {
void SetDisconnectTimeout(GGPOPlayerHandle handle, int when, int timeout) {
for (int i = 0; i < num_players; i++) {
if (players[i].handle == handle) {
players[i].disconnect_start = now;
players[i].disconnect_start = when;
players[i].disconnect_timeout = timeout;
players[i].state = Disconnecting;
break;

View File

@@ -27,7 +27,7 @@ fletcher32_checksum(short *data, size_t len)
int sum1 = 0xffff, sum2 = 0xffff;
while (len) {
unsigned tlen = len > 360 ? 360 : len;
size_t tlen = len > 360 ? 360 : len;
len -= tlen;
do {
sum1 += *data++;
@@ -50,7 +50,7 @@ fletcher32_checksum(short *data, size_t len)
* so just return true.
*/
bool __cdecl
vw_begin_game_callback(const char *name)
vw_begin_game_callback(const char *)
{
return true;
}
@@ -106,7 +106,7 @@ vw_on_event_callback(GGPOEvent *info)
* during a rollback.
*/
bool __cdecl
vw_advance_frame_callback(int flags)
vw_advance_frame_callback(int)
{
int inputs[MAX_SHIPS] = { 0 };
int disconnect_flags;
@@ -137,7 +137,7 @@ vw_load_game_state_callback(unsigned char *buffer, int len)
* buffer and len parameters.
*/
bool __cdecl
vw_save_game_state_callback(unsigned char **buffer, int *len, int *checksum, int frame)
vw_save_game_state_callback(unsigned char **buffer, int *len, int *checksum, int)
{
*len = sizeof(gs);
*buffer = (unsigned char *)malloc(*len);
@@ -155,9 +155,10 @@ vw_save_game_state_callback(unsigned char **buffer, int *len, int *checksum, int
* Log the gamestate. Used by the synctest debugging tool.
*/
bool __cdecl
vw_log_game_state(char *filename, unsigned char *buffer, int len)
vw_log_game_state(char *filename, unsigned char *buffer, int)
{
FILE *fp = fopen(filename, "w");
FILE* fp = nullptr;
fopen_s(&fp, filename, "w");
if (fp) {
GameState *gamestate = (GameState *)buffer;
fprintf(fp, "GameState object.\n");
@@ -205,7 +206,7 @@ vw_free_buffer(void *buffer)
* the video renderer and creates a new network session.
*/
void
VectorWar_Init(HWND hwnd, int localport, int num_players, GGPOPlayer *players, int num_spectators)
VectorWar_Init(HWND hwnd, unsigned short localport, int num_players, GGPOPlayer *players, int num_spectators)
{
GGPOErrorCode result;
renderer = new GDIRenderer(hwnd);
@@ -262,7 +263,7 @@ VectorWar_Init(HWND hwnd, int localport, int num_players, GGPOPlayer *players, i
* Create a new spectator session
*/
void
VectorWar_InitSpectator(HWND hwnd, int localport, int num_players, char *host_ip, int host_port)
VectorWar_InitSpectator(HWND hwnd, unsigned short localport, int num_players, char *host_ip, unsigned short host_port)
{
GGPOErrorCode result;
renderer = new GDIRenderer(hwnd);
@@ -302,9 +303,9 @@ VectorWar_DisconnectPlayer(int player)
char logbuf[128];
GGPOErrorCode result = ggpo_disconnect_player(ggpo, ngs.players[player].handle);
if (GGPO_SUCCEEDED(result)) {
sprintf(logbuf, "Disconnected player %d.\n", player);
sprintf_s(logbuf, ARRAYSIZE(logbuf), "Disconnected player %d.\n", player);
} else {
sprintf(logbuf, "Error while disconnecting player (err:%d).\n", result);
sprintf_s(logbuf, ARRAYSIZE(logbuf), "Error while disconnecting player (err:%d).\n", result);
}
renderer->SetStatusText(logbuf);
}
@@ -448,4 +449,5 @@ VectorWar_Exit()
ggpo = NULL;
}
delete renderer;
renderer = NULL;
}

View File

@@ -19,8 +19,8 @@ enum VectorWarInputs {
INPUT_BOMB = (1 << 5),
};
void VectorWar_Init(HWND hwnd, int localport, int num_players, GGPOPlayer *players, int num_spectators);
void VectorWar_InitSpectator(HWND hwnd, int localport, int num_players, char *host_ip, int host_port);
void VectorWar_Init(HWND hwnd, unsigned short localport, int num_players, GGPOPlayer *players, int num_spectators);
void VectorWar_InitSpectator(HWND hwnd, unsigned short localport, int num_players, char *host_ip, unsigned short host_port);
void VectorWar_DrawCurrentFrame();
void VectorWar_AdvanceFrame(int inputs[], int disconnect_flags);
void VectorWar_RunFrame(HWND hwnd);

View File

@@ -78,8 +78,8 @@ typedef struct GGPOPlayer {
struct {
} local;
struct {
char ip_address[32];
short port;
char ip_address[32];
unsigned short port;
} remote;
} u;
} GGPOPlayer;
@@ -324,7 +324,7 @@ GGPO_API GGPOErrorCode __cdecl ggpo_start_session(GGPOSession **session,
const char *game,
int num_players,
int input_size,
int localport);
unsigned short localport);
/*
@@ -404,9 +404,9 @@ GGPO_API GGPOErrorCode __cdecl ggpo_start_spectating(GGPOSession **session,
const char *game,
int num_players,
int input_size,
int local_port,
unsigned short local_port,
char *host_ip,
int host_port);
unsigned short host_port);
/*
* ggpo_close_session --

View File

@@ -13,7 +13,7 @@ static const int DEFAULT_DISCONNECT_NOTIFY_START = 750;
Peer2PeerBackend::Peer2PeerBackend(GGPOSessionCallbacks *cb,
const char *gamename,
int localport,
uint16 localport,
int num_players,
int input_size) :
_num_players(num_players),
@@ -62,7 +62,7 @@ Peer2PeerBackend::~Peer2PeerBackend()
void
Peer2PeerBackend::AddRemotePlayer(char *ip,
int port,
uint16 port,
int queue)
{
/*
@@ -77,7 +77,7 @@ Peer2PeerBackend::AddRemotePlayer(char *ip,
}
GGPOErrorCode Peer2PeerBackend::AddSpectator(char *ip,
int port)
uint16 port)
{
if (_num_spectators == GGPO_MAX_SPECTATORS) {
return GGPO_ERRORCODE_TOO_MANY_SPECTATORS;
@@ -498,7 +498,7 @@ Peer2PeerBackend::DisconnectPlayerQueue(int queue, int syncto)
Log("Changing queue %d local connect status for last frame from %d to %d on disconnect request (current: %d).\n",
queue, _local_connect_status[queue].last_frame, syncto, framecount);
_local_connect_status[queue].disconnected = true;
_local_connect_status[queue].disconnected = 1;
_local_connect_status[queue].last_frame = syncto;
if (syncto < framecount) {

View File

@@ -17,7 +17,7 @@
class Peer2PeerBackend : public IQuarkBackend, IPollSink, Udp::Callbacks {
public:
Peer2PeerBackend(GGPOSessionCallbacks *cb, const char *gamename, int localport, int num_players, int input_size);
Peer2PeerBackend(GGPOSessionCallbacks *cb, const char *gamename, uint16 localport, int num_players, int input_size);
virtual ~Peer2PeerBackend();
@@ -46,8 +46,8 @@ protected:
void CheckInitialSync(void);
int Poll2Players(int current_frame);
int PollNPlayers(int current_frame);
void AddRemotePlayer(char *remoteip, int reportport, int queue);
GGPOErrorCode AddSpectator(char *remoteip, int reportport);
void AddRemotePlayer(char *remoteip, uint16 reportport, int queue);
GGPOErrorCode AddSpectator(char *remoteip, uint16 reportport);
virtual void OnSyncEvent(Sync::Event &e) { }
virtual void OnUdpProtocolEvent(UdpProtocol::Event &e, GGPOPlayerHandle handle);
virtual void OnUdpProtocolPeerEvent(UdpProtocol::Event &e, int queue);

View File

@@ -9,11 +9,11 @@
SpectatorBackend::SpectatorBackend(GGPOSessionCallbacks *cb,
const char* gamename,
int localport,
uint16 localport,
int num_players,
int input_size,
char *hostip,
int hostport) :
u_short hostport) :
_num_players(num_players),
_input_size(input_size),
_next_input_to_send(0)
@@ -149,8 +149,6 @@ SpectatorBackend::OnUdpProtocolEvent(UdpProtocol::Event &evt)
break;
case UdpProtocol::Event::Disconnected:
GGPOEvent info;
info.code = GGPO_EVENTCODE_DISCONNECTED_FROM_PEER;
info.u.disconnected.player = 0;
_callbacks.on_event(&info);

View File

@@ -19,7 +19,7 @@
class SpectatorBackend : public IQuarkBackend, IPollSink, Udp::Callbacks {
public:
SpectatorBackend(GGPOSessionCallbacks *cb, const char *gamename, int localport, int num_players, int input_size, char *hostip, int hostport);
SpectatorBackend(GGPOSessionCallbacks *cb, const char *gamename, uint16 localport, int num_players, int input_size, char *hostip, u_short hostport);
virtual ~SpectatorBackend();

View File

@@ -21,7 +21,7 @@ SyncTestBackend::SyncTestBackend(GGPOSessionCallbacks *cb,
_running = false;
_logfp = NULL;
_current_input.erase();
strcpy(_game, gamename);
strcpy_s(_game, gamename);
/*
* Initialize the synchronziation layer
@@ -136,7 +136,7 @@ SyncTestBackend::IncrementFrame(void)
// Verify that the checksumn of this frame is the same as the one in our
// list.
SavedInfo info = _saved_frames.front();
info = _saved_frames.front();
_saved_frames.pop();
if (info.frame != _sync.GetFrameCount()) {
@@ -163,7 +163,7 @@ SyncTestBackend::RaiseSyncError(const char *fmt, ...)
char buf[1024];
va_list args;
va_start(args, fmt);
vsprintf(buf, fmt, args);
vsprintf_s(buf, ARRAY_SIZE(buf), fmt, args);
va_end(args);
puts(buf);
@@ -188,12 +188,12 @@ SyncTestBackend::BeginLog(int saving)
char filename[MAX_PATH];
CreateDirectoryA("synclogs", NULL);
sprintf(filename, "synclogs\\%s-%04d-%s.log",
sprintf_s(filename, ARRAY_SIZE(filename), "synclogs\\%s-%04d-%s.log",
saving ? "state" : "log",
_sync.GetFrameCount(),
_rollingback ? "replay" : "original");
_logfp = fopen(filename, "w");
fopen_s(&_logfp, filename, "w");
}
void
@@ -209,9 +209,9 @@ void
SyncTestBackend::LogSaveStates(SavedInfo &info)
{
char filename[MAX_PATH];
sprintf(filename, "synclogs\\state-%04d-original.log", _sync.GetFrameCount());
sprintf_s(filename, ARRAY_SIZE(filename), "synclogs\\state-%04d-original.log", _sync.GetFrameCount());
_callbacks.log_game_state(filename, (unsigned char *)info.buf, info.cbuf);
sprintf(filename, "synclogs\\state-%04d-replay.log", _sync.GetFrameCount());
sprintf_s(filename, ARRAY_SIZE(filename), "synclogs\\state-%04d-replay.log", _sync.GetFrameCount());
_callbacks.log_game_state(filename, _sync.GetLastSavedFrame().buf, _sync.GetLastSavedFrame().cbuf);
}

View File

@@ -36,32 +36,35 @@ GameInput::init(int iframe, char *ibits, int isize)
}
void
GameInput::desc(char *buf, bool show_frame) const
GameInput::desc(char *buf, size_t buf_size, bool show_frame) const
{
ASSERT(size);
int offset = 0;
size_t remaining = buf_size;
if (show_frame) {
sprintf(buf, "(frame:%d size:%d ", frame, size);
remaining -= sprintf_s(buf, buf_size, "(frame:%d size:%d ", frame, size);
} else {
sprintf(buf, "(size:%d ", size);
remaining -= sprintf_s(buf, buf_size, "(size:%d ", size);
}
for (int i = 0; i < size * 8; i++) {
char buf2[16];
if (value(i)) {
sprintf(buf2, "%2d ", i);
strcat(buf, buf2);
int c = sprintf_s(buf2, ARRAY_SIZE(buf2), "%2d ", i);
strncat_s(buf, remaining, buf2, ARRAY_SIZE(buf2));
remaining -= c;
}
}
strcat(buf, ")");
strncat_s(buf, remaining, ")", 1);
}
void
GameInput::log(char *prefix, bool show_frame) const
{
char buf[1024];
strcpy(buf, prefix);
desc(buf + strlen(prefix), show_frame);
strcat(buf, "\n");
size_t c = strlen(prefix);
strcpy_s(buf, prefix);
desc(buf + c, ARRAY_SIZE(buf) - c, show_frame);
strncat_s(buf, ARRAY_SIZE(buf) - strlen(buf), "\n", 1);
Log(buf);
}

View File

@@ -32,7 +32,7 @@ struct GameInput {
void set(int i) { bits[i/8] |= (1 << (i%8)); }
void clear(int i) { bits[i/8] &= ~(1 << (i%8)); }
void erase() { memset(bits, 0, sizeof(bits)); }
void desc(char *buf, bool show_frame = true) const;
void desc(char *buf, size_t buf_size, bool show_frame = true) const;
void log(char *prefix, bool show_frame = true) const;
bool equal(GameInput &input, bool bitsonly = false);
};

View File

@@ -311,8 +311,7 @@ InputQueue::Log(const char *fmt, ...)
size_t offset;
va_list args;
sprintf(buf, "input q%d | ", _id);
offset = strlen(buf);
offset = sprintf_s(buf, ARRAY_SIZE(buf), "input q%d | ", _id);
va_start(args, fmt);
vsnprintf(buf + offset, ARRAY_SIZE(buf) - offset - 1, fmt, args);
buf[ARRAY_SIZE(buf)-1] = '\0';

View File

@@ -28,19 +28,19 @@ void Log(const char *fmt, ...)
void Logv(const char *fmt, va_list args)
{
if (!getenv("ggpo.log") || getenv("ggpo.log.ignore")) {
if (!Platform::GetConfigBool("ggpo.log") || Platform::GetConfigBool("ggpo.log.ignore")) {
return;
}
if (!logfile) {
sprintf(logbuf, "log-%d.log", Platform::GetProcessID());
logfile = fopen(logbuf, "w");
sprintf_s(logbuf, ARRAY_SIZE(logbuf), "log-%d.log", Platform::GetProcessID());
fopen_s(&logfile, logbuf, "w");
}
Logv(logfile, fmt, args);
}
void Logv(FILE *fp, const char *fmt, va_list args)
{
if (getenv("ggpo.log.timestamps")) {
if (Platform::GetConfigBool("ggpo.log.timestamps")) {
static int start = 0;
int t = 0;
if (!start) {
@@ -54,7 +54,6 @@ void Logv(FILE *fp, const char *fmt, va_list args)
vfprintf(fp, fmt, args);
fflush(fp);
vsprintf(logbuf, fmt, args);
//OutputDebugStringA(logbuf);
vsprintf_s(logbuf, ARRAY_SIZE(logbuf), fmt, args);
}

View File

@@ -41,7 +41,7 @@ ggpo_start_session(GGPOSession **session,
const char *game,
int num_players,
int input_size,
int localport)
unsigned short localport)
{
*session= (GGPOSession *)new Peer2PeerBackend(cb,
game,
@@ -192,9 +192,9 @@ GGPOErrorCode ggpo_start_spectating(GGPOSession **session,
const char *game,
int num_players,
int input_size,
int local_port,
unsigned short local_port,
char *host_ip,
int host_port)
unsigned short host_port)
{
*session= (GGPOSession *)new SpectatorBackend(cb,
game,

View File

@@ -9,11 +9,11 @@
#include "udp.h"
SOCKET
CreateSocket(int bind_port, int retries)
CreateSocket(uint16 bind_port, int retries)
{
SOCKET s;
sockaddr_in sin;
int port;
uint16 port;
int optval = 1;
s = socket(AF_INET, SOCK_DGRAM, 0);
@@ -52,7 +52,7 @@ Udp::~Udp(void)
}
void
Udp::Init(int port, Poll *poll, Callbacks *callbacks)
Udp::Init(uint16 port, Poll *poll, Callbacks *callbacks)
{
_callbacks = callbacks;
@@ -71,11 +71,11 @@ Udp::SendTo(char *buffer, int len, int flags, struct sockaddr *dst, int destlen)
int res = sendto(_socket, buffer, len, flags, dst, destlen);
if (res == SOCKET_ERROR) {
DWORD err = WSAGetLastError();
DWORD e2 = WSAENOTSOCK;
Log("unknown error in sendto (erro: %d wsaerr: %d).\n", res, err);
ASSERT(FALSE && "Unknown error in sendto");
}
Log("sent packet length %d to %s:%d (ret:%d).\n", len, inet_ntoa(to->sin_addr), ntohs(to->sin_port), res);
char dst_ip[1024];
Log("sent packet length %d to %s:%d (ret:%d).\n", len, inet_ntop(AF_INET, (void *)&to->sin_addr, dst_ip, ARRAY_SIZE(dst_ip)), ntohs(to->sin_port), res);
}
bool
@@ -98,7 +98,8 @@ Udp::OnLoopPoll(void *cookie)
}
break;
} else if (len > 0) {
Log("recvfrom returned (len:%d from:%s:%d).\n", len,inet_ntoa(recv_addr.sin_addr), ntohs(recv_addr.sin_port) );
char src_ip[1024];
Log("recvfrom returned (len:%d from:%s:%d).\n", len, inet_ntop(AF_INET, (void*)&recv_addr.sin_addr, src_ip, ARRAY_SIZE(src_ip)), ntohs(recv_addr.sin_port) );
UdpMsg *msg = (UdpMsg *)recv_buf;
_callbacks->OnMsg(recv_addr, msg, len);
}
@@ -114,7 +115,7 @@ Udp::Log(const char *fmt, ...)
size_t offset;
va_list args;
strcpy(buf, "udp | ");
strcpy_s(buf, "udp | ");
offset = strlen(buf);
va_start(args, fmt);
vsnprintf(buf + offset, ARRAY_SIZE(buf) - offset - 1, fmt, args);

View File

@@ -38,7 +38,7 @@ protected:
public:
Udp();
void Init(int port, Poll *p, Callbacks *callbacks);
void Init(uint16 port, Poll *p, Callbacks *callbacks);
void SendTo(char *buffer, int len, int flags, struct sockaddr *dst, int destlen);

View File

@@ -27,8 +27,8 @@ struct UdpMsg
};
struct connect_status {
int disconnected:1;
int last_frame:31;
unsigned int disconnected:1;
int last_frame:31;
};
struct {
@@ -99,7 +99,7 @@ public:
return 0;
}
UdpMsg(MsgType t) { hdr.type = t; }
UdpMsg(MsgType t) { hdr.type = (uint8)t; }
};
#pragma pack(pop)

View File

@@ -52,10 +52,8 @@ UdpProtocol::UdpProtocol() :
memset(&_peer_addr, 0, sizeof _peer_addr);
_oo_packet.msg = NULL;
char *delay = getenv("ggpo.network.delay");
char *oop_pct = getenv("ggpo.oop.percent");
_send_latency = delay ? atoi(delay) : 0;
_oop_percent = oop_pct ? atoi(oop_pct) : 0;
_send_latency = Platform::GetConfigInt("ggpo.network.delay");
_oop_percent = Platform::GetConfigInt("ggpo.oop.percent");
}
UdpProtocol::~UdpProtocol()
@@ -68,7 +66,7 @@ UdpProtocol::Init(Udp *udp,
Poll &poll,
int queue,
char *ip,
int port,
u_short port,
UdpMsg::connect_status *status)
{
_udp = udp;
@@ -76,11 +74,11 @@ UdpProtocol::Init(Udp *udp,
_local_connect_status = status;
_peer_addr.sin_family = AF_INET;
_peer_addr.sin_addr.s_addr = inet_addr(ip);
_peer_addr.sin_port = htons(port);
inet_pton(AF_INET, ip, &_peer_addr.sin_addr.s_addr);
do {
_magic_number = rand();
_magic_number = (uint16)rand();
} while (_magic_number == 0);
poll.RegisterLoop(this);
}
@@ -122,7 +120,7 @@ UdpProtocol::SendPendingOutput()
bits = msg->u.input.bits;
msg->u.input.start_frame = _pending_output.front().frame;
msg->u.input.input_size = _pending_output.front().size;
msg->u.input.input_size = (uint8)_pending_output.front().size;
ASSERT(last.frame == -1 || last.frame + 1 == msg->u.input.start_frame);
for (j = 0; j < _pending_output.size(); j++) {
@@ -146,7 +144,7 @@ UdpProtocol::SendPendingOutput()
msg->u.input.input_size = 0;
}
msg->u.input.ack_frame = _last_received_input.frame;
msg->u.input.num_bits = offset;
msg->u.input.num_bits = (uint16)offset;
msg->u.input.disconnect_requested = _current_state == Disconnected;
if (_local_connect_status) {
@@ -211,7 +209,7 @@ UdpProtocol::OnLoopPoll(void *cookie)
if (!_state.running.last_quality_report_time || _state.running.last_quality_report_time + QUALITY_REPORT_INTERVAL < now) {
UdpMsg *msg = new UdpMsg(UdpMsg::QualityReport);
msg->u.quality_report.ping = Platform::GetCurrentTimeMS();
msg->u.quality_report.frame_advantage = _local_frame_advantage;
msg->u.quality_report.frame_advantage = (uint8)_local_frame_advantage;
SendMsg(msg);
_state.running.last_quality_report_time = now;
}
@@ -317,7 +315,7 @@ UdpProtocol::OnMsg(UdpMsg *msg, int len)
};
// filter out messages that don't match what we expect
int seq = msg->hdr.sequence_number;
uint16 seq = msg->hdr.sequence_number;
if (msg->hdr.type != UdpMsg::SyncRequest &&
msg->hdr.type != UdpMsg::SyncReply) {
if (msg->hdr.magic != _remote_magic_number) {
@@ -326,7 +324,7 @@ UdpProtocol::OnMsg(UdpMsg *msg, int len)
}
// filter out out-of-order packets
uint16 skipped = seq - _next_recv_seq;
uint16 skipped = (uint16)((int)seq - (int)_next_recv_seq);
// Log("checking sequence number -> next - seq : %d - %d = %d\n", seq, _next_recv_seq, skipped);
if (skipped > MAX_SEQ_DISTANCE) {
Log("dropping out of order packet (seq: %d, last seq:%d)\n", seq, _next_recv_seq);
@@ -407,7 +405,7 @@ UdpProtocol::Log(const char *fmt, ...)
size_t offset;
va_list args;
sprintf(buf, "udpproto%d | ", _queue);
sprintf_s(buf, ARRAY_SIZE(buf), "udpproto%d | ", _queue);
offset = strlen(buf);
va_start(args, fmt);
vsnprintf(buf + offset, ARRAY_SIZE(buf) - offset - 1, fmt, args);
@@ -594,7 +592,7 @@ UdpProtocol::OnInput(UdpMsg *msg, int len)
UdpProtocol::Event evt(UdpProtocol::Event::Input);
evt.u.input.input = _last_received_input;
_last_received_input.desc(desc);
_last_received_input.desc(desc, ARRAY_SIZE(desc));
_state.running.last_input_packet_recv_time = Platform::GetCurrentTimeMS();

View File

@@ -63,7 +63,7 @@ public:
UdpProtocol();
virtual ~UdpProtocol();
void Init(Udp *udp, Poll &p, int queue, char *ip, int port, UdpMsg::connect_status *status);
void Init(Udp *udp, Poll &p, int queue, char *ip, u_short port, UdpMsg::connect_status *status);
void Synchronize();
bool GetPeerConnectStatus(int id, int *frame);

View File

@@ -0,0 +1,27 @@
/* -----------------------------------------------------------------------
* GGPO.net (http://ggpo.net) - Copyright 2009 GroundStorm Studios, LLC.
*
* Use of this software is governed by the MIT license that can be found
* in the LICENSE file.
*/
#include "platform_windows.h"
int
Platform::GetConfigInt(const char* name)
{
char buf[1024];
if (GetEnvironmentVariable(name, buf, ARRAY_SIZE(buf)) == 0) {
return 0;
}
return atoi(buf);
}
bool Platform::GetConfigBool(const char* name)
{
char buf[1024];
if (GetEnvironmentVariable(name, buf, ARRAY_SIZE(buf)) == 0) {
return false;
}
return atoi(buf) != 0 || _stricmp(buf, "true") == 0;
}

View File

@@ -9,8 +9,10 @@
#define _GGPO_WINDOWS_H_
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <stdio.h>
#include "types.h"
class Platform {
public: // types
@@ -20,6 +22,8 @@ public: // functions
static ProcessID GetProcessID() { return GetCurrentProcessId(); }
static void AssertFailed(char *msg) { MessageBoxA(NULL, msg, "GGPO Assertion Failed", MB_OK | MB_ICONEXCLAMATION); }
static uint32 GetCurrentTimeMS() { return timeGetTime(); }
static int GetConfigInt(const char* name);
static bool GetConfigBool(const char* name);
};
#endif

View File

@@ -21,8 +21,6 @@ TimeSync::~TimeSync()
void
TimeSync::advance_frame(GameInput &input, int advantage, int radvantage)
{
int sleep_time = 0;
// Remember the last frame and frame advantage
_last_inputs[input.frame % ARRAY_SIZE(_last_inputs)] = input;
_local[input.frame % ARRAY_SIZE(_local)] = advantage;

View File

@@ -54,13 +54,13 @@ typedef int int32;
#define ASSERT(x) \
do { \
if (!(x)) { \
char buf[1024]; \
snprintf(buf, sizeof(buf) - 1, "Assertion: %s @ %s:%d (pid:%d)", #x, __FILE__, __LINE__, Platform::GetProcessID()); \
Log("%s\n", buf); \
char assert_buf[1024]; \
snprintf(assert_buf, sizeof(assert_buf) - 1, "Assertion: %s @ %s:%d (pid:%d)", #x, __FILE__, __LINE__, Platform::GetProcessID()); \
Log("%s\n", assert_buf); \
Log("\n"); \
Log("\n"); \
Log("\n"); \
Platform::AssertFailed(buf); \
Platform::AssertFailed(assert_buf); \
exit(0); \
} \
} while (false)