初年度にゲームエンジンを書く:簡単!(ほとんど)

こんにちは!私の名前はグレブマリインです。サンクトペテルブルクHSEで「応用数学とコンピュータサイエンス」の学部1年生になりました。2学期では、プログラムの新入生全員がチームプロジェクトをC ++で作成します。チームメイトと私はゲームエンジンを作成することにしました。 

猫の下にあるものを読んでください。


チームには、私、アレクセイルキニン、イリヤオノフリチュクの3人がいます。私たちの誰もゲーム開発の専門家ではなく、ゲームエンジンの作成の専門家でもありません。これは私たちにとって最初の大きなプロジェクトです。それ以前は宿題と実験室の仕事しかしていなかったので、コンピュータグラフィックスの分野の専門家がここで新しい情報を見つけることはほとんどありません。私たちのアイデアが、独自のエンジンを作成したい人にも役立つなら、私たちは喜んでいます。しかし、このトピックは複雑で多面的であり、記事は完全な専門文献であるとは決して主張していません。

私たちの実装について学ぶことに興味がある他のすべての人-読書をお楽しみください!

グラフィックアート


最初のウィンドウ、マウス、キーボード


ウィンドウを作成し、マウスとキーボードの入力を処理するために、SDL2ライブラリを選択しました。ランダムな選択でしたが、今のところ後悔はしていません。 

数行のウィンドウを作成し、カーソルを移動して全画面モードに入るなどの操作を行い、キーストローク、カーソルの移動などのイベントを処理できるように、ライブラリの便利なラッパーを作成することが最初の段階で重要でした。タスクは難しくありませんでした。ウィンドウを閉じたり開いたりできるプログラムをすばやく作成し、RMBをクリックすると、「Hello、World!」と表示しました。 

次に、メインのゲームサイクルが表示されます。

Event ev;
bool running = true;
while (running):
	ev = pullEvent();
	for handler in handlers[ev.type]:
		handler.handleEvent(ev);

添付の各イベントハンドラ- handlersなどhandlers[QUIT] = {QuitHandler()}彼らの仕事は、対応するイベントを処理することです。QuitHandlerこの例では、を公開してrunning = false、ゲームを停止します。

こんにちは世界


エンジンでの描画には、を使用していますOpenGLHello World私が思うに、最初のものは、多くのプロジェクトで、黒い背景に白い四角でした。 

glBegin(GL_QUADS);
glVertex2f(-1.0f, 1.0f);
glVertex2f(1.0f, 1.0f);
glVertex2f(1.0f, -1.0f);
glVertex2f(-1.0f, -1.0f);
glEnd();


次に、2次元のポリゴンを描画する方法を学び、でGraphicalObject2d回転glRotate、移動glTranslate、ストレッチできる別のクラス図を実行しましたglScaleを使用して、4つのチャネルで色を設定しglColor4f(r, g, b, a)ます。

この機能を使用すると、すでに正方形の美しい噴水を作成できます。ParticleSystemオブジェクトの配列を持つクラス作成します。メインループが繰り返されるたびに、パーティクルシステムは古い正方形を更新し、ランダムな方向から始まる新しい正方形を収集します。



カメラ


次のステップは、さまざまな方向に移動して見ることができるカメラを作成することでした。この問題を解決する方法を理解するには、線形代数の知識が必要でした。これがあまり面白くない場合は、セクションをスキップしてgifを参照し、を読んでください

画面の座標に頂点を描画し、それが属しているオブジェクトの中心を基準にした座標を知っているとします。

  1. まず、オブジェクトが配置されている世界の中心を基準にした座標を見つける必要があります。
  2. 次に、カメラの座標と位置を知って、カメラのベースにある頂点の位置を見つけます。
  3. 次に、頂点をスクリーンの平面に投影します。 

ご覧のとおり、3つの段階があります。3つの行列による乗算はそれらに対応します。私たちは、これらの行列と呼ばれるModelViewProjection

まず、世界を基準にしたオブジェクトの座標を取得します。オブジェクトを使用して、スケール、回転、移動という3つの変形を行うことができます。これらの操作はすべて、元のベクトル(オブジェクトに基づく座標)に対応する行列を乗算することによって指定されます。すると、マトリックスModelは次のようになります。 

Model = Translate * Scale * Rotate. 

さらに、カメラの位置がわかっているので、その基準で座標を決定します。以前に取得した座標に行列を掛けViewます。C ++では、これは関数を使用して簡単に計算されます。


glm::mat4 View = glm::lookAt(cameraPosition, objectPosition, up);

文字通り:objectPosition位置から見てcameraPosition、上方向が「上」になります。なぜこの方向が必要なのですか?ティーポットの写真を想像してみてください。カメラを彼に向け、やかんをフレームに置きます。この時点で、フレームが一番上にある場所(たぶん、やかんに蓋がある場所)を正確に言うことができます。プログラムは、フレームの配置方法を私たちに理解させることができません。そのため、「アップ」ベクトルを指定する必要があります。

カメラを基準に座標を取得しましたが、取得した座標をカメラの平面に投影したままです。マトリックスはこれProjection関与し、オブジェクトが私たちから削除されたときにオブジェクトを減らす効果を生み出します。

画面上の頂点の座標を取得するには、ベクトルに行列を少なくとも5回掛ける必要があります。すべての行列のサイズは4 x 4であるため、かなりの数の乗算演算を実行する必要があります。多くの単純なタスクでプロセッサコアをロードしたくありません。これには、必要なリソースを備えたビデオカードの方が適しています。したがって、シェーダーを作成する必要があります。これは、ビデオカード用の小さな命令です。OpenGLには、Cに類似した特別なGLSLシェーダー言語があり、これを行うのに役立ちます。シェーダーの作成の詳細については説明しません。何が起こったかを最後に確認することをお勧めします。


説明:10個の正方形があり、それらの間隔は短いです。それらの右側には、カメラを回転させて動かすプレーヤーがいます。 

物理


物理学のないゲームとは何ですか?物理的な相互作用を処理するために、Box2dライブラリを使用することを決定し、WorldObject2dから継承するクラス作成しましたGraphicalObject2d。残念ながら、Box2dはそのままでは機能しなかったため、勇敢なイリヤはb2Bodyおよびこのライブラリにあるすべての物理接続のラッパーを作成しました。


それまでは、エンジン内のグラフィックを完全に2次元にすることを考えていました。照明については、追加する場合はレイキャスティングテクニックを使用します。しかし、3次元すべてでオブジェクトを表示できる素晴らしいカメラを手元に用意しました。したがって、すべての2次元オブジェクトに厚さを追加しました-なぜですか?さらに、将来的には、これにより、厚いオブジェクトからの影を残す非常に美しい照明を作成できるようになります。

ケース間に照明が現れました。これを作成するには、各ピクセルを描画するための適切な指示、つまりフラグメントシェーダーを記述する必要がありました。



テクスチャー


DevILライブラリを使用して画像をアップロードしました。それぞれGraphicalObject2dがクラスの1つのインスタンス(GraphicalPolygonオブジェクトの前部とGraphicalEdge側部)に適合しましたそれぞれでテクスチャをストレッチできます。最初の結果:


グラフィックから必要なすべての準備ができています:描画、1つの光源、テクスチャ。グラフィックス-それは今のところそれです。

ステートマシン、オブジェクトの動作の設定


すべてのオブジェクトは、それが何であれ、ステートマシン内の状態、グラフィック、または物理的なものであり、「動く」必要があります。つまり、ゲームループの各反復が更新されます。

更新できるオブジェクトは、作成したBehaviorクラスから継承されます。これにはonStart, onActive, onStop、起動時、寿命中、およびアクティビティの終了時に、相続人の動作をオーバーライドできる機能があります。次にActivity、すべてのオブジェクトからこれらの関数を呼び出す最高のオブジェクトを作成する必要があります。これを行うループ関数は次のとおりです。

void loop():
    onAwake();
    awake = true;
    while (awake):
        onStart();
        running = true
        while (running):
            onActive();
        onStop();
    onDestroy();

現時点ではrunning == true、誰かが関数pause()呼び出すことができますrunning = false。誰かの呼び出しが場合はkill()、その後awake、とrunningに変わりfalse完全に停止する、と活動。

問題:パーティクルのシステムとその中のパーティクルなど、オブジェクトのグループを一時停止したい。現在の状態ではonPause、各オブジェクトを手動で呼び出す必要があるため、あまり便利ではありません。

解決策:誰もBehaviorsubBehaviors彼が更新する配列持っています、つまり:

void onStart():
	onStart() 		//     
	for sb in subBehaviors:
		sb.onStart()	//       Behavior
void onActive():
	onActive()
	for sb in subBehaviors:
		sb.onActive()

など、関数ごとに。

ただし、すべての動作をこのように設定できるわけではありません。たとえば、敵がプラットフォーム上を歩いている場合、敵はさまざまな状態を持っている可能性があります。立っているidle_stay、私たちidle_walk気付かれずにプラットフォーム上を歩いている、いつでも私たち気づいて攻撃状態に入ることができattackます。また、状態間の遷移の条件を便利に設定したいと思います。次に例を示します。

bool isTransitionActivated(): 		//  idle_walk->attack
	return canSee(enemy);

望ましいパターンはステートマシンです。またBehavior、ティックごとに状態を切り替える時期が来たかどうかを確認する必要があるため、私たちは彼女を相続人にしましたこれはゲーム内のオブジェクトだけでなく便利です。たとえば、Levelこれは状態Level Switcherであり、コントローラーマシン内の遷移は、ゲームのレベルを切り替えるための条件です。

状態には3つの段階があります。それは始まった、刻々と過ぎている、それは止まっています。各ステージにアクションを追加できます。たとえば、オブジェクトにテクスチャをアタッチしたり、インパルスを適用したり、速度を設定したりできます。

保全


エディターでレベルを作成し、それを保存できるようにしたいので、ゲーム自体が保存されたデータからレベルをロードできるはずです。したがって、保存する必要があるすべてのオブジェクトは、クラスから継承されNamedStoredObjectます。名前、クラス名dump()を含む文字列を格納し、オブジェクトに関するデータを文字列にダンプする機能備えています。  

保存するには、dump()オブジェクトごとにオーバーライドするだけです。 Loadingは、オブジェクトに関するすべての情報を含む文字列からのコンストラクタです。このようなコンストラクタがオブジェクトごとに作成されると、ダウンロードは完了です。 

実際、ゲームとエディターはほぼ同じクラスであり、ゲームでのみ、レベルは読み取りモードで読み込まれ、エディターでは記録モードで読み込まれます。エンジンは、rapidjsonライブラリを使用して、jsonからオブジェクトを読み書きします。

GUI


ある時点で、私たちの前で疑問が浮上しました。グラフィックス、ステートマシン、およびその他すべてを記述します。これを使用してユーザーはどのようにゲームを書くことができますか? 

元のバージョンでは、彼はから継承しGame2dてオーバーライドしonActive、クラスのフィールドにオブジェクトを作成する必要がありました。しかし、作成中は、自分が作成しているものを見ることができず、プログラムをコンパイルしてライブラリにリンクする必要もあります。ホラー!プラスがあります-想像できるような複雑な動作を尋ねることができます。たとえば、土地のブロックをプレーヤーの生活と同じくらい移動します。天王星がおうし座にあり、ユーロが超えない限り、そうします40ルーブル。ただし、それでもグラフィカルインターフェイスを作成することにしました。

グラフィカルインターフェイスでは、オブジェクトで実行できるアクションの数が制限されます。アニメーションスライドをめくる、力を加える、特定の速度を設定するなどです。状態マシンの遷移と同じ状況。大規模なエンジンでは、現在のプログラムを別のプログラムとリンクすることにより、アクションの数が限られているという問題が解決されます。たとえば、UnityとGodotはC#とのバインディングを使用します。既にこのスクリプトから、あなたは何でもすることができます:そして、どの星座天王星で、そして現在のユーロ為替レートは何であるかを見てください。現時点ではそのような機能はありませんが、エンジンとPython 3の接続を計画しています。

グラフィカルインターフェースを実装するために、Dear ImGuiを使用することにしました。これは、(よく知られたQtと比較して)非常に小さく、書き込みが非常に簡単であるためです。ImGui-グラフィカルインターフェイスを作成するパラダイム。その中で、メインループのすべての繰り返し、すべてのウィジェットとウィンドウは必要な場合にのみ再描画されます。一方で、これは消費されるメモリの量を削減しますが、他方では、後続の描画に必要な情報を作成および保存する複雑な機能の1回の実行よりも時間がかかる可能性があります。作成と編集のためのインターフェースを実装するだけです。

記事のリリース時のGUIは次のようになります。


レベルエディター


ステートマシンエディター

結論


私たちはあなたがもっと面白いものを掛けることができる基礎だけを作りました。つまり、成長の余地があります。シャドウレンダリングを実装したり、複数の光源を作成したり、エンジンをPython 3インタープリターに接続して、ゲームのスクリプトを作成したりできます。インターフェイスを改良したいと思います。それをより美しくし、より多くの異なるオブジェクトを追加し、ホットキーをサポートします...

まだ多くの作業がありますが、現時点で満足しています。 

プロジェクトの作成中に、グラフィックスの操作、グラフィカルインターフェイスの作成、jsonファイルの操作、多数のCライブラリのラッパーなど、さまざまな経験を積みました。また、チームで最初の大きなプロジェクトを書いた経験。対処するのが面白かったのと同じくらい面白かったので、お伝えできたことを願っています:)

gihabプロジェクトへのリンク:github.com/Glebanister/ample

All Articles