web-dev-qa-db-ja.com

javascriptを使用してタイルマップをレンダリングする

次のようなタイルマップを作成する際のサンプル実装のアイデアを使用して、論理的な理解を求めています。

http://thorsummoner.github.io/old-html-tabletop-test/pallete/tilesets/fullmap/scbw_tiles.png

そして、次のような論理的な方法でレンダリングします。

http://thorsummoner.github.io/old-html-tabletop-test/

すべてのタイルがそこにあるのはわかりますが、形を形成するようにタイルがどのように配置されているのかわかりません。

これまでのタイルのレンダリングについての私の理解は単純で、非常に手動です。数字(1、2、3など)があるマップ配列をループして、指定されたタイルをレンダリングします。

var mapArray = [
    [0, 0, 0, 0 ,0],
    [0, 1, 0, 0 ,0],
    [0, 0, 0, 0 ,0],
    [0, 0, 0, 0 ,0],
    [0, 0, 1, 1 ,0]
];

function drawMap() {
    background = new createjs.Container();      
    for (var y = 0; y < mapArray.length; y++) {
        for (var x = 0; x < mapArray[y].length; x++) {
            if (parseInt(mapArray[y][x]) == 0) {
                var tile = new createjs.Bitmap('images/tile.png');
            }
            if (parseInt(mapArray[y][x]) == 1) {
                var tile = new createjs.Bitmap('images/tile2.png'); 
            }
            tile.x = x * 28;
            tile.y = y * 28;
            background.addChild(tile);
        }
    }
    stage.addChild(background);     
}   

私を取得します:

enter image description here

しかし、これは、論理的な形状(岩の形成、草のパッチなど)が作成されるように、各タイルが配列のどこに配置されるかを手動で把握する必要があることを意味します

明らかに、上記のgithubコードを作成した人は別の方法を使用していました。 logic(単に擬似コードを使用)を理解するためのガイダンスは非常に役立ちます

12
Growler

そこにはロジックがありません。

ページのソースを調べると、本文の最後のスクリプトタグに膨大な数のタイル座標が含まれていることがわかります。

その例には、形を形成する方法を理解するための「インテリジェント」システムを示す魔法はありません。

さて、そうは言っても、そのようなことがあります... ...しかし、それらはリモートで単純ではありません。

isがよりシンプルで管理しやすいのは、マップエディターです。


タイルエディタ

箱から出して:

これを行う方法はたくさんあります...タイルをペイントし、XML、JSON、CSV、または特定のプログラムがサポート/エクスポートするものを吐き出すことができる無料または安価なプログラムがあります。

Tiledhttp://mapeditor.org )はそのような例の1つです。
他にもありますが、Tiledは私が最初に思いついたもので、無料で、実際にはかなりまともです。

長所:
当面の利点は、画像タイルを読み込んでマップにペイントできるアプリを入手できることです。
これらのアプリは、衝突レイヤーとエンティティレイヤーの追加をサポートする場合もあります([2,1]に敵を置き、[3,5]にパワーアップし、「ハートプレーヤー」トリガーを溶岩)。

短所:...欠点は、これらのファイルがどのようにフォーマットされているかを正確に知って、ゲームエンジンに読み込むことができるようにする必要があることです。
現在、これらのシステムの出力は比較的標準化されています...そのため、そのマップデータをさまざまなゲームエンジンにプラグインできます(それ以外の場合は何が重要ですか?)。ゲームエンジンはすべてを使用するわけではありません。まったく同じタイルファイルで、ほとんどの優れたタイルエディターでは、いくつかの形式にエクスポートできます(独自の形式を定義できるものもあります)。

...そうは言っても、代替案(または実際には同じソリューション、手作り)は、独自のタイルエディターを作成することです。

[〜#〜] DIY [〜#〜]
タイルをペイントするエンジンを作成するのと同じくらい簡単に、Canvasで作成できます。
主な違いは、タイルのマップがあることです(StarCrのtilemap .pngのように...えーと...例の「ファウンドアート」があります)。
配列をループしてタイルの座標を見つけ、そのインデックスに一致する世界座標でペイントする代わりに、マップからタイルを選択します(MS Paintで色を選択するなど)。 )、次にクリック(またはドラッグ)する場所で、関連する配列ポイントを特定し、そのインデックスをそのタイルと等しくなるように設定します。

長所:
空が限界です。好きなものを作ったり、使いたいファイル形式に合わせたり、投げたいクレイジーなものを処理したりできます...
短所:
...もちろん、これは、自分で作成し、使用するファイル形式を定義し、それらの奇妙なアイデアをすべて処理するロジックを作成する必要があることを意味します...

基本的な実装
私は通常、これをきちんと、JSパラダイムに適したものにしようとしますが、ここでは多くのコードが生成されます。
それで、おそらくどこでそれを別々のモジュールに分割すべきかを示しようとします。

// assuming images are already loaded properly
// and have fired onload events, which you've listened for
// so that there are no surprises, when your engine tries to
// Paint something that isn't there, yet


// this should all be wrapped in a module that deals with
// loading tile-maps, selecting the tile to "Paint" with,
// and generating the data-format for the tile, for you to put into the array
// (or accepting plug-in data-formatters, to do so)
var selected_tile = null,
    selected_tile_map = get_tile_map(), // this would be an image with your tiles
    tile_width  = 64, // in image-pixels, not canvas/screen-pixels
    tile_height = 64, // in image-pixels, not canvas/screen-pixels

    num_tiles_x = selected_tile_map.width  / tile_width,
    num_tiles_y = selected_tile_map.height / tile_height,

    select_tile_num_from_map = function (map_px_X, map_px_Y) {
        // there are *lots* of ways to do this, but keeping it simple
        var tile_y = Math.floor(map_px_Y / tile_height), // 4 = floor(280/64)
            tile_x = Math.floor(map_px_X / tile_width ),

            tile_num = tile_y * num_tiles_x + tile_x;
            // 23 = 4 down * 5 per row + 3 over

        return tile_num;
    };

    // won't go into event-handling and coordinate-normalization
    selected_tile_map.onclick = function (evt) {
        // these are the coordinates of the click,
        //as they relate to the actual image at full scale
        map_x, map_y;
        selected_tile = select_tile_num_from_map(map_x, map_y);
    };

これで、どのタイルがクリックされたかを把握するための簡単なシステムができました。
繰り返しになりますが、これを構築する方法はたくさんあり、よりOOにすることができます。
そして、エンジン全体で読み取って使用することを期待する適切な「タイル」データ構造を作成します。

今、私はタイルのゼロベースの数値を返し、左から右、上から下に読んでいます。
行ごとに5つのタイルがあり、誰かが2番目の行の最初のタイルを選択した場合、それはタイル#5です。

次に、「ペイント」の場合は、キャンバスのクリックを聞いて、XとYが何であるかを把握し、世界のどこにあるか、そしてどの配列スポットに等しいかを把握する必要があります。
そこから、selected_tileの値をダンプするだけで、それだけです。

// this might be one long array, like I did with the tile-map and the number of the tile
// or it might be an array of arrays: each inner-array would be a "row",
// and the outer array would keep track of how many rows down you are,
// from the top of the world
var world_map = [],

    selected_coordinate = 0,

    world_tile_width  = 64, // these might be in *canvas* pixels, or "world" pixels
    world_tile_height = 64, // this is so you can scale the size of tiles,
                            // or zoom in and out of the map, etc

    world_width  = 320,
    world_height = 320,


    num_world_tiles_x = world_width  / world_tile_width,
    num_world_tiles_y = world_height / world_tile_height,

    get_map_coordinates_from_click = function (world_x, world_y) {
        var coord_x = Math.floor(world_px_x / num_world_tiles_x),
            coord_y = Math.floor(world_px_y / num_world_tiles_y),

            array_coord = coord_y * num_world_tiles_x + coord_x;

        return array_coord;
    },

    set_map_tile = function (index, tile) {
        world_map[index] = tile;
    };

    canvas.onclick = function (evt) {
        // convert screen x/y to canvas, and canvas to world
        world_px_x, world_px_y;
        selected_coordinate = get_map_coordinates_from_click(world_px_x, world_px_y);

        set_map_tile(selected_coordinate, selected_tile);
    };

ご覧のとおり、一方を実行する手順は、もう一方を実行する手順とほとんど同じです(1つの座標セットにxとyが指定されているため、別のスケール/セットに変換します)。

したがって、タイルを描画する手順は、ほぼ正反対です。
world-indexとtile-numberを指定して、逆に作業してworld-x/yとtilemap-x/yを見つけます。
サンプルコードでもその部分を確認できます。

このタイルペイントは、StarCraft、Zelda、Mario Brosのいずれについて話している場合でも、2Dマップを作成する従来の方法です。
[。 WarCraft III(3D)、そして彼らのエディターに入ると、タイル-Painterはまさにあなたが得るものであり、Blizzardがそれらのマップを作成した方法とまったく同じです。

追加

基本的な前提が整ったので、他の「マップ」も必要になります。これらのタイルのどれを歩くことができるか、または歩くことができなかったかを知るために、衝突マップ、エンティティマップが必要になります。ドア、パワーアップ、ミネラル、敵のスポーン、カットシーンのイベントトリガーがどこにあるかを示します...

これらのすべてが世界地図と同じ座標空間で動作する必要はありませんが、役立つ場合があります。

また、よりインテリジェントな「世界」が必要になる場合もあります。
たとえば、1つのレベルで複数のタイルマップを使用する機能...
そして、タイルマップを交換するためのタイルエディタのドロップダウン。

...両方のタイル情報(X/Yだけでなく、タイルに関するその他の情報)を保存する方法と、タイルで満たされた完成した「マップ」配列を保存する方法。

JSONをコピーして、独自のファイルに貼り付けるだけでも...


手続き型生成

これを行うもう1つの方法は、前に提案した方法(「岩、草などを接続する方法を知る」)を手続き型生成と呼びます。
これは非常に難しく、より複雑です。
Diabloのようなゲームはこれを使用するため、プレイするたびにランダムに生成された異なる環境にいます。 Warframeは、手続き型生成を使用して同じことを行うFPSです。

前提:
基本的にはタイルから始めます。タイルは単なる画像ではなく、画像と位置を持つオブジェクトである必要がありますが、周りにある可能性のあるもののリストもあります。それ。
草のパッチを置くと、その草はその横にさらに草を生成する可能性があります。
草は、周囲の4つの方向のいずれかで、水が10%、岩が20%、土が30%、草が40%多いと言うかもしれません。

もちろん、それは実際にはそれほど単純ではありません(または、間違っている場合はそうなる可能性があります)。

それはアイデアですが、手続き型生成のトリッキーな部分は、実際にはすべてが壊れることなく機能することを確認することです。
制約
たとえば、崖の壁を高台の内側に表示することはできませんでした。上と右に高地があり、下と左に低地がある場合にのみ表示されます(そして、StarCraftエディターはペイントしたときにこれを自動的に行いました)。傾斜路は、意味のあるタイルのみを接続できます。ドアを壁で覆ったり、川や湖で世界を包んだりして、移動できないようにすることはできません(さらに悪いことに、レベルを終えることができなくなります)。

pros
パスファインディングと制約のすべてを機能させることができれば、長寿に本当に最適です-地形とレイアウトを疑似ランダムに生成するだけでなく、敵の配置、戦利品の配置なども。
14年近く経った今でも、人々はディアブロIIをプレイしています。

短所
あなたが一人のチームである場合(暇なときに数学者/データサイエンティストにならない)、正しく理解するのは本当に難しいです。
マップが楽しく/バランスが取れて/競争力があることを保証するには本当に悪いです...
StarCraftは、公正なゲームプレイのために100%ランダム生成を使用することはできませんでした。
手続き型生成は「シード」として使用できます。
「ランダム化」ボタンを押して、何が得られるかを確認し、そこから微調整して修正することができますが、「バランス」の修正が非常に多いか、ゲームルールが非常に多く書かれているため、伝播、つまり、マップを自分でペイントするよりも、ジェネレーターの修正に多くの時間を費やすことになります。

そこにはいくつかのチュートリアルがあり、遺伝的アルゴリズム、経路探索などを学ぶことはすべて持っている素晴らしいスキルです... ... buuuut、2Dトップダウンタイルゲームを作ることを学ぶためには、やり過ぎです、というよりは、ゲーム/エンジンを1つか2つ手に入れた後で調べる必要があります。

31
Norguard