T2DScript/TileMovement

From TDN

This page is a Work In Progress.


Contents

Moving Along a Tile Map, Tile by Tile

This TDN Article will, hopefully, explain the general concept of 'tile movement', most commonly found in games such as Pac-Man and First Rate Games Magic Pearls. The code found in this Article is free to use, for any purpose.


Things to Note about this Article
I will refer to both C++ and TorqueScript code, and when possible will show both a C++ (quicker) and a TorqueScript (slower, sometimes) way of performing the same 'out come'.

General Logic

For starters, you need to build a tile map (t2dTileLayer object) and populate it with Sprites, while doing this, you might find it fairly useful for your general movement logic if you attach readable "Custom Data" to your cells. You can refer to this article for help on doing this.


Question

  1. Why do we want Custom Data?
Answer

  1. Custom Data, such as "wall", "grass", "water", "fire", "lava", "pellet", "special", "dirt", etc, allows you to easily create switch/case statements and determine what to do when your mobile object is standing on, or attempting to walk onto the tile. You can also integrate a form of A* Path-Finding, and utilize these values to determine your 'weights'.


Now, let's create a simple tile-map using whatever art resources you have available to you, we'll start off simple and build a map that has only "wall" and "" custom-data cells, the "wall" cells should represent tiles that are currently drawing "walls" (duh?), and the "" custom-data cells will be generic "floor" cells -- for simplicity, you don't have to have actually draw your "floor" tiles with custom-data, as you may be using something different for your floor -- such as a nicely made t2dStaticSprite that has some nifty looking 3D feel to it, and your just simply drawing your walls around the sprite layout, so that things look purty.

General Player Movement

Ok, so you've got a tile-map laid out, and, presumably, have placed a t2dStaticSprite or t2dAnimatedSprite in your level and have some way of referencing it -- this (animated)sprite will be our 'player'.

Create a basic movement system for your player, like so:

function player::onLevelLoaded(%this, %sg)
{
  moveMap.bindCmd("keyboard", "w", %this @ ".move(\"up\");", "");
  moveMap.bindCmd("keyboard", "s", %this @ ".move(\"down\");", "");
  moveMap.bindCmd("keyboard", "a", %this @ ".move(\"left\");", "");
  moveMap.bindCmd("keyboard", "d", %this @ ".move(\"right\");", "");
}

function player::isPlayable(%this, %tile)
{
   %customData = moveLayer.getTileCustomData(%tile);
   if(%customData !$= "") return true;
   else return false;
}

function player::move(%this, %direction)
{
   %currentTile = movementLayer.pickTile(%this.getPosition());
   %nextTile = %currentTile;

   %x = getWord(%currentTile, 0);
   %y = getWord(%currentTile, 1);   
   
   switch$(%direction)
   {
      case "up":
         %nextTile = %x SPC %y--;
      case "down":
         %nextTile = %x SPC %y++;
      case "left":
         %nextTile = %x-- SPC %y;
      case "right":
         %nextTile = %x++ SPC %y;
   }

   %playable = %this.isPlayable(%nextTile);
   
   if(%playable) // && !%this.isMoving)
   {
      %this.nextDirection = %direction;
      %this.nextTile = %nextTile;
   }
}

You'll notice the first function we created was a generic 'onLevelLoaded' function, and in it we create some key-bindings for the 'wasd' keys and bind them to the player.move() function, and pass into it the direction the key represents.

In the player.move() function, the first thing we do is retrieve the tile the player is currently standing on and store it in 'currentTile', then we create a 'nextTile' variable and store the 'currentTile' in it (this is a sanity check, heh).

We then retrieve the individual values of the 'currentTile' vector, and store them in 'x' and 'y' respectively.

After which, we look at the direction the key we pressed represents, then do some simple math and create a new value for our 'nextTile' vector.

We then call our 'isPlayable' function to determine if the 'nextTile' is playable or not, and if it is, we then set our players 'nextDirection' and 'nextTile' values accordingly.

The 'isPlayable' function is where our tiles custom data is evaluated to determine whether or not the tile were attempting to move to is 'playable' (safe to move too) or not. In here, you can write any game specific logic you want, such as in the case of an RPG, the tile could have a custom data value of 'hill' and if the players currently injured, you could determine if their injuries are too great for them to climb the hill or not ... returning 'true' or 'false' to indicate whether they can move to that tile. For the purposes of simplicity, however, we'll stick to using 'playable' and 'non-playable' tiles by simply assuming any tile with custom data is 'non-playable'.

Get the World Position of a Tile

Here, we'll introduce the 't2dTileLayer::getTileWorldPosition(this,x,y)' method, which returns back a t2dVector (x/y) representing the World Position of a given Tile within a Tile Layer.


To determine where in the 'world' a tile is located, we first need to know a few things about the Tile Layer, as well as know what tile in the layer we are trying to retrieve. Let's say we have a 10x10 grid in our Tile Layer, and we want to retrieve the 2nd column, 3rd row (Which would be tile 1,2 since Tile Layers are 0-based).

TGB's Coordinate system is center-based, however, Tile Positions are Top-Left based, so to get the Top-Left of a Tile Layer, we need to do the following:

tlx = px - (sx / 2)
tly = py - (py / 2)

Where 'tlx' is the Top-Left coordinate, and 'sx' is the Size of the tile-layer's X-Axis and 'px' is the Position of the Tile-Layers X-Axis, the same is true for the 'tly', 'py' and 'sy' values, with the exception that they are the Y-Axis.

Now that we have 'x' and 'y' values, which point to the '0,0' position (logical top-left) of our tile-map, we can start doing some additional math to obtain the relative position of the tile within the layer.

If we take the top-left position (0,0), and then add to it the width/height of the tiles multiplied by the tile we want plus one, we can retrieve the relative position.

rtlx = tlx + (tsx * (x + 1))
rtly = tlx + (tsy * (y + 1))

Where 'tsx' and 'tsy' are the values retrieved from getTileSize() then split using getWord(), and 'y' and 'x' are the Tile we are trying to retrieve (3,4).

Now, if we take into account the tile-layers top-left world position ('px' and 'py') and add them to the value we just retrieved:

wx = px + (tlx + (tsx * (x + 1))
wy = py + (tly + (tsy * (y + 1))

We can get the literal world position of the tiles Top-Left position.

Now, the tiles top-left position is fairly useless, since TGB uses a center-based coordinate system, so how do we correct this?

Well, there's two ways we could do this, one would be to change the initial layers top-left to actually be the top-left tile's center point when we initialize the 'tlx' and 'tly' values, like so:

tlx = (((tcx * tsx) / 2) + (tsx / 2))
tly = (((tcy * tsy) / 2) + (tsy / 2))

And another would be to simply add half a tile to the ending world positions equation.

wx = px + (tlx + (tsx * (x + 1)) + (tsx / 2)
wy = py + (tly + (tsy * (y + 1)) + (tsy / 2)

Note, both of these work, and its really up to you to decide which is easier to figure out a few weeks or months, maybe even years down the road when you revisit your code and wonder "what the hell was I thinking?" (especially if you don't work with tile-layer math on a regular basis). I personally prefer the first method, as the second method complicates the final equation and makes it even harder to look at. Simplifying the equations as best you can helps when revisiting the code, takes less brain-cycles to read it.

And, with no further ado, here's the actual function (additional items are added in the actual function, and will be explained after the function has been displayed):

function t2dTileLayer::getTileWorldPosition(%this, %tileXPosition, %tileYPosition)
{
   // determine if we were passed a word-list or x/y coords
   if(getWordCount(%tileXPosition) > 1)
   {
      %tilex = getWord(%tileXPosition, 0);
      %tiley = getWord(%tileXPosition, 1);
   }
   else
   {
      %tilex = %tileXPosition;
      %tiley = %tileYPosition;
   }
   
   // %buff just stores stuff real quick -- I like 'getSize' over 'getSizeX', personal preference
   %buff = %this.getTileSize();
   %tsx = getWord(%buff, 0);
   %tsy = getWord(%buff, 1);
   
   %buff = %this.getTileCount();
   %tcx = getWord(%buff, 0);
   %tcy = getWord(%buff, 1);
   
   %p = %this.getPosition();
   %px = getWord(%p, 0);
   %py = getWord(%p, 1);
   
   // get the top-left
   %tlx = -(((%tcx * %tsx) / 2) + (%tsx / 2));
   %tly = -(((%tcy * %tsy) / 2) + (%tsy / 2));
   
   // determine tile world position
   %wx = %px + (%tlx + (%tsx * (%tilex++)));
   %wy = %py + (%tly + (%tsy * (%tiley++)));
   return %wx SPC %wy;
}


Ok, first up for explanation is the 'if' statement at the top:

   // determine if we were passed a word-list or x/y coords
   if(getWordCount(%tileXPosition) > 1)
   {
      %tilex = getWord(%tileXPosition, 0);
      %tiley = getWord(%tileXPosition, 1);
   }
   else
   {
      %tilex = %tileXPosition;
      %tiley = %tileYPosition;
   }

This statement basically allows us to call our function how ever we want, we can call it as "getTileWorldPosition(x,y)" or "getTileWorldPosition("x y")". Note the second calling syntax was a "vector" (word list). We basically just check to see if the wordCount of tileXPosition is greater then one, and if it is, we assume the first parameter was a vector and split its values to obtain the 'tilex' and 'tiley' values. If 'tileXPosition' only contains one word though, we use the second parameter to store the 'tiley' value.

We do this because if you pass a vector into the function, 'tileYPosition' will not exist, so checking for an empty-string or null value will fail as the parameter itself does not even exist when the function is called in this manner (odd, eh?). This is a good example of creating pseudo-overloads though.

The next few lines:

   // %buff just stores stuff real quick -- I like 'getSize' over 'getSizeX', personal preference
   %buff = %this.getTileSize();
   %tsx = getWord(%buff, 0);
   %tsy = getWord(%buff, 1);
   
   %buff = %this.getTileCount();
   %tcx = getWord(%buff, 0);
   %tcy = getWord(%buff, 1);
   
   %buff = %this.getPosition();
   %px = getWord(%buff, 0);
   %py = getWord(%buff, 1);

With these lines, we reuse the variable 'buff' to temporarily store the Size, Count and Position vectors for later use in our equation.

The rest of the code was explained at the start of this section, with the exception of the "return %wx SPC %wy;" line, which simply returns back a vector which can be used in setPosition(), moveTo() and other spacial movement functions.

Move Player to Tile

Here, we'll introduce the 'moveToTile' method, which combined with the t2dTileLayer::getTileWorldPosition(this, x, y) method, will allow you to move an object to the world position of the given tile.

function t2dSceneObject::moveToTile(%this, %lyr, %x, %y, %spd, %autoStop, %callBack)
{
   %as = false; %cb = false;
   if(!%autoStop) %as = false; else %as = true;
   if(!%callBack) %cb = false; else %cb = true;
   %this.moveTo(%lyr.getTileWorldPosition(%x, %y), %spd, %as, %cb);
}

As you can see, we are using the getTileWorldPosition() function we created in the last section, and this function basically gets added to the function list for every t2dSceneObject in your game.

NEEDS TO BE TESTED: I have not actually verified that the 'as' and 'cb' if-statements work yet (burning DVD's right now ;p)

<i>The first line sets 'as' and 'cb' to false, which are our defaults. We then check the value of 'autoStop' and 'callBack' and then set 'as' and 'cb' to the appropriate value. We do this because of the previously mentioned 'lack of parameter passing' issue, which is both useful and annoying.

If 'callBack' or 'autoStop' are not passed, then they will not exist. If they do not exist, the 'if' statements will fail to have there conditions result in either true OR false, and as such, we store the actual 'autoStop' or 'callBack' values in the 'as' and 'cb' values and pass them along to the 'moveTo' function.

Now we have a 'getTileWorldPosition' function that can tell us 'where in the world' our tile is located, and a 'moveToTile' function that allows any of the objects in our scene to be moved to the position of the tile.

What's next?

Well, we could complicate this function a bit more, and add some of that 'vector' logic we added to getTileWorldPosition() where we check to see if the value 'x' contains 1 or 2 words, and then proceed from there, but that makes this function look really ugly (makes it more useful, and a tad bit more versatile, but ugly looking none-the-less). If you want to do this to your function, go for it, i see no real reason not too, other then perhaps performance in evaluating the functions call stack during game execution -- if your game uses this function repeatedly in short periods of time, and you add the extra 'vector' logic, you may want to make the function work one way or the other, to help stream line the call stack and reduce the CPU cycles required to process the function.