使用 javascript 渲染瓦片地图

Posted

技术标签:

【中文标题】使用 javascript 渲染瓦片地图【英文标题】:render a tile map using javascript 【发布时间】:2014-03-17 15:45:08 【问题描述】:

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

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

得到我:

但这意味着我必须手动确定每个图块在阵列中的位置,以便形成逻辑形状(岩层、草地等)

显然,制作上述 github 代码的人使用了不同的方法。任何有关理解逻辑(仅使用伪代码)的指导都会非常有帮助

【问题讨论】:

【参考方案1】:

那里没有任何逻辑。

如果您检查页面的源代码,您会看到正文中的最后一个脚本标记有一个巨大的磁贴坐标数组。

在那个例子中没有什么魔法可以展示一个“智能”系统来弄清楚如何形成形状。

现在,就是说,有这样的事情......但它们并不简单。

更简单、更易于管理的是地图编辑器。


平铺编辑器

开箱即用:

有很多方法可以做到这一点...有免费或便宜的程序可以让您绘制瓷砖,然后会吐出 XML 或 JSON 或 CSV 或给定程序支持/导出的任何内容。

平铺(http://mapeditor.org)就是这样一个例子。 还有其他的,但 Tiled 是我能想到的第一个,是免费的,而且实际上相当不错。

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

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

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

DIY 您可以在 Canvas 中创建它,就像创建绘制图块的引擎一样容易。 主要区别在于您拥有瓷砖地图(例如 StarCr 中的 tilemap .png... erm... 示例中的“found-art”)。 不是循环遍历数组,查找图块的坐标并在与该索引匹配的世界坐标处绘制它们,您要做的是从地图中选择一个图块(如在 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 个图块,并且有人选择了第二行的第一个图块,那就是第 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);
    ;

如您所见,执行一个的过程与执行另一个的过程几乎相同(因为它是 -- 在一个坐标集中给定 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% 随机生成来实现公平的游戏玩法。 程序生成可以用作“种子”。 你可以点击“随机化”按钮,看看你得到了什么,然后从那里进行调整和修复,但是对于“平衡”会有很多修复,或者为了限制传播而编写了很多游戏规则,你最终会花费更多的时间来修复生成器,而不仅仅是自己绘制地图。

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

【讨论】:

感谢 Norguard。所以我认为我现在没有足够的带宽来创建自己的图块编辑器。我将使用平铺。所以当你说the downside is that you need to know exactly how these files are formatted 时,Tiled 会将地图保存为 .tmx,对吗?所以我应该把它作为 XML 读入我的 javascript 游戏并解析它。这是基本前提吗? @Growler 这就是想法。 Tiled 也支持 JSON,尽管我不能 100% 确定为它编写的插件现在是否是安装的一部分(不过我很确定它是)。无论哪种方式,添加 JSON 输出都很容易。因此,您需要制作地图,导出 JSON,查看文件是如何构建的,并弄清楚如何在引擎中解析它,将其数据结构与您的瓦片地图联系起来。 使用 melonJS (melonjs.github.io/tutorial) 代替 createJS 怎么样。 melonJS 似乎具有本机 Tiled 支持,因此我不必为与语言无关的 Tiled 生成的 .tmx 文件编写解释器。 melonJS 是否比 createJS 更适合网页游戏开发(创建和动画形状、对象、补间、声音等)? 感谢您的详尽回答。

以上是关于使用 javascript 渲染瓦片地图的主要内容,如果未能解决你的问题,请参考以下文章

Unity入门计划基本概念-瓦片地图 TileMap 01

瓦片地图是什么鬼

瓦片切图算法以及并发切图实践

瓦片切图算法以及并发切图实践(上篇)

访问 MKMapView 的地图瓦片

openlayers6结合geoserver实现地图矢量瓦片(附源码下载)