Search code examples
javascriptarrayscanvasgame-enginepath-finding

Negative coordinates in a grid based game


I have written a small 2D game in javascript that uses a grid where the player starts at position [0,0] and can move an almost infinite distance in either direction.

Now I want to implement A* pathfinding, but I'm having some problems finding the best way to store the world with all it's different obstacles, enemies and terrain. This is what I have tried or thought about so far.

Array of arrays

Here I store the world in an array of arrays [x][y].

var world = [[]];
world[312][11] = 0;
world[312][12] = 0;
world[312][13] = 1;
world[312][14] = 1;
...

This works great with A* pathfinding! It's easy and very fast to access a specific coordinate and populate the world. In the example above I just store passable (0) or impassable (1) terrain, but I can store pretty much whatever I want there. However, this doesn't work very well with negative coordinates like if my players is at [-12][-230]. Negative keys in a javascript array isn't actually part of the array, they won't be included in world.length or world[3].length and from what I understand, it's overall bad practice and might have some impact on the performance as well. I read somewhere that if you are using negative keys in your array, you are doing it wrong. I would still not pass the entire world into the A* function for obvious reasons. Just a small part close to my player, but the coordinates would correspond to the positions in the array which is easy to work with.

A separate array of arrays just for A* pathfinding

This is where I'm at right now. I have a separate 50x50 grid called pathMap = [[]], that is only used for pathfinding.

var pathMap = [[]];
pathMap[0][0] = 0;
pathMap[0][1] = 0;
pathMap[0][2] = 1;
pathMap[0][3] = 1;
...

It starts at pathMap[0][0] and goes to pathMap[50][50] and is working as an overlay on my current position where I (as the player) will always be in the center position. My real coordinates may be something like [-5195,323], but it translates to pathMap[25][25] and everything close to me is put on the pathMap in relation to my position. Now this works, but it's a huge mess. All the translations from one coordinate to another back and forth makes my brain hurt. Also, when I get the path back from A*, I have to translate each step of it back to the actual position my element should move to in the real world. I also have to populate the same object into 2 different grids every update which hurts performance a bit as well.

Array of objects

I think this is where I want to be, but I have some issues with this as well.

var world = [];
world[0] = { x: -10, y: 3, impassable: 0 };
world[1] = { x: -10, y: 4, impassable: 0 };
world[2] = { x: -10, y: 5, impassable: 1 };
world[3] = { x: -10, y: 6, impassable: 1 };
...

Works great with negative x or y values! However, it's not as easy to find for instance [10,3] in this array. I have to loop through the entire array to look for an object where x == 10 and y == 3 instead of the very easy and fast approach world[10][3] in the first example. Also, I can't really rely on the coordinates being in the right order using this version, sorting becomes harder, as does other things that was a lot easier with the array of arrays.

Rebuild the game to always be on the positive side

I would prefer not to do this, but I have considered placing the players starting position at something like [1000000,1000000] instead, and making negative coordinates off limits. It seems like a failure if I have to remove the vision I have of endlessness just to make the pathfinding work with less code. I know there will always be some upper or lower limits anyways, but I just want to start at [0,0] and not some arbitrary coordinate for array related reasons.

Other?

In javascript, is there another option that works better and is not described above? I'm very open to suggestions!

Is there a best practice for similar cases?


Solution

  • You have three coordinates system you must distinguish :

    • the world coordinates.
    • the world model / path-finding (array) coordinates.
    • the screen coordinates.

    The screen coordinates system depends upon :

    • the viewport = the canvas. (width, height in pixels).
    • a camera = (x,y) center in world coordinates + a viewWidth (in world coordinates).

    To avoid headaches, build a small abstraction layer that will do the math for you.

    You might want to use Object.defineProperty to define properties, that will provide a fluent interface.

    var canvas = ... ;
    var canvasWidth = canvas.width;
    var canvasHeigth = canvas.heigth;
    
    
    var world = {
                  width  : 1000,  // meters
                  height : 1000,  // meters
                  tileSize : 0.5,   // height and width of a tile, in meter
                  model  : null,  // 2D array sized ( width/tileSize, XtileSize )
                 };
    // possibles world coordinates range from -width/2 to width/2 ; - height/2 height/2.
    
     var camera = {
                     x : -1,
                     y : -1,
                     viewWidth : 10, // we see 10 meters wide scene
                     viewHeight : -1 // height is deduced from canvas aspect ratio
                  };
    camera.viewHeight = camera.viewWidth * canvasWidth / canvasHeight ;
    

    Then your character looks like :

    // (x,y) is the center of the character in centered world coordinates
    // here (0,0) means (500,500) world coords
    //                  (1000,1000) array coords
    //                  (320, 240) screen coords in 640X480
    function /*Class*/ Character(x, y) {
         var _x=x;
         var _y=y;
         var _col=0;
         var _row=0;
         var _sx=0.0;
         var _sy=0.0;         
         var dirty = true;
         Object.defineProperty(this,'x', 
                                  { get : function() {return _x; } 
                                    set : function(v) { _x=v; 
                                                        dirty=true; } });
         Object.defineProperty(this,'x', 
                                  { get : function() {return _y; } 
                                    set : function(v) { _y=v; 
                                                        dirty=true; } });
         Object.defineProperty(this,'col', 
                                  { get : function() {
                                                   if (dirty) updateCoords();
                                                   return _col; }  });
         Object.defineProperty(this,'row', 
                                  { get : function() {
                                                   if (dirty) updateCoords();
                                                   return _row; }  });
         Object.defineProperty(this,'sx', 
                                  { get : function() {
                                                   if (dirty) updateCoords();
                                                   return _sx; }  });
         Object.defineProperty(this,'sy', 
                                  { get : function() {
                                                   if (dirty) updateCoords();
                                                   return _sy; }  });
         function updateCoords() {
            _row = ( ( _x + 0.5 * world.width )/ world.tileSize ) | 0 ;
            _col = ( ( _x + 0.5 * world.height )/ world.tileSize ) | 0 ;
            _sx =  canvasWidth  * ( 0.5 + ( _x - camera.x ) / camera.viewWidth ) ;
            _sy =  canvasHeight * ( 0.5 + ( _y - camera.y ) / camera.viewHeight ) ;
            dirty = false;            
         }
    }