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
をリリースします。
残念ながら、ゲームの両方の部分のソースコードは同時に開いていません。
- エンジンのソースコードは、2000年6月20日にKen Silvermanによってリリースされました。
- ゲームモジュールのソースコードは、2003年4月1日に3D Realmsによってリリースされました。
その結果、完全なソースコードは、ゲームのリリースからわずか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がすべての素晴らしさで表示され、次のことを示します。
- 傾斜天井
- 現実的な環境
- 自由落下
- 真の3Dの感覚。
最後のポイントは、おそらく1996年に最も影響を受けたプレーヤーです。 潜水レベルは最高でした。 2次元マップにより技術が限界に達した場合でも、Todd ReplogleとAllen Blumは、プレイヤーをテレポートし、3次元の世界への没入感を高める「セクターエフェクター」を実装しました。 この関数は、伝説的なLAメルトダウンマップで使用されます。
プレイヤーが換気シャフトに飛び込むと:
セクターエフェクターは機能し、「着陸」前にプレイヤーをマップ上の完全に異なる場所にテレポートします。
良いゲームはゆっくりと老朽化し、デュークヌケムも例外ではありませんでした。20年経った今でも、プレイするのは信じられないほど楽しいです。 そして今、そのソースを調べることもできます!
エンジンライブラリの概要
エンジンコードは8503行の1つのファイルにあり、10個のメイン関数(
Engine.c
)と2つの追加ファイルにあります。
-
cache1.c
:仮想ファイルシステム(sic!)およびキャッシュシステムプロシージャが含まれています。 -
ac
:高度に最適化されたx86アセンブラーである、再作成されたリバースエンジニアリングコードのC実装。 コードは動作しますが、それを読むことは大きな苦痛です!
3つの翻訳モジュールといくつかの機能が、理解しにくい高レベルのアーキテクチャを構成しています。 残念ながら、読者が遭遇する困難はこれらだけではありません。
セクション全体をビルドエンジンの内部専用にしました(以下を参照)。
ゲームモジュールの概要
ゲームモジュールは完全にエンジンモジュールの上に構築され、システムはプロセスで使用されるオペレーティングシステムを呼び出します。 ゲーム内のすべてはビルド (レンダリング、リソースのロード、ファイルシステム、キャッシングシステムなど)を通じて行われます。 唯一の例外はサウンドと音楽であり、 ゲームに完全に関連しています。
エンジンにもっと興味があったので、ここではあまり理解していませんでした。 しかし、このモジュールでは、より多くの経験と組織を見ることができます。15個のファイルがソースコードを理解可能なモジュールに分割します。 さらに、
types.h
(
stdint.h
のイニシエーター)があり、移植性が向上しています。
いくつかの興味深い点:
-
game.c
は、game.c
行のコードのモンスターです。 -
menu.c
は、「スイッチケース」をmenu.c
3,000行があります。 - ほとんどのメソッドには「void」パラメータがあり、「void」を返します。 すべてはグローバル変数を介して行われます。
- メソッド名はcamelCaseまたはNAMESPACEプレフィックスを使用しません。
- このモジュールには優れたパーサー/レキシカルアナライザーがあります が 、 トークン値 は
#define
ではなく10進数値で渡されます。
一般に、コードのこの部分は読みやすく、理解しやすいです。
継承されたソースコード
Doom / Quakeによって生成される無数のポートを見ると、なぜDuke Nukem 3Dポートがそんなに少ないのかといつも疑問に思いました。 Ken Silvermanが自分でそれを行うことを決定した後にのみ、エンジンがOpenGLに移植されたときに、同じ質問が生じました。
コードを確認したので、この記事の第2部でこれを説明しようと思います。
チョコレートデュークヌケム3D
私はこのエンジンが大好きでゲームが大好きなので、そのままにしておくことはできませんでした。「バニラ」ソースコードの移植版であるChocolate Duke Nukem 3Dを作成し、次の2つの目標を達成しようとしました。
- トレーニング:読みやすさ、理解、移植性。
- 信頼性:ゲームプレイは、1996年に486で見たものと似ているはずです。
このイニシアチブがコードの継承に役立つことを願っています。 最も重要な変更については、記事の第2部で説明します。
ビルドエンジンの内部
Buildは 、Duke Nukem 3Dや、 Shadow WarriorやBloodなどの多くの成功したゲームで使用されています。 1996年1月29日にリリースされた時点で、彼は革新的な機能を備えたDoomエンジンを破壊しました。
- 破壊可能な環境
- 傾斜した床と天井
- 鏡
- 上下に見る機能
- 水中を飛ぶ、う、泳ぐ能力
- ボクセルオブジェクト (後で「血液」に表示)
- 3Dに没頭する(テレポーターのおかげ)。
王冠は、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. . 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
動作パターンによって最適化されます。
-
lastKnownSectorID
と、アルゴリズムはプレーヤーがあまり遠くに移動していないと想定し、セクターlastKnownSectorID
からチェックを開始します。 -
lastKnownSectorID
1を完了できなかった場合、アルゴリズムはポータルを使用して隣接するlastKnownSectorID
セクターをチェックします。 - そして最後に、最悪のシナリオでは、彼はすべてのセクターを線形検索でチェックします。
左側のマップでは、プレーヤーの位置の最後の既知のセクターはID
1
のセクターでした。プレーヤーが移動した距離に応じて、
updatesector
は次の順序でチェックします。
-
inside(x,y,1)
(プレーヤーはセクターを出るまで移動していません)。 -
inside(x,y,0)
(プレーヤーは隣接するセクターにわずかに移動しました)。
inside(x,y,2)
-
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。
- ポイント1は外にあると見なされます。
- ポイント2は内部にあると見なされます。
ポリゴン内のポイント(ポリゴン内ポイント、PIP)を決定するための「最新の」アルゴリズムは、左側にビームを放射し、交差する辺の数を決定することです。 奇数の場合、ポイントは内側にあり、偶数の場合-外側にあります。
Buildはこのアルゴリズムのバリエーションを使用します。各辺のエッジの数をカウントし、XORを使用して結果を結合します。
興味深い事実: DoomエンジンはR_PointOnSideでほぼ同じトリックを実行する必要がありました 。 QuakeはMod_PointInLeafでプレーンと浮動小数点演算を使用しました 。
興味深い事実:読みにくい場合は、Chocolate Duke Nukemのバージョンを調べることをお勧めします。コメントがあります。
2.ガントリーと不透明な壁
開始セクターは、 ゲームモジュールによってビルドエンジンに渡されます 。 レンダリングは、
drawrooms
不透明な壁から始まります。スタックで接続された2つのステップです。
- 前処理段階では、ポータルシステムを
startingSectorID
scansector()
させ(startingSectorIDでstartingSectorID
)、スタック上の壁を保存します:scansector()
。 - スタックは「束」要素で構成されます。
- スタック要素はレンダラーメソッドに渡されます:
drawwalls()
。
「束」とは何ですか?
グループは、「潜在的に見える」と考えられる壁のセットです。 これらの壁は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つのグループを生成したことを示しています。
- セクター1は、1つの壁を含む1つのグループを生成しました。
- セクター2は、3つの壁を含む1つのグループを生成しました。
- セクター3は2つのグループを生成し、各グループには2つの壁が含まれています。
壁をグループにグループ化するのはなぜですか?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()
。
- 視点から各スプライトまでの距離が計算されます。
- シェルアルゴリズムによってソートされたスプライトの配列
- エンジンは、2つのリスト(スプライト用と透明な壁用)から交互にデータを取得します。
- 1つのリストを使い果たした後、エンジンは分岐を最小限に抑えようとし、1つのタイプ(スプライトまたは壁)のみをレンダリングするループに切り替えます。
プロファイリング
Instrumentsデバッガーを介してDuke Nukem 3Dを実行すると、どのプロセッサーサイクルが費やされるかがわかります。
メインメソッド:
不思議ではありません。ほとんどの時間は不透明な壁のレンダリングとバッファーの切り替えの待機に費やされます(vsyncがオン)。
展示室の方法:
93%の時間が壁の塗装に費やされています。6%は、スプライト/透明な壁の視覚化のために予約されています。
ドロールーム方式:
その複雑さにもかかわらず、可視表面の定義(可視表面の決定)は、固体壁の可視化段階の0.1%しかかかりません。
Drawallsメソッド:
*スキャン関数-これは、エンジンとアセンブラープロシージャ間のインターフェイスです。
-
wallscan()
:壁 -
ceilscan()
:傾斜のない天井 -
florscan()
:傾斜のない床 -
parascan()
: (wallscan()
) -
grouscan()
: —
:
ceilscan() florscan() , . while. , . , , , . while Doom.
ケンは私スクリプト送信evaldraw、span_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の創造
- ポータルに基づく別のゲームについてお読みになりたい場合は、Shee BarrettのThief:The Dark Projectに関する優れた事後分析をお勧めします。
- ケン・シルバーマンとの多くのインタビュー:
- 2005年11月21日、classicdosgames.com(ミラー)。
- 2005年、strifestreams.com(ミラー)。
- 27 2006 , 3drealms.com ( ).
- 25 2010 misterdai.yougeezer.co.uk ( ).
- 1 2012 videogamepotpourri.blogspot.ca ( ).
[ .]