From 1b0e21e0b270b3938b81df119d5a741a3e1e1257 Mon Sep 17 00:00:00 2001 From: wiseoldman95 Date: Wed, 29 Apr 2015 19:24:14 +0300 Subject: [PATCH] A* Pathfinding and better monster AI --- src/Mobs/AggressiveMonster.cpp | 7 +- src/Mobs/CMakeLists.txt | 2 + src/Mobs/Monster.cpp | 270 ++++++++++++----------- src/Mobs/Monster.h | 20 +- src/Mobs/Path.cpp | 379 +++++++++++++++++++++++++++++++++ src/Mobs/Path.h | 161 ++++++++++++++ src/Mobs/Pig.cpp | 1 - src/Mobs/Sheep.cpp | 2 +- src/Mobs/Skeleton.cpp | 15 +- src/Mobs/Wolf.cpp | 4 +- src/Mobs/Zombie.cpp | 13 +- src/Server.cpp | 38 ++-- src/Vector3.h | 1 + 13 files changed, 737 insertions(+), 176 deletions(-) create mode 100644 src/Mobs/Path.cpp create mode 100644 src/Mobs/Path.h diff --git a/src/Mobs/AggressiveMonster.cpp b/src/Mobs/AggressiveMonster.cpp index 526b39e39..d0fb79f6d 100644 --- a/src/Mobs/AggressiveMonster.cpp +++ b/src/Mobs/AggressiveMonster.cpp @@ -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); diff --git a/src/Mobs/CMakeLists.txt b/src/Mobs/CMakeLists.txt index 7a291dcf2..ffbcdf3ea 100644 --- a/src/Mobs/CMakeLists.txt +++ b/src/Mobs/CMakeLists.txt @@ -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 diff --git a/src/Mobs/Monster.cpp b/src/Mobs/Monster.cpp index 55d83302a..e225ff9b1 100644 --- a/src/Mobs/Monster.cpp +++ b/src/Mobs/Monster.cpp @@ -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 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::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); diff --git a/src/Mobs/Monster.h b/src/Mobs/Monster.h index 21ed0c25a..43861e021 100644 --- a/src/Mobs/Monster.h +++ b/src/Mobs/Monster.h @@ -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*/ diff --git a/src/Mobs/Path.cpp b/src/Mobs/Path.cpp new file mode 100644 index 000000000..0cb03c925 --- /dev/null +++ b/src/Mobs/Path.cpp @@ -0,0 +1,379 @@ +#include "Globals.h" +#ifndef COMPILING_PATHFIND_DEBUGGER + /* MCServer headers */ + #include "../World.h" + #include "../Chunk.h" +#endif + +#include +#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 diff --git a/src/Mobs/Path.h b/src/Mobs/Path.h new file mode 100644 index 000000000..05fd59155 --- /dev/null +++ b/src/Mobs/Path.h @@ -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 + +/* MCServer forward declarations */ +#ifndef COMPILING_PATHFIND_DEBUGGER + +// fwd: cChunkMap.h +typedef cItemCallback 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, compareHeuristics> m_OpenList; + std::unordered_map 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 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 +}; diff --git a/src/Mobs/Pig.cpp b/src/Mobs/Pig.cpp index edd4d9de4..56d6abfd5 100644 --- a/src/Mobs/Pig.cpp +++ b/src/Mobs/Pig.cpp @@ -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; } } } diff --git a/src/Mobs/Sheep.cpp b/src/Mobs/Sheep.cpp index c0cdec035..ec24f167e 100644 --- a/src/Mobs/Sheep.cpp +++ b/src/Mobs/Sheep.cpp @@ -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) diff --git a/src/Mobs/Skeleton.cpp b/src/Mobs/Skeleton.cpp index 331c8e8ad..ef049f8d4 100644 --- a/src/Mobs/Skeleton.cpp +++ b/src/Mobs/Skeleton.cpp @@ -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); } diff --git a/src/Mobs/Wolf.cpp b/src/Mobs/Wolf.cpp index b3eefdf79..c66763f17 100644 --- a/src/Mobs/Wolf.cpp +++ b/src/Mobs/Wolf.cpp @@ -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(); } } diff --git a/src/Mobs/Zombie.cpp b/src/Mobs/Zombie.cpp index 63042e252..b2738050e 100644 --- a/src/Mobs/Zombie.cpp +++ b/src/Mobs/Zombie.cpp @@ -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); } diff --git a/src/Server.cpp b/src/Server.cpp index 8b6a2e769..996de2695 100644 --- a/src/Server.cpp +++ b/src/Server.cpp @@ -113,7 +113,7 @@ void cServer::cTickThread::Execute(void) auto msec = std::chrono::duration_cast(NowTime - LastTime).count(); m_ShouldTerminate = !m_Server.Tick(static_cast(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 AStringPair; typedef std::vector 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(); diff --git a/src/Vector3.h b/src/Vector3.h index 36f277ba4..c5431438e 100644 --- a/src/Vector3.h +++ b/src/Vector3.h @@ -354,6 +354,7 @@ protected: + template <> inline Vector3 Vector3::Floor(void) const { return *this;