1
0
Fork 0

A* Pathfinding and better monster AI

This commit is contained in:
wiseoldman95 2015-04-29 19:24:14 +03:00
parent 259132d17e
commit 1b0e21e0b2
13 changed files with 737 additions and 176 deletions

View File

@ -37,10 +37,7 @@ void cAggressiveMonster::InStateChasing(std::chrono::milliseconds a_Dt)
}
}
if (!IsMovingToTargetPosition())
{
MoveToPosition(m_Target->GetPosition());
}
MoveToPosition(m_Target->GetPosition());
}
}
@ -100,7 +97,7 @@ void cAggressiveMonster::Attack(std::chrono::milliseconds a_Dt)
{
return;
}
// Setting this higher gives us more wiggle room for attackrate
m_AttackInterval = 0.0;
m_Target->TakeDamage(dtMobAttack, this, m_AttackDamage, 0);

View File

@ -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

View File

@ -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},
} ;
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;
// 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);
m_IsFollowingPath = false;
}
for (size_t i = 0; i < ARRAYCOUNT(gCrossCoords); i++)
m_PathStatus = m_Path->Step();
switch (m_PathStatus)
{
if (IsCoordinateInTraversedList(Vector3i(gCrossCoords[i].x + PosX, PosY, gCrossCoords[i].z + PosZ)))
case ePathFinderStatus::PATH_NOT_FOUND:
{
continue;
FinishPathFinding();
break;
}
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)
)
case ePathFinderStatus::CALCULATING:
{
m_PotentialCoordinates.push_back(Vector3d((gCrossCoords[i].x + PosX), PosY, gCrossCoords[i].z + PosZ));
m_Destination = GetPosition();
break;
}
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)
case ePathFinderStatus::PATH_FOUND:
{
Vector3f Distance = m_FinalDestination - ShortestCoords;
Vector3f Distance2 = m_FinalDestination - *itr;
if (Distance.SqrLength() > Distance2.SqrLength())
if (ReachedDestination() || !m_IsFollowingPath)
{
ShortestCoords = *itr;
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;
m_Destination = ShortestCoords;
m_Destination.z += 0.5f;
m_Destination.x += 0.5f;
}
else
{
FinishPathFinding();
}
}
}
@ -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,13 +236,14 @@ bool cMonster::ReachedDestination()
bool cMonster::ReachedFinalDestination()
{
if ((GetPosition() - m_FinalDestination).Length() <= m_AttackRange)
{
return true;
}
return false;
}
@ -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,53 +293,51 @@ 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
{
Distance.y = 0;
Distance.Normalize();
if (m_bOnGround)
{
Distance *= 2.5f;
}
else if (IsSwimming())
{
Distance *= 1.3f;
}
else
{
// Don't let the mob move too much if he's falling.
Distance *= 0.25f;
}
// Apply walk speed:
Distance *= m_RelativeWalkSpeed;
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
{
if (ReachedFinalDestination()) // If we have reached the ultimate, final destination, stop pathfinding and attack if appropriate
if (--m_GiveUpCounter == 0)
{
FinishPathFinding();
}
else
{
TickPathFinding(); // We have reached the next point in our path, calculate another point
Distance.y = 0;
Distance.Normalize();
if (m_bOnGround)
{
Distance *= 2.5f;
}
else if (IsSwimming())
{
Distance *= 1.3f;
}
else
{
// Don't let the mob move too much if he's falling.
Distance *= 0.25f;
}
// 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);
}
}
else if (ReachedFinalDestination())
{
StopMovingToPosition();
}
}
SetPitchAndYawFromDestination();
@ -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;
}
@ -360,12 +362,13 @@ void cMonster::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
InStateEscaping(a_Dt);
break;
}
case ATTACKING: break;
} // 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;
}
@ -692,7 +696,7 @@ void cMonster::InStateChasing(std::chrono::milliseconds a_Dt)
void cMonster::InStateEscaping(std::chrono::milliseconds a_Dt)
{
UNUSED(a_Dt);
if (m_Target != nullptr)
{
Vector3d newloc = GetPosition();
@ -771,7 +775,7 @@ AString cMonster::MobTypeToString(eMonsterType a_MobType)
return g_MobTypeNames[i].m_lcName;
}
}
// Not found:
return "";
}
@ -866,7 +870,7 @@ cMonster::eFamily cMonster::FamilyFromType(eMonsterType a_Type)
case mtWolf: return mfHostile;
case mtZombie: return mfHostile;
case mtZombiePigman: return mfHostile;
case mtInvalidType: break;
}
ASSERT(!"Unhandled mob type");
@ -1041,7 +1045,7 @@ void cMonster::AddRandomArmorDropItem(cItems & a_Drops, short a_LootingLevel)
a_Drops.push_back(GetEquippedHelmet());
}
}
if (r1.randInt() % 200 < ((m_DropChanceChestplate * 200) + (a_LootingLevel * 2)))
{
if (!GetEquippedChestplate().IsEmpty())
@ -1049,7 +1053,7 @@ void cMonster::AddRandomArmorDropItem(cItems & a_Drops, short a_LootingLevel)
a_Drops.push_back(GetEquippedChestplate());
}
}
if (r1.randInt() % 200 < ((m_DropChanceLeggings * 200) + (a_LootingLevel * 2)))
{
if (!GetEquippedLeggings().IsEmpty())
@ -1057,7 +1061,7 @@ void cMonster::AddRandomArmorDropItem(cItems & a_Drops, short a_LootingLevel)
a_Drops.push_back(GetEquippedLeggings());
}
}
if (r1.randInt() % 200 < ((m_DropChanceBoots * 200) + (a_LootingLevel * 2)))
{
if (!GetEquippedBoots().IsEmpty())
@ -1093,23 +1097,34 @@ void cMonster::HandleDaylightBurning(cChunk & a_Chunk)
{
return;
}
int RelY = POSY_TOINT;
if ((RelY < 0) || (RelY >= cChunkDef::Height))
{
// 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);

View File

@ -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
View 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
View 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
};

View File

@ -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;
}
}
}

View File

@ -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)

View File

@ -37,7 +37,7 @@ void cSkeleton::GetDrops(cItems & a_Drops, cEntity * a_Killer)
else
{
AddRandomDropItem(a_Drops, 0, 2 + LootingLevel, E_ITEM_ARROW);
}
AddRandomDropItem(a_Drops, 0, 2 + LootingLevel, E_ITEM_BONE);
AddRandomArmorDropItem(a_Drops, LootingLevel);
@ -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);
}

View File

@ -137,7 +137,7 @@ void cWolf::OnRightClicked(cPlayer & a_Player)
}
}
}
m_World->BroadcastEntityMetadata(*this);
}
@ -203,7 +203,7 @@ void cWolf::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
}
else if (IsSitting())
{
m_bMovingToDestination = false;
StopMovingToPosition();
}
}

View File

@ -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);
}

View File

@ -113,7 +113,7 @@ void cServer::cTickThread::Execute(void)
auto msec = std::chrono::duration_cast<std::chrono::milliseconds>(NowTime - LastTime).count();
m_ShouldTerminate = !m_Server.Tick(static_cast<float>(msec));
auto TickTime = std::chrono::steady_clock::now() - NowTime;
if (TickTime < msPerTick)
{
// Stretch tick time until it's at least msPerTick
@ -206,7 +206,7 @@ bool cServer::InitServer(cIniFile & a_SettingsIni, bool a_ShouldAuth)
LOGINFO("Compatible protocol versions %s", MCS_PROTOCOL_VERSIONS);
m_Ports = ReadUpgradeIniPorts(a_SettingsIni, "Server", "Ports", "Port", "PortsIPv6", "25565");
m_RCONServer.Initialize(a_SettingsIni);
m_bIsConnected = true;
@ -231,10 +231,10 @@ bool cServer::InitServer(cIniFile & a_SettingsIni, bool a_ShouldAuth)
{
LOGWARNING("WARNING: BungeeCord is allowed and server set to online mode. This is unsafe and will not work properly. Disable either authentication or BungeeCord in settings.ini.");
}
m_ShouldLoadOfflinePlayerData = a_SettingsIni.GetValueSetB("PlayerData", "LoadOfflinePlayerData", false);
m_ShouldLoadNamedPlayerData = a_SettingsIni.GetValueSetB("PlayerData", "LoadNamedPlayerData", true);
m_ClientViewDistance = a_SettingsIni.GetValueSetI("Server", "DefaultViewDistance", cClientHandle::DEFAULT_VIEW_DISTANCE);
if (m_ClientViewDistance < cClientHandle::MIN_VIEW_DISTANCE)
{
@ -246,9 +246,9 @@ bool cServer::InitServer(cIniFile & a_SettingsIni, bool a_ShouldAuth)
m_ClientViewDistance = cClientHandle::MAX_VIEW_DISTANCE;
LOGINFO("Setting default viewdistance to the maximum of %d", m_ClientViewDistance);
}
PrepareKeys();
return true;
}
@ -320,13 +320,13 @@ bool cServer::Tick(float a_Dt)
cCSLock Lock(m_CSPlayerCount);
m_PlayerCount += PlayerCountDiff;
}
// Send the tick to the plugins, as well as let the plugin manager reload, if asked to (issue #102):
cPluginManager::Get()->Tick(a_Dt);
// Let the Root process all the queued commands:
cRoot::Get()->TickCommands();
// Tick all clients not yet assigned to a world:
TickClients(a_Dt);
@ -351,7 +351,7 @@ void cServer::TickClients(float a_Dt)
cClientHandlePtrs RemoveClients;
{
cCSLock Lock(m_CSClients);
// Remove clients that have moved to a world (the world will be ticking them from now on)
for (auto itr = m_ClientsToRemove.begin(), end = m_ClientsToRemove.end(); itr != end; ++itr)
{
@ -365,7 +365,7 @@ void cServer::TickClients(float a_Dt)
}
} // for itr - m_ClientsToRemove[]
m_ClientsToRemove.clear();
// Tick the remaining clients, take out those that have been destroyed into RemoveClients
for (auto itr = m_Clients.begin(); itr != m_Clients.end();)
{
@ -380,7 +380,7 @@ void cServer::TickClients(float a_Dt)
++itr;
} // for itr - m_Clients[]
}
// Delete the clients that have been destroyed
RemoveClients.clear();
}
@ -439,7 +439,7 @@ void cServer::ExecuteConsoleCommand(const AString & a_Cmd, cCommandOutputCallbac
}
// "stop" and "restart" are handled in cRoot::ExecuteConsoleCommand, our caller, due to its access to controlling variables
// "help" and "reload" are to be handled by MCS, so that they work no matter what
if (split[0] == "help")
{
@ -529,7 +529,7 @@ void cServer::ExecuteConsoleCommand(const AString & a_Cmd, cCommandOutputCallbac
DumpUsedMemory(&Output);
return;
}
else if (split[0].compare("killmem") == 0)
{
for (;;)
@ -544,7 +544,7 @@ void cServer::ExecuteConsoleCommand(const AString & a_Cmd, cCommandOutputCallbac
a_Output.Finished();
return;
}
a_Output.Out("Unknown command, type 'help' for all commands.");
a_Output.Finished();
}
@ -558,13 +558,13 @@ void cServer::PrintHelp(const AStringVector & a_Split, cCommandOutputCallback &
UNUSED(a_Split);
typedef std::pair<AString, AString> AStringPair;
typedef std::vector<AStringPair> AStringPairs;
class cCallback :
public cPluginManager::cCommandEnumCallback
{
public:
cCallback(void) : m_MaxLen(0) {}
virtual bool Command(const AString & a_Command, const cPlugin * a_Plugin, const AString & a_Permission, const AString & a_HelpString) override
{
UNUSED(a_Plugin);
@ -579,7 +579,7 @@ void cServer::PrintHelp(const AStringVector & a_Split, cCommandOutputCallback &
}
return false;
}
AStringPairs m_Commands;
size_t m_MaxLen;
} Callback;
@ -625,7 +625,7 @@ void cServer::Shutdown(void)
srv->Close();
}
m_ServerHandles.clear();
// Notify the tick thread and wait for it to terminate:
m_bRestarting = true;
m_RestartEvent.Wait();

View File

@ -354,6 +354,7 @@ protected:
template <> inline Vector3<int> Vector3<int>::Floor(void) const
{
return *this;