Added a first implementation of the new pathfinder, based on the a* algorithm.

This commit is contained in:
frossi 2000-04-03 12:23:02 +00:00
parent 9241274f33
commit 53bab85ed3
5 changed files with 557 additions and 62 deletions

View file

@ -47,6 +47,12 @@ enum _move_return_ {
extern unsigned char Matrix[(MaxMapWidth+1)*(MaxMapHeight+1)]; /// Path matrix
extern int AStarOn; /// are we using a* or the old path finder
extern int AStarFixedUnitCrossingCost; /// cost associated to move on a tile
/// occupied by a fixed unit
extern int AStarMovingUnitCrossingCost; /// cost associated to move on a tile
/// occupied by a moving unit
/*----------------------------------------------------------------------------
-- Functions
@ -64,6 +70,12 @@ extern int UnitReachable(Unit* unit,Unit* dest);
/// Returns the next element of the path
extern int NextPathElement(Unit*,int* xdp,int* ydp);
//
// in ccl_pathfinder.c
//
/// register ccl features
extern void PathfinderCclRegister(void);
//@}
#endif // !__PATH_FINDER_H__

View file

@ -14,6 +14,6 @@ include $(TOPDIR)/Rules.make
MODULE = pathfinder
OBJS = pathfinder.$(OE) astar.$(OE)
OBJS = pathfinder.$(OE) astar.$(OE) ccl_pathfinder.$(OE)
include $(TOPDIR)/Common.mk

View file

@ -1,13 +1,23 @@
/*
** A clone of a famous game.
*/
// ___________ _________ _____ __
// \_ _____/______ ____ ____ \_ ___ \____________ _/ ____\/ |_
// | __) \_ __ \_/ __ \_/ __ \/ \ \/\_ __ \__ \\ __\\ __\
// | \ | | \/\ ___/\ ___/\ \____| | \// __ \| | | |
// \___ / |__| \___ >\___ >\______ /|__| (____ /__| |__|
// \/ \/ \/ \/ \/
// ______________________ ______________________
// T H E W A R B E G I N S
// FreeCraft - A free fantasy real time strategy game engine
//
/**@name astar.c - The a* path finder routines. */
/*
** (c) Copyright 1999 by Lutz Sammer
** (c) Copyright 1999-2000 by Lutz Sammer and Fabrice Rossi
**
** $Id$
*/
//FIXME: I don't fully understand the range parameter in the Unit
// structure. Something might be broken because of this misunderstanding
//@{
#include <stdio.h>
@ -19,7 +29,8 @@
#include "pathfinder.h"
typedef struct _node_ {
int Direction; /// Direction for trace back
short Direction; /// Direction for trace back
short InGoal; /// is this point in the goal
int CostFromStart; /// Real costs to reach this point
int CostToGoal; /// Aproximated costs until goal
} Node;
@ -31,9 +42,19 @@ typedef struct _open_ {
int Costs; /// complete costs to goal
} Open;
/// heuristic cost fonction for a star
#define AStarCosts(sx,sy,ex,ey) max(abs(sx-ex),abs(sy-ey))
/// cost matrix
local Node AStarMatrix[(MaxMapWidth+1)*(MaxMapHeight+1)];
/// a list of close nodes, helps to speed up the matrix cleaning
local int CloseSet[(MaxMapWidth+1)*(MaxMapHeight+1)];
local int Threshold=(MaxMapWidth+1)*(MaxMapHeight+1)/4;
/// see pathfinder.h
global int AStarFixedUnitCrossingCost=MaxMapWidth*MaxMapHeight;
global int AStarMovingUnitCrossingCost=2;
global int AStarOn=0;
/**
** Prepare path finder.
@ -43,66 +64,287 @@ local void AStarPrepare(void)
memset(AStarMatrix,0,sizeof(AStarMatrix));
}
/**
** Clean up the AStarMatrix
*/
local void AStarCleanUp(int num_in_close)
{
int i;
if(num_in_close>Threshold) {
AStarPrepare();
} else {
for(i=0;i<num_in_close;i++) {
AStarMatrix[CloseSet[i]].CostFromStart=0;
AStarMatrix[CloseSet[i]].InGoal=0;
}
}
}
/**
** The Open set is handled by a Heap stored in a table
** 0 is the root
** node i has left son is at 2*i+1 and right son is at 2*i+2
*/
/// The set of Open nodes
local Open OpenSet[MaxMapWidth*MaxMapHeight/64];
/// The size of the open node set
local int OpenSetSize;
/**
** Find the best node in the current open node set
** Returns the position of this node in the open node set (always 0 in the
** current heap based implementation)
*/
local int AStarFindMinimum()
{
return 0;
}
/**
** Remove the minimum from the open node set (and update the heap)
** pos is the position of the minimum (0 in the heap based implementation)
*/
local void AStarRemoveMinimum(int pos)
{
int i,j,end;
Open swap;
if(--OpenSetSize) {
OpenSet[pos]=OpenSet[OpenSetSize];
// now we exchange the new root with its smallest child until the
// order is correct
i=0;
end=(OpenSetSize>>1)-1;
while(i<=end) {
j=(i<<1)+1;
if(j<OpenSetSize-1 && OpenSet[j].Costs>=OpenSet[j+1].Costs)
j++;
if(OpenSet[i].Costs>OpenSet[j].Costs) {
swap=OpenSet[i];
OpenSet[i]=OpenSet[j];
OpenSet[j]=swap;
i=j;
} else {
break;
}
}
}
}
/**
** Add a new node to the open set (and update the heap structure)
*/
local void AStarAddNode(int x,int y,int o,int costs)
{
int i=OpenSetSize;
int j;
Open swap;
OpenSet[i].X=x;
OpenSet[i].Y=y;
OpenSet[i].O=o;
OpenSet[i].Costs=costs;
OpenSetSize++;
while(i>0) {
j=(i-1)>>1;
if(OpenSet[i].Costs<OpenSet[j].Costs) {
swap=OpenSet[i];
OpenSet[i]=OpenSet[j];
OpenSet[j]=swap;
i=j;
} else {
break;
}
}
}
/**
** Change the cost associated to an open node. The new cost MUST BE LOWER
** than the old one in the current heap based implementation.
*/
local void AStarReplaceNode(int pos,int costs)
{
int i=pos;
int j;
Open swap;
OpenSet[pos].Costs=costs;
// we need to go up, as the cost can only decrease
while(i>0) {
j=(i-1)>>1;
if(OpenSet[i].Costs<OpenSet[j].Costs) {
swap=OpenSet[i];
OpenSet[i]=OpenSet[j];
OpenSet[j]=swap;
i=j;
} else {
break;
}
}
}
/**
** Check if a node is already in the open set.
** Return -1 if not found and the position of the node in the table is found.
*/
local int AStarFindNode(int eo)
{
int i;
for( i=0; i<OpenSetSize; ++i ) {
if( OpenSet[i].O==eo ) {
return i;
}
}
return -1;
}
/**
** Compute the cost of crossing tile (dx,dy)
** -1 -> impossible to cross
** 0 -> no induced cost, except move
** >0 -> costly tile
*/
local int CostMoveTo(int ex,int ey,int mask,int current_cost) {
int j;
Unit* goal;
j=TheMap.Fields[ex+ey*TheMap.Width].Flags&mask;
if (j) {
if(j&~(MapFieldLandUnit|MapFieldAirUnit|MapFieldSeaUnit)) {
// we can't cross fixed units and other unpassable things
return -1;
}
if(current_cost>=AStarFixedUnitCrossingCost) {
// we are already crossing a fixed unit. We don't need details
return AStarMovingUnitCrossingCost;
} else {
goal=UnitOnMapTile(ex,ey);
if( !goal ) {
return -1;//FIXME: is this a bug?
}
if( goal->Moving ) {
// moving unit are crossable
return AStarMovingUnitCrossingCost;
}
// for non moving unit
return AStarFixedUnitCrossingCost;
}
}
// empty tile
return 0;
}
/**
** Find path.
*/
local int AStarFindPath(Unit* unit,int* pxd,int* pyd)
{
int i;
int i,j;
int o;
int x;
int y;
int ex;
int ey;
int num_in_open;
int dx,dy;
int eo,gx,gy,cx,cy,sx;
int b_x,b_y,b_d,b_c;
int r;
int shortest;
int counter;
Open OpenSet[MaxMapWidth*MaxMapHeight/64];
int new_cost;
int last_dir;
int path_length;
int num_in_close=0;
int mask=UnitMovementMask(unit);
int base_mask=mask&~(MapFieldLandUnit|MapFieldAirUnit|MapFieldSeaUnit);
Unit* goal;
static int xoffset[]={ 0,-1,+1, 0, -1,+1,-1,+1 };
static int yoffset[]={ -1, 0, 0,+1, -1,-1,+1,+1 };
DebugLevel0(__FUNCTION__": %Zd %d,%d->%d,%d\n",
DebugLevel3(__FUNCTION__": %Zd %d,%d->%d,%d\n",
UnitNumber(unit),
unit->X,unit->Y,
unit->Command.Data.Move.DX,unit->Command.Data.Move.DY);
AStarPrepare();
OpenSetSize=0;
/* AStarPrepare();*/
x=unit->X;
y=unit->Y;
ex=unit->Command.Data.Move.DX;
ey=unit->Command.Data.Move.DY;
OpenSet[0].X=x; // place start point in open
OpenSet[0].Y=y;
OpenSet[0].O=x*TheMap.Width+y;
OpenSet[0].Costs=AStarCosts(x,y,ex,ey);
AStarMatrix[OpenSet[0].O].CostFromStart=0; // mark in matrix
AStarMatrix[OpenSet[0].O].CostToGoal=OpenSet[0].Costs;
num_in_open=1;
r=unit->Command.Data.Move.Range;
i=0;
// Let's first mark goal
if(unit->Command.Data.Move.Goal) {
goal=unit->Command.Data.Move.Goal;
j=goal->Type->Type;
cx=goal->X;
cy=goal->Y;
ey=UnitTypes[j].TileHeight+r-1;
sx=UnitTypes[j].TileWidth+r-1;
// approximate goal for A*
gx=goal->X+UnitTypes[j].TileHeight/2;
gy=goal->Y+UnitTypes[j].TileWidth/2;
} else {
cx=gx=unit->Command.Data.Move.DX;
cy=gy=unit->Command.Data.Move.DY;
ey=r;
sx=r;
r=0;
}
for(;ey>=-r;ey--){
dy=cy+ey;
if( dy<0 || dy>=TheMap.Height ) {
continue;
}
for(ex=sx;ex>=-r;ex--) {
dx=cx+ex;
if( dx<0 || dx>=TheMap.Width ) {
continue;
}
if(CostMoveTo(dx,dy,mask,AStarFixedUnitCrossingCost)>=0) {
eo=dx*TheMap.Width+dy;
AStarMatrix[eo].InGoal=1;
CloseSet[num_in_close]=eo;
num_in_close++;
i=1;
}
}
}
if(i) {
eo=x*TheMap.Width+y;
AStarMatrix[eo].CostToGoal=AStarCosts(x,y,gx,gy);
// it is quite important to start from 1 rather than 0, because we use
// 0 as a way to represent nodes that we have not visited yet.
AStarMatrix[eo].CostFromStart=1;
// place start point in open
AStarAddNode(x,y,eo,AStarMatrix[eo].CostFromStart+1);
CloseSet[num_in_close]=OpenSet[0].O;
num_in_close++;
b_c=1;
b_d=AStarMatrix[eo].CostToGoal;
b_x=x;
b_y=y;
} else {
AStarCleanUp(num_in_close);
return -2;
}
counter=MaxMapWidth*MaxMapHeight; // how many tries
for( ;; ) {
//
// Find the best node of from the open set
//
for( i=shortest=0; i<num_in_open; ++i ) {
if( OpenSet[i].Costs<OpenSet[shortest].Costs ) {
shortest=i;
}
}
shortest=AStarFindMinimum();
x=OpenSet[shortest].X;
y=OpenSet[shortest].Y;
o=OpenSet[shortest].O;
// remove by inserting the last
OpenSet[shortest]=OpenSet[num_in_open--];
AStarRemoveMinimum(shortest);
//
// If we have reached the goal, then exit.
//
if( x==ex && y==ey ) {
if( AStarMatrix[o].InGoal==1 ) {
ex=x;
ey=y;
DebugLevel3(__FUNCTION__": a star goal reached\n");
break;
}
@ -113,20 +355,23 @@ local int AStarFindPath(Unit* unit,int* pxd,int* pyd)
//
// Select a "good" point from the open set.
// Nearest point to goal.
/*
for( i=shortest=0; i<num_in_open; ++i ) {
if( OpenSet[i].Costs<OpenSet[shortest].Costs ) {
shortest=i;
}
}
*/
DebugLevel0(__FUNCTION__": %Zd way too long\n",UnitNumber(unit));
return 0;
ex=b_x;
ey=b_y;
}
//
// Generate successors of this node.
//
if(AStarMatrix[o].CostToGoal<b_d
|| (AStarMatrix[o].CostToGoal==b_d
&& AStarMatrix[o].CostFromStart<b_c)) {
b_d=AStarMatrix[o].CostToGoal;
b_c=AStarMatrix[o].CostFromStart;
b_x=x;
b_y=y;
}
DebugLevel3("Cost from start: %d %d %d\n",x,y,new_cost);
for( i=0; i<8; ++i ) {
ex=x+xoffset[i];
ey=y+yoffset[i];
@ -140,16 +385,96 @@ local int AStarFindPath(Unit* unit,int* pxd,int* pyd)
if( ey<0 || ey>=TheMap.Height ) {
continue;
}
// if the point is "move to"-able an
// if we have not reached this point before,
// or if we have a better path to it, we add it to open set
new_cost=CostMoveTo(ex,ey,mask,AStarMatrix[o].CostFromStart);
if(new_cost==-1) {
// uncrossable tile
continue;
}
eo=ex*TheMap.Width+ey;
new_cost+=AStarMatrix[o].CostFromStart+1;
if(AStarMatrix[eo].CostFromStart==0) {
// we are sure the current node has not been already visited
AStarMatrix[eo].CostFromStart=new_cost;
AStarMatrix[eo].CostToGoal=AStarCosts(ex,ey,gx,gy);
AStarMatrix[eo].Direction=i;
AStarAddNode(ex,ey,eo,
AStarMatrix[eo].CostFromStart+
AStarMatrix[eo].CostToGoal);
// we add the point to the close set
CloseSet[num_in_close++]=eo;
} else if(new_cost<AStarMatrix[eo].CostFromStart) {
// Already visited node, but we have here a better path
// I know, it's redundant (but simpler like this)
AStarMatrix[eo].CostFromStart=new_cost;
// CostToGoal has not to be updated has it depends only on the
// position, not on the path
// AStarMatrix[eo].CostToGoal=AStarCosts(ex,ey,gx,gy);
AStarMatrix[eo].Direction=i;
// this point might be already in the OpenSet
j=AStarFindNode(eo);
if(j==-1) {
AStarAddNode(ex,ey,eo,
AStarMatrix[eo].CostFromStart+
AStarMatrix[eo].CostToGoal);
} else {
AStarReplaceNode(j,AStarMatrix[eo].CostFromStart+
AStarMatrix[eo].CostToGoal);
}
// we don't have to add this point to the close set
}
}
if( !num_in_open ) { // no new nodes generated
DebugLevel0(__FUNCTION__": %Zd unreachable\n",UnitNumber(unit));
return 0;
if( OpenSetSize<=0 ) { // no new nodes generated
// we go to the best node
ex=b_x;
ey=b_y;
if(ex==x && ey==y) {
DebugLevel3(__FUNCTION__": %Zd unreachable\n",UnitNumber(unit));
AStarCleanUp(num_in_close);
return -2;
}
DebugLevel3(__FUNCTION__": %Zd unreachable: going to closest\n",UnitNumber(unit));
break;
}
}
DebugLevel0(__FUNCTION__": %Zd\n",UnitNumber(unit));
return 0;
// now we need to backtrack
path_length=0;
x=unit->X;
y=unit->Y;
DebugLevel3("%d %d %d %d\n",x,y,ex,ey);
while(ex != x || ey !=y) {
eo=ex*TheMap.Width+ey;
i=AStarMatrix[eo].Direction;
ex-=xoffset[i];
ey-=yoffset[i];
DebugLevel3("%d %d %d %d\n",x,y,ex,ey);
path_length++;
}
*pxd=xoffset[i];
*pyd=yoffset[i];
j=CostMoveTo(ex+*pxd,ey+*pyd,mask,0);
if(j!=0) {
if(j==AStarMovingUnitCrossingCost) {
// we should wait, we are blocked by a moving unit
//FIXME: this might lead to a deadlock, or something similar
DebugLevel3("Unit %Zd waiting. Proposed move: %d %d\n",
UnitNumber(unit),*pxd,*pyd);
path_length=0;
} else {
// j==-1 is a bug, so we should have only
// j==AStarFixedUnitCrossingCost, which means
// the way is blocked by a non moving unit. Waiting is here
// pointless.
path_length=-2;
}
}
// let's clean up the matrix now
AStarCleanUp(num_in_close);
DebugLevel3(__FUNCTION__": %Zd\n",UnitNumber(unit));
DebugLevel3(__FUNCTION__": proposed move: %d %d (%d)\n",*pxd,*pyd,path_length);
return path_length;
}
/**
@ -165,16 +490,55 @@ local int AStarFindPath(Unit* unit,int* pxd,int* pyd)
global int AStarNextPathElement(Unit* unit,int* pxd,int *pyd)
{
// FIXME: Cache for often used pathes, like peons to goldmine.
AStarFindPath(unit,pxd,pyd);
// FIXME: (fabrice) I've copied here the code from NewPath. Is it really
// needed?
int x;
int y;
int r=unit->Command.Data.Move.Range;
Unit* goal=unit->Command.Data.Move.Goal;
UnitType* type;
switch( NewPath(unit,pxd,pyd) ) {
case 0:
return 999;
case 1:
x=unit->X;
y=unit->Y;
if( goal ) { // goal unit
type=goal->Type;
DebugLevel3(__FUNCTION__": Unit %d,%d Goal %d,%d - %d,%d\n"
,x,y
,goal->X-r,goal->Y-r
,goal->X+type->TileWidth+r
,goal->Y+type->TileHeight+r);
if( x>=goal->X-r && x<goal->X+type->TileWidth+r
&& y>=goal->Y-r && y<goal->Y+type->TileHeight+r ) {
DebugLevel3(__FUNCTION__": Goal reached\n");
*pxd=*pyd=0;
return -1;
default:
return -2;
}
} else { // goal map field
if( x>=unit->Command.Data.Move.DX
&& x<=unit->Command.Data.Move.DX+r
&& y>=unit->Command.Data.Move.DY
&& y<=unit->Command.Data.Move.DY+r ) {
DebugLevel3(__FUNCTION__": Field reached\n");
*pxd=*pyd=0;
return -1;
}
// This reduces the processor use,
// If target isn't reachable and were beside it
if( r==0 && x>=unit->Command.Data.Move.DX-1
&& x<=unit->Command.Data.Move.DX+1
&& y>=unit->Command.Data.Move.DY-1
&& y<=unit->Command.Data.Move.DY+1 ) {
if( !CheckedCanMoveToMask(unit->Command.Data.Move.DX
,unit->Command.Data.Move.DY
,UnitMovementMask(unit)) ) { // blocked
DebugLevel3(__FUNCTION__": Field unreached\n");
*pxd=*pyd=0;
return -2;
}
}
}
return AStarFindPath(unit,pxd,pyd);
}
/**
@ -190,7 +554,9 @@ global int AStarNextPathElement(Unit* unit,int* pxd,int *pyd)
global int NextPathElement(Unit* unit,int* pxd,int *pyd)
{
// Convert old version to new version
if( 1 || unit!=Selected[0] ) {
if(AStarOn) {
return AStarNextPathElement(unit,pxd,pyd);
} else {
switch( NewPath(unit,pxd,pyd) ) {
case 0:
return 999;
@ -200,10 +566,6 @@ global int NextPathElement(Unit* unit,int* pxd,int *pyd)
return -2;
}
}
DebugLevel0(__FUNCTION__": %Zd#%s\n",UnitNumber(unit),unit->Type->Ident);
return AStarNextPathElement(unit,pxd,pyd);
}
//@}

View file

@ -0,0 +1,117 @@
// ___________ _________ _____ __
// \_ _____/______ ____ ____ \_ ___ \____________ _/ ____\/ |_
// | __) \_ __ \_/ __ \_/ __ \/ \ \/\_ __ \__ \\ __\\ __\
// | \ | | \/\ ___/\ ___/\ \____| | \// __ \| | | |
// \___ / |__| \___ >\___ >\______ /|__| (____ /__| |__|
// \/ \/ \/ \/ \/
// ______________________ ______________________
// T H E W A R B E G I N S
// FreeCraft - A free fantasy real time strategy game engine
//
/**@name ccl_pathfinder.c - pathfinder ccl functions. */
/*
** (c) Copyright 2000 by Lutz Sammer and Fabrice Rossi
**
** $Id$
*/
//@{
/*----------------------------------------------------------------------------
-- Includes
----------------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "clone.h"
#if defined(USE_CCL) || defined(USE_CCL2) // {
#include "video.h"
#include "tileset.h"
#include "map.h"
#include "sound_id.h"
#include "unitsound.h"
#include "unittype.h"
#include "player.h"
#include "unit.h"
#include "ccl.h"
#include "pathfinder.h"
/*----------------------------------------------------------------------------
-- Functions
----------------------------------------------------------------------------*/
/**
** Enable a*.
*/
local SCM CclAStar(void)
{
AStarOn=1;
return SCM_UNSPECIFIED;
}
/**
** Disable a*.
*/
local SCM CclNoAStar(void)
{
AStarOn=0;
return SCM_UNSPECIFIED;
}
/**
** Set a* parameter (cost of FIXED unit tile crossing).
*/
local SCM CclAStarSetFixedUCC(SCM cost)
{
int i;
i=gh_scm2int(cost);
if( i<=0) {
fprintf(stderr,__FUNCTION__": Fixed unit crossing cost must be strictly positive\n");
i=MaxMapWidth*MaxMapHeight;
}
AStarFixedUnitCrossingCost=i;
return SCM_UNSPECIFIED;
}
/**
** Set a* parameter (cost of MOVING unit tile crossing).
*/
local SCM CclAStarSetMovingUCC(SCM cost)
{
int i;
i=gh_scm2int(cost);
if( i<=0) {
fprintf(stderr,__FUNCTION__": Moving unit crossing cost must be strictly positive\n");
i=1;
}
AStarMovingUnitCrossingCost=i;
return SCM_UNSPECIFIED;
}
/**
** Register CCL features for pathfinder.
*/
global void PathfinderCclRegister(void)
{
gh_new_procedure0_0("a-star",CclAStar);
gh_new_procedure0_0("no-a-star",CclNoAStar);
gh_new_procedure1_0("a-star-fixed-unit-cost",CclAStarSetFixedUCC);
gh_new_procedure1_0("a-star-moving-unit-cost",CclAStarSetMovingUCC);
}
#endif // } defined(USE_CCL) || defined(USE_CCL2)
//@}

View file

@ -47,6 +47,7 @@
#include "ccl_sound.h"
#include "ccl.h"
#include "font.h"
#include "pathfinder.h"
#include <guile/gh.h> // I use guile for a quick hack
@ -79,6 +80,7 @@ extern void sgtk_init_gtk_gdk_glue();
#include "ccl_sound.h"
#include "ui.h"
#include "font.h"
#include "pathfinder.h"
#include "ai.h"
#endif // USE_CCL2
@ -702,6 +704,7 @@ local void gh_main_prog(int argc,char* argv[])
gh_new_procedureN("missile-type",CclMissileType);
MapCclRegister();
PathfinderCclRegister();
UnitButtonCclRegister();
UnitTypeCclRegister();
SoundCclRegister();
@ -812,6 +815,7 @@ global void CclInit(void)
init_lsubr("missile-type",CclMissileType);
MapCclRegister();
PathfinderCclRegister();
UnitButtonCclRegister();
UnitTypeCclRegister();
UpgradesCclRegister();