Merge pull request #1920 from wiseoldman95/SquashedPathFinder
Implemented A* Pathfinding and updated monster.cpp to use it.
This commit is contained in:
commit
aa142757db
@ -37,11 +37,8 @@ void cAggressiveMonster::InStateChasing(std::chrono::milliseconds a_Dt)
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsMovingToTargetPosition())
|
||||
{
|
||||
MoveToPosition(m_Target->GetPosition());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -24,6 +24,7 @@ SET (SRCS
|
||||
Mooshroom.cpp
|
||||
PassiveAggressiveMonster.cpp
|
||||
PassiveMonster.cpp
|
||||
Path.cpp
|
||||
Pig.cpp
|
||||
Rabbit.cpp
|
||||
Sheep.cpp
|
||||
@ -62,6 +63,7 @@ SET (HDRS
|
||||
Ocelot.h
|
||||
PassiveAggressiveMonster.h
|
||||
PassiveMonster.h
|
||||
Path.h
|
||||
Pig.h
|
||||
Rabbit.h
|
||||
Sheep.h
|
||||
|
@ -13,7 +13,7 @@
|
||||
#include "../Chunk.h"
|
||||
#include "../FastRandom.h"
|
||||
|
||||
|
||||
#include "Path.h"
|
||||
|
||||
|
||||
|
||||
@ -74,6 +74,10 @@ cMonster::cMonster(const AString & a_ConfigName, eMonsterType a_MobType, const A
|
||||
, m_EMState(IDLE)
|
||||
, m_EMPersonality(AGGRESSIVE)
|
||||
, m_Target(nullptr)
|
||||
, m_Path(nullptr)
|
||||
, m_PathStatus(ePathFinderStatus::PATH_NOT_FOUND)
|
||||
, m_IsFollowingPath(false)
|
||||
, m_GiveUpCounter(0)
|
||||
, m_bMovingToDestination(false)
|
||||
, m_LastGroundHeight(POSY_TOINT)
|
||||
, m_IdleInterval(0)
|
||||
@ -94,8 +98,9 @@ cMonster::cMonster(const AString & a_ConfigName, eMonsterType a_MobType, const A
|
||||
, m_DropChanceLeggings(0.085f)
|
||||
, m_DropChanceBoots(0.085f)
|
||||
, m_CanPickUpLoot(true)
|
||||
, m_TicksSinceLastDamaged(100)
|
||||
, m_BurnsInDaylight(false)
|
||||
, m_RelativeWalkSpeed(1.0)
|
||||
, m_RelativeWalkSpeed(1)
|
||||
{
|
||||
if (!a_ConfigName.empty())
|
||||
{
|
||||
@ -118,87 +123,52 @@ void cMonster::SpawnOn(cClientHandle & a_Client)
|
||||
|
||||
void cMonster::TickPathFinding()
|
||||
{
|
||||
const int PosX = POSX_TOINT;
|
||||
const int PosY = POSY_TOINT;
|
||||
const int PosZ = POSZ_TOINT;
|
||||
|
||||
std::vector<Vector3d> m_PotentialCoordinates;
|
||||
m_TraversedCoordinates.push_back(Vector3i(PosX, PosY, PosZ));
|
||||
if (m_Path == nullptr)
|
||||
{
|
||||
Vector3d position = GetPosition();
|
||||
Vector3d Dest = m_FinalDestination;
|
||||
|
||||
static const struct // Define which directions to try to move to
|
||||
{
|
||||
int x, z;
|
||||
} gCrossCoords[] =
|
||||
{
|
||||
{ 1, 0},
|
||||
{-1, 0},
|
||||
{ 0, 1},
|
||||
{ 0, -1},
|
||||
} ;
|
||||
// Can someone explain why are these two NOT THE SAME???
|
||||
// m_Path = new cPath(GetWorld(), GetPosition(), m_FinalDestination, 30);
|
||||
m_Path = new cPath(GetWorld(), Vector3d(floor(position.x), floor(position.y), floor(position.z)), Vector3d(floor(Dest.x), floor(Dest.y), floor(Dest.z)), 20);
|
||||
|
||||
if ((PosY - 1 < 0) || (PosY + 2 >= cChunkDef::Height) /* PosY + 1 will never be true if PosY + 2 is not */)
|
||||
{
|
||||
// Too low/high, can't really do anything
|
||||
FinishPathFinding();
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < ARRAYCOUNT(gCrossCoords); i++)
|
||||
{
|
||||
if (IsCoordinateInTraversedList(Vector3i(gCrossCoords[i].x + PosX, PosY, gCrossCoords[i].z + PosZ)))
|
||||
{
|
||||
continue;
|
||||
m_IsFollowingPath = false;
|
||||
}
|
||||
m_PathStatus = m_Path->Step();
|
||||
switch (m_PathStatus)
|
||||
{
|
||||
|
||||
BLOCKTYPE BlockAtY = m_World->GetBlock(gCrossCoords[i].x + PosX, PosY, gCrossCoords[i].z + PosZ);
|
||||
BLOCKTYPE BlockAtYP = m_World->GetBlock(gCrossCoords[i].x + PosX, PosY + 1, gCrossCoords[i].z + PosZ);
|
||||
BLOCKTYPE BlockAtYPP = m_World->GetBlock(gCrossCoords[i].x + PosX, PosY + 2, gCrossCoords[i].z + PosZ);
|
||||
int LowestY = FindFirstNonAirBlockPosition(gCrossCoords[i].x + PosX, gCrossCoords[i].z + PosZ);
|
||||
BLOCKTYPE BlockAtLowestY = (LowestY >= cChunkDef::Height) ? E_BLOCK_AIR : m_World->GetBlock(gCrossCoords[i].x + PosX, LowestY, gCrossCoords[i].z + PosZ);
|
||||
|
||||
if (
|
||||
(!cBlockInfo::IsSolid(BlockAtY)) &&
|
||||
(!cBlockInfo::IsSolid(BlockAtYP)) &&
|
||||
(!IsBlockLava(BlockAtLowestY)) &&
|
||||
(BlockAtLowestY != E_BLOCK_CACTUS) &&
|
||||
(PosY - LowestY < FALL_DAMAGE_HEIGHT)
|
||||
)
|
||||
{
|
||||
m_PotentialCoordinates.push_back(Vector3d((gCrossCoords[i].x + PosX), PosY, gCrossCoords[i].z + PosZ));
|
||||
}
|
||||
else if (
|
||||
(cBlockInfo::IsSolid(BlockAtY)) &&
|
||||
(BlockAtY != E_BLOCK_CACTUS) &&
|
||||
(!cBlockInfo::IsSolid(BlockAtYP)) &&
|
||||
(!cBlockInfo::IsSolid(BlockAtYPP)) &&
|
||||
(BlockAtY != E_BLOCK_FENCE) &&
|
||||
(BlockAtY != E_BLOCK_FENCE_GATE)
|
||||
)
|
||||
{
|
||||
m_PotentialCoordinates.push_back(Vector3d((gCrossCoords[i].x + PosX), PosY + 1, gCrossCoords[i].z + PosZ));
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_PotentialCoordinates.empty())
|
||||
{
|
||||
Vector3f ShortestCoords = m_PotentialCoordinates.front();
|
||||
for (std::vector<Vector3d>::const_iterator itr = m_PotentialCoordinates.begin(); itr != m_PotentialCoordinates.end(); ++itr)
|
||||
{
|
||||
Vector3f Distance = m_FinalDestination - ShortestCoords;
|
||||
Vector3f Distance2 = m_FinalDestination - *itr;
|
||||
if (Distance.SqrLength() > Distance2.SqrLength())
|
||||
{
|
||||
ShortestCoords = *itr;
|
||||
}
|
||||
}
|
||||
|
||||
m_Destination = ShortestCoords;
|
||||
m_Destination.z += 0.5f;
|
||||
m_Destination.x += 0.5f;
|
||||
}
|
||||
else
|
||||
case ePathFinderStatus::PATH_NOT_FOUND:
|
||||
{
|
||||
FinishPathFinding();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case ePathFinderStatus::CALCULATING:
|
||||
{
|
||||
m_Destination = GetPosition();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case ePathFinderStatus::PATH_FOUND:
|
||||
{
|
||||
if (ReachedDestination() || !m_IsFollowingPath)
|
||||
{
|
||||
m_Destination = m_Path->GetNextPoint();
|
||||
m_IsFollowingPath = true;
|
||||
m_GiveUpCounter = 40; // Give up after 40 ticks (2 seconds) if failed to reach m_Dest.
|
||||
}
|
||||
if (m_Path->IsLastPoint())
|
||||
{
|
||||
FinishPathFinding();
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -206,17 +176,28 @@ void cMonster::TickPathFinding()
|
||||
|
||||
|
||||
|
||||
/* Currently, the mob will only start moving to a new position after the position it is
|
||||
currently going to is reached. */
|
||||
void cMonster::MoveToPosition(const Vector3d & a_Position)
|
||||
{
|
||||
FinishPathFinding();
|
||||
|
||||
m_FinalDestination = a_Position;
|
||||
m_bMovingToDestination = true;
|
||||
TickPathFinding();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void cMonster::StopMovingToPosition()
|
||||
{
|
||||
m_bMovingToDestination = false;
|
||||
FinishPathFinding();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
bool cMonster::IsCoordinateInTraversedList(Vector3i a_Coords)
|
||||
{
|
||||
return (std::find(m_TraversedCoordinates.begin(), m_TraversedCoordinates.end(), a_Coords) != m_TraversedCoordinates.end());
|
||||
@ -226,6 +207,22 @@ bool cMonster::IsCoordinateInTraversedList(Vector3i a_Coords)
|
||||
|
||||
|
||||
|
||||
/* No one should call this except the pathfinder orthe monster tick or StopMovingToPosition.
|
||||
Resets the pathfinder, usually starting a brand new path, unless called from StopMovingToPosition. */
|
||||
void cMonster::FinishPathFinding(void)
|
||||
{
|
||||
if (m_Path != nullptr)
|
||||
{
|
||||
delete m_Path;
|
||||
m_Path = nullptr;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
bool cMonster::ReachedDestination()
|
||||
{
|
||||
if ((m_Destination - GetPosition()).Length() < 0.5f)
|
||||
@ -239,6 +236,7 @@ bool cMonster::ReachedDestination()
|
||||
|
||||
|
||||
|
||||
|
||||
bool cMonster::ReachedFinalDestination()
|
||||
{
|
||||
if ((GetPosition() - m_FinalDestination).Length() <= m_AttackRange)
|
||||
@ -268,6 +266,10 @@ void cMonster::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_TicksSinceLastDamaged < 100)
|
||||
{
|
||||
++m_TicksSinceLastDamaged;
|
||||
}
|
||||
if ((m_Target != nullptr) && m_Target->IsDestroyed())
|
||||
{
|
||||
m_Target = nullptr;
|
||||
@ -276,6 +278,8 @@ void cMonster::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
|
||||
// Burning in daylight
|
||||
HandleDaylightBurning(a_Chunk);
|
||||
|
||||
|
||||
|
||||
if (m_bMovingToDestination)
|
||||
{
|
||||
if (m_bOnGround)
|
||||
@ -289,8 +293,16 @@ void cMonster::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
|
||||
}
|
||||
}
|
||||
|
||||
TickPathFinding();
|
||||
|
||||
Vector3d Distance = m_Destination - GetPosition();
|
||||
if (!ReachedDestination() && !ReachedFinalDestination()) // If we haven't reached any sort of destination, move
|
||||
{
|
||||
if (--m_GiveUpCounter == 0)
|
||||
{
|
||||
FinishPathFinding();
|
||||
}
|
||||
else
|
||||
{
|
||||
Distance.y = 0;
|
||||
Distance.Normalize();
|
||||
@ -312,29 +324,19 @@ void cMonster::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
|
||||
// Apply walk speed:
|
||||
Distance *= m_RelativeWalkSpeed;
|
||||
|
||||
/* Reduced default speed.
|
||||
Close to Vanilla, easier for mobs to follow m_Destinations, hence
|
||||
better pathfinding. */
|
||||
Distance *= 0.5;
|
||||
|
||||
AddSpeedX(Distance.x);
|
||||
AddSpeedZ(Distance.z);
|
||||
|
||||
// It's too buggy!
|
||||
/*
|
||||
if (m_EMState == ESCAPING)
|
||||
{
|
||||
// Runs Faster when escaping :D otherwise they just walk away
|
||||
SetSpeedX (GetSpeedX() * 2.f);
|
||||
SetSpeedZ (GetSpeedZ() * 2.f);
|
||||
}
|
||||
*/
|
||||
}
|
||||
else
|
||||
else if (ReachedFinalDestination())
|
||||
{
|
||||
if (ReachedFinalDestination()) // If we have reached the ultimate, final destination, stop pathfinding and attack if appropriate
|
||||
{
|
||||
FinishPathFinding();
|
||||
}
|
||||
else
|
||||
{
|
||||
TickPathFinding(); // We have reached the next point in our path, calculate another point
|
||||
}
|
||||
StopMovingToPosition();
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,13 +347,13 @@ void cMonster::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
|
||||
{
|
||||
case IDLE:
|
||||
{
|
||||
// If enemy passive we ignore checks for player visibility
|
||||
// If enemy passive we ignore checks for player visibility.
|
||||
InStateIdle(a_Dt);
|
||||
break;
|
||||
}
|
||||
case CHASING:
|
||||
{
|
||||
// If we do not see a player anymore skip chasing action
|
||||
// If we do not see a player anymore skip chasing action.
|
||||
InStateChasing(a_Dt);
|
||||
break;
|
||||
}
|
||||
@ -365,7 +367,8 @@ void cMonster::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
|
||||
} // switch (m_EMState)
|
||||
|
||||
BroadcastMovementUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -409,6 +412,7 @@ void cMonster::SetPitchAndYawFromDestination()
|
||||
|
||||
|
||||
|
||||
|
||||
void cMonster::HandleFalling()
|
||||
{
|
||||
if (m_bOnGround)
|
||||
@ -460,7 +464,6 @@ int cMonster::FindFirstNonAirBlockPosition(double a_PosX, double a_PosZ)
|
||||
|
||||
|
||||
|
||||
|
||||
bool cMonster::DoTakeDamage(TakeDamageInfo & a_TDI)
|
||||
{
|
||||
if (!super::DoTakeDamage(a_TDI))
|
||||
@ -476,6 +479,7 @@ bool cMonster::DoTakeDamage(TakeDamageInfo & a_TDI)
|
||||
if (a_TDI.Attacker != nullptr)
|
||||
{
|
||||
m_Target = a_TDI.Attacker;
|
||||
m_TicksSinceLastDamaged = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -1100,16 +1104,27 @@ void cMonster::HandleDaylightBurning(cChunk & a_Chunk)
|
||||
// Outside the world
|
||||
return;
|
||||
}
|
||||
|
||||
int RelX = POSX_TOINT - GetChunkX() * cChunkDef::Width;
|
||||
int RelZ = POSZ_TOINT - GetChunkZ() * cChunkDef::Width;
|
||||
|
||||
if (!a_Chunk.IsLightValid())
|
||||
{
|
||||
m_World->QueueLightChunk(GetChunkX(), GetChunkZ());
|
||||
return;
|
||||
}
|
||||
|
||||
if (WouldBurnAt(GetPosition(), a_Chunk))
|
||||
{
|
||||
// Burn for 100 ticks, then decide again
|
||||
StartBurning(100);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
bool cMonster::WouldBurnAt(Vector3d a_Location, cChunk & a_Chunk)
|
||||
{
|
||||
int RelX = FloorC(a_Location.x) - a_Chunk.GetPosX() * cChunkDef::Width;
|
||||
int RelY = FloorC(a_Location.y);
|
||||
int RelZ = FloorC(a_Location.z) - a_Chunk.GetPosZ() * cChunkDef::Width;
|
||||
if (
|
||||
(a_Chunk.GetSkyLight(RelX, RelY, RelZ) == 15) && // In the daylight
|
||||
(a_Chunk.GetBlock(RelX, RelY, RelZ) != E_BLOCK_SOULSAND) && // Not on soulsand
|
||||
@ -1118,14 +1133,15 @@ void cMonster::HandleDaylightBurning(cChunk & a_Chunk)
|
||||
GetWorld()->IsWeatherSunnyAt(POSX_TOINT, POSZ_TOINT) // Not raining
|
||||
)
|
||||
{
|
||||
// Burn for 100 ticks, then decide again
|
||||
StartBurning(100);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
cMonster::eFamily cMonster::GetMobFamily(void) const
|
||||
{
|
||||
return FamilyFromType(m_MobType);
|
||||
|
@ -10,11 +10,12 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class cClientHandle;
|
||||
class cWorld;
|
||||
|
||||
// Fwd: cPath
|
||||
enum class ePathFinderStatus;
|
||||
class cPath;
|
||||
|
||||
|
||||
|
||||
@ -61,6 +62,7 @@ public:
|
||||
virtual void OnRightClicked(cPlayer & a_Player) override;
|
||||
|
||||
virtual void MoveToPosition(const Vector3d & a_Position); // tolua_export
|
||||
virtual void StopMovingToPosition();
|
||||
virtual bool ReachedDestination(void);
|
||||
|
||||
// tolua_begin
|
||||
@ -162,6 +164,11 @@ protected:
|
||||
|
||||
/** A pointer to the entity this mobile is aiming to reach */
|
||||
cEntity * m_Target;
|
||||
cPath * m_Path; // TODO unique ptr
|
||||
ePathFinderStatus m_PathStatus;
|
||||
bool m_IsFollowingPath;
|
||||
/* If 0, will give up reaching the next m_Dest and will re-compute path. */
|
||||
int m_GiveUpCounter;
|
||||
/** Coordinates of the next position that should be reached */
|
||||
Vector3d m_Destination;
|
||||
/** Coordinates for the ultimate, final destination. */
|
||||
@ -201,11 +208,7 @@ protected:
|
||||
This is based on the ultimate, final destination and the current position, as well as the traversed coordinates, and any environmental hazards */
|
||||
void TickPathFinding(void);
|
||||
/** Finishes a pathfinding task, be it due to failure or something else */
|
||||
inline void FinishPathFinding(void)
|
||||
{
|
||||
m_TraversedCoordinates.clear();
|
||||
m_bMovingToDestination = false;
|
||||
}
|
||||
void FinishPathFinding(void);
|
||||
/** Sets the body yaw and head yaw/pitch based on next/ultimate destinations */
|
||||
void SetPitchAndYawFromDestination(void);
|
||||
|
||||
@ -239,10 +242,11 @@ protected:
|
||||
float m_DropChanceLeggings;
|
||||
float m_DropChanceBoots;
|
||||
bool m_CanPickUpLoot;
|
||||
int m_TicksSinceLastDamaged; // How many ticks ago we were last damaged by a player?
|
||||
|
||||
void HandleDaylightBurning(cChunk & a_Chunk);
|
||||
bool WouldBurnAt(Vector3d a_Location, cChunk & a_Chunk);
|
||||
bool m_BurnsInDaylight;
|
||||
|
||||
double m_RelativeWalkSpeed;
|
||||
|
||||
/** Adds a random number of a_Item between a_Min and a_Max to itemdrops a_Drops*/
|
||||
|
379
src/Mobs/Path.cpp
Normal file
379
src/Mobs/Path.cpp
Normal file
@ -0,0 +1,379 @@
|
||||
#include "Globals.h"
|
||||
#ifndef COMPILING_PATHFIND_DEBUGGER
|
||||
/* MCServer headers */
|
||||
#include "../World.h"
|
||||
#include "../Chunk.h"
|
||||
#endif
|
||||
|
||||
#include <cmath>
|
||||
#include "Path.h"
|
||||
|
||||
#define DISTANCE_MANHATTAN 0 // 1: More speed, a bit less accuracy 0: Max accuracy, less speed.
|
||||
#define HEURISTICS_ONLY 0 // 1: Much more speed, much less accurate.
|
||||
#define CALCULATIONS_PER_STEP 5 // Higher means more CPU load but faster path calculations.
|
||||
// The only version which guarantees the shortest path is 0, 0.
|
||||
|
||||
enum class eCellStatus {OPENLIST, CLOSEDLIST, NOLIST};
|
||||
struct cPathCell
|
||||
{
|
||||
Vector3d m_Location; // Location of the cell in the world.
|
||||
int m_F, m_G, m_H; // F, G, H as defined in regular A*.
|
||||
eCellStatus m_Status; // Which list is the cell in? Either non, open, or closed.
|
||||
cPathCell * m_Parent; // Cell's parent, as defined in regular A*.
|
||||
bool m_IsSolid; // Is the cell an air or a solid? Partial solids are currently considered solids.
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
bool compareHeuristics::operator()(cPathCell * & a_Cell1, cPathCell * & a_Cell2)
|
||||
{
|
||||
return a_Cell1->m_F > a_Cell2->m_F;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* cPath implementation */
|
||||
cPath::cPath(
|
||||
cWorld * a_World,
|
||||
const Vector3d & a_StartingPoint, const Vector3d & a_EndingPoint, int a_MaxSteps,
|
||||
double a_BoundingBoxWidth, double a_BoundingBoxHeight,
|
||||
int a_MaxUp, int a_MaxDown
|
||||
)
|
||||
{
|
||||
// TODO: if src not walkable OR dest not walkable, then abort.
|
||||
// Borrow a new "isWalkable" from ProcessIfWalkable, make ProcessIfWalkable also call isWalkable
|
||||
|
||||
m_World = a_World;
|
||||
// m_World = cRoot::Get()->GetDefaultWorld();
|
||||
|
||||
m_Source = a_StartingPoint.Floor();
|
||||
m_Destination = a_EndingPoint.Floor();
|
||||
|
||||
if (GetCell(m_Source)->m_IsSolid || GetCell(m_Destination)->m_IsSolid)
|
||||
{
|
||||
m_Status = ePathFinderStatus::PATH_NOT_FOUND;
|
||||
return;
|
||||
}
|
||||
|
||||
m_Status = ePathFinderStatus::CALCULATING;
|
||||
|
||||
m_StepsLeft = a_MaxSteps;
|
||||
m_PointCount = 0;
|
||||
|
||||
ProcessCell(GetCell(a_StartingPoint), nullptr, 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
cPath::~cPath()
|
||||
{
|
||||
if (m_Status == ePathFinderStatus::CALCULATING)
|
||||
{
|
||||
FinishCalculation();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
ePathFinderStatus cPath::Step()
|
||||
{
|
||||
if (m_Status != ePathFinderStatus::CALCULATING)
|
||||
{
|
||||
return m_Status;
|
||||
}
|
||||
|
||||
if (m_StepsLeft == 0)
|
||||
{
|
||||
FinishCalculation(ePathFinderStatus::PATH_NOT_FOUND);
|
||||
}
|
||||
else
|
||||
{
|
||||
--m_StepsLeft;
|
||||
int i;
|
||||
for (i = 0; i < CALCULATIONS_PER_STEP; ++i)
|
||||
{
|
||||
if (Step_Internal()) // Step_Internal returns true when no more calculation is needed.
|
||||
{
|
||||
break; // if we're here, m_Status must have changed either to PATH_FOUND or PATH_NOT_FOUND.
|
||||
}
|
||||
}
|
||||
}
|
||||
return m_Status;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#ifndef COMPILING_PATHFIND_DEBUGGER
|
||||
bool cPath::IsSolid(const Vector3d & a_Location)
|
||||
{
|
||||
int ChunkX, ChunkZ;
|
||||
m_Item_CurrentBlock = a_Location;
|
||||
cChunkDef::BlockToChunk(a_Location.x, a_Location.z, ChunkX, ChunkZ);
|
||||
return !m_World->DoWithChunk(ChunkX, ChunkZ, * this);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
bool cPath::Step_Internal()
|
||||
{
|
||||
cPathCell * CurrentCell = OpenListPop();
|
||||
|
||||
// Path not reachable, open list exauhsted.
|
||||
if (CurrentCell == nullptr)
|
||||
{
|
||||
FinishCalculation(ePathFinderStatus::PATH_NOT_FOUND);
|
||||
ASSERT(m_Status == ePathFinderStatus::PATH_NOT_FOUND);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Path found.
|
||||
if (CurrentCell->m_Location == m_Destination)
|
||||
{
|
||||
do
|
||||
{
|
||||
AddPoint(CurrentCell->m_Location + Vector3d(0.5, 0, 0.5)); // Populate the cPath with points.
|
||||
CurrentCell = CurrentCell->m_Parent;
|
||||
} while (CurrentCell != nullptr);
|
||||
|
||||
m_CurrentPoint = -1;
|
||||
FinishCalculation(ePathFinderStatus::PATH_FOUND);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Calculation not finished yet, process a currentCell by inspecting all neighbors.
|
||||
|
||||
// Check North, South, East, West on all 3 different heights.
|
||||
int i;
|
||||
for (i = -1; i <= 1; ++i)
|
||||
{
|
||||
ProcessIfWalkable(CurrentCell->m_Location + Vector3d(1, i, 0), CurrentCell, 10);
|
||||
ProcessIfWalkable(CurrentCell->m_Location + Vector3d(-1, i, 0), CurrentCell, 10);
|
||||
ProcessIfWalkable(CurrentCell->m_Location + Vector3d(0, i, 1), CurrentCell, 10);
|
||||
ProcessIfWalkable(CurrentCell->m_Location + Vector3d(0, i, -1), CurrentCell, 10);
|
||||
}
|
||||
|
||||
// Check diagonals on mob's height only.
|
||||
int x, z;
|
||||
for (x = -1; x <= 1; x += 2)
|
||||
{
|
||||
for (z = -1; z <= 1; z += 2)
|
||||
{
|
||||
// This condition prevents diagonal corner cutting.
|
||||
if (!GetCell(CurrentCell->m_Location + Vector3d(x, 0, 0))->m_IsSolid && !GetCell(CurrentCell->m_Location + Vector3d(0, 0, z))->m_IsSolid)
|
||||
{
|
||||
// This prevents falling of "sharp turns" e.g. a 1x1x20 rectangle in the air which breaks in a right angle suddenly.
|
||||
if (GetCell(CurrentCell->m_Location + Vector3d(x, -1, 0))->m_IsSolid && GetCell(CurrentCell->m_Location + Vector3d(0, -1, z))->m_IsSolid)
|
||||
{
|
||||
ProcessIfWalkable(CurrentCell->m_Location + Vector3d(x, 0, z), CurrentCell, 14); // 14 is a good enough approximation of sqrt(10 + 10).
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void cPath::FinishCalculation()
|
||||
{
|
||||
for (auto && pair : m_Map)
|
||||
{
|
||||
delete pair.second;
|
||||
}
|
||||
|
||||
m_Map.clear();
|
||||
m_OpenList.empty();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void cPath::FinishCalculation(ePathFinderStatus a_NewStatus)
|
||||
{
|
||||
m_Status = a_NewStatus;
|
||||
FinishCalculation();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void cPath::OpenListAdd(cPathCell * a_Cell)
|
||||
{
|
||||
a_Cell->m_Status = eCellStatus::OPENLIST;
|
||||
m_OpenList.push(a_Cell);
|
||||
#ifdef COMPILING_PATHFIND_DEBUGGER
|
||||
si::setBlock(a_Cell->m_Location.x, a_Cell->m_Location.y, a_Cell->m_Location.z, debug_open, SetMini(a_Cell));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
cPathCell * cPath::OpenListPop() // Popping from the open list also means adding to the closed list.
|
||||
{
|
||||
if (m_OpenList.size() == 0)
|
||||
{
|
||||
return nullptr; // We've exhausted the search space and nothing was found, this will trigger a PATH_NOT_FOUND status.
|
||||
}
|
||||
|
||||
cPathCell * Ret = m_OpenList.top();
|
||||
m_OpenList.pop();
|
||||
Ret->m_Status = eCellStatus::CLOSEDLIST;
|
||||
#ifdef COMPILING_PATHFIND_DEBUGGER
|
||||
si::setBlock((Ret)->m_Location.x, (Ret)->m_Location.y, (Ret)->m_Location.z, debug_closed, SetMini(Ret));
|
||||
#endif
|
||||
return Ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void cPath::ProcessIfWalkable(const Vector3d & a_Location, cPathCell * a_Parent, int a_Cost)
|
||||
{
|
||||
cPathCell * cell = GetCell(a_Location);
|
||||
if (!cell->m_IsSolid && GetCell(a_Location + Vector3d(0, -1, 0))->m_IsSolid && !GetCell(a_Location + Vector3d(0, 1, 0))->m_IsSolid)
|
||||
{
|
||||
ProcessCell(cell, a_Parent, a_Cost);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void cPath::ProcessCell(cPathCell * a_Cell, cPathCell * a_Caller, int a_GDelta)
|
||||
{
|
||||
// Case 1: Cell is in the closed list, ignore it.
|
||||
if (a_Cell->m_Status == eCellStatus::CLOSEDLIST)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (a_Cell->m_Status == eCellStatus::NOLIST) // Case 2: The cell is not in any list.
|
||||
{
|
||||
// Cell is walkable, add it to the open list.
|
||||
// Note that non-walkable cells are filtered out in Step_internal();
|
||||
// Special case: Start cell goes here, gDelta is 0, caller is NULL.
|
||||
a_Cell->m_Parent = a_Caller;
|
||||
if (a_Caller != nullptr)
|
||||
{
|
||||
a_Cell->m_G = a_Caller->m_G + a_GDelta;
|
||||
}
|
||||
else
|
||||
{
|
||||
a_Cell->m_G = 0;
|
||||
}
|
||||
|
||||
// Calculate H. This is A*'s Heuristics value.
|
||||
#if DISTANCE_MANHATTAN == 1
|
||||
// Manhattan distance. DeltaX + DeltaY + DeltaZ.
|
||||
a_Cell->m_H = 10 * (abs(a_Cell->m_Location.x-m_Destination.x) + abs(a_Cell->m_Location.y-m_Destination.y) + abs(a_Cell->m_Location.z-m_Destination.z));
|
||||
#else
|
||||
// Euclidian distance. sqrt(DeltaX^2 + DeltaY^2 + DeltaZ^2), more precise.
|
||||
a_Cell->m_H = std::sqrt((a_Cell->m_Location.x - m_Destination.x) * (a_Cell->m_Location.x - m_Destination.x) * 100 + (a_Cell->m_Location.y - m_Destination.y) * (a_Cell->m_Location.y - m_Destination.y) * 100 + (a_Cell->m_Location.z - m_Destination.z) * (a_Cell->m_Location.z - m_Destination.z) * 100);
|
||||
#endif
|
||||
|
||||
#if HEURISTICS_ONLY == 1
|
||||
a_Cell->m_F = a_Cell->m_H; // Greedy search. https://en.wikipedia.org/wiki/Greedy_search
|
||||
#else
|
||||
a_Cell->m_F = a_Cell->m_H + a_Cell->m_G; // Regular A*.
|
||||
#endif
|
||||
|
||||
OpenListAdd(a_Cell);
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 3: Cell is in the open list, check if G and H need an update.
|
||||
int NewG = a_Caller->m_G + a_GDelta;
|
||||
if (NewG < a_Cell->m_G)
|
||||
{
|
||||
a_Cell->m_G = NewG;
|
||||
a_Cell->m_H = a_Cell->m_F + a_Cell->m_G;
|
||||
a_Cell->m_Parent = a_Caller;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
cPathCell * cPath::GetCell(const Vector3d & a_Location)
|
||||
{
|
||||
// Create the cell in the hash table if it's not already there.
|
||||
cPathCell * Cell;
|
||||
if (m_Map.count(a_Location) == 0) // Case 1: Cell is not on any list. We've never checked this cell before.
|
||||
{
|
||||
Cell = new cPathCell();
|
||||
Cell->m_Location = a_Location;
|
||||
m_Map[a_Location] = Cell;
|
||||
Cell->m_IsSolid = IsSolid(a_Location);
|
||||
Cell->m_Status = eCellStatus::NOLIST;
|
||||
#ifdef COMPILING_PATHFIND_DEBUGGER
|
||||
#ifdef COMPILING_PATHFIND_DEBUGGER_MARK_UNCHECKED
|
||||
si::setBlock(a_Location.x, a_Location.y, a_Location.z, debug_unchecked, Cell->m_IsSolid ? NORMAL : MINI);
|
||||
#endif
|
||||
#endif
|
||||
return Cell;
|
||||
}
|
||||
else
|
||||
{
|
||||
return m_Map[a_Location];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Add the next point in the final path.
|
||||
void cPath::AddPoint(Vector3d a_Vector)
|
||||
{
|
||||
m_PathPoints.push_back(a_Vector);
|
||||
++m_PointCount;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#ifndef COMPILING_PATHFIND_DEBUGGER
|
||||
bool cPath::Item(cChunk * a_Chunk) // returns FALSE if there's a solid or if we failed.
|
||||
{
|
||||
int RelX = m_Item_CurrentBlock.x - a_Chunk->GetPosX() * cChunkDef::Width;
|
||||
int RelZ = m_Item_CurrentBlock.z - a_Chunk->GetPosZ() * cChunkDef::Width;
|
||||
|
||||
if (!a_Chunk->IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
BLOCKTYPE BlockType;
|
||||
NIBBLETYPE BlockMeta;
|
||||
a_Chunk->GetBlockTypeMeta(RelX, m_Item_CurrentBlock.y, RelZ, BlockType, BlockMeta);
|
||||
return (!cBlockInfo::IsSolid(BlockType));
|
||||
|
||||
// TODO Maybe I should queue several blocks and call item() at once for all of them for better performance?
|
||||
// I think Worktycho said each item() call needs 2 locks.
|
||||
|
||||
}
|
||||
#endif
|
161
src/Mobs/Path.h
Normal file
161
src/Mobs/Path.h
Normal file
@ -0,0 +1,161 @@
|
||||
#pragma once
|
||||
|
||||
/* Wanna use the pathfinder? Put this in your header file:
|
||||
|
||||
// Fwd: cPath
|
||||
enum class ePathFinderStatus;
|
||||
class cPath;
|
||||
|
||||
Put this in your .cpp:
|
||||
#include "...Path.h"
|
||||
*/
|
||||
|
||||
#ifdef COMPILING_PATHFIND_DEBUGGER
|
||||
/* Note: the COMPILING_PATHFIND_DEBUGGER flag is used by Native/WiseOldMan95 to debug
|
||||
this class outside of MCServer. This preprocessor flag is never set when compiling MCServer. */
|
||||
#include "PathFinderIrrlicht_Head.h"
|
||||
#endif
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
/* MCServer forward declarations */
|
||||
#ifndef COMPILING_PATHFIND_DEBUGGER
|
||||
|
||||
// fwd: cChunkMap.h
|
||||
typedef cItemCallback<cChunk> cChunkCallback;
|
||||
#endif
|
||||
|
||||
/* Various little structs and classes */
|
||||
enum class ePathFinderStatus {CALCULATING, PATH_FOUND, PATH_NOT_FOUND};
|
||||
struct cPathCell; // Defined inside Path.cpp
|
||||
class compareHeuristics
|
||||
{
|
||||
public:
|
||||
bool operator()(cPathCell * & a_V1, cPathCell * & a_V2);
|
||||
};
|
||||
|
||||
class cPath
|
||||
#ifndef COMPILING_PATHFIND_DEBUGGER
|
||||
: public cChunkCallback
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
/** Creates a pathfinder instance. A Mob will probably need a single pathfinder instance for its entire life.
|
||||
|
||||
Note that if you have a man-sized mob (1x1x2, zombies, etc), you are advised to call this function without parameters
|
||||
because the declaration might change in later version of the pathFinder, and a parameter-less call always assumes a man-sized mob.
|
||||
|
||||
If your mob is not man-sized, you are advised to use cPath(width, height), this would be compatible with future versions,
|
||||
but please be aware that as of now those parameters will be ignored and your mob will be assumed to be man sized.
|
||||
|
||||
@param a_BoundingBoxWidth the character's boundingbox width in blocks. Currently the parameter is ignored and 1 is assumed.
|
||||
@param a_BoundingBoxHeight the character's boundingbox width in blocks. Currently the parameter is ignored and 2 is assumed.
|
||||
@param a_MaxUp the character's max jump height in blocks. Currently the parameter is ignored and 1 is assumed.
|
||||
@param a_MaxDown How far is the character willing to fall? Currently the parameter is ignored and 1 is assumed. */
|
||||
/** Attempts to find a path starting from source to destination.
|
||||
After calling this, you are expected to call Step() once per tick or once per several ticks until it returns true. You should then call getPath() to obtain the path.
|
||||
Calling this before a path is found resets the current path and starts another search.
|
||||
@param a_StartingPoint The function expects this position to be the lowest block the mob is in, a rule of thumb: "The block where the Zombie's knees are at".
|
||||
@param a_EndingPoint "The block where the Zombie's knees want to be".
|
||||
@param a_MaxSteps The maximum steps before giving up. */
|
||||
cPath(
|
||||
cWorld * a_World,
|
||||
const Vector3d & a_StartingPoint, const Vector3d & a_EndingPoint, int a_MaxSteps,
|
||||
double a_BoundingBoxWidth = 1, double a_BoundingBoxHeight = 2,
|
||||
int a_MaxUp = 1, int a_MaxDown = 1
|
||||
);
|
||||
|
||||
/** Destroys the path and frees its memory. */
|
||||
~cPath();
|
||||
|
||||
/** Performs part of the path calculation and returns true if the path computation has finished. */
|
||||
ePathFinderStatus Step();
|
||||
|
||||
/* Point retrieval functions, inlined for performance. */
|
||||
/** Returns the next point in the path. */
|
||||
inline Vector3d GetNextPoint()
|
||||
{
|
||||
ASSERT(m_Status == ePathFinderStatus::PATH_FOUND);
|
||||
return m_PathPoints[m_PointCount - 1 - (++m_CurrentPoint)];
|
||||
}
|
||||
/** Checks whether this is the last point or not. Never call getnextPoint when this is true. */
|
||||
inline bool IsLastPoint()
|
||||
{
|
||||
ASSERT(m_Status == ePathFinderStatus::PATH_FOUND);
|
||||
ASSERT(m_CurrentPoint != -1); // You must call getFirstPoint at least once before calling this.
|
||||
return (m_CurrentPoint == m_PointCount - 1);
|
||||
}
|
||||
/** Get the point at a_index. Remark: Internally, the indexes are reversed. */
|
||||
inline Vector3d GetPoint(int a_index)
|
||||
{
|
||||
ASSERT(m_Status == ePathFinderStatus::PATH_FOUND);
|
||||
ASSERT(a_index < m_PointCount);
|
||||
return m_PathPoints[m_PointCount - 1 - a_index];
|
||||
}
|
||||
/** Returns the total number of points this path has. */
|
||||
inline int GetPointCount()
|
||||
{
|
||||
ASSERT(m_Status == ePathFinderStatus::PATH_FOUND);
|
||||
return m_PointCount;
|
||||
}
|
||||
|
||||
struct VectorHasher
|
||||
{
|
||||
std::size_t operator()(const Vector3d & a_Vector) const
|
||||
{
|
||||
// Guaranteed to have no hash collisions for any 128x128x128 area. Suitable for pathfinding.
|
||||
int32_t t = 0;
|
||||
t += (int8_t)a_Vector.x;
|
||||
t = t << 8;
|
||||
t += (int8_t)a_Vector.y;
|
||||
t = t << 8;
|
||||
t += (int8_t)a_Vector.z;
|
||||
t = t << 8;
|
||||
return (size_t)t;
|
||||
}
|
||||
};
|
||||
private:
|
||||
|
||||
/* General */
|
||||
bool IsSolid(const Vector3d & a_Location); // Query our hosting world and ask it if there's a solid at a_location.
|
||||
bool Step_Internal(); // The public version just calls this version * CALCULATIONS_PER_CALL times.
|
||||
void FinishCalculation(); // Clears the memory used for calculating the path.
|
||||
void FinishCalculation(ePathFinderStatus a_NewStatus); // Clears the memory used for calculating the path and changes the status.
|
||||
|
||||
/* Openlist and closedlist management */
|
||||
void OpenListAdd(cPathCell * a_Cell);
|
||||
cPathCell * OpenListPop();
|
||||
void ProcessIfWalkable(const Vector3d &a_Location, cPathCell * a_Parent, int a_Cost);
|
||||
|
||||
/* Map management */
|
||||
void ProcessCell(cPathCell * a_Cell, cPathCell * a_Caller, int a_GDelta);
|
||||
cPathCell * GetCell(const Vector3d & a_location);
|
||||
|
||||
/* Pathfinding fields */
|
||||
std::priority_queue<cPathCell *, std::vector<cPathCell *>, compareHeuristics> m_OpenList;
|
||||
std::unordered_map<Vector3d, cPathCell *, VectorHasher> m_Map;
|
||||
Vector3d m_Destination;
|
||||
Vector3d m_Source;
|
||||
int m_StepsLeft;
|
||||
|
||||
/* Control fields */
|
||||
ePathFinderStatus m_Status;
|
||||
|
||||
/* Final path fields */
|
||||
int m_PointCount;
|
||||
int m_CurrentPoint;
|
||||
std::vector<Vector3d> m_PathPoints;
|
||||
void AddPoint(Vector3d a_Vector);
|
||||
|
||||
/* Interfacing with MCServer's world */
|
||||
cWorld * m_World;
|
||||
#ifndef COMPILING_PATHFIND_DEBUGGER
|
||||
Vector3d m_Item_CurrentBlock; // Read by Item();, it's the only way to "pass it" parameters
|
||||
protected:
|
||||
virtual bool Item(cChunk * a_Chunk) override;
|
||||
|
||||
/* Interfacing with Irrlicht, has nothing to do with MCServer*/
|
||||
#else
|
||||
#include "../path_irrlicht.cpp"
|
||||
#endif
|
||||
};
|
@ -90,7 +90,6 @@ void cPig::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
|
||||
if (m_Attachee->IsPlayer() && (m_Attachee->GetEquippedWeapon().m_ItemType == E_ITEM_CARROT_ON_STICK))
|
||||
{
|
||||
MoveToPosition((m_Attachee->GetPosition()) + (m_Attachee->GetLookVector()*10));
|
||||
m_bMovingToDestination = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -98,7 +98,7 @@ void cSheep::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
|
||||
|
||||
if (m_TimeToStopEating > 0)
|
||||
{
|
||||
m_bMovingToDestination = false; // The sheep should not move when he's eating
|
||||
StopMovingToPosition();
|
||||
m_TimeToStopEating--;
|
||||
|
||||
if (m_TimeToStopEating == 0)
|
||||
|
@ -50,17 +50,18 @@ void cSkeleton::GetDrops(cItems & a_Drops, cEntity * a_Killer)
|
||||
|
||||
void cSkeleton::MoveToPosition(const Vector3d & a_Position)
|
||||
{
|
||||
// If the destination is sufficiently skylight challenged AND the skeleton isn't on fire then block the movement
|
||||
// Todo use WouldBurnAt(), not sure how to obtain a chunk though...
|
||||
super::MoveToPosition(a_Position); // Look at the player and update m_Destination to hit them if they're close
|
||||
|
||||
// If the destination is sufficiently skylight challenged AND the skeleton isn't on fire AND we weren't attacked recently then block the movement
|
||||
if (
|
||||
!IsOnFire() &&
|
||||
(m_World->GetBlockSkyLight((int)floor(a_Position.x), (int)floor(a_Position.y), (int)floor(a_Position.z)) - m_World->GetSkyDarkness() > 8)
|
||||
(m_World->GetBlockSkyLight((int)floor(a_Position.x), (int)floor(a_Position.y), (int)floor(a_Position.z)) - m_World->GetSkyDarkness() > 8) &&
|
||||
m_TicksSinceLastDamaged == 100
|
||||
)
|
||||
{
|
||||
m_bMovingToDestination = false;
|
||||
return;
|
||||
StopMovingToPosition();
|
||||
}
|
||||
|
||||
super::MoveToPosition(a_Position);
|
||||
}
|
||||
|
||||
|
||||
|
@ -203,7 +203,7 @@ void cWolf::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
|
||||
}
|
||||
else if (IsSitting())
|
||||
{
|
||||
m_bMovingToDestination = false;
|
||||
StopMovingToPosition();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,17 +44,18 @@ void cZombie::GetDrops(cItems & a_Drops, cEntity * a_Killer)
|
||||
|
||||
void cZombie::MoveToPosition(const Vector3d & a_Position)
|
||||
{
|
||||
// If the destination is sufficiently skylight challenged AND the skeleton isn't on fire then block the movement
|
||||
// Todo use WouldBurnAt(), not sure how to obtain a chunk though...
|
||||
super::MoveToPosition(a_Position); // Look at the player and update m_Destination to hit them if they're close
|
||||
|
||||
// If the destination is sufficiently skylight challenged AND the skeleton isn't on fire AND we weren't attacked recently then block the movement
|
||||
if (
|
||||
!IsOnFire() &&
|
||||
(m_World->GetBlockSkyLight((int)floor(a_Position.x), (int)floor(a_Position.y), (int)floor(a_Position.z)) - m_World->GetSkyDarkness() > 8)
|
||||
(m_World->GetBlockSkyLight((int)floor(a_Position.x), (int)floor(a_Position.y), (int)floor(a_Position.z)) - m_World->GetSkyDarkness() > 8) &&
|
||||
m_TicksSinceLastDamaged == 100
|
||||
)
|
||||
{
|
||||
m_bMovingToDestination = false;
|
||||
return;
|
||||
StopMovingToPosition();
|
||||
}
|
||||
|
||||
super::MoveToPosition(a_Position);
|
||||
}
|
||||
|
||||
|
||||
|
@ -354,6 +354,7 @@ protected:
|
||||
|
||||
|
||||
|
||||
|
||||
template <> inline Vector3<int> Vector3<int>::Floor(void) const
|
||||
{
|
||||
return *this;
|
||||
|
Loading…
Reference in New Issue
Block a user