merge
This commit is contained in:
commit
cfee60d71a
@ -47,6 +47,7 @@ add_library(main SHARED
|
|||||||
../../../../src/utils/Perlin.cpp
|
../../../../src/utils/Perlin.cpp
|
||||||
../../../../src/utils/TaskManager.cpp
|
../../../../src/utils/TaskManager.cpp
|
||||||
../../../../src/utils/Utils.cpp
|
../../../../src/utils/Utils.cpp
|
||||||
|
../../../../src/navigation/PathFinder.cpp
|
||||||
../../../../src/planet/PlanetData.cpp
|
../../../../src/planet/PlanetData.cpp
|
||||||
../../../../src/planet/PlanetObject.cpp
|
../../../../src/planet/PlanetObject.cpp
|
||||||
../../../../src/planet/StoneObject.cpp
|
../../../../src/planet/StoneObject.cpp
|
||||||
|
|||||||
@ -118,6 +118,8 @@ set(SOURCES
|
|||||||
../src/GameConstants.cpp
|
../src/GameConstants.cpp
|
||||||
../src/ScriptEngine.h
|
../src/ScriptEngine.h
|
||||||
../src/ScriptEngine.cpp
|
../src/ScriptEngine.cpp
|
||||||
|
../src/navigation/PathFinder.h
|
||||||
|
../src/navigation/PathFinder.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(space-game001 ${SOURCES})
|
add_executable(space-game001 ${SOURCES})
|
||||||
|
|||||||
@ -75,6 +75,8 @@ add_executable(space-game001
|
|||||||
../src/GameConstants.cpp
|
../src/GameConstants.cpp
|
||||||
../src/ScriptEngine.h
|
../src/ScriptEngine.h
|
||||||
../src/ScriptEngine.cpp
|
../src/ScriptEngine.cpp
|
||||||
|
../src/navigation/PathFinder.h
|
||||||
|
../src/navigation/PathFinder.cpp
|
||||||
../src/items/GameObjectLoader.h
|
../src/items/GameObjectLoader.h
|
||||||
../src/items/GameObjectLoader.cpp
|
../src/items/GameObjectLoader.cpp
|
||||||
../src/items/Item.h
|
../src/items/Item.h
|
||||||
@ -112,6 +114,7 @@ target_compile_definitions(space-game001 PRIVATE
|
|||||||
PNG_ENABLED
|
PNG_ENABLED
|
||||||
SDL_MAIN_HANDLED
|
SDL_MAIN_HANDLED
|
||||||
# DEBUG_LIGHT
|
# DEBUG_LIGHT
|
||||||
|
# SHOW_PATH
|
||||||
# NETWORK
|
# NETWORK
|
||||||
# SIMPLIFIED
|
# SIMPLIFIED
|
||||||
)
|
)
|
||||||
|
|||||||
178
resources/config2/navigation.json
Normal file
178
resources/config2/navigation.json
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
{
|
||||||
|
"cellSize": 0.4,
|
||||||
|
"agentRadius": 0.45,
|
||||||
|
"floorY": 0.0,
|
||||||
|
"objectPadding": 0.25,
|
||||||
|
"areas": [
|
||||||
|
{
|
||||||
|
"name": "main_corridor",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[-2.2, 0.8],
|
||||||
|
[2.2, 0.8],
|
||||||
|
[2.2, -40.8],
|
||||||
|
[-2.2, -40.8]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "left_door_05",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[-3.4, -4.2],
|
||||||
|
[-2.0, -4.2],
|
||||||
|
[-2.0, -5.8],
|
||||||
|
[-3.4, -5.8]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "right_door_05",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[2.0, -4.2],
|
||||||
|
[3.4, -4.2],
|
||||||
|
[3.4, -5.8],
|
||||||
|
[2.0, -5.8]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "left_room_05",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[-8.8, -1.0],
|
||||||
|
[-3.0, -1.0],
|
||||||
|
[-3.0, -9.0],
|
||||||
|
[-8.8, -9.0]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "right_room_05",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[3.0, -1.0],
|
||||||
|
[8.8, -1.0],
|
||||||
|
[8.8, -9.0],
|
||||||
|
[3.0, -9.0]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "left_door_15",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[-3.4, -14.2],
|
||||||
|
[-2.0, -14.2],
|
||||||
|
[-2.0, -15.8],
|
||||||
|
[-3.4, -15.8]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "right_door_15",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[2.0, -14.2],
|
||||||
|
[3.4, -14.2],
|
||||||
|
[3.4, -15.8],
|
||||||
|
[2.0, -15.8]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "left_room_15",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[-8.8, -11.0],
|
||||||
|
[-3.0, -11.0],
|
||||||
|
[-3.0, -19.0],
|
||||||
|
[-8.8, -19.0]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "right_room_15",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[3.0, -11.0],
|
||||||
|
[8.8, -11.0],
|
||||||
|
[8.8, -19.0],
|
||||||
|
[3.0, -19.0]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "left_door_25",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[-3.4, -24.2],
|
||||||
|
[-2.0, -24.2],
|
||||||
|
[-2.0, -25.8],
|
||||||
|
[-3.4, -25.8]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "right_door_25",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[2.0, -24.2],
|
||||||
|
[3.4, -24.2],
|
||||||
|
[3.4, -25.8],
|
||||||
|
[2.0, -25.8]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "left_room_25",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[-8.8, -21.0],
|
||||||
|
[-3.0, -21.0],
|
||||||
|
[-3.0, -29.0],
|
||||||
|
[-8.8, -29.0]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "right_room_25",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[3.0, -21.0],
|
||||||
|
[8.8, -21.0],
|
||||||
|
[8.8, -29.0],
|
||||||
|
[3.0, -29.0]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "left_door_35",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[-3.4, -34.2],
|
||||||
|
[-2.0, -34.2],
|
||||||
|
[-2.0, -35.8],
|
||||||
|
[-3.4, -35.8]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "right_door_35",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[2.0, -34.2],
|
||||||
|
[3.4, -34.2],
|
||||||
|
[3.4, -35.8],
|
||||||
|
[2.0, -35.8]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "left_room_35",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[-8.8, -31.0],
|
||||||
|
[-3.0, -31.0],
|
||||||
|
[-3.0, -39.0],
|
||||||
|
[-8.8, -39.0]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "right_room_35",
|
||||||
|
"available": true,
|
||||||
|
"polygon": [
|
||||||
|
[3.0, -31.0],
|
||||||
|
[8.8, -31.0],
|
||||||
|
[8.8, -39.0],
|
||||||
|
[3.0, -39.0]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -3,7 +3,5 @@ varying vec3 color;
|
|||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
//gl_FragColor = vec4(color, 1.0);
|
gl_FragColor = vec4(color, 1.0);
|
||||||
gl_FragColor = vec4(1.0, 1.0, 0.5, 1.0);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -33,8 +33,46 @@ void Character::loadBinaryAnimation(AnimationState state, const std::string& fil
|
|||||||
|
|
||||||
void Character::setTarget(const Eigen::Vector3f& target,
|
void Character::setTarget(const Eigen::Vector3f& target,
|
||||||
std::function<void()> onArrived) {
|
std::function<void()> onArrived) {
|
||||||
walkTarget = target;
|
Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z());
|
||||||
|
|
||||||
|
const bool sameRequestedTarget =
|
||||||
|
(normalizedTarget - requestedWalkTarget).norm() <= TARGET_REPLAN_THRESHOLD;
|
||||||
|
const bool alreadyMovingToTarget =
|
||||||
|
!pathWaypoints.empty() || (walkTarget - normalizedTarget).norm() <= TARGET_REPLAN_THRESHOLD;
|
||||||
|
const bool stoppedAfterFailedPath =
|
||||||
|
pathWaypoints.empty() && (walkTarget - position).norm() <= TARGET_REPLAN_THRESHOLD;
|
||||||
|
|
||||||
|
if (!onArrived && sameRequestedTarget && (alreadyMovingToTarget || stoppedAfterFailedPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedWalkTarget = normalizedTarget;
|
||||||
onArrivedCallback = std::move(onArrived);
|
onArrivedCallback = std::move(onArrived);
|
||||||
|
|
||||||
|
if (pathPlanner) {
|
||||||
|
pathWaypoints = pathPlanner(position, normalizedTarget);
|
||||||
|
currentWaypointIndex = 0;
|
||||||
|
|
||||||
|
if (!pathWaypoints.empty()) {
|
||||||
|
for (Eigen::Vector3f& waypoint : pathWaypoints) {
|
||||||
|
waypoint.y() = 0.f;
|
||||||
|
}
|
||||||
|
walkTarget = pathWaypoints.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z());
|
||||||
|
onArrivedCallback = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pathWaypoints.clear();
|
||||||
|
currentWaypointIndex = 0;
|
||||||
|
walkTarget = normalizedTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Character::setPathPlanner(PathPlanner planner) {
|
||||||
|
pathPlanner = std::move(planner);
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimationState Character::resolveActiveState() const {
|
AnimationState Character::resolveActiveState() const {
|
||||||
@ -46,7 +84,12 @@ AnimationState Character::resolveActiveState() const {
|
|||||||
void Character::update(int64_t deltaMs) {
|
void Character::update(int64_t deltaMs) {
|
||||||
//std::cout << "update called with deltaMs: " << deltaMs << std::endl;
|
//std::cout << "update called with deltaMs: " << deltaMs << std::endl;
|
||||||
// Move toward walk target on the XZ plane
|
// Move toward walk target on the XZ plane
|
||||||
Eigen::Vector3f toTarget = walkTarget - position;
|
Eigen::Vector3f activeTarget = walkTarget;
|
||||||
|
if (!pathWaypoints.empty() && currentWaypointIndex < pathWaypoints.size()) {
|
||||||
|
activeTarget = pathWaypoints[currentWaypointIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
Eigen::Vector3f toTarget = activeTarget - position;
|
||||||
toTarget.y() = 0.f;
|
toTarget.y() = 0.f;
|
||||||
float dist = toTarget.norm();
|
float dist = toTarget.norm();
|
||||||
|
|
||||||
@ -54,7 +97,7 @@ void Character::update(int64_t deltaMs) {
|
|||||||
Eigen::Vector3f dir = toTarget / dist;
|
Eigen::Vector3f dir = toTarget / dist;
|
||||||
float moveAmount = walkSpeed * static_cast<float>(deltaMs) / 1000.f;
|
float moveAmount = walkSpeed * static_cast<float>(deltaMs) / 1000.f;
|
||||||
if (moveAmount >= dist) {
|
if (moveAmount >= dist) {
|
||||||
position = walkTarget;
|
position = activeTarget;
|
||||||
position.y() = 0.f;
|
position.y() = 0.f;
|
||||||
} else {
|
} else {
|
||||||
position += dir * moveAmount;
|
position += dir * moveAmount;
|
||||||
@ -71,7 +114,17 @@ void Character::update(int64_t deltaMs) {
|
|||||||
{
|
{
|
||||||
currentState = AnimationState::STAND;
|
currentState = AnimationState::STAND;
|
||||||
}
|
}
|
||||||
if (onArrivedCallback) {
|
|
||||||
|
const bool hasNextWaypoint = !pathWaypoints.empty() && currentWaypointIndex + 1 < pathWaypoints.size();
|
||||||
|
if (hasNextWaypoint) {
|
||||||
|
++currentWaypointIndex;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pathWaypoints.clear();
|
||||||
|
currentWaypointIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasNextWaypoint && onArrivedCallback) {
|
||||||
auto cb = std::move(onArrivedCallback);
|
auto cb = std::move(onArrivedCallback);
|
||||||
onArrivedCallback = nullptr;
|
onArrivedCallback = nullptr;
|
||||||
cb();
|
cb();
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace ZL {
|
namespace ZL {
|
||||||
|
|
||||||
@ -22,6 +23,10 @@ enum class AnimationState {
|
|||||||
|
|
||||||
class Character {
|
class Character {
|
||||||
public:
|
public:
|
||||||
|
using PathPlanner = std::function<std::vector<Eigen::Vector3f>(
|
||||||
|
const Eigen::Vector3f& start,
|
||||||
|
const Eigen::Vector3f& end)>;
|
||||||
|
|
||||||
void loadAnimation(AnimationState state, const std::string& filename, const std::string& zipFile = "");
|
void loadAnimation(AnimationState state, const std::string& filename, const std::string& zipFile = "");
|
||||||
void loadBinaryAnimation(AnimationState state, const std::string& filename, const std::string& zipFile = "");
|
void loadBinaryAnimation(AnimationState state, const std::string& filename, const std::string& zipFile = "");
|
||||||
// void setTexture(std::shared_ptr<Texture> texture);
|
// void setTexture(std::shared_ptr<Texture> texture);
|
||||||
@ -33,6 +38,7 @@ public:
|
|||||||
// From Python you can chain further commands (walk, wait, trigger others).
|
// From Python you can chain further commands (walk, wait, trigger others).
|
||||||
void setTarget(const Eigen::Vector3f& target,
|
void setTarget(const Eigen::Vector3f& target,
|
||||||
std::function<void()> onArrived = nullptr);
|
std::function<void()> onArrived = nullptr);
|
||||||
|
void setPathPlanner(PathPlanner planner);
|
||||||
void draw(Renderer& renderer);
|
void draw(Renderer& renderer);
|
||||||
void drawShadowDepth(Renderer& renderer);
|
void drawShadowDepth(Renderer& renderer);
|
||||||
void drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
|
void drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
|
||||||
@ -86,10 +92,15 @@ private:
|
|||||||
std::shared_ptr<Texture> texture;
|
std::shared_ptr<Texture> texture;
|
||||||
|
|
||||||
Eigen::Vector3f walkTarget = Eigen::Vector3f(0.f, 0.f, 0.f);
|
Eigen::Vector3f walkTarget = Eigen::Vector3f(0.f, 0.f, 0.f);
|
||||||
|
Eigen::Vector3f requestedWalkTarget = Eigen::Vector3f(0.f, 0.f, 0.f);
|
||||||
|
std::vector<Eigen::Vector3f> pathWaypoints;
|
||||||
|
size_t currentWaypointIndex = 0;
|
||||||
|
PathPlanner pathPlanner;
|
||||||
float targetFacingAngle = 0.0f;
|
float targetFacingAngle = 0.0f;
|
||||||
std::function<void()> onArrivedCallback;
|
std::function<void()> onArrivedCallback;
|
||||||
|
|
||||||
static constexpr float WALK_THRESHOLD = 0.05f;
|
static constexpr float WALK_THRESHOLD = 0.05f;
|
||||||
|
static constexpr float TARGET_REPLAN_THRESHOLD = 0.25f;
|
||||||
|
|
||||||
// Returns the animation state to actually play/draw, falling back to IDLE
|
// Returns the animation state to actually play/draw, falling back to IDLE
|
||||||
// if the requested state has no loaded animation.
|
// if the requested state has no loaded animation.
|
||||||
|
|||||||
77
src/Game.cpp
77
src/Game.cpp
@ -269,6 +269,8 @@ namespace ZL
|
|||||||
shadowMap->setLightDirection(Eigen::Vector3f(-0.5f, -1.0f, -0.3f));
|
shadowMap->setLightDirection(Eigen::Vector3f(-0.5f, -1.0f, -0.3f));
|
||||||
std::cout << "Shadow map initialized" << std::endl;
|
std::cout << "Shadow map initialized" << std::endl;
|
||||||
|
|
||||||
|
setupNavigation();
|
||||||
|
|
||||||
loadingCompleted = true;
|
loadingCompleted = true;
|
||||||
|
|
||||||
dialogueSystem.init(renderer, CONST_ZIP_FILE);
|
dialogueSystem.init(renderer, CONST_ZIP_FILE);
|
||||||
@ -314,6 +316,73 @@ namespace ZL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Game::setupNavigation()
|
||||||
|
{
|
||||||
|
std::vector<PathFinder::ObstacleMesh> obstacles;
|
||||||
|
obstacles.reserve(gameObjects.size() + interactiveObjects.size());
|
||||||
|
|
||||||
|
for (const auto& item : gameObjects) {
|
||||||
|
const LoadedGameObject& gameObj = item.second;
|
||||||
|
obstacles.push_back({ &gameObj.mesh.data, Eigen::Vector3f::Zero() });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const InteractiveObject& intObj : interactiveObjects) {
|
||||||
|
if (!intObj.isActive) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
obstacles.push_back({ &intObj.mesh.data, intObj.position });
|
||||||
|
}
|
||||||
|
|
||||||
|
navigation.build(obstacles, "resources/config2/navigation.json", CONST_ZIP_FILE);
|
||||||
|
|
||||||
|
#ifdef SHOW_PATH
|
||||||
|
buildDebugNavMeshes();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
auto planner = [this](const Eigen::Vector3f& start, const Eigen::Vector3f& end) {
|
||||||
|
return navigation.findPath(start, end);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (player) {
|
||||||
|
player->setPathPlanner(planner);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& npc : npcs) {
|
||||||
|
if (npc) {
|
||||||
|
npc->setPathPlanner(planner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef SHOW_PATH
|
||||||
|
void Game::buildDebugNavMeshes()
|
||||||
|
{
|
||||||
|
debugNavMeshes.clear();
|
||||||
|
const auto& areas = navigation.getAreas();
|
||||||
|
float y = navigation.getFloorY() + 0.02f;
|
||||||
|
Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
|
||||||
|
|
||||||
|
for (const auto& area : areas) {
|
||||||
|
if (area.polygon.size() < 3) continue;
|
||||||
|
VertexRenderStruct mesh;
|
||||||
|
mesh.data = CreatePolygonFloor(area.polygon, y, red);
|
||||||
|
mesh.RefreshVBO();
|
||||||
|
debugNavMeshes.push_back(std::move(mesh));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::drawDebugNavigation()
|
||||||
|
{
|
||||||
|
renderer.shaderManager.PushShader("defaultColor");
|
||||||
|
renderer.SetMatrix();
|
||||||
|
for (const auto& mesh : debugNavMeshes) {
|
||||||
|
renderer.DrawVertexRenderStruct(mesh);
|
||||||
|
}
|
||||||
|
renderer.shaderManager.PopShader();
|
||||||
|
renderer.SetMatrix();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
InteractiveObject* Game::raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir) {
|
InteractiveObject* Game::raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir) {
|
||||||
if (interactiveObjects.empty()) {
|
if (interactiveObjects.empty()) {
|
||||||
std::cout << "[RAYCAST] No interactive objects to check" << std::endl;
|
std::cout << "[RAYCAST] No interactive objects to check" << std::endl;
|
||||||
@ -464,6 +533,9 @@ namespace ZL
|
|||||||
if (player) player->draw(renderer);
|
if (player) player->draw(renderer);
|
||||||
for (auto& npc : npcs) npc->draw(renderer);
|
for (auto& npc : npcs) npc->draw(renderer);
|
||||||
|
|
||||||
|
#ifdef SHOW_PATH
|
||||||
|
drawDebugNavigation();
|
||||||
|
#endif
|
||||||
|
|
||||||
renderer.PopMatrix();
|
renderer.PopMatrix();
|
||||||
|
|
||||||
@ -1092,5 +1164,10 @@ namespace ZL
|
|||||||
return dialogueSystem.getFlag(flag);
|
return dialogueSystem.getFlag(flag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Game::setNavigationAreaAvailable(const std::string& areaName, bool available)
|
||||||
|
{
|
||||||
|
return navigation.setAreaAvailable(areaName, available);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
} // namespace ZL
|
} // namespace ZL
|
||||||
|
|||||||
13
src/Game.h
13
src/Game.h
@ -23,6 +23,7 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include "dialogue/DialogueSystem.h"
|
#include "dialogue/DialogueSystem.h"
|
||||||
#include "render/ShadowMap.h"
|
#include "render/ShadowMap.h"
|
||||||
|
#include "navigation/PathFinder.h"
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace ZL {
|
namespace ZL {
|
||||||
@ -41,6 +42,7 @@ namespace ZL {
|
|||||||
bool requestDialogueStart(const std::string& dialogueId);
|
bool requestDialogueStart(const std::string& dialogueId);
|
||||||
void setDialogueFlag(const std::string& flag, int value);
|
void setDialogueFlag(const std::string& flag, int value);
|
||||||
int getDialogueFlag(const std::string& flag) const;
|
int getDialogueFlag(const std::string& flag) const;
|
||||||
|
bool setNavigationAreaAvailable(const std::string& areaName, bool available);
|
||||||
|
|
||||||
Renderer renderer;
|
Renderer renderer;
|
||||||
TaskManager taskManager;
|
TaskManager taskManager;
|
||||||
@ -85,6 +87,11 @@ namespace ZL {
|
|||||||
// Public access for ScriptEngine
|
// Public access for ScriptEngine
|
||||||
MenuManager menuManager;
|
MenuManager menuManager;
|
||||||
ScriptEngine scriptEngine;
|
ScriptEngine scriptEngine;
|
||||||
|
PathFinder navigation;
|
||||||
|
|
||||||
|
#ifdef SHOW_PATH
|
||||||
|
std::vector<VertexRenderStruct> debugNavMeshes;
|
||||||
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool rightMouseDown = false;
|
bool rightMouseDown = false;
|
||||||
@ -101,12 +108,18 @@ namespace ZL {
|
|||||||
void drawLoading();
|
void drawLoading();
|
||||||
void drawShadowDepthPass();
|
void drawShadowDepthPass();
|
||||||
void drawGameWithShadows();
|
void drawGameWithShadows();
|
||||||
|
void setupNavigation();
|
||||||
void handleDown(int64_t fingerId, int mx, int my);
|
void handleDown(int64_t fingerId, int mx, int my);
|
||||||
void handleUp(int64_t fingerId, int mx, int my);
|
void handleUp(int64_t fingerId, int mx, int my);
|
||||||
void handleMotion(int64_t fingerId, int mx, int my);
|
void handleMotion(int64_t fingerId, int mx, int my);
|
||||||
InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir);
|
InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir);
|
||||||
Character* raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance = 100.0f);
|
Character* raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance = 100.0f);
|
||||||
|
|
||||||
|
#ifdef SHOW_PATH
|
||||||
|
void buildDebugNavMeshes();
|
||||||
|
void drawDebugNavigation();
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef EMSCRIPTEN
|
#ifdef EMSCRIPTEN
|
||||||
static Game* s_instance;
|
static Game* s_instance;
|
||||||
static void onResourcesZipLoaded(const char* filename);
|
static void onResourcesZipLoaded(const char* filename);
|
||||||
|
|||||||
@ -155,6 +155,14 @@ namespace ZL {
|
|||||||
return game->getDialogueFlag(flag);
|
return game->getDialogueFlag(flag);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
api.set_function("set_navigation_area_available",
|
||||||
|
[game](const std::string& areaName, bool available) {
|
||||||
|
if (!game->setNavigationAreaAvailable(areaName, available)) {
|
||||||
|
std::cerr << "[script] set_navigation_area_available: area not found: "
|
||||||
|
<< areaName << "\n";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// receive_npc_gift(npc_index)
|
// receive_npc_gift(npc_index)
|
||||||
api.set_function("receive_npc_gift", [game](int npcIndex) {
|
api.set_function("receive_npc_gift", [game](int npcIndex) {
|
||||||
std::cout << "[script] receive_npc_gift: npc index " << npcIndex << std::endl;
|
std::cout << "[script] receive_npc_gift: npc index " << npcIndex << std::endl;
|
||||||
|
|||||||
518
src/navigation/PathFinder.cpp
Normal file
518
src/navigation/PathFinder.cpp
Normal file
@ -0,0 +1,518 @@
|
|||||||
|
#include "navigation/PathFinder.h"
|
||||||
|
#include "external/nlohmann/json.hpp"
|
||||||
|
#include "utils/Utils.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <iostream>
|
||||||
|
#include <limits>
|
||||||
|
#include <queue>
|
||||||
|
|
||||||
|
namespace ZL {
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
const float INF_COST = (std::numeric_limits<float>::max)();
|
||||||
|
|
||||||
|
float distanceCells(const PathFinder::Cell& a, const PathFinder::Cell& b)
|
||||||
|
{
|
||||||
|
const float dx = static_cast<float>(a.x - b.x);
|
||||||
|
const float dz = static_cast<float>(a.z - b.z);
|
||||||
|
return std::sqrt(dx * dx + dz * dz);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Eigen::Vector2f> readPolygon(const json& item)
|
||||||
|
{
|
||||||
|
std::vector<Eigen::Vector2f> polygon;
|
||||||
|
if (!item.contains("polygon") || !item["polygon"].is_array()) {
|
||||||
|
return polygon;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& point : item["polygon"]) {
|
||||||
|
if (point.is_array() && point.size() >= 2) {
|
||||||
|
polygon.emplace_back(point[0].get<float>(), point[1].get<float>());
|
||||||
|
} else if (point.is_object()) {
|
||||||
|
polygon.emplace_back(point.value("x", 0.0f), point.value("z", 0.0f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return polygon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PathFinder::build(const std::vector<ObstacleMesh>& obstacleMeshes,
|
||||||
|
const std::string& configPath,
|
||||||
|
const std::string& zipPath)
|
||||||
|
{
|
||||||
|
ready = false;
|
||||||
|
loadedConfigPath = configPath;
|
||||||
|
loadedZipPath = zipPath;
|
||||||
|
obstacles = obstacleMeshes;
|
||||||
|
|
||||||
|
loadConfig(configPath, zipPath);
|
||||||
|
resetGridBounds();
|
||||||
|
|
||||||
|
if (gridWidth <= 0 || gridDepth <= 0) {
|
||||||
|
std::cerr << "[nav] Could not build grid: no navigation polygons\n";
|
||||||
|
walkable.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuildWalkableGrid();
|
||||||
|
ready = true;
|
||||||
|
|
||||||
|
std::cout << "[nav] Polygon grid built: " << gridWidth << "x" << gridDepth
|
||||||
|
<< ", cell=" << cellSize << ", areas=" << areas.size() << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Eigen::Vector3f> PathFinder::findPath(const Eigen::Vector3f& start,
|
||||||
|
const Eigen::Vector3f& end) const
|
||||||
|
{
|
||||||
|
if (!ready || walkable.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Cell startCell;
|
||||||
|
Cell endCell;
|
||||||
|
if (!findNearestWalkableCell(start, startCell) ||
|
||||||
|
!findNearestWalkableCell(end, endCell)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startCell.x == endCell.x && startCell.z == endCell.z) {
|
||||||
|
return { cellCenter(endCell) };
|
||||||
|
}
|
||||||
|
|
||||||
|
struct QueueNode {
|
||||||
|
int index = 0;
|
||||||
|
float priority = 0.0f;
|
||||||
|
|
||||||
|
bool operator<(const QueueNode& other) const
|
||||||
|
{
|
||||||
|
return priority > other.priority;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const int cellCount = gridWidth * gridDepth;
|
||||||
|
std::vector<float> cost(static_cast<size_t>(cellCount), INF_COST);
|
||||||
|
std::vector<int> cameFrom(static_cast<size_t>(cellCount), -1);
|
||||||
|
std::priority_queue<QueueNode> open;
|
||||||
|
|
||||||
|
const int startIndex = indexOf(startCell);
|
||||||
|
const int endIndex = indexOf(endCell);
|
||||||
|
|
||||||
|
cost[static_cast<size_t>(startIndex)] = 0.0f;
|
||||||
|
open.push({ startIndex, 0.0f });
|
||||||
|
|
||||||
|
static const int offsets[8][2] = {
|
||||||
|
{ 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 },
|
||||||
|
{ 1, 1 }, { 1, -1 }, { -1, 1 }, { -1, -1 }
|
||||||
|
};
|
||||||
|
|
||||||
|
while (!open.empty()) {
|
||||||
|
const QueueNode current = open.top();
|
||||||
|
open.pop();
|
||||||
|
|
||||||
|
if (current.index == endIndex) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Cell currentCell{ current.index % gridWidth, current.index / gridWidth };
|
||||||
|
|
||||||
|
for (const auto& offset : offsets) {
|
||||||
|
Cell next{ currentCell.x + offset[0], currentCell.z + offset[1] };
|
||||||
|
if (!isCellWalkable(next)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool diagonal = offset[0] != 0 && offset[1] != 0;
|
||||||
|
if (diagonal) {
|
||||||
|
Cell horizontal{ currentCell.x + offset[0], currentCell.z };
|
||||||
|
Cell vertical{ currentCell.x, currentCell.z + offset[1] };
|
||||||
|
if (!isCellWalkable(horizontal) || !isCellWalkable(vertical)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const int nextIndex = indexOf(next);
|
||||||
|
const float stepCost = diagonal ? 1.41421356f : 1.0f;
|
||||||
|
const float newCost = cost[static_cast<size_t>(current.index)] + stepCost;
|
||||||
|
if (newCost >= cost[static_cast<size_t>(nextIndex)]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cost[static_cast<size_t>(nextIndex)] = newCost;
|
||||||
|
cameFrom[static_cast<size_t>(nextIndex)] = current.index;
|
||||||
|
const float priority = newCost + distanceCells(next, endCell);
|
||||||
|
open.push({ nextIndex, priority });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cameFrom[static_cast<size_t>(endIndex)] == -1) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Cell> cells;
|
||||||
|
for (int current = endIndex; current != -1; current = cameFrom[static_cast<size_t>(current)]) {
|
||||||
|
cells.push_back({ current % gridWidth, current / gridWidth });
|
||||||
|
if (current == startIndex) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::reverse(cells.begin(), cells.end());
|
||||||
|
cells = smoothCells(cells);
|
||||||
|
|
||||||
|
std::vector<Eigen::Vector3f> path;
|
||||||
|
path.reserve(cells.size());
|
||||||
|
for (const Cell& cell : cells) {
|
||||||
|
path.push_back(cellCenter(cell));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.empty() && (path.front() - Eigen::Vector3f(start.x(), floorY, start.z())).norm() < cellSize * 0.75f) {
|
||||||
|
path.erase(path.begin());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.empty()) {
|
||||||
|
Cell requestedEndCell;
|
||||||
|
if (worldToCell(end, requestedEndCell) &&
|
||||||
|
requestedEndCell.x == endCell.x &&
|
||||||
|
requestedEndCell.z == endCell.z) {
|
||||||
|
path.back() = Eigen::Vector3f(end.x(), floorY, end.z());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PathFinder::setAreaAvailable(const std::string& areaName, bool available)
|
||||||
|
{
|
||||||
|
bool found = false;
|
||||||
|
bool changed = false;
|
||||||
|
for (NavigationArea& area : areas) {
|
||||||
|
if (area.name == areaName) {
|
||||||
|
found = true;
|
||||||
|
changed = changed || area.available != available;
|
||||||
|
area.available = available;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
rebuildWalkableGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PathFinder::isWalkable(const Eigen::Vector3f& point) const
|
||||||
|
{
|
||||||
|
Cell cell;
|
||||||
|
return worldToCell(point, cell) && isCellWalkable(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PathFinder::loadConfig(const std::string& configPath, const std::string& zipPath)
|
||||||
|
{
|
||||||
|
cellSize = 0.4f;
|
||||||
|
agentRadius = 0.45f;
|
||||||
|
floorY = 0.0f;
|
||||||
|
objectPadding = 0.25f;
|
||||||
|
areas.clear();
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::string content;
|
||||||
|
if (!zipPath.empty()) {
|
||||||
|
const std::vector<char> bytes = readFileFromZIP(configPath, zipPath);
|
||||||
|
content.assign(bytes.begin(), bytes.end());
|
||||||
|
} else {
|
||||||
|
content = readTextFile(configPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.empty()) {
|
||||||
|
std::cerr << "[nav] Navigation config is empty or missing: " << configPath << "\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json root = json::parse(content);
|
||||||
|
cellSize = root.value("cellSize", cellSize);
|
||||||
|
agentRadius = root.value("agentRadius", agentRadius);
|
||||||
|
floorY = root.value("floorY", floorY);
|
||||||
|
objectPadding = root.value("objectPadding", objectPadding);
|
||||||
|
|
||||||
|
if (!root.contains("areas") || !root["areas"].is_array()) {
|
||||||
|
std::cerr << "[nav] Navigation config has no 'areas' array: " << configPath << "\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& item : root["areas"]) {
|
||||||
|
NavigationArea area;
|
||||||
|
area.name = item.value("name", "");
|
||||||
|
area.available = item.value("available", true);
|
||||||
|
area.polygon = readPolygon(item);
|
||||||
|
|
||||||
|
if (area.name.empty()) {
|
||||||
|
std::cerr << "[nav] Skipping navigation area without name\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (area.polygon.size() < 3) {
|
||||||
|
std::cerr << "[nav] Skipping navigation area '" << area.name << "': polygon has fewer than 3 points\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
areas.push_back(area);
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[nav] Failed to load config '" << configPath << "': " << e.what() << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
cellSize = (std::max)(cellSize, 0.1f);
|
||||||
|
agentRadius = (std::max)(agentRadius, 0.0f);
|
||||||
|
objectPadding = (std::max)(objectPadding, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PathFinder::resetGridBounds()
|
||||||
|
{
|
||||||
|
if (areas.empty()) {
|
||||||
|
gridWidth = 0;
|
||||||
|
gridDepth = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
minX = areas.front().polygon.front().x();
|
||||||
|
float maxX = minX;
|
||||||
|
minZ = areas.front().polygon.front().y();
|
||||||
|
float maxZ = minZ;
|
||||||
|
|
||||||
|
auto includePoint = [&](float x, float z) {
|
||||||
|
minX = (std::min)(minX, x);
|
||||||
|
maxX = (std::max)(maxX, x);
|
||||||
|
minZ = (std::min)(minZ, z);
|
||||||
|
maxZ = (std::max)(maxZ, z);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const NavigationArea& area : areas) {
|
||||||
|
for (const Eigen::Vector2f& point : area.polygon) {
|
||||||
|
includePoint(point.x(), point.y());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ObstacleMesh& obstacle : obstacles) {
|
||||||
|
if (!obstacle.mesh) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const Eigen::Vector3f& point : obstacle.mesh->PositionData) {
|
||||||
|
const Eigen::Vector3f worldPoint = point + obstacle.offset;
|
||||||
|
includePoint(worldPoint.x(), worldPoint.z());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const float padding = cellSize * 2.0f + agentRadius + objectPadding;
|
||||||
|
minX -= padding;
|
||||||
|
minZ -= padding;
|
||||||
|
maxX += padding;
|
||||||
|
maxZ += padding;
|
||||||
|
|
||||||
|
gridWidth = static_cast<int>(std::ceil((maxX - minX) / cellSize));
|
||||||
|
gridDepth = static_cast<int>(std::ceil((maxZ - minZ) / cellSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PathFinder::rebuildWalkableGrid()
|
||||||
|
{
|
||||||
|
walkable.assign(static_cast<size_t>(gridWidth * gridDepth), 0);
|
||||||
|
markAvailableAreasWalkable();
|
||||||
|
markObstacleMeshesBlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PathFinder::markAvailableAreasWalkable()
|
||||||
|
{
|
||||||
|
for (int z = 0; z < gridDepth; ++z) {
|
||||||
|
for (int x = 0; x < gridWidth; ++x) {
|
||||||
|
const Cell cell{ x, z };
|
||||||
|
const Eigen::Vector3f center = cellCenter(cell);
|
||||||
|
|
||||||
|
for (const NavigationArea& area : areas) {
|
||||||
|
if (!area.available) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (pointInPolygon(center.x(), center.z(), area.polygon)) {
|
||||||
|
walkable[static_cast<size_t>(indexOf(cell))] = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PathFinder::markObstacleMeshesBlocked()
|
||||||
|
{
|
||||||
|
const float padding = agentRadius + objectPadding;
|
||||||
|
|
||||||
|
for (const ObstacleMesh& obstacle : obstacles) {
|
||||||
|
if (!obstacle.mesh || obstacle.mesh->PositionData.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Eigen::Vector3f minPoint = obstacle.mesh->PositionData[0] + obstacle.offset;
|
||||||
|
Eigen::Vector3f maxPoint = minPoint;
|
||||||
|
for (const Eigen::Vector3f& localPoint : obstacle.mesh->PositionData) {
|
||||||
|
const Eigen::Vector3f point = localPoint + obstacle.offset;
|
||||||
|
minPoint = minPoint.cwiseMin(point);
|
||||||
|
maxPoint = maxPoint.cwiseMax(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cell from;
|
||||||
|
Cell to;
|
||||||
|
worldToCell({ minPoint.x() - padding, floorY, minPoint.z() - padding }, from);
|
||||||
|
worldToCell({ maxPoint.x() + padding, floorY, maxPoint.z() + padding }, to);
|
||||||
|
from.x = (std::max)(from.x, 0);
|
||||||
|
from.z = (std::max)(from.z, 0);
|
||||||
|
to.x = (std::min)(to.x, gridWidth - 1);
|
||||||
|
to.z = (std::min)(to.z, gridDepth - 1);
|
||||||
|
|
||||||
|
for (int z = from.z; z <= to.z; ++z) {
|
||||||
|
for (int x = from.x; x <= to.x; ++x) {
|
||||||
|
const Cell cell{ x, z };
|
||||||
|
if (isInsideGrid(cell)) {
|
||||||
|
walkable[static_cast<size_t>(indexOf(cell))] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PathFinder::worldToCell(const Eigen::Vector3f& point, Cell& out) const
|
||||||
|
{
|
||||||
|
if (gridWidth <= 0 || gridDepth <= 0) {
|
||||||
|
out = {};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.x = static_cast<int>(std::floor((point.x() - minX) / cellSize));
|
||||||
|
out.z = static_cast<int>(std::floor((point.z() - minZ) / cellSize));
|
||||||
|
return isInsideGrid(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
Eigen::Vector3f PathFinder::cellCenter(const Cell& cell) const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
minX + (static_cast<float>(cell.x) + 0.5f) * cellSize,
|
||||||
|
floorY,
|
||||||
|
minZ + (static_cast<float>(cell.z) + 0.5f) * cellSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PathFinder::isInsideGrid(const Cell& cell) const
|
||||||
|
{
|
||||||
|
return cell.x >= 0 && cell.z >= 0 && cell.x < gridWidth && cell.z < gridDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PathFinder::isCellWalkable(const Cell& cell) const
|
||||||
|
{
|
||||||
|
return isInsideGrid(cell) && walkable[static_cast<size_t>(indexOf(cell))] != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int PathFinder::indexOf(const Cell& cell) const
|
||||||
|
{
|
||||||
|
return cell.z * gridWidth + cell.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out) const
|
||||||
|
{
|
||||||
|
Cell origin;
|
||||||
|
if (!worldToCell(point, origin)) {
|
||||||
|
const int x = static_cast<int>(std::floor((point.x() - minX) / cellSize));
|
||||||
|
const int z = static_cast<int>(std::floor((point.z() - minZ) / cellSize));
|
||||||
|
origin.x = (std::min)((std::max)(x, 0), gridWidth - 1);
|
||||||
|
origin.z = (std::min)((std::max)(z, 0), gridDepth - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCellWalkable(origin)) {
|
||||||
|
out = origin;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int maxRadius = (std::max)(4, static_cast<int>(std::ceil(8.0f / cellSize)));
|
||||||
|
for (int radius = 1; radius <= maxRadius; ++radius) {
|
||||||
|
for (int dz = -radius; dz <= radius; ++dz) {
|
||||||
|
for (int dx = -radius; dx <= radius; ++dx) {
|
||||||
|
if (std::abs(dx) != radius && std::abs(dz) != radius) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cell candidate{ origin.x + dx, origin.z + dz };
|
||||||
|
if (isCellWalkable(candidate)) {
|
||||||
|
out = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PathFinder::hasLineOfSight(const Cell& from, const Cell& to) const
|
||||||
|
{
|
||||||
|
const Eigen::Vector3f a = cellCenter(from);
|
||||||
|
const Eigen::Vector3f b = cellCenter(to);
|
||||||
|
const float distance = (b - a).norm();
|
||||||
|
const int steps = (std::max)(1, static_cast<int>(std::ceil(distance / (cellSize * 0.5f))));
|
||||||
|
|
||||||
|
for (int i = 0; i <= steps; ++i) {
|
||||||
|
const float t = static_cast<float>(i) / static_cast<float>(steps);
|
||||||
|
const Eigen::Vector3f p = a * (1.0f - t) + b * t;
|
||||||
|
Cell cell;
|
||||||
|
if (!worldToCell(p, cell) || !isCellWalkable(cell)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<PathFinder::Cell> PathFinder::smoothCells(const std::vector<Cell>& cells) const
|
||||||
|
{
|
||||||
|
if (cells.size() <= 2) {
|
||||||
|
return cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Cell> result;
|
||||||
|
size_t anchor = 0;
|
||||||
|
result.push_back(cells[anchor]);
|
||||||
|
|
||||||
|
while (anchor < cells.size() - 1) {
|
||||||
|
size_t next = cells.size() - 1;
|
||||||
|
while (next > anchor + 1 && !hasLineOfSight(cells[anchor], cells[next])) {
|
||||||
|
--next;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push_back(cells[next]);
|
||||||
|
anchor = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PathFinder::pointInPolygon(float x, float z, const std::vector<Eigen::Vector2f>& polygon)
|
||||||
|
{
|
||||||
|
bool inside = false;
|
||||||
|
size_t previous = polygon.size() - 1;
|
||||||
|
|
||||||
|
for (size_t current = 0; current < polygon.size(); ++current) {
|
||||||
|
const Eigen::Vector2f& a = polygon[current];
|
||||||
|
const Eigen::Vector2f& b = polygon[previous];
|
||||||
|
|
||||||
|
const bool crossesZ = (a.y() > z) != (b.y() > z);
|
||||||
|
if (crossesZ) {
|
||||||
|
const float t = (z - a.y()) / (b.y() - a.y());
|
||||||
|
const float intersectionX = a.x() + t * (b.x() - a.x());
|
||||||
|
if (x < intersectionX) {
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previous = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ZL
|
||||||
79
src/navigation/PathFinder.h
Normal file
79
src/navigation/PathFinder.h
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "render/Renderer.h"
|
||||||
|
#include <Eigen/Core>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ZL {
|
||||||
|
|
||||||
|
class PathFinder {
|
||||||
|
public:
|
||||||
|
struct ObstacleMesh {
|
||||||
|
const VertexDataStruct* mesh = nullptr;
|
||||||
|
Eigen::Vector3f offset = Eigen::Vector3f::Zero();
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Cell {
|
||||||
|
int x = 0;
|
||||||
|
int z = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NavigationArea {
|
||||||
|
std::string name;
|
||||||
|
bool available = true;
|
||||||
|
std::vector<Eigen::Vector2f> polygon;
|
||||||
|
};
|
||||||
|
|
||||||
|
void build(const std::vector<ObstacleMesh>& obstacleMeshes,
|
||||||
|
const std::string& configPath,
|
||||||
|
const std::string& zipPath = "");
|
||||||
|
|
||||||
|
std::vector<Eigen::Vector3f> findPath(const Eigen::Vector3f& start,
|
||||||
|
const Eigen::Vector3f& end) const;
|
||||||
|
|
||||||
|
bool setAreaAvailable(const std::string& areaName, bool available);
|
||||||
|
bool isReady() const { return ready; }
|
||||||
|
bool isWalkable(const Eigen::Vector3f& point) const;
|
||||||
|
const std::vector<NavigationArea>& getAreas() const { return areas; }
|
||||||
|
float getFloorY() const { return floorY; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
float cellSize = 0.4f;
|
||||||
|
float agentRadius = 0.45f;
|
||||||
|
float floorY = 0.0f;
|
||||||
|
float objectPadding = 0.25f;
|
||||||
|
|
||||||
|
float minX = 0.0f;
|
||||||
|
float minZ = 0.0f;
|
||||||
|
int gridWidth = 0;
|
||||||
|
int gridDepth = 0;
|
||||||
|
bool ready = false;
|
||||||
|
|
||||||
|
std::string loadedConfigPath;
|
||||||
|
std::string loadedZipPath;
|
||||||
|
std::vector<ObstacleMesh> obstacles;
|
||||||
|
std::vector<unsigned char> walkable;
|
||||||
|
std::vector<NavigationArea> areas;
|
||||||
|
|
||||||
|
void loadConfig(const std::string& configPath, const std::string& zipPath);
|
||||||
|
void resetGridBounds();
|
||||||
|
void rebuildWalkableGrid();
|
||||||
|
void markAvailableAreasWalkable();
|
||||||
|
void markObstacleMeshesBlocked();
|
||||||
|
|
||||||
|
bool worldToCell(const Eigen::Vector3f& point, Cell& out) const;
|
||||||
|
Eigen::Vector3f cellCenter(const Cell& cell) const;
|
||||||
|
bool isInsideGrid(const Cell& cell) const;
|
||||||
|
bool isCellWalkable(const Cell& cell) const;
|
||||||
|
int indexOf(const Cell& cell) const;
|
||||||
|
|
||||||
|
bool findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out) const;
|
||||||
|
bool hasLineOfSight(const Cell& from, const Cell& to) const;
|
||||||
|
std::vector<Cell> smoothCells(const std::vector<Cell>& cells) const;
|
||||||
|
|
||||||
|
static bool pointInPolygon(float x, float z, const std::vector<Eigen::Vector2f>& polygon);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZL
|
||||||
@ -179,6 +179,24 @@ namespace ZL {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VertexDataStruct CreatePolygonFloor(const std::vector<Eigen::Vector2f>& polygon, float yLevel, const Vector3f& color)
|
||||||
|
{
|
||||||
|
VertexDataStruct result;
|
||||||
|
if (polygon.size() < 3) return result;
|
||||||
|
|
||||||
|
for (size_t i = 1; i + 1 < polygon.size(); ++i) {
|
||||||
|
result.PositionData.push_back({ polygon[0].x(), yLevel, polygon[0].y() });
|
||||||
|
result.PositionData.push_back({ polygon[i].x(), yLevel, polygon[i].y() });
|
||||||
|
result.PositionData.push_back({ polygon[i + 1].x(), yLevel, polygon[i + 1].y() });
|
||||||
|
|
||||||
|
result.ColorData.push_back(color);
|
||||||
|
result.ColorData.push_back(color);
|
||||||
|
result.ColorData.push_back(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
VertexDataStruct CreateRectHorizontalSections2D(Vector2f center, Vector2f halfWidthHeight, float zLevel, size_t sectionCount)
|
VertexDataStruct CreateRectHorizontalSections2D(Vector2f center, Vector2f halfWidthHeight, float zLevel, size_t sectionCount)
|
||||||
{
|
{
|
||||||
Vector2f posFrom = center - halfWidthHeight;
|
Vector2f posFrom = center - halfWidthHeight;
|
||||||
|
|||||||
@ -87,6 +87,7 @@ namespace ZL {
|
|||||||
};
|
};
|
||||||
|
|
||||||
VertexDataStruct CreateRect2D(Vector2f center, Vector2f halfWidthHeight, float zLevel);
|
VertexDataStruct CreateRect2D(Vector2f center, Vector2f halfWidthHeight, float zLevel);
|
||||||
|
VertexDataStruct CreatePolygonFloor(const std::vector<Eigen::Vector2f>& polygon, float yLevel, const Vector3f& color);
|
||||||
VertexDataStruct CreateRectHorizontalSections2D(Vector2f center, Vector2f halfWidthHeight, float zLevel, size_t sectionCount);
|
VertexDataStruct CreateRectHorizontalSections2D(Vector2f center, Vector2f halfWidthHeight, float zLevel, size_t sectionCount);
|
||||||
VertexDataStruct CreateCube3D(float scale);
|
VertexDataStruct CreateCube3D(float scale);
|
||||||
VertexDataStruct CreateCubemap(float scale = 1000.f);
|
VertexDataStruct CreateCubemap(float scale = 1000.f);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user