冒険の書き方
この記事では、アドベンチャー開発の仕組みについて説明します。 提示されたアイデアのほとんどは抽象的なものですが、コードが提示されたり実装の詳細が議論される場所では、私の本「C#Game programming」のエンジンが使用されます。 すべてのコードは、MITライセンスの下でオンラインで入手できます。 この記事で説明したアプローチは、アドベンチャーゲームだけでなく広く適用できます。 たとえば、ナビゲーションメッシュのアイデアは、一度開発されると、Baulder's Gate、Planescape Tormentなどのゲームで使用されます。 わずかな変更で、ナビゲーションメッシュは3Dゲームでも使用できます。 インベントリと対話システムの相互作用のシステムは、RPG要素を備えたほとんどのゲームで変更できます。
アドベンチャーゲームとは何ですか?
アドベンチャーゲームは90年代前半に人気のあるジャンルでしたが、最近人気を集め始めました。 最も有名なクエストはおそらくモンキーアイランドシリーズで、野心的な海賊としてプレイします。
アドベンチャーユーザーインターフェイスはゲームごとに異なる場合がありますが、上の写真では「モンキーアイランド」インターフェイスを示しています。 プレイヤーはゲームシーンを利用でき、キャラクターは画面上のどこかにあります。 デフォルトでは、シーンをクリックすると、キャラクターは指定された場所に到達する傾向があります。 ゲーム画面上でマウスを動かすと、連絡できるものが強調表示されます。 たとえば、上の写真では、ダンジョンケージの城に沿ってコースを移動できます。 キャラクターはオブジェクトとやり取りするいくつかの方法があり、ゲーム「Monkey Island」では画面の下半分にリストされています。
「見る」を選択し、「開く」をクリックすると、ガイブラシは城の印象をささやきます(ゲーム内のキャラクターは、ガイブラシがオブジェクトと話しているという事実についてコメントしていません。 。
以下は、キャラクターが現在持っているアイテムを示す目録です。 Guybrushは、インベントリ内のアイテムやゲームシーン内のオブジェクトとやり取りできます。
これで、ユーザーインターフェースとゲームワールドとの相互作用に関する基本的なプレゼンテーションを受け取ったことを願っています。 本当に難しいことではありません。 当然、クエストはインターフェイスだけで決定されるわけではありません。 基本的に、このタイプのゲームには強力な陰謀があり、その間にプレーヤーはさまざまな問題を克服し、独創的なパズルを解決しなければなりません。 ほとんどのクエストでは、プレイヤーは対話システムを使用して他のゲームキャラクターと通信できます。 これらのキャラクターはNPC(非プレイキャラクター、非プレイキャラクター)とも呼ばれます。
アドベンチャーをサブシステムに分割するにはどうすればよいですか?
アドベンチャーは定評のあるジャンルです。つまり、ゲームは将来プログラムされるサブシステムに簡単に分割できます。
システムのコアは、一連のレベルで構成されています。 レベルの1つが他のレベルより高い場合、これは低いレベルがシステムのサブレベルとして使用されることを意味します。 これは、分離されたクリーンなアーキテクチャを作成するための優れたソリューションです。 エンジンはすでに多くのことをしてくれます:スプライトのロードと描画、オブジェクトのレンダリングとアニメーション、テキストは整列とさまざまなフォントで描画できます-ゲームのこれらの部分はすでに完了しています!
プログラムの最も難しい部分はナビゲーションシステムであり、記事のほとんどはそれに専念します。
ナビゲーション
ナビゲーションシステムは、現在の場所からクリックが行われたポイントにキャラクターを移動します。 このようなメカニズムを実装するには多くの方法がありますが、真の方法はありません。 一部のクエストでは一人称ビューを使用しているため、サイドトラッキングが問題になります(ShadowGateとMystを見てください)。
ほとんどの場合、クエストはプレイスペースにキャラクターを表示しますが、ヒーローが特定のタイプの問題を解決する場所が重要な場合があります。
通常、ゲームのナビゲーションはパスを見つけることで構成されます。 この記事よりもこのトピックに関する詳細情報を検索する場合は、これが適切なキーワードです。 パスは、壁、テーブル、小型犬などを通過せずに、ゲームキャラクターが目的の位置を達成するためにたどるラインです。 現在の場所のポイントとクリックが発生した位置の間のパスが検索されます。 何よりも、構築されたパスが最短であるか、最も正確に最短に近づいている場合、左に5メートル歩くように頼んだときにゲームキャラクターが画面をさまようことは望ましくありません。
パス検索アルゴリズムは、ゲームキャラクターの移動空間を表すグラフに基づいて構築されます。 グラフとは、アルゴリズムの理論分野の用語であり(心配しないで、定義は非常に単純です)、一連の頂点とそれらの間の関係から構築されます。
上記のグラフは、「外部からの道路」から「店舗ドア」ノードを経由して店舗までの経路のみを示しています。 これにより、キャラクターが画面をさまようことはありません。 システムにはいくつかのことに気付くかもしれません:
「キャラクターの自由度が少なすぎるように思えませんが、店の真ん中で立ち止まりたい場合はどうすればいいですか?」 カウントは彼がこれを行うことを許可しません。
これは絶対に正しい質問であり、許容移動ゾーンをグラフのノードに関連付けるナビゲーションシステムを使用して解決できます。
グラフはどのように構築されますか?
私たちはあえてそれを作りますが、それについては少し後で検討します。
グラフで最短経路を検索する
グラフで最短経路を見つけるための最も一般的なアルゴリズムは、アルゴリズムA *(星印)です。 このメソッドは、選択されたノードへの最短パスを見つけるためにグラフを検索します。 このアルゴリズムは、ゲームで広く使用されています。 A *に最適なリソースは、 A * Amitページです。 本質的に、彼はこの方法を説明する素晴らしい仕事をしたと思います、そして私はもっと良くしようとさえしません。 このページを読んでください! :D
A *のこのバージョンは、Googleコードリポジトリで入手できます。 もちろん、それは非常に基本的でシンプルですが、動作し、最短経路を見つけます。 これが実装へのリンクです。 (2つのピーク間のパスが見つからない場合、プログラムはクラッシュします-もちろん、近い将来、これを削除します:D)
*は、開始ノードから作業を開始し、すべての隣接ノードを通過し、最も近い頂点の重み値を計算します。 次に、アルゴリズムは、重みが最も高いすべてのネイバーに再帰的に適用されます。 重みは通常、ノードからターゲットまでの距離を設定する関数によって決定されます。 アルゴリズムは、ターゲットノードを検出するか、パスが存在しないと判断すると停止します。 これは要約にすぎません。理解を深めるために、 Amitのサイトにアクセスしてください。
ベルトの後ろにA *があると、グラフ上で最短経路を見つけることができますが、今のところ作成していません。
Navmeshアルゴリズムの使用
NavMeshは、すでにお気づきかもしれませんが、ナビゲーションメッシュ(グリッド)という用語の略語であり、簡単に言えば、キャラクターが移動できる領域を記述するポリゴンのセットにすぎません。 このメッシュは、特別に設計されたツールを使用して、図面の背景に描かれます。
上の図の青いポリゴンは、プレーヤーが移動できるスペースを表しています。 ピンクのドットは、プレイヤーが1つのポリゴンから別のポリゴンに移動できる場所を示すポリゴン間のリンクです。 テスト用のピンクのタイポイントとすべてのポリゴンの中心からグラフが作成されます。 グラフの開始点と終了点は、青いポリゴン内のどこにでも設定できます。
マウスクリックの時点で、プレーヤーの位置からパスを作成します。 これは、ナビゲーションメッシュの特定のポイント(x、y)が、どのポリゴンにあるかを判断できることを意味します。
Polygon FindPolygonPointIsIn(Point point) { foreach(Polygon polygon in _polygons) { if ( polygon.IntersectsWith(p) ) { return polygon; } } return null; }
上記のコードは多角形を見つけるトリックを行うことができますが、ポイントが多角形の内側にあるか外側にあるかを確認する方法の問題が残っています。 この例では、ナビゲーションメッシュ内のすべてのポリゴンが凸面であり、これはエディターに組み込まれているグリッドに厳密に設定されています。 凸ポリゴンは、テストの交差点をより簡単にするのに役立ち、作成されたnavmesでの作業がはるかに簡単です。
凹多角形の定義は非常に簡単です。 頂点の1つが多角形の内側にある場合、凹面になります。 中国と日本では、漢字はアイデアを表すために使用されます。次の2つの漢字を見てください。
どの図形が凸で、どの図形が凹であるかを判断してみてください。
以下は、多角形が凸面かどうかを判断するプログラムのリストです。 この場合、ポリゴンはポイントのセットを持つ単なるクラス(List _vertices)です。
bool IsConcave() { int positive = 0; int negative = 0; int length = _vertices.Count; for (int i = 0; i < length; i++) { Point p0 = _vertices[i]; Point p1 = _vertices[(i + 1) % length]; Point p2 = _vertices[(i + 2) % length]; // Subtract to get vectors Point v0 = new Point(p0.X - p1.X, p0.Y - p1.Y); Point v1 = new Point(p1.X - p2.X, p1.Y - p2.Y); float cross = (v0.X * v1.Y) - (v0.Y * v1.X); if (cross < 0) { negative++; } else { positive++; } } return (negative != 0 && positive != 0); }
交差点を確認するには、次のリストが必要です。
/// /// , . /// <a href="http://local.wasp.uwa.edu.au/%7Epbourke/geometry/insidepoly/">http://local.wasp.uwa.edu.au/~pbourke/geometry/insidepoly/</a> /// /// X /// Y /// ? public bool Intersects(float x, float y) { bool intersects = false; for (int i = 0, j = _vertices.Count - 1; i < _vertices.Count; j = i++) { if ((((_vertices[i].Y <= y) && (y < _vertices[j].Y)) || ((_vertices[j].Y <= y) && (y < _vertices[i].Y))) && (x < (_vertices[j].X - _vertices[i].X) * (y - _vertices[i].Y) / (_vertices[j].Y - _vertices[i].Y) + _vertices[i].X)) intersects = !intersects; } return intersects; }
現時点では、ポイントがポリゴン内にあるかどうかを判断できます。その結果、マウスのクリックが、キャラクターを移動するためにアクセス可能なゾーンに収まるかどうかを確認できます。 到達するまでに交差するポリゴンがない場合、選択したポイントに到達できず、クリックは無視されます。
ステージの最後のステップは、キャラクターの開始位置と終了位置、ポリゴンの中心を取得し、節点を接続して、A *アルゴリズムを実行するグラフを作成することです。
アルゴリズムの結果は、キャラクターが移動できるポイントのリストになります。 以下は、パスに沿ってキャラクターを移動するためのアルゴリズムの擬似コードです。
- 文字位置を取得-> pc_position
- パス上の現在のポイントの位置を取得する->ターゲット
- ターゲットへのpc_position方向を取得->方向
- pc_postionにwalkspeed方向を追加*
- PCが途中で現在の位置に近いかどうかを確認します
- trueの場合、現在のポイントをパスに沿って移動します
- パス上の現在のポイントが最終的な場合、キャラクターの動きを止めます
実際に使用するコードは、UpdatePath関数にあります。
アニメーション
最後のステップは、キャラクターのスプライトをレンダリングし、動きの方向に応じてアニメーションを実行することです。 実装されたコードは、選択されたベクトルの方向にキャラクターが移動することを既に保証しています。この方向をアニメーションの描画と比較するためだけに残ります。
これは、 kafkaskoffee.comで見つけたアートの例です(使用したバージョンの緑の枠線を削除しました)。
したがって、3つの静止位置とアニメーションの3つのオプションがあります。 「左」、「右」、「上」、「下」の方向への移動は、通常の変換によってプログラムで実装できます。
これらのアニメーションのいずれかのモーションベクトルを比較するために、次の変換を行いました。 4つのベクトルが各方向に導入されました:(-1、0)、(1、0)、(0、1)、(0、-1)。 次に、スカラー積を計算し、動きベクトルに近い方向ベクトルを調べた結果、目的のアニメーションを選択しました。
private Direction VectorToDirection(Vector direction) { Vector up = new Vector(0, 1, 0); Vector down = new Vector(0, -1, 0); Vector left = new Vector(-1, 0, 0); Vector right = new Vector(1, 0, 0); double upDiff = Math.Acos(direction.DotProduct(up)); double downDiff = Math.Acos(direction.DotProduct(down)); double leftDiff = Math.Acos(direction.DotProduct(left)); double rightDiff = Math.Acos(direction.DotProduct(right)); double smallest = Math.Min(Math.Min(upDiff, downDiff), Math.Min(leftDiff, rightDiff)); // yes there's a precidence if they're the same value, it doesn't matter if (smallest == upDiff) { return Direction.Up; } else if (smallest == downDiff) { return Direction.Down; } else if (smallest == leftDiff) { return Direction.Left; } else { return Direction.Right; } }
より良い方法を見つけることができると確信していますが、このオプションは確実に機能します。 キャラクターがクリックポイントに到達すると、アルゴリズムは終了し、最終ガイドベクトルに基づくスプライトオプションが最後の状態として選択されます。
すべてをまとめる
PS著者は翻訳を承認し、数字は実際に両方凹であると言ったが、漢字の1つの翻訳は凸であり、2番目は凹である