Map: visual and computational scalability

We encourage users to post events happening in the community to the community events group on https://www.drupal.org.
litwol's picture

The proposal for a 'Map Engine'

The map data will be stored in a Cartesian coordinate system in the database. From a visual stand point, simple Cartesian coordinate system can be represented in square tile system or a Hex tile system. Square system is much easier to render for display and it is much more robust because it need not involve clever css hacks to emulate the hexagonal shape. in fact by using square tiles we can use the html table tag for display because it is more supported than css table implementation (not yet supported in IE for example). But from computational/programming stand point, there is no difference to prefer one system over the other ( readers are encouraged to provide arguments and examples against this statement. Contribute to the development!)

Functions of the Map Engine:

  • Render the map on every page request.
  • Provide multiple zoom levels
  • Provide ability to move
  • Infinite map / object scalability
  • Possible extended functionality: Map builder

Provide Multiple Zoom Levels

The idea: Objects are larger than they appear.

The concept of zoom is rather complex. zoom creates another layer of complexity and at the same time useful functionality at a developer's and player's disposal. the easiest example of concept would be the way the game 'Civilization' did their maps. in map display all objects appeared to be of the same scale. Cities, mountains, trees, rivers, ships, and even characters are all the same size when looking at them in a map overview. But when a character steps into a city all objects scale up and appear of their 'normal' size. The city becomes the size of a full map, buildings become the size of a character. but when character steps into a building, the room he is in becomes the size of a full map, and each tile that he can move onto and different objects in the room are seen to be of the same size as the character.

under this concept, all objects will share several common zoom states or levels. zoom 1 meanning no zoom. zoom 10 means 10x magnification of all items. Deppending on the current zoom level, objects will either look their normal size, or if the zoom is lesser (zooming out) then the object appears much smaller and if we zoom out some more the object dissapears completely.

This zooming functionality provides for a number of very interesting features.

Provide ability to move

Movement is an essential part of the map engine, perhaps one of the most important parts. The importance of movement comes from the fact that if you cant move close to an object then you cant do anything with it, basically if there's no movement then there is no fun of exploration and actions in a game.

a very simple movement system could be something that is based on fixed distances: at any given time i can move 1 tile to any allowed direction (directions are limited by which rendering engine a game uses, squares or hex allow movement in 4 and 6 directions respectively).

If a game movement is based on a limited action points, or if certain skills or abilities are distance based then the game will be more "correct" to use a hex tile maps rather than square tiles.

the reason for this comes from thagoras Theorem of a^2 + b^2 = c^2. in a square map system, if a player wanted to move his character in a diagonal direction (c in this example) then he would really have 2 options of movement: walk straight 1 tile, then turn to either side and walk for another tile in that direction. this creates a diagonal disposition from moving 2 tiles. but perhaps instead we could have made a direct diagonal movement that would be just 1 tile displacement too? in square tile system it is not possible because (for the example i will use 2 tiles in each direction instead of 1, math is more obvious that way) if we move up 2 tiles, then right 2 tiles it is not the same as moving 2 tiles in diagonal.

The math is like this 2^2 + 2^2 = C^2 (c is our diagonal distance). so according to this formula our diagonal distance is sqrt(8) which is 2.82 and not just 2! so in a square tile system our diagonals are longer in distance than 4 dimentional movement of left, right, up, down.




In hexagonal tile system a player is allowed to move in 6 directions, regardless of which direction a player moves he always moves the same distance, whether its in diagonal or not. however hexagonal tile system does not come without any cons! the downside of hexagonal tile system is that the movement is restricted into the following directions: up, down, up right, up left, down left, down right. the movement of left and right is completely missing! in fact you can never achieve 2 object standing left and right of eachother respectively and be in adjacent tiles.

For the initial project i propose we only build square tile system because it is easier to build and at this point i think it is more important to concentrate on releasing a robust system quickly rather than postpone it untill we work out all the "extra" stuff which arent really essential for the game at this stage. This means that the player can only move in 4 directions: up, down, left, right.

I will explain how zoom affects movement after i address the next topic which is infinite object scalability

Infinite map / object scalability

my brain power is running low now taking into account that it is 2:20 am! i will make it short and sweet. the idea of infinite scalability is that you can take any map object which can be an outdoor map or an room inside a building or a building itself or a cave or anything of that sort. the idea is that at different zoom levels you can view a whole map as a 7x7 grid, but then you can define of the tiles to be a map link, when a player steps onto that tile he is taken into that other map. a real life example of this could be you stepping into a door (stepping onto the map linked tile) and you appear inside a room (you are taken into the new map that was linked to by that link tile). then inside that next map you can go into another tile (could possibly be thought of as a closet in a room).

so with this idea in mind we can create the following things:
1) map overview: display huge cities or mountains or other items the same scale as the character itself
2) step into a city which was represented as 1 tile in map overview and it "zooms in" to show many different buildings in their own tiles.
3) step into a building and you now can see many rooms and objects in their own tiles, all "zoomed in" and are now visible.

also zoom allows for greater movement options. when you zoom all the way into the maximum zoom where you see the character and different objects in their correct scale (a treasure box is smaller than a character). and if the player tries to walk on that zoom level then he can move only +1 tile in either direction. but if the player zooms out to the maximum zoom distance, then when a player tries to move 1 tile, he's really now muving 10s or even 100s of tiles into a chosen direction.
for good practice such movement should be restricted by fog of war, you cant quickly "zoom" into an area that you haven't discovered yet.

that is all for now. i will try to elaborate more on scalability later when i am less tired.

AttachmentSize
gameapi_map_grid_scalability.JPG11.18 KB
gameapi_map_hex.jpeg4.13 KB
gameapi_map_square.jpeg5.48 KB

Comments

MapAPI

litwol's picture

With clear head after long night sleep i can see some things that we could simplify further with regards to the map engine.

so far i can clearly isolate the need for MapAPI.

<?php
//$mid is an ID for a map object
//$args is an array for any extra arguments.
//  for now i thing we need to include dimentions limiter inside $args, but i am sure we can expand it further
//$map_array is a two dimentional array with data on a particular map
function mapapi_load($mid, $args){
//perform load map operation here


return $map_array;
}

function
mapapi_save($map_array){
//perform save map operations here
//because the map array can contain multiple values we need to think of performance hit here

return true or false;
}
?>

We need to decide just what kind of information we return on mapapi_load which leads into the discussion of just how we will represent our maps?

These are the map data we definitely need for now, please add more fields and reasons for them as you see fit.

Map definition table

column description
mid Map ID
name name of the map
limit_x the limit of the x field: 0 <= x <= limit_x
limit_y the limit of the y field: 0 <= y <= limit_y

Tile data table

column description
mid Map ID that this tile belonds to
type Tile type. Currently possible: Tile, Map
coord_x the limit of the x field: 0 <= coord_x <= map_definition_table.limit_x
coord_y the limit of the y field: 0 <= coord_y <= map_definition_table.limit_y
name name of the tile. can be used to group multiple tiles that are close together into a neighborhood relationship. I dont think this will have any effect on the programming. it could simply be used for display to the player: You are located in (map)New York: (neighborhood/tile) Brooklyn.
terrain a helper data for themers to know what kind of tile to display for visual effect: water, dirt, grass, mountain, etc...

Also by using this data we can calculate whether a character has permission to cross that particular type of terrain

Tile content table

column description
mid Map ID that this tile belonds to
coord_x the x coordinate: 0 <= coord_x <= map_definition_table.limit_x
coord_y the y coordinate: 0 <= coord_y <= map_definition_table.limit_y
data name name of the object. used in a cck manner.
example: name = character
so in database i will look up select * from character
cid content id. used in combination with above name in the WHERE clause
example: SELECT * FROM character WHERE id = cid

Nightly thoughts

what seems to be an obvious idea has been eluding me and i think i have finally found a way to piece all of our "unknown ahead of time" objects with our map!

its pretty simple, we must assume that if a data name is defined inside tile content table (which it must! otherwise that tile would be empty ;) ) then that content would have to define a function:
[data_name]_load()

so if we have data type "player" and "monster" then each would have function player_load and monster_load respectively.

deppending on the 'depth' of our map_load query, we can use this idea to preload map array with more data for each map array cell. however we must be VERY cautious as, i believe, it will be a noticeable performance hit.

Map query depth? whats that?

Our map design and content is rather complex, so deppending on what kind of information and who is calling it we will have to enforce 'limiters' to make sure we always return a very restricted subset of the whole map data for optimization purposes.

the core idea of 'limiters' is this: dont return more than we ask for.
While this idea might be very obvious, it is not often practiced especially in complicated functions such as mapapi_load which is a multi-depth query function.

limiters will be passed into mapapi_load through the $args argument.

to understand what kind of limiters we need, we first need to understand what types of uses will we have for our mapapi_load function. I encourage everyone's discussion regarding this function's purpose. i will start by placing my own thoughts.

mapapi_load() explained

In short: it loads a matrix of arbitrary size from the database, populates it with some map related data and returns it to the caller.

now the question is, what kind of data do we put in the array that we return? Because of the above mentioned [data_name]_load() hook, our mapapi_load function gains an incredible power of loading ANY type of game data into the map array. however this power is also it's greatest weakness and it will be a HUGE performance hit, so we must enforce some default behavior and deppending on some other limiters we will load extra data.

actually i think it is better to first ask, how can we communicate to the mapapi_load function so it knows what we want from it.

<?php
//focus - this will be the coordinate at which we will center. should be the absolute middle cell/row of our map array that we return
//x_limit and y_limit - this defines our "box" or "viewable map", it can be a 5x5 matrix, or a 2x10. This design is limited to a 4 sided with each side connected at 90 degrees shape.

$args = array(
 
'focus' => array('x'=>x_coord,'y'=>y_coord),
 
'x_limit' => 'maximum number of columns',
 
'y_limit' => 'maximum number of rows',
);
?>

possible uses of the above code:
1) i want to display a 20x20 mini map. so i only need to display tiles and their pictures and no extra data. this is acquired from the tile data table.
2) by the design of the mapapi_load, our centered coordinate will also contain extra information about the content of that tile (acquired from tile content table).

passing any kind of coordinates through the $args array should make the mapapi_load function to load the tile content table data regarding that tile.
$args[x_coord][y_coord] = true; //this will tell our mapapi_load to call [data_name]_load on all items that are located on a tile in the tile content table.

in case if our tile contains hundreds of items we really dont want to call [data_name]_load hundreds of times (could happen if all those items just so happen to be on the coordinate that we are focusing). if we standardize all our objects and force certain field names for them then we can optimize this a little. we can introduce 'profile' queries, example: 'id' would return only the ID of that data, 'name' would return only name, 'minimal' would return only id and name, 'full' would return ALL info of that data (similar to * in mysql). i really fail to see what information other than id and name we will need (except some rare ocasions) to get inside the mapapi_load function. by returning data_name and id, a developer can query his own information later.

ok so i think the question at hand now is: should we rely on the caller function to apply [data_name]_load to every cell in the map array as per it's own need, or should we let our mapapi_load() to handle calling this hook?

I propose that we let the caller function to handle calling that hook for the following reasons:
1) reduces work load and improves performance of mapapi_load function
2) from themer's perspective, some might want to display different information on the tiles, perhaps deppending on the tile type! so ahead of time we will never know what information to load from [data_name] objects (food for thought: unless we define a configuration array that contains information regarding objects that should always be fully loaded and others ignored?????)
3) from developer's perspective. not quite sure what to say here. the only reason why i could justify sticking [data_name]_load into mapapi_load is if a character performs some kind of area attack or area effect so the developer needs to load "extra" information about multiple tiles and multiple contents of that tile. before mapapi_load is called the developer should already know what range his offect will take, so inserting those coordinates into our $args array will force mapapi_load to load extra information about those coords (reminder: extra information means calling [data_name]_load, data_name is acquired from tile_content_table.data_name).

however i must add that we will always call the [data_name]_load function for all objects that are located on the 'focus' tile which is the one the player stands on. we must decide whether we will let the developer call that function for every [data_name] object that is located in that tile of will mapapi_load automate it? if mapapi_load automates it the it makes no sence to not enable it for other uses for other tiles that are non focused. but if we let the developer call the [data_name]_load function then it SHOULD improve performance alot because the developer will only call that function when he knows it needs to be ran.

the lazy programmer in me tells me that i want to put [data_name]_load inside the mapapi_load
the infrastracture architect and/or performance optimizer in me tells me to take it out and let the developer handle his call [data_name]_load calls. this will also allow for more complicated algorithms to be implemented in game that will decide which [data_name] object to load or not.

i try to be smart by letting the infrastracture architect and/or performance optimizer win the argument, but i would also like to hear from the rest of the team and what their thoughts are.

by the way, i think the following function is a nice helper function that all [data_name]_load (or other [data_name] hooks) should be ran from. it will check for existance of a function or return false. the key benefit of using this over using [data_name]_load directly is that calling a non existent function from this script will not fause the whole script to fataly fail

<?php
function game_call($func_name,$args = array()){
   if(
function_exists($func_name))
        return
call_user_func_array($func_name,$args);
else   
       return
false;
}
?>


------------------
Sometimes interesting things appears on http://litwol.com

Fog Of war

litwol's picture

Map definition table

column description
mid Map ID
x_coord the x coordinate
x_coord the y coordinate
cid character id (if a player creates another character then he needs to explore again!)


------------------
Sometimes interesting things appears on http://litwol.com

I think game_call should look like this...

dmitrig01's picture

<?php
function game_call($func_name) {
  if(
function_exists($func_name)) {
   
$args = func_get_args();
   
array_shift($args);
    return
call_user_func_array($func_name, $args);
  }
  else {
    return
false;
  }
}
?>