初めてのディアブロ、エイジオブエンパイア、コマンドなど、私たちは皆素晴らしいアイソメトリックゲームをプレイしました。 等尺性ゲームとの最初の会議で、 2次元 、 3次元、またはまったく異なる何かを尋ねることができます。 アイソメトリックゲームの世界自体は、開発者にとって魅力的な魅力があります。 等角投影の秘密を明らかにし、簡単な等尺性レベルを作成してみましょう。
このため、JSコードでPhaserを使用することにしました。 結果は、インタラクティブなHTML5アプリケーションです。
これはPhaser開発のチュートリアルではないことに注意してください。アイソメトリックシーンを作成するための基本的な概念を簡単に理解するために使用します。 Phaserには、 Phaser Isometric Pluginなどの等尺性コンテンツを簡単に作成する方法もあります。
シーンの作成を簡素化するために、タイルを使用します。
1.タイルベースのゲーム
2次元タイルゲームでは、各視覚要素はタイルと呼ばれる標準サイズの小さな断片に分割されます。 これらのタイルのうち、レベルデータ(通常は2次元配列)に基づいて、ゲームワールドが形成されます。
ほとんどの場合、タイルゲームでは、 トップ ビューまたはサイドビューが使用されます。 図に示すように、2つのタイル( 草 タイルと壁タイル )を備えた標準の2次元の上面図を想像してみましょう。
これらのタイルは両方とも同じサイズの正方形の画像です。つまり、タイルの高さと幅は同じです。 ゲームのレベルは、すべての側面が壁で囲まれた芝生であると想定します。 この場合、レベルデータは次のような2次元配列です。
[ [1,1,1,1,1,1], [1,0,0,0,0,1], [1,0,0,0,0,1], [1,0,0,0,0,1], [1,0,0,0,0,1], [1,1,1,1,1,1] ]
ここで、
0
は草のタイル、
1
は壁のタイルです。 レベルデータに従ってタイルを配置したら、図に示すようにフェンス付きの芝生を作成します。
別の手順を実行して、コーナータイルと、垂直壁と水平壁の個別のタイルを追加できます。 これには、さらに5つのタイルが必要です。さらに、レベルデータを変更する必要があります。
[ [3,1,1,1,1,4], [2,0,0,0,0,2], [2,0,0,0,0,2], [2,0,0,0,0,2], [2,0,0,0,0,2], [6,1,1,1,1,5] ]
以下の画像を参照してください。レベルデータの値に対応する番号でタイルをマークしました。
タイルレベルの概念を理解したので、2次元グリッドの単純な擬似コードを使用してレベルを構築する方法を見てみましょう。
for (i, loop through rows) for (j, loop through columns) x = j * tile width y = i * tile height tileType = levelData[i][j] placetile(tileType, x, y)
上記の幅と高さが同じタイルの画像を使用すると、同じレベルの寸法が得られます。 この例のタイルの幅と高さが50ピクセルの場合、レベルの合計サイズは300 x 300ピクセルになります。
前述のように、通常、タイルは上面または側面ビューのゲームで使用されます。 等角投影ビューの場合、等角投影を実装する必要があります。
2.等角図
等角投影法の最良の技術的説明は、Clint Bellengerによるこの記事で説明されているように思えます。
カメラを2つの軸に沿って傾けます(カメラを横に45度、次に30度下に回転させます)。 これにより、セルの幅が高さの2倍の菱形グリッドが作成されます。 このスタイルは、戦略ゲームとアクションRPGのおかげで人気になりました。 このフォームのキューブを見ると、その3つの側面(上側と2つの側面)が見えます。
これは少し複雑に聞こえますが、この種の実装は非常に簡単です。 2次元空間と等尺性空間がどのように関連しているか、つまりレベルデータとビューの関係を理解する必要があります。 トップビューのデカルト座標を等角座標に変換する必要があります。 以下の図は、グラフィック変換を示しています。
等尺性タイルの配置
2次元配列に保存されたレベルデータと等角投影ビューの関係、つまりデカルト座標を等角投影に変換するプロセスを単純化してみましょう。 フェンスで囲まれた芝生の等角図を作成します。 このレベルの2次元実装は、幅と高さでオフセットされた正方形タイルを使用した2サイクルの単純な反復でした。 アイソメビューの場合、擬似コードは同じままですが、
placeTile()
関数は変更されます。
元の関数は、渡された
x
および
y
にタイルの画像を描画するだけです。アイソメビューの場合、対応するアイソメ座標を計算する必要があります。 この方程式を以下に示します。
isoX
および
isoY
は等尺性のXおよびY座標であり、
cartX
および
cartY
はデカルトのXおよびY座標です。
// : isoX = cartX - cartY; isoY = (cartX + cartY) / 2;
// : cartX = (2 * isoY + isoX) / 2; cartY = (2 * isoY - isoX) / 2;
はい、それだけです。 これらの単純な方程式は、等角投影の魔法を作成します。 非常に便利な
Point
クラスを使用して、あるシステムから別のシステムに変換するために使用できるPhaserのヘルパー関数を次に示します。
function cartesianToIsometric(cartPt){ var tempPt=new Phaser.Point(); tempPt.x=cartPt.x-cartPt.y; tempPt.y=(cartPt.x+cartPt.y)/2; return (tempPt); }
function isometricToCartesian(isoPt){ var tempPt=new Phaser.Point(); tempPt.x=(2*isoPt.y+isoPt.x)/2; tempPt.y=(2*isoPt.y-isoPt.x)/2; return (tempPt); }
したがって、
cartesianToIsometric
ヘルパーメソッドを使用して、
placeTile
メソッド内で入力2D座標をアイソメトリックに変換できます。 これ以外は、表示コードは同じままですが、新しいタイル画像を作成する必要があります。 トップビューから古い正方形のタイルを使用することはできません。 下の図は、草と壁の新しい等尺性タイルと完成した等尺性レベルを示しています。
信じられないよね? 通常の2次元位置がどのように等尺性に変換されるかを見てみましょう。
2D point = [100, 100]; // isoX = 100 - 100; // = 0 isoY = (100 + 100) / 2; // = 100 Iso point == [0, 100];
つまり、入力データ
[0, 0]
[0, 0]
に、
[10, 5]
[5, 7.5]
変換されます。
フェンスで囲まれた芝生の場合、目的の座標で配列要素の値が
0
に等しいかどうかを確認することで、通過可能な領域を決定できます。 等しい場合、これは草です。 これを行うには、配列の座標を決定する必要があります。 この関数を使用して、デカルト座標からレベルデータでタイルの座標を見つけることができます。
function getTileCoordinates(cartPt, tileHeight){ var tempPt=new Phaser.Point(); tempPt.x=Math.floor(cartPt.x/tileHeight); tempPt.y=Math.floor(cartPt.y/tileHeight); return(tempPt); }
(ここでは、ほとんどの場合と同様に、タイルの高さと幅が同じであると想定しています。)
つまり、一対の画面(等尺性)座標がわかっているので、関数を呼び出すことでタイル座標を見つけることができます。
getTileCoordinates(isometricToCartesian(screen point), tile height);
画面上のこのポイントは、たとえば、マウスカーソルまたは選択したアイテムの位置です。
登録ポイント
Flashでは、任意のグラフィックポイントをベースポイントまたは
[0,0]
として選択できます。 Phaserのこの類似物は
Pivot
です。 たとえば、ポイント
[10,20]
にグラフを配置すると、この
Pivot
ポイントは
[10,20]
対応します。 デフォルトでは、
[0,0]
または
Pivot
は左上のポイントと見なされます。 このコードを使用して上記のレベルを作成しようとすると、目的の結果が得られません。 代わりに、以下に示すように、壁のない平らな地面になります。
これは、タイル画像のサイズが異なるため、壁タイルの高さ属性を考慮していないためです。 下の図は、私たちが使用するタイルのさまざまな画像と、デフォルトで配置されている白い円[0,0]を示しています。
基点(ピボット)を使用する場合、デフォルトではヒーローは間違った場所にいることに注意してください。 また、デフォルトの基点を使用して壁を描画すると、壁の高さが失われることに注意してください。 右の図は、壁タイルの高さを考慮し、ヒーローが草のタイルの真ん中にいるように、正しく配置する方法を示しています。 この問題はさまざまな方法で解決できます。
- すべてのタイルの画像サイズを同じにし、グラフィックを画像に正しく配置します。 同時に、多くの空の領域が各タイル画像に作成されます。
- タイルが正しく配置されるように、各タイルの基点を手動で設定します。
- 特定のオフセットでタイルを描きます。
このチュートリアルでは、ベースポイントを変更できないフレームワークでも機能するため、3番目の方法を選択しました。
3.等尺性座標の動き
等尺性座標の文字やオブジェクトを直接移動しないでください。 代わりに、デカルト座標でゲームワールドのデータを管理し、上記の関数を使用して画面上の位置を更新します。 たとえば、キャラクターをY軸に沿って正の方向に前方に移動する場合、2次元座標で
y
プロパティを単純に増やし、最終位置を等角座標に変換できます。
y = y + speed; placetile(cartesianToIsometric(new Phaser.Point(x, y)))
私たちが学んだすべての新しい概念を要約し、等尺性の世界で動くオブジェクトの実例を実装してみましょう。 gitソースコードリポジトリの
assets
フォルダーから必要なグラフィックリソースを使用できます 。
深さソート
フェンスで囲まれた庭でボールのイメージを移動しようとすると、 深さによるソートに問題があります。 等尺性の世界に移動する要素がある場合、通常の場所に加えて、 depthによる並べ替えの世話をする必要があります。 適切な並べ替えにより、画面に近いオブジェクトがより遠いオブジェクトの上に描画されます。 この記事で説明したように、最も単純なソート方法はデカルトY座標を使用することです。画面上のオブジェクトが高ければ高いほど、早く描画されるはずです。 これは単純な等尺性シーンではうまく機能しますが、配列内のタイルの座標に従って移動するプロセスで等尺性シーン全体を再描画する方が良いでしょう。 レベルレンダリングに擬似コードを使用して、このアプローチを詳しく説明します。
for (i, loop through rows) for (j, loop through columns) x = j * tile width y = i * tile height tileType = levelData[i][j] placetile(tileType, x, y)
オブジェクトまたはキャラクターがタイル
[1,1]
、つまり等角図の一番上の緑のタイルの上にあると想像してください。 レベルを正しく描画するには、図に示すように、コーナーの壁タイル、左右の壁タイル、地面を描画した後にキャラクターを描画する必要があります。
擬似コードに従ってレンダリングサイクルを実行すると、最初に中央のコーナーの壁が描画され、その後、右上隅のすべての壁が右コーナーに到達するまでサイクルが描画され続けます。 次のサイクルで、彼はキャラクターの左側に壁を描き、次にキャラクターが立つ草のタイルを描きます。 これがキャラクターが占めるタイルであると判断したため、芝生タイルの後にキャラクターを描画します。 したがって、キャラクターのタイルの隣にある無料の3つの草のタイルに壁がある場合、これらの壁はキャラクターと重なり、深さによる適切なソートを保証します。
4.グラフィックの作成
等尺性グラフィックスは、ピクセルアートである場合がありますが、そうである必要はありません。 等尺性ピクセルアートを使用する場合、必要なものがすべて含まれているRhysDマニュアルを学習すると役立ちます 。 理論はウィキペディアで学ぶことができます。
アイソメ図を作成するときは、次の規則に従う必要があります。
- 空の等尺性グリッドから始めて、ピクセル精度を維持します。
- グラフィックを単純な等尺性タイル画像に分割してみてください。
- 各タイルを通過可能または通過不能にします。 そうしないと、通過可能な領域と通過できない領域の両方を含むタイルで作業することが困難になります。
- ほとんどのタイルは、レベルを任意の方向にタイル表示できるようにシームレスである必要があります。
- レイヤーを含むソリューションを使用しない場合、最初にグラウンドレイヤーに影が描画され、次にキャラクター(または木や他のオブジェクト)がトップレイヤーに描画される場合、シャドウを作成することは困難です。 複数のレイヤーを使用しない場合は、影が前方に落ちるようにし、たとえば木の後ろに立っているヒーローを覆わないようにしてください。
- 等尺性タイルの標準サイズを超えるタイル画像を使用する必要がある場合は、タイルの標準サイズの倍数のサイズを選択してください。 このような場合は、レイヤーを使用して、グラフィックを高さに応じてさまざまな部分に切り分けることをお勧めします。 たとえば、木は根、幹、葉の3つの部分にカットできます。 そのため、深さに対応するレイヤーにパーツを描くことができるため、深さのソートが簡単になります。
単一のタイルサイズより大きい等尺性タイルは、深さでソートするときに問題を引き起こします。 このような問題は、次の記事で対処されています。
関連する投稿
5.等尺性文字
まず、ゲーム内でどの方向に移動できるかを決定する必要があります。 通常、ゲームでは4つまたは8つの方向に移動できます。 次の図を見て、2次元空間と等尺性空間の関係を理解してください。
トップビューのゲームでは、上キーを押すとキャラクターが垂直に上に移動しますが、等尺性ゲームでは右上の角に向かって45度の角度で移動します。
トップビューの場合、一方向に見えるキャラクターアニメーションのセットを1つ作成し、すべてのアニメーションを回転させるだけです。 アイソメトリックキャラクターのグラフィックの場合、許容可能な各方向のアニメーションを作成する必要があります。つまり、8方向の移動の場合、アクションごとに8つのアニメーションを作成する必要があります。
理解を容易にするために、方向は通常「北」、「北西」、「西」などと呼ばれます。 文字フレームでは、図は南東から時計回りに固定位置のフレームを示しています。
キャラクターをタイルと同じように配置します。 キャラクターの移動は、デカルト座標を計算し、それらを等尺性に変換することにより実行されます。 キーボードを使用してキャラクターを制御するとします。
2つの変数
dX
と
dY
を割り当てます。その値は、押されたコントロールキーに依存します。 デフォルトでは、これらの変数は
0
であり、以下の表に従って値が割り当てられます。ここで、
、
、
、および
は、それぞれ、 上下 、 左右の方向キーを意味します。 キーの下の値
1
はキーが押されていることを意味し、
0
押されていないことを意味します。
dX dY ================ 0 0 0 0 0 0 1 0 0 0 0 1 0 1 0 0 0 -1 0 0 1 0 1 0 0 0 0 1 -1 0 1 0 1 0 1 1 1 0 0 1 -1 1 0 1 1 0 1 -1 0 1 0 1 -1 -1
dX
と
dY
値を使用して、次のようにデカルト座標を更新できます。
newX = currentX + (dX * speed); newY = currentY + (dY * speed);
したがって、
dX
と
dY
は、押されたキーに応じて、XとYでのキャラクターの位置の変化を表します。 上記のように、新しい等角座標を簡単に計算できます。
Iso = cartesianToIsometric(new Phaser.Point(newX, newY))
新しい等尺性の位置を受け取ったら、キャラクターをこの位置に移動する必要があります。
dX
と
dY
値に基づいて、キャラクターがどの方向を向いているかを理解し、適切なアニメーションを使用できます。 キャラクターを移動した後、キャラクターのタイル座標が変更される可能性があるため、深度による適切な並べ替えでレベルを再描画することを忘れないでください。
衝突認識
衝突の認識は、オブジェクトの新しい計算された位置のタイルが通過できないかどうかをチェックすることによって実行されます。 したがって、新しい位置を見つけた後、すぐにキャラクターを移動することはできません。まず、この場所がどの種類のタイルを占有しているかを確認する必要があります。
tile coordinate = getTileCoordinates(isometricToCartesian(current position), tile height); if (isWalkable(tile coordinate)) { moveCharacter(); } else { // ; }
isWalkable()
関数では、指定された座標のレベルデータ配列の値がトラバースタイルかどうかを確認します。 また、キャラクターが見えないタイルに遭遇した場合に備えて、キャラクターが動いていない場合でも 、キャラクターが見ている方向を更新する必要があります。
これは正しい解決策のように思えますが、ボリュームのないオブジェクトに対してのみ機能します。 衝突を計算するために、1つのポイント(キャラクターの中心点)のみを考慮します。 実際、特定の2次元の中心点から4つの角度をすべて見つけ、これらすべての角度の衝突を計算する必要があります。 コーナーのいずれかが貫通できないタイルに落ちた場合、キャラクターを移動することはできません。
文字による深さソート
どんなに不自然に見えても、同じ画像サイズの等尺性の世界のキャラクターとツリータイルを考えてみましょう。
深さによるソートの理解を深めるために、文字のX座標とY座標がツリーの座標よりも小さい場合、ツリーが文字と重なることを理解する必要があります。 キャラクターのX座標とY座標がツリーの座標よりも大きい場合、キャラクターはツリーに重なります。 X座標が等しい場合、決定はY座標でのみ行われます。Y座標が大きいオブジェクトは別のオブジェクトと重なります。 Y座標が一致する場合、決定はXでのみ行われます。Xが大きいオブジェクトは他のオブジェクトと重なります。
上記のように、アルゴリズムの単純化バージョンは、離れたタイル(つまり、
tile[0][0]
)から隣接するレベルまで、行ごとにレベルを単純に描画することにあります。 キャラクターがタイルを占有する場合、最初に土地タイルを描画し、次にキャラクタータイルを描画します。 キャラクターが壁のタイルを占有できないため、これはうまく機能します。
6.デモの時間です!
Phaserのデモはこちらです。 クリックしてインタラクティブ領域に切り替えてから、矢印キーでキャラクターを制御します。 斜めに移動するには、2つのキーを押します。
7.収集されたアイテム
収集されたアイテムは、レベルで拾うことができるアイテムであり、通常それらを踏むだけです。 たとえば、コイン、クリスタル、弾薬などです。
以下に示すように、アイテムデータはレベルデータに直接保存できます。
[ [1,1,1,1,1,1], [1,0,0,0,0,1], [1,0,8,0,0,1], [1,0,0,8,0,1], [1,0,0,0,0,1], [1,1,1,1,1,1] ]
このレベルのデータでは、草のタイル上のオブジェクトを示すために
8
を使用します(前述のように
1
と
0
は草と壁を示します)。 これは、オブジェクトの画像が重ねられた草タイルのタイル画像です。 このロジックによれば、アイテムを配置できるタイルごとに2つの異なる状態のタイルが必要になります。1つはアイテムあり、もう1つはアイテムなしで、アイテムを受け取った後に表示されます。
通常、アイソメトリックグラフィックスには、多くの通行可能なタイルがあります。 上記のアプローチを使用する場合、持ち上げるN個のオブジェクトがある場合、既存の30個のタイルに対してN x 30が必要になりますが、これはあまり効果的ではありません。 したがって、このような組み合わせを動的に作成する必要があります。 この問題を解決するには、上記で使用したのと同じ方法でキャラクターを配置します。 アイテムのあるタイルに到達したら、まず草のタイルを描画し、次にその上にアイテムを配置します。 したがって、30個の通過可能なタイルに加えて、N個のアイテムタイルだけが必要ですが、レベルデータの各組み合わせを示す数値が必要です。 N x 30の値を入力しないようにするために、
pickupArray
とは別に、アイテムデータを保存するための個別の
pickupArray
を保存できます。 アイテムの完成したレベルを以下に示します。
この例では、よりシンプルにし、オブジェクトに別の配列を使用しません。
アイテムを拾う
オブジェクト認識は、衝突認識と同じ方法で実行されますが、キャラクターを移動した後です。
if(onPickupTile()){ pickupItem(); } function onPickupTile(){//, return (levelData[heroMapTile.y][heroMapTile.x]==8); }
関数
onPickupTile()
では
levelData
、座標の配列の値が
heroMapTile
オブジェクトを含むタイルかどうかを確認します。
levelData
このタイルの座標の配列内の番号は、アイテムのタイプを示します。キャラクターを移動する前に衝突をチェックしますが、アイテムは後にチェックされます:衝突の場合、キャラクターはすでに通過できないタイルで占められている場合、ポイントを取ることができません。オブジェクトの場合、キャラクターはタイルに自由に移動できます。
また、衝突データは通常変更されることはなく、アイテムをピックアップするとアイテムデータが変更されることにも注意してください。 (これは通常、配列の値を、
levelData
例えばから
8
に変更するだけ
0
です。)
これは問題につながります。レベルを再起動する必要がある場合、つまり、すべてのオブジェクトを元のポイントに復元する必要がある場合はどうなりますか?
levelData
アイテムが発生すると配列が変化するため、これに関する情報はありません。解決策は、ゲーム中にレベル配列のコピーを使用し、配列を変更しないこと
levelData
です。たとえば、
levelData
and を使用して
levelDataLive[]
、レベルの最初の最初の最後のクローンを作成し、ゲーム中にのみ変更し
levelDataLive[]
ます。
たとえば、各アイテムを収集した後、無料の芝生タイルにランダムなアイテムを作成し、値を増やします
pickupCount
。機能は
pickupItem
次のとおりです。
function pickupItem(){ pickupCount++; levelData[heroMapTile.y][heroMapTile.x]=0; // spawnNewPickup(); }
キャラクターがタイル上にあるときはいつでもアイテムをチェックすることに気づいたでしょう。これは1秒間に数回発生する可能性があります(ユーザーが移動したときのみチェックしますが、1つのタイルで何度も繰り返すことができます)が、上記のロジックは正しく実行されます。我々は、アレイデータ割り当てたら
levelData
値を
0
オブジェクトを持ち上げるの最初の検出時に、後続のすべてのチェックが
onPickupTile()
タイルに戻ります
false
。このインタラクティブな例をご覧ください。
8.トリガータイル
名前が示すとおり、トリガータイルは、プレーヤーが踏むかキーを押すとアクションをトリガーします。プレイヤーを別の場所にテレポートしたり、ゲートを開いたり、敵を作成したりできます。ある意味では、収集されるアイテムは単に特殊なタイプのトリガーです。プレイヤーがコインでタイルを踏むと、コインが消えてカウンターが上がります。
プレイヤーを別のレベルに導くドアを実装する方法を見てみましょう。ドアの横のタイルがトリガーになります。プレーヤーがxキーを押すと、別のレベルに移動します。
レベルを変更するには、現在の配列
levelData
を新しいレベルの配列に置き換えてから、新しい位置と方向を割り当てるだけです。
heroMapTile
キャラクター。通過できるドアのある2つのレベルがあるとします。ドアの隣の地面のタイルは両方のレベルでアクティブなタイルになるため、レベルに表示される新しいキャラクターの位置として使用できます。
ここでの実装のロジックは、収集されたアイテムのロジックと同じです。トリガータイルの値を保存するには、再度arrayを使用し
levelData
ます。この例で
2
は、ドアのあるタイルを意味し、その隣の値がトリガーになります。私が使用している
101
と
102
、100を超える値の大きい持つ任意のタイルがタイルアクティブになると判断し、マイナス100の値は、それがリードしたレベルです。
var level1Data= [[1,1,1,1,1,1], [1,1,0,0,0,1], [1,0,0,0,0,1], [2,102,0,0,0,1], [1,0,0,0,1,1], [1,1,1,1,1,1]]; var level2Data= [[1,1,1,1,1,1], [1,0,0,0,0,1], [1,0,8,0,0,1], [1,0,0,0,101,2], [1,0,1,0,0,1], [1,1,1,1,1,1]];
トリガーイベントを確認するためのコードを以下に示します。
var xKey=game.input.keyboard.addKey(Phaser.Keyboard.X); xKey.onUp.add(triggerListener);// Signal listener up function triggerListener(){ var trigger=levelData[heroMapTile.y][heroMapTile.x]; if(trigger>100){// trigger-=100; if(trigger==1){// 1 levelData=level1Data; }else {// 2 levelData=level2Data; } for (var i = 0; i < levelData.length; i++) { for (var j = 0; j < levelData[0].length; j++) { trigger=levelData[i][j]; if(trigger>100){// heroMapTile.y=j; heroMapTile.x=i; heroMapPos=new Phaser.Point(heroMapTile.y * tileWidth, heroMapTile.x * tileWidth); heroMapPos.x+=(tileWidth/2); heroMapPos.y+=(tileWidth/2); } } } } }
この関数
triggerListener()
は、指定された座標でトリガーデータ配列の値が100より大きいかどうかを確認します。もしそうなら、タイル値から100を引くことで、どのレベルに行く必要があるかを決定します。この関数は、新しいトリガーでタイルトリガーを見つけ
levelData
ます。これは、キャラクター作成の座標になります。xキーが放されたときにトリガーを起動しました。押されたキーを単純に読み取ると、キーが押されている間にレベル間で移動するサイクルで自分自身を見つけることができます。これは、キャラクターが常にアクティブなタイルの新しいレベルで作成されるためです。これが実際のデモです。踏みながらアイテムを収集し、ドアの前に立ってxを押してレベルを変更します。
9.シェル
弾丸、魔法、ボールなど、特定の速度で特定の方向に移動するシェルを呼び出します。キャラクターに関連するものはすべて、シェルに適用されます。ただし、高さは例外です。シェルは通常、地面では転がりませんが、特定の高さで飛行します。弾丸はキャラクターのウエストレベルで飛ぶため、ボールはジャンプすることさえできます。
興味深いことに、等尺性の高さは2次元側面図の高さに対応していますが、サイズは小さくなっています。複雑な変換はありません。デカルト座標のボールが地面から10ピクセル上にある場合、アイソメトリック座標では、10ピクセルまたは6ピクセルで地面より上になります。 (この場合、対応する軸はY軸になります。)
フェンスで囲まれた庭の周りを疾走するボールを実現してみましょう。リアルさを追加するには、ボールに影を追加します。必要なのは、バウンスの高さの値をボールの等尺性値Yに追加することです。リバウンドの高さの値は、重力に応じてフレームごとに変化し、ボールが地面に触れるとすぐに、Y軸に沿って現在の速度の符号を変更し
ます。変数によってボールの跳ね返りの強さを示します
zValue
。まず、ボールのバウンス力が100、つまりであるとし
zValue = 100
ます。
私たちは2つの変数を使用します。
incrementValue
最初は重要な変数
0
、
gravity
もう1つは重要な変数です
-1
。各フレームで減算します
incrementValue
アウト
zValue
及び減算
gravity
から
incrementValue
減衰効果を作成します。に
zValue
達する
0
と、ボールが地面に到達したことを意味します。この時点で、signを変更し
incrementValue
、それに乗算
-1
し、正の数に変換します。これは、次のフレームからボールが上に移動し始める、つまりバウンドすることを意味します。
コードでは次のようになります。
if(game.input.keyboard.isDown(Phaser.Keyboard.X)){ zValue=100; } incrementValue-=gravity; zValue-=incrementValue; if(zValue<=0){ zValue=0; incrementValue*=-1; }
アイソメビューの場合、コードも同じままで、わずかな違いがあります
zValue
。より小さな初期値を使用します。以下は
zValue
、
y
レンダリング中にボールのアイソメトリック座標の値にどのように追加されるかを示しています。
function drawBallIso(){ var isoPt= new Phaser.Point();// var ballCornerPt=new Phaser.Point(ballMapPos.x-ball2DVolume.x/2,ballMapPos.y-ball2DVolume.y/2); isoPt=cartesianToIsometric(ballCornerPt);// gameScene.renderXY(ballShadowSprite,isoPt.x+borderOffset.x+shadowOffset.x, isoPt.y+borderOffset.y+shadowOffset.y, false);// gameScene.renderXY(ballSprite,isoPt.x+borderOffset.x+ballOffset.x, isoPt.y+borderOffset.y-ballOffset.y-zValue, false);// }
インタラクティブな例を参照してください。
影が演じる役割は非常に重要であり、この幻想にリアリズムを加えます。また、2つのスクリーン座標(xおよびy)を使用して、アイソメトリック座標で3次元を表すことに注意してください。スクリーン座標のY軸はアイソメトリック座標のZ軸でもあります。これはわかりにくいかもしれません。
10.パスを見つけてその上を移動する
パスを見つけてそれに沿って移動するのは、かなり複雑なプロセスです。2つのポイント間のパスを見つけるには、さまざまなアルゴリズムを使用したさまざまなソリューションがあります
levelData
が、2次元配列であるため、すべてが可能な限り単純です。プレーヤーが占有できる一意のノードを明確に定義しており、それらを通過できるかどうかを簡単に確認できます。
関連する投稿
この記事では、パス検索アルゴリズムの詳細な概要は大きすぎますが、最も一般的な方法、最短パスアルゴリズム、その最も有名な実装はA *およびダイクストラのアルゴリズムです。
私たちの目標は、開始ノードと終了ノードを接続するノードを見つけることです。開始ノードから、8つのすべての隣接ノードを訪問し、訪問済みとしてマークします。このプロセスは、訪問された新しいサイトごとに再帰的に繰り返されます。
各スレッドは、訪問したノードを追跡します。隣接ノードに行くとき、すでに訪れたノードはスキップされます(再帰は停止します)。プロセスは最後のノードに到達するまで続きます。最後のノードでは再帰が完了し、移動したパス全体がノードの配列として返されます。エンドノードに到達できない場合があります。つまり、パスの検索が失敗します。通常、ノード間にいくつかのパスがあります。この場合、最小数のノードを持つそれらの1つを選択します。
道を探す
明確に定義されたアルゴリズムに関しては、車輪を再発明するのは愚かなことなので、既存のソリューションを使用して方法を見つけます。PhaserではJavaScriptソリューションが必要なので、EasyStarJSを選択しました。パス検索エンジンは次のように初期化されます。
easystar = new EasyStar.js(); easystar.setGrid(levelData); easystar.setAcceptableTiles([0]); easystar.enableDiagonals();// , easystar.disableCornerCutting();//
配列は以降
levelData
のみを含む
0
と
1
私たちはノードの配列に直接渡すことができます。
0
通過可能なノードを示した値。また、斜めに移動する機能をオンにしましたが、移動が通過できないタイルの角に近づくとオフにしました。
これは、斜めの動きでキャラクターが通過できないタイルに衝突する可能性があるためです。この場合、衝突認識システムはキャラクターの通過を許可しません。さらに、この例ではキャラクターが人工知能とともに移動するため、衝突認識が完全に削除されていることに注意してください。これは必須ではありません。
レベル内の空きタイルのクリックを認識し、関数を使用してパスを計算します
findPath
。コールバックメソッド
plotAndMove
は、作成されたパスのノードの配列を取得します。見つかったパスをにマークし
ます。
game.input.activePointer.leftButton.onUp.add(findPath) function findPath(){ if(isFindingPath || isWalking)return; var pos=game.input.activePointer.position; var isoPt= new Phaser.Point(pos.x-borderOffset.x,pos.y-borderOffset.y); tapPos=isometricToCartesian(isoPt); tapPos.x-=tileWidth/2;// - tapPos.y+=tileWidth/2; tapPos=getTileCoordinates(tapPos,tileWidth); if(tapPos.x>-1&&tapPos.y>-1&&tapPos.x<7&&tapPos.y<7){// if(levelData[tapPos.y][tapPos.x]!=1){// isFindingPath=true; // easystar.findPath(heroMapTile.x, heroMapTile.y, tapPos.x, tapPos.y, plotAndMove); easystar.calculate(); } } } function plotAndMove(newPath){ destination=heroMapTile; path=newPath; isFindingPath=false; repaintMinimap(); if (path === null) { console.log("No Path was found."); }else{ path.push(tapPos); path.reverse(); path.pop(); for (var i = 0; i < path.length; i++) { var tmpSpr=minimap.getByName("tile"+path[i].y+"_"+path[i].x); tmpSpr.tint=0x0000ff; //console.log("p "+path[i].x+":"+path[i].y); } } }
道に沿った動き
ノードの配列の形式でパスを受け取ったら、キャラクターを強制的にそれに沿って移動させる必要があります。
クリックしたタイルにキャラクターを移動させたいとします。まず、キャラクターが占めているノードとクリックしたノードの間のパスを探します。パスが見つかったら、文字をノード配列の最初のノードに移動し、それを宛先としてマークする必要があります。宛先ノードに到達したら、ノードの配列にさらにノードがあるかどうかを確認し、ある場合は、最終ノードに到達するまで次のノードを宛先としてマークします。
また、ノードに到達するたびに、現在のノードと新しい宛先ノードに基づいてプレーヤーの方向を変更します。ノード間では、目的のノードに到達するまで正しい方向に歩きます。これは非常に単純なAIであり、この例では、
aiWalk
以下に部分的に示すメソッドで実装されています。
function aiWalk(){ if(path.length==0){// if(heroMapTile.x==destination.x&&heroMapTile.y==destination.y){ dX=0; dY=0; isWalking=false; return; } } isWalking=true; if(heroMapTile.x==destination.x&&heroMapTile.y==destination.y){// , , // , stepsTaken++; if(stepsTaken<stepsTillTurn){ return; } console.log("at "+heroMapTile.x+" ; "+heroMapTile.y); // heroMapSprite.x=(heroMapTile.x * tileWidth)+(tileWidth/2)-(heroMapSprite.width/2); heroMapSprite.y=(heroMapTile.y * tileWidth)+(tileWidth/2)-(heroMapSprite.height/2); heroMapPos.x=heroMapSprite.x+heroMapSprite.width/2; heroMapPos.y=heroMapSprite.y+heroMapSprite.height/2; stepsTaken=0; destination=path.pop();// if(heroMapTile.x<destination.x){ dX = 1; }else if(heroMapTile.x>destination.x){ dX = -1; }else { dX=0; } if(heroMapTile.y<destination.y){ dY = 1; }else if(heroMapTile.y>destination.y){ dY = -1; }else { dY=0; } if(heroMapTile.x==destination.x){ dX=0; }else if(heroMapTile.y==destination.y){ dY=0; } //...... } }
正しいマウスクリックポイントを除外する必要があります。これを行うには、壁タイルや他の貫通できないタイルではなく、歩行可能な領域をクリックしたと判断します。
AIのコーディングのもう1つの興味深い点:現在のノードに到達するとすぐに、ノードの配列内の次のタイルにキャラクターが直面するのは望ましくありません。代わりに、キャラクターがタイルに数歩行くのを待ってから、次の目的地を探し始める必要があります。また、ターンの直前に現在のタイルの中央にキャラクターを手動で配置して、すべてが最高に見えるようにすることをお勧めします。
動作デモを見ることができます。
11.等尺スクロール
レベル領域が画面領域よりもはるかに大きい場合、スクロールする必要があります。
表示可能な画面領域は、レベル領域全体の大きな長方形の中の小さな長方形と見なすことができます。スクロールとは、単に内側の長方形を内側に移動することです。通常、スクロール処理中、画面の長方形内のキャラクターの位置は一定のままです。ほとんどの場合、画面の中央にあります。興味深いことに、スクロールを実装するには、内側の四角形のコーナーポイントを追跡するだけです。
デカルト座標で指定するこのコーナーポイントは、レベルデータのタイルの1つに該当します。スクロールするには、デカルト座標のX軸とY軸に沿ってコーナーポイントの位置を増やします。これで、このポイントを等角座標に変換し、それらを使用して画面を描画できます。
等角空間での新しい変換値は、画面の角度、つまりnewである必要があります
(0, 0)
。したがって、レベルデータを解析およびレンダリングするとき、この値を各タイルの等角位置から減算します。新しいタイルの位置が画面内にあるかどうかを判断できます。
または、サイズX x Yの等尺性タイルのグリッドを画面上に描画して、レンダリングサイクルが大きなレベルで有効になるようにすることができます。
これらのすべての手順は、次のように表現できます。
- XおよびYコーナーポイントのデカルト座標を更新します。
- アイソメ空間に変換します。
- 各タイルの等角描画位置からこの値を減算します。
- この新しい角度から開始して、指定された数のタイルのみを画面に描画します。
- オプション:図面の新しい等角位置が画面内にある場合にのみ、タイルを描画します。
var cornerMapPos=new Phaser.Point(0,0); var cornerMapTile=new Phaser.Point(0,0); var visibleTiles=new Phaser.Point(6,6); //... function update(){ //... if (isWalkable()) { heroMapPos.x += heroSpeed * dX; heroMapPos.y += heroSpeed * dY; // cornerMapPos.x -= heroSpeed * dX; cornerMapPos.y -= heroSpeed * dY; cornerMapTile=getTileCoordinates(cornerMapPos,tileWidth); // heroMapTile=getTileCoordinates(heroMapPos,tileWidth); // renderScene(); } } function renderScene(){ gameScene.clear();// , var tileType=0; // var startTileX=Math.max(0,0-cornerMapTile.x); var startTileY=Math.max(0,0-cornerMapTile.y); var endTileX=Math.min(levelData[0].length,startTileX+visibleTiles.x); var endTileY=Math.min(levelData.length,startTileY+visibleTiles.y); startTileX=Math.max(0,endTileX-visibleTiles.x); startTileY=Math.max(0,endTileY-visibleTiles.y); // for (var i = startTileY; i < endTileY; i++) { for (var j = startTileX; j < endTileX; j++) { tileType=levelData[i][j]; drawTileIso(tileType,i,j); if(i==heroMapTile.y&&j==heroMapTile.x){ drawHeroIso(); } } } } function drawHeroIso(){ var isoPt= new Phaser.Point();// var heroCornerPt=new Phaser.Point(heroMapPos.x-hero2DVolume.x/2+cornerMapPos.x,heroMapPos.y-hero2DVolume.y/2+cornerMapPos.y); isoPt=cartesianToIsometric(heroCornerPt);// 2D- gameScene.renderXY(sorcererShadow,isoPt.x+borderOffset.x+shadowOffset.x, isoPt.y+borderOffset.y+shadowOffset.y, false);// gameScene.renderXY(sorcerer,isoPt.x+borderOffset.x+heroWidth, isoPt.y+borderOffset.y-heroHeight, false);// } function drawTileIso(tileType,i,j){// var isoPt= new Phaser.Point();// var cartPt=new Phaser.Point();// . cartPt.x=j*tileWidth+cornerMapPos.x; cartPt.y=i*tileWidth+cornerMapPos.y; isoPt=cartesianToIsometric(cartPt); // . if(tileType==1){ gameScene.renderXY(wallSprite, isoPt.x+borderOffset.x, isoPt.y+borderOffset.y-wallHeight, false); }else{ gameScene.renderXY(floorSprite, isoPt.x+borderOffset.x, isoPt.y+borderOffset.y, false); } }
コーナーポイントの増分は、キャラクターが移動するときにキャラクターの位置を更新する方向とは反対の方向に発生することに注意してください。これにより、キャラクターは画面に対して同じ場所に残ります。この例を見てください(矢印キーを使用してスクロールし、マウスをクリックして表示されているグリッドを拡大します)。
いくつかのメモ:
- , .
- , , . , X Y, X , Y . , .
- - , .
- , . . . ?
おわりに
このチュートリアルは、主に等尺性ゲームの世界を学ぶ初心者を対象としています。ここで紹介する概念の多くには、他のやや複雑なソリューションがあり、私は意図的に最も単純なものを選択しました。
このマニュアルでは、すべての問題を解決できるわけではありませんが、受け取った情報により、これらの概念を開発してより複雑なソリューションを作成できます。たとえば、実装された深さによる単純な並べ替えは、複数のフロアとプラットフォームタイルがあるフロアから別のフロアに移動するレベルの場合には役に立ちません。しかし、これは別のチュートリアルのタスクです。