Jump to content

Chris Paulson

Members
  • Posts

    134
  • Joined

  • Last visited

Blog Entries posted by Chris Paulson

  1. Chris Paulson
    I've being working on a locomotion/animation system that will provide nice results and gives good transitions from one stance/animation to the next. I was inspired by articles I read on the Internet and the Unity locomotion video. Luckily the person who did the Unity locomotion system posted his thesis on the Internet so I could read some of the concepts and then do futher study of the concepts from the internet.
     
    The Unity system is highly mathimatical/analytical and because of this I have not done an exact copy, as to be honest it's out of my league, I'm not bright and my maths is poor. I've therefore implemented a system that requires less maths.
     
    Here's an overview of what I've done.
     
     
    The Problem
    -----------
     
    To get an nice animation of someone standing still then breaking into a full run you need to blend idle into walk, into run, giving each one an ever increasing/decreasing blend weight.
     
    For example:-
    Idle weight 1 walk weight 0
    .
    .
    .
    Idle weight 0.75 walk weight 0.25
    .
    .
    .
    Idle weight 0 walk weight 1
    .
    .
    etc etc
     
     
    However to blend a run and a walk together you need to synchronise the animations so the legs are in a similar position, otherwise the blend may cancel each other out and you'd get animation artifacts.
     
    To solve the problem you need to be able to take a leg position in the walk animation and match the most similar position in the run animation.
     
    The solution
    -------------
     
    I have broken down walking/running into the following stance components:-
     
    Land - When the foot is flat on the floor (toe and heel on floor)
    Strike - When the heel strikes the floor
    Lift - When the heel leaves the floor (toe only on the floor)
    Flight - The foot is not on the floor
     
    I pre analyse the animations and work out where each stance start/stop frame is and it's length. This can then be used for synchronisation. I also work out the travel distance of a animation so I can accurately move a model/alter playback speed depending on it's movement speed and eliminate foot skate.
     
    Now for example if my animation was 75% of the way through a strike stance I can sync in the run animation 75% of the way through the same stance for the same leg.
     
     
    What blending gives me
    ----------------------
     
    Instead of just having a idle, walk and run animation using blended weights I procedurely have the equivlent of 100's of animations between idle and run.
     
     
    What next
    ---------
     
    I've cracked (in a none refined way) blending based on speed, I can now move onto blending based on turning angle. For example: -
     
    If I was walking and turning at 45 degrees and I had an animation which did a turn of 90 degrees I would play 50% weight of walk and 50% of turn animation giving me the required angle. Hope the next stage works, wish me luck...
     
     
     
    Video of walk to run (to follow when upload done)
    ---------------------
    (I've only got 30 seconds of fraps)
    It might not look very sexy but it taken quite a few week of coding effort to get to this stage.
    In the text stats at the top it displays: -
    The master animation (the one with the hightest weight)
    The playback speed of each animation (controlled by the speed the model would be moving)
    The calculated weights and the calculated stance for each leg.
     


  2. Chris Paulson
    To get nice locomotion and not have NPC walking with feet stuck in the terrain or hovering in the air you need to place the feet to match the terrain, coping with slopes etc.
     
    Leadwerks has forward kinematics but not inverse (which is needed to do this). Because of this I have written (with help) an IK solver.
     
    Thanks to Tyler for supplying the code as a starting point.
     
    Here's a video of it working: -


     
    Here's the code (I actually ended up doing 2 solvers)
     

    #include "IKSimple.h" #include "engine.h" float IKSolver::positionAccuracy = 0.001f; int IKSimple::maxIterations = 100; float IKSimple::calcBonesLength() { boneLength.resize(bones.size()-1); float totalLength = 0.0f; for (int j = 0; j < (bones.size() - 1); j++) { Vector3 vector = bones[j + 1].localPosition() - bones[j].localPosition(); boneLength[j] = vector.Magnitude(); totalLength += boneLength[j]; } this->positionAccuracy = totalLength * 0.001f; return totalLength; } void IKSimple::Solve( TEntity entity, std::vector<Transform> pbones, Vector3 target) { /// Local Variables /////////////////////////////////////////////////////////// Vector3 rootPos,curEnd,desiredEnd,targetVector,curVector,crossResult; double cosAngle,turnAngle,turnDeg; int link,tries; Quaternion aquat; /////////////////////////////////////////////////////////////////////////////// // START AT THE LAST LINK IN THE CHAIN bones = pbones; startBone = &pbones[0]; endBone = &pbones[pbones.size() - 1]; link = pbones.size() - 1; tries = 0; // LOOP COUNTER SO I KNOW WHEN TO QUIT do { // THE COORDS OF THE X,Y,Z POSITION OF THE ROOT OF THIS BONE IS IN THE MATRIX // TRANSLATION PART WHICH IS IN THE 12,13,14 POSITION OF THE MATRIX rootPos = pbones[link].localPosition(); // POSITION OF THE END EFFECTOR curEnd = endBone->localPosition(); // DESIRED END EFFECTOR POSITION desiredEnd = target; // SEE IF I AM ALREADY CLOSE ENOUGH if ( curEnd.DistanceSquared(desiredEnd) > positionAccuracy) { // CREATE THE VECTOR TO THE CURRENT EFFECTOR POS curVector = curEnd - rootPos; // CREATE THE DESIRED EFFECTOR POSITION VECTOR targetVector = target - rootPos; // NORMALIZE THE VECTORS (EXPENSIVE, REQUIRES A SQRT) curVector.Normalize(); targetVector.Normalize(); // THE DOT PRODUCT GIVES ME THE COSINE OF THE DESIRED ANGLE cosAngle = targetVector.Dot( curVector); // IF THE DOT PRODUCT RETURNS 1.0, I DON'T NEED TO ROTATE AS IT IS 0 DEGREES if (cosAngle < 0.99999) { // USE THE CROSS PRODUCT TO CHECK WHICH WAY TO ROTATE crossResult = curVector.Cross(targetVector); crossResult.Normalize(); turnAngle = acos((float)cosAngle); // GET THE ANGLE turnDeg = rad2deg(turnAngle); // COVERT TO DEGREES // DAMPING turnDeg *= pbones[link].m_damper; aquat = Quaternion::Quaternion(crossResult, turnDeg ); aquat = pbones[link].boneLocalRotation() * aquat; pbones[link].setLocalRotation( aquat ); } if (--link < 0) link = pbones.size() - 1; // START OF THE CHAIN, RESTART } // QUIT IF I AM CLOSE ENOUGH OR BEEN RUNNING LONG ENOUGH } while (tries++ < maxIterations && curEnd.DistanceSquared( desiredEnd) > positionAccuracy); } void IKSimple::calcCurrentBones( ) { rotateArray.resize(bones.size()-2); angles.resize(bones.size()-2); quaternionArray.resize(bones.size()-2); // Work out where current bones are for (int i = 0; i < (bones.size() - 2); i++) { rotateArray[i] = (bones[i + 1].localPosition() - bones[i].localPosition()).Cross(bones[i + 2].localPosition() - bones[i + 1].localPosition()); rotateArray[i] = (Vector3) ((bones[i].localRotation().Inverse()) * rotateArray[i]); rotateArray[i].Normalize(); angles[i] = (bones[i + 1].localPosition() - bones[i].localPosition() ).Angle(bones[i + 1].localPosition() - bones[i + 2].localPosition() ); quaternionArray[i] = bones[i + 1].localRotation(); } } void IKSimple::SolveRelative( TEntity entity, std::vector<Transform> pbones, Vector3 target) { float doLow; float doHigh; bones = pbones; startBone = &bones[0]; endBone = &bones[bones.size() - 1]; // Work out length of bones float totalLength = calcBonesLength(); Vector3 curdis = endBone->localPosition() - startBone->localPosition(); float currentDistance = curdis.Magnitude(); Vector3 vtarget = target - startBone->localPosition(); float targetDistance = vtarget.Magnitude(); bool minFound = false; bool moreToDo = false; if (targetDistance > currentDistance) { minFound = true; doHigh = 1.0f; doLow = 0.0f; } else { moreToDo = true; doHigh = 1.0f; doLow = 0.0f; } int currentIter = 0; while ((abs((float) (currentDistance - targetDistance)) > this->positionAccuracy) && (currentIter < this->maxIterations)) { float newBend; currentIter++; if (!minFound) newBend = doHigh; else newBend = (doLow + doHigh) / 2.0f; for (int i = 0; i < (bones.size() - 2); i++) { float calcAngle; if (!moreToDo) { calcAngle = math::Lerp(180.0f, angles[i], newBend); } else { calcAngle = (angles[i] * (1.0f - newBend)) + ((angles[i] - 30.0f) * newBend); } float angleDiff = angles[i] - calcAngle; Quaternion newRot = Quaternion::Quaternion(rotateArray[i], angleDiff ); newRot = quaternionArray[i] * newRot; bones[i + 1].setLocalRotation( newRot ); } Vector3 totalLen = endBone->localPosition() - startBone->localPosition(); currentDistance = totalLen.Magnitude(); if (targetDistance > currentDistance) minFound = true; if (minFound) { if (targetDistance > currentDistance) doHigh = newBend; else doLow = newBend; if (doHigh < 0.01f) break; } else { doLow = doHigh; doHigh++; } } // Change master bone (if we was doing a leg this would be the hip) float hipAngle = (endBone->localPosition() - startBone->localPosition()).Angle(target - startBone->localPosition()); Vector3 hipAxis = (endBone->localPosition() - startBone->localPosition()).Cross(target - startBone->localPosition()); AngleAxis hipAngleAxis = AngleAxis( hipAxis, hipAngle ); Quaternion hipRot = startBone->localRotation() * Quaternion::Quaternion( hipAngleAxis ); startBone->setLocalRotation( hipRot ); }
     
     
    Here's the transform object which controls the bones. It was a little bit tricky to do because LE annoyingly does local rotation relative to the parent bone not relative to the model.
     

    #ifndef TRANSFORM_H #define TRANSFORM_H /// @file locomotion\tranform.h /// @brief This is the transform object which handles rotation/positioning of bones /// for the IK solver /// #include "SVector3.h" #include "SQuaternion.h" class Transform { public: Transform() {}; Transform(TEntity parent, TEntity entity) : m_parent(parent) { m_entity = entity; TVec3 v; v = EntityScale(entity); m_scale = Vector3(v.X,v.Y,v.Z); v = Vec3(0,1,0); v = TFormVector(v, entity, NULL); m_up = Vector3(v.X,v.Y,v.Z); v = Vec3(0,0,1); v = TFormVector(v, entity, NULL); m_forward = Vector3(v.X,v.Y,v.Z); v = Vec3(1,0,0); v = TFormVector(v, entity, NULL); m_right = Vector3(v.X,v.Y,v.Z); m_damper = 1; m_minRotation = Vector3(-360,-360,-360); m_maxRotation = Vector3(360,360,360); } inline Quaternion boneLocalRotation() { TVec3 v; v = EntityRotation(m_entity,0); Quaternion q = Quaternion( Vector3(v) ); return q; } inline Quaternion localRotation() { TVec3 v; TEntity par = GetParent( m_entity ); EntityParent( m_entity, m_parent ); v = EntityRotation(m_entity,0); EntityParent( m_entity, par ); Quaternion q = Quaternion( Vector3(v) ); return q; } inline Quaternion rotation() { TVec3 v; v = EntityRotation(m_entity,1); Quaternion q1 = Quaternion(Vector3(v)); return q1; } inline void alignToVector( Vector3 v, int axis = 3, int rate = 1, int roll = 0 ) { AlignToVector( m_entity, v.vec3(), axis, rate, 0); } inline void setLocalRotation( Quaternion rot ) { Vector3 vrot; vrot = rot.eulerAngles(); vrot.X = __max( vrot.X, m_minRotation.X ); vrot.Y = __max( vrot.Y, m_minRotation.Y ); vrot.Z = __max( vrot.Z, m_minRotation.Z ); vrot.X = __min( vrot.X, m_maxRotation.X ); vrot.Y = __min( vrot.Y, m_maxRotation.Y ); vrot.Z = __min( vrot.Z, m_maxRotation.Z ); RotateEntity( m_entity, vrot.vec3(), 0); } inline void setRotation( Quaternion rot ) { Vector3 vrot; vrot = rot.eulerAngles(); RotateEntity( m_entity, vrot.vec3(), 1); } inline Vector3 position() { TVec3 v; v = EntityPosition(m_entity,1); v = TFormPoint(v ,NULL, m_parent); return Vector3( v ); } inline Vector3 localPosition() { TVec3 v; v = EntityPosition(m_entity, 1); v = TFormPoint(v, NULL, m_parent); return Vector3( v ); } TEntity m_entity; Vector3 m_minRotation; Vector3 m_maxRotation; float m_damper; private: TEntity m_parent; Vector3 m_scale; Vector3 m_up; Vector3 m_forward; Vector3 m_right; }; #endif
     
    Just ask if you want me to supply the Vector3 and Quaternion math code if you need it.
  3. Chris Paulson
    Since my last update I have tried to get recast callable from within blitz to make it available for everyone. However I have failed as I have not got enough Blitz/C++/DLL skills. I'm hoping someone will offer some help.
     
    I was unhappy how I handled dynamic movable objects as there was a chance the steering wouldn't avoid hitting them. Because of this I rewrote my interface to recast to use a tiled nav mesh, this means I could dynamically update a tile if a object moved in/out of a tile.
     
    The regeneration of a tile could take upto 20ms which would cause nasty frame rate spikes so I had to move the processing and pathfinding etc into a different thread. This has done the trick and I now get smooth frame rates even when causing lots of tile regenerations.
     
    I found out while doing this however that Leadwerks doesn't like threading, if ANY LW commands is issued while a thread is doings a LW command the program crashes. This mean't I had to implement mutex locks around LW commands.
     
    Here's the code for handling the navmesh requests:-
     

    #include "leadcast/include/navmesh.h" #include <time.h> #include "gamelib.h" #define DEBUG_PATH NavMesh::NavMesh() : m_keepInterResults(false), m_tileMesh(NULL), m_chunkyMesh(0), m_triflags(0), m_solid(0), m_chf(0), m_cset(0), m_pmesh(0), m_dmesh(0), m_tileSize(32), m_tileTriCount(0), m_leadMesh(NULL), m_showMesh(false), m_stoprequested(false) { resetCommonSettings(); memset(m_tileBmin, 0, sizeof(m_tileBmin)); memset(m_tileBmax, 0, sizeof(m_tileBmax)); m_polyPickExt[0] = 2; m_polyPickExt[1] = 4; m_polyPickExt[2] = 2; } static void getPolyCenter(dtTiledNavMesh* navMesh, dtTilePolyRef ref, float* center) { const dtTilePoly* p = navMesh->getPolyByRef(ref); if (!p) return; const float* verts = navMesh->getPolyVertsByRef(ref); center[0] = 0; center[1] = 0; center[2] = 0; for (int i = 0; i < (int)p->nv; ++i) { const float* v = &verts[p->v[i]*3]; center[0] += v[0]; center[1] += v[1]; center[2] += v[2]; } const float s = 1.0f / p->nv; center[0] *= s; center[1] *= s; center[2] *= s; } void NavMesh::makeTileMesh(const dtTileHeader* header) { TMaterial material; for (int i = 0; i < header->npolys; ++i) { const dtTilePoly* p = &header->polys[i]; const dtTilePolyDetail* pd = &header->dmeshes[i]; int vp; material = CreateMaterial(); SetMaterialColor( material , Vec4( float(rand()) / float(RAND_MAX), float(rand()) / float(RAND_MAX), float(rand()) / float(RAND_MAX), 1) ); TSurface surface = CreateSurface(m_leadMesh, material); int vidx = 0; for (int j = 0; j < pd->ntris; ++j) { const unsigned char* t = &header->dtris[(pd->tbase+j)*4]; for (int k = 0; k < 3; ++k) { if (t[k] < p->nv) { vp = p->v[t[k]]*3; AddVertex( surface, Vec3( header->verts[ vp ]*-1, header->verts[ vp+1], header->verts[ vp+2]),Vec3(0,0,1) ); } else { vp = (pd->vbase+t[k]-p->nv)*3; AddVertex( surface, Vec3( header->dverts[ vp ]*-1, header->dverts[ vp+1], header->dverts[ vp+2]),Vec3(0,0,1) ); } } AddTriangle( surface, vidx+1, vidx, vidx+2 ); vidx +=3; } } } void NavMesh::makeTiledNavMesh(const dtTiledNavMesh* mesh) { if (!mesh) return; if (m_leadMesh) { FreeEntity(m_leadMesh); } m_leadMesh = CreateMesh(NULL); for (int i = 0; i < DT_MAX_TILES; ++i) { const dtTile* tile = mesh->getTile(i); if (!tile->header) continue; makeTileMesh(tile->header); } // FlipMesh(m_leadMesh); UpdateMesh( m_leadMesh ); } void NavMesh::buildAllTiles() { const float ts = m_tileSize*m_cellSize; float x,y,z; float sTime = clock(); char buf[256]; y = m_bmin[1]; for(x = m_bmin[0]-ts; x<=(m_bmax[0]+ts);x+=ts) { for(z = m_bmin[2]-ts; z<=(m_bmax[2]+ts);z+=ts) { buildTile( Vec3(x,y,z) ); } } sprintf(buf, "Time %f", clock() - sTime); AppLogMode(1); AppLog(buf); } bool NavMesh::init() { if (!m_verts || !m_tris) { printf("No verts or tris\n"); return false; } // delete m_navMesh; m_tileMesh = NULL; m_tileMesh = new dtTiledNavMesh; if (!m_tileMesh) { AppLog("Could not allocate navmehs"); return false; } if (!m_tileMesh->init(m_bmin, m_tileSize*m_cellSize, m_agentMaxClimb*m_cellHeight)) { AppLog("Could not init navmesh"); return false; } // Build chunky mesh. // delete m_chunkyMesh; m_chunkyMesh = new rcChunkyTriMesh; if (!m_chunkyMesh) { AppLog("buildTiledNavigation: Out of memory 'm_chunkyMesh'."); return false; } if (!rcCreateChunkyTriMesh(m_verts, m_tris, m_ntris, 256, m_chunkyMesh)) { AppLog("buildTiledNavigation: Could not build chunky mesh."); return false; } return true; } void NavMesh::buildTile(const TVec3 pos) { if (!m_tileMesh) return; const float ts = m_tileSize*m_cellSize; int tx = (int)floorf((pos.X-m_bmin[0]) / ts); int ty = (int)floorf((pos.Z-m_bmin[2]) / ts); if (tx < 0 || ty < 0) return; rcMeshLoaderSBX *loader = new rcMeshLoaderSBX; m_tileBmin[0] = m_bmin[0] + tx*ts; m_tileBmin[1] = m_bmin[1]; m_tileBmin[2] = m_bmin[2] + ty*ts; m_tileBmax[0] = m_bmin[0] + (tx+1)*ts; m_tileBmax[1] = m_bmax[1]; m_tileBmax[2] = m_bmin[2] + (ty+1)*ts; { boost::mutex::scoped_lock l(m_mutex); TVec6 box; box.X1 = m_tileBmin[0]*-1; box.Y0 = m_tileBmin[1]; box.Z0 = m_tileBmin[2]; box.X0 = m_tileBmax[0]*-1; box.Y1 = m_tileBmax[1]; box.Z1 = m_tileBmax[2]; loader->processDynamic( box ); m_dverts = loader->getVerts(); m_ndverts = loader->getVertCount(); m_dtris = loader->getTris(); m_ndtris = loader->getTriCount(); } // m_tileCol[0] = 0.3f; m_tileCol[1] = 0.8f; m_tileCol[2] = 0; m_tileCol[3] = 1; int dataSize = 0; unsigned char* data = buildTileMesh(m_tileBmin, m_tileBmax, dataSize); if (data) { boost::mutex::scoped_lock l(m_mutex); // Remove any previous data (navmesh owns and deletes the data). m_tileMesh->removeTileAt(tx,ty,0,0); // Let the navmesh own the data. if (!m_tileMesh->addTileAt(tx,ty,data,dataSize,true)) delete [] data; } delete loader; } void NavMesh::removeTile(const TVec3 pos) { if (!m_tileMesh) return; const float ts = m_tileSize*m_cellSize; const int tx = (int)floorf((pos.X-m_bmin[0]) / ts); const int ty = (int)floorf((pos.Z-m_bmin[2]) / ts); m_tileBmin[0] = m_bmin[0] + tx*ts; m_tileBmin[1] = m_bmin[1]; m_tileBmin[2] = m_bmin[2] + ty*ts; m_tileBmax[0] = m_bmin[0] + (tx+1)*ts; m_tileBmax[1] = m_bmax[1]; m_tileBmax[2] = m_bmin[2] + (ty+1)*ts; m_tileCol[0] = 0.8f; m_tileCol[1] = 0.1f; m_tileCol[2] = 0; m_tileCol[3] = 1; unsigned char* rdata = 0; int rdataSize = 0; if (m_tileMesh->removeTileAt(tx,ty,&rdata,&rdataSize)) delete [] rdata; } void NavMesh::cleanup() { delete [] m_triflags; m_triflags = 0; delete m_solid; m_solid = 0; delete m_chf; m_chf = 0; delete m_cset; m_cset = 0; delete m_pmesh; m_pmesh = 0; delete m_dmesh; m_dmesh = 0; } unsigned char* NavMesh::buildTileMesh(const float* bmin, const float* bmax, int& dataSize) { if (!m_verts || ! m_tris) { AppLog( "buildNavigation: Input mesh is not specified."); return 0; } cleanup(); // Init build configuration from GUI memset(&m_cfg, 0, sizeof(m_cfg)); m_cfg.cs = m_cellSize; m_cfg.ch = m_cellHeight; m_cfg.walkableSlopeAngle = m_agentMaxSlope; m_cfg.walkableHeight = (int)ceilf(m_agentHeight / m_cfg.ch); m_cfg.walkableClimb = (int)ceilf(m_agentMaxClimb / m_cfg.ch); m_cfg.walkableRadius = (int)ceilf(m_agentRadius / m_cfg.cs); m_cfg.maxEdgeLen = (int)(m_edgeMaxLen / m_cellSize); m_cfg.maxSimplificationError = m_edgeMaxError; m_cfg.minRegionSize = (int)rcSqr(m_regionMinSize); m_cfg.mergeRegionSize = (int)rcSqr(m_regionMergeSize); m_cfg.maxVertsPerPoly = (int)m_vertsPerPoly; m_cfg.tileSize = (int)m_tileSize; m_cfg.borderSize = m_cfg.walkableRadius*2 + 2; // Reserve enough padding. m_cfg.width = m_cfg.tileSize + m_cfg.borderSize*2; m_cfg.height = m_cfg.tileSize + m_cfg.borderSize*2; m_cfg.detailSampleDist = m_detailSampleDist < 0.9f ? 0 : m_cellSize * m_detailSampleDist; m_cfg.detailSampleMaxError = m_cellHeight * m_detailSampleMaxError; vcopy(m_cfg.bmin, bmin); vcopy(m_cfg.bmax, bmax); m_cfg.bmin[0] -= m_cfg.borderSize*m_cfg.cs; m_cfg.bmin[2] -= m_cfg.borderSize*m_cfg.cs; m_cfg.bmax[0] += m_cfg.borderSize*m_cfg.cs; m_cfg.bmax[2] += m_cfg.borderSize*m_cfg.cs; // Allocate voxel heighfield where we rasterize our input data to. m_solid = new rcHeightfield; if (!m_solid) { AppLog("buildNavigation: Out of memory 'solid'."); return 0; } if (!rcCreateHeightfield(*m_solid, m_cfg.width, m_cfg.height, m_cfg.bmin, m_cfg.bmax, m_cfg.cs, m_cfg.ch)) { AppLog( "buildNavigation: Could not create solid heightfield."); return 0; } // Allocate array that can hold triangle flags. // If you have multiple meshes you need to process, allocate // and array which can hold the max number of triangles you need to process. m_triflags = new unsigned char[__max(m_chunkyMesh->maxTrisPerChunk,m_ndtris)]; if (!m_triflags) { AppLog( "buildNavigation: Out of memory 'triangleFlags' (%d).", m_chunkyMesh->maxTrisPerChunk); return 0; } float tbmin[2], tbmax[2]; tbmin[0] = m_cfg.bmin[0]; tbmin[1] = m_cfg.bmin[2]; tbmax[0] = m_cfg.bmax[0]; tbmax[1] = m_cfg.bmax[2]; int cid[256];// TODO: Make grow when returning too many items. int ncid = rcGetChunksInRect(m_chunkyMesh, tbmin, tbmax, cid, 256); if (!ncid) return 0; m_tileTriCount = 0; for (int i = 0; i < ncid; ++i) { const rcChunkyTriMeshNode& node = m_chunkyMesh->nodes[cid[i]]; const int* tris = &m_chunkyMesh->tris[node.i*3]; const int ntris = node.n; m_tileTriCount += ntris; memset(m_triflags, 0, ntris*sizeof(unsigned char)); rcMarkWalkableTriangles(m_cfg.walkableSlopeAngle, m_verts, m_nverts, tris, ntris, m_triflags); rcRasterizeTriangles(m_verts, m_nverts, tris, m_triflags, ntris, *m_solid); } // Do dynamic stuff here if (m_ndverts) { memset(m_triflags, 0, m_ndtris*sizeof(unsigned char)); m_tileTriCount += m_ndtris; rcMarkWalkableTriangles(m_cfg.walkableSlopeAngle, m_dverts, m_ndverts, m_dtris, m_ndtris, m_triflags); rcRasterizeTriangles(m_dverts, m_ndverts, m_dtris, m_triflags, m_ndtris, *m_solid); } if (!m_keepInterResults) { delete [] m_triflags; m_triflags = 0; } // Once all geoemtry is rasterized, we do initial pass of filtering to // remove unwanted overhangs caused by the conservative rasterization // as well as filter spans where the character cannot possibly stand. rcFilterLedgeSpans(m_cfg.walkableHeight, m_cfg.walkableClimb, *m_solid); rcFilterWalkableLowHeightSpans(m_cfg.walkableHeight, *m_solid); // Compact the heightfield so that it is faster to handle from now on. // This will result more cache coherent data as well as the neighbours // between walkable cells will be calculated. m_chf = new rcCompactHeightfield; if (!m_chf) { AppLog( "buildNavigation: Out of memory 'chf'."); return 0; } if (!rcBuildCompactHeightfield(m_cfg.walkableHeight, m_cfg.walkableClimb, RC_WALKABLE, *m_solid, *m_chf)) { AppLog( "buildNavigation: Could not build compact data."); return 0; } if (!m_keepInterResults) { delete m_solid; m_solid = 0; } // Prepare for region partitioning, by calculating distance field along the walkable surface. if (!rcBuildDistanceField(*m_chf)) { AppLog( "buildNavigation: Could not build distance field."); return 0; } // Partition the walkable surface into simple regions without holes. if (!rcBuildRegions(*m_chf, m_cfg.walkableRadius, m_cfg.borderSize, m_cfg.minRegionSize, m_cfg.mergeRegionSize)) { AppLog( "buildNavigation: Could not build regions."); return 0; } // Create contours. m_cset = new rcContourSet; if (!m_cset) { AppLog("buildNavigation: Out of memory 'cset'."); return 0; } if (!rcBuildContours(*m_chf, m_cfg.maxSimplificationError, m_cfg.maxEdgeLen, *m_cset)) { AppLog( "buildNavigation: Could not create contours."); return 0; } // Build polygon navmesh from the contours. m_pmesh = new rcPolyMesh; if (!m_pmesh) { AppLog("buildNavigation: Out of memory 'pmesh'."); return 0; } if (!rcBuildPolyMesh(*m_cset, m_cfg.maxVertsPerPoly, *m_pmesh)) { AppLog( "buildNavigation: Could not triangulate contours."); return 0; } // Build detail mesh. m_dmesh = new rcPolyMeshDetail; if (!m_dmesh) { AppLog( "buildNavigation: Out of memory 'dmesh'."); return 0; } if (!rcBuildPolyMeshDetail(*m_pmesh, *m_chf, m_cfg.detailSampleDist, m_cfg.detailSampleMaxError, *m_dmesh)) { AppLog( "buildNavigation: Could build polymesh detail."); return 0; } if (!m_keepInterResults) { delete m_chf; m_chf = 0; delete m_cset; m_cset = 0; } unsigned char* navData = 0; int navDataSize = 0; if (m_cfg.maxVertsPerPoly == DT_TILE_VERTS_PER_POLYGON) { // Remove padding from the polymesh data. TODO: Remove this odditity. for (int i = 0; i < m_pmesh->nverts; ++i) { unsigned short* v = &m_pmesh->verts[i*3]; v[0] -= (unsigned short)m_cfg.borderSize; v[2] -= (unsigned short)m_cfg.borderSize; } if (!dtCreateNavMeshTileData(m_pmesh->verts, m_pmesh->nverts, m_pmesh->polys, m_pmesh->npolys, m_pmesh->nvp, m_dmesh->meshes, m_dmesh->verts, m_dmesh->nverts, m_dmesh->tris, m_dmesh->ntris, bmin, bmax, m_cfg.cs, m_cfg.ch, m_cfg.tileSize, m_cfg.walkableClimb, &navData, &navDataSize)) { AppLog( "Could not build Detour navmesh."); } } m_tileMemUsage = navDataSize/1024.0f; dataSize = navDataSize; return navData; } void NavMesh::testBuild() { float meshBMin[3], meshBMax[3]; m_loader = new rcMeshLoaderSBX(); m_loader->process(); resetCommonSettings(); rcCalcBounds(m_loader->getVerts(), m_loader->getVertCount(), m_bmin, m_bmax); m_verts = m_loader->getVerts(); m_tris = m_loader->getTris(); m_ntris = m_loader->getTriCount(); init(); buildAllTiles(); if (m_showMesh) makeTiledNavMesh( m_tileMesh ); } void NavMesh::calcTilePos( TVec3 pos, TilePos &tpos) { const float ts = m_tileSize*m_cellSize; tpos.tx = (int)floorf((pos.X-m_bmin[0]) / ts); tpos.ty = (int)floorf((pos.Z-m_bmin[2]) / ts); } void NavMesh::addBuildRequest( TVec3 pos, TVec6 box, TEntity e ) { TilePos tpos; box.X0 *=-1; box.X1 *=-1; //bl pos = Vec3(box.X0,box.Y0,box.Z0); calcTilePos( pos, tpos ); m_rebuildRequest[ tpos ] = pos; //tl pos = Vec3( box.X0, box.Y0, box.Z1); calcTilePos( pos, tpos ); m_rebuildRequest[ tpos ] = pos; //br pos = Vec3( box.X1, box.Y0, box.Z0); calcTilePos( pos, tpos ); m_rebuildRequest[ tpos ] = pos; //tr pos = Vec3( box.X1, box.Y0, box.Z1); calcTilePos( pos, tpos ); m_rebuildRequest[ tpos ] = pos; } NavRequest* NavMesh::newPathRequest( TVec3 spos, TVec3 epos ) { if (spos == epos) return NULL; NavRequest *req = new NavRequest; req->m_spos[0] = spos.X*-1; req->m_spos[1] = spos.Y; req->m_spos[2] = spos.Z; req->m_epos[0] = epos.X*-1; req->m_epos[1] = epos.Y; req->m_epos[2] = epos.Z; req->m_startPos = spos; req->m_endPos = spos; req->m_mesh = m_tileMesh; m_pathRequests[ req ] = req; return req; } void NavMesh::removePathRequest( NavRequest *nav) { m_pathRequests.erase( nav ); delete nav; } float NavMesh::findDistanceToWall( TVec3 p, float dist, float *hit, float *hitNormal, int *npolys, float *navhit ) { float spos[3], epos[3]; float wallDist; dtTilePolyRef startRef; spos[0] = p.X*-1; spos[1] = p.Y; spos[2] = p.Z; epos[0] = p.X*-1; epos[1] = p.Y; epos[2] = p.Z; startRef = m_tileMesh->findNearestPoly(spos, m_polyPickExt); if (!startRef) { AppLog( "Not anywhere near navmesh"); return 999999; } *npolys = m_tileMesh->raycast( startRef, spos, epos, *navhit, m_polys, MAX_POLYS); wallDist = m_tileMesh->findDistanceToWall(startRef, spos, dist, hit, hitNormal); return wallDist; } void NavMesh::startThread() { m_thread = boost::shared_ptr<boost::thread>(new boost::thread(boost::bind(&NavMesh::handleThread, this))); } void NavMesh::handleThread() { while (!m_stoprequested) { update(); Sleep(30); } } void NavMesh::update() { // float startTime = AppTime(); NavRequest *navreq; std::map<TilePos,TVec3,TilePosCompare>::iterator req; std::map<NavRequest*,NavRequest*>::iterator findList; bool doneOne = false; req=m_rebuildRequest.begin(); if (req!=m_rebuildRequest.end()) { //removeTile(req->second); buildTile( req->second ); m_rebuildRequest.erase(req); navVersion++; doneOne = true; } if (m_showMesh && doneOne) makeTiledNavMesh( m_tileMesh ); { boost::mutex::scoped_lock l(m_mutex); findList = m_pathRequests.begin(); while (findList != m_pathRequests.end()) { findList->second->processPathRequest(); m_pathRequests.erase(findList++); } } } void NavRequest::makeBasicPath() { m_navVersion = navVersion; m_startRef = m_mesh->findNearestPoly(m_spos, m_polyPickExt); m_endRef = m_mesh->findNearestPoly(m_epos, m_polyPickExt); m_npolys = m_mesh->findPath(m_startRef, m_endRef, m_spos, m_epos, m_polys, MAX_POLYS); if (m_npolys) m_nstraightPath = m_mesh->findStraightPath(m_spos, m_epos, m_polys, m_npolys, m_straightPath, MAX_POLYS); m_status = NAV_COMPLETED; } void NavRequest::processPathRequest() { makeBasicPath(); makeSteerPath( m_startPos, m_nstraightPath ); } TVec3 toVec3Open( OpenSteer::Vec3 p ) { return Vec3(p.x,p.y,p.z); } void NavRequest::render( TCamera cam ) { #ifdef DEBUG_PATH TVec3 p1; TVec3 p2; if (m_nstraightPath < 2) return; SetBlend(BLEND_ALPHA); SetColor( Vec4(1,0,0,1) ); // red for( int i=0;i<m_nstraightPath-1;i++) { tdDraw( cam, pathVec3( i ), pathVec3( i+1 ) ); } SetColor( Vec4(0,1,0,1) ); // red for( int i=0;i<m_numPoints-1;i++) { tdDraw( cam, toVec3Open(m_pathPoints[i]) , toVec3Open(m_pathPoints[i+1]) ); } #endif } OpenSteer::Vec3 *toOpenVec3( TVec3 pos ) { OpenSteer::Vec3 *rpos = new OpenSteer::Vec3; rpos->x = pos.X; rpos->y = pos.Y; rpos->z = pos.Z; return rpos; } bool NavRequest::makeSteerPath( TVec3 startPos, int maxPoints ) { if (!complete()) return false; maxPoints = MAX_POLYS; // temp fix // Check if recalc needed if (m_navVersion != navVersion) makeBasicPath(); else { float spos[3]; spos[0] = startPos.X*-1; spos[1] = startPos.Y; spos[2] = startPos.Z; m_nstraightPath = m_mesh->findStraightPath(spos, m_epos, m_polys, m_npolys, m_straightPath, maxPoints); } m_numPoints = __min( m_nstraightPath, maxPoints ); if (m_numPoints <= 2 && startPos == m_endPos) { m_numPoints = 0; return false; } if (m_openPath) { delete m_openPath; m_openPath = NULL; } if (m_pathPoints) { delete m_pathPoints; m_pathPoints = NULL; } m_openPath = new OpenSteer::PolylineSegmentedPathwaySingleRadius; if (m_numPoints > 2) { m_pathPoints = new OpenSteer::Vec3[m_numPoints]; for( int i=0;i < m_numPoints;i++) { m_pathPoints[i] = *toOpenVec3( pathVec3( i ) ); } m_numPoints -=1; } else { m_pathPoints = new OpenSteer::Vec3[2]; m_pathPoints[0] = *toOpenVec3(startPos); m_pathPoints[1] = *toOpenVec3(m_endPos); m_numPoints = 2; } m_openPath->setPathway( m_numPoints, m_pathPoints, 0.01, false ); return true; }
     
     
    Here's the recast vert loader code: -
     

    #include "leo.h" #include "recast.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #define _USE_MATH_DEFINES #include <math.h> #include "ledcast.h" #include "ledcast_statmeshSimple.h" #include "rcMeshLoaderSBX.h" using namespace LEO; rcMeshLoaderSBX *currentLoader; bool processMesh( TEntity entity ) { TMesh mesh = (TMesh)entity; int SurfCnt = CountSurfaces( mesh ); TSurface surface; TVec3 vec; int vertIdx; float factor=1; vertIdx = currentLoader->getVertCount(); for( int s = 1; s <= SurfCnt; s++ ) { surface = GetSurface( mesh, s ); if (surface) { for( int t = 0; t < CountTriangles( surface ); t++) { for( int v = 0; v < 3; v++) { vec = GetVertexPosition( surface, TriangleVertex( surface, t, v )); vec=TFormPoint(Vec3(vec.X,vec.Y,vec.Z),entity,NULL); currentLoader->addVertex( (vec.X)*-factor, (vec.Y)*factor, (vec.Z)*factor, currentLoader->vcap ); } currentLoader->addTriangle( vertIdx+1, vertIdx, vertIdx+2, currentLoader->tcap ); vertIdx=vertIdx+3; } } else break; } return true; } int _stdcall recastLoadMesh( TEntity entity, byte* extra ) { TMesh mesh = (TMesh)entity; std::string entityClass, cn, en; int type, type1; // Only bother with collision type 1 cn=GetEntityKey(entity,"classname"); en=GetEntityKey(entity,"name"); entityClass = GetEntityKey(entity, "class",""); type = atoi( GetEntityKey(entity, "collisiontype","1") ); if (type >0) EntityType(entity, type, 1); type1 = GetEntityType(entity); if (currentLoader->m_dynamic) { if (type1 != 2) return true; }else{ if (type1 != 1) return true; } if (entityClass != "Model") return true; if( (cn=="water")|| (cn=="physics_prop")|| (cn=="physics_pivot")|| (cn=="joint_motor")|| (cn=="info_waypoint")|| (cn=="info_playerstart")|| (cn=="player")|| (cn=="light_directional")|| (cn=="light_spot")|| (cn=="light_point")|| (cn=="env_emitter")|| (cn=="env_sound")) { HideEntity(entity); return true; } processMesh( GetChild(entity, 1) ); return true; } int _stdcall recastLoadTerrain( TEntity terrain, byte* extra ) { std::string entityClass; entityClass = GetEntityKey(terrain, "class",""); if (entityClass != "Terrain") return false; float mapSize = 1024; float x; float z; float mapHalfSize = 512; float height; int vertIdx; float minx = 0, minz =0, maxx=0, maxz=0; if (!terrain) return false; vertIdx = currentLoader->getVertCount(); for(x = -mapHalfSize; x<mapHalfSize - 1; x++) { for(z = -mapHalfSize; z<mapHalfSize - 1; z++) { height = TerrainElevation( terrain, x, z); if ((height != 0) || ((x>=minx && x<=maxx) && (z>=minz && z <= maxz))) { minx = __min(minx, x); minz = __min(minz, z); maxx = __max(maxx, x); maxz = __max(maxz, z); //br height = TerrainElevation( terrain, (float)x+1, (float)z); currentLoader->addVertex( (x+1)*-1, height, z, currentLoader->vcap ); //tr height = TerrainElevation( terrain, (float)x+1, (float)z+1); currentLoader->addVertex( (x+1)*-1, height, z+1, currentLoader->vcap ); //bl height = TerrainElevation( terrain, x, z); currentLoader->addVertex( x*-1, height, z, currentLoader->vcap ); currentLoader->addTriangle( vertIdx, vertIdx+1, vertIdx+2, currentLoader->tcap ); vertIdx=vertIdx+3; //Trianlge 2 //tl height = TerrainElevation( terrain, (float)x, (float)z+1); currentLoader->addVertex( x*-1, height, z+1, currentLoader->vcap ); //bl height = TerrainElevation( terrain, (float)x, (float)z); currentLoader->addVertex( x*-1, height, z, currentLoader->vcap ); //tr height = TerrainElevation( terrain, (float)x+1, (float)z+1); currentLoader->addVertex( (x+1)*-1, height, z+1, currentLoader->vcap ); currentLoader->addTriangle( vertIdx, vertIdx+1, vertIdx+2, currentLoader->tcap ); vertIdx=vertIdx+3; } else height = -1; } } return true; } int _stdcall recastLoadMeshNormals( TEntity entity, byte* extra ) { TMesh mesh = (TMesh)entity; std::string entityClass; entityClass = GetEntityKey(entity, "class",""); if (entityClass != "Mesh") return false; TSurface surface; int SurfCnt = CountSurfaces( mesh ); float *normals; normals = (float *)(currentLoader->getNormals()); TVec3 vnorm; for( int s = 1; s <= SurfCnt; s++ ) { surface = GetSurface( mesh, s ); for( int t = 0; t < CountTriangles( surface ); t++) { vnorm = GetVertexNormal( surface, TriangleVertex( surface, t, 1 )); normals[currentLoader->nidx] = vnorm.X; normals[currentLoader->nidx+1] = vnorm.Y; normals[currentLoader->nidx+2] = vnorm.Z; currentLoader->nidx=currentLoader->nidx+3; } } return true; } bool rcMeshLoaderSBX::load(const char* filename) { std::string f = filename; if (f.find("sbx")) scene = LoadScene((str)filename); else if (f.find("gmf")) scene = LoadModel((str)filename); if (!scene) return false; return process(); } bool rcMeshLoaderSBX::process() { currentLoader = this; m_dynamic = false; ForEachEntityDo( (BP)recastLoadMesh,NULL, ENTITY_MESH|ENTITY_MODEL ); ForEachEntityDo( (BP)recastLoadTerrain,NULL, ENTITY_TERRAIN ); setNormals( new float[getTriCount()*3] ); nidx = 0; calcNormals(); return true; } bool rcMeshLoaderSBX::processDynamic( TVec6 box ) { currentLoader = this; m_dynamic = true; ForEachEntityInAABBDo( box, (BP)recastLoadMesh, (byte*)this, ENTITY_MESH|ENTITY_MODEL ); return true; } rcMeshLoaderSBX::~rcMeshLoaderSBX() { if (scene) FreeEntity(scene); }
  4. Chris Paulson
    When your NPC moves it may move at different speeds, especially if you are using some sort of steering. This presents the problem of what speed should you play your walk/run animations. My solution is to analyse the animation and see how far the foot travels from start frame to end frame and when animating play the correct amount of frame(s) for the distance.
     
    Here’s the code:-
     

    #include "anim.h" void animItem::calcAnimDist( TEntity e, TEntity bone ) { TVec3 startPos; TVec3 newPos; TVec3 prevPos; TVec3 rot; float totalZ = 0; float totalX= 0; if (!bone) return; rot = EntityRotation(e,1); RotateEntity(e,Vec3(0),1); startPos = EntityPosition(bone,1); for(int i=startFrame; i<endFrame; i++) { prevPos = EntityPosition(bone,1); Animate( e, i ); newPos = EntityPosition(bone,1); totalZ += abs(newPos.Z - prevPos.Z); totalX += abs(newPos.X - prevPos.X); } RotateEntity(e,rot,1); distanceZ = totalZ*2; distanceX = totalX*2; perFrameZ = totalZ / frameCount; perFrameX = totalX / frameCount; maxSpeed = perFrameZ / 1.8; minSpeed = perFrameZ / 3.6; }
     
     
    Here’s it’s header file, it has loads more stuff in that I will touch on in later blog entries.
     

    #ifndef ANIM_H #define ANIM_H #include <string> #include <map> #include <vector> #include <algorithm> #include "engine.h" #include "actor/include/animbody.h" using namespace std; typedef enum animDirection { ANIM_OTHER }; class keyFrame { int parentFrame; int childFrame; }; typedef enum animType { ANIM_NONE = 0, ANIM_IDLE = 1, ANIM_WALK = 2, ANIM_RUN = 4, ANIM_CROUCH = 8, ANIM_TURN = 16, ANIM_STRAFE = 32, ANIM_RIFLE = 64, ANIM_PISTOL = 128, ANIM_DEATH = 256, ANIM_FORWARD = 512, ANIM_BACKWARD = 1024, ANIM_PAIN = 2048, ANIM_LEFT = 4096, ANIM_RIGHT = 8192, ANIM_JUMP = 16384 }; typedef enum animStatus { ANIM_DEFAULT, ANIM_STARTING, ANIM_PLAYING, ANIM_STOPPING, ANIM_STOPPED }; class animItem { protected: float distanceZ; float distanceX; float rotationAngle; map<std::string,keyFrame> keyFrames; public: animType animType; std::string name; int sequence; int startFrame; int endFrame; int frameCount; float perFrameX; float perFrameZ; bool speedDepdent; float maxSpeed; float minSpeed; animDirection direction; animStatus m_status; animItem() {}; animItem( string pName, int pStart, int pEnd, animDirection pDirection = ANIM_OTHER ); ~animItem() {}; inline int getFrameCount() { return frameCount; } //void calcAnimDist( TEntity e, string boneName ); void calcAnimDist( TEntity e, TEntity bone ); }; typedef enum animPlayType { PLAY_REPEAT, PLAY_ONCE, PLAY_ONCE_RESET }; class playingAnim { protected: public: animItem* myAnimItem; float currentFrame; // Zero based float speed; animStatus status; animPlayType animType; TEntity entity; float transitionTime; float startTime; float stopTime; int blendTime; playingAnim() {}; playingAnim( animItem* pAnim, float pTransitionTime = 0, float pSpeed = 0, animPlayType pAnimType = PLAY_REPEAT): currentFrame(0), blendTime(0), stopTime(0), startTime(0) { myAnimItem = pAnim; speed = pSpeed; animType = pAnimType; transitionTime = pTransitionTime; status = ANIM_STARTING; } inline void setStartTime( float pTime ) { startTime = pTime; } inline void setStopTime( float pTime ) { stopTime = pTime; } inline void setEntity( TEntity ent ) { entity = ent; } inline void setCurrentFrame( float pFrame ) { currentFrame = pFrame; } inline float getStopTime() { return stopTime; } inline float getStartTime() { return startTime; } inline float getTransitionTime() { return transitionTime; } inline animStatus getStatus() { return status; } inline animPlayType getAnimType() { return animType; } }; class entityAnimation { protected: map<string,animItem*> items; map<string,playingAnim*> animQueue; playingAnim* activeItem; TEntity entity; public: entityAnimation( TEntity pEnt ); //~entityAnimation(); animItem* addItem( string pName, int pStart, int pEnd ); playingAnim* start( string pName, float pTranTime = 0, float animSpeed = 1, animPlayType pAnimType = PLAY_REPEAT, string entityName = ""); void stop( playingAnim* anim, float transition); void stopAll( float time); void update( float dist = 0); bool isPlayingAnimName( string pName ); bool animationActive( string pName); bool animationStopped( string pName ); inline float startBlend( playingAnim* play ); inline float stopBlend( playingAnim* play ); }; class locomotion: public entityAnimation { public: animType currentAnimType; bool crouching; bool holdingPistol; bool holdingRifle; bool movingForward; bodyParts* m_bodyParts; bodyControl* m_bodyControl; float swapTime; void update( float dist = 0); animItem* locomotion::findItemForSpeed( float dist ); animItem* locomotion::findItemForType( animType pAnimType ); bool readAnimFile( string pFile ); void setCrouching( bool pCrouch = true ); void setHoldingPistol( bool pPistol = true ); void setHoldingRifle( bool pRifle = true ); void setMovingForward( bool pForward = true ); locomotion( TEntity e); void render( TCamera cam); }; #endif
  5. Chris Paulson
    Youtube video of my AI.
     
    Red line is path found by recast.
    White text is current behaviours running in behaviour tree.
     

     
     
    Here's what the BT looks like (yes it is C++)

    alive::Node* moveToEnemy(std::string type = "walk") { return alive::TreeBuilder() .execute<ActionMoveToEnemy>() .type(type) .end(); } alive::Node* resetToStart() { return alive::TreeBuilder() .composite<alive::SequenceNode>() .execute<ActionCurrentMode>() .mode("resetToStart") .end() .execute<ActionAnimate>() .Speed(1) .Name("rifle_idle") .AnimType(PLAY_REPEAT) .end() .composite<alive::ParallelNode>() .execute<LookForward>() .Parallel(true) .end() .execute<ActionStopMove>() .end() .end() .end(); } alive::Node* gunAttack() { return alive::TreeBuilder() .composite<alive::SequenceNode>() .execute<ActionCurrentMode>() .mode("gunAttack") .end() .execute<hasGun>() .Equals(true) .end() .execute<CanSeeEnemy>() .Equals(true) .Parallel(false) .end() .execute<ActionStopMove>() .end() .execute<ActionAnimate>() .Speed(1) .TranTime(1) .Name("rifle_idle") .AnimType(PLAY_REPEAT) .end() .composite<alive::ParallelNode>() .execute<CanSeeEnemy>() .Equals(true) .end() .execute<WithinFiringRange>() .Equals(true) .end() .execute<AimAtEnemy>() .end() .composite<alive::SequenceNode>() .decorator<alive::RepeatNode>() .composite<alive::SelectorNode>() .execute<NeedToAim>() .Equals(true) .end() .execute<FireAtEnemy>() .end() .execute<ActionTimer>() .time(0.01) .end() .end() .end() .end() .end() .end(); } alive::Node* moveToAttackPosition() { return alive::TreeBuilder() .composite<alive::SequenceNode>() .execute<ActionCurrentMode>() .mode("moveToAttackPosition") .end() .execute<hasGun>() .Equals(true) .end() .add(resetToStart()).end() .composite<alive::ParallelNode>() .execute<IsSeenIdle>() .Equals(false) .end() .execute<WithinFiringRange>() .Equals(true) .end() .execute<hasGoodAimAtEnemy>() .Equals(false) .end() .add(moveToEnemy()).end() .end() .end(); } alive::Node* moveToWithinRifleRange() { return alive::TreeBuilder() .composite<alive::SequenceNode>() .execute<ActionCurrentMode>() .mode("moveToWithinRifleRange") .end() .execute<hasGun>() .Equals(true) .end() .composite<alive::ParallelNode>() .execute<LookForward>() .Parallel(true) .end() .execute<IsSeenIdle>() .Equals(false) .end() .execute<WithinFiringRange>() .Equals(false) .end() .add(moveToEnemy()).end() .end() .end(); } alive::Node* hunt() { return alive::TreeBuilder() .composite<alive::SequenceNode>() .execute<ActionCurrentMode>() .mode("hunt") .end() .composite<alive::ParallelNode>() .execute<LookForward>() .Parallel(true) .end() .execute<IsSeenIdle>() .Equals(false) .end() .execute<CanSeeEnemy>() .Equals(false) .end() .add(moveToEnemy()).end() .end() .end(); } void Crawler::init(Pedestrian* pPedestrian, Vision *pvision, ActorGun *pRifle) { // .decorator<alive::RepeatNode>() // .decorator<alive::ErrorHandlerNode>() alive::Node* behave = alive::TreeBuilder() .decorator<alive::RepeatNode>() .add(resetToStart()).end() .composite<alive::SelectorNode>() // Reset to default // Attack with gun .add(gunAttack()).end() // Move to a aim position .add(moveToAttackPosition()).end() // Move to attack (to melle or to get in firing range) .add(moveToWithinRifleRange()).end() // Hunt .add(hunt()).end() // Move to waypoint .composite<alive::ParallelNode>() .execute<CanSeeEnemy>() .Equals(false) .end() .composite<alive::SequenceNode>() .add(resetToStart()).end() .execute< CrawlerCompare<bool> >() .Variable(&Crawler::hasWaypoint) .Reference(true) .end() .execute<ActionMoveToWaypoint>() .end() .end() .end() // Idle .composite<alive::SequenceNode>() .execute<ActionCurrentMode>() .mode("idle") .end() .composite<alive::ParallelNode>() .execute<CanSeeEnemy>() .Equals(false) .end() .composite<alive::SequenceNode>() .add(resetToStart()).end() .execute<ActionAnimate>() .Speed(1) .Name("rifle_idle") .AnimType(PLAY_ONCE) .end() .end() .end() .end() .end() .end(); rifle = pRifle; pedestrian = pPedestrian; vision = pvision; // alive::InitializeWithBlackboard<Crawler> initialize(*this); alive::InitializeWithBlackboard<Crawler> *initialize = new alive::InitializeWithBlackboard<Crawler>(*this); m_Brain.registry.setAddObserver(*initialize); m_Brain.registry.setAddObserver(*new InitializeAction(*this)); //m_Brain.registry.setAddObserver(*new InitializeAction(*this)); m_Brain.add(*behave); } void Crawler::Update( float gameLoopTime ) { pedestrian->currentAction = pedestrian->currentMode + ","; m_Brain.tick(); pedestrian->Update( gameLoopTime ); } void Crawler::Render( float gameLoopTime) { pedestrian->Render( gameLoopTime ); vision->Render( pedestrian->scene->cam ); }
  6. Chris Paulson
    I haven't been blogging since I started with Leadwerks so this blog will be a bit of a catchup until now. Unlike most people working with Leadwerks I thought I'd ignore all the graphic eye candy as icing on the cake and concentrate on the importing matter of making things happen ie. AI
     
    The bacis of AI is:-
     
    Physics
    -------
     
    Knowing whats bumped into what. This is already taken care of in Leadwerks.
     
     
    Sight
    -----
     
    NPC's need to have a sense of sight, this is mimicked by doing Line of sight tests using raycasts. In leadwerks you use LinePick, the knack is though where to linepick. I post example code latter.
     
     
    Pathfinding
    -----------
     
    NPCs need to know how to get from A to B. This is a big one and quite complicated. Older games used AI Nodes bit this is now seen as outdated, the thing to use in a modern game is a NAVMESH. I wrote Blitzmax stuff to generate my own navmeshes by analysing the level with linepicks however this proved to be too slow. I dumped Blitz and went onto C++ and intergrated in recast ( http://code.google.com/p/recastnavigation/ ). This library is really good and fast. My NPCs knows now how to get from A to B.
     
    See http://www.ai-blog.net/archives/000152.html
     
    My blitzmax navmesh
     
     
     
    Locomotion
    ----------
     
    Once a NPC knows how to get from A to B they need to do the actual physical move. Moving the character controller along will not derive a quality appearance as it will bump into things, other NPC dynamic moving objects etc. To get the actual controller to move nicely I have implemented OpenSteer on top of the pathfinding. Using combined steering forces allows collision avoidence of moving objects.
     
    See http://opensteer.sourceforge.net/
     
    I am only 80% satisfied with this solution an may abandon it for something better in the future.
     
    A dark video example of navmesh and opensteer working
     
     
    Animation
    ---------
     
    To avoid foot skating/moonwalking I play the movement animation the correct proportion of frames to match the distance travelled by the locomotion. I pre analyis the animation sequence to calculate how for a sequence my travel.
     
    Descesion making
    ----------------
     
    Another big one. A lot of people implement FPS games using finite state machines - a posh was of saying lots of if then else ifs.. I wrote my own FSM machine again in Blitz but decided this did not have the quality I was looking for. I have dropped this for behavoir trees and I am using a library called A++.
     
    See http://forums.aigamedev.com for more info on BTs and A++
     
     
     
    I can't comment any further than as this is as far as I've got, I am currently implementing all the different behaviours for my AI.
  7. Chris Paulson
    Placing a rifle to sit in the hands of an NPC can be a bit of a fiddle so I thought I'd post the code.
     
    The code relies on bones for accurate placement:-
     
    Trigger bone
    Gun Butt/Rest bone
    Trigger hand
    Butt hand
     
    I place config stuff like this in an XML file for easy editting, to read the XML I use a nice library tinyXML.
     
    Ignore the bullet code as this is a temp hack I will tidy later.
     
    Hope this helps.
     
    PS
     
    Thanks to Pixel Perfect for supplying gun model etc for my testing, without him I'd be "assetless".
     

    #include "gun/include/gun.h" #include "gun/include/bullet.h" #include "gamelib.h" //#define DEBUG_GUN_POS ActorGun::ActorGun( TScene *pScene, string pFile, TEntity par, TEntity spine, TEntity pTrig, TEntity pRest ): m_fireRate(300), m_range(50) { m_lastFireTime = AppTime(); m_scene = pScene; m_scene->framewerk.GetMain().SetWorld(); m_entity = LoadModel( (char *)pFile.c_str() ); m_spine = spine; m_parent = par; EntityType(m_entity, 5,1); EntityParent( m_entity, m_parent); m_handTrigger = pTrig; m_handRest = pRest; HookUpdateEvent(m_scene->updateEvent); #ifdef DEBUG_GUN_POS HookRenderEvent(m_scene->renderEvent); #endif } bool ActorGun::withinRange( float pDist ) { return (pDist <= m_range); } void ActorGun::Render( float gameLoopTime) { #ifdef DEBUG_GUN_POS TVec3 trigPos = EntityPosition( m_handTrigger, 1); TVec3 restPos = EntityPosition( m_handRest, 1); TVec3 trigBonePos = EntityPosition( m_triggerBone, 1); TVec3 restBonePos = EntityPosition( m_restBone, 1); SetColor( Vec4(1,0,0,1) ); // red tdDraw( m_scene->cam, trigPos, Vec3( trigPos.X, trigPos.Y + 0.1, trigPos.Z) ); SetColor( Vec4(0,0,1,1) ); // blue tdDraw( m_scene->cam, restPos, Vec3( restPos.X, restPos.Y + 0.1, restPos.Z) ); SetColor( Vec4(0,1,0,1) ); // green tdDraw( m_scene->cam, trigBonePos, Vec3( trigBonePos.X, trigBonePos.Y - 0.1, trigBonePos.Z) ); SetColor( Vec4(1,1,1,1) ); // white tdDraw( m_scene->cam, restBonePos, Vec3( restBonePos.X, restBonePos.Y - 0.1, restBonePos.Z) ); TVec3 pos = EntityPosition(m_muzzelBone,1); TVec3 pos1 = TFormVector( Vec3(0,0,-1), m_spine, NULL); pos1.X += pos.X; pos1.Y += pos.Y; pos1.Z += pos.Z; tdDraw( m_scene->cam, pos, pos1 ); #endif } // Position the gun in the actors hand void ActorGun::Update(float gameLoopTime) { TVec3 trigPos; TVec3 restPos = EntityPosition( m_handRest, 1); TVec3 handPos = EntityPosition( m_handTrigger, 1); float diffx = handPos.X - restPos.X; float diffy = handPos.Y - restPos.Y; float diffz = handPos.Z - restPos.Z; AlignToVector( m_entity, Vec3(diffx,diffy,diffz),3 ); TVec3 pos = EntityPosition(m_entity,1); trigPos = EntityPosition(m_triggerBone,1); diffx = pos.X - trigPos.X; diffy = pos.Y - trigPos.Y; diffz = pos.Z - trigPos.Z; trigPos = EntityPosition( m_handTrigger, 1); trigPos.X += diffx; trigPos.Y += diffy; trigPos.Z += diffz; PositionEntity(m_entity, trigPos, 1); list<Bullet*>::iterator bulletList; for(bulletList = m_bullets.begin(); bulletList != m_bullets.end(); ) { if ((*bulletList)->m_life == 0) { delete *bulletList; m_bullets.erase( bulletList++ ); } else ++bulletList; } } bool ActorGun::fireGun() { if (AppTime() < m_lastFireTime ) return false; m_lastFireTime = AppTime() + m_fireRate; Bullet *bullet; TVec3 pos; pos = EntityPosition(m_muzzelBone,1); //bullet = new Bullet( pos, TFormVector( Vec3(0,0,-1), m_muzzelBone, NULL), m_scene ); bullet = new Bullet( pos, TFormVector( Vec3(0,0,-1), m_spine, NULL), m_scene ); m_bullets.push_back( bullet ); return true; } TEntity gunPickSource; int _stdcall gunFilter( TEntity entity ) { if ( isParent(gunPickSource, entity) ) return 0; return 1; } bool ActorGun::canHit( TEntity target, TVec3 tagetPos) { gunPickSource = m_entity; TVec3 pos = EntityPosition(m_muzzelBone,1); TPick pick; if (LinePick( &pick, pos, tagetPos, 0, 0, (BP)gunFilter)) { if (isParent(target, pick.entity )) return true; } else return true; return false; } // Read in gun information from an XML file bool ActorGun::readGunFile( string pFile ) { string filename; string typeName; string bone; filename = AbstractPath( (str)pFile.c_str()); TiXmlDocument doc( filename.c_str() ); TiXmlElement* child = 0; TiXmlElement *bones = 0; doc.LoadFile( filename.c_str() ); bones = doc.FirstChildElement("bones"); if(bones) { child = bones->FirstChildElement(); while(child) { typeName = child->Attribute("type"); if(typeName == "trigger") { m_triggerBone = FindChild( m_entity, (str)child->Attribute("name") ); m_triggerPos = EntityPosition( m_triggerBone ); } typeName = child->Attribute("type"); if(typeName == "rest") { m_restBone = FindChild( m_entity, (str)child->Attribute("name") ); m_restPos = EntityPosition( m_restBone ); } if(typeName == "muzzel") { m_muzzelBone = FindChild( m_entity, (str)child->Attribute("name") ); } child = child->NextSiblingElement(); } } return true; }
     
    Include: -
     

    #ifndef GUN_H #define GUN_H #include "tinyxml.h" #include "gamelib/include/scene.h" #include "event/include/gameEvent.h" #include "gun/include/bullet.h" #include <list> class ActorGun : public GameObject { protected: TScene *m_scene; public: TEntity m_parent; TEntity m_entity; TEntity m_actor; TEntity m_handTrigger; TEntity m_handRest; TEntity m_triggerBone; TEntity m_restBone; TEntity m_muzzelBone; TEntity m_spine; TVec3 m_triggerPos; TVec3 m_restPos; list<Bullet*> m_bullets; float m_fireRate; float m_lastFireTime; float m_range; ActorGun( TScene *pScene, std::string pFile, TEntity par, TEntity spine, TEntity pTrig, TEntity pRest ); virtual void Update( float gameLoopTime); virtual void Render( float gameLoopTime); bool fireGun(); bool withinRange( float pDist ); bool canHit( TEntity target, TVec3 tagetPos); bool readGunFile( std::string pFile ); }; #endif
  8. Chris Paulson
    In a previous blog entry I showed a code sample of the behaviour tree. In the code below is the code that is fired for each behaviour.
     
    The return status mean: -
     
    RUNNING - Keep on running the behaviour
    FAILED - Exit this behaviour with error (has different effect depending on sequeance/selector/parallel node.
    COMPLETE - Exit this behaviour with complete (has different effect depending on sequeance/selector/parallel node.
     
     
     

    #include "actor/include/action.h" #include <alive/TreeBuilder.h> #include <alive/engine/ConstantNode.h> #include <alive/tree/Parallel.h> #include <alive/tree/Repeat.h> #include <alive/tree/ErrorHandler.h> void SpatialAction::setup(Pedestrian& ped, Vision& vis, ActorGun& rifle) { m_pedestrian = &ped; m_vision = &vis; m_rifle = &rifle; } void SpatialAction::init() { m_pedestrian = NULL; m_Observer.BIND(SpatialAction, this, stop); } // Moves the actor to the nearest enemy alive::Status ActionMoveToEnemy::execute() { if (m_pedestrian == 0) { return FAILED; } if (m_vision == 0) { return FAILED; } m_pedestrian->currentAction += "ActionMoveToEnemy,"; if (m_pedestrian->moveToPosition( m_vision->m_lastSeenPos )) return RUNNING; else return FAILED; } void ActionMoveToEnemy::stop(alive::Status) { m_pedestrian->stopMove(); } alive::Status ActionStopMove::execute() { m_pedestrian->stop(); if(m_pedestrian->moveStatus == MOVE_IDLE) return COMPLETED; return RUNNING; } alive::Status hasGun::execute() { if( (m_rifle != NULL) == m_Settings.getEquals()) { m_pedestrian->currentAction += "hasGun,"; return COMPLETED; } return FAILED; } alive::Status WithinFiringRange::execute() { if(!m_rifle) { // If no rifle I'm not in firing range if (!m_Settings.getEquals()) return RUNNING; else return RUNNING; } if( m_rifle->withinRange( m_vision->distanceToEnemy() ) == m_Settings.getEquals()) { m_pedestrian->currentAction += "WithinFiringRange,"; return RUNNING; } return FAILED; } // Moves the actor to the next waypoint alive::Status ActionMoveToWaypoint::execute() { if (m_pedestrian == 0) { return FAILED; } m_pedestrian->currentAction += "ActionMoveToWaypoint,"; if (m_pedestrian->moveToNextWaypoint()) return RUNNING; else return COMPLETED; } void ActionMoveToWaypoint::stop(alive::Status) { m_pedestrian->stopMove(); } alive::Status ActionCurrentMode::execute() { m_pedestrian->currentMode = m_Settings.getmode(); return COMPLETED; } void AimAtEnemy::init() { } alive::Status LookForward::execute() { m_pedestrian->currentAction += "LookForward,"; if (m_pedestrian->anim->m_bodyControl->needToReset( BONE_SPINE ) ) { m_pedestrian->anim->m_bodyControl->reset( BONE_SPINE ); return RUNNING; } if(m_Settings.getParallel()) return RUNNING; else return COMPLETED; }
     
    This isn't the complete code but it might help people get a feel.
  9. Chris Paulson
    Here's the code that loads in the verticies to recast:-
     
    Include:-
     

    #ifndef MESHLOADER_SBX #define MESHLOADER_SBX #include "leo.h" #include "MeshLoaderObj.h" class rcMeshLoaderSBX : public rcMeshLoaderObj { public: TEntity scene; int vcap; int tcap; int nidx; rcMeshLoaderSBX() { vcap = 0; tcap = 0; scene = NULL;}; ~rcMeshLoaderSBX(); void addVertex(float x, float y, float z, int& cap) { rcMeshLoaderObj::addVertex( x, y, z, cap); } void addTriangle(int a, int b, int c, int& cap) { rcMeshLoaderObj::addTriangle(a, b, c, cap); } bool load(const char* filename); private: }; #endif
     
     
     
    Source:-
     

    #include "leo.h" #include "recast.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #define _USE_MATH_DEFINES #include <math.h> #include "ledcast.h" #include "ledcast_statmeshSimple.h" #include "rcMeshLoaderSBX.h" using namespace LEO; rcMeshLoaderSBX *currentLoader; bool processMesh( TEntity entity ) { TMesh mesh = (TMesh)entity; int SurfCnt = CountSurfaces( mesh ); TSurface surface; TVec3 vec; int vertIdx; float factor=1; vertIdx = currentLoader->getVertCount(); for( int s = 1; s <= SurfCnt; s++ ) { surface = GetSurface( mesh, s ); if (surface) { for( int t = 0; t < CountTriangles( surface ); t++) { for( int v = 0; v < 3; v++) { vec = GetVertexPosition( surface, TriangleVertex( surface, t, v )); vec=TFormPoint(Vec3(vec.X,vec.Y,vec.Z),entity,NULL); currentLoader->addVertex( (vec.X)*-factor, (vec.Y)*factor, (vec.Z)*factor, currentLoader->vcap ); } currentLoader->addTriangle( vertIdx+1, vertIdx, vertIdx+2, currentLoader->tcap ); vertIdx=vertIdx+3; } } else break; } return true; } int _stdcall recastLoadMesh( TEntity entity, byte* extra ) { TMesh mesh = (TMesh)entity; std::string entityClass, cn, en; int type, type1; // Only bother with collision type 1 cn=GetEntityKey(entity,"classname"); en=GetEntityKey(entity,"name"); entityClass = GetEntityKey(entity, "class",""); type = atoi( GetEntityKey(entity, "collisiontype","1") ); if (type >0) EntityType(entity, type, 1); type1 = GetEntityType(entity); //if (EntityHidden(entity)) return true; if (entityClass != "Model") return true; if( (type1 != 1)|| (cn=="water")|| (cn=="physics_prop")|| (cn=="physics_pivot")|| (cn=="joint_motor")|| (cn=="info_waypoint")|| (cn=="info_playerstart")|| (cn=="player")|| (cn=="light_directional")|| (cn=="light_spot")|| (cn=="light_point")|| (cn=="env_emitter")|| (cn=="env_sound")) { HideEntity(entity); return true; } int child = CountChildren(entity); for(int i=1; i<=child;i++) { processMesh( GetChild(entity, i) ); }; return true; } int _stdcall recastLoadTerrain( TEntity terrain, byte* extra ) { std::string entityClass; entityClass = GetEntityKey(terrain, "class",""); if (entityClass != "Terrain") return false; float mapSize = 1024; float x; float z; float mapHalfSize = 512; float height; int vertIdx; float minx = 0, minz =0, maxx=0, maxz=0; if (!terrain) return false; vertIdx = currentLoader->getVertCount(); for(x = -mapHalfSize; x<mapHalfSize - 1; x++) { for(z = -mapHalfSize; z<mapHalfSize - 1; z++) { height = TerrainElevation( terrain, x, z); if ((height != 0) || ((x>=minx && x<=maxx) && (z>=minz && z <= maxz))) { minx = __min(minx, x); minz = __min(minz, z); maxx = __max(maxx, x); maxz = __max(maxz, z); //br height = TerrainElevation( terrain, (float)x+1, (float)z); currentLoader->addVertex( (x+1)*-1, height, z, currentLoader->vcap ); //tr height = TerrainElevation( terrain, (float)x+1, (float)z+1); currentLoader->addVertex( (x+1)*-1, height, z+1, currentLoader->vcap ); //bl height = TerrainElevation( terrain, x, z); currentLoader->addVertex( x*-1, height, z, currentLoader->vcap ); currentLoader->addTriangle( vertIdx, vertIdx+1, vertIdx+2, currentLoader->tcap ); vertIdx=vertIdx+3; //Trianlge 2 //tl height = TerrainElevation( terrain, (float)x, (float)z+1); currentLoader->addVertex( x*-1, height, z+1, currentLoader->vcap ); //bl height = TerrainElevation( terrain, (float)x, (float)z); currentLoader->addVertex( x*-1, height, z, currentLoader->vcap ); //tr height = TerrainElevation( terrain, (float)x+1, (float)z+1); currentLoader->addVertex( (x+1)*-1, height, z+1, currentLoader->vcap ); currentLoader->addTriangle( vertIdx, vertIdx+1, vertIdx+2, currentLoader->tcap ); vertIdx=vertIdx+3; } else height = -1; } } return true; } int _stdcall recastLoadMeshNormals( TEntity entity, byte* extra ) { TMesh mesh = (TMesh)entity; std::string entityClass; entityClass = GetEntityKey(entity, "class",""); if (entityClass != "Mesh") return false; TSurface surface; int SurfCnt = CountSurfaces( mesh ); float *normals; normals = (float *)(currentLoader->getNormals()); TVec3 vnorm; for( int s = 1; s <= SurfCnt; s++ ) { surface = GetSurface( mesh, s ); for( int t = 0; t < CountTriangles( surface ); t++) { vnorm = GetVertexNormal( surface, TriangleVertex( surface, t, 1 )); normals[currentLoader->nidx] = vnorm.X; normals[currentLoader->nidx+1] = vnorm.Y; normals[currentLoader->nidx+2] = vnorm.Z; currentLoader->nidx=currentLoader->nidx+3; } } return true; } bool rcMeshLoaderSBX::load(const char* filename) { TWorld world; TWorld cur; cur = CurrentWorld(); std::string f = filename; if (f.find("sbx")) scene = LoadScene((str)filename); else if (f.find("gmf")) scene = LoadModel((str)filename); if (!scene) return false; currentLoader = this; ForEachEntityDo( (BP)recastLoadMesh,NULL, ENTITY_MESH|ENTITY_MODEL ); ForEachEntityDo( (BP)recastLoadTerrain,NULL, ENTITY_TERRAIN ); setNormals( new float[getTriCount()*3] ); nidx = 0; calcNormals(); SetWorld(cur); return true; } rcMeshLoaderSBX::~rcMeshLoaderSBX() { if (scene) FreeEntity(scene); }
  10. Chris Paulson
    For NPC AI to work they have to have simulated vision. This is done by using raycast/linepick.
     
    My vision system does the following:-
     
    Takes into account head position - using head bone
    Takes into account rotation of NPC spine bone (used for aiming)
    Build list of enemies in view range
    Takes into account FOV
    It looks through each of the ememies bones to see if any part of the enemy is visible. It returns location of the first bone found. This is to make sure enemies are still seen even if they are partially obscured. It has a render function to help with debugging the AI.
     
    Here's the code: -
     

    #ifndef VISION_H #define VISION_H #include "gamelib/include/gamelib.h" #include "gamelib/include/scene.h" #include "actor/include/animBody.h" typedef enum SeenState { SEEN_IDLE, SEEN_ALERT, SEEN_ACTIVE }; typedef enum visionMode { VISION_BONE, VISION_BOUNDING, VISION_NAVMESH }; // // Implement actor vision // class Vision { protected: TScene *m_scene; bodyControl *m_body; float m_lastCheckTime; float m_lastSeenTime; float m_notSeenTime; float m_lockTime; float m_updateRate; float m_idleTime; bool m_seen; float m_viewRange; float m_viewAngle; int m_cnt; public: SeenState m_state; map<float,TEntity> m_nearbyEnemy; TEntity m_entity, m_lastTarget, m_pivot, m_head, m_target; string m_team; TPick m_pick; TVec3 m_lastSeenPos, m_targetPos; Vision(TScene *s, TEntity e, TEntity head, bodyControl *pbody); bool canSeeEnemy(visionMode mode= VISION_BONE); bool canSeeEntity(TEntity target, visionMode mode = VISION_BONE); bool canSeeBoundBox(TVec3 pos, TEntity target); bool canSeeBone( TVec3 pos, TEntity target); TVec3 getAimAtLocation(); bool navMeshCanSeeEntity(TEntity target); inline TEntity getLastTarget() { return m_lastTarget; } inline SeenState Vision::getSeenState() { return m_state; } float distanceToEnemy(); void Render(TCamera cam); }; #endif
     

    #include "actor/include/vision.h" #include "gamelib/include/gamemath.h" //#define DEBUG_VISION Vision::Vision( TScene *s, TEntity e, TEntity h, bodyControl *pBody ) : m_lastCheckTime(0), m_updateRate(100), m_lastTarget(NULL), m_seen(false), m_viewRange(50), m_viewAngle(180), m_cnt(0), m_lockTime(5000), m_idleTime(60000) { m_entity = e; m_scene = s; m_cnt = 0; m_lastSeenTime = 0; m_seen = false; m_state = SEEN_IDLE; m_pivot = CreatePivot(); EntityParent( m_pivot, m_entity ); m_body = pBody; m_head = h; m_notSeenTime = AppTime(); m_team = GetEntityKey(e,"team"); } bool Vision::navMeshCanSeeEntity(TEntity target) { static const int MAX_POLYS = 256; dtStatPolyRef m_startRef; dtStatPolyRef m_endRef; dtStatPolyRef m_polys[MAX_POLYS]; dtStatPolyRef m_parent[MAX_POLYS]; float m_spos[3], m_epos[3]; int m_npolys; float m_polyPickExt[3]; float wallDist, navhit; float elapse = AppTime() - m_lastCheckTime; if((target == m_lastTarget) && (elapse < m_updateRate)) return m_seen; m_targetPos = EntityPosition( target, 1); m_lastCheckTime = AppTime(); m_lastTarget = target; m_cnt += 1; m_seen = false; float dist = EntityDistance(m_entity, target); // check If the target is within viewrange if (dist<=m_viewRange) { // observer vector PositionEntity( m_pivot, EntityPosition( m_head,1),1); TVec3 tform = TFormVector( Vec3(0,0,1), m_body->pivot, NULL); AlignToVector(m_pivot, tform, 3, 1); TVec3 observerPos = EntityPosition( m_entity, 1); // pick vector float angle = angleToDest( m_pivot, m_targetPos ); if(angle <= (m_viewAngle/2.0)) { // check If something is blocking the view m_spos[0] = observerPos.X*-1; m_spos[1] = observerPos.Y; m_spos[2] = observerPos.Z; m_epos[0] = m_targetPos.X*-1; m_epos[1] = m_targetPos.Y; m_epos[2] = m_targetPos.Z; m_polyPickExt[0] = 2; m_polyPickExt[1] = 4; m_polyPickExt[2] = 2; m_startRef = m_scene->navmesh->m_navMesh->findNearestPoly(m_spos, m_polyPickExt); m_npolys = m_scene->navmesh->m_navMesh->raycast(m_startRef, m_spos, m_epos, navhit, m_polys, MAX_POLYS); if (navhit >=1) { m_seen = true; m_lastTarget = target; m_lastSeenPos = m_targetPos; m_lastSeenTime = AppTime(); m_state = SEEN_ACTIVE; } } } // observer cannot see target if (!m_seen) { if ((AppTime() - m_lastSeenTime) < this->m_lockTime) m_lastSeenPos = EntityPosition( target, 1); m_lastTarget = NULL; if ((AppTime() - m_lastSeenTime) > m_idleTime) m_state = SEEN_IDLE; else m_state = SEEN_ACTIVE; } else if (m_seen) // Cheat if it's with locked on time { m_lastTarget = target; m_lastSeenPos = EntityPosition( target, 1); } return m_seen; } TEntity pickSource, pickDest; int _stdcall filter( TEntity entity ) { if ( isParent(pickSource, entity) ) return 0; return 1; } bool Vision::canSeeBoundBox( TVec3 pos, TEntity target) { TVec6 box; float margin = 0.3; box = GetEntityAABB( target ); box.X0 += margin; box.Y0 += margin; box.Z0 += margin; box.X1 -= margin; box.Y1 -= margin; box.Z1 -= margin; if (!LinePick( &m_pick, pos, Vec3(box.X0, box.Y0, box.Z0), 0.1,0, (BP)filter )) return true; if (!m_pick.entity || isParent(m_target, m_pick.entity )) return true; if (!LinePick( &m_pick, pos, Vec3(box.X0, box.Y1, box.Z0), 0.1,0, (BP)filter )) return true; if (!m_pick.entity || isParent(m_target, m_pick.entity )) return true; if (!LinePick( &m_pick, pos, Vec3(box.X1, box.Y0, box.Z0), 0.1,0, (BP)filter )) return true; if (!m_pick.entity || isParent(m_target, m_pick.entity )) return true; if (!LinePick( &m_pick, pos, Vec3(box.X1, box.Y1, box.Z0), 0.1,0, (BP)filter )) return true; if (!m_pick.entity || isParent(m_target, m_pick.entity )) return true; if (!LinePick( &m_pick, pos, Vec3(box.X0, box.Y0, box.Z1), 0.1,0, (BP)filter )) return true; if (!m_pick.entity || isParent(m_target, m_pick.entity )) return true; if (!LinePick( &m_pick, pos, Vec3(box.X0, box.Y1, box.Z1), 0.1,0, (BP)filter )) return true; if (!m_pick.entity || isParent(m_target, m_pick.entity )) return true; if (!LinePick( &m_pick, pos, Vec3(box.X1, box.Y0, box.Z1), 0.1,0, (BP)filter )) return true; if (!m_pick.entity || isParent(m_target, m_pick.entity )) return true; if (!LinePick( &m_pick, pos, Vec3(box.X1, box.Y1, box.Z1), 0.1,0, (BP)filter )) return true; if (!m_pick.entity || isParent(m_target, m_pick.entity )) return true; return false; } bool Vision::canSeeBone( TVec3 pos, TEntity target) { int i; for(i = 1; i <= CountChildren(target); i++) { TEntity e = GetChild(target, i); string name=GetEntityKey(e,"name"); if (LinePick( &m_pick, pos, EntityPosition(e,1), 0,0, (BP)filter )) if (isParent(m_target, m_pick.entity )) return true; if (canSeeBone(pos, e )) return true; } return false; } int _stdcall checkIfEnemy( TEntity e, byte* extra ) { Vision *vis = (Vision*)extra; if (GetEntityType(e) != 3) return 1; // Not a NPC/Player string team = GetEntityKey(e,"team"); if (team == vis->m_team) return 1; // On same team vis->m_nearbyEnemy[ EntityDistance(vis->m_entity,e) ] = e; return 1; } bool Vision::canSeeEnemy(visionMode mode) { if (m_lastTarget) { if (GetEntityType(m_lastTarget) == 3) { if ((AppTime() - m_lastSeenTime) < this->m_lockTime) return canSeeEntity(m_lastTarget, mode ); } else { m_lastTarget = NULL; m_state = SEEN_IDLE; } } m_nearbyEnemy.clear(); TVec6 box; TVec3 pos; pos = EntityPosition( m_entity, 1); box.X0 = pos.X - m_viewRange; box.Y0 = pos.Y - m_viewRange; box.Z0 = pos.Z - m_viewRange; box.X1 = pos.X + m_viewRange; box.Y1 = pos.Y + m_viewRange; box.Z1 = pos.Z + m_viewRange; ForEachEntityInAABBDo( box, (BP)checkIfEnemy, (byte*)this, ENTITY_MODEL ); map<float,TEntity>::iterator enemies; enemies=m_nearbyEnemy.begin(); while(enemies!=m_nearbyEnemy.end()) { if (canSeeEntity(enemies->second, mode)) return true; enemies++; } return false; } bool Vision::canSeeEntity(TEntity target, visionMode mode) { BOOL seen; float elapse = AppTime() - m_lastCheckTime; if((target == m_lastTarget) && (elapse < m_updateRate)) return m_seen; m_targetPos = EntityPosition( target, 1); m_lastCheckTime = AppTime(); m_lastTarget = target; m_cnt += 1; seen = false; float dist = EntityDistance(m_entity, target); // check If the target is within viewrange if (dist<=m_viewRange) { // observer vector TVec3 pos = EntityPosition( m_head,1); PositionEntity( m_pivot, pos,1); TVec3 tform = TFormVector( Vec3(0,0,1), m_body->pivot, NULL); AlignToVector(m_pivot, tform, 3, 1); TVec3 observerPos = EntityPosition( m_entity, 1); // pick vector float angle = angleToDest( m_pivot, m_targetPos ); if(angle <= (m_viewAngle/2.0)) { m_target = target; pickSource = m_entity; if (mode == VISION_BONE) seen = canSeeBone(pos, GetChild(GetChild(target,1),1)); else if (mode == VISION_BOUNDING) seen = canSeeBoundBox(pos, GetChild(target,1)); else // todo seen = navMeshCanSeeEntity(GetChild(target,1)); if (seen) { m_lastTarget = target; m_lastSeenPos = m_targetPos; m_lastSeenTime = AppTime(); m_state = SEEN_ACTIVE; } } } // observer cannot see target if (!seen) { m_notSeenTime = AppTime(); if ((AppTime() - m_lastSeenTime) < this->m_lockTime) m_lastSeenPos = EntityPosition( target, 1); if ((AppTime() - m_lastSeenTime) > m_idleTime) m_state = SEEN_IDLE; else m_state = SEEN_ACTIVE; if ((AppTime() - m_lastSeenTime) < 0) // 0.25 sec delay time - to stop twitchy ai m_seen = true; else m_seen = false; } else if (seen) // Cheat if it's with locked on time { m_lastTarget = target; m_lastSeenPos = EntityPosition( target, 1); if ((AppTime() - m_notSeenTime) < 0) // 0.25 sec delay time - to stop twitchy ai m_seen = false; else m_seen = true; } return m_seen; } TVec3 Vision::getAimAtLocation() { if (m_pick.entity) return Vec3(m_pick.X, m_pick.Y, m_pick.Z ); TEntity target = getLastTarget(); target = GetChild( target, 1 ); TVec6 ab = GetEntityAABB( target ); TVec3 pos = EntityPosition( target, 1 ); pos.Y = ab.Y0 + ((ab.Y1 - ab.Y0) * 0.5); return pos; } float Vision::distanceToEnemy() { if (!m_seen) return MAX_DEC; return EntityDistance(m_entity, m_lastTarget); } void Vision::Render(TCamera cam) { #ifdef DEBUG_VISION TVec3 pos,pos1; float elapse = AppTime() - m_lastCheckTime; char buf[150]; string from, to; string stateString[3]; stateString[0] = "idle"; stateString[1] = "alert"; stateString[2] = "active"; pos = EntityPosition(m_pivot,1); pos1 = TFormVector( Vec3( 0,0,-2), m_pivot, NULL); SetColor( Vec4(0,1,0,1) ); // green pos1.X += pos.X; pos1.Y += pos.Y; pos1.Z += pos.Z; tdDraw( cam, pos, pos1 ); tdDrawText( cam, pos, "s" ); tdDrawText( cam, pos1, "e" ); if (m_seen) { SetColor( Vec4(1,1,1,1) ); // White tdDraw( cam, pos, m_lastSeenPos ); from = GetEntityKey(m_lastTarget,"name","none"); to = GetEntityKey(m_entity,"name","none"); sprintf(buf,"See (%f) (%d) %s %s Angle (%f)", elapse, m_cnt, from.c_str(), to.c_str(), angleToDest(m_pivot, m_targetPos) ); tdDrawText( cam, pos, buf ); } else { SetColor( Vec4(1,0,0,1) ); // red sprintf(buf,"No See %s (%f) (%d) Angle (%f)", stateString[m_state].c_str(), elapse, m_cnt, angleToDest(m_pivot, m_targetPos) ); tdDrawText( cam, pos, buf ); if (m_pick.entity) { SetColor( Vec4(0,0,1,1) ); // blue tdDraw( cam, pos, Vec3(m_pick.X, m_pick.Y, m_pick.Z) ); } } if (m_lastTarget) { SetColor( Vec4(0,1,1,1) ); // yellow tdDraw( cam, pos, EntityPosition(m_lastTarget,1) ); } #endif }
     

    #include "gamemath.h" #include "pathsteer.h" #include <assert.h> // // Convert radians to degress // float Rad2Deg (double Angle) { static double ratio = 180.0 / 3.141592653589793238; return Angle * ratio; } float calcAngle( TVec3 pOrigin, TVec3 pDest ) { float a; float b; a = pDest.X - pOrigin.X; b = pDest.Z - pOrigin.Z; if (a == 0 && b == 0) return 0; if (a < 0) return Rad2Deg( acos( b / sqrt( (a * a) + (b * b ))) ); else return Rad2Deg( -acos( b / sqrt( (a * a) + (b * b ))) ); } // // Calc distance to destination // float distanceTo(TVec3 spos, TVec3 epos, distanceType distanceFunction ) { float dx= abs(epos.X - spos.X); float dy = abs(epos.Y - spos.Y); float dz= abs(epos.Z - spos.Z); switch(distanceFunction) { case distanceEuclidean: return sqrt( (dx * dx) + (dz * dz) + (dy * dy)); case distancePseudoEuclidean: return (dx * dx) + (dz * dz) + (dy * dy); case distanceManhatten: return dx + dz + dy; case distanceDiagonalShortcut: if( dx > dz) return 1.4*dz + (dx-dz); else return 1.4*dx + (dz-dx); default: assert( 0,"Bad distance function"); } return 0; } // // Calc angle from src to dst // float angleToDest( TEntity observer, TVec3 targetPos ) { //observer vector TVec3 tform = TFormVector( Vec3(0,0,1), observer, NULL); TVec3 observerPos = EntityPosition( observer,1 ); float dist = distanceTo( observerPos, targetPos ); //pick vector float dx = (observerPos.X - targetPos.X) / dist; float dy = (observerPos.Y - targetPos.Y) / dist; float dz = (observerPos.Z - targetPos.Z) / dist; //dot product float dot = (tform.X*dx) + (tform.Y*dy) + (tform.Z*dz); float ang = Rad2Deg( acos( dot ) ); return ang; }
×
×
  • Create New...