Duke Nukem 3Dソースコード分析:パート1

画像



Amazonを仕事に任せて、すばらしいソースコードを読むのに多くの時間を費やしました。



信じられないほど 素晴らしい idSoftware コード を扱っ たので 、私は史上最高のゲームの 1つ、Duke Nukem 3Dと「 Build 」と呼ばれるそのエンジンについて設定しました。



エンジン自体は非常に重要であり、その速度、安定性、メモリ消費量で高く評価されていますが、私の熱意はソースコードに出くわしました。これは、秩序、推奨事項、コメント/ドキュメントの順守という点で矛盾していました。 コードを読んでいる間、私はレガシーコードとソフトウェアが長生きできるようにするものについて多くを学びました。



いつものように、メモを記事に書き直しました。 彼女があなたにソースコードを読んであなたのスキルを向上させてくれることを願っています。



この記事を校正してくれたKen Silvermanに感謝したい。私の手紙に対する彼の忍耐と良心的な答えは私にとって重要だった。



起源



Duke Nukem 3D-1つではなく、 2つのコードベース:





なぜこの分離が必要なのですか? 開発が始まった1993年には、優れた3Dエンジンを作成するために必要なスキルと熱意を持っていたのは少数の人だけだったからです。 3D RealmsがDoomと競合するゲームを書くことにしたとき、強力なテクノロジーを見つける必要がありました。 その瞬間、ケンシルバーマンが登場します。



よく文書化されたWebサイトインタビューによると、ケン(当時18歳)は自宅で3Dエンジンを作成し、評価のために3D Realmsにデモを送りました。 彼らは彼のスキルが有望であることに気づき、同意しました







Silvermanは3D Realms用の新しいエンジンを作成しますが、ソースコードは保持します。



Engine.h



ヘッダーEngine.h



Engine.OBJ



バイナリスタティックライブラリEngine.OBJ



)のみを提供しEngine.h



。 3D Realmsチームは、ゲームモジュール( Game.OBJ



)を引き継ぎ、最終的な実行可能ファイルDUKE3D.EXE



をリリースします。



残念ながら、ゲームの両方の部分のソースコードは同時に開いていません。





その結果、完全なソースコードは、ゲームのリリースからわずか7年で利用可能になりました。



興味深い事実:エンジン名前「 Build 」は、新しいエンジンのカタログを作成するときにKen Silvermanによって選択されました。 彼はシソーラスを使用して、 「Construction」という単語の同義語を見つけました



最初の連絡



ソースコードはかなり前にリリースされたので(Watcom C / C ++コンパイラーとDOSシステム向け)、 Chocolate Doomに似たものを見つけようとしました。これは、Duke Nukem 3Dゲームプレイを90 -x、および最新のシステムでのシームレスなコンパイル。



Duke Nukemのソースコードコミュニティはもはやアクティブではないことが判明しました。多くのポートが再び古くなっており、一部はMacOS 9 PowerPC用です。 まだ1 つしかサポートされていません( EDuke32 )が、元のコードと比較して進化が進みすぎています。



その結果、 xDukeでの作業を開始しましたが 、LinuxおよびMac OS Xではコンパイルできませんでした(コードの読み取りとプロファイリングに最適なIDEであるXcodeの使用を排除しました)。



Visual Studioで作成されたxDukeは、元のコードを忠実に再現します。 エンジンとゲームの2つのプロジェクトが含まれています。 Engineプロジェクトは静的ライブラリ( Engine.lib



)にコンパイルされ、Gameプロジェクト( main



メソッドを含む)はそれにリンクされてduke3D.exe



を生成しduke3D.exe







VSを開くと、複雑なファイル名( ac



cache1d.c



)のために、エンジンソースはやや不親切に見えます。 これらのファイルには、目と脳に敵対的なものが含まれています。 Engine.cファイルの多くの例の1つを次に示します(693行目)



 if ((globalorientation&0x10) > 0) globalx1 = -globalx1, globaly1 = -globaly1, globalxpanning = -globalxpanning; if ((globalorientation&0x20) > 0) globalx2 = -globalx2, globaly2 = -globaly2, globalypanning = -globalypanning; globalx1 <<= globalxshift; globaly1 <<= globalxshift; globalx2 <<= globalyshift; globaly2 <<= globalyshift; globalxpanning <<= globalxshift; globalypanning <<= globalyshift; globalxpanning += (((long)sec->ceilingxpanning)<<24); globalypanning += (((long)sec->ceilingypanning)<<24); globaly1 = (-globalx1-globaly1)*halfxdimen; globalx2 = (globalx2-globaly2)*halfxdimen;
      
      





注:ファイル/変数名に数字が含まれている場合、これは非常に適切な名前ではない可能性があります!

game.c



game.c



コード最後の部分には、Duke Vスクリプトのドラフトが含まれています

注: xDukeはSDLを使用しますが、クロスプラットフォームAPIはWIN32タイマー( QueryPerformanceFrequency



)の利点を失いました。 使用されているSDLタイマーは、DOSで120 Hzの周波数をエミュレートするには不正確すぎるようです。



組立



SDLとDirectXヘッダー/ライブラリの場所を理解することにより、ワンクリックでコードをコンパイルできます。 とてもいいです。 最後に残されたのは、 DUKE3D.GRP



リソースDUKE3D.GRP



を取得することです。ゲームが開始されます...まあ、またはそのようなものです。 Vista / Windows 7では、SDLにパレットの問題があるようです。







ウィンドウモード(またはWindows 8のほうが良い)で実行すると、問題が解決するようです。







プロセス浸漬



これでゲームは機能します。 数秒で、 Buildがすべての素晴らしさで表示され、次のことを示します。





最後のポイントは、おそらく1996年に最も影響を受けたプレーヤーです。 潜水レベルは最高でした。 2次元マップにより技術が限界に達した場合でも、Todd ReplogleとAllen Blumは、プレイヤーをテレポートし、3次元の世界への没入感を高める「セクターエフェクター」を実装しました。 この関数は、伝説的なLAメルトダウンマップで使用されます。



プレイヤーが換気シャフトに飛び込むと:











セクターエフェクターは機能し、「着陸」前にプレイヤーをマップ上の完全に異なる場所にテレポートします。











良いゲームはゆっくりと老朽化し、デュークヌケムも例外ではありませんでした。20年経った今でも、プレイするのは信じられないほど楽しいです。 そして今、そのソースを調べることもできます!



エンジンライブラリの概要







エンジンコードは8503行の1つのファイルにあり、10個のメイン関数( Engine.c



)と2つの追加ファイルにあります。





3つの翻訳モジュールといくつかの機能が、理解しにくい高レベルのアーキテクチャを構成しています。 残念ながら、読者が遭遇する困難はこれらだけではありません。



セクション全体をビルドエンジンの内部専用にしました(以下を参照)。



ゲームモジュールの概要







ゲームモジュールは完全にエンジンモジュールの上に構築され、システムはプロセスで使用されるオペレーティングシステムを呼び出します。 ゲーム内のすべてはビルド (レンダリング、リソースのロード、ファイルシステム、キャッシングシステムなど)を通じて行われます。 唯一の例外はサウンドと音楽であり、 ゲームに完全に関連しています。



エンジンにもっと興味があったので、ここではあまり理解していませんでした。 しかし、このモジュールでは、より多くの経験と組織を見ることができます。15個のファイルがソースコードを理解可能なモジュールに分割します。 さらに、 types.h



stdint.h



のイニシエーター)があり、移植性が向上しています。



いくつかの興味深い点:





一般に、コードのこの部分は読みやすく、理解しやすいです。



継承されたソースコード



Doom / Quakeによって生成される無数のポートを見ると、なぜDuke Nukem 3Dポートがそんなに少ないのかといつも疑問に思いました。 Ken Silvermanが自分でそれを行うことを決定した後にのみ、エンジンがOpenGLに移植されたときに、同じ質問が生じました。



コードを確認したので、この記事の第2部でこれを説明しようと思います。



チョコレートデュークヌケム3D



私はこのエンジンが大好きでゲームが大好きなので、そのままにしておくことはできませんでした。「バニラ」ソースコードの移植版であるChocolate Duke Nukem 3Dを作成し、次の2つの目標を達成しようとしました。





このイニシアチブがコードの継承に役立つことを願っています。 最も重要な変更については、記事の第2部で説明します。



ビルドエンジンの内部



Buildは 、Duke Nukem 3Dや、 Shadow WarriorBloodなどの多くの成功したゲームで使用されています。 1996年1月29日にリリースされた時点で、彼は革新的な機能を備えたDoomエンジンを破壊しました。





王冠は、1996年6月に強力なPentiumで発売されたQuakeが彼から取ったものですが、数年の間、 Buildは当時のほとんどのコンピューターで高品質、デザイナーの自由、そして最も重要なこととして高速を提供しました。



基本コンセプト:ポータルシステム



ほとんどの3Dエンジンは、バイナリスペースパーティションまたはOctreeを使用してゲームカードを分割します。 たとえば、Doomは時間のかかる方法(最大30分)で各カードを前処理して、次のことを可能にするBSPツリーを作成しました。





しかし、スピードのために、譲歩しなければなりませんでした 。壁は動くことができませんでしたビルドはこの制限を削除し、カードを前処理しませんでしたが、代わりにポータルシステムを使用しまし











このマップでは、ゲームデザイナーは5つのセクター(上記)を描き、それらを互いに接続して、壁をポータルとしてマークしました(下)。



その結果、 Build worldデータベースはとてつもなくシンプルになりました。セクター用の1つの配列と壁用の1つの配列です。



 セクター(5エントリー):壁(29エントリー):
   ================================================== ======================
   0 |  startWall:0 numWalls:6 0 | ポイント= [x、y]、nextsector = -1 //セクター0の壁
   1 |  startWall:6 numWalls:8 .. |  //セクター0の壁
   2 |  startWall:14 numWalls:4 .. |  //セクター0の壁
   3 |  startWall:18 numWalls:3 3 | ポイント= [x、y]、nextsector = 1 //セクター0からセクター1へのポータル
   4 |  startWall:21 numWalls:8 .. |  //セクター0の壁
   ============================== .. |  //セクター0の壁
                                                .. |
                  セクターの最初の壁1 >> 6 | ポイント= [x、y]、nextsector = -1
                                                 7 | ポイント= [x、y]、nextsector = -1
                                                 8 | ポイント= [x、y]、nextsector = -1   
                                                 9 | ポイント= [x、y]、nextsector = 2 //セクター1からセクター2へのポータル
                                                10 | ポイント= [x、y]、nextsector = -1   
                                                11 | ポイント= [x、y]、nextsector = 0 //セクター1からセクター0へのポータル
                                                12 | ポイント= [x、y]、nextsector = -1
                セクターの最後の壁1 >> 13 | ポイント= [x、y]、nextsector = -1
                                                .. |
                                                28 | ポイント= [x、y]、nextsector = -1
                                                ============================================ 


ビルドに関する別の誤解-光線を放出しません。最初に頂点がプレーヤーの空間に投影され、次に視点から列/距離が生成されます。



デューティサイクルの概要



フレームレンダリングプロセスの概要:



  1. ゲームモジュールは、レンダリングを開始するセクターをエンジン モジュールに送信します(通常、これはプレーヤーのセクターですが、ミラーのあるセクターもあります)。
  2. エンジンモジュールはポータルシステムをバイパスし、 興味のあるセクターを訪問ます。 訪問した各セクター:

    • 壁はグループ(「バンチ」)と呼ばれるセットにグループ化されます。 それらはスタックに保存されます。
    • このセクターに表示されるスプライトが決定され、スタックに保存されます。
  3. グループは近くから遠い順に処理されます:固体の壁とポータルがレンダリングされます。
  4. レンダリングの停止: ゲームモジュールが表示されているスプライト更新するのを待機しています
  5. すべてのスプライトと透明な壁は、遠くから隣に向かって順番にレンダリングされます。
  6. バッファが切り替えられます。


コードの各ステップは次のとおりです。



  // 1.          . updatesector(int x, int y, int* lastKnownSectorID) displayrooms() { //   ,   .     (    ). drawrooms(int startingSectorID) { //   "gotsector",   "visited sectors". clearbufbyte(&gotsector[0],(long)((numsectors+7)>>3),0L); //   umost  dmost (  ). // 2.    :    ("bunch"). scansector(startingSectorID) { //        BUNCH. //          tsprite, spritesortcnt++ } //     numbunches   bunches. //   .   (O)n*n,        . while ((numbunches > 0) && (numhits > 0)) { //    (o) n*n for(i=1;i>numbunches;i++) { //   bunchfront test } //  ,   bunchID (closest) drawalls(closest); } } // 3.      ,       . animatesprites() // 4.    ,     ,    (, ). drawmasks() { while ((spritesortcnt > 0) && (maskwallcnt > 0)) { drawsprite or drawmaskwall } } } //   .  2D- (,   ) displayrest(); // 5.   nextpage()
      
      





興味深い事実:コードを学習すると、マップとして使用した完全に展開されたサイクルがあります。



興味深い事実:バッファスイッチングメソッドがnextpage()



と呼ばれるのはなぜですか? 90年代には、VGA / VESAのプログラミングの喜びにはダブルバッファリングの実装が含まれていました。ビデオメモリの2つの部分が順番に割り当てられ、使用されていました。 各部分は「ページ」と呼ばれていました。 1つの部分はVGA CRTモジュールによって使用され、2番目の部分はエンジンによって更新されました。 バッファの切り替えは、ベースアドレスを置き換える次のページ(「次のページ」)のCRTを使用することで構成されていました。 これについては、Michael Abrashのグラフィックプログラミングのブラックブック:Bones and sinewの第23章で詳しく読むことができます。



今日、SDLはビデオモードSDL_DOUBLEBUF



単純なフラグでこの作業を簡素化しますが、メソッド名は過去の成果物のままです。



1.レンダリングを開始する場所



BSPが存在しないということは、ポイントp(x,y)



を取得し、シートのセクターに到達するまでツリーのノードを通過することが不可能であることを意味します。 Buildではupdatesector(int newX, int newY, int* lastKnownSectorID)



を使用して各位置が更新された後、プレーヤーの現在のセクターを監視する必要があります。 ゲームモジュール、エンジンモジュールのこのメソッドを頻繁に呼び出します。



単純なupdatesector



実装updatesector



すべてのセクターを線形にスキャンし、毎回p(x,y)



セクターS 内にあるかどうかをチェックします。しかし、 updatesector



動作パターンによって最適化されます。



  1. lastKnownSectorID



    と、アルゴリズムはプレーヤーがあまり遠くに移動していないと想定し、セクターlastKnownSectorID



    からチェックを開始します。
  2. lastKnownSectorID



    1を完了できなかった場合、アルゴリズムはポータルを使用して隣接するlastKnownSectorID



    セクターをチェックします。
  3. そして最後に、最悪のシナリオでは、彼はすべてのセクターを線形検索でチェックします。


画像 左側のマップでは、プレーヤーの位置の最後の既知のセクターはID 1



のセクターでした。プレーヤーが移動した距離に応じて、 updatesector



は次の順序でチェックします。



  1. inside(x,y,1)



    (プレーヤーはセクターを出るまで移動していません)。
  2. inside(x,y,0)



    (プレーヤーは隣接するセクターにわずかに移動しました)。

    inside(x,y,2)





  3. inside(x,y,0)



    (プレイヤーはたくさん動いた:ゲームのすべてのセクターのチェックが潜在的に必要です)。

    inside(x,y,1)





    inside(x,y,2)





    inside(x,y,3)





    inside(x,y,4)





最悪のシナリオは非常にコストがかかる可能性があります。 しかし、ほとんどの場合、プレーヤー/シェルはあまり遠くに移動せず、ゲームの速度は高いままです。



内部の詳細



内部は、次の2つの理由から注目に値する方法です。





この方法は、 Buildがどのように機能するかを完全に示しているため、この方法を詳しく見ていきます。古き良きベクターアートワークとXORを使用します。



固定小数点コンピューティングとユビキタスベクターアートワークの時代



90年代のほとんどのコンピューターには浮動小数点数(FPU)のコプロセッサー(386SX、386DX、486SX)がなかったため、 Buildでは整数のみが使用されました。







この例は、エンドポイントAおよびBを持つ壁を示しています。タスクは、ポイントが左か右かを判断することです。







Michael AbrashのBlack Book of Programming:Reference of Frameの第61章で、この問題は単純なスカラー積と比較によって解決されています。



 bool inFrontOfWall(float plan[4], float point[3]) { float dot = dotProduct(plan,point); return dot < plan[3]; }
      
      





画像



しかし、浮動小数点演算のない世界では、問題はベクトル積によって解決されます。



 bool inFrontOfWall(int wall[2][2], int point[2]) { int pointVector[2], wallVector[2] ; pointVector[0] = point[0] - wall[0][0]; //  pointVector[1] = point[1] - wall[0][1]; wallVector[0] = wall[1][0] - wall[0][0]; //  wallVector[1] = wall[1][1] - wall[0][1]; //   crossProduct    Z:     Z. return 0 < crossProduct(wallVector,wallVector); }
      
      





興味深い事実: ビルドソースコードで検索文字列「float」を設定した場合、一致するものは1つもありません。

興味深い事実: float



型の使用は、Pentiumプロセッサおよび浮動小数点数用のコプロセッサを対象としたため、Quakeを普及させました。



凹多角形の内側



ベクトル積を使用して壁に対する点の位置を決定する方法を学習したので、 inside



詳しく見ることができます。







凹面のポリゴンと2つのポイントがある例:ポイント1とポイント2。





ポリゴン内のポイント(ポリゴン内ポイント、PIP)を決定するための「最新の」アルゴリズムは、左側にビームを放射し、交差する辺の数を決定することです。 奇数の場合、ポイントは内側にあり、偶数の場合-外側にあります。











Buildはこのアルゴリズムのバリエーションを使用します。各辺のエッジの数をカウントし、XORを使用して結果を結合します。











興味深い事実: DoomエンジンはR_PointOnSideでほぼ同じトリックを実行する必要がありました 。 QuakeはMod_PointInLeafでプレーンと浮動小数点演算を使用しました



興味深い事実:読みにくい場合は、Chocolate Duke Nukemのバージョンを調べることをお勧めします。コメントがあります。



2.ガントリーと不透明な壁



開始セクターは、 ゲームモジュールによってビルドエンジンに渡されます 。 レンダリングは、 drawrooms



不透明な壁から始まります。スタックで接続された2つのステップです。













「束」とは何ですか?



グループは、「潜在的に見える」と考えられる壁のセットです。 これらの壁は1つのセクターに属し、常に(ポイントで接続されて)プレーヤーに向けられています。



スタック内の壁のほとんどがドロップされ、その結果、画面上にレンダリングされるのは一部のみです。



注: 「壁プロキシ」は、「潜在的に見える」壁のリスト内の壁を指す整数です。 pvWalls配列には、ワールドデータベース内の壁へのリンクと、その座標、プレーヤーのスペースと画面スペースへの回転/移動が含まれます。



注:データ構造は実際にはより複雑です。グループの最初の壁のみがスタックに保存されます。 残りは、識別子へのリンクを含むリストとして使用される配列にあります。これは、グループをスタック内ですばやく上下に移動できるように行われます。



興味深い事実:フィルプロセスは配列を使用して、訪問した「セクター」をマークします。この配列は、各フレームの前にクリアする必要があります。現在のフレームでセクターが訪問されたかどうかを判断するために、 framenumberトリックは使用されません



興味深い事実: Doomエンジンでは、角度を画面の列に変換するために定量化が使用されました。ではビルドプレイヤーの空間に世界の頂点を変換する行列のCOS /罪を使用します



ポータルに入力する場合、次のヒューリスティックが使用されます。プレーヤーに向けられ、90度の範囲内にあるすべてのポータルがフラッディングされます。この部分は理解しにくいです。 しかし、サイクルを節約するために開発者がどのようにあらゆる場所を探したかを示しているため、興味深いものです。



 //  ->  Z tempint = x1*y2-x2*y1; //     ,     ,  . //  :        . if (((uint32_t)tempint+262144) < 524288) { //(x2-x1)*(x2-x1)+(y2-y1)*(y2-y1) is the squared length of the wall if (mulscale5(tempint,tempint) <= (x2-x1)*(x2-x1)+(y2-y1)*(y2-y1)) sectorsToVisit[numSectorsToVisit++] = nextsectnum; }
      
      





グループ生成



セクター内の壁は「バンチ」にグループ化されます。アイデアを説明する







図を次に示します。上の図は、3つのセクターが4つのグループを生成したことを示しています。





壁をグループにグループ化するのはなぜですか?Buildには迅速な並べ替えを可能にするデータ構造がないためです。彼はプロセス(O²)を使用して最も近いグループを取得します。これは、各壁で実行すると非常にコストがかかります。リソースのコストは、多くの壁で実行する場合よりもはるかに低くなります。



グループを使用する



グループのスタックを埋めると、エンジンは近くから遠くに向かってグループを描き始めます。エンジンは、別のグループによってブロックされていない最初のグループを選択します(常に、この条件を満たすグループが少なくとも1つあります)。



 /*  ,    :( */ closest = 0; tempbuf[closest] = 1; for(i=1; i < numbunches; i++){ if ((j = bunchfront(i,closest)) < 0) continue; tempbuf[i] = 1; if (j == 0){ tempbuf[closest] = 1; closest = i; } }
      
      





注:変数の名前にもかかわらず、選択されたグループは必ずしも最も近いとは限りません。



ケンシルバーマンによって与えられた選択の原則の説明:

2 . , x-. , . , x-. . : , . (: , , «» !) ( ) . , , , —


bunchfront



-高速で複雑で不完全なため、Build 結果をレンダラーに送信する前に二重チェックを実行します。これはコードを驚かせますが、結果としてO(n²)ではなくO(n)を取得します。



選択された各グループにはdrawalls(closest)



レンダラーが送信されます。コードのこの部分は、できるだけ多くの壁/床/天井を描画します。



壁/床/天井の可視化



この部分を理解するには、壁、床、天井など、すべてが垂直にレンダリングされることを理解することが重要です。



レンダラーコアには2つのクリッピング配列があります。一緒に、画面上のピクセルの各列の上下のクリッピング境界を追跡します。



 //    - 1600x1200 #define MAXXDIM 1600 //FCS: (     x,    ) short umost[MAXXDIM+1]; //FCS: (     x,    ) short dmost[MAXXDIM+1];
      
      





注:エンジンは通常、構造体配列ではなくプリミティブ型の配列を使用します。



スライダーは、上から下に向かって垂直方向のピクセル範囲を記録します。境界値は互いに向かって移動します。ピクセル列が完全にオーバーラップしていると判断すると、カウンター値が減少します。







注:ほとんどの場合、クリッピング配列は部分的にのみ更新されます。ポータルは「穴」を残します。







グループ内の各壁はスクリーンスペースに投影され、次のようになります。









停止条件:このサイクルは、すべてのグループが処理されるか、ピクセルのすべての列が完了としてマークされるまで続きます。



これは、シーンを分割する例、たとえば、誰もがよく知っているホールによって理解するのがはるかに簡単です:







マップでは、ポータルは赤で、白で塗りつぶされた壁で表示されます:







最初のグループの3つの壁は、画面スペースに投影されます:下部のみが画面に表示されます。







したがって、エンジンは床を垂直にレンダリングできます。







次に、エンジンは3つの壁のそれぞれに隣接するセクターを「見」-1



ます。値がでないため、これらの壁はポータルです。床の高さの違いを見て、エンジンはそれぞれの床にステップアップ(「UP」)を描く必要があることを理解しています。







そして、それが最初のグループでレンダリングするすべてです。これで、2番目のグループが画面スペースに投影されます。







また、下部のみが取得されます。これは、エンジンがフロアをレンダリングできることを意味します。







次のセクターを見て、最も長いポータルを見ると、エンジンは棚を引き上げることができます(「STEP UP」)。ただし、グループの2番目のポータルは下位セクターにつながります。棚は描画されません。







このプロセスが繰り返されます。シーン全体を示すビデオ



は次のとおりです。





ドアポータルレンダリングの結果として、Buildは2つの異なるテクスチャーでレッジを上下にレンダリングしました。これは、1つだけでどのように可能picnum



ですか?:



これは、構造に「picnum



があるために可能です。これはoverpicnum



片面の壁またはマスクのある壁の「」であり、反対セクターの壁から下のテクスチャのインデックスを「盗む」ことができるフラグです。それはセクター構造のサイズを小さく保つことを可能にしたハックでした。



私はビデオで他の2つのシーンを編集しました:



Street:





0:00から0:08まで:歩道の最下線のポータルを使用して、床の垂直部分を描画します。



0:08に、エンジンは歩道の後のセクターのフロアレベルを検索します。上昇すると、ポータルの壁が描画されます。歩道のレンダリングが完了します。



0:18〜0:30:左側の2つの歩道のグループを使用して、巨大な床をレンダリングします。



劇場の外:





これは、ウィンドウが表示される興味深いシーンです。



最後のビデオはウィンドウを示しています。作成方法の詳細は次の



とおり です。マップ:







最初のグループには、画面スペースに投影されたときに天井と床を描画できる4つの壁が含まれます:







グループの左壁はポータルです。勉強した後、次のセクターの床は同じ高さであり、天井が高いことがわかります。ここでは、棚壁をレンダリングする必要はありません。 2番目の壁は不透明です(マップ上で白でマークされています)。その結果、ピクセルの完全な列が描画されます







。3番目の壁はポータルです。次のセクターの高さを見ると、それが低いことがわかるので、棚壁を下にレンダリングする必要があります。







同じ「ピーピング」プロセスがフロアに対して実行されます。高いため、棚壁が作成され







ます。最後に、最後の壁が処理されます。これはポータルではないため、ピクセルの列全体が描画されます。







興味深い事実:壁は垂直に描画されるため、Buildは90度回転したRAMにテクスチャを保存します。これにより、キャッシュの使用が大幅に最適化されます。



3.一時停止



この時点で、表示されているすべての固体壁がオフスクリーンバッファに書き込まれます。エンジンは作業を停止し、ゲームモジュールが完了するのを待って、表示されているすべてのスプライトをアニメーション化します。これらのスプライトは、次の配列に書き込まれます。



 #define MAXSPRITESONSCREEN 1024 extern long spritesortcnt; extern spritetype tsprite[MAXSPRITESONSCREEN];
      
      





4.スプライトのレンダリング



一度ゲームモジュールは、すべての可視スプライトのアニメーションを完了し、ビルドは遠くから近くまでに:描画を開始しますdrawmasks()







  1. 視点から各スプライトまでの距離が計算されます。
  2. シェルアルゴリズムによってソートされたスプライトの配列
  3. エンジンは、2つのリスト(スプライト用と透明な壁用)から交互にデータを取得します。
  4. 1つのリストを使い果たした後、エンジンは分岐を最小限に抑えようとし、1つのタイプ(スプライトまたは壁)のみをレンダリングするループに切り替えます。


プロファイリング



Instrumentsデバッガーを介してDuke Nukem 3D実行すると、どのプロセッサーサイクルが費やされるかがわかります。



メインメソッド:







不思議ではありません。ほとんどの時間は不透明な壁のレンダリングとバッファーの切り替えの待機に費やされます(vsyncがオン)。



展示室の方法:







93%の時間が壁の塗装に費やされています。6%は、スプライト/透明な壁の視覚化のために予約されています。



ドロールーム方式:







その複雑さにもかかわらず、可視表面の定義(可視表面の決定)は、固体壁の可視化段階の0.1%しかかかりません。



Drawallsメソッド:







*スキャン関数-これは、エンジンとアセンブラープロシージャ間のインターフェイスです。





:

ceilscan() florscan() , . while. , . , , , . while Doom.


ケンは私スクリプト送信evaldrawspan_v2h.kcを水平バンドのリストに垂直バンドのリストを変換する方法ceilscan()とflorscan()を示す、







displayrest方法:







displayrest



から採取されたゲームユニットここでの主なコストは、レンダリング(武器)のコストです。ステータスバーはフレームごとにレンダリングされず、弾薬カウンターが変更されたときにのみ挿入されます。



VGAおよびVESAに関する興味深い事実



X-Mode VGAのおかげで、ほとんどのプレーヤーは320x240でビルド実行しました。しかし、VESA規格のおかげで、エンジンはSuper-VGA解像度もサポートしています。バニラソースにはVESAコードが装備されており、ポータブルな解像度の決定とレンダリングを提供します。



ノスタルジストはこの良いガイドでVESAのプログラミングについて読むことができます今日は、コード内で、たとえば、メソッドの名前だけを残しますgetvalidvesamodes()







サウンドエンジン



かつて、Duke3Dには印象的なサウンドシステムエンジンがありました。彼はサウンドエフェクトをミックスしてリバーブをシミュレートすることさえできました。詳細については、reverb.cをご覧ください



レガシーコード



ビルドコードは非常に読みにくいです。Duke Nukem 3DおよびBuildエンジン:ソースコードの問題とレガシーページに、これらの困難のすべての理由をリストしました



推奨読書



なし。より良く登る-それは素晴らしいです!



しかし、あなたが主張するなら、あなたはスーパーエゴとしてIDを読むことができます:デューク・ヌケム3Dの創造





[ .]



All Articles