navMesh.cpp
Engine/source/navigation/navMesh.cpp
Classes:
class
class
Public Variables
CornerAngle (0.0f, 90.0f)
bool
For frame signal.
NaturalNumber (1, S32_MAX)
PositiveInt (0, S32_MAX)
ValidCellSize (0.01f, 10.0f)
ValidSlopeAngle (0.0f, 89.9f)
Public Functions
buildCallback(SceneObject * object, void * key)
DefineConsoleFunction(getNavMeshEventManager , S32 , () , "@brief Get the <a href="/coding/class/classeventmanager/">EventManager</a> object <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1a2732ab74fa0237854c2ba0f75f88a624">for</a> all <a href="/coding/class/classnavmesh/">NavMesh</a> updates." )
DefineConsoleFunction(NavMeshIgnore , void , (S32 objid, bool _ignore) , (0, true) , "@brief Flag this object as not generating a navmesh result." )
DefineConsoleFunction(NavMeshUpdateAll , void , (S32 objid, bool remove) , (0, false) , "@brief Update all <a href="/coding/class/classnavmesh/">NavMesh</a> tiles that intersect the given object's world box." )
DefineConsoleFunction(NavMeshUpdateAroundObject , void , (S32 objid, bool remove) , (0, false) , "@brief Update all <a href="/coding/class/classnavmesh/">NavMesh</a> tiles that intersect the given object's world box." )
DefineConsoleFunction(NavMeshUpdateOne , void , (S32 meshid, S32 objid, bool remove) , (0, 0, false) , "@brief Update all tiles in a given <a href="/coding/class/classnavmesh/">NavMesh</a> that intersect the given object's world box." )
DefineEngineMethod(NavMesh , addLink , S32 , (Point3F from, Point3F to, U32 flags) , (0) , "Add a link <a href="/coding/file/cmdgram_8cpp/#cmdgram_8cpp_1a5bafda9519252aa2d0fd038153f77dca">to</a> this <a href="/coding/class/classnavmesh/">NavMesh</a> between two <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1aeab71244afb687f16d8c4f5ee9d6ef0e">points.\n\n</a>" "" )
DefineEngineMethod(NavMesh , build , bool , (bool background, bool save) , (true, false) , "@brief Create a Recast nav mesh." )
DefineEngineMethod(NavMesh , buildLinks , void , () , "@brief Build tiles of this mesh where there are unsynchronised links." )
DefineEngineMethod(NavMesh , buildTiles , void , (Box3F box) , "@brief Rebuild the tiles overlapped by the input box." )
DefineEngineMethod(NavMesh , cancelBuild , void , () , "@brief Cancel the current <a href="/coding/class/classnavmesh/">NavMesh</a> build." )
DefineEngineMethod(NavMesh , createCoverPoints , bool , () , "@brief Create cover points <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1a2732ab74fa0237854c2ba0f75f88a624">for</a> this NavMesh." )
DefineEngineMethod(NavMesh , deleteCoverPoints , void , () , "@brief Remove all cover points <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1a2732ab74fa0237854c2ba0f75f88a624">for</a> this NavMesh." )
DefineEngineMethod(NavMesh , deleteLink , void , (U32 id) , "Delete a given off-mesh link." )
DefineEngineMethod(NavMesh , deleteLinks , void , () , "Deletes all off-mesh links on this NavMesh." )
DefineEngineMethod(NavMesh , getLink , S32 , (Point3F pos) , "Get the off-mesh link closest <a href="/coding/file/cmdgram_8cpp/#cmdgram_8cpp_1a5bafda9519252aa2d0fd038153f77dca">to</a> a given world point." )
DefineEngineMethod(NavMesh , getLinkCount , S32 , () , "Return the number of links this mesh has." )
DefineEngineMethod(NavMesh , getLinkEnd , Point3F , (U32 id) , "Get the ending point of an off-mesh link." )
DefineEngineMethod(NavMesh , getLinkFlags , S32 , (U32 id) , "Get the flags set <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1a2732ab74fa0237854c2ba0f75f88a624">for</a> a particular off-mesh link." )
DefineEngineMethod(NavMesh , getLinkStart , Point3F , (U32 id) , "Get the starting point of an off-mesh link." )
DefineEngineMethod(NavMesh , load , bool , () , "@brief Load this <a href="/coding/class/classnavmesh/">NavMesh</a> from its file." )
DefineEngineMethod(NavMesh , save , void , () , "@brief Save this <a href="/coding/class/classnavmesh/">NavMesh</a> <a href="/coding/file/cmdgram_8cpp/#cmdgram_8cpp_1a5bafda9519252aa2d0fd038153f77dca">to</a> its file." )
ImplementEnumType(NavMeshWaterMethod , "The method used <a href="/coding/file/cmdgram_8cpp/#cmdgram_8cpp_1a5bafda9519252aa2d0fd038153f77dca">to</a> include water surfaces in the <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1aeab71244afb687f16d8c4f5ee9d6ef0e">NavMesh.\n</a>" )
Detailed Description
Public Variables
FRangeValidator CornerAngle (0.0f, 90.0f)
EndImplementEnumType
bool gEditingMission
For frame signal.
IRangeValidator NaturalNumber (1, S32_MAX)
const int NAVMESHSET_MAGIC
const int NAVMESHSET_VERSION
IRangeValidator PositiveInt (0, S32_MAX)
FRangeValidator ValidCellSize (0.01f, 10.0f)
FRangeValidator ValidSlopeAngle (0.0f, 89.9f)
Public Functions
buildCallback(SceneObject * object, void * key)
DefineConsoleFunction(getNavMeshEventManager , S32 , () , "@brief Get the <a href="/coding/class/classeventmanager/">EventManager</a> object <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1a2732ab74fa0237854c2ba0f75f88a624">for</a> all <a href="/coding/class/classnavmesh/">NavMesh</a> updates." )
DefineConsoleFunction(NavMeshIgnore , void , (S32 objid, bool _ignore) , (0, true) , "@brief Flag this object as not generating a navmesh result." )
DefineConsoleFunction(NavMeshUpdateAll , void , (S32 objid, bool remove) , (0, false) , "@brief Update all <a href="/coding/class/classnavmesh/">NavMesh</a> tiles that intersect the given object's world box." )
DefineConsoleFunction(NavMeshUpdateAroundObject , void , (S32 objid, bool remove) , (0, false) , "@brief Update all <a href="/coding/class/classnavmesh/">NavMesh</a> tiles that intersect the given object's world box." )
DefineConsoleFunction(NavMeshUpdateOne , void , (S32 meshid, S32 objid, bool remove) , (0, 0, false) , "@brief Update all tiles in a given <a href="/coding/class/classnavmesh/">NavMesh</a> that intersect the given object's world box." )
DefineEngineMethod(NavMesh , addLink , S32 , (Point3F from, Point3F to, U32 flags) , (0) , "Add a link <a href="/coding/file/cmdgram_8cpp/#cmdgram_8cpp_1a5bafda9519252aa2d0fd038153f77dca">to</a> this <a href="/coding/class/classnavmesh/">NavMesh</a> between two <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1aeab71244afb687f16d8c4f5ee9d6ef0e">points.\n\n</a>" "" )
DefineEngineMethod(NavMesh , build , bool , (bool background, bool save) , (true, false) , "@brief Create a Recast nav mesh." )
DefineEngineMethod(NavMesh , buildLinks , void , () , "@brief Build tiles of this mesh where there are unsynchronised links." )
DefineEngineMethod(NavMesh , buildTiles , void , (Box3F box) , "@brief Rebuild the tiles overlapped by the input box." )
DefineEngineMethod(NavMesh , cancelBuild , void , () , "@brief Cancel the current <a href="/coding/class/classnavmesh/">NavMesh</a> build." )
DefineEngineMethod(NavMesh , createCoverPoints , bool , () , "@brief Create cover points <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1a2732ab74fa0237854c2ba0f75f88a624">for</a> this NavMesh." )
DefineEngineMethod(NavMesh , deleteCoverPoints , void , () , "@brief Remove all cover points <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1a2732ab74fa0237854c2ba0f75f88a624">for</a> this NavMesh." )
DefineEngineMethod(NavMesh , deleteLink , void , (U32 id) , "Delete a given off-mesh link." )
DefineEngineMethod(NavMesh , deleteLinks , void , () , "Deletes all off-mesh links on this NavMesh." )
DefineEngineMethod(NavMesh , getLink , S32 , (Point3F pos) , "Get the off-mesh link closest <a href="/coding/file/cmdgram_8cpp/#cmdgram_8cpp_1a5bafda9519252aa2d0fd038153f77dca">to</a> a given world point." )
DefineEngineMethod(NavMesh , getLinkCount , S32 , () , "Return the number of links this mesh has." )
DefineEngineMethod(NavMesh , getLinkEnd , Point3F , (U32 id) , "Get the ending point of an off-mesh link." )
DefineEngineMethod(NavMesh , getLinkFlags , S32 , (U32 id) , "Get the flags set <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1a2732ab74fa0237854c2ba0f75f88a624">for</a> a particular off-mesh link." )
DefineEngineMethod(NavMesh , getLinkStart , Point3F , (U32 id) , "Get the starting point of an off-mesh link." )
DefineEngineMethod(NavMesh , load , bool , () , "@brief Load this <a href="/coding/class/classnavmesh/">NavMesh</a> from its file." )
DefineEngineMethod(NavMesh , save , void , () , "@brief Save this <a href="/coding/class/classnavmesh/">NavMesh</a> <a href="/coding/file/cmdgram_8cpp/#cmdgram_8cpp_1a5bafda9519252aa2d0fd038153f77dca">to</a> its file." )
DefineEngineMethod(NavMesh , setLinkFlags , void , (U32 id, U32 flags) , "Set the flags of a particular off-mesh link." )
IMPLEMENT_CO_NETOBJECT_V1(NavMesh )
ImplementEnumType(NavMeshWaterMethod , "The method used <a href="/coding/file/cmdgram_8cpp/#cmdgram_8cpp_1a5bafda9519252aa2d0fd038153f77dca">to</a> include water surfaces in the <a href="/coding/file/cmdscan_8cpp/#cmdscan_8cpp_1aeab71244afb687f16d8c4f5ee9d6ef0e">NavMesh.\n</a>" )
1 2//----------------------------------------------------------------------------- 3// Copyright (c) 2014 Daniel Buckmaster 4// 5// Permission is hereby granted, free of charge, to any person obtaining a copy 6// of this software and associated documentation files (the "Software"), to 7// deal in the Software without restriction, including without limitation the 8// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9// sell copies of the Software, and to permit persons to whom the Software is 10// furnished to do so, subject to the following conditions: 11// 12// The above copyright notice and this permission notice shall be included in 13// all copies or substantial portions of the Software. 14// 15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21// IN THE SOFTWARE. 22//----------------------------------------------------------------------------- 23 24#include "navMesh.h" 25#include "navContext.h" 26#include <DetourDebugDraw.h> 27#include <RecastDebugDraw.h> 28 29#include "math/mathUtils.h" 30#include "math/mRandom.h" 31#include "console/consoleTypes.h" 32#include "console/engineAPI.h" 33#include "console/typeValidators.h" 34 35#include "scene/sceneRenderState.h" 36#include "gfx/gfxDrawUtil.h" 37#include "renderInstance/renderPassManager.h" 38#include "gfx/primBuilder.h" 39 40#include "core/stream/bitStream.h" 41#include "math/mathIO.h" 42 43#include "core/fileio.h" 44 45extern bool gEditingMission; 46 47IMPLEMENT_CO_NETOBJECT_V1(NavMesh); 48 49const U32 NavMesh::mMaxVertsPerPoly = 3; 50 51SimObjectPtr<SimSet> NavMesh::smServerSet = NULL; 52 53ImplementEnumType(NavMeshWaterMethod, 54 "The method used to include water surfaces in the NavMesh.\n") 55 { NavMesh::Ignore, "Ignore", "Ignore all water surfaces.\n" }, 56 { NavMesh::Solid, "Solid", "Treat water surfaces as solid and walkable.\n" }, 57 { NavMesh::Impassable, "Impassable", "Treat water as an impassable obstacle.\n" }, 58EndImplementEnumType; 59 60SimSet *NavMesh::getServerSet() 61{ 62 if(!smServerSet) 63 { 64 SimSet *set = NULL; 65 if(Sim::findObject("ServerNavMeshSet", set)) 66 smServerSet = set; 67 else 68 { 69 smServerSet = new SimSet(); 70 smServerSet->registerObject("ServerNavMeshSet"); 71 Sim::getRootGroup()->addObject(smServerSet); 72 } 73 } 74 return smServerSet; 75} 76 77SimObjectPtr<EventManager> NavMesh::smEventManager = NULL; 78 79EventManager *NavMesh::getEventManager() 80{ 81 if(!smEventManager) 82 { 83 smEventManager = new EventManager(); 84 smEventManager->registerObject("NavEventManager"); 85 Sim::getRootGroup()->addObject(smEventManager); 86 smEventManager->setMessageQueue("NavEventManagerQueue"); 87 smEventManager->registerEvent("NavMeshCreated"); 88 smEventManager->registerEvent("NavMeshRemoved"); 89 smEventManager->registerEvent("NavMeshStartUpdate"); 90 smEventManager->registerEvent("NavMeshUpdate"); 91 smEventManager->registerEvent("NavMeshTileUpdate"); 92 smEventManager->registerEvent("NavMeshUpdateBox"); 93 smEventManager->registerEvent("NavMeshObstacleAdded"); 94 smEventManager->registerEvent("NavMeshObstacleRemoved"); 95 } 96 return smEventManager; 97} 98 99DefineConsoleFunction(getNavMeshEventManager, S32, (),, 100 "@brief Get the EventManager object for all NavMesh updates.") 101{ 102 return NavMesh::getEventManager()->getId(); 103} 104 105DefineConsoleFunction(NavMeshUpdateAll, void, (S32 objid, bool remove), (0, false), 106 "@brief Update all NavMesh tiles that intersect the given object's world box.") 107{ 108 SceneObject *obj; 109 if(!Sim::findObject(objid, obj)) 110 return; 111 if(remove) 112 obj->disableCollision(); 113 SimSet *set = NavMesh::getServerSet(); 114 for(U32 i = 0; i < set->size(); i++) 115 { 116 NavMesh *m = dynamic_cast<NavMesh*>(set->at(i)); 117 if (m) 118 { 119 m->cancelBuild(); 120 m->buildTiles(obj->getWorldBox()); 121 } 122 } 123 if(remove) 124 obj->enableCollision(); 125} 126 127DefineConsoleFunction(NavMeshUpdateAroundObject, void, (S32 objid, bool remove), (0, false), 128 "@brief Update all NavMesh tiles that intersect the given object's world box.") 129{ 130 SceneObject *obj; 131 if (!Sim::findObject(objid, obj)) 132 return; 133 if (remove) 134 obj->disableCollision(); 135 SimSet *set = NavMesh::getServerSet(); 136 for (U32 i = 0; i < set->size(); i++) 137 { 138 NavMesh *m = dynamic_cast<NavMesh*>(set->at(i)); 139 if (m) 140 { 141 m->cancelBuild(); 142 m->buildTiles(obj->getWorldBox()); 143 } 144 } 145 if (remove) 146 obj->enableCollision(); 147} 148 149 150DefineConsoleFunction(NavMeshIgnore, void, (S32 objid, bool _ignore), (0, true), 151 "@brief Flag this object as not generating a navmesh result.") 152{ 153 SceneObject *obj; 154 if(!Sim::findObject(objid, obj)) 155 return; 156 157 obj->mPathfindingIgnore = _ignore; 158} 159 160DefineConsoleFunction(NavMeshUpdateOne, void, (S32 meshid, S32 objid, bool remove), (0, 0, false), 161 "@brief Update all tiles in a given NavMesh that intersect the given object's world box.") 162{ 163 NavMesh *mesh; 164 SceneObject *obj; 165 if(!Sim::findObject(meshid, mesh)) 166 { 167 Con::errorf("NavMeshUpdateOne: cannot find NavMesh %d", meshid); 168 return; 169 } 170 if(!Sim::findObject(objid, obj)) 171 { 172 Con::errorf("NavMeshUpdateOne: cannot find SceneObject %d", objid); 173 return; 174 } 175 if(remove) 176 obj->disableCollision(); 177 mesh->buildTiles(obj->getWorldBox()); 178 if(remove) 179 obj->enableCollision(); 180} 181 182NavMesh::NavMesh() 183{ 184 mTypeMask |= StaticShapeObjectType | MarkerObjectType; 185 mFileName = StringTable->EmptyString(); 186 mNetFlags.clear(Ghostable); 187 188 mSaveIntermediates = false; 189 nm = NULL; 190 ctx = NULL; 191 192 mWaterMethod = Ignore; 193 194 dMemset(&cfg, 0, sizeof(cfg)); 195 mCellSize = mCellHeight = 0.2f; 196 mWalkableHeight = 2.0f; 197 mWalkableClimb = 0.3f; 198 mWalkableRadius = 0.5f; 199 mWalkableSlope = 40.0f; 200 mBorderSize = 1; 201 mDetailSampleDist = 6.0f; 202 mDetailSampleMaxError = 1.0f; 203 mMaxEdgeLen = 12; 204 mMaxSimplificationError = 1.3f; 205 mMinRegionArea = 8; 206 mMergeRegionArea = 20; 207 mTileSize = 10.0f; 208 mMaxPolysPerTile = 128; 209 210 mSmallCharacters = false; 211 mRegularCharacters = true; 212 mLargeCharacters = false; 213 mVehicles = false; 214 215 mCoverSet = StringTable->EmptyString(); 216 mInnerCover = false; 217 mCoverDist = 1.0f; 218 mPeekDist = 0.7f; 219 220 mAlwaysRender = false; 221 222 mBuilding = false; 223} 224 225NavMesh::~NavMesh() 226{ 227 dtFreeNavMesh(nm); 228 nm = NULL; 229 delete ctx; 230 ctx = NULL; 231} 232 233bool NavMesh::setProtectedDetailSampleDist(void *obj, const char *index, const char *data) 234{ 235 F32 dist = dAtof(data); 236 if(dist == 0.0f || dist >= 0.9f) 237 return true; 238 Con::errorf("NavMesh::detailSampleDist must be 0 or greater than 0.9!"); 239 return false; 240} 241 242bool NavMesh::setProtectedAlwaysRender(void *obj, const char *index, const char *data) 243{ 244 NavMesh *mesh = static_cast<NavMesh*>(obj); 245 bool always = dAtob(data); 246 if(always) 247 { 248 if(!gEditingMission) 249 mesh->mNetFlags.set(Ghostable); 250 } 251 else 252 { 253 if(!gEditingMission) 254 mesh->mNetFlags.clear(Ghostable); 255 } 256 mesh->mAlwaysRender = always; 257 mesh->setMaskBits(LoadFlag); 258 return true; 259} 260 261FRangeValidator ValidCellSize(0.01f, 10.0f); 262FRangeValidator ValidSlopeAngle(0.0f, 89.9f); 263IRangeValidator PositiveInt(0, S32_MAX); 264IRangeValidator NaturalNumber(1, S32_MAX); 265FRangeValidator CornerAngle(0.0f, 90.0f); 266 267void NavMesh::initPersistFields() 268{ 269 addGroup("NavMesh Options"); 270 271 addField("fileName", TypeString, Offset(mFileName, NavMesh), 272 "Name of the data file to store this navmesh in (relative to engine executable)."); 273 274 addField("waterMethod", TYPEID<NavMeshWaterMethod>(), Offset(mWaterMethod, NavMesh), 275 "The method to use to handle water surfaces."); 276 277 addFieldV("cellSize", TypeF32, Offset(mCellSize, NavMesh), &ValidCellSize, 278 "Length/width of a voxel."); 279 addFieldV("cellHeight", TypeF32, Offset(mCellHeight, NavMesh), &ValidCellSize, 280 "Height of a voxel."); 281 addFieldV("tileSize", TypeF32, Offset(mTileSize, NavMesh), &CommonValidators::PositiveNonZeroFloat, 282 "The horizontal size of tiles."); 283 284 addFieldV("actorHeight", TypeF32, Offset(mWalkableHeight, NavMesh), &CommonValidators::PositiveFloat, 285 "Height of an actor."); 286 addFieldV("actorClimb", TypeF32, Offset(mWalkableClimb, NavMesh), &CommonValidators::PositiveFloat, 287 "Maximum climbing height of an actor."); 288 addFieldV("actorRadius", TypeF32, Offset(mWalkableRadius, NavMesh), &CommonValidators::PositiveFloat, 289 "Radius of an actor."); 290 addFieldV("walkableSlope", TypeF32, Offset(mWalkableSlope, NavMesh), &ValidSlopeAngle, 291 "Maximum walkable slope in degrees."); 292 293 addField("smallCharacters", TypeBool, Offset(mSmallCharacters, NavMesh), 294 "Is this NavMesh for smaller-than-usual characters?"); 295 addField("regularCharacters", TypeBool, Offset(mRegularCharacters, NavMesh), 296 "Is this NavMesh for regular-sized characters?"); 297 addField("largeCharacters", TypeBool, Offset(mLargeCharacters, NavMesh), 298 "Is this NavMesh for larger-than-usual characters?"); 299 addField("vehicles", TypeBool, Offset(mVehicles, NavMesh), 300 "Is this NavMesh for characters driving vehicles?"); 301 302 endGroup("NavMesh Options"); 303 304 addGroup("NavMesh Annotations"); 305 306 addField("coverGroup", TypeString, Offset(mCoverSet, NavMesh), 307 "Name of the SimGroup to store cover points in."); 308 309 addField("innerCover", TypeBool, Offset(mInnerCover, NavMesh), 310 "Add cover points everywhere, not just on corners?"); 311 312 addField("coverDist", TypeF32, Offset(mCoverDist, NavMesh), 313 "Distance from the edge of the NavMesh to search for cover."); 314 addField("peekDist", TypeF32, Offset(mPeekDist, NavMesh), 315 "Distance to the side of each cover point that peeking happens."); 316 317 endGroup("NavMesh Annotations"); 318 319 addGroup("NavMesh Rendering"); 320 321 addProtectedField("alwaysRender", TypeBool, Offset(mAlwaysRender, NavMesh), 322 &setProtectedAlwaysRender, &defaultProtectedGetFn, 323 "Display this NavMesh even outside the editor."); 324 325 endGroup("NavMesh Rendering"); 326 327 addGroup("NavMesh Advanced Options"); 328 329 addFieldV("borderSize", TypeS32, Offset(mBorderSize, NavMesh), &PositiveInt, 330 "Size of the non-walkable border around the navigation mesh (in voxels)."); 331 addProtectedField("detailSampleDist", TypeF32, Offset(mDetailSampleDist, NavMesh), 332 &setProtectedDetailSampleDist, &defaultProtectedGetFn, 333 "Sets the sampling distance to use when generating the detail mesh."); 334 addFieldV("detailSampleError", TypeF32, Offset(mDetailSampleMaxError, NavMesh), &CommonValidators::PositiveFloat, 335 "The maximum distance the detail mesh surface should deviate from heightfield data."); 336 addFieldV("maxEdgeLen", TypeS32, Offset(mDetailSampleDist, NavMesh), &PositiveInt, 337 "The maximum allowed length for contour edges along the border of the mesh."); 338 addFieldV("simplificationError", TypeF32, Offset(mMaxSimplificationError, NavMesh), &CommonValidators::PositiveFloat, 339 "The maximum distance a simplfied contour's border edges should deviate from the original raw contour."); 340 addFieldV("minRegionArea", TypeS32, Offset(mMinRegionArea, NavMesh), &PositiveInt, 341 "The minimum number of cells allowed to form isolated island areas."); 342 addFieldV("mergeRegionArea", TypeS32, Offset(mMergeRegionArea, NavMesh), &PositiveInt, 343 "Any regions with a span count smaller than this value will, if possible, be merged with larger regions."); 344 addFieldV("maxPolysPerTile", TypeS32, Offset(mMaxPolysPerTile, NavMesh), &NaturalNumber, 345 "The maximum number of polygons allowed in a tile."); 346 347 endGroup("NavMesh Advanced Options"); 348 349 Parent::initPersistFields(); 350} 351 352bool NavMesh::onAdd() 353{ 354 if(!Parent::onAdd()) 355 return false; 356 357 mObjBox.set(Point3F(-0.5f, -0.5f, -0.5f), 358 Point3F( 0.5f, 0.5f, 0.5f)); 359 resetWorldBox(); 360 361 addToScene(); 362 363 if(gEditingMission || mAlwaysRender) 364 { 365 mNetFlags.set(Ghostable); 366 if(isClientObject()) 367 renderToDrawer(); 368 } 369 370 if(isServerObject()) 371 { 372 getServerSet()->addObject(this); 373 ctx = new NavContext(); 374 setProcessTick(true); 375 if(getEventManager()) 376 getEventManager()->postEvent("NavMeshCreated", getIdString()); 377 } 378 379 load(); 380 381 return true; 382} 383 384void NavMesh::onRemove() 385{ 386 if(getEventManager()) 387 getEventManager()->postEvent("NavMeshRemoved", getIdString()); 388 389 removeFromScene(); 390 391 Parent::onRemove(); 392} 393 394void NavMesh::setTransform(const MatrixF &mat) 395{ 396 Parent::setTransform(mat); 397} 398 399void NavMesh::setScale(const VectorF &scale) 400{ 401 Parent::setScale(scale); 402} 403 404S32 NavMesh::addLink(const Point3F &from, const Point3F &to, U32 flags) 405{ 406 Point3F rcFrom = DTStoRC(from), rcTo = DTStoRC(to); 407 mLinkVerts.push_back(rcFrom.x); 408 mLinkVerts.push_back(rcFrom.y); 409 mLinkVerts.push_back(rcFrom.z); 410 mLinkVerts.push_back(rcTo.x); 411 mLinkVerts.push_back(rcTo.y); 412 mLinkVerts.push_back(rcTo.z); 413 mLinksUnsynced.push_back(true); 414 mLinkRads.push_back(mWalkableRadius); 415 mLinkDirs.push_back(0); 416 mLinkAreas.push_back(OffMeshArea); 417 if (flags == 0) { 418 Point3F dir = to - from; 419 F32 drop = -dir.z; 420 dir.z = 0; 421 // If we drop more than we travel horizontally, we're a drop link. 422 if(drop > dir.len()) 423 mLinkFlags.push_back(DropFlag); 424 else 425 mLinkFlags.push_back(JumpFlag); 426 } 427 mLinkIDs.push_back(1000 + mCurLinkID); 428 mLinkSelectStates.push_back(Unselected); 429 mDeleteLinks.push_back(false); 430 mCurLinkID++; 431 return mLinkIDs.size() - 1; 432} 433 434DefineEngineMethod(NavMesh, addLink, S32, (Point3F from, Point3F to, U32 flags), (0), 435 "Add a link to this NavMesh between two points.\n\n" 436 "") 437{ 438 return object->addLink(from, to, flags); 439} 440 441S32 NavMesh::getLink(const Point3F &pos) 442{ 443 for(U32 i = 0; i < mLinkIDs.size(); i++) 444 { 445 if(mDeleteLinks[i]) 446 continue; 447 SphereF start(getLinkStart(i), mLinkRads[i]); 448 SphereF end(getLinkEnd(i), mLinkRads[i]); 449 if(start.isContained(pos) || end.isContained(pos)) 450 return i; 451 } 452 return -1; 453} 454 455DefineEngineMethod(NavMesh, getLink, S32, (Point3F pos),, 456 "Get the off-mesh link closest to a given world point.") 457{ 458 return object->getLink(pos); 459} 460 461S32 NavMesh::getLinkCount() 462{ 463 return mLinkIDs.size(); 464} 465 466DefineEngineMethod(NavMesh, getLinkCount, S32, (),, 467 "Return the number of links this mesh has.") 468{ 469 return object->getLinkCount(); 470} 471 472LinkData NavMesh::getLinkFlags(U32 idx) 473{ 474 if(idx < mLinkIDs.size()) 475 { 476 return LinkData(mLinkFlags[idx]); 477 } 478 return LinkData(); 479} 480 481DefineEngineMethod(NavMesh, getLinkFlags, S32, (U32 id),, 482 "Get the flags set for a particular off-mesh link.") 483{ 484 return object->getLinkFlags(id).getFlags(); 485} 486 487void NavMesh::setLinkFlags(U32 idx, const LinkData &d) 488{ 489 if(idx < mLinkIDs.size()) 490 { 491 mLinkFlags[idx] = d.getFlags(); 492 mLinksUnsynced[idx] = true; 493 } 494} 495 496DefineEngineMethod(NavMesh, setLinkFlags, void, (U32 id, U32 flags),, 497 "Set the flags of a particular off-mesh link.") 498{ 499 LinkData d(flags); 500 object->setLinkFlags(id, d); 501} 502 503Point3F NavMesh::getLinkStart(U32 idx) 504{ 505 return RCtoDTS(Point3F( 506 mLinkVerts[idx*6], 507 mLinkVerts[idx*6 + 1], 508 mLinkVerts[idx*6 + 2])); 509} 510 511DefineEngineMethod(NavMesh, getLinkStart, Point3F, (U32 id),, 512 "Get the starting point of an off-mesh link.") 513{ 514 return object->getLinkStart(id); 515} 516 517Point3F NavMesh::getLinkEnd(U32 idx) 518{ 519 return RCtoDTS(Point3F( 520 mLinkVerts[idx*6 + 3], 521 mLinkVerts[idx*6 + 4], 522 mLinkVerts[idx*6 + 5])); 523} 524 525DefineEngineMethod(NavMesh, getLinkEnd, Point3F, (U32 id),, 526 "Get the ending point of an off-mesh link.") 527{ 528 return object->getLinkEnd(id); 529} 530 531void NavMesh::selectLink(U32 idx, bool select, bool hover) 532{ 533 if(idx < mLinkIDs.size()) 534 { 535 if(!select) 536 mLinkSelectStates[idx] = Unselected; 537 else 538 mLinkSelectStates[idx] = hover ? Hovered : Selected; 539 } 540} 541 542void NavMesh::eraseLink(U32 i) 543{ 544 mLinkVerts.erase(i*6, 6); 545 mLinksUnsynced.erase(i); 546 mLinkRads.erase(i); 547 mLinkDirs.erase(i); 548 mLinkAreas.erase(i); 549 mLinkFlags.erase(i); 550 mLinkIDs.erase(i); 551 mLinkSelectStates.erase(i); 552 mDeleteLinks.erase(i); 553} 554 555void NavMesh::eraseLinks() 556{ 557 mLinkVerts.clear(); 558 mLinksUnsynced.clear(); 559 mLinkRads.clear(); 560 mLinkDirs.clear(); 561 mLinkAreas.clear(); 562 mLinkFlags.clear(); 563 mLinkIDs.clear(); 564 mLinkSelectStates.clear(); 565 mDeleteLinks.clear(); 566} 567 568void NavMesh::setLinkCount(U32 c) 569{ 570 eraseLinks(); 571 mLinkVerts.setSize(c * 6); 572 mLinksUnsynced.setSize(c); 573 mLinkRads.setSize(c); 574 mLinkDirs.setSize(c); 575 mLinkAreas.setSize(c); 576 mLinkFlags.setSize(c); 577 mLinkIDs.setSize(c); 578 mLinkSelectStates.setSize(c); 579 mDeleteLinks.setSize(c); 580} 581 582void NavMesh::deleteLink(U32 idx) 583{ 584 if(idx < mLinkIDs.size()) 585 { 586 mDeleteLinks[idx] = true; 587 if(mLinksUnsynced[idx]) 588 eraseLink(idx); 589 else 590 mLinksUnsynced[idx] = true; 591 } 592} 593 594DefineEngineMethod(NavMesh, deleteLink, void, (U32 id),, 595 "Delete a given off-mesh link.") 596{ 597 object->deleteLink(id); 598} 599 600DefineEngineMethod(NavMesh, deleteLinks, void, (),, 601 "Deletes all off-mesh links on this NavMesh.") 602{ 603 //object->eraseLinks(); 604} 605 606bool NavMesh::build(bool background, bool saveIntermediates) 607{ 608 if(mBuilding) 609 cancelBuild(); 610 else 611 { 612 if(getEventManager()) 613 getEventManager()->postEvent("NavMeshStartUpdate", getIdString()); 614 } 615 616 mBuilding = true; 617 618 ctx->startTimer(RC_TIMER_TOTAL); 619 620 dtFreeNavMesh(nm); 621 // Allocate a new navmesh. 622 nm = dtAllocNavMesh(); 623 if(!nm) 624 { 625 Con::errorf("Could not allocate dtNavMesh for NavMesh %s", getIdString()); 626 return false; 627 } 628 629 updateConfig(); 630 631 // Build navmesh parameters from console members. 632 dtNavMeshParams params; 633 rcVcopy(params.orig, cfg.bmin); 634 params.tileWidth = cfg.tileSize * mCellSize; 635 params.tileHeight = cfg.tileSize * mCellSize; 636 params.maxTiles = mCeil(getWorldBox().len_x() / params.tileWidth) * mCeil(getWorldBox().len_y() / params.tileHeight); 637 params.maxPolys = mMaxPolysPerTile; 638 639 // Initialise our navmesh. 640 if(dtStatusFailed(nm->init(¶ms))) 641 { 642 Con::errorf("Could not init dtNavMesh for NavMesh %s", getIdString()); 643 return false; 644 } 645 646 // Update links to be deleted. 647 for(U32 i = 0; i < mLinkIDs.size();) 648 { 649 if(mDeleteLinks[i]) 650 eraseLink(i); 651 else 652 i++; 653 } 654 mLinksUnsynced.fill(false); 655 mCurLinkID = 0; 656 657 mSaveIntermediates = saveIntermediates; 658 659 updateTiles(true); 660 661 if(!background) 662 { 663 while(!mDirtyTiles.empty()) 664 buildNextTile(); 665 } 666 667 return true; 668} 669 670DefineEngineMethod(NavMesh, build, bool, (bool background, bool save), (true, false), 671 "@brief Create a Recast nav mesh.") 672{ 673 return object->build(background, save); 674} 675 676void NavMesh::cancelBuild() 677{ 678 mDirtyTiles.clear(); 679 ctx->stopTimer(RC_TIMER_TOTAL); 680 mBuilding = false; 681} 682 683DefineEngineMethod(NavMesh, cancelBuild, void, (),, 684 "@brief Cancel the current NavMesh build.") 685{ 686 object->cancelBuild(); 687} 688 689void NavMesh::inspectPostApply() 690{ 691 if(mBuilding) 692 cancelBuild(); 693} 694 695void NavMesh::updateConfig() 696{ 697 // Build rcConfig object from our console members. 698 dMemset(&cfg, 0, sizeof(cfg)); 699 cfg.cs = mCellSize; 700 cfg.ch = mCellHeight; 701 Box3F box = DTStoRC(getWorldBox()); 702 rcVcopy(cfg.bmin, box.minExtents); 703 rcVcopy(cfg.bmax, box.maxExtents); 704 rcCalcGridSize(cfg.bmin, cfg.bmax, cfg.cs, &cfg.width, &cfg.height); 705 706 cfg.walkableHeight = mCeil(mWalkableHeight / mCellHeight); 707 cfg.walkableClimb = mCeil(mWalkableClimb / mCellHeight); 708 cfg.walkableRadius = mCeil(mWalkableRadius / mCellSize); 709 cfg.walkableSlopeAngle = mWalkableSlope; 710 cfg.borderSize = cfg.walkableRadius + 3; 711 712 cfg.detailSampleDist = mDetailSampleDist; 713 cfg.detailSampleMaxError = mDetailSampleMaxError; 714 cfg.maxEdgeLen = mMaxEdgeLen; 715 cfg.maxSimplificationError = mMaxSimplificationError; 716 cfg.maxVertsPerPoly = mMaxVertsPerPoly; 717 cfg.minRegionArea = mMinRegionArea; 718 cfg.mergeRegionArea = mMergeRegionArea; 719 cfg.tileSize = mTileSize / cfg.cs; 720} 721 722S32 NavMesh::getTile(const Point3F& pos) 723{ 724 if(mBuilding) 725 return -1; 726 for(U32 i = 0; i < mTiles.size(); i++) 727 { 728 if(mTiles[i].box.isContained(pos)) 729 return i; 730 } 731 return -1; 732} 733 734Box3F NavMesh::getTileBox(U32 id) 735{ 736 if(mBuilding || id >= mTiles.size()) 737 return Box3F::Invalid; 738 return mTiles[id].box; 739} 740 741void NavMesh::updateTiles(bool dirty) 742{ 743 if(!isProperlyAdded()) 744 return; 745 746 mTiles.clear(); 747 mTileData.clear(); 748 mDirtyTiles.clear(); 749 750 const Box3F &box = DTStoRC(getWorldBox()); 751 if(box.isEmpty()) 752 return; 753 754 updateConfig(); 755 756 // Calculate tile dimensions. 757 const U32 ts = cfg.tileSize; 758 const U32 tw = (cfg.width + ts-1) / ts; 759 const U32 th = (cfg.height + ts-1) / ts; 760 const F32 tcs = cfg.tileSize * cfg.cs; 761 762 // Iterate over tiles. 763 F32 tileBmin[3], tileBmax[3]; 764 for(U32 y = 0; y < th; ++y) 765 { 766 for(U32 x = 0; x < tw; ++x) 767 { 768 tileBmin[0] = cfg.bmin[0] + x*tcs; 769 tileBmin[1] = cfg.bmin[1]; 770 tileBmin[2] = cfg.bmin[2] + y*tcs; 771 772 tileBmax[0] = cfg.bmin[0] + (x+1)*tcs; 773 tileBmax[1] = cfg.bmax[1]; 774 tileBmax[2] = cfg.bmin[2] + (y+1)*tcs; 775 776 mTiles.push_back( 777 Tile(RCtoDTS(tileBmin, tileBmax), 778 x, y, 779 tileBmin, tileBmax)); 780 781 if(dirty) 782 mDirtyTiles.push_back_unique(mTiles.size() - 1); 783 784 if(mSaveIntermediates) 785 mTileData.increment(); 786 } 787 } 788} 789 790void NavMesh::processTick(const Move *move) 791{ 792 buildNextTile(); 793} 794 795void NavMesh::buildNextTile() 796{ 797 if(!mDirtyTiles.empty()) 798 { 799 // Pop a single dirty tile and process it. 800 U32 i = mDirtyTiles.front(); 801 mDirtyTiles.pop_front(); 802 const Tile &tile = mTiles[i]; 803 // Intermediate data for tile build. 804 TileData tempdata; 805 TileData &tdata = mSaveIntermediates ? mTileData[i] : tempdata; 806 807 // Remove any previous data. 808 nm->removeTile(nm->getTileRefAt(tile.x, tile.y, 0), 0, 0); 809 810 // Generate navmesh for this tile. 811 U32 dataSize = 0; 812 unsigned char* data = buildTileData(tile, tdata, dataSize); 813 if(data) 814 { 815 // Add new data (navmesh owns and deletes the data). 816 dtStatus status = nm->addTile(data, dataSize, DT_TILE_FREE_DATA, 0, 0); 817 int success = 1; 818 if(dtStatusFailed(status)) 819 { 820 success = 0; 821 dtFree(data); 822 } 823 if(getEventManager()) 824 { 825 String str = String::ToString("%d %d %d (%d, %d) %d %.3f %s", 826 getId(), 827 i, mTiles.size(), 828 tile.x, tile.y, 829 success, 830 ctx->getAccumulatedTime(RC_TIMER_TOTAL) / 1000.0f, 831 castConsoleTypeToString(tile.box)); 832 getEventManager()->postEvent("NavMeshTileUpdate", str.c_str()); 833 setMaskBits(LoadFlag); 834 } 835 } 836 // Did we just build the last tile? 837 if(mDirtyTiles.empty()) 838 { 839 ctx->stopTimer(RC_TIMER_TOTAL); 840 if(getEventManager()) 841 { 842 String str = String::ToString("%d", getId()); 843 getEventManager()->postEvent("NavMeshUpdate", str.c_str()); 844 setMaskBits(LoadFlag); 845 } 846 mBuilding = false; 847 } 848 } 849} 850 851static void buildCallback(SceneObject* object,void *key) 852{ 853 SceneContainer::CallbackInfo* info = reinterpret_cast<SceneContainer::CallbackInfo*>(key); 854 if (!object->mPathfindingIgnore) 855 object->buildPolyList(info->context,info->polyList,info->boundingBox,info->boundingSphere); 856} 857 858unsigned char *NavMesh::buildTileData(const Tile &tile, TileData &data, U32 &dataSize) 859{ 860 // Push out tile boundaries a bit. 861 F32 tileBmin[3], tileBmax[3]; 862 rcVcopy(tileBmin, tile.bmin); 863 rcVcopy(tileBmax, tile.bmax); 864 tileBmin[0] -= cfg.borderSize * cfg.cs; 865 tileBmin[2] -= cfg.borderSize * cfg.cs; 866 tileBmax[0] += cfg.borderSize * cfg.cs; 867 tileBmax[2] += cfg.borderSize * cfg.cs; 868 869 // Parse objects from level into RC-compatible format. 870 Box3F box = RCtoDTS(tileBmin, tileBmax); 871 SceneContainer::CallbackInfo info; 872 info.context = PLC_Navigation; 873 info.boundingBox = box; 874 data.geom.clear(); 875 info.polyList = &data.geom; 876 info.key = this; 877 getContainer()->findObjects(box, StaticObjectType | DynamicShapeObjectType, buildCallback, &info); 878 879 // Parse water objects into the same list, but remember how much geometry was /not/ water. 880 U32 nonWaterVertCount = data.geom.getVertCount(); 881 U32 nonWaterTriCount = data.geom.getTriCount(); 882 if(mWaterMethod != Ignore) 883 { 884 getContainer()->findObjects(box, WaterObjectType, buildCallback, &info); 885 } 886 887 // Check for no geometry. 888 if (!data.geom.getVertCount()) 889 { 890 data.geom.clear(); 891 return NULL; 892 } 893 894 // Figure out voxel dimensions of this tile. 895 U32 width = 0, height = 0; 896 width = cfg.tileSize + cfg.borderSize * 2; 897 height = cfg.tileSize + cfg.borderSize * 2; 898 899 // Create a heightfield to voxelise our input geometry. 900 data.hf = rcAllocHeightfield(); 901 if(!data.hf) 902 { 903 Con::errorf("Out of memory (rcHeightField) for NavMesh %s", getIdString()); 904 return NULL; 905 } 906 if(!rcCreateHeightfield(ctx, *data.hf, width, height, tileBmin, tileBmax, cfg.cs, cfg.ch)) 907 { 908 Con::errorf("Could not generate rcHeightField for NavMesh %s", getIdString()); 909 return NULL; 910 } 911 912 unsigned char *areas = new unsigned char[data.geom.getTriCount()]; 913 914 dMemset(areas, 0, data.geom.getTriCount() * sizeof(unsigned char)); 915 916 // Mark walkable triangles with the appropriate area flags, and rasterize. 917 if(mWaterMethod == Solid) 918 { 919 // Treat water as solid: i.e. mark areas as walkable based on angle. 920 rcMarkWalkableTriangles(ctx, cfg.walkableSlopeAngle, 921 data.geom.getVerts(), data.geom.getVertCount(), 922 data.geom.getTris(), data.geom.getTriCount(), areas); 923 } 924 else 925 { 926 // Treat water as impassable: leave all area flags 0. 927 rcMarkWalkableTriangles(ctx, cfg.walkableSlopeAngle, 928 data.geom.getVerts(), nonWaterVertCount, 929 data.geom.getTris(), nonWaterTriCount, areas); 930 } 931 rcRasterizeTriangles(ctx, 932 data.geom.getVerts(), data.geom.getVertCount(), 933 data.geom.getTris(), areas, data.geom.getTriCount(), 934 *data.hf, cfg.walkableClimb); 935 936 delete[] areas; 937 938 // Filter out areas with low ceilings and other stuff. 939 rcFilterLowHangingWalkableObstacles(ctx, cfg.walkableClimb, *data.hf); 940 rcFilterLedgeSpans(ctx, cfg.walkableHeight, cfg.walkableClimb, *data.hf); 941 rcFilterWalkableLowHeightSpans(ctx, cfg.walkableHeight, *data.hf); 942 943 data.chf = rcAllocCompactHeightfield(); 944 if(!data.chf) 945 { 946 Con::errorf("Out of memory (rcCompactHeightField) for NavMesh %s", getIdString()); 947 return NULL; 948 } 949 if(!rcBuildCompactHeightfield(ctx, cfg.walkableHeight, cfg.walkableClimb, *data.hf, *data.chf)) 950 { 951 Con::errorf("Could not generate rcCompactHeightField for NavMesh %s", getIdString()); 952 return NULL; 953 } 954 if(!rcErodeWalkableArea(ctx, cfg.walkableRadius, *data.chf)) 955 { 956 Con::errorf("Could not erode walkable area for NavMesh %s", getIdString()); 957 return NULL; 958 } 959 960 //-------------------------- 961 // Todo: mark areas here. 962 //const ConvexVolume* vols = m_geom->getConvexVolumes(); 963 //for (int i = 0; i < m_geom->getConvexVolumeCount(); ++i) 964 //rcMarkConvexPolyArea(m_ctx, vols[i].verts, vols[i].nverts, vols[i].hmin, vols[i].hmax, (unsigned char)vols[i].area, *m_chf); 965 //-------------------------- 966 967 if(false) 968 { 969 if(!rcBuildRegionsMonotone(ctx, *data.chf, cfg.borderSize, cfg.minRegionArea, cfg.mergeRegionArea)) 970 { 971 Con::errorf("Could not build regions for NavMesh %s", getIdString()); 972 return NULL; 973 } 974 } 975 else 976 { 977 if(!rcBuildDistanceField(ctx, *data.chf)) 978 { 979 Con::errorf("Could not build distance field for NavMesh %s", getIdString()); 980 return NULL; 981 } 982 if(!rcBuildRegions(ctx, *data.chf, cfg.borderSize, cfg.minRegionArea, cfg.mergeRegionArea)) 983 { 984 Con::errorf("Could not build regions for NavMesh %s", getIdString()); 985 return NULL; 986 } 987 } 988 989 data.cs = rcAllocContourSet(); 990 if(!data.cs) 991 { 992 Con::errorf("Out of memory (rcContourSet) for NavMesh %s", getIdString()); 993 return NULL; 994 } 995 if(!rcBuildContours(ctx, *data.chf, cfg.maxSimplificationError, cfg.maxEdgeLen, *data.cs)) 996 { 997 Con::errorf("Could not construct rcContourSet for NavMesh %s", getIdString()); 998 return NULL; 999 } 1000 if(data.cs->nconts <= 0) 1001 { 1002 Con::errorf("No contours in rcContourSet for NavMesh %s", getIdString()); 1003 return NULL; 1004 } 1005 1006 data.pm = rcAllocPolyMesh(); 1007 if(!data.pm) 1008 { 1009 Con::errorf("Out of memory (rcPolyMesh) for NavMesh %s", getIdString()); 1010 return NULL; 1011 } 1012 if(!rcBuildPolyMesh(ctx, *data.cs, cfg.maxVertsPerPoly, *data.pm)) 1013 { 1014 Con::errorf("Could not construct rcPolyMesh for NavMesh %s", getIdString()); 1015 return NULL; 1016 } 1017 1018 data.pmd = rcAllocPolyMeshDetail(); 1019 if(!data.pmd) 1020 { 1021 Con::errorf("Out of memory (rcPolyMeshDetail) for NavMesh %s", getIdString()); 1022 return NULL; 1023 } 1024 if(!rcBuildPolyMeshDetail(ctx, *data.pm, *data.chf, cfg.detailSampleDist, cfg.detailSampleMaxError, *data.pmd)) 1025 { 1026 Con::errorf("Could not construct rcPolyMeshDetail for NavMesh %s", getIdString()); 1027 return NULL; 1028 } 1029 1030 if(data.pm->nverts >= 0xffff) 1031 { 1032 Con::errorf("Too many vertices in rcPolyMesh for NavMesh %s", getIdString()); 1033 return NULL; 1034 } 1035 for(U32 i = 0; i < data.pm->npolys; i++) 1036 { 1037 if(data.pm->areas[i] == RC_WALKABLE_AREA) 1038 data.pm->areas[i] = GroundArea; 1039 1040 if(data.pm->areas[i] == GroundArea) 1041 data.pm->flags[i] |= WalkFlag; 1042 if(data.pm->areas[i] == WaterArea) 1043 data.pm->flags[i] |= SwimFlag; 1044 } 1045 1046 unsigned char* navData = 0; 1047 int navDataSize = 0; 1048 1049 dtNavMeshCreateParams params; 1050 dMemset(¶ms, 0, sizeof(params)); 1051 1052 params.verts = data.pm->verts; 1053 params.vertCount = data.pm->nverts; 1054 params.polys = data.pm->polys; 1055 params.polyAreas = data.pm->areas; 1056 params.polyFlags = data.pm->flags; 1057 params.polyCount = data.pm->npolys; 1058 params.nvp = data.pm->nvp; 1059 1060 params.detailMeshes = data.pmd->meshes; 1061 params.detailVerts = data.pmd->verts; 1062 params.detailVertsCount = data.pmd->nverts; 1063 params.detailTris = data.pmd->tris; 1064 params.detailTriCount = data.pmd->ntris; 1065 1066 params.offMeshConVerts = mLinkVerts.address(); 1067 params.offMeshConRad = mLinkRads.address(); 1068 params.offMeshConDir = mLinkDirs.address(); 1069 params.offMeshConAreas = mLinkAreas.address(); 1070 params.offMeshConFlags = mLinkFlags.address(); 1071 params.offMeshConUserID = mLinkIDs.address(); 1072 params.offMeshConCount = mLinkIDs.size(); 1073 1074 params.walkableHeight = mWalkableHeight; 1075 params.walkableRadius = mWalkableRadius; 1076 params.walkableClimb = mWalkableClimb; 1077 params.tileX = tile.x; 1078 params.tileY = tile.y; 1079 params.tileLayer = 0; 1080 rcVcopy(params.bmin, data.pm->bmin); 1081 rcVcopy(params.bmax, data.pm->bmax); 1082 params.cs = cfg.cs; 1083 params.ch = cfg.ch; 1084 params.buildBvTree = true; 1085 1086 if(!dtCreateNavMeshData(¶ms, &navData, &navDataSize)) 1087 { 1088 Con::errorf("Could not create dtNavMeshData for tile (%d, %d) of NavMesh %s", 1089 tile.x, tile.y, getIdString()); 1090 return NULL; 1091 } 1092 1093 dataSize = navDataSize; 1094 1095 return navData; 1096} 1097 1098/// This method should never be called in a separate thread to the rendering 1099/// or pathfinding logic. It directly replaces data in the dtNavMesh for 1100/// this NavMesh object. 1101void NavMesh::buildTiles(const Box3F &box) 1102{ 1103 // Make sure we've already built or loaded. 1104 if(!nm) 1105 return; 1106 // Iterate over tiles. 1107 for(U32 i = 0; i < mTiles.size(); i++) 1108 { 1109 const Tile &tile = mTiles[i]; 1110 // Check tile box. 1111 if(!tile.box.isOverlapped(box)) 1112 continue; 1113 // Mark as dirty. 1114 mDirtyTiles.push_back_unique(i); 1115 } 1116 if(mDirtyTiles.size()) 1117 ctx->startTimer(RC_TIMER_TOTAL); 1118} 1119 1120DefineEngineMethod(NavMesh, buildTiles, void, (Box3F box),, 1121 "@brief Rebuild the tiles overlapped by the input box.") 1122{ 1123 return object->buildTiles(box); 1124} 1125 1126void NavMesh::buildTile(const U32 &tile) 1127{ 1128 if(tile < mTiles.size()) 1129 { 1130 mDirtyTiles.push_back_unique(tile); 1131 ctx->startTimer(RC_TIMER_TOTAL); 1132 } 1133} 1134 1135void NavMesh::buildLinks() 1136{ 1137 // Make sure we've already built or loaded. 1138 if(!nm) 1139 return; 1140 // Iterate over tiles. 1141 for(U32 i = 0; i < mTiles.size(); i++) 1142 { 1143 const Tile &tile = mTiles[i]; 1144 // Iterate over links 1145 for(U32 j = 0; j < mLinkIDs.size(); j++) 1146 { 1147 if (mLinksUnsynced[j]) 1148 { 1149 if(tile.box.isContained(getLinkStart(j)) || 1150 tile.box.isContained(getLinkEnd(j))) 1151 { 1152 // Mark tile for build. 1153 mDirtyTiles.push_back_unique(i); 1154 // Delete link if necessary 1155 if(mDeleteLinks[j]) 1156 { 1157 eraseLink(j); 1158 j--; 1159 } 1160 else 1161 mLinksUnsynced[j] = false; 1162 } 1163 } 1164 } 1165 } 1166 if(mDirtyTiles.size()) 1167 ctx->startTimer(RC_TIMER_TOTAL); 1168} 1169 1170DefineEngineMethod(NavMesh, buildLinks, void, (),, 1171 "@brief Build tiles of this mesh where there are unsynchronised links.") 1172{ 1173 object->buildLinks(); 1174} 1175 1176void NavMesh::deleteCoverPoints() 1177{ 1178 SimSet *set = NULL; 1179 if(Sim::findObject(mCoverSet, set)) 1180 set->deleteAllObjects(); 1181} 1182 1183DefineEngineMethod(NavMesh, deleteCoverPoints, void, (),, 1184 "@brief Remove all cover points for this NavMesh.") 1185{ 1186 object->deleteCoverPoints(); 1187} 1188 1189bool NavMesh::createCoverPoints() 1190{ 1191 if(!nm || !isServerObject()) 1192 return false; 1193 1194 SimSet *set = NULL; 1195 if(Sim::findObject(mCoverSet, set)) 1196 { 1197 set->deleteAllObjects(); 1198 } 1199 else 1200 { 1201 set = new SimGroup(); 1202 if(set->registerObject(mCoverSet)) 1203 { 1204 getGroup()->addObject(set); 1205 } 1206 else 1207 { 1208 delete set; 1209 set = getGroup(); 1210 } 1211 } 1212 1213 dtNavMeshQuery *query = dtAllocNavMeshQuery(); 1214 if(!query || dtStatusFailed(query->init(nm, 1))) 1215 return false; 1216 1217 dtQueryFilter f; 1218 1219 // Iterate over all polys in our navmesh. 1220 const int MAX_SEGS = 6; 1221 for(U32 i = 0; i < nm->getMaxTiles(); ++i) 1222 { 1223 const dtMeshTile* tile = ((const dtNavMesh*)nm)->getTile(i); 1224 if(!tile->header) continue; 1225 const dtPolyRef base = nm->getPolyRefBase(tile); 1226 for(U32 j = 0; j < tile->header->polyCount; ++j) 1227 { 1228 const dtPolyRef ref = base | j; 1229 float segs[MAX_SEGS*6]; 1230 int nsegs = 0; 1231 query->getPolyWallSegments(ref, &f, segs, NULL, &nsegs, MAX_SEGS); 1232 for(int j = 0; j < nsegs; ++j) 1233 { 1234 const float* sa = &segs[j*6]; 1235 const float* sb = &segs[j*6+3]; 1236 Point3F a = RCtoDTS(sa), b = RCtoDTS(sb); 1237 F32 len = (b - a).len(); 1238 if(len < mWalkableRadius * 2) 1239 continue; 1240 Point3F edge = b - a; 1241 edge.normalize(); 1242 // Number of points to try placing - for now, one at each end. 1243 U32 pointCount = (len > mWalkableRadius * 4) ? 2 : 1; 1244 for(U32 i = 0; i < pointCount; i++) 1245 { 1246 MatrixF mat; 1247 Point3F pos; 1248 // If we're only placing one point, put it in the middle. 1249 if(pointCount == 1) 1250 pos = a + edge * len / 2; 1251 // Otherwise, stand off from edge ends. 1252 else 1253 { 1254 if(i % 2) 1255 pos = a + edge * (i/2+1) * mWalkableRadius; 1256 else 1257 pos = b - edge * (i/2+1) * mWalkableRadius; 1258 } 1259 CoverPointData data; 1260 if(testEdgeCover(pos, edge, data)) 1261 { 1262 CoverPoint *m = new CoverPoint(); 1263 if(!m->registerObject()) 1264 delete m; 1265 else 1266 { 1267 m->setTransform(data.trans); 1268 m->setSize(data.size); 1269 m->setPeek(data.peek[0], data.peek[1], data.peek[2]); 1270 if(set) 1271 set->addObject(m); 1272 } 1273 } 1274 } 1275 } 1276 } 1277 } 1278 return true; 1279} 1280 1281DefineEngineMethod(NavMesh, createCoverPoints, bool, (),, 1282 "@brief Create cover points for this NavMesh.") 1283{ 1284 return object->createCoverPoints(); 1285} 1286 1287bool NavMesh::testEdgeCover(const Point3F &pos, const VectorF &dir, CoverPointData &data) 1288{ 1289 data.peek[0] = data.peek[1] = data.peek[2] = false; 1290 // Get the edge normal. 1291 Point3F norm; 1292 mCross(dir, Point3F(0, 0, 1), &norm); 1293 RayInfo ray; 1294 U32 hits = 0; 1295 for(U32 j = 0; j < CoverPoint::NumSizes; j++) 1296 { 1297 Point3F test = pos + Point3F(0.0f, 0.0f, mWalkableHeight * j / CoverPoint::NumSizes); 1298 if(getContainer()->castRay(test, test + norm * mCoverDist, StaticObjectType, &ray)) 1299 { 1300 // Test peeking. 1301 Point3F left = test + dir * mPeekDist; 1302 data.peek[0] = !getContainer()->castRay(test, left, StaticObjectType, &ray) 1303 && !getContainer()->castRay(left, left + norm * mCoverDist, StaticObjectType, &ray); 1304 1305 Point3F right = test - dir * mPeekDist; 1306 data.peek[1] = !getContainer()->castRay(test, right, StaticObjectType, &ray) 1307 && !getContainer()->castRay(right, right + norm * mCoverDist, StaticObjectType, &ray); 1308 1309 Point3F over = test + Point3F(0, 0, 1) * 0.2f; 1310 data.peek[2] = !getContainer()->castRay(test, over, StaticObjectType, &ray) 1311 && !getContainer()->castRay(over, over + norm * mCoverDist, StaticObjectType, &ray); 1312 1313 if(mInnerCover || data.peek[0] || data.peek[1] || data.peek[2]) 1314 hits++; 1315 // If we couldn't peek here, we may be able to peek further up. 1316 } 1317 else 1318 // No cover at this height - break off. 1319 break; 1320 } 1321 if(hits > 0) 1322 { 1323 data.size = (CoverPoint::Size)(hits - 1); 1324 data.trans = MathUtils::createOrientFromDir(norm); 1325 data.trans.setPosition(pos); 1326 } 1327 return hits > 0; 1328} 1329 1330void NavMesh::renderToDrawer() 1331{ 1332 dd.clear(); 1333 // Recast debug draw 1334 NetObject *no = getServerObject(); 1335 if(no) 1336 { 1337 NavMesh *n = static_cast<NavMesh*>(no); 1338 1339 if(n->nm) 1340 { 1341 dd.beginGroup(0); 1342 duDebugDrawNavMesh (&dd, *n->nm, 0); 1343 dd.beginGroup(1); 1344 duDebugDrawNavMeshPortals(&dd, *n->nm); 1345 dd.beginGroup(2); 1346 duDebugDrawNavMeshBVTree (&dd, *n->nm); 1347 } 1348 } 1349} 1350 1351void NavMesh::prepRenderImage(SceneRenderState *state) 1352{ 1353 ObjectRenderInst *ri = state->getRenderPass()->allocInst<ObjectRenderInst>(); 1354 ri->renderDelegate.bind(this, &NavMesh::render); 1355 ri->type = RenderPassManager::RIT_Object; 1356 ri->translucentSort = true; 1357 ri->defaultKey = 1; 1358 state->getRenderPass()->addInst(ri); 1359} 1360 1361void NavMesh::render(ObjectRenderInst *ri, SceneRenderState *state, BaseMatInstance *overrideMat) 1362{ 1363 if(overrideMat) 1364 return; 1365 1366 if(state->isReflectPass()) 1367 return; 1368 1369 PROFILE_SCOPE(NavMesh_Render); 1370 1371 // Recast debug draw 1372 NetObject *no = getServerObject(); 1373 if(no) 1374 { 1375 NavMesh *n = static_cast<NavMesh*>(no); 1376 1377 if(n->isSelected()) 1378 { 1379 GFXDrawUtil *drawer = GFX->getDrawUtil(); 1380 1381 GFXStateBlockDesc desc; 1382 desc.setZReadWrite(true, false); 1383 desc.setBlend(true); 1384 desc.setCullMode(GFXCullNone); 1385 1386 drawer->drawCube(desc, getWorldBox(), n->mBuilding 1387 ? ColorI(255, 0, 0, 80) 1388 : ColorI(136, 228, 255, 45)); 1389 desc.setFillModeWireframe(); 1390 drawer->drawCube(desc, getWorldBox(), ColorI::BLACK); 1391 } 1392 1393 if(n->mBuilding) 1394 { 1395 int alpha = 80; 1396 if(!n->isSelected() || !Con::getBoolVariable("$Nav::EditorOpen")) 1397 alpha = 20; 1398 dd.overrideColor(duRGBA(255, 0, 0, alpha)); 1399 } 1400 else 1401 { 1402 dd.cancelOverride(); 1403 } 1404 1405 if((!gEditingMission && n->mAlwaysRender) || (gEditingMission && Con::getBoolVariable("$Nav::Editor::renderMesh", 1))) dd.renderGroup(0); 1406 if(Con::getBoolVariable("$Nav::Editor::renderPortals")) dd.renderGroup(1); 1407 if(Con::getBoolVariable("$Nav::Editor::renderBVTree")) dd.renderGroup(2); 1408 } 1409} 1410 1411void NavMesh::renderLinks(duDebugDraw &dd) 1412{ 1413 if(mBuilding) 1414 return; 1415 dd.depthMask(true); 1416 dd.begin(DU_DRAW_LINES); 1417 for(U32 i = 0; i < mLinkIDs.size(); i++) 1418 { 1419 U32 col = 0; 1420 switch(mLinkSelectStates[i]) 1421 { 1422 case Unselected: col = mLinksUnsynced[i] ? duRGBA(255, 0, 0, 200) : duRGBA(0, 0, 255, 255); break; 1423 case Hovered: col = duRGBA(255, 255, 255, 255); break; 1424 case Selected: col = duRGBA(0, 255, 0, 255); break; 1425 } 1426 F32 *s = &mLinkVerts[i*6]; 1427 F32 *e = &mLinkVerts[i*6 + 3]; 1428 if(!mDeleteLinks[i]) 1429 duAppendCircle(&dd, s[0], s[1], s[2], mLinkRads[i], col); 1430 duAppendArc(&dd, 1431 s[0], s[1], s[2], 1432 e[0], e[1], e[2], 1433 0.3f, 1434 0.0f, mLinkFlags[i] == DropFlag ? 0.0f : 0.4f, 1435 col); 1436 if(!mDeleteLinks[i]) 1437 duAppendCircle(&dd, e[0], e[1], e[2], mLinkRads[i], col); 1438 } 1439 dd.end(); 1440} 1441 1442void NavMesh::renderTileData(duDebugDrawTorque &dd, U32 tile) 1443{ 1444 if(tile >= mTileData.size()) 1445 return; 1446 if(nm) 1447 { 1448 dd.beginGroup(0); 1449 if(mTileData[tile].chf) duDebugDrawCompactHeightfieldSolid(&dd, *mTileData[tile].chf); 1450 1451 dd.beginGroup(1); 1452 int col = duRGBA(255, 0, 255, 255); 1453 RecastPolyList &in = mTileData[tile].geom; 1454 dd.begin(DU_DRAW_LINES); 1455 const F32 *verts = in.getVerts(); 1456 const S32 *tris = in.getTris(); 1457 for(U32 t = 0; t < in.getTriCount(); t++) 1458 { 1459 dd.vertex(&verts[tris[t*3]*3], col); 1460 dd.vertex(&verts[tris[t*3+1]*3], col); 1461 dd.vertex(&verts[tris[t*3+1]*3], col); 1462 dd.vertex(&verts[tris[t*3+2]*3], col); 1463 dd.vertex(&verts[tris[t*3+2]*3], col); 1464 dd.vertex(&verts[tris[t*3]*3], col); 1465 } 1466 dd.end(); 1467 } 1468} 1469 1470void NavMesh::onEditorEnable() 1471{ 1472 mNetFlags.set(Ghostable); 1473 if(isClientObject() && !mAlwaysRender) 1474 addToScene(); 1475} 1476 1477void NavMesh::onEditorDisable() 1478{ 1479 if(!mAlwaysRender) 1480 { 1481 mNetFlags.clear(Ghostable); 1482 if(isClientObject()) 1483 removeFromScene(); 1484 } 1485} 1486 1487U32 NavMesh::packUpdate(NetConnection *conn, U32 mask, BitStream *stream) 1488{ 1489 U32 retMask = Parent::packUpdate(conn, mask, stream); 1490 1491 mathWrite(*stream, getTransform()); 1492 mathWrite(*stream, getScale()); 1493 stream->writeFlag(mAlwaysRender); 1494 1495 return retMask; 1496} 1497 1498void NavMesh::unpackUpdate(NetConnection *conn, BitStream *stream) 1499{ 1500 Parent::unpackUpdate(conn, stream); 1501 1502 mathRead(*stream, &mObjToWorld); 1503 mathRead(*stream, &mObjScale); 1504 mAlwaysRender = stream->readFlag(); 1505 1506 setTransform(mObjToWorld); 1507 1508 renderToDrawer(); 1509} 1510 1511static const int NAVMESHSET_MAGIC = 'M'<<24 | 'S'<<16 | 'E'<<8 | 'T'; //'MSET'; 1512static const int NAVMESHSET_VERSION = 1; 1513 1514struct NavMeshSetHeader 1515{ 1516 int magic; 1517 int version; 1518 int numTiles; 1519 dtNavMeshParams params; 1520}; 1521 1522struct NavMeshTileHeader 1523{ 1524 dtTileRef tileRef; 1525 int dataSize; 1526}; 1527 1528bool NavMesh::load() 1529{ 1530 if(!dStrlen(mFileName)) 1531 return false; 1532 1533 File file; 1534 if(file.open(mFileName, File::Read) != File::Ok) 1535 { 1536 file.close(); 1537 Con::errorf("Could not open file %s when loading navmesh %s.", 1538 mFileName, getName() ? getName() : getIdString()); 1539 return false; 1540 } 1541 1542 // Read header. 1543 NavMeshSetHeader header; 1544 file.read(sizeof(NavMeshSetHeader), (char*)&header); 1545 if(header.magic != NAVMESHSET_MAGIC) 1546 { 1547 file.close(); 1548 Con::errorf("Navmesh magic incorrect when loading navmesh %s; possible corrupt navmesh file %s.", 1549 getName() ? getName() : getIdString(), mFileName); 1550 return false; 1551 } 1552 if(header.version != NAVMESHSET_VERSION) 1553 { 1554 file.close(); 1555 Con::errorf("Navmesh version incorrect when loading navmesh %s; possible corrupt navmesh file %s.", 1556 getName() ? getName() : getIdString(), mFileName); 1557 return false; 1558 } 1559 1560 if(nm) 1561 dtFreeNavMesh(nm); 1562 nm = dtAllocNavMesh(); 1563 if(!nm) 1564 { 1565 file.close(); 1566 Con::errorf("Out of memory when loading navmesh %s.", 1567 getName() ? getName() : getIdString()); 1568 return false; 1569 } 1570 1571 dtStatus status = nm->init(&header.params); 1572 if(dtStatusFailed(status)) 1573 { 1574 file.close(); 1575 Con::errorf("Failed to initialise navmesh params when loading navmesh %s.", 1576 getName() ? getName() : getIdString()); 1577 return false; 1578 } 1579 1580 // Read tiles. 1581 for(U32 i = 0; i < header.numTiles; ++i) 1582 { 1583 NavMeshTileHeader tileHeader; 1584 file.read(sizeof(NavMeshTileHeader), (char*)&tileHeader); 1585 if(!tileHeader.tileRef || !tileHeader.dataSize) 1586 break; 1587 1588 unsigned char* data = (unsigned char*)dtAlloc(tileHeader.dataSize, DT_ALLOC_PERM); 1589 if(!data) break; 1590 memset(data, 0, tileHeader.dataSize); 1591 file.read(tileHeader.dataSize, (char*)data); 1592 1593 nm->addTile(data, tileHeader.dataSize, DT_TILE_FREE_DATA, tileHeader.tileRef, 0); 1594 } 1595 1596 S32 s; 1597 file.read(sizeof(S32), (char*)&s); 1598 setLinkCount(s); 1599 if (s > 0) 1600 { 1601 file.read(sizeof(F32) * s * 6, (char*)const_cast<F32*>(mLinkVerts.address())); 1602 file.read(sizeof(F32) * s, (char*)const_cast<F32*>(mLinkRads.address())); 1603 file.read(sizeof(U8) * s, (char*)const_cast<U8*>(mLinkDirs.address())); 1604 file.read(sizeof(U8) * s, (char*)const_cast<U8*>(mLinkAreas.address())); 1605 file.read(sizeof(U16) * s, (char*)const_cast<U16*>(mLinkFlags.address())); 1606 file.read(sizeof(F32) * s, (char*)const_cast<U32*>(mLinkIDs.address())); 1607 } 1608 mLinksUnsynced.fill(false); 1609 mLinkSelectStates.fill(Unselected); 1610 mDeleteLinks.fill(false); 1611 1612 file.close(); 1613 1614 updateTiles(); 1615 1616 if(isServerObject()) 1617 { 1618 setMaskBits(LoadFlag); 1619 if(getEventManager()) 1620 getEventManager()->postEvent("NavMeshUpdate", getIdString()); 1621 } 1622 1623 return true; 1624} 1625 1626DefineEngineMethod(NavMesh, load, bool, (),, 1627 "@brief Load this NavMesh from its file.") 1628{ 1629 return object->load(); 1630} 1631 1632bool NavMesh::save() 1633{ 1634 if(!dStrlen(mFileName) || !nm) 1635 return false; 1636 1637 File file; 1638 if(file.open(mFileName, File::Write) != File::Ok) 1639 { 1640 file.close(); 1641 Con::errorf("Could not open file %s when saving navmesh %s.", 1642 mFileName, getName() ? getName() : getIdString()); 1643 return false; 1644 } 1645 1646 // Store header. 1647 NavMeshSetHeader header; 1648 header.magic = NAVMESHSET_MAGIC; 1649 header.version = NAVMESHSET_VERSION; 1650 header.numTiles = 0; 1651 for(U32 i = 0; i < nm->getMaxTiles(); ++i) 1652 { 1653 const dtMeshTile* tile = ((const dtNavMesh*)nm)->getTile(i); 1654 if (!tile || !tile->header || !tile->dataSize) continue; 1655 header.numTiles++; 1656 } 1657 memcpy(&header.params, nm->getParams(), sizeof(dtNavMeshParams)); 1658 file.write(sizeof(NavMeshSetHeader), (const char*)&header); 1659 1660 // Store tiles. 1661 for(U32 i = 0; i < nm->getMaxTiles(); ++i) 1662 { 1663 const dtMeshTile* tile = ((const dtNavMesh*)nm)->getTile(i); 1664 if(!tile || !tile->header || !tile->dataSize) continue; 1665 1666 NavMeshTileHeader tileHeader; 1667 tileHeader.tileRef = nm->getTileRef(tile); 1668 tileHeader.dataSize = tile->dataSize; 1669 1670 file.write(sizeof(tileHeader), (const char*)&tileHeader); 1671 file.write(tile->dataSize, (const char*)tile->data); 1672 } 1673 1674 S32 s = mLinkIDs.size(); 1675 file.write(sizeof(S32), (const char*)&s); 1676 if (s > 0) 1677 { 1678 file.write(sizeof(F32) * s * 6, (const char*)mLinkVerts.address()); 1679 file.write(sizeof(F32) * s, (const char*)mLinkRads.address()); 1680 file.write(sizeof(U8) * s, (const char*)mLinkDirs.address()); 1681 file.write(sizeof(U8) * s, (const char*)mLinkAreas.address()); 1682 file.write(sizeof(U16) * s, (const char*)mLinkFlags.address()); 1683 file.write(sizeof(U32) * s, (const char*)mLinkIDs.address()); 1684 } 1685 1686 file.close(); 1687 1688 return true; 1689} 1690 1691DefineEngineMethod(NavMesh, save, void, (),, 1692 "@brief Save this NavMesh to its file.") 1693{ 1694 object->save(); 1695} 1696 1697void NavMesh::write(Stream &stream, U32 tabStop, U32 flags) 1698{ 1699 save(); 1700 Parent::write(stream, tabStop, flags); 1701} 1702
