2
\$\begingroup\$

I'm coding from scratch a small experimental game on a medium-sized random rectangular square tile map. (Say, a map of a dungeon.)

There are several types of tiles (for example: floor, wall, monster, boss, treasure, player etc.). There are no extra information stored in the map tiles, just tile type.

Most parts of map are randomly generated. However there are several features that are to appear on every generated map in pre-set places. Say, walls around the dungeon, boss locations, player spawn point and the treasure.

Note: Since the game is small, coded for fun, and the only game designer is a programmer (that is, me myself), I do not plan to spend resources on coding a level editor or supporting an existing one. Also, I do not plan to support existing tile map libraries as I want to dig into some of the related coding problems myself (see above about fun), but I be happy happily to look at relevant existing code as a reference.

When I look at my map generation code, I find it rather ugly.

Is there an elegant way to deal with the problem? If that matters, I'm writing this game in JavaScript, but I will be happy to see a relevant reference in any programming language.

Here is a partial pseudocode to illustrate how my current tile generator solution looks like:

this.init = function(w, h) {
  this.w = w;
  this.h = h;
  this.tile_presets_ = { };

  var mid_h = Math.floor(h / 2);
  var quarter_w = Math.floor(w / 4);

  /* Here is the part I don't like -- a set of generator rules: */

  this.preset_col_(0, Tile.GRASS);
  this.preset_tile_(0, mid_h, Tile.FLOOR);

  this.preset_col_(1, Tile.GRASS);
  this.preset_tile_(1, mid_h, Tile.FLOOR);

  this.preset_col_(2, Tile.WALL);
  this.preset_tile_(2, mid_h, Tile.FLOOR);

  this.preset_tile_(2, mid_h, Tile.PLAYER);

  this.preset_tile_(1 * quarter_w, mid_h, Tile.BOSS);

  this.preset_tile_(2 * quarter_w, mid_h, Tile.BOSS);

  this.preset_tile_(3 * quarter_w, mid_h_, Tile.TREASURE);

  this.preset_col_(w - 1, Tile.WALL);
};

Implementation details that may help to understand the code above:

this.tile = function(x, y) {
  var preset = this.tile_presets_[x + y * this.w];
  if (preset !== undefined) {
    return preset;
  }

  return this.random_tile_();
};

this.preset_tile_ = function(x, y, tile) {
  this.tile_presets_[x + y * this.w] = tile;
};

this.preset_col_ = function(x, tile) {
  for (var y = 0; y < this.h; ++y) {
    this.preset_tile_(x, y, tile);
  }
};

Note that I shown a rather simple set of generator rules above. Actual thing is several times longer.

I hate that the code looks imperative — and it is hard to figure out what is going on from the first glance. I think that declarative approach could help. But so far I can't figure out a good solution.

Any clues?

\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

I've created several similar dungeon-based games and I've always used a string-based representation for the map, where a different character represents each tile, e.g:

var map = [
    "##########",
    "#........#",
    "#..>.....#",
    "#........+",
    "#........#",
    "##########"
];

In my opinion, this gives a great visual representation and allows you to use a regular text editor as "tile editor". This character representation is used for generating the actual level data, if you e.g. need coloring, graphical tiles or as I've done, a 3D dungeon mesh.

As for the handling of preset sections, just create similar character representations for each of them and in your generator, loop through them and put them into correct locations in the big dungeon map. Here's some pseudo JavaScript for one possible approach:

var map = /* stuff */ // Here's the dungeon map without preset stuff
var presets = [
    { x: 2, y: 5, map: [ "()]" ] },
    /* More map fragments with positions */
];
for (var i = 0; i < presets.length; ++i) {
    for (var j = 0; j < presets[i].map.length; ++j) {
        /* Figure out coordinates based on presets[i].x/y and j */
        var x = , y = ; 
        map[y][x] = presets[i].map[j];
    }
}
/* Now the presets are part of the main map 
   and you can process the map into final representation if needed */

EDIT:

I seem to have thought this a bit too high level (as in preset rooms instead of preset walls as you mentioned). However, you could change the preset x and y to be relative to the map size, e.g. x=0.333 would be 1/3 of map width (the loop would multiply x by the width). Null could mean "repeat the pattern through the dimension". Thus the preset for your lava-column example in the comment would be { x:0.333, y:null, map:"L" }.

However, this is rather limited and getting a little complex. For such components as straight, varying size wall sections you are eventually going to have a loop that creates it. The easiest way is to call a function as you did, but if that looks ugly, you can use data-oriented approach as I outlined.

One wild idea I just came up would be to create a small hand-made prototype map which would have all the bosses and treasures in their relative positions, but during the map generation you would multiply all their positions with the actual map dimensions (i.e. you kind of magnify the map, but keep the preset features as one tile. With some constraints, this could be expanded to walls too (if the proto map has adjacent wall tiles, the gaps are filled during expansion in that dimension, but not in the dimension where there is no neighbouring wall). Below is an illustration with only horizontal expansion (S and E are just some preset tiles you want to place relatively but don't want to use explicit function calls).

#####         #############
#S# #         #  S  #     #
# # #   ==>   #     #     #  
# #E#         #     #  E  #
#####         #############

In the end I think the best way to make the generator feel nice, is to structure it with high-level functions as ashes999 suggested.

\$\endgroup\$
5
  • \$\begingroup\$ Nice! But this assumes fixed map dimensions. If map size is not fixed, how would you represent, say, a preset with vertical 1-tile-wide channel of lava across whole map height, at 1/3 of map width? (Or dungeon walls in my original example.) \$\endgroup\$ Jan 31, 2013 at 15:18
  • \$\begingroup\$ @AlexanderGladysh Updated my answer. \$\endgroup\$
    – Tapio
    Jan 31, 2013 at 16:43
  • \$\begingroup\$ Thanks. Another use case while I'm digesting about your answer: A square lava pool with a statue in the middle, 1/8 of map height in size, placed in the center of the map. \$\endgroup\$ Jan 31, 2013 at 17:03
  • \$\begingroup\$ I'm thinking about extrapolation rules. Something like "#.*#>#.*#", expanding to "#...#>#...#" for map of width 11. But that's a strange road to follow... \$\endgroup\$ Jan 31, 2013 at 17:07
  • \$\begingroup\$ As for null as a wildcard (regardless of extrapolation rules above). Probably better idea would be to allow filter functions at x and y fields (i.e. x: function(x) { return x % 2 }, and use certain strings as a shorthand for some of the functions. I.e. y : "*" would be equivalent to y:function(y) { return true }. Not quite declarative, but almost. Note that this is not extrapolation, but something like cloning, and can lead to strange results if preset patterns overlap. \$\endgroup\$ Jan 31, 2013 at 19:40
1
\$\begingroup\$

If you're not going to invest in a level editor, the only thing left is to invest in the level generation algorithm. This is generally how any randomly-generated game (like a roguelike) will work out.

What you may want to do is extract things out into high-level functions so you can tweak the generation, eg.

function createaMap() {
  setDefaultFloorTo(grass);
  generateBorderWalls;
  placeBossInCenter();
  placeSpawnPoint();
  // ...
}

This will help you stay organized and tweak the order of generation if you need to (eg. spawn point first, then boss, or vice-versa).

With respect to fixed items, you should probably place those first, and then generate everything else around them.

Also, as mentioned in Tapio's answer, you may want to dump the final map out to a file in some format that you can easily review to see what the final dungeon looks like.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .