使用 javascript 渲染瓦片地图

我正在寻找一个逻辑上的理解与示例实现想法来获取这样的瓦片地图:

)就是这样一个例子.
还有其他的,但 Tiled 是我能想到的第一个,是免费的,而且实际上相当不错.

优点:
直接的好处是您将获得一个应用程序,该应用程序可让您加载图像图块并将它们绘制到地图中.
这些应用程序甚至可能支持添加碰撞层和实体层(在 [2,1] 处放置一个敌人,在 [3,5] 处放置一个加电装置,并在熔岩上方放置一个伤害玩家"触发器).

缺点:...缺点是您需要准确了解这些文件的格式,以便将它们读入游戏引擎.
现在,这些系统的输出是相对标准化的……因此您可以将地图数据插入不同的游戏引擎(否则有什么意义?),虽然游戏引擎并不都使用完全一致的图块文件同样,大多数优秀的平铺编辑器允许导出为多种格式(有些可以让您定义自己的格式).

...也就是说,替代方案(或者实际上是相同的解决方案,只是手工制作)是创建自己的图块编辑器.

DIY
您可以在 Canvas 中创建它,就像创建绘制图块的引擎一样容易.
主要区别在于您拥有瓷砖地图(例如 StarCr 中的 tilemap .png... erm... 示例中的found-art").
不是循环遍历数组,找到图块的坐标并在与该索引匹配的世界坐标处绘制它们,您要做的是从地图中选择一个图块(就像在 MS Paint 中选择一种颜色),然后在任何地方您单击(或拖动),找出与之相关的数组点,并将该索引设置为等于该图块.

优点:
天空才是极限;你可以制作任何你想要的东西,让它适合你想要使用的任何文件格式,让它处理你想要扔给它的任何疯狂的东西......
缺点:
...当然,这意味着您必须自己制作并定义要使用的文件格式,并编写处理所有这些滑稽想法的逻辑...

基本实现
虽然我通常会尝试使这个整洁,并且对 JS 范式友好,但这会导致大量代码,在这里.
所以我会尝试指出它应该在哪里分解成单独的模块.

//假设图片已经正确加载//并触发了 onload 事件,你已经监听了//这样当您的引擎尝试//绘制尚不存在的东西//这应该都被包装在一个处理的模块中//加载瓦片地图,选择要绘制"的瓦片,//并为图块生成数据格式,供您放入数组//(或接受插件数据格式化程序,这样做)var selected_tile = null,selected_tile_map = get_tile_map(),//这将是一张带有你的瓷砖的图像tile_width = 64,//以图像像素为单位,而不是画布/屏幕像素tile_height = 64,//以图像像素为单位,而不是画布/屏幕像素num_tiles_x = selected_tile_map.width/tile_width,num_tiles_y = selected_tile_map.height/tile_height,select_tile_num_from_map = 函数(map_px_X,map_px_Y){//有很多方法可以做到这一点,但要保持简单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返回 tile_num;};//不会进入事件处理和坐标归一化selected_tile_map.onclick = 函数 (evt) {//这些是点击的坐标,//因为它们与全尺寸的实际图像相关地图_x,地图_y;selected_tile = select_tile_num_from_map(map_x, map_y);};

现在您有了一个简单的系统来确定点击了哪个图块.
同样,有很多方法可以构建它,你可以让它更 OO,
并制作一个适当的平铺"数据结构,您希望在整个引擎中阅读和使用它.

现在,我只是返回从零开始的磁贴编号,从左到右、从上到下读取.
如果每行有 5 个图块,并且有人选择了第二行的第一个图块,那就是第 5 个图块.

然后,对于绘画",你只需要听一下画布的点击,弄清楚 X 和 Y 是什么,找出世界上的哪个位置,以及等于哪个阵列点.
从那里,您只需输入 selected_tile 的值,仅此而已.

//这可能是一个长数组,就像我对 tile-map 和 tile 的编号所做的那样//或者它可能是一个数组数组:每个内部数组都是一个行",//并且外部数组将跟踪您向下的行数,//从世界之巅var world_map = [],selected_coordinate = 0,world_tile_width = 64,//这些可能是 *canvas* 像素,或世界"像素world_tile_height = 64,//这样你就可以缩放图块的大小,//或放大缩小地图等世界宽度 = 320,世界高度 = 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;返回数组坐标;},set_map_tile = 函数(索引,图块){world_map[index] = 瓦片;};canvas.onclick = 函数 (evt) {//将屏幕 x/y 转换为画布,将画布转换为世界世界_px_x,世界_px_y;selected_coordinate = get_map_coordinates_from_click(world_px_x, world_px_y);set_map_tile(selected_coordinate, selected_tile);};

如您所见,做一个的过程与做另一个的过程几乎相同(因为它是 -- 在一个坐标集中给定 x 和 y,将其转换为另一个比例/集).

因此,绘制瓷砖的过程几乎完全相反.
给定 world-index 和 tile-number,反向工作以找到 world-x/y 和 tilemap-x/y.
您也可以在示例代码中看到该部分.

这种瓷砖绘画是制作 2D 地图的传统方式,无论我们谈论的是《星际争霸》、《塞尔达》还是《马里奥兄弟》.
并非所有人都拥有用瓷砖绘制"编辑器的奢侈(有些是在文本文件甚至电子表格中手动获得正确的间距),但如果你加载星际争霸甚至魔兽争霸III(这是3D),然后进入他们的编辑器,你得到的正是瓷砖画家,这正是暴雪制作这些地图的方式.

补充

有了基本前提,您现在还需要其他地图":你需要一个碰撞地图来知道你可以/不能在哪些瓷砖上行走,一个实体地图,以显示哪里有门,或能量提升或矿物,或敌人产卵,或事件-过场动画的触发器...

并非所有这些都需要在与世界地图相同的坐标空间中运行,但它可能会有所帮助.

此外,您可能想要一个更智能的世界".
例如,在一个级别中使用多个瓦片地图的能力...
以及用于交换瓷砖地图的瓷砖编辑器中的下拉菜单.

...一种保存图块信息(不仅是 X/Y,还包括有关图块的其他信息)和保存完成的地图"数组的方法,其中填充了图块.

即使只是复制 JSON,并将其粘贴到自己的文件中...

<小时>

程序生成

另一种方法,您之前建议的方法(知道如何连接岩石、草等")称为程序生成.
这要困难得多,涉及的要多得多.
像暗黑破坏神这样的游戏使用这个,所以你每次玩时都处于一个不同的随机生成的环境中.Warframe 是一个 FPS,它使用程序生成来做同样的事情.

前提:
基本上,您从瓦片开始,瓦片不仅仅是图像,瓦片必须是具有图像和位置的对象,但也有可能在其周围的事物的列表.
当您放下一块草时,该草可能会在其旁边产生更多的草.
草可能会说,在它周围的四个方向中,有 10% 的机会是水,20% 的机会是岩石,30% 的机会是泥土,还有 40% 的机会是更多的草.

当然,它真的没有那么简单(或者它可能是,如果你错了).

虽然是这样的想法,但程序生成的棘手部分实际上是确保一切正常工作而不会中断.
约束
例如,在那个例子中,你不能让悬崖墙出现在高地的内部.它只能出现在上方和右侧有高地,下方和左侧有低地的地方(星际争霸编辑器会自动执行此操作,就像您绘制的那样).坡道只能连接有意义的瓷砖.你不能把门围起来,或者把世界包裹在一个阻止你移动(或者更糟的是,阻止你完成一个关卡)的河流/湖泊中.

专业人士
如果您可以使所有寻路和约束都起作用,那么长寿真的很棒-不仅可以伪随机生成地形和布局,还可以用于放置敌人,放置战利品等.
将近 14 年后,人们仍在玩暗黑破坏神 II.

缺点
当你是一个单人团队(在业余时间碰巧不是数学家/数据科学家)时,真的很难做到正确.
确保地图有趣/平衡/有竞争力真的很糟糕......
星际争霸永远不可能使用 100% 随机生成来实现公平的游戏玩法.
程序生成可以用作种子".
你可以点击随机化"按钮,看看你得到了什么,然后从那里进行调整和修复,但是对于平衡"会有很多修复,或者为了限制传播而编写了很多游戏规则,你最终会花费更多的时间来修复生成器,而不仅仅是自己绘制地图.

那里有一些教程,学习遗传算法、寻路等等,都是很棒的技能......方式过度杀伤力,而是在您获得一两个游戏/引擎后需要考虑的事情.

I'm looking for a logical understanding with sample implementation ideas on taking a tilemap such as this:

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

And rendering in a logical way such as this:

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

I see all of the tiles are there, but I don't understand how they are placed in a way that forms shapes.

My understanding of rendering tiles so far is simple, and very manual. Loop through map array, where there are numbers (1, 2, 3, whatever), render that specified tile.

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);     
}   

Gets me:

But this means I have to manually figure out where each tile goes in the array so that logical shapes are made (rock formations, grass patches, etc)

Clearly, the guy who made the github code above used a different method. Any guidance on understanding the logic (with simply pseudo code) would be very helpful

解决方案

There isn't any logic there.

If you inspect the page's source, you'll see that the last script tag, in the body, has a huge array of tile coordinates.

There is no magic in that example which demonstrates an "intelligent" system for figuring out how to form shapes.

Now, that said, there are such things... ...but they're not remotely simple.

What is more simple, and more manageable, is a map-editor.


Tile Editors

out of the box:

There are lots of ways of doing this... There are free or cheap programs which will allow you to paint tiles, and will then spit out XML or JSON or CSV or whatever the given program supports/exports.

Tiled ( http://mapeditor.org ) is one such example.
There are others, but Tiled is the first I could think of, is free, and is actually quite decent.

pros:
The immediate upside is that you get an app that lets you load image tiles, and paint them into maps.
These apps might even support adding collision-layers and entity-layers (put an enemy at [2,1], a power-up at [3,5] and a "hurt-player" trigger, over the lava).

cons: ...the downside is that you need to know exactly how these files are formatted, so that you can read them into your game engines.
Now, the outputs of these systems are relatively-standardized... so that you can plug that map data into different game engines (what's the point, otherwise?), and while game-engines don't all use tile files that are exactly the same, most good tile-editors allow for export into several formats (some will let you define your own format).

...so that said, the alternative (or really, the same solution, just hand-crafted), would be to create your own tile-editor.

DIY
You could create it in Canvas, just as easily as creating the engine to paint the tiles.
The key difference is that you have your map of tiles (like the tilemap .png from StarCr... erm... the "found-art" from the example, there).
Instead of looping through an array, finding the coordinates of the tile and painting them at the world-coordinates which match that index, what you would do is choose a tile from the map (like choosing a colour in MS Paint), and then wherever you click (or drag), figure out which array point that relates to, and set that index to be equal to that tile.

pros:
The sky is the limit; you can make whatever you want, make it fit any file-format you want to use, and make it handle any crazy stuff you want to throw at it...
cons:
...this of course, means you have to make it, yourself, and define the file-format you want to use, and write the logic to handle all of those zany ideas...

basic implementation
While I'd normally try to make this tidy, and JS-paradigm friendly, that would result in a LOT of code, here.
So I'll try to denote where it should probably be broken up into separate modules.

// 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);
    };

Now you have a simple system for figuring out which tile was clicked.
Again, there are lots of ways of building this, and you can make it more OO,
and make a proper "tile" data-structure, that you expect to read and use throughout your engine.

Right now, I'm just returning the zero-based number of the tile, reading left to right, top to bottom.
If there are 5 tiles per row, and someone picks the first tile of the second row, that's tile #5.

Then, for "painting", you just need to listen to a canvas click, figure out what the X and Y were, figure out where in the world that is, and what array spot that's equal to.
From there, you just dump in the value of selected_tile, and that's about it.

// 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);
    };

As you can see, the procedure for doing one is pretty much the same as the procedure for doing the other (because it is -- given an x and y in one coordinate-set, convert it to another scale/set).

The procedure for drawing the tiles, then, is nearly the exact opposite.
Given the world-index and tile-number, work in reverse to find the world-x/y and tilemap-x/y.
You can see that part in your example code, as well.

This tile-painting is the traditional way of making 2d maps, whether we're talking about StarCraft, Zelda, or Mario Bros.
Not all of them had the luxury of having a "paint with tiles" editor (some were by hand in text-files, or even spreadsheets, to get the spacing right), but if you load up StarCraft or even WarCraft III (which is 3D), and go into their editors, a tile-painter is exactly what you get, and is exactly how Blizzard made those maps.

additions

With the basic premise out of the way, you now have other "maps" which are also required: you'd need a collision-map to know which of those tiles you could/couldn't walk on, an entity-map, to show where there are doors, or power-ups or minerals, or enemy-spawns, or event-triggers for cutscenes...

Not all of these need to operate in the same coordinate-space as the world map, but it might help.

Also, you might want a more intelligent "world".
The ability to use multiple tile-maps in one level, for instance...
And a drop-down in a tile-editor to swap tile-maps.

...a way to save out both tile-information (not just X/Y, but also other info about a tile), and to save out the finished "map" array, filled with tiles.

Even just copying JSON, and pasting it into its own file...


Procedural Generation

The other way of doing this, the way you suggested earlier ("knowing how to connect rocks, grass, etc") is called Procedural Generation.
This is a LOT harder and a LOT more involved.
Games like Diablo use this, so that you're in a different randomly-generated environment, every time you play. Warframe is an FPS which uses procedural generation to do the same thing.

premise:
Basically, you start with tiles, and instead of just a tile being an image, a tile has to be an object that has an image and a position, but ALSO has a list of things that are likely to be around it.
When you put down a patch of grass, that grass will then have a likelihood of generating more grass beside it.
The grass might say that there's a 10% chance of water, a 20% chance of rocks, a 30% chance of dirt, and a 40% chance of more grass, in any of the four directions around it.

Of course, it's really not that simple (or it could be, if you're wrong).

While that's the idea, the tricky part of procedural generation is actually in making sure everything works without breaking.
constraints
You couldn't, for example have the cliff wall, in that example, appear on the inside of the high-ground. It can only appear where there's high ground above and to the right, and low-ground below and to the left (and the StarCraft editor did this automatically, as you painted). Ramps can only connect tiles that make sense. You can't wall off doors, or wrap the world in a river/lake that prevents you from moving (or worse, prevents you from finishing a level).

pros
Really great for longevity, if you can get all of your pathfinding and constraints to work -- not only for pseudo-randomly generating the terrain and layout, but also enemy-placement, loot-placement, et cetera.
People are still playing Diablo II, nearly 14 years later.

cons
Really difficult to get right, when you're a one-man team (who doesn't happen to be a mathematician/data-scientist in their spare time).
Really bad for guaranteeing that maps are fun/balanced/competitive...
StarCraft could never have used 100% random-generation for fair gameplay.
Procedural-generation can be used as a "seed".
You can hit the "randomize" button, see what you get, and then tweak and fix from there, but there'll be so much fixing for "balance", or so many game-rules written to constrain the propagation, that you'll end up spending more time fixing the generator than just painting a map, yourself.

There are some tutorials out there, and learning genetic-algorithms, pathfinding, et cetera, are all great skills to have... ...buuuut, for purposes of learning to make 2D top-down tile-games, are way-overkill, and rather, are something to look into after you get a game/engine or two under your belt.

相关文章