使用JavaScript渲染瓦片地图

16

我希望找到一种逻辑理解方法,并提供实现想法,以处理这样的瓦片地图:

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代码的作者使用了不同的方法。有关如何理解其逻辑(仅包含伪代码)的任何指导都将非常有帮助。

1个回答

40

这里面没有任何逻辑。

如果您检查页面源代码,您会看到最后一个脚本标签(在body中)有一个巨大的瓦片坐标数组。

那个例子中没有展示出一个“智能”系统来找出如何形成形状的魔法。

现在,话虽如此,确实存在这样的东西......但它们绝不简单。

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


瓦片编辑器

开箱即用:

有很多方法可以做到这一点...... 有免费或便宜的程序,可以让您绘制瓦片,然后输出XML、JSON、CSV或其他给定程序支持/导出的格式。

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

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

缺点:
...缺点是你需要确切地知道这些文件的格式,以便你可以将它们读入游戏引擎。
现在,这些系统的输出是相对标准化的......所以你可以将那个地图数据插入不同的游戏引擎中(否则有什么意义呢?),虽然游戏引擎并不都使用完全相同的瓦片文件,但大多数好的瓦片编辑器允许导出到几种格式(有些甚至允许您定义自己的格式)。

......因此,另一种选择(或者说,同样的解决方案,只是手工制作),就是创建自己的瓦片编辑器。

自己动手:
您可以像创建绘制瓦片的引擎一样轻松地在Canvas中创建它。
关键的区别是您拥有瓦片地图(就像示例中的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);
    };

现在您有一个简单的系统来确定哪个瓷砖被点击了。
再次说明,有许多构建此系统的方法,您可以使它更加面向对象,并创建一个适合在整个引擎中阅读和使用的“tile”数据结构。

目前,我只返回从左到右、从上到下读取的基于零的瓷砖编号。
如果每行有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,将其转换为另一个比例/集合)。绘制瓷砖的过程几乎完全相反。给定世界索引和瓷砖编号,反向查找世界 x/y 和瓷砖地图 x/y。您可以在示例代码中看到该部分。这种瓷砖绘画是制作2D地图的传统方法,无论我们谈论的是星际争霸、塞尔达还是马里奥兄弟。并非所有游戏都有“用瓷砖绘画”编辑器的便利(有些是手工制作文本文件,甚至是电子表格,以获得正确的间距),但如果您加载星际争霸或甚至魔兽争霸III(这是3D游戏),并进入它们的编辑器,您会发现一个瓷砖绘画器正是您所需要的,也是暴雪制作这些地图的方式。
补充:
完成基本前提后,您现在还需要其他“地图”:您需要一个碰撞地图来知道哪些瓷砖可以/不能行走,一个实体地图来显示门、电源或矿物等位置,或敌人生成点或剧情事件触发器...
并非所有这些都需要在与世界地图相同的坐标空间中操作,但是这可能会有所帮助。
此外,您可能需要一个更智能的“世界”。例如,使用多个瓷砖地图在一个级别中,并在瓷砖编辑器中使用下拉列表来交换瓷砖地图...
一种方法是保存瓷砖信息(不仅是X/Y,还包括关于瓷砖的其他信息),并保存填充瓷砖的完成“地图”数组。
即使只是复制JSON,并将其粘贴到自己的文件中...
程序生成:
另一种方法是您之前提到的方法(“知道如何连接岩石、草等”),称为“程序生成”。这要困难得多,涉及的内容也更多。像《暗黑破坏神》这样的游戏使用此功能,因此每次玩游戏时,您都处于不同的随机生成环境中。《星际战甲》是一款FPS游戏,它使用程序生成来做同样的事情。
前提:
基本上,您从瓷砖开始,而不仅仅是瓷砖作为图像,瓷砖必须是一个具有图像和位置的对象,但也必须具有可能在其周围出现的事物列表。当您放置一片草地时,该草地将具有在其旁边生成更多草地的可能性。草地可能会说有10%的水、20%的岩石、30%的土壤和40%的更多草地,在其四个方向中的任何一个方向上。
当然,实际上并不是那么简单的(如果你错了话就会变得简单)。虽然这是个好主意,但程序生成的棘手之处在于确保一切都能正常运作而不会出问题。
限制条件:例如,在上述例子中,悬崖墙不能出现在高地内部。它只能出现在上方和右侧有高地以及下方和左侧有低地的地方(StarCraft编辑器会自动完成此操作,因为你进行了绘画)。坡道只能连接有意义的瓷砖。你不能封锁门,或用河流/湖泊包围世界以防止移动(更糟糕的是,阻止你完成一个级别)。
优点:如果您能让所有寻路和约束条件都正常工作,那么这对于伸缩性来说非常好,不仅可以伪随机生成地形和布局,还可以生成敌人位置、战利品位置等等。人们仍在玩《暗黑破坏神II》,近14年了。
缺点:如果你是一个单人团队(谁碰巧不是一个数学家/数据科学家),那么很难做到正确。对于保证地图有趣/平衡/具有竞争力来说真的很糟糕。StarCraft永远不能使用100%的随机生成来进行公平的游戏。程序生成可以用作“种子”。你可以点击“随机化”按钮,看看你得到了什么,然后进行微调和修复,但是会有很多修复“平衡”的问题,或者写出很多游戏规则来限制传播,这样你最终会花更多的时间来修复生成器,而不是自己画地图。
虽然有一些教程,学习遗传算法、寻路等都是非常好的技能...... ...但是,为了学习制作2D俯视角瓷砖游戏,这些技能都过于繁琐了,而且在做完一个或两个游戏/引擎之后再去学习这些技能可能更好。

谢谢Norguard。我现在没有足够的时间来创建自己的瓦片编辑器,所以我打算使用Tiled。当你说“缺点是你需要确切地知道这些文件的格式”时,Tiled会将地图保存为.tmx格式,对吗?那么我应该将其作为XML读入我的JavaScript游戏并解析它。这是基本前提吗? - user3871
@Growler 这就是我的想法。Tiled也支持JSON,虽然我不确定它的插件是否已经成为安装的一部分(但我相当确定)。无论哪种方式,都很容易添加JSON输出。所以你可以创建一个地图,导出JSON,查看文件的构建方式,并找出如何在你的引擎中解析它,将其数据结构连接到你的瓷砖地图上。 - Norguard
使用melonJS(http://melonjs.github.io/tutorial/)代替createJS怎么样?melonJS似乎具有本地Tiled支持,因此我不必为与语言无关的Tiled生成的`.tmx`文件编写解释器。相比createJS,melonJS是否更适合Web游戏开发(创建和动画形状、对象、缓动、声音等)? - user3871
2
感谢您的详细回答。 - Anthony Gatlin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接