デューン2:王朝の建築

開発者: Westwood Studios

パブリッシャー: Virgin Games

ジャンル:戦略(リアルタイム)/トップダウン

システム要件:

モデムブラウザ





導入する代わりに



Dune 2は素晴らしい戦略であり、このジャンルの最初の戦略の1つです。 このゲームは、90代の子供たち全体にとって素晴らしいことだと言っても意味がありません。 もちろん、コードをいじってJavaScriptに移植するのは意外と楽しいので、もちろんTTD( 記事 )の後、Dune 2は必然的に私の目標になりました。対処した。 判明したように、Dune 2はTTDよりも機能的には簡単ですが、移植するのはより困難でしたが、それについては後で詳しく説明します。



コードベース



「正しい」コードベースの選択は、 emscriptenを使用したプロジェクトの移植を成功させる主な要因です。 たとえば、 SDLの使用、マルチスレッドの欠如は、移植が成功する良いマーカーです。 どうやら Dune 2に関連しているように見えるすべてのプロジェクトを何らかの方法で調べOpenDuneに落ち着きました。 私を捕まえたのは、すべてのバグを含む、元のゲームのすべての動作の完全なコピーでした。 このプロジェクトのコードは、元のコードから元々半自動で取得されたようです。 コードにはあちこちにlocal_03FFという名前の変数があり、多くのグローバル変数があり、コードは非常に読みにくいです。 ソースコードベースの最も重大な欠点はマルチスレッド化であり、移植時に多くの問題を引き起こしました。 しかし、結果は本当に良いです。ブラウザでは、ゲームはオリジナルのゲームに非常に似ていますが、バグの新しいパックがあります。



だから、乾燥した事実:


言語:C

ソースファイルの数:143

コードの行数:59151

バイナリサイズ:423.2 Kb

JavaScript相当サイズ:〜1000 Kb

移植時間:〜2か月



この記事の後半で、移植中に遭遇した困難について説明します。 確かにこれは誰にとっても興味深いことではありませんが、そうであれば、このサブセクションを「既知の問題」に下げてください。



マルチスレッドVS非同期



OpenDuneには、興味深い割り込みベースのマルチスレッドモデルがあります。 マルチスレッドを保証するために、アイドル時のゲームコードは無限のサイクルでスピンし、次のようになります。



while (true) { uint16 key; key = GUI_Widget_HandleEvents(w); if (key = 13) { break; } sleepIdle(); }
      
      







アプリケーションが起動すると、インターバルタイマーはsetitimer関数で初期化されます。 このタイマーにより、定期的に中断が発生します。 メインスレッドの実行を一時停止し、任意のコードの実行を許可します。 JavaScriptの場合、同様のタイマーの実装は簡単ですが、プロジェクトをJavaScript実装とC実装に人為的に分割しないように、異なる移植方法が選択されました。 setitimer関数の使用を完全に放棄することが決定され、代わりにsleepIdle()呼び出しがタイマーイベント処理関数に置き換えられました。 この関数は、ダウンタイムの代わりに、どのスケジュール済みイベントが近づいているかを判断し、それらを実行するために起動します。



より深刻な問題は、内部のwhileループです。JavaScriptでこのようなループが発生すると、ブラウザーのタブ(またはブラウザー全体)が必然的にフリーズします。 これは、ほとんどのループがユーザー入力(マウスボタン、キーボードのクリック)を想定しているためですが、ブラウザーは入力デバイスからのイベントを処理できず、現在のJavaScript実行可能ブロックの後に実行チェーンに入れられます。 この問題を解決する可能な方法は、コードを手動で編集し、問題のコードを非同期モードに転送することです。



小さな例。 問題を引き起こすドラフトコードは次のとおりです。



 void someProblemFunction() { { //open 1 } while (true) { // open 2 while (true) { // code 2 } // close 2 } { //close 2 } }
      
      







投機的な操作を耐え難いものにした後、非同期コードは次のようになります。



 void asyncSomeProblemFunction() { Async_InvokeInLoop( asyncSomeProblemFunctionOpen1, asyncSomeProblemFunctionCondition1, asyncSomeProblemFunctionLoop1, asyncSomeProblemFunctionClose1); } void asyncSomeProblemFunctionOpen1() { // code from open 1 } void asyncSomeProblemFunctionCondition1() { // code from loop 1 condition } void asyncSomeProblemFunctionLoop1() { Async_InvokeInLoop( asyncSomeProblemFunctionOpen2, asyncSomeProblemFunctionCondition2, asyncSomeProblemFunctionLoop2, asyncSomeProblemFunctionClose2); } void asyncSomeProblemFunctionClose1() { // code from close 1 }
      
      







地獄の仕事。 システム全体の中核はAsync_InvokeInLoop関数です。



 void Async_InvokeInLoop( void (*open)(), void (*condition)(bool* ref), void (*loop)(), void (*close)());
      
      







Async_InvokeInLoop - while(true)ループを非同期の同等のものに置き換えることができます。 この関数は、ループの開始前に開いた呼び出しを保証し、ループの終了後に閉じます。 条件およびループ関数への参照は、非同期反復への同等の参加者であり、名前から明らかです。 反復は、 Async_Loop関数を介して実装されます。



 void Async_Loop() { ScheduledAsync *top = STACK_TOP; switch (top->state) { case ScheduledAsync_OPEN: { top->open(); top->state = ScheduledAsync_CONDITION; return; } case ScheduledAsync_CONDITION: { top->condition(&top->conditionValue); top->state = ScheduledAsync_LOOP; return; } case ScheduledAsync_LOOP: { if (top->conditionValue) { top->loop(); top->state = ScheduledAsync_CONDITION; } else { top->state = ScheduledAsync_CLOSE; } return; } case ScheduledAsync_CLOSE: { popStack(); top->close(); free(top); return; } default: abort(); } }
      
      







ゲームループ(またはJavaScriptのタイマー)は定期的にこの関数をヤンクし、ゲーム内のすべてを強制的にスピンさせます。 元の関数が結果を返す場合、問題は2倍になります。結果をメモリにグローバルに保存し、他の関数で取得する必要があります。 すべてが合意に基づいて機能します。 その結果、プロジェクトを非同期化するためのフレームワークの地獄を手に入れました。そのインターフェースは次のとおりです。



 /* * async.h * * Created on: 19.10.2012 * Author: caiiiycuk */ #ifndef ASYNC_H_ #define ASYNC_H_ #include "types.h" extern void async_noop(); extern void async_false(bool *condition); extern void async_true(bool *condition); extern void Async_InvokeInLoop(void (*open)(), void (*condition)(bool* ref), void (*loop)(), void (*close)()); extern bool Async_IsPending(); extern void Async_Loop(); extern void Async_InvokeAfterAsync(void (*callback)()); extern void Async_InvokeAfterAsyncOrNow(void (*callback)()); extern void Async_Storage_uint16(uint16* storage); extern void Async_StorageSet_uint16(uint16 value); #endif /* ASYNC_H_ */
      
      







ゲームの同期性が非同期に変化し、いくつかの面白いバグが発生しました。





既知の問題



テスターのスタッフが私と私の架空の友人で構成されているという事実のため、私たちはただ知っています:



他のすべてが機能するか、機能するはずです。

トラッカー: https : //github.com/caiiiycuk/play-dune



私たちは遊んでいますか



http://play-dune.com/



71



UPD1。 ホットキー: habrahabr.ru/post/159501/#comment_5516325



UPD2。 サーバーが尋ねる、私は静的なリンクを直接与える:

Atreidesホームでプレイ

オルドスハウスでプレイ

ハルコネンハウスでプレイ



UPD3。 サーバーはtcpsndbuf制限(TCP接続を介してデータを送信するために使用できるバッファーの合計サイズ)に遭遇しました。 プロバイダーが私に設定した制限により、より効率的なサーバーを購入する余裕はありません。プレイできなかったらごめんなさい。 負荷が通常に戻るのを待っています。



UPD4。 それにもかかわらず、問題は私の曲がった手にありました。サーバーは負荷に対処する必要があります。



UPD5。 [請求書]ボタンはスペースポートでは機能しません。スペースポートでユニットを注文せずに[注文を送信]をクリックすると、[請求書]ボタンが自動的に押され、すべてがフリーズします。



UPD6。 新しいプロジェクトサイト。



All Articles