Jump to content

Vehicle following navpath.


flysouth

Recommended Posts

Looking at the navmesh documentation seems to indicate that the methods have been implemented in the AiPlayer class. What happens if I want an Ai controlled hover vehicle that can follow nav paths for example?

All sane suggestions will be apreciated. :mrgreen:

Link to comment
Share on other sites

This will need conversion work but here's a direct extract from one of our old games for folks to play with based on the resource Jason mentioned:

PT1 - strategic AI (shared across the racers so they all get more agressive the better folks do. 10-1 skippable, but there's references)

#ifndef _AIStrategy_H_
#define _AIStrategy_H_
#ifndef _GAMEBASE_H_
#include "console/simBase.h"
#endif

#include "console/console.h"
#include "console/consoleTypes.h"
#include "core/bitStream.h"

class SAI : public SimObject
{
  typedef SimObject Parent;

public:
F32 mAgression;
F32 mPrecision;

  SAI();
  ~SAI();
  bool onAdd();
  void onRemove();

  DECLARE_CONOBJECT(SAI);
  static void initPersistFields();
};

DECLARE_CONSOLETYPE(SAI)

#endif

#include "AIStrategy.h"

IMPLEMENT_CONOBJECT(SAI);

SAI::SAI()
{
mAgression = 0.1;
mPrecision = 0.1;
}
SAI::~SAI()
{

}
void SAI::initPersistFields()
{
addField("Agression", TypeF32, Offset(mAgression, SAI));
addField("Precision", TypeF32, Offset(mPrecision, SAI));
}

bool SAI::onAdd()
{
  if(!Parent::onAdd())
     return false;
  return true;
}
void SAI::onRemove()
{
Parent::onRemove();
}

Pt 2- our lil bot-heads

// AIWheeledVehicleBrain.h
// Defines a wheeled vehicle that is driven by AI

#ifndef _AIWheeledVehicleBrain_h
#define _AIWheeledVehicleBrain_h

#ifndef _AIStrategy_H_
#include "game/AI/AIStrategy.h"
#endif

#ifndef _WHEELEDVEHICLE_H_
#include "game/vehicles/wheeledVehicle.h"
#endif

#include "stdio.h"

class PathPoint
{
public:
Point3F m_Point;
float m_desiredSpeed;

};

class AIWheeledVehicleBrain : public WheeledVehicle 
{
typedef WheeledVehicle Parent;
public:
enum MoveState {
	ModeStop,
	ModeMove,
	ModeStuck,
	ModeReverse,
};
enum DrivingState {
	SteerNull,
	Left,
	Right,
	Straight,
	TurnAround
};
enum FeelerState {
	FrontFeeler,
	LeftFeeler,
	RightFeeler,
	Open,
};

protected:

//vehicle info
Point3F mVehiclePos;
F32 mCurrentSpeed;

MoveState mMoveState;
F32 mMoveSpeed;
F32 mMoveTolerance;                 // Distance from destination before we stop
Point3F mMoveDestination;           // Destination for movement
Point3F mLastLocation;              // For stuck check
bool mbBetweenLines;
F32 mTurnAngle;
S32 mCurrentNode;
S32 mLastNode;
bool mbInit;
int mTargetNode;

F32 mMovecount;

Point3F mstartPos;

F32 mDistance;
Point3F mIntersection;
VectorF mRacelinedir;
VectorF mVehicledir;

Vector<PathPoint> mPath;
Vector<F32> mvecStop;

F32 followRacingLine();
int CalcPrevNode(int node);
int CalcNextNode(int node);

F32 mStuckTolerance;	//minimal motion before were considered stuck
S32 mMaxStuckCount;		//max cycles before stuck turns into totally stuck
S32 mStuckCycles;		//cycles we've been stuck
S32 mStuckCount;		//times we've exceeded max stuck cycles allowable 
F32 mLostDistance;		//maximum distance before were considered lost
F32 mPrecision;
bool mstart;
void SetLastNode();

// Utility Methods
void throwCallback( const char *name );
virtual bool getAIMove(Move* move);

public:
AIWheeledVehicleBrain();

void setMoveTolerance( const F32 tolerance );
F32 getMoveTolerance() const { return mMoveTolerance; }
void setMoveDestination( const Point3F &location );
Point3F getMoveDestination() const { return mMoveDestination; }
F32 avoidCollisions(void);
void addPathPoint( const Point3F &location, const float &speed);
void setStartNode(int startnode);

void BuildStopVector();
void InitAI();
static void initPersistFields();

F32 getDistanceToNode(U32 node);
void updateVehicleData();

F32 getMaxTurnAngle(F32 currentSpeed);
F32 getSlowDownDistance(F32 currentspeed,F32 desiredspeed);
F32 TurnToRaceLine(Point3F vehicledir,Point3F racelinedir,F32 maxTurnAngle);


// Steering
DrivingState   steerState;
F32   mLastSteered;
F32 getSteeringAngle();

virtual bool  onNewSAI(SAI* dptr);
SAI* mSAI;
bool setSAI(SAI* dptr);
SAI* getSAI()  { return mSAI; }
F32 mAvoidanceTick;
bool mAvoiding;

DECLARE_CONOBJECT(AIWheeledVehicleBrain);
};
bool DistancePointToLine( Point3F Point, Point3F LineStart, Point3F LineEnd, F32 &distance, Point3F &Intersection );
#endif

#include "AIWheeledVehicleBrain.h"
#include "math/mMatrix.h"
#include "math/mPoint.h"
#include "core/realComp.h"

//TWEAKABLE constants
#define STOPSAFETY 15 //How we pad the stop distance value to make sure car can stop in time
#define TURNANGLE 0.5 
#define LOSTDISTANCE 30 //When vehicle is this far treat as lost
#define MAXLOSTSPEED 20 //Max speed when lost

IMPLEMENT_CO_NETOBJECT_V1(AIWheeledVehicleBrain);

AIWheeledVehicleBrain::AIWheeledVehicleBrain() :
WheeledVehicle()
{
  mMoveDestination.set( 0.0f, 0.0f, 0.0f );
  mMoveSpeed = 1.0f;
  mMoveTolerance = 0.25f;
  mbInit = false;
  mDistance = 1e20f;

  mMovecount = 0;
  mCurrentNode = 0;
  mStuckCount = 0;
  mMaxStuckCount = 50;
  mStuckCycles = 0;
  mLostDistance = LOSTDISTANCE;
  mPrecision = 20;
  mStuckTolerance = 0.01f;
  mstart = false;
  mAvoidanceTick = 2;
  mAvoiding = false;
}

void AIWheeledVehicleBrain::initPersistFields()
{
  Parent::initPersistFields();
  addField("StuckTolerance",TypeF32,Offset(mStuckTolerance,AIWheeledVehicleBrain));
  addField("MaxStuckCount",TypeS32,Offset(mMaxStuckCount,AIWheeledVehicleBrain));
  addField("LostDistance",TypeF32,Offset(mLostDistance,AIWheeledVehicleBrain));
  addField("Precision",TypeF32,Offset(mPrecision,AIWheeledVehicleBrain));
  addField("AvoidanceTick",TypeF32,Offset(mAvoidanceTick,AIWheeledVehicleBrain));
}


/**
* Sets how far away from the move location is considered
* "on target"
* 
* @param tolerance Movement tolerance for error
*/
void AIWheeledVehicleBrain::setMoveTolerance( const F32 tolerance )
{
  mMoveTolerance = getMax( 0.1f, tolerance );
}


void AIWheeledVehicleBrain::addPathPoint( const Point3F &location, const float &speed)
{
PathPoint v;
v.m_Point = location;
v.m_desiredSpeed = speed;
mPath.push_back(v);
}

void AIWheeledVehicleBrain::InitAI()
{
BuildStopVector();
mbInit = true;
return;
}

/*	Adjust this function for the performance of your vehicle
mvecStop is a vector that store the stop distance need by the vehicle 
at a certain speed
*/
void AIWheeledVehicleBrain::BuildStopVector()
{
int maxSpeed = 250;
F32 factor = 0.05f;
F32 value = 0;

for (int i=0;i<maxSpeed;i++)
{
	value = (i*i)*factor;
	mvecStop.push_back(value);
}
}

/**
* Sets the location for the bot to run to
*
* @param location Point to run to
*/
void AIWheeledVehicleBrain::setMoveDestination( const Point3F &location)
{
  mMoveDestination = location;
  mMoveState = ModeMove;
}


//Try to have vehicle follow racing line / Path
F32 AIWheeledVehicleBrain::followRacingLine()
{
Point3F v1 = mIntersection-mVehiclePos;
v1.z = 0;
Point3F v2 = mIntersection-mPath[mCurrentNode].m_Point;
v2.z = 0;

v1.normalize();
v2.normalize();
Point3F lv = mCross(v1,v2);
//Use cross product to figure out if we are left or right of racing line
if (lv.z<0)
{
	//vehicle is left of racing line
	return TurnToRaceLine(mVehicledir,mRacelinedir,TURNANGLE);

}
else
{
	//vehicle is right of racing line
	return TurnToRaceLine(mVehicledir,mRacelinedir,-TURNANGLE);
}

return 0;
}

F32 AIWheeledVehicleBrain::TurnToRaceLine(Point3F vehicledir,Point3F racelinedir,F32 maxTurnAngle)
{
Point3F desiredDir;

F32 ratio = mDistance/10;
if (ratio>1)
	ratio = 1;
else if (ratio<(1/10))
	ratio = 0;

//Square this value to have a smaller turn angle as we get closer to the racing line
ratio =ratio *ratio;
F32 turnAngle = ratio*maxTurnAngle;

F32 targetDistance = getDistanceToNode(mTargetNode);

if (targetDistance<mPrecision)
{
	//Just aim right for the node instead of following the racing line
	desiredDir = mPath[mTargetNode].m_Point - mVehiclePos;
	desiredDir.normalize();

	//skip turning
       //return 0.0;
}
else
{
	//rotate about the z axis so that we will turn twords racing line
	desiredDir.x = racelinedir.x*cos(turnAngle)+racelinedir.y*sin(turnAngle);
	desiredDir.y = racelinedir.x*-sin(turnAngle)+racelinedir.y*cos(turnAngle);
	desiredDir.z = 0;
	desiredDir.normalize();
}

F32 dot = mDot(desiredDir,vehicledir);

F32 turn;
//This section of code could use some improvement
//Try to have vehicledir match desiredDir 

//Don't turn
if (dot>0.999)
	turn = 0;
else if (dot>0.995f)
	turn = 0.2f;
else if (dot>0.98f)
	turn = 0.4f;
else if (dot>0.97f)
	turn = 0.6f;
else if (dot>0.96f)
	turn = 0.8f;
else 
	turn = 1;

F32 cz = desiredDir.x*vehicledir.y - desiredDir.y*vehicledir.x;

if (cz<0)
	turn *=-1;

return turn;
}

/**
 Calculates the max turn angle based on current speed
 that won't flip the vehicle
 currently does nothing
*/
F32 AIWheeledVehicleBrain::getMaxTurnAngle(F32 currentSpeed)
{
F32 maxTurnAngle = 50;
return maxTurnAngle;
}

/**
* Calculates the distance needed to slow down to a desired speed
*
*/
F32 AIWheeledVehicleBrain::getSlowDownDistance(F32 currentspeed,F32 desiredspeed)
{
if (currentspeed<=desiredspeed)
	return 0.0;

F32 distance = mvecStop[(int)currentspeed] - mvecStop[(int)desiredspeed];

return distance;
}

void AIWheeledVehicleBrain::setStartNode(int startnode)
{
mCurrentNode = startnode;
SetLastNode();
setMoveDestination(mPath[mCurrentNode].m_Point);
}

int AIWheeledVehicleBrain::CalcNextNode(int node)
{
int nextnode = node+1;
if (nextnode>mPath.size()-1)
	nextnode = 0;

return nextnode;
}

int AIWheeledVehicleBrain::CalcPrevNode(int node)
{
int prevnode = node-1;
if (prevnode<0)
	prevnode = mPath.size()-1;

return prevnode;
}

void AIWheeledVehicleBrain::SetLastNode()
{
mLastNode = CalcPrevNode(mCurrentNode);
}
F32 AIWheeledVehicleBrain::avoidCollisions()
{
disableCollision();
F32 thetaScale = 200.0f;
F32 safeDistance = (getVelocity() * thetaScale).magnitudeSafe();
F32 steering = 0.0;

Point3F start = getPosition();
Point3F feeler,temp;

F32 velocity = getVelocity().len() * thetaScale;

Point3F ForwardRot,LeftRot,RightRot;
getTransform().getColumn(0,&LeftRot);
getTransform().getColumn(1,&ForwardRot);
RightRot = -LeftRot;

   Point3F frontFeeler = (ForwardRot * velocity) + start;
frontFeeler.z = start.z;

Point3F leftFeeler = (LeftRot * velocity) + start;
   leftFeeler.z = start.z;

Point3F rightFeeler = (RightRot * velocity) + start;
   rightFeeler.z = start.z;

// Find closest intersection with wall (static shape) line if any
bool intersectionFound = false;
F32 closestDis;
RayInfo closestRay;
FeelerState closestFeelerState = Open;
U32 avoidanceMask = STATIC_COLLISION_MASK|DAMAGEABLE_MASK;
RayInfo rayInfo_1;
closestRay.distance = rayInfo_1.distance = (frontFeeler-start).len();

if (getContainer()->castRay( start, frontFeeler, avoidanceMask , &rayInfo_1 ))
{
	if (rayInfo_1.distance < closestRay.distance)
	{
		closestRay = rayInfo_1;
		intersectionFound = true;
		closestFeelerState = FrontFeeler;
	}
}
RayInfo rayInfo_2;
rayInfo_2.distance = (leftFeeler-start).len();
if (getContainer()->castRay( start, leftFeeler, avoidanceMask , &rayInfo_2 ))
{
	intersectionFound = true;
	if (rayInfo_2.distance < closestRay.distance)
	{
		closestRay = rayInfo_2;
		closestFeelerState = LeftFeeler;
	}		
}
RayInfo rayInfo_3;
rayInfo_3.distance = (rightFeeler-start).len();
if (getContainer()->castRay( start, rightFeeler, avoidanceMask , &rayInfo_3 ))
{
	intersectionFound = true;
	if (rayInfo_3.distance < closestRay.distance)
	{
		closestRay = rayInfo_3;
		closestFeelerState = RightFeeler;
	}		
}
if (!intersectionFound)
{
	mMoveSpeed = 1.0;
	steering = 0.0;
}
else
{
	switch (closestFeelerState){
		case FrontFeeler:
			mMoveSpeed -= 0.05f;
			if (mMoveSpeed<0.01f) mMoveSpeed = 0.01f;
		case LeftFeeler:
			if (closestRay.distance >0)
			{
				steering = mDataBlock->maxSteeringAngle / (closestRay.distance / safeDistance);
				mMoveSpeed -= 0.01f;
				if (mMoveSpeed<0.01) mMoveSpeed = 0.01f;
				break;
			}
		case RightFeeler:
			if (closestRay.distance >0)
			{
				steering = -mDataBlock->maxSteeringAngle / (closestRay.distance / safeDistance);
				mMoveSpeed -= 0.01f;
				if (mMoveSpeed<0.01f) mMoveSpeed = 0.01f;
				break;
			}
	}
}
enableCollision();
return steering;
}
void AIWheeledVehicleBrain::updateVehicleData()
{
  //Update vehicle data
  mVehiclePos = getPosition();
  mCurrentSpeed = fabs(getVelocity().len());

MatrixF mat = getTransform();

VectorF vehicledir;
vehicledir.set(0,1,0);
mat.mulV(vehicledir);
//ignore the z part because we can't fix that
vehicledir.z = 0;
vehicledir.normalize();

mVehicledir = vehicledir;

Point3F pt1 = mPath[mCurrentNode].m_Point;
Point3F pt2 = mPath[mLastNode].m_Point;

mbBetweenLines = false;
//Figure distance and intersection to racing line
mTargetNode = mCurrentNode;
bool b = true;
if (!DistancePointToLine(mVehiclePos,pt2,pt1,mDistance,mIntersection))
{
	//Try the next node
	mTargetNode = CalcNextNode(mCurrentNode);
	pt1 = mPath[mTargetNode].m_Point;
	pt2 = mPath[mCurrentNode].m_Point;
	if (DistancePointToLine(mVehiclePos,pt2,pt1,mDistance,mIntersection))
	{
		mLastNode = mCurrentNode;
		mCurrentNode = mTargetNode;
		mbBetweenLines = true;
	}
	else
	{
		//In between line segments
		Point3F inter = mPath[mTargetNode].m_Point;
		mDistance = getDistanceToNode(mTargetNode);
		mbBetweenLines = true;
	}
}

F32 distance = getDistanceToNode(mTargetNode);
//pretty basic right now. need to add better conditions to being 'stuck'
if (mCurrentSpeed < mStuckTolerance)
{
	mStuckCount++;
	if (mStuckCount>mMaxStuckCount)
	{
		mStuckCycles++;
		if (mStuckCycles>mMaxStuckCount)
		{
			mStuckCount = 0;
			mStuckCycles = 0;
			Con::executef(this, 1, "onStuck");
		}
	}
	else mMoveState = ModeMove;
	if (distance>mLostDistance)
	{
		mStuckCycles = 0;
		mStuckCount = 0;
		Con::executef(this, 1, "onLost");
	}
}
else mStuckCycles = mStuckCount = 0;
if (distance<mPrecision)
{
	//Use the next node
	mTargetNode = CalcNextNode(mCurrentNode);
	pt1 = mPath[mTargetNode].m_Point;
	pt2 = mPath[mCurrentNode].m_Point;
}

VectorF racelinedir;
racelinedir = pt1-pt2;
racelinedir.normalize();
mRacelinedir = racelinedir;
}

// Think - figure out speed(accelerate or apply brakes) and steer the vehicle
bool AIWheeledVehicleBrain::getAIMove(Move *movePtr)
{
   if (!mbInit)
	return false;

if (mDisableMove)
{
  mRigid.setAtRest();
  return true;
}

  *movePtr = NullMove;

  updateVehicleData();
Point3F pt1 = mPath[mCurrentNode].m_Point;
Point3F pt2 = mPath[mLastNode].m_Point;

// Orient towards our destination.
mMovecount++;
if (mMovecount > mAvoidanceTick)
{
	mMovecount = 1;
	movePtr->yaw = avoidCollisions();
	movePtr->y = mMoveSpeed;
}
else
{
	if (mMoveState == ModeMove || mMoveState == ModeReverse)
	{
		mTurnAngle = followRacingLine();
		movePtr->yaw = mTurnAngle;
	}
	// Move towards the destination
	if (mMoveState == ModeMove)
	{
		movePtr->y = mMoveSpeed;
		setMoveDestination(mPath[mCurrentNode].m_Point);
		//Do we need to slow down or speed up?
		F32 targetSpeed = mPath[mCurrentNode].m_desiredSpeed;
		if (mCurrentSpeed>targetSpeed * mSAI->mAgression)
		{
			F32 distance = getDistanceToNode(mCurrentNode);
			F32 slowdistance = getSlowDownDistance(mCurrentSpeed,mPath[mCurrentNode].m_desiredSpeed);
			if (distance<(slowdistance+STOPSAFETY))
			{
				movePtr->y = 0.0;
				movePtr->trigger[2] = true;
			}
			else
				if ((mPath[mCurrentNode].m_desiredSpeed>mCurrentSpeed)&&(mPath[mLastNode].m_desiredSpeed>mCurrentSpeed))
				{
					movePtr->y = 0.0;
					movePtr->trigger[2] = true;
				}
				else
				{
					if ((mPath[mCurrentNode].m_desiredSpeed<999)&&(mPath[mLastNode].m_desiredSpeed<999))
					{
						movePtr->y = mMoveSpeed;
					}
					else
					{
						movePtr->y = mMoveSpeed;
					}
				}
		}
		else
		{
			movePtr->y = mMoveSpeed;
		}
	}
	else if(mMoveState == ModeReverse)
	{
		movePtr->y = -1 * mMoveSpeed;
	}
	else if(mMoveState == ModeStop)
	{
		movePtr->y = 0;
	}
}
//Don't apply gas if vehicle is badly sliding
Point3F vel = getVelocity();
vel.z = 0;
vel.normalize();
F32 d = mDot(vel,mVehicledir);
bool bSlide = false;
if (mCurrentSpeed>400)
{
	if (d<0.95)
	{
		movePtr->y = 0;
		movePtr->trigger[2] = true;
	}
}
// Replicate the trigger state into the move so that
// triggers can be controlled from scripts.
for( int i = 0; i < MaxTriggerKeys; i++ )
	movePtr->trigger[i] = getImageTriggerState(i);
return true;
}

F32 AIWheeledVehicleBrain::getDistanceToNode(U32 node)
{
Point3F v = mVehiclePos-mPath[node].m_Point;
return v.len();
}
/**
* Utility function to throw callbacks. Callbacks always occure
* on the datablock class.
*
* @param name Name of script function to call
*/
void AIWheeledVehicleBrain::throwCallback( const char *name )
{
  Con::executef(getDataBlock(), 2, name, scriptThis());
}



//Perpendicular distance from point to line
//returns false if the point is not perpendicular to line
//Ignores the z value
bool DistancePointToLine( Point3F Point, Point3F LineStart, Point3F LineEnd, F32 &distance, Point3F &Intersection )
{
   F32 LineMag;
   F32 U;
//Ignore z value
Intersection.set(0,0,0);
Point.z = 0;
LineEnd.z = 0;
LineStart.z = 0;

Point3F l = LineEnd-LineStart;
LineMag = l.len();
if (LineMag!=0.0f)
{
	U = ( ( ( Point.x - LineStart.x ) * ( LineEnd.x - LineStart.x ) ) +
		( ( Point.y - LineStart.y ) * ( LineEnd.y - LineStart.y ) ))/
		( LineMag * LineMag );
}
   if( U < 0.0f || U > 1.0f )
       return false;   // closest point does not fall within the line segment

   Intersection.x = LineStart.x + U * ( LineEnd.x - LineStart.x );
   Intersection.y = LineStart.y + U * ( LineEnd.y - LineStart.y );

Point3F l2 = Point - Intersection;
   distance = l2.len();

return true;
}
// --------------------------------------------------------------------------------------------
// Console Functions
// --------------------------------------------------------------------------------------------

ConsoleMethod( AIWheeledVehicleBrain, init, void, 2, 2, "()"
             "Initialize AI for vehicle")
{
object->InitAI();
}


ConsoleMethod( AIWheeledVehicleBrain, setStartNode, void, 3, 3, "( int startnode )"
             "Sets the move speed for an AI object.")
{
  object->setStartNode( dAtoi( argv[2] ) );
}

ConsoleMethod( AIWheeledVehicleBrain, setMoveTolerance, void, 3, 3, "(float speed)" "Sets the movetolerance")
{
object->setMoveTolerance(dAtof(argv[2]));
}

ConsoleMethod( AIWheeledVehicleBrain, addPathPoint, void, 4, 4, "(Point3F goal)(float speed)"
             "Add a point to the vehicle travel path")
{
float speed = 0;
  Point3F v( 0.0f, 0.0f, 0.0f );
  dSscanf( argv[2], "%f %f %f", &v.x, &v.y, &v.z );
  dSscanf( argv[3], "%f", &speed );
  object->addPathPoint( v, speed );
}

ConsoleMethod( AIWheeledVehicleBrain, getMoveDestination, const char *, 2, 2, "()"
             "Returns the point the AI is set to move to.")
{
  Point3F movePoint = object->getMoveDestination();

  char *returnBuffer = Con::getReturnBuffer( 256 );
  dSprintf( returnBuffer, 256, "%f %f %f", movePoint.x, movePoint.y, movePoint.z );

  return returnBuffer;
}

//==============================================================================================
bool AIWheeledVehicleBrain::setSAI(SAI* dptr)
{
  if (isGhost() || isProperlyAdded()) {
     if (mSAI != dptr)
        return onNewSAI(dptr);
  }
  else
     mSAI = dptr;
  return true;
}

bool AIWheeledVehicleBrain::onNewSAI(SAI* dptr)
{
  mSAI = dptr;

  if (!mSAI)
     return false;

  setMaskBits(DataBlockMask);
  return true;
}
//----------------------------------------------------------------------------
ConsoleMethod( AIWheeledVehicleBrain, getSAI, S32, 2, 2, "()"
             "Return the SAI this AIWheeledVehicleBrain is using.")
{
  return object->getSAI()? object->getSAI()->getId(): 0;
}

//----------------------------------------------------------------------------
ConsoleMethod(AIWheeledVehicleBrain, setSAI, bool, 3, 3, "(SAI db)"
             "Assign this AIWheeledVehicleBrain to use the specified SAI.")
{
  SAI* data;
  if (Sim::findObject(argv[2],data)) {
     return object->setSAI(data);
  }
  Con::errorf("Could not find SAI Template \"%s\"!",argv[2]);
  return false;
}

 

Not working on a racing game this time around, so hope it helps.

Edited by Azaezel
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...