View Single Post
  #1  
Old 07-29-2011, 12:51 PM
xXziroXx xXziroXx is offline
Malorian
xXziroXx's Avatar
Join Date: May 2004
Posts: 5,289
xXziroXx has a brilliant futurexXziroXx has a brilliant futurexXziroXx has a brilliant futurexXziroXx has a brilliant futurexXziroXx has a brilliant futurexXziroXx has a brilliant futurexXziroXx has a brilliant future
A* Movement System

With this code, you can tell an NPC to move from point A to point B, and it'll calculate the best path using the Manhattan method. If you want to learn how this system works, I recommend reading this.

The only parts you might need to customize can be found in the onCreated() function. The system can be used both serverside and clientside.


Class: aStar
PHP Code:
//#CLIENTSIDE
function onCreated()
{
  
// The following settings are adjusted for useage with a "normal" showCharacter() and can be changed to your likings.
  
enum {
    
WIDTH_IN_TILES = 2.5,
    
HEIGHT_IN_TILES = 2,
    
MOVEMENT_SPEED = 0.1,
    
X_OFFSET = 0,
    
Y_OFFSET = 1,
    
NORMAL_GSCORE = 10,
    
DIAGONAL_GSCORE = 14
  
};
  
  
showCharacter();
}

function 
moveTo(temp.destinationX, temp.destinationY)
{
  if (
onwall2(temp.destinationX + X_OFFSET, temp.destinationY + Y_OFFSET, WIDTH_IN_TILES, HEIGHT_IN_TILES))
    return 
this.chat = "Error: blocking!";
  
  
this.chat = "";
  
  
temp.startX = this.x;
  
temp.startY = this.y;
  
temp.identifier = temp.startX @ "_" @ temp.startY;
  
  
this.closedNodes = new TStaticVar();
  
this.openNodes   = new TStaticVar();
  
this.openNodes.(@temp.identifier).nodeX = temp.startX;
  
this.openNodes.(@temp.identifier).nodeY = temp.startY;
  
this.openNodes.(@temp.identifier).parent = "";
  
  
processNodes(temp.destinationX, temp.destinationY);
}

function 
processNodes(temp.destinationX, temp.destinationY)
{  
  
temp.lowestFScore = getLowestFScore();
  
temp.node.copyFrom(this.openNodes.(@temp.lowestFScore));
  
  
temp.pathFound = closeNode(temp.lowestFScore, temp.destinationX, temp.destinationY);
  
checkSurroundingNodes(temp.node.nodeX, temp.node.nodeY, temp.lowestFScore, temp.destinationX, temp.destinationY);
  
  if (!
temp.pathFound)
    
processNodes(temp.destinationX, temp.destinationY);
  else
    
followPath(temp.lowestFScore);
}

function 
checkSurroundingNodes(temp.startX, temp.startY, temp.parentNode, temp.destinationX, temp.destinationY)
{
  
temp.surroundingNodes = {
    { 
temp.startX - 1, temp.startY - 1 }, // top left
    
{ temp.startX, temp.startY - 1 },     // top middle
    
{ temp.startX + 1, temp.startY - 1 }, // top right
    
{ temp.startX + 1, temp.startY },     // middle right
    
{ temp.startX + 1, temp.startY + 1 }, // bottom right
    
{ temp.startX, temp.startY + 1 },     // bottom middle
    
{ temp.startX - 1, temp.startY + 1 }, // bottom left
    
{ temp.startX - 1, temp.startY },     // middle left
  
};
  
  
temp.counter = 0;
  for (
temp.node: temp.surroundingNodes) {
    
temp.nodeX = temp.node[0];
    
temp.nodeY = temp.node[1];
    
temp.identifier = temp.nodeX @ "_" @ temp.nodeY;
    
    
temp.isDiagonal = (temp.counter%2 == 0 ? true : false);
    
temp.nodeOnWall = onwall2(temp.nodeX + X_OFFSET, temp.nodeY + Y_OFFSET, WIDTH_IN_TILES, HEIGHT_IN_TILES);
    
    
temp.gScore = (temp.isDiagonal ? DIAGONAL_GSCORE : NORMAL_GSCORE);
    
temp.hScore = getNodeDistance(temp.nodeX, temp.nodeY, temp.destinationX, temp.destinationY) * 10;
    
temp.fScore = temp.gScore + temp.hScore;
    
    
temp.counter ++;
    
    
// On a wall? Ignore.
    
if (temp.nodeOnWall)
      continue;
    
// Closed already? Ignore.
    
if (temp.identifier in this.closedNodes.getDynamicVarNames())
      continue;
    
// On the open list already?
    
if (temp.identifier in this.openNodes.getDynamicVarNames()) {
      
// Is this path better?
      
if (temp.gScore >= this.openNodes.(@temp.identifier).gScore)
        continue;
    }
    
    
this.openNodes.(@temp.identifier).nodeX = temp.nodeX;
    
this.openNodes.(@temp.identifier).nodeY = temp.nodeY;
    
this.openNodes.(@temp.identifier).parentNode = temp.parentNode;
    
this.openNodes.(@temp.identifier).gScore = temp.gScore;
    
this.openNodes.(@temp.identifier).hScore = temp.hScore;
    
this.openNodes.(@temp.identifier).fScore = temp.fScore;
    
    if (
this.lowestFScore == "" || temp.fScore < this.openNodes.(@this.lowestFScore).fScore || this.lowestFScore in this.closedNodes.getDynamicVarNames())
      
this.lowestFScore = temp.identifier;
  }
}

function 
closeNode(temp.identifier, temp.destinationX, temp.destinationY)
{  
  if (
temp.identifier == this.lowestFScore)
    
this.lowestFScore = "";
  
  
temp.node.copyFrom(this.openNodes.(@temp.identifier));
  
this.openNodes.(@temp.identifier).clearVars();
  
this.openNodes.clearEmptyVars();
  
  if (
temp.identifier in this.closedNodes.getDynamicVarNames())
    return;
  
  
this.closedNodes.(@temp.identifier).copyFrom(temp.node);
  
this.closedNodes.(@temp.identifier).pathIndex = this.closedNodes.getDynamicVarNames().size() - 1;
  
  if (
this.closedNodes.(@temp.identifier).nodeX == temp.destinationX
   
&& this.closedNodes.(@temp.identifier).nodeY == temp.destinationY)
    return 
true;
}

function 
getNodeDistance(temp.nodeX, temp.nodeY, temp.destinationX, temp.destinationY)
{
  
temp.distance = ((temp.nodeX - temp.destinationX)^2 + (temp.nodeY - temp.destinationY)^2)^0.5;
  return 
int(temp.distance);
}

function 
getLowestFScore()
{
  if (
this.lowestFScore != "")
    return 
this.lowestFScore;
  
  for (
temp.identifier: this.openNodes.getDynamicVarNames()) {
    
temp.node.copyFrom(this.openNodes.(@temp.identifier));
    
    if (
temp.lowestFScore == "" || temp.node.fScore < this.openNodes.(@temp.lowestFScore).fScore)
      
temp.lowestFScore = temp.identifier;
  }
  
  return 
temp.lowestFScore;
}

function 
followPath(temp.destinationNode)
{
  
temp.nodeList = getPath(temp.destinationNode);
  
  for (
temp.identifier: temp.nodeList) {
    
temp.node.copyFrom(this.closedNodes.(@temp.identifier));
    
    
setCharAni("walk", "");
    
move(temp.node.nodeX - this.x, temp.node.nodeY - this.y, MOVEMENT_SPEED, 16);
    
sleep(MOVEMENT_SPEED);
  }
  
  
this.trigger("MovementFinished", "");
}

function 
getPath(temp.destinationNode)
{
  
temp.nodeList = new[0];
  
temp.nodeList.insert(0, temp.destinationNode);
  
temp.identifier = temp.destinationNode;
  
  while (
temp.identifier != "") {
    
temp.node.copyFrom(this.closedNodes.(@temp.identifier));
    if (
temp.node.parentNode == "")
      break;
    
    if (
temp.node.parentNode != this.x @ "_" @ this.y)
      
temp.nodeList.insert(0, temp.node.parentNode);
    
    
temp.identifier = temp.node.parentNode;
  }
  
  return 
temp.nodeList;
} 

Usage example:
PHP Code:
//#CLIENTSIDE
function onCreated()
{
  
this.join("aStar");  
  
showCharacter();
  
moveTo(48, 8);
}

function 
onMovementFinished()
{
  
setCharAni("idle", "");
  
this.movementCounter ++;
  
  
this.dir = 2;
  
this.chat = "Movement finished!";
  
sleep(1);
  
this.chat = "";
  
  if (
this.movementCounter == 1)
    
moveTo(45, 29);
  else if (
this.movementCounter == 2)
    
moveTo(50, 53);
  else if (
this.movementCounter == 3)
    
moveTo(25, 47);
  else if (
this.movementCounter == 4)
    
moveTo(8, 19);
} 

Here's a video showing an example of usage. Note that in a lot of cases, it's better to split up the movement sequence in several shorter routes than one big, especially when your target position is close to the start from a birds eye point of view but still so far away.

The movement looks a bit choppy in the video, but it's not in-game.

__________________
Follow my work on social media post-Graal:Updated august 2025.
Reply With Quote