render a tile map using javascript

后端 未结 1 1534
半阙折子戏
半阙折子戏 2021-01-31 11:43

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/

1条回答
  •  陌清茗
    陌清茗 (楼主)
    2021-01-31 12:28

    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.

    0 讨论(0)
提交回复
热议问题