Compare commits

..
3 Commits
28 changed files with 1654 additions and 667 deletions

No files matched your search

+7
View File
@@ -131,8 +131,14 @@ set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT S
# include-пути проекта # include-пути проекта
target_include_directories(ShadowOverBishkekDemo PRIVATE target_include_directories(ShadowOverBishkekDemo PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/../src" "${CMAKE_CURRENT_SOURCE_DIR}/../src"
"${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/Clipper2-Clipper2_2.0.1/CPP/build/Release/include"
) )
#target_link_directories(ShadowOverBishkekDemo PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/Clipper2-Clipper2_2.0.1/CPP/build/Release/lib")
target_link_directories(ShadowOverBishkekDemo PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/Clipper2-Clipper2_2.0.1/CPP/build_debug/Debug/lib")
set_target_properties(ShadowOverBishkekDemo PROPERTIES set_target_properties(ShadowOverBishkekDemo PROPERTIES
OUTPUT_NAME "ShadowOverBishkekDemo" OUTPUT_NAME "ShadowOverBishkekDemo"
) )
@@ -162,6 +168,7 @@ target_link_libraries(ShadowOverBishkekDemo PRIVATE
sol2_external_lib sol2_external_lib
SDL2_mixer_external_lib SDL2_mixer_external_lib
openssl_ssl_external_lib openssl_ssl_external_lib
Clipper2.lib
) )
# Линкуем OpenGL (Windows) # Линкуем OpenGL (Windows)
+7 -1
View File
@@ -28,6 +28,12 @@ namespace FRG {
return Eigen::Vector3f(j[0].get<float>(), j[1].get<float>(), j[2].get<float>()); return Eigen::Vector3f(j[0].get<float>(), j[1].get<float>(), j[2].get<float>());
} }
static Vec3fp readVec3fp(const json& j) {
if (!j.is_array() || j.size() != 3) return Vec3fp();
return Vec3fp(static_cast<fp>(j[0].get<double>()), static_cast<fp>(j[1].get<double>()), static_cast<fp>(j[2].get<double>()));
}
bool LocationConfig::fromFile(const std::string& path, const std::string& zipFile, LocationConfig& out) { bool LocationConfig::fromFile(const std::string& path, const std::string& zipFile, LocationConfig& out) {
json j; json j;
if (!parseJsonFile(path, zipFile, j)) return false; if (!parseJsonFile(path, zipFile, j)) return false;
@@ -43,7 +49,7 @@ namespace FRG {
out.teleportsJsonPath = j.value("teleports", ""); out.teleportsJsonPath = j.value("teleports", "");
out.triggerZonesJsonPath = j.value("triggerZones", ""); out.triggerZonesJsonPath = j.value("triggerZones", "");
out.lightsJsonPath = j.value("lights", ""); out.lightsJsonPath = j.value("lights", "");
if (j.contains("playerPosition")) out.playerPosition = readVec3(j["playerPosition"]); if (j.contains("playerPosition")) out.playerPosition = readVec3fp(j["playerPosition"]);
out.lockInteractiveObjectsDuringTutorial = j.value("lockInteractiveObjectsDuringTutorial", false); out.lockInteractiveObjectsDuringTutorial = j.value("lockInteractiveObjectsDuringTutorial", false);
out.loadingScreenPath = j.value("loadingScreen", ""); out.loadingScreenPath = j.value("loadingScreen", "");
+2 -1
View File
@@ -5,6 +5,7 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
#include "navigation/PathFinder.h"
namespace FRG { namespace FRG {
@@ -21,7 +22,7 @@ namespace FRG {
std::string teleportsJsonPath; std::string teleportsJsonPath;
std::string triggerZonesJsonPath; std::string triggerZonesJsonPath;
std::string lightsJsonPath; std::string lightsJsonPath;
Eigen::Vector3f playerPosition = Eigen::Vector3f::Zero(); Vec3fp playerPosition;
// Where the taxi drives in when the player calls it. No taxi when absent. // Where the taxi drives in when the player calls it. No taxi when absent.
struct Taxi { struct Taxi {
+33 -43
View File
@@ -42,19 +42,9 @@ bool Character::isMoving() const {
return state && state->isMoving(); return state && state->isMoving();
} }
Eigen::Vector3f Character::getRequestedWalkTarget() const {
auto state = stateSnapshot();
return state ? state->getRequestedWalkTarget() : Eigen::Vector3f::Zero();
}
Eigen::Vector3f Character::getCurrentNavigationTarget() const {
auto state = stateSnapshot();
return state ? state->getCurrentNavigationTarget() : Eigen::Vector3f::Zero();
}
Eigen::Vector3f Character::renderPosition() const { Eigen::Vector3f Character::renderPosition() const {
auto state = stateSnapshot(); auto state = stateSnapshot();
return state ? interpolatedState(*state).position : Eigen::Vector3f::Zero(); return state ? interpolatedState(*state).position.toEigen() : Eigen::Vector3f::Zero();
} }
float Character::getHp() const { float Character::getHp() const {
@@ -181,8 +171,8 @@ CharacterState Character::interpolatedState(const CharacterState& state) const {
const float alpha = (std::max)(0.0f, (std::min)(1.0f, elapsedMs / kSimulationTickMs)); const float alpha = (std::max)(0.0f, (std::min)(1.0f, elapsedMs / kSimulationTickMs));
CharacterState result = state; CharacterState result = state;
result.position = previousPresentationSample_.position + result.position = Vec3fp::fromEigen(previousPresentationSample_.position +
alpha * (currentPresentationSample_.position - previousPresentationSample_.position); alpha * (currentPresentationSample_.position - previousPresentationSample_.position));
constexpr float kPi = 3.14159265358979323846f; constexpr float kPi = 3.14159265358979323846f;
constexpr float kTwoPi = 2.0f * kPi; constexpr float kTwoPi = 2.0f * kPi;
@@ -190,7 +180,7 @@ CharacterState Character::interpolatedState(const CharacterState& state) const {
previousPresentationSample_.facingAngle; previousPresentationSample_.facingAngle;
while (angleDelta > kPi) angleDelta -= kTwoPi; while (angleDelta > kPi) angleDelta -= kTwoPi;
while (angleDelta < -kPi) angleDelta += kTwoPi; while (angleDelta < -kPi) angleDelta += kTwoPi;
result.facingAngle = previousPresentationSample_.facingAngle + alpha * angleDelta; result.facingAngle = static_cast<fp>(previousPresentationSample_.facingAngle + alpha * angleDelta);
if (previousPresentationSample_.currentState == state.currentState && if (previousPresentationSample_.currentState == state.currentState &&
currentPresentationSample_.currentState == state.currentState) { currentPresentationSample_.currentState == state.currentState) {
float previousFrame = previousPresentationSample_.animationFrame; float previousFrame = previousPresentationSample_.animationFrame;
@@ -235,8 +225,8 @@ void Character::update(int64_t deltaMs, const CharacterUpdateEvents* events) {
const auto now = std::chrono::steady_clock::now(); const auto now = std::chrono::steady_clock::now();
PresentationSample sample; PresentationSample sample;
sample.position = state.position; sample.position = state.position.toEigen();
sample.facingAngle = state.facingAngle; sample.facingAngle = static_cast<float>(state.facingAngle);
sample.animationFrame = state.animationFrame; sample.animationFrame = state.animationFrame;
sample.currentState = state.currentState; sample.currentState = state.currentState;
sample.enabled = state.enabled; sample.enabled = state.enabled;
@@ -299,8 +289,8 @@ void Character::draw(Renderer& renderer) {
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() }); renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
@@ -353,8 +343,8 @@ void Character::drawGpuSkinning(Renderer& renderer, const CharacterState& state)
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() }); renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
@@ -417,8 +407,8 @@ void Character::drawAttachedWeapon(Renderer& renderer, const CharacterState& sta
Eigen::Matrix4f currentBone = anim.gpuSkinningShaderData.skinningMatrices[boneIdx] * bindBone; Eigen::Matrix4f currentBone = anim.gpuSkinningShaderData.skinningMatrices[boneIdx] * bindBone;
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() }); renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
renderer.TranslateMatrix(Eigen::Vector3f(currentBone.block<3, 1>(0, 3))); renderer.TranslateMatrix(Eigen::Vector3f(currentBone.block<3, 1>(0, 3)));
@@ -452,8 +442,8 @@ void Character::drawShadowDepthCpu(Renderer& renderer, const CharacterState& sta
if (it == animations.end()) return; if (it == animations.end()) return;
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() }); renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
@@ -488,8 +478,8 @@ void Character::drawShadowDepthGpuSkinning(Renderer& renderer, const CharacterSt
renderer.shaderManager.PushShader(shadowSkinningShader); renderer.shaderManager.PushShader(shadowSkinningShader);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() }); renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
@@ -561,8 +551,8 @@ void Character::drawCpuWithShadow(Renderer& renderer, const CharacterState& stat
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() }); renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
@@ -623,8 +613,8 @@ void Character::drawGpuSkinningWithShadow(Renderer& renderer, const CharacterSta
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() }); renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
@@ -726,8 +716,8 @@ void Character::drawNightGpuSkinning(Renderer& renderer,
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() }); renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
@@ -798,8 +788,8 @@ void Character::drawNightCpu(Renderer& renderer,
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() }); renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
@@ -900,8 +890,8 @@ void Character::drawNightGpuSkinningWithShadow(Renderer& renderer,
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() }); renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
@@ -985,8 +975,8 @@ void Character::drawNightCpuWithShadow(Renderer& renderer,
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() }); renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(state.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
@@ -1027,7 +1017,7 @@ void Character::setupHitSparks(std::shared_ptr<Texture> sparkTexture) {
} }
void Character::presentDamage(float hp, float maxHp, void Character::presentDamage(float hp, float maxHp,
const Eigen::Vector3f& attackDirection, const Vec3fp& attackDirection,
const std::string& attackerId) { const std::string& attackerId) {
if (onHpChanged) if (onHpChanged)
onHpChanged(hp, maxHp, attackerId); onHpChanged(hp, maxHp, attackerId);
@@ -1037,9 +1027,9 @@ void Character::presentDamage(float hp, float maxHp,
if (!stateSnapshot) return; if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot; const CharacterState& state = *stateSnapshot;
Eigen::Vector3f emitPos = state.position + Eigen::Vector3f(0.f, 1.f, 0.f); Eigen::Vector3f emitPos = state.position.toEigen() + Eigen::Vector3f(0.f, 1.f, 0.f);
hitSparkEmitter.resetEmissionPoints({ emitPos }); hitSparkEmitter.resetEmissionPoints({ emitPos });
hitSparkEmitter.setEmissionDirection(attackDirection); hitSparkEmitter.setEmissionDirection(attackDirection.toEigen());
static std::mt19937 gen(std::random_device{}()); static std::mt19937 gen(std::random_device{}());
std::uniform_int_distribution<int> countDist(10, 15); std::uniform_int_distribution<int> countDist(10, 15);
@@ -1065,7 +1055,7 @@ static void drawCharacterNameLabel(
if (!state.enabled || state.hp <= 0.f || displayName.empty()) return; if (!state.enabled || state.hp <= 0.f || displayName.empty()) return;
Eigen::Vector4f worldPos( Eigen::Vector4f worldPos(
state.position.x(), state.position.y() + 1.83f, state.position.z(), 1.f); static_cast<float>(state.position.x), static_cast<float>(state.position.y) + 1.83f, static_cast<float>(state.position.z), 1.f);
Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos; Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos;
if (clip.w() <= 0.f) return; if (clip.w() <= 0.f) return;
@@ -1126,7 +1116,7 @@ void Character::drawHealthBar(Renderer& renderer,
if (state.hp <= 0.f) return; if (state.hp <= 0.f) return;
if (state.initialHp <= 0.f) return; if (state.initialHp <= 0.f) return;
Eigen::Vector4f worldPos(state.position.x(), state.position.y() + 1.75f, state.position.z(), 1.f); Eigen::Vector4f worldPos(static_cast<float>(state.position.x), static_cast<float>(state.position.y) + 1.75f, static_cast<float>(state.position.z), 1.f);
Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos; Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos;
if (clip.w() <= 0.f) return; if (clip.w() <= 0.f) return;
float ndcX = clip.x() / clip.w(); float ndcX = clip.x() / clip.w();
+1 -3
View File
@@ -55,7 +55,7 @@ public:
void update(int64_t deltaMs, const CharacterUpdateEvents* events = nullptr); void update(int64_t deltaMs, const CharacterUpdateEvents* events = nullptr);
// ---- Damage ---- // ---- Damage ----
void presentDamage(float hp, float maxHp, const Eigen::Vector3f& attackDirection, void presentDamage(float hp, float maxHp, const Vec3fp& attackDirection,
const std::string& attackerId); const std::string& attackerId);
// ---- Spark setup ---- // ---- Spark setup ----
@@ -93,8 +93,6 @@ public:
// ---- Query helpers (read immutable snapshots) ---- // ---- Query helpers (read immutable snapshots) ----
bool isMoving() const; bool isMoving() const;
Eigen::Vector3f getRequestedWalkTarget() const;
Eigen::Vector3f getCurrentNavigationTarget() const;
Eigen::Vector3f renderPosition() const; Eigen::Vector3f renderPosition() const;
float getHp() const; float getHp() const;
+207 -176
View File
@@ -9,7 +9,13 @@ namespace {
// Tick counts are authoritative and therefore independent of render rate. // Tick counts are authoritative and therefore independent of render rate.
constexpr float ATTACK_COOLDOWN_TIME = 1.6f; constexpr float ATTACK_COOLDOWN_TIME = 1.6f;
constexpr float HOME_DRIFT_CHECK_INTERVAL = 5.0f; constexpr float HOME_DRIFT_CHECK_INTERVAL = 5.0f;
fp HOME_DRIFT_CHECK_INTERVAL_FP = static_cast<fp>(HOME_DRIFT_CHECK_INTERVAL);
constexpr float TARGET_REPLAN_THRESHOLD = 0.25f; constexpr float TARGET_REPLAN_THRESHOLD = 0.25f;
fp TARGET_REPLAN_THRESHOLD_SQUARED = static_cast<fp>(TARGET_REPLAN_THRESHOLD * TARGET_REPLAN_THRESHOLD);
constexpr float PI = 3.14159265358979323846f; constexpr float PI = 3.14159265358979323846f;
constexpr int BLOCKED_REPLAN_TICKS = 3; constexpr int BLOCKED_REPLAN_TICKS = 3;
@@ -25,8 +31,8 @@ std::uint32_t stableIdHash(const std::string& value)
} }
void CharacterState::configureAsPlayer() { void CharacterState::configureAsPlayer() {
walkSpeed = 3.0f; walkSpeed = static_cast<fp>(3.0f);
rotationSpeed = 8.0f; rotationSpeed = static_cast < fp>(8.0f);
modelScale = 1.f; modelScale = 1.f;
modelCorrectionRotation = Eigen::Quaternionf( modelCorrectionRotation = Eigen::Quaternionf(
Eigen::AngleAxisf(PI, Eigen::Vector3f::UnitY()) * Eigen::AngleAxisf(PI, Eigen::Vector3f::UnitY()) *
@@ -66,12 +72,12 @@ void CharacterState::scheduleNextNavigationRetry()
} }
bool CharacterState::isMoving() const { bool CharacterState::isMoving() const {
Eigen::Vector3f toTarget = walkTarget - position; Vec3fp toTarget = walkTarget - position;
toTarget.y() = 0.f; toTarget.y = static_cast<fp>(0.f);
return !pathWaypoints.empty() || toTarget.norm() > kWalkThreshold; return !pathWaypoints.empty() || toTarget.normSquared() > kWalkThreshold_SQUARED();
} }
Eigen::Vector3f CharacterState::getCurrentNavigationTarget() const { Vec3fp CharacterState::getCurrentNavigationTarget() const {
if (!pathWaypoints.empty() && currentWaypointIndex < pathWaypoints.size()) { if (!pathWaypoints.empty() && currentWaypointIndex < pathWaypoints.size()) {
return pathWaypoints[currentWaypointIndex]; return pathWaypoints[currentWaypointIndex];
} }
@@ -79,32 +85,34 @@ Eigen::Vector3f CharacterState::getCurrentNavigationTarget() const {
} }
void CharacterState::stopInPlace() { void CharacterState::stopInPlace() {
walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z()); walkTarget = Vec3fp(position.x, static_cast<fp>(0.f), position.z);
requestedWalkTarget = walkTarget; requestedWalkTarget = walkTarget;
pathWaypoints.clear(); pathWaypoints.clear();
currentWaypointIndex = 0; currentWaypointIndex = 0;
onArrivedCallbackName.clear(); onArrivedCallbackName.clear();
movementBlockedTicks = 0; movementBlockedTicks = 0;
navigationRetryTicks = 0; navigationRetryTicks = 0;
blockingCharacterId.clear(); velocity = Vec2fp(fp(0), fp(0));
preferredVelocity = Vec2fp(fp(0), fp(0));
} }
void CharacterState::setTarget( void CharacterState::setTarget(
const Eigen::Vector3f& target, const Vec3fp& target,
std::string callbackName, std::string callbackName,
const LocationNetworkState& location, const LocationNetworkState& location,
const std::string& entityId, const std::string& entityId)
const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles)
{ {
const Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z());
const Vec3fp normalizedTarget(target.x, static_cast<fp>(0.f), target.z);
const bool sameRequestedTarget = const bool sameRequestedTarget =
(normalizedTarget - requestedWalkTarget).norm() <= TARGET_REPLAN_THRESHOLD; (normalizedTarget - requestedWalkTarget).normSquared() <= TARGET_REPLAN_THRESHOLD_SQUARED;
const bool alreadyMovingToTarget = const bool alreadyMovingToTarget =
!pathWaypoints.empty() || !pathWaypoints.empty() ||
(walkTarget - normalizedTarget).norm() <= TARGET_REPLAN_THRESHOLD; (walkTarget - normalizedTarget).normSquared() <= TARGET_REPLAN_THRESHOLD_SQUARED;
const bool stoppedAfterFailedPath = const bool stoppedAfterFailedPath =
pathWaypoints.empty() && pathWaypoints.empty() &&
(walkTarget - position).norm() <= TARGET_REPLAN_THRESHOLD; (walkTarget - position).normSquared() <= TARGET_REPLAN_THRESHOLD_SQUARED;
if (callbackName.empty() && sameRequestedTarget && if (callbackName.empty() && sameRequestedTarget &&
(alreadyMovingToTarget || stoppedAfterFailedPath)) { (alreadyMovingToTarget || stoppedAfterFailedPath)) {
return; return;
@@ -114,44 +122,62 @@ void CharacterState::setTarget(
onArrivedCallbackName = std::move(callbackName); onArrivedCallbackName = std::move(callbackName);
movementBlockedTicks = 0; movementBlockedTicks = 0;
navigationRetryTicks = 0; navigationRetryTicks = 0;
blockingCharacterId.clear();
pathWaypoints = location.navigationMaps.empty() // В setTarget и forceReplan:
? std::vector<Eigen::Vector3f>{} if (location.activeNavigationIndex < 0 ||
: location.planPathExceptMyself( location.activeNavigationIndex >= static_cast<int>(location.navigationMaps.size())) {
position, normalizedTarget, entityId, dynamicObstacles); walkTarget = Vec3fp(position.x, static_cast<fp>(0.f), position.z);
currentWaypointIndex = 0; return;
}
pathWaypoints = location.navigationMaps[location.activeNavigationIndex]
.findPathToNearest(this->position, normalizedTarget);
// Сразу целимся во второй элемент (индекс 1), так как индекс 0 — это точка старта
currentWaypointIndex = (pathWaypoints.size() > 1) ? 1 : 0;
if (!pathWaypoints.empty()) { if (!pathWaypoints.empty()) {
for (Eigen::Vector3f& waypoint : pathWaypoints) waypoint.y() = 0.f; for (Vec3fp& waypoint : pathWaypoints)
{
waypoint.y = static_cast<fp>(0.f);
}
walkTarget = pathWaypoints.back(); walkTarget = pathWaypoints.back();
return; return;
} }
walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z()); walkTarget = Vec3fp(position.x, static_cast<fp>(0.f), position.z);
if (!isPlayer) scheduleNextNavigationRetry(); if (!isPlayer) scheduleNextNavigationRetry();
} }
void CharacterState::forceReplan( void CharacterState::forceReplan(
const LocationNetworkState& location, const LocationNetworkState& location,
const std::string& entityId, const std::string& entityId)
const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles)
{ {
const Eigen::Vector3f normalizedTarget( const Vec3fp normalizedTarget(
requestedWalkTarget.x(), 0.f, requestedWalkTarget.z()); requestedWalkTarget.x, static_cast<fp>(0.f), requestedWalkTarget.z);
pathWaypoints = location.navigationMaps.empty()
? std::vector<Eigen::Vector3f>{} if (location.activeNavigationIndex < 0 ||
: location.planPathExceptMyself( location.activeNavigationIndex >= static_cast<int>(location.navigationMaps.size())) {
position, normalizedTarget, entityId, dynamicObstacles); walkTarget = Vec3fp(position.x, static_cast<fp>(0.f), position.z);
currentWaypointIndex = 0;
movementBlockedTicks = 0;
navigationRetryTicks = 0;
if (!pathWaypoints.empty()) {
for (Eigen::Vector3f& waypoint : pathWaypoints) waypoint.y() = 0.f;
walkTarget = pathWaypoints.back();
blockingCharacterId.clear();
return; return;
} }
walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z()); pathWaypoints = location.navigationMaps[location.activeNavigationIndex]
.findPathToNearest(this->position, normalizedTarget);
currentWaypointIndex = (pathWaypoints.size() > 1) ? 1 : 0;
movementBlockedTicks = 0;
navigationRetryTicks = 0;
if (!pathWaypoints.empty()) {
for (Vec3fp& waypoint : pathWaypoints) {
waypoint.y = static_cast<fp>(0.f);
}
walkTarget = pathWaypoints.back();
return;
}
walkTarget = Vec3fp(position.x, static_cast<fp>(0.f), position.z);
if (!isPlayer) scheduleNextNavigationRetry(); if (!isPlayer) scheduleNextNavigationRetry();
} }
@@ -213,145 +239,134 @@ CharacterUpdateEvents CharacterState::update(
attack = 0; attack = 0;
attackTargetId.clear(); attackTargetId.clear();
} else { } else {
const float distance = (attackTarget->position - position).norm(); const fp distanceSquared = (attackTarget->position - position).normSquared();
if (distance >= visibleRange) { if (distanceSquared >= visibleRange* visibleRange) {
if ((attackTarget->position - requestedWalkTarget).norm() > if ((attackTarget->position - requestedWalkTarget).normSquared() >
TARGET_REPLAN_THRESHOLD) { TARGET_REPLAN_THRESHOLD_SQUARED) {
events.navigationRequested = true; events.navigationRequested = true;
events.navigationTarget = attackTarget->position; events.navigationTarget = attackTarget->position;
} }
battle_state = 0; battle_state = 0;
} else if (distance >= 1.25f) { } else if (distanceSquared >= static_cast<fp>(1.25f*1.25f)) {
if ((attackTarget->position - requestedWalkTarget).norm() > TARGET_REPLAN_THRESHOLD) { if ((attackTarget->position - requestedWalkTarget).normSquared() > TARGET_REPLAN_THRESHOLD_SQUARED) {
events.navigationRequested = true; events.navigationRequested = true;
events.navigationTarget = attackTarget->position; events.navigationTarget = attackTarget->position;
} }
battle_state = 0; battle_state = 0;
} else { } else {
battle_state = 1; battle_state = 1;
velocity = Vec2fp(fp(0), fp(0));
} }
} }
const Eigen::Vector3f activeTarget = getCurrentNavigationTarget(); const Vec3fp activeTarget = getCurrentNavigationTarget();
const Eigen::Vector3f lookTarget =
battle_state == 1 && attackTarget ? attackTarget->position : activeTarget;
if (currentState == AnimationState::WALK || currentState == AnimationState::STAND) { preferredVelocity = Vec2fp(fp(0), fp(0));
Eigen::Vector3f toTarget = activeTarget - position;
toTarget.y() = 0.f; if (hp > 0.f && battle_state == 0 && !pathWaypoints.empty() &&
const float distance = toTarget.norm(); (currentState == AnimationState::WALK || currentState == AnimationState::STAND))
if (distance > kWalkThreshold) { {
const Eigen::Vector3f direction = toTarget / distance; // Если мы уже достигли промежуточного waypoint'а, сразу переключаемся на следующий в этом же тике
const float moveAmount = walkSpeed * static_cast<float>(deltaMs) / 1000.f; while (!pathWaypoints.empty() && currentWaypointIndex + 1 < pathWaypoints.size()) {
Eigen::Vector3f candidate = Vec3fp diff = pathWaypoints[currentWaypointIndex] - position;
moveAmount >= distance ? activeTarget : position + direction * moveAmount; diff.y = fp(0);
candidate.y() = 0.f; if (diff.normSquared() <= kWalkThreshold_SQUARED()) {
const CharacterMovementObstacle* blocker = nullptr; ++currentWaypointIndex;
if (context.movementObstacles) {
for (const CharacterMovementObstacle& obstacle :
*context.movementObstacles) {
if (obstacle.id == context.characterId) continue;
Eigen::Vector3f startDelta = position - obstacle.position;
Eigen::Vector3f endDelta = candidate - obstacle.position;
startDelta.y() = 0.f;
endDelta.y() = 0.f;
const float minimumDistance =
movementContactRadius + obstacle.radius;
const float minimumDistanceSq =
minimumDistance * minimumDistance;
const Eigen::Vector3f segment = candidate - position;
const float segmentLengthSq =
segment.x() * segment.x() + segment.z() * segment.z();
float t = 0.f;
if (segmentLengthSq > 1e-8f) {
t = -(startDelta.x() * segment.x() +
startDelta.z() * segment.z()) / segmentLengthSq;
t = (std::max)(0.f, (std::min)(1.f, t));
} }
Eigen::Vector3f closest = startDelta + segment * t; else {
closest.y() = 0.f;
const bool movingCloser =
endDelta.squaredNorm() + 1e-6f < startDelta.squaredNorm();
if (movingCloser &&
closest.squaredNorm() < minimumDistanceSq) {
blocker = &obstacle;
break; break;
} }
} }
const Vec3fp currentTarget = getCurrentNavigationTarget();
Vec3fp toTarget = currentTarget - position;
toTarget.y = fp(0);
const fp distanceSquared = toTarget.normSquared();
if (distanceSquared > kWalkThreshold_SQUARED()) {
const fp distance = fpm::sqrt(distanceSquared);
const fp dtSec = static_cast<fp>(deltaMs) / fp(1000);
fp desiredSpeed = distance / dtSec;
if (desiredSpeed > walkSpeed) {
desiredSpeed = walkSpeed;
} }
if (blocker) {
blockingCharacterId = blocker->id; preferredVelocity = Vec2fp(
(toTarget.x / distance) * desiredSpeed,
(toTarget.z / distance) * desiredSpeed
);
// Если стартуем с места — сразу целим поворот на точку,
// а в движении угол скорректирует Фаза 3 по реальному вектору ORCA (newVel)
if (velocity.lengthSq() <= static_cast<fp>(0.05f)) {
targetFacingAngle = fpm::atan2(toTarget.x, -toTarget.z);
}
}
else {
// Дошли до ФИНАЛЬНОЙ точки маршрута
velocity = Vec2fp(fp(0), fp(0));
if (currentState == AnimationState::WALK) {
currentState = AnimationState::STAND; currentState = AnimationState::STAND;
const bool sameDirection = resetAnim = true;
blocker->moving &&
blocker->movementDirection.dot(direction) > 0.5f;
if (sameDirection) {
movementBlockedTicks = 0;
} else if (++movementBlockedTicks >= BLOCKED_REPLAN_TICKS) {
if (isPlayer) {
stopInPlace();
} else {
pathWaypoints.clear();
currentWaypointIndex = 0;
walkTarget =
Eigen::Vector3f(position.x(), 0.f, position.z());
movementBlockedTicks = 0;
scheduleNextNavigationRetry();
navigationPausedThisTick = true;
} }
}
} else {
movedLastTick =
(candidate - position).squaredNorm() > 1e-8f;
position = candidate;
movementBlockedTicks = 0;
blockingCharacterId.clear();
}
targetFacingAngle = std::atan2(direction.x(), -direction.z());
if (!blocker) currentState = AnimationState::WALK;
} else {
currentState = AnimationState::STAND;
const bool completedPath = !pathWaypoints.empty(); const bool completedPath = !pathWaypoints.empty();
const bool hasNextWaypoint =
!pathWaypoints.empty() && currentWaypointIndex + 1 < pathWaypoints.size();
if (hasNextWaypoint) {
++currentWaypointIndex;
} else {
pathWaypoints.clear(); pathWaypoints.clear();
currentWaypointIndex = 0; currentWaypointIndex = 0;
if (completedPath && if (completedPath &&
(requestedWalkTarget - position).norm() > (requestedWalkTarget - position).normSquared() > TARGET_REPLAN_THRESHOLD_SQUARED &&
TARGET_REPLAN_THRESHOLD && !isPlayer) {
!isPlayer)
scheduleNextNavigationRetry(); scheduleNextNavigationRetry();
}
if (completedPath && !onArrivedCallbackName.empty()) { if (completedPath && !onArrivedCallbackName.empty()) {
events.arrivedCallbackName = onArrivedCallbackName; events.arrivedCallbackName = onArrivedCallbackName;
onArrivedCallbackName.clear(); onArrivedCallbackName.clear();
} }
}
if (faceTarget) { if (faceTarget) {
Eigen::Vector3f toFace = faceTarget->position - position; Vec3fp toFace = faceTarget->position - position;
toFace.y() = 0.f; toFace.y = fp(0);
const float distanceToFace = toFace.norm(); const fp distanceToFace = fpm::sqrt(toFace.normSquared());
targetFacingAngle = distanceToFace > 1e-3f targetFacingAngle = distanceToFace > static_cast<fp>(1e-3f)
? std::atan2(toFace.x() / distanceToFace, -toFace.z() / distanceToFace) ? fpm::atan2(toFace.x / distanceToFace, -toFace.z / distanceToFace)
: facingAngle; : facingAngle;
} else if (!faceTargetId.empty()) { }
else if (!faceTargetId.empty()) {
faceTargetId.clear(); faceTargetId.clear();
} }
} }
} }
else if (hp > 0.f && battle_state == 0 && pathWaypoints.empty() &&
(currentState == AnimationState::WALK || currentState == AnimationState::STAND))
{
// Маршрута нет — стоим на месте и при необходимости поворачиваемся к faceTarget
velocity = Vec2fp(fp(0), fp(0));
if (currentState == AnimationState::WALK) {
currentState = AnimationState::STAND;
resetAnim = true;
}
if (faceTarget) {
Vec3fp toFace = faceTarget->position - position;
toFace.y = fp(0);
const fp distanceToFace = fpm::sqrt(toFace.normSquared());
targetFacingAngle = distanceToFace > static_cast<fp>(1e-3f)
? fpm::atan2(toFace.x / distanceToFace, -toFace.z / distanceToFace)
: facingAngle;
}
else if (!faceTargetId.empty()) {
faceTargetId.clear();
}
}
if (!isPlayer && !navigationPausedThisTick && pathWaypoints.empty() && if (!isPlayer && !navigationPausedThisTick && pathWaypoints.empty() &&
(requestedWalkTarget - position).norm() > TARGET_REPLAN_THRESHOLD && (requestedWalkTarget - position).normSquared() > TARGET_REPLAN_THRESHOLD_SQUARED &&
!events.navigationRequested) { !events.navigationRequested) {
if (navigationRetryTicks > 0) --navigationRetryTicks; if (navigationRetryTicks > 0) --navigationRetryTicks;
if (navigationRetryTicks == 0) { if (navigationRetryTicks == 0) {
events.navigationRequested = true; events.navigationRequested = true;
events.forceNavigationReplan = true; events.forceNavigationReplan = true;
events.navigationTarget = requestedWalkTarget; events.navigationTarget = requestedWalkTarget;
events.blockingCharacterId = blockingCharacterId;
} }
} }
@@ -362,9 +377,11 @@ CharacterUpdateEvents CharacterState::update(
resetAnim = true; resetAnim = true;
attackTargetId.clear(); attackTargetId.clear();
} }
} else if (battle_state == 1 && attackTarget) { }
targetFacingAngle = std::atan2( else if (battle_state == 1 && attackTarget) {
lookTarget.x() - position.x(), -(lookTarget.z() - position.z())); targetFacingAngle = fpm::atan2(
attackTarget->position.x - position.x, -(attackTarget->position.z - position.z));
if (currentState == AnimationState::STAND || currentState == AnimationState::WALK) { if (currentState == AnimationState::STAND || currentState == AnimationState::WALK) {
currentState = AnimationState::STAND_TO_ACTION; currentState = AnimationState::STAND_TO_ACTION;
resetAnim = true; resetAnim = true;
@@ -388,24 +405,25 @@ CharacterUpdateEvents CharacterState::update(
resetAnim = true; resetAnim = true;
} }
float angleDiff = targetFacingAngle - facingAngle; fp angleDiff = targetFacingAngle - facingAngle;
while (angleDiff > PI) angleDiff -= 2.f * PI; const fp PIfp = static_cast<fp>(PI);
while (angleDiff < -PI) angleDiff += 2.f * PI; while (angleDiff > PIfp) angleDiff -= static_cast<fp>(2.f) * PIfp;
const float rotationStep = rotationSpeed * static_cast<float>(deltaMs) / 1000.f; while (angleDiff < -PIfp) angleDiff += static_cast<fp>(2.f) * PIfp;
if (std::fabs(angleDiff) <= rotationStep) { const fp rotationStep = rotationSpeed * static_cast<fp>(deltaMs) / static_cast<fp>(1000.f);
if (fpm::abs(angleDiff) <= rotationStep) {
facingAngle = targetFacingAngle; facingAngle = targetFacingAngle;
faceTargetId.clear(); faceTargetId.clear();
} else { } else {
facingAngle += angleDiff > 0.f ? rotationStep : -rotationStep; facingAngle += angleDiff > static_cast<fp>(0.f) ? rotationStep : -rotationStep;
} }
if (!isPlayer && hp > 0.f && battle_state == 0 && maxHomeDrift > 0.f) { if (!isPlayer && hp > 0.f && battle_state == 0 && maxHomeDriftSquared > static_cast<fp>(0.f)) {
homeDriftCheckTimer -= static_cast<float>(deltaMs) / 1000.f; homeDriftCheckTimer -= static_cast<fp>(deltaMs) / static_cast<fp>(1000.f);
if (homeDriftCheckTimer <= 0.f) { if (homeDriftCheckTimer <= static_cast <fp>(0.f)) {
homeDriftCheckTimer = HOME_DRIFT_CHECK_INTERVAL; homeDriftCheckTimer = HOME_DRIFT_CHECK_INTERVAL_FP;
Eigen::Vector3f toHome = homePosition - position; Vec3fp toHome = homePosition - position;
toHome.y() = 0.f; toHome.y = static_cast<fp>(0.f);
if (!isMoving() && toHome.norm() > maxHomeDrift) { if (!isMoving() && toHome.normSquared() > maxHomeDriftSquared) {
events.navigationRequested = true; events.navigationRequested = true;
events.navigationTarget = homePosition; events.navigationTarget = homePosition;
} }
@@ -428,10 +446,18 @@ CharacterUpdateEvents CharacterState::update(
events.attackImpact = true; events.attackImpact = true;
events.attackTargetId = attackTargetId; events.attackTargetId = attackTargetId;
events.attackDirection = attackTarget->position - position; events.attackDirection = attackTarget->position - position;
events.attackDirection.y() = 0.f; events.attackDirection.y = static_cast<fp>(0.f);
const float length = events.attackDirection.norm(); const fp lengthSquared = events.attackDirection.normSquared();
if (length > 1e-3f) events.attackDirection /= length; if (lengthSquared > static_cast<fp>(0.001f))
else events.attackDirection = Eigen::Vector3f::Zero(); {
const fp length = fpm::sqrt(lengthSquared); //FP SQRT
events.attackDirection = events.attackDirection / length;
}
else
{
events.attackDirection = Vec3fp{};
}
} }
showWeapon = showWeapon =
@@ -467,9 +493,9 @@ CharacterUpdateEvents CharacterState::update(
void CharacterState::save(nlohmann::json& out) const void CharacterState::save(nlohmann::json& out) const
{ {
out["position"] = { position.x(), position.y(), position.z() }; out["position"] = { static_cast<double>(position.x), static_cast<double>(position.y), static_cast<double>(position.z) };
out["facingAngle"] = facingAngle; out["facingAngle"] = static_cast<double>(facingAngle);
out["targetFacingAngle"] = targetFacingAngle; out["targetFacingAngle"] = static_cast<double>(targetFacingAngle);
out["hp"] = hp; out["hp"] = hp;
out["enabled"] = enabled; out["enabled"] = enabled;
out["battle_state"] = battle_state; out["battle_state"] = battle_state;
@@ -478,25 +504,24 @@ void CharacterState::save(nlohmann::json& out) const
out["showWeapon"] = showWeapon; out["showWeapon"] = showWeapon;
out["attackTargetId"] = attackTargetId; out["attackTargetId"] = attackTargetId;
out["faceTargetId"] = faceTargetId; out["faceTargetId"] = faceTargetId;
out["homePosition"] = { homePosition.x(), homePosition.y(), homePosition.z() }; out["homePosition"] = { static_cast<double>(homePosition.x), static_cast<double>(homePosition.y), static_cast<double>(homePosition.z) };
out["walkTarget"] = { walkTarget.x(), walkTarget.y(), walkTarget.z() }; out["walkTarget"] = { static_cast<double>(walkTarget.x), static_cast<double>(walkTarget.y), static_cast<double>(walkTarget.z) };
out["requestedWalkTarget"] = { requestedWalkTarget.x(), requestedWalkTarget.y(), requestedWalkTarget.z() }; out["requestedWalkTarget"] = { static_cast<double>(requestedWalkTarget.x), static_cast<double>(requestedWalkTarget.y), static_cast<double>(requestedWalkTarget.z) };
nlohmann::json wpArr = nlohmann::json::array(); nlohmann::json wpArr = nlohmann::json::array();
for (const auto& wp : pathWaypoints) { for (const auto& wp : pathWaypoints) {
wpArr.push_back({ wp.x(), wp.y(), wp.z() }); wpArr.push_back({ static_cast<double>(wp.x), static_cast<double>(wp.y), static_cast<double>(wp.z) });
} }
out["pathWaypoints"] = std::move(wpArr); out["pathWaypoints"] = std::move(wpArr);
out["currentWaypointIndex"] = currentWaypointIndex; out["currentWaypointIndex"] = currentWaypointIndex;
out["homeDriftCheckTimer"] = homeDriftCheckTimer; out["homeDriftCheckTimer"] = static_cast<double>(homeDriftCheckTimer);
out["movementBlockedTicks"] = movementBlockedTicks; out["movementBlockedTicks"] = movementBlockedTicks;
out["navigationRetryTicks"] = navigationRetryTicks; out["navigationRetryTicks"] = navigationRetryTicks;
out["movedLastTick"] = movedLastTick; out["movedLastTick"] = movedLastTick;
out["navigationRetryCooldownTicks"] = navigationRetryCooldownTicks; out["navigationRetryCooldownTicks"] = navigationRetryCooldownTicks;
out["navigationRetryCooldownIndex"] = navigationRetryCooldownIndex; out["navigationRetryCooldownIndex"] = navigationRetryCooldownIndex;
if (!blockingCharacterId.empty())
out["blockingCharacterId"] = blockingCharacterId;
out["animationFrame"] = animationFrame; out["animationFrame"] = animationFrame;
out["visibleRange"] = visibleRange; out["visibleRange"] = static_cast<double>(visibleRange);
if (!onArrivedCallbackName.empty()) if (!onArrivedCallbackName.empty())
out["onArrivedCallbackName"] = onArrivedCallbackName; out["onArrivedCallbackName"] = onArrivedCallbackName;
} }
@@ -509,11 +534,19 @@ void CharacterState::load(const nlohmann::json& in)
return def; return def;
}; };
if (in.contains("position")) position = readVec3(in["position"], position); auto readVec3fp = [](const nlohmann::json& j, Vec3fp def) -> Vec3fp {
facingAngle = in.value("facingAngle", facingAngle); if (j.is_array() && j.size() == 3)
targetFacingAngle = in.value("targetFacingAngle", targetFacingAngle); return Vec3fp{ static_cast<fp>(j[0].get<double>()), static_cast<fp>(j[1].get<double>()), static_cast<fp>(j[2].get<double>()) };
return def;
};
if (in.contains("position")) {
position = readVec3fp(in["position"], position);
}
facingAngle = static_cast<fp>(in.value("facingAngle", static_cast<double>(facingAngle)));
targetFacingAngle = static_cast<fp>(in.value("targetFacingAngle", static_cast<double>(targetFacingAngle)));
hp = in.value("hp", hp); hp = in.value("hp", hp);
visibleRange = in.value("visibleRange", visibleRange); visibleRange = static_cast<fp>(in.value("visibleRange", static_cast<double>(visibleRange)));
enabled = in.value("enabled", enabled); enabled = in.value("enabled", enabled);
battle_state = in.value("battle_state", battle_state); battle_state = in.value("battle_state", battle_state);
currentState = static_cast<AnimationState>(in.value("currentState", static_cast<int>(currentState))); currentState = static_cast<AnimationState>(in.value("currentState", static_cast<int>(currentState)));
@@ -521,17 +554,17 @@ void CharacterState::load(const nlohmann::json& in)
showWeapon = in.value("showWeapon", showWeapon); showWeapon = in.value("showWeapon", showWeapon);
attackTargetId = in.value("attackTargetId", attackTargetId); attackTargetId = in.value("attackTargetId", attackTargetId);
faceTargetId = in.value("faceTargetId", faceTargetId); faceTargetId = in.value("faceTargetId", faceTargetId);
if (in.contains("homePosition")) homePosition = readVec3(in["homePosition"], homePosition); if (in.contains("homePosition")) homePosition = readVec3fp(in["homePosition"], homePosition);
if (in.contains("walkTarget")) walkTarget = readVec3(in["walkTarget"], walkTarget); if (in.contains("walkTarget")) walkTarget = readVec3fp(in["walkTarget"], walkTarget);
if (in.contains("requestedWalkTarget")) requestedWalkTarget = readVec3(in["requestedWalkTarget"], requestedWalkTarget); if (in.contains("requestedWalkTarget")) requestedWalkTarget = readVec3fp(in["requestedWalkTarget"], requestedWalkTarget);
pathWaypoints.clear(); pathWaypoints.clear();
if (in.contains("pathWaypoints") && in["pathWaypoints"].is_array()) { if (in.contains("pathWaypoints") && in["pathWaypoints"].is_array()) {
for (const auto& wp : in["pathWaypoints"]) { for (const auto& wp : in["pathWaypoints"]) {
pathWaypoints.push_back(readVec3(wp, Eigen::Vector3f::Zero())); pathWaypoints.push_back(readVec3fp(wp, Vec3fp()));
} }
} }
currentWaypointIndex = in.value("currentWaypointIndex", currentWaypointIndex); currentWaypointIndex = in.value("currentWaypointIndex", currentWaypointIndex);
homeDriftCheckTimer = in.value("homeDriftCheckTimer", homeDriftCheckTimer); homeDriftCheckTimer = static_cast<fp>(in.value("homeDriftCheckTimer", static_cast<double>(homeDriftCheckTimer)));
movementBlockedTicks = in.value("movementBlockedTicks", movementBlockedTicks); movementBlockedTicks = in.value("movementBlockedTicks", movementBlockedTicks);
navigationRetryTicks = in.value("navigationRetryTicks", navigationRetryTicks); navigationRetryTicks = in.value("navigationRetryTicks", navigationRetryTicks);
movedLastTick = in.value("movedLastTick", movedLastTick); movedLastTick = in.value("movedLastTick", movedLastTick);
@@ -541,8 +574,6 @@ void CharacterState::load(const nlohmann::json& in)
"navigationRetryCooldownIndex", navigationRetryCooldownIndex); "navigationRetryCooldownIndex", navigationRetryCooldownIndex);
if (!navigationRetryCooldownTicks.empty()) if (!navigationRetryCooldownTicks.empty())
navigationRetryCooldownIndex %= navigationRetryCooldownTicks.size(); navigationRetryCooldownIndex %= navigationRetryCooldownTicks.size();
blockingCharacterId =
in.value("blockingCharacterId", std::string());
animationFrame = in.value("animationFrame", animationFrame); animationFrame = in.value("animationFrame", animationFrame);
onArrivedCallbackName = in.value("onArrivedCallbackName", std::string()); onArrivedCallbackName = in.value("onArrivedCallbackName", std::string());
} }
+34 -40
View File
@@ -61,6 +61,9 @@ public:
// Distance below which the character is considered to have reached its target. // Distance below which the character is considered to have reached its target.
static constexpr float kWalkThreshold = 0.05f; static constexpr float kWalkThreshold = 0.05f;
static fp kWalkThreshold_SQUARED() { return static_cast<fp>(kWalkThreshold * kWalkThreshold); };
// --- Identity --- // --- Identity ---
std::string npcName; std::string npcName;
bool isPlayer = false; bool isPlayer = false;
@@ -68,15 +71,15 @@ public:
bool canAttack = false; bool canAttack = false;
// --- Position / orientation --- // --- Position / orientation ---
Eigen::Vector3f position = Eigen::Vector3f::Zero(); Vec3fp position;
float facingAngle = 0.0f; fp facingAngle;
float targetFacingAngle = 0.0f; fp targetFacingAngle;
// --- Health --- // --- Health ---
float hp = 200.f; float hp = 200.f;
float initialHp = 200.f; float initialHp = 200.f;
float visibleRange = VISIBLE_RANGE; fp visibleRange = static_cast<fp>(VISIBLE_RANGE);
// --- Combat flags --- // --- Combat flags ---
int battle_state = 0; int battle_state = 0;
@@ -95,8 +98,8 @@ public:
std::string faceTargetId; std::string faceTargetId;
// --- Home position for drift recovery --- // --- Home position for drift recovery ---
Eigen::Vector3f homePosition = Eigen::Vector3f::Zero(); Vec3fp homePosition;
float maxHomeDrift = 3.0f; fp maxHomeDriftSquared = static_cast<fp>(3.f*3.f);
// --- Authoritative logical animation clock --- // --- Authoritative logical animation clock ---
// Frame counts are immutable metadata copied from loaded clips during // Frame counts are immutable metadata copied from loaded clips during
@@ -106,29 +109,33 @@ public:
std::array<int, 9> animationFrameCounts = {{1, 1, 1, 1, 1, 1, 1, 1, 1}}; std::array<int, 9> animationFrameCounts = {{1, 1, 1, 1, 1, 1, 1, 1, 1}};
// --- Movement / visual parameters --- // --- Movement / visual parameters ---
float walkSpeed = 3.0f; fp walkSpeed = static_cast<fp>(3.0f);
float rotationSpeed = 4.0f; fp rotationSpeed = static_cast<fp>(4.0f);
float modelScale = 0.12f; float modelScale = 0.12f;
Eigen::Quaternionf modelCorrectionRotation = Eigen::Quaternionf::Identity(); Eigen::Quaternionf modelCorrectionRotation = Eigen::Quaternionf::Identity();
bool useGpuSkinning = true; bool useGpuSkinning = true;
// Full clearance used while planning around stationary characters.
float collisionRadius = 0.45f; fp collisionRadius = static_cast<fp>(0.4f); // Радиус капсулы персонажа
// Smaller physical contact threshold used by the movement guard.
float movementContactRadius = 0.32f; // Векторы скорости в плоскости XZ (сериализовать в JSON не обязательно, если считаются каждый тик,
float interactionRadius = 0.0f; // но для строгого lockstep-снапшота velocity лучше сохранять)
Vec2fp velocity{ fp(0), fp(0) }; // Реальная скорость с прошлого тика (нужна соседям в ORCA)
Vec2fp preferredVelocity{ fp(0), fp(0) }; // Желаемая скорость к waypoint'у на текущем тике
fp interactionRadius = static_cast<fp>(0.0f);
bool movedLastTick = false; bool movedLastTick = false;
// --- Pathfinding data (serializable parts) --- // --- Pathfinding data (serializable parts) ---
Eigen::Vector3f walkTarget = Eigen::Vector3f::Zero(); Vec3fp walkTarget;
Eigen::Vector3f requestedWalkTarget = Eigen::Vector3f::Zero(); Vec3fp requestedWalkTarget;
std::vector<Eigen::Vector3f> pathWaypoints; std::vector<Vec3fp> pathWaypoints;
size_t currentWaypointIndex = 0; size_t currentWaypointIndex = 0;
// Internal drift-check timer (serializable so it survives save/load). // Internal drift-check timer (serializable so it survives save/load).
float homeDriftCheckTimer = 5.0f; fp homeDriftCheckTimer = static_cast<fp>(5.0f);
int movementBlockedTicks = 0; int movementBlockedTicks = 0;
int navigationRetryTicks = 0; int navigationRetryTicks = 0;
std::string blockingCharacterId;
std::vector<int> navigationRetryCooldownTicks; std::vector<int> navigationRetryCooldownTicks;
std::size_t navigationRetryCooldownIndex = 0; std::size_t navigationRetryCooldownIndex = 0;
@@ -145,19 +152,15 @@ public:
const std::string& entityId, const std::string& entityId,
std::vector<int> cooldownTicks = {}); std::vector<int> cooldownTicks = {});
bool isMoving() const; bool isMoving() const;
const Eigen::Vector3f& getRequestedWalkTarget() const { return requestedWalkTarget; } const Vec3fp& getRequestedWalkTarget() const { return requestedWalkTarget; }
Eigen::Vector3f getCurrentNavigationTarget() const; Vec3fp getCurrentNavigationTarget() const;
void stopInPlace(); void stopInPlace();
void setTarget(const Eigen::Vector3f& target, void setTarget(const Vec3fp& target,
std::string callbackName, std::string callbackName,
const LocationNetworkState& location, const LocationNetworkState& location,
const std::string& entityId, const std::string& entityId);
const std::vector<PathFinder::DynamicObstacle>&
dynamicObstacles = {});
void forceReplan(const LocationNetworkState& location, void forceReplan(const LocationNetworkState& location,
const std::string& entityId, const std::string& entityId);
const std::vector<PathFinder::DynamicObstacle>&
dynamicObstacles = {});
void stopAndRotateToTarget(std::string characterId); void stopAndRotateToTarget(std::string characterId);
void resetPlayerAfterDeath(); void resetPlayerAfterDeath();
void tryAttackCombo(); void tryAttackCombo();
@@ -177,23 +180,14 @@ using PlayerStateMap =
std::unordered_map<std::string, std::shared_ptr<CharacterState>>; std::unordered_map<std::string, std::shared_ptr<CharacterState>>;
struct CharacterObservation { struct CharacterObservation {
Eigen::Vector3f position = Eigen::Vector3f::Zero(); Vec3fp position;
float hp = 0.f; float hp = 0.f;
}; };
struct CharacterMovementObstacle {
std::string id;
Eigen::Vector3f position = Eigen::Vector3f::Zero();
float radius = 0.f;
bool moving = false;
Eigen::Vector3f movementDirection = Eigen::Vector3f::Zero();
};
struct CharacterUpdateContext { struct CharacterUpdateContext {
std::string characterId; std::string characterId;
const CharacterObservation* attackTarget = nullptr; const CharacterObservation* attackTarget = nullptr;
const CharacterObservation* faceTarget = nullptr; const CharacterObservation* faceTarget = nullptr;
const std::vector<CharacterMovementObstacle>* movementObstacles = nullptr;
}; };
struct CharacterUpdateEvents { struct CharacterUpdateEvents {
@@ -201,11 +195,11 @@ struct CharacterUpdateEvents {
bool deathAnimationComplete = false; bool deathAnimationComplete = false;
bool navigationRequested = false; bool navigationRequested = false;
bool forceNavigationReplan = false; bool forceNavigationReplan = false;
Eigen::Vector3f navigationTarget = Eigen::Vector3f::Zero(); Vec3fp navigationTarget;
std::string blockingCharacterId;
std::string arrivedCallbackName; std::string arrivedCallbackName;
std::string attackTargetId; std::string attackTargetId;
Eigen::Vector3f attackDirection = Eigen::Vector3f::Zero(); Vec3fp attackDirection;
}; };
} // namespace FRG } // namespace FRG
+2 -2
View File
@@ -664,9 +664,9 @@ namespace FRG
if (!loc) return; if (!loc) return;
if (loc->player) { if (loc->player) {
server().submit(SetCharacterPositionCommand{ server().submit(SetCharacterPositionCommand{
destName, loc->player->entityId(), destPos}); destName, loc->player->entityId(), Vec3fp::fromEigen(destPos)});
server().submit(SetCharacterFacingAngleCommand{ server().submit(SetCharacterFacingAngleCommand{
destName, loc->player->entityId(), destRotY, true}); destName, loc->player->entityId(), static_cast<fp>(destRotY), true});
loc->stopCharacter(*loc->player); loc->stopCharacter(*loc->player);
} }
loc->localState().cameraAzimuth = destRotY; loc->localState().cameraAzimuth = destRotY;
+26 -58
View File
@@ -126,7 +126,7 @@ namespace FRG
} }
void Location::setCharacterTarget( void Location::setCharacterTarget(
Character& character, const Eigen::Vector3f& target, std::string callbackName) Character& character, const Vec3fp& target, std::string callbackName)
{ {
gameServer_.submit(SetCharacterDestinationCommand{ gameServer_.submit(SetCharacterDestinationCommand{
locationId_, character.entityId(), target, std::move(callbackName)}); locationId_, character.entityId(), target, std::move(callbackName)});
@@ -851,8 +851,8 @@ namespace FRG
float t0 = (worldMin[axis] - rayOrigin[axis]) * invD; float t0 = (worldMin[axis] - rayOrigin[axis]) * invD;
float t1 = (worldMax[axis] - rayOrigin[axis]) * invD; float t1 = (worldMax[axis] - rayOrigin[axis]) * invD;
if (invD < 0.0f) std::swap(t0, t1); if (invD < 0.0f) std::swap(t0, t1);
tMin = max(tMin, t0); tMin = std::max(tMin, t0);
tMax = min(tMax, t1); tMax = std::min(tMax, t1);
if (tMax < tMin) { miss = true; break; } if (tMax < tMin) { miss = true; break; }
} }
} }
@@ -1597,7 +1597,7 @@ namespace FRG
if (playerState && !pointLights.empty()) { if (playerState && !pointLights.empty()) {
float minDist = FLT_MAX; float minDist = FLT_MAX;
for (int i = 0; i < static_cast<int>(pointLights.size()); ++i) { for (int i = 0; i < static_cast<int>(pointLights.size()); ++i) {
const float d = (pointLights[i].position - playerState->position).norm(); const float d = (pointLights[i].position - playerState->position.toEigen()).norm();
if (d < minDist) { minDist = d; nearestLightIdx = i; } if (d < minDist) { minDist = d; nearestLightIdx = i; }
} }
} }
@@ -1611,12 +1611,12 @@ namespace FRG
if (!active && playerState) { if (!active && playerState) {
if (pl.autoLightLimitX > 0.0f && pl.autoLightLimitY > 0.0f) { if (pl.autoLightLimitX > 0.0f && pl.autoLightLimitY > 0.0f) {
// Rectangular activation zone — independent of nearest-light heuristic // Rectangular activation zone — independent of nearest-light heuristic
const float dx = std::abs(playerState->position.x() - pl.position.x()); const float dx = std::abs(static_cast<float>(playerState->position.x) - pl.position.x());
const float dz = std::abs(playerState->position.z() - pl.position.z()); const float dz = std::abs(static_cast<float>(playerState->position.z) - pl.position.z());
active = (dx <= pl.autoLightLimitX && dz <= pl.autoLightLimitY); active = (dx <= pl.autoLightLimitX && dz <= pl.autoLightLimitY);
} else { } else {
// only the nearest autoLight activates, with optional distance cap // only the nearest autoLight activates, with optional distance cap
const float dist = (pl.position - playerState->position).norm(); const float dist = (pl.position - playerState->position.toEigen()).norm();
active = (i == nearestLightIdx) && active = (i == nearestLightIdx) &&
(pl.autoLightDistance <= 0.0f || dist <= pl.autoLightDistance); (pl.autoLightDistance <= 0.0f || dist <= pl.autoLightDistance);
} }
@@ -1632,7 +1632,7 @@ namespace FRG
if (playerState && !activeLightIndices.empty()) { if (playerState && !activeLightIndices.empty()) {
float minDist = FLT_MAX; float minDist = FLT_MAX;
for (int idx : activeLightIndices) { for (int idx : activeLightIndices) {
const float d = (pointLights[idx].position - playerState->position).norm(); const float d = (pointLights[idx].position - playerState->position.toEigen()).norm();
if (d < minDist) { minDist = d; shadowLight = &pointLights[idx]; } if (d < minDist) { minDist = d; shadowLight = &pointLights[idx]; }
} }
} }
@@ -1838,50 +1838,18 @@ namespace FRG
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) { state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) {
return false; return false;
} }
const auto& areas = const auto& area =
state->navigationMaps[state->activeNavigationIndex].getAreas(); state->navigationMaps[state->activeNavigationIndex].getArea();
const bool found = std::any_of( if (area.name != areaName)
areas.begin(), areas.end(), {
[&](const PathFinder::NavigationArea& area) { return false;
return area.name == areaName; }
});
if (!found) return false;
gameServer_.submit(SetNavigationAreaAvailableCommand{ gameServer_.submit(SetNavigationAreaAvailableCommand{
locationId_, areaName, available}); locationId_, areaName, available});
return true; return true;
} }
void Location::nudgeCharacterAside(Character* standing, const Eigen::Vector3f& awayFrom)
{
auto standingState = standing ? standing->stateSnapshot() : nullptr;
if (!standingState || standingState->isPlayer) return;
auto locationState = stateSnapshot();
if (!locationState || locationState->activeNavigationIndex < 0 ||
locationState->activeNavigationIndex >=
static_cast<int>(locationState->navigationMaps.size())) return;
const PathFinder& navigation =
locationState->navigationMaps[locationState->activeNavigationIndex];
if (navigation.isReady()) return;
static constexpr float kNudgeDist = 1.2f;
Eigen::Vector3f dir = standingState->position - awayFrom;
dir.y() = 0.f;
if (dir.norm() < 1e-3f) dir = Eigen::Vector3f(1.f, 0.f, 0.f);
else dir.normalize();
const float angles[] = { 0.f, static_cast<float>(M_PI * 0.5), static_cast<float>(-M_PI * 0.5), static_cast<float>(M_PI) };
for (float angle : angles) {
Eigen::Vector3f rotated = Eigen::AngleAxisf(angle, Eigen::Vector3f::UnitY()) * dir;
Eigen::Vector3f candidate = standingState->position + rotated * kNudgeDist;
candidate.y() = 0.f;
if (navigation.isWalkable(candidate)) {
setCharacterTarget(*standing, candidate);
return;
}
}
}
void Location::update(int64_t delta, const LocationSimulationEvents& events) void Location::update(int64_t delta, const LocationSimulationEvents& events)
{ {
synchronizeRemotePlayers(); synchronizeRemotePlayers();
@@ -1952,7 +1920,7 @@ namespace FRG
player->update(delta, eventFor(gameServer_.localPlayerId())); player->update(delta, eventFor(gameServer_.localPlayerId()));
auto playerState = player->stateSnapshot(); auto playerState = player->stateSnapshot();
if (playerState && playerState->enabled) if (playerState && playerState->enabled)
updateTriggerZones(playerState->position); updateTriggerZones(playerState->position.toEigen());
} }
for (auto& [id, remote] : remotePlayers_) for (auto& [id, remote] : remotePlayers_)
if (remote) remote->update(delta, eventFor(id)); if (remote) remote->update(delta, eventFor(id));
@@ -1989,7 +1957,7 @@ namespace FRG
auto playerState = player ? player->stateSnapshot() : nullptr; auto playerState = player ? player->stateSnapshot() : nullptr;
if (auto* tz = getTargetTeleportZone(*locationState); if (auto* tz = getTargetTeleportZone(*locationState);
tz && playerState && playerState->enabled) { tz && playerState && playerState->enabled) {
float dist = (playerState->position - tz->position).norm(); float dist = (playerState->position.toEigen() - tz->position).norm();
if (dist <= tz->radius) { if (dist <= tz->radius) {
logger() << "[TELEPORT] Player reached teleport zone '" << tz->id << "'" << std::endl; logger() << "[TELEPORT] Player reached teleport zone '" << tz->id << "'" << std::endl;
gameServer_.submit(SetLocationTargetsCommand{ gameServer_.submit(SetLocationTargetsCommand{
@@ -2006,7 +1974,7 @@ namespace FRG
for (auto& tz : teleportZones) { for (auto& tz : teleportZones) {
if (!tz.active || !tz.automatic) continue; if (!tz.active || !tz.automatic) continue;
const float autoR = tz.automaticRadius > 0.0f ? tz.automaticRadius : tz.radius; const float autoR = tz.automaticRadius > 0.0f ? tz.automaticRadius : tz.radius;
const float dist = (playerState->position - tz.position).norm(); const float dist = (playerState->position.toEigen() - tz.position).norm();
if (!tz.automaticInitialized) { if (!tz.automaticInitialized) {
// If the player starts inside the zone (e.g. just arrived here via teleport), // If the player starts inside the zone (e.g. just arrived here via teleport),
@@ -2102,7 +2070,7 @@ namespace FRG
const Eigen::Vector3f interactionTarget = clickedObject->state.hasInteractionPosition const Eigen::Vector3f interactionTarget = clickedObject->state.hasInteractionPosition
? clickedObject->state.interactionPosition ? clickedObject->state.interactionPosition
: clickedObject->state.position; : clickedObject->state.position;
setCharacterTarget(*player, interactionTarget); setCharacterTarget(*player, Vec3fp::fromEigen(interactionTarget));
pathVisualization.start(*player, interactionTarget); pathVisualization.start(*player, interactionTarget);
clearCharacterAttackTarget(*player); clearCharacterAttackTarget(*player);
//logger() << "[CLICK] Player moving to object..." << std::endl; //logger() << "[CLICK] Player moving to object..." << std::endl;
@@ -2115,7 +2083,7 @@ namespace FRG
if (clickedNpc && player) { if (clickedNpc && player) {
auto clickedNpcState = clickedNpc->stateSnapshot(); auto clickedNpcState = clickedNpc->stateSnapshot();
if (!clickedNpcState) return; if (!clickedNpcState) return;
float distance = (playerState->position - clickedNpcState->position).norm(); float distance = (playerState->position - clickedNpcState->position).toEigen().norm();
int npcIndex = -1; int npcIndex = -1;
for (size_t i = 0; i < npcs.size(); ++i) { for (size_t i = 0; i < npcs.size(); ++i) {
if (npcs[i].get() == clickedNpc) { if (npcs[i].get() == clickedNpc) {
@@ -2145,11 +2113,11 @@ namespace FRG
setCharacterTarget(*player, clickedNpcState->position); setCharacterTarget(*player, clickedNpcState->position);
pathVisualization.start( pathVisualization.start(
*player, *player,
clickedNpcState->position, clickedNpcState->position.toEigen(),
PathVisualization::Style::Hostile); PathVisualization::Style::Hostile);
} }
if (distance <= clickedNpcState->interactionRadius) { if (distance <= static_cast<float>(clickedNpcState->interactionRadius)) {
//logger() << "[CLICK] Hostile NPC " << npcIndex << " in range; firing on_npc_interact" << std::endl; //logger() << "[CLICK] Hostile NPC " << npcIndex << " in range; firing on_npc_interact" << std::endl;
scriptEngine.callNpcInteractCallback(clickedNpc->entityId()); scriptEngine.callNpcInteractCallback(clickedNpc->entityId());
} }
@@ -2173,7 +2141,7 @@ namespace FRG
//logger() << "[CLICK] NPC " << npcIndex << " out of talk range (distance " << distance //logger() << "[CLICK] NPC " << npcIndex << " out of talk range (distance " << distance
// << " > " << NPC_TALK_DISTANCE << "); walking to NPC..." << std::endl; // << " > " << NPC_TALK_DISTANCE << "); walking to NPC..." << std::endl;
setCharacterTarget(*player, clickedNpcState->position); setCharacterTarget(*player, clickedNpcState->position);
pathVisualization.start(*player, clickedNpcState->position); pathVisualization.start(*player, clickedNpcState->position.toEigen());
gameServer_.submit(SetLocationTargetsCommand{ gameServer_.submit(SetLocationTargetsCommand{
locationId_, std::string(), clickedNpc->entityId(), locationId_, std::string(), clickedNpc->entityId(),
std::string()}); std::string()});
@@ -2200,7 +2168,7 @@ namespace FRG
gameServer_.submit(SetLocationTargetsCommand{ gameServer_.submit(SetLocationTargetsCommand{
locationId_, std::string(), std::string(), locationId_, std::string(), std::string(),
clickedTeleport->id}); clickedTeleport->id});
setCharacterTarget(*player, clickedTeleport->position); setCharacterTarget(*player, Vec3fp::fromEigen(clickedTeleport->position));
pathVisualization.start( pathVisualization.start(
*player, *player,
clickedTeleport->position, clickedTeleport->position,
@@ -2208,7 +2176,7 @@ namespace FRG
clearCharacterAttackTarget(*player); clearCharacterAttackTarget(*player);
} else { } else {
const Eigen::Vector3f clickedDestination(hit.x(), 0.f, hit.z()); const Eigen::Vector3f clickedDestination(hit.x(), 0.f, hit.z());
setCharacterTarget(*player, clickedDestination); setCharacterTarget(*player, Vec3fp::fromEigen(clickedDestination));
pathVisualization.start(*player, clickedDestination); pathVisualization.start(*player, clickedDestination);
clearCharacterAttackTarget(*player); clearCharacterAttackTarget(*player);
gameServer_.submit(SetLocationTargetsCommand{ gameServer_.submit(SetLocationTargetsCommand{
@@ -2253,7 +2221,7 @@ namespace FRG
const float minInclination = M_PI * 30.f / 180.f; const float minInclination = M_PI * 30.f / 180.f;
const float maxInclination = M_PI * 0.5f; const float maxInclination = M_PI * 0.5f;
localState_->cameraInclination = localState_->cameraInclination =
max(minInclination, min(maxInclination, inclination)); std::max(minInclination, std::min(maxInclination, inclination));
} }
} }
+2 -3
View File
@@ -64,7 +64,7 @@ namespace FRG
std::string teleportsJsonPath; std::string teleportsJsonPath;
std::string triggerZonesJsonPath; std::string triggerZonesJsonPath;
std::string lightsJsonPath; std::string lightsJsonPath;
Eigen::Vector3f playerPosition = Eigen::Vector3f::Zero(); Vec3fp playerPosition;
// Player shared by all locations of a campaign (see Location::createPlayer); // Player shared by all locations of a campaign (see Location::createPlayer);
// setup() creates a private one when this is null. // setup() creates a private one when this is null.
std::shared_ptr<Character> player; std::shared_ptr<Character> player;
@@ -174,7 +174,7 @@ namespace FRG
void setup(const LocationSetup& params, AudioPlayerAsync* audioPlayer); void setup(const LocationSetup& params, AudioPlayerAsync* audioPlayer);
//void setupNavigation(const std::vector<std::string>& paths); //void setupNavigation(const std::vector<std::string>& paths);
bool switchNavigation(int index); bool switchNavigation(int index);
void setCharacterTarget(Character& character, const Eigen::Vector3f& target, void setCharacterTarget(Character& character, const Vec3fp& target,
std::string callbackName = ""); std::string callbackName = "");
void clearCharacterAttackTarget(Character& character); void clearCharacterAttackTarget(Character& character);
void stopCharacter(Character& character); void stopCharacter(Character& character);
@@ -259,7 +259,6 @@ namespace FRG
void loadTriggerZones(const std::string& jsonPath, const char* zipFile); void loadTriggerZones(const std::string& jsonPath, const char* zipFile);
void loadPointLights(const std::string& jsonPath, const char* zipFile); void loadPointLights(const std::string& jsonPath, const char* zipFile);
void updateTriggerZones(const Eigen::Vector3f& playerPos); void updateTriggerZones(const Eigen::Vector3f& playerPos);
void nudgeCharacterAside(Character* standing, const Eigen::Vector3f& awayFrom);
void captureAuxiliaryState(LocationGameState& out) const; void captureAuxiliaryState(LocationGameState& out) const;
void applyAuxiliaryState(const LocationGameState& in); void applyAuxiliaryState(const LocationGameState& in);
+11 -11
View File
@@ -30,12 +30,12 @@ namespace FRG
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return; state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
const PathFinder& navigation = const PathFinder& navigation =
state->navigationMaps[state->activeNavigationIndex]; state->navigationMaps[state->activeNavigationIndex];
const float y = navigation.getFloorY() + 0.02f; const float y = static_cast<float>(navigation.getFloorY()) + 0.02f;
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f); const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
for (const auto& obs : navigation.getObstaclePolygons()) { for (const auto& obs : navigation.getObstaclePolygons()) {
if (obs.polygon.size() < 3) continue; if (obs.polygon.size() < 3) continue;
VertexRenderStruct mesh; VertexRenderStruct mesh;
mesh.data = CreatePolygonFloor(obs.polygon, y, red); mesh.data = CreatePolygonFloor(toEigenVector(obs.polygon), y, red);
mesh.RefreshVBO(); mesh.RefreshVBO();
navigationEditorNavMeshes.push_back(std::move(mesh)); navigationEditorNavMeshes.push_back(std::move(mesh));
} }
@@ -60,7 +60,7 @@ namespace FRG
if (!state || state->activeNavigationIndex < 0 || if (!state || state->activeNavigationIndex < 0 ||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return; state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
const float y = const float y =
state->navigationMaps[state->activeNavigationIndex].getFloorY() + 0.05f; static_cast<float>(state->navigationMaps[state->activeNavigationIndex].getFloorY()) + 0.05f;
const float s = 0.2f; const float s = 0.2f;
for (const auto& pt : navigationEditorPoints) { for (const auto& pt : navigationEditorPoints) {
@@ -133,7 +133,7 @@ namespace FRG
PathFinder::ObstaclePolygon poly; PathFinder::ObstaclePolygon poly;
poly.name = "editor_obstacle_" + std::to_string(++navigationEditorObstacleCounter); poly.name = "editor_obstacle_" + std::to_string(++navigationEditorObstacleCounter);
for (const auto& pt : navigationEditorPoints) { for (const auto& pt : navigationEditorPoints) {
poly.polygon.emplace_back(pt.x(), pt.z()); poly.polygon.emplace_back(static_cast<FRG::fp>(pt.x()), static_cast<FRG::fp>(pt.z()));
} }
loc.gameServer_.submit(AddNavigationObstacleCommand{ loc.gameServer_.submit(AddNavigationObstacleCommand{
@@ -146,10 +146,10 @@ namespace FRG
if (!state || state->activeNavigationIndex < 0 || if (!state || state->activeNavigationIndex < 0 ||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return; state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
const float y = const float y =
state->navigationMaps[state->activeNavigationIndex].getFloorY() + 0.02f; static_cast<float>(state->navigationMaps[state->activeNavigationIndex].getFloorY()) + 0.02f;
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f); const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
VertexRenderStruct mesh; VertexRenderStruct mesh;
mesh.data = CreatePolygonFloor(poly.polygon, y, red); mesh.data = CreatePolygonFloor(toEigenVector(poly.polygon), y, red);
mesh.RefreshVBO(); mesh.RefreshVBO();
navigationEditorNavMeshes.push_back(std::move(mesh)); navigationEditorNavMeshes.push_back(std::move(mesh));
} }
@@ -379,9 +379,9 @@ namespace FRG
auto& target = loc.interactiveObjects[selectedInteractiveObjectIndex]; auto& target = loc.interactiveObjects[selectedInteractiveObjectIndex];
target.state.boundsMin = Eigen::Vector3f( target.state.boundsMin = Eigen::Vector3f(
min(localA.x(), localB.x()), 0.f, min(localA.z(), localB.z())); std::min(localA.x(), localB.x()), 0.f, std::min(localA.z(), localB.z()));
target.state.boundsMax = Eigen::Vector3f( target.state.boundsMax = Eigen::Vector3f(
max(localA.x(), localB.x()), 1.f, max(localA.z(), localB.z())); std::max(localA.x(), localB.x()), 1.f, std::max(localA.z(), localB.z()));
boundsClickCount = 0; boundsClickCount = 0;
buildInteractiveObjectBoundsMeshes(); buildInteractiveObjectBoundsMeshes();
@@ -484,9 +484,9 @@ namespace FRG
data.rotationZ = 0.0f; data.rotationZ = 0.0f;
auto playerState = loc.player ? loc.player->stateSnapshot() : nullptr; auto playerState = loc.player ? loc.player->stateSnapshot() : nullptr;
if (!playerState) return; if (!playerState) return;
data.positionX = playerState->position.x(); data.positionX = static_cast<float>(playerState->position.x);
data.positionY = playerState->position.y();//-0.1; data.positionY = static_cast<float>(playerState->position.y);
data.positionZ = playerState->position.z(); data.positionZ = static_cast<float>(playerState->position.z);
data.scale = scaleDist(rng); data.scale = scaleDist(rng);
GameObjectData data2; GameObjectData data2;
+137 -198
View File
@@ -2,6 +2,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <stdexcept> #include <stdexcept>
#include "OrcaSolver.h"
namespace FRG { namespace FRG {
@@ -126,39 +127,9 @@ const CharacterState* LocationGameState::findCharacter(
return nullptr; return nullptr;
} }
std::vector<PathFinder::DynamicObstacle> LocationGameState::collectPathObstacles(
const std::string& requesterId,
const PlayerStateMap& players,
const std::string& forcedObstacleId) const
{
std::vector<std::pair<std::string, const CharacterState*>> characters;
characters.reserve(players.size() + npcs.size());
for (const auto& [id, character] : players)
if (character) characters.emplace_back(id, character.get());
for (const auto& [id, character] : npcs)
if (character) characters.emplace_back(id, character.get());
std::sort(characters.begin(), characters.end(),
[](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; });
const CharacterState* requester = findCharacter(requesterId, players);
const std::string attackTargetId =
requester ? requester->attackTargetId : std::string();
std::vector<PathFinder::DynamicObstacle> obstacles;
obstacles.reserve(characters.size());
for (const auto& [id, character] : characters) {
if (id == requesterId || !character->enabled || character->hp <= 0.f)
continue;
if (id == attackTargetId && id != forcedObstacleId)
continue;
if (character->movedLastTick && id != forcedObstacleId) continue;
obstacles.push_back({character->position, character->collisionRadius});
}
return obstacles;
}
bool LocationGameState::setCharacterDestination( bool LocationGameState::setCharacterDestination(
const std::string& id, const std::string& id,
const Eigen::Vector3f& target, const Vec3fp& target,
std::string callbackName, std::string callbackName,
PlayerStateMap& players, PlayerStateMap& players,
const std::string& forcedObstacleId) const std::string& forcedObstacleId)
@@ -166,54 +137,18 @@ bool LocationGameState::setCharacterDestination(
CharacterState* character = findCharacter(id, players); CharacterState* character = findCharacter(id, players);
if (!character || !location) return false; if (!character || !location) return false;
character->setTarget( character->setTarget(
target, std::move(callbackName), *location, id, target, std::move(callbackName), *location, id);
collectPathObstacles(id, players, forcedObstacleId));
return true; return true;
} }
bool LocationGameState::replanCharacter( bool LocationGameState::replanCharacter(
const std::string& id, const std::string& id,
PlayerStateMap& players, PlayerStateMap& players)
const std::string& forcedObstacleId)
{ {
CharacterState* character = findCharacter(id, players); CharacterState* character = findCharacter(id, players);
if (!character || !location) return false; if (!character || !location) return false;
if (!forcedObstacleId.empty() && !character->isPlayer &&
character->canAttack && character->hp > 0.f) {
const std::string selectedTarget =
selectNpcTarget(*character, players, false);
if (selectedTarget != character->attackTargetId) {
character->stopInPlace();
character->attackTargetId = selectedTarget;
character->battle_state = 0;
character->attack = 0;
}
if (!selectedTarget.empty()) { character->forceReplan(*location, id);
const CharacterState* target = findCharacter(selectedTarget, players);
if (target) {
character->setTarget(
target->position, {}, *location, id,
collectPathObstacles(id, players, forcedObstacleId));
return true;
}
}
character->attackTargetId.clear();
character->battle_state = 0;
character->attack = 0;
const Eigen::Vector3f homeTarget(
character->homePosition.x(), 0.f, character->homePosition.z());
if ((homeTarget - character->position).norm() >
CharacterState::kWalkThreshold) {
character->setTarget(
homeTarget, {}, *location, id,
collectPathObstacles(id, players, forcedObstacleId));
}
return true;
}
character->forceReplan(
*location, id, collectPathObstacles(id, players, forcedObstacleId));
return true; return true;
} }
@@ -236,14 +171,14 @@ std::string LocationGameState::selectNpcTarget(
playerIds.push_back(id); playerIds.push_back(id);
std::sort(playerIds.begin(), playerIds.end()); std::sort(playerIds.begin(), playerIds.end());
const float visibleRangeSquared = npc.visibleRange * npc.visibleRange; const fp visibleRangeSquared = npc.visibleRange * npc.visibleRange;
std::string selectedTarget; std::string selectedTarget;
float nearestDistanceSquared = visibleRangeSquared; fp nearestDistanceSquared = visibleRangeSquared;
for (const std::string& playerId : playerIds) { for (const std::string& playerId : playerIds) {
const Eigen::Vector3f delta = const Vec3fp delta =
players.at(playerId)->position - npc.position; players.at(playerId)->position - npc.position;
const float distanceSquared = const fp distanceSquared =
delta.x() * delta.x() + delta.z() * delta.z(); delta.x * delta.x + delta.z * delta.z;
if (distanceSquared <= nearestDistanceSquared && if (distanceSquared <= nearestDistanceSquared &&
(selectedTarget.empty() || (selectedTarget.empty() ||
distanceSquared < nearestDistanceSquared || distanceSquared < nearestDistanceSquared ||
@@ -367,12 +302,19 @@ LocationSimulationEvents LocationGameState::update(
std::int64_t deltaMs, PlayerStateMap& players, bool interactiveObjectsLocked) std::int64_t deltaMs, PlayerStateMap& players, bool interactiveObjectsLocked)
{ {
LocationSimulationEvents result; LocationSimulationEvents result;
//Taxi cars update:
result.taxiArrived = updateNpcCar(deltaMs); result.taxiArrived = updateNpcCar(deltaMs);
//Interactive objects update:
std::vector<std::string> objectIds; std::vector<std::string> objectIds;
objectIds.reserve(interactiveObjects.size()); objectIds.reserve(interactiveObjects.size());
for (const auto& [id, object] : interactiveObjects) for (const auto& [id, object] : interactiveObjects)
{
if (object) objectIds.push_back(id); if (object) objectIds.push_back(id);
}
std::sort(objectIds.begin(), objectIds.end()); std::sort(objectIds.begin(), objectIds.end());
for (const std::string& id : objectIds) { for (const std::string& id : objectIds) {
InteractiveObjectUpdateEvents events = InteractiveObjectUpdateEvents events =
interactiveObjects.at(id)->update(deltaMs); interactiveObjects.at(id)->update(deltaMs);
@@ -381,35 +323,49 @@ LocationSimulationEvents LocationGameState::update(
{id, std::move(events.onCompleteName), false, {}}); {id, std::move(events.onCompleteName), false, {}});
} }
// All characters observe the same tick-start state. Damage is applied only //Character update:
// after every state has advanced, so vector order cannot suppress an attack. const fp dtSec = static_cast<fp>(deltaMs) / fp(1000);
// 1. Собираем всех персонажей в единый массив и сортируем по ID
// (чтобы даже порядок вызовов CharacterState::update был строго детерминирован!)
struct Entry {
std::string id;
CharacterState* state;
};
std::vector<Entry> allChars;
allChars.reserve(players.size() + npcs.size());
for (auto& [id, ptr] : players) {
if (ptr && ptr->enabled) allChars.push_back({ id, ptr.get() });
}
for (auto& [id, ptr] : npcs) {
if (ptr && ptr->enabled) allChars.push_back({ id, ptr.get() });
}
std::sort(allChars.begin(), allChars.end(), [](const Entry& a, const Entry& b) {
return a.id < b.id;
});
std::unordered_map<std::string, CharacterObservation> observations; std::unordered_map<std::string, CharacterObservation> observations;
for (const auto& [id, player] : players) for (const auto& [id, player] : players)
if (player && player->enabled) if (player && player->enabled)
observations.emplace( observations.emplace(
id, CharacterObservation{player->position, player->hp}); id, CharacterObservation{ player->position, player->hp });
for (const auto& [id, npc] : npcs) for (const auto& [id, npc] : npcs)
if (npc && npc->enabled) if (npc && npc->enabled)
observations.emplace( observations.emplace(
id, CharacterObservation{npc->position, npc->hp}); id, CharacterObservation{ npc->position, npc->hp });
std::vector<std::string> playerIds;
playerIds.reserve(players.size());
for (const auto& [id, player] : players)
if (player) playerIds.push_back(id);
std::sort(playerIds.begin(), playerIds.end());
std::vector<std::string> npcIds;
npcIds.reserve(npcs.size());
for (const auto& [id, npc] : npcs)
if (npc) npcIds.push_back(id);
std::sort(npcIds.begin(), npcIds.end());
// Target selection is authoritative and runs before all characters observe // Target selection is authoritative and runs before all characters observe
// the tick. A valid target is a lock: it is retained until the player dies // the tick. A valid target is a lock: it is retained until the player dies
// or becomes disabled, even when the player leaves visibleRange. // or becomes disabled, even when the player leaves visibleRange.
for (const std::string& id : npcIds) { for (auto& entry : allChars) {
CharacterState& npc = *npcs.at(id); if (entry.state->isPlayer)
{
continue;
}
CharacterState& npc = *entry.state;
if (!npc.enabled || !npc.canAttack || npc.hp <= 0.f) if (!npc.enabled || !npc.canAttack || npc.hp <= 0.f)
continue; continue;
@@ -425,88 +381,105 @@ LocationSimulationEvents LocationGameState::update(
if (selectedTarget.empty()) { if (selectedTarget.empty()) {
npc.battle_state = 0; npc.battle_state = 0;
npc.attack = 0; npc.attack = 0;
const Eigen::Vector3f homeTarget( const Vec3fp homeTarget(
npc.homePosition.x(), 0.f, npc.homePosition.z()); npc.homePosition.x, static_cast<fp>(0.f), npc.homePosition.z);
const Eigen::Vector3f toHome = homeTarget - npc.position; const Vec3fp toHome = homeTarget - npc.position;
if (toHome.norm() > CharacterState::kWalkThreshold && if (toHome.normSquared() > CharacterState::kWalkThreshold_SQUARED() &&
(npc.requestedWalkTarget - homeTarget).norm() > (npc.requestedWalkTarget - homeTarget).normSquared() >
CharacterState::kWalkThreshold) { CharacterState::kWalkThreshold_SQUARED()) {
setCharacterDestination(id, homeTarget, {}, players); setCharacterDestination(entry.id, homeTarget, {}, players);
} }
} }
} }
std::vector<CharacterMovementObstacle> movementObstacles; // 2. ФАЗА 1: Обновляем логику каждого персонажа (вычисляется preferredVelocity)
movementObstacles.reserve(playerIds.size() + npcIds.size()); for (auto& entry : allChars) {
auto addMovementObstacle =
[&movementObstacles](const std::string& id,
const CharacterState& character) {
Eigen::Vector3f direction =
character.getCurrentNavigationTarget() - character.position;
direction.y() = 0.f;
const float length = direction.norm();
if (length > CharacterState::kWalkThreshold) direction /= length;
else direction = Eigen::Vector3f::Zero();
movementObstacles.push_back({
id,
character.position,
character.movementContactRadius,
character.movedLastTick,
direction});
};
for (const std::string& id : playerIds) {
const CharacterState& character = *players.at(id);
if (character.enabled && character.hp > 0.f)
addMovementObstacle(id, character);
}
for (const std::string& id : npcIds) {
const CharacterState& character = *npcs.at(id);
if (character.enabled && character.hp > 0.f)
addMovementObstacle(id, character);
}
// One global ID order also makes blocker selection deterministic.
std::sort(
movementObstacles.begin(), movementObstacles.end(),
[](const CharacterMovementObstacle& lhs,
const CharacterMovementObstacle& rhs) { return lhs.id < rhs.id; });
auto advance = [&](const std::string& id, CharacterState& state) {
CharacterUpdateContext context; CharacterUpdateContext context;
context.characterId = id; context.characterId = entry.id;
context.movementObstacles = &movementObstacles;
auto attack = observations.find(state.attackTargetId);
auto attack = observations.find(entry.state->attackTargetId);
if (attack != observations.end()) context.attackTarget = &attack->second; if (attack != observations.end()) context.attackTarget = &attack->second;
auto face = observations.find(state.faceTargetId); auto face = observations.find(entry.state->faceTargetId);
if (face != observations.end()) context.faceTarget = &face->second; if (face != observations.end()) context.faceTarget = &face->second;
CharacterUpdateEvents events = state.update(deltaMs, context);
auto reservation = std::lower_bound( CharacterUpdateEvents events = entry.state->update(deltaMs, context);
movementObstacles.begin(), movementObstacles.end(), id,
[](const CharacterMovementObstacle& obstacle,
const std::string& value) { return obstacle.id < value; });
if (reservation != movementObstacles.end() && reservation->id == id) {
reservation->position = state.position;
reservation->moving = state.movedLastTick;
Eigen::Vector3f direction =
state.getCurrentNavigationTarget() - state.position;
direction.y() = 0.f;
const float length = direction.norm();
if (length > CharacterState::kWalkThreshold)
reservation->movementDirection = direction / length;
else
reservation->movementDirection = Eigen::Vector3f::Zero();
}
if (!events.arrivedCallbackName.empty() || events.attackImpact || if (!events.arrivedCallbackName.empty() || events.attackImpact ||
events.deathAnimationComplete || events.navigationRequested) { events.deathAnimationComplete || events.navigationRequested) {
result.characters.push_back({id, std::move(events)}); result.characters.push_back({ entry.id, std::move(events) });
}
} }
};
for (const std::string& id : playerIds) std::vector<OrcaAgent> orcaAgents;
if (players.at(id)->enabled) orcaAgents.reserve(allChars.size());
advance(id, *players.at(id));
for (const std::string& id : npcIds) for (auto& entry : allChars) {
if (npcs.at(id)->enabled) CharacterState* st = entry.state;
advance(id, *npcs.at(id)); if (st->hp <= 0.f) continue; // Мертвые не участвуют в коллизиях
const bool wantsToMove = st->preferredVelocity.lengthSq() > fp(0.001f);
const bool inCombat = (st->battle_state == 1);
OrcaAgent agent;
agent.id = entry.id;
agent.state = st;
agent.pos = Vec2fp(st->position.x, st->position.z);
agent.vel = st->velocity;
agent.prefVel = st->preferredVelocity;
agent.radius = st->collisionRadius;
agent.maxSpeed = st->walkSpeed;
agent.isStationary = (!wantsToMove || inCombat);
agent.isGhost = (st->canAttack && !st->isPlayer);
agent.constantSpeed = !agent.isGhost;
orcaAgents.push_back(agent);
}
const PathFinder* activeNavMesh = nullptr;
if (location && location->activeNavigationIndex >= 0 &&
location->activeNavigationIndex < static_cast<int>(location->navigationMaps.size())) {
activeNavMesh = &location->navigationMaps[location->activeNavigationIndex];
}
OrcaSolver::step(orcaAgents, dtSec, activeNavMesh);
// 4. ФАЗА 3: Записываем новые позиции, скорости и обновляем анимацию/поворот
for (const auto& agent : orcaAgents) {
CharacterState* st = agent.state;
const Vec2fp oldPos(st->position.x, st->position.z);
const Vec2fp displacement = agent.pos - oldPos;
const fp movedDistSq = displacement.lengthSq();
st->position.x = agent.pos.x;
st->position.z = agent.pos.y;
st->velocity = agent.newVel;
st->movedLastTick = (movedDistSq > fp(0.0005f));
// Если персонаж хотел идти, проверяем — продвинулся ли он или уперся в глухую пробку
const bool wantedToMove = st->preferredVelocity.lengthSq() > fp(0.01f);
if (wantedToMove) {
if (st->movedLastTick) {
st->movementBlockedTicks = 0;
if (st->currentState == AnimationState::STAND) {
st->currentState = AnimationState::WALK;
st->resetAnim = true;
}
if (agent.newVel.lengthSq() > fp(0.05f)) {
st->targetFacingAngle = fpm::atan2(agent.newVel.x, -agent.newVel.y);
}
}
else {
st->movementBlockedTicks++;
if (st->currentState == AnimationState::WALK) {
st->currentState = AnimationState::STAND;
st->resetAnim = true;
}
}
}
}
for (const CharacterSimulationEvent& source : result.characters) { for (const CharacterSimulationEvent& source : result.characters) {
if (!source.events.attackImpact) continue; if (!source.events.attackImpact) continue;
@@ -520,41 +493,7 @@ LocationSimulationEvents LocationGameState::update(
damage, damage,
target->hp, target->hp,
target->initialHp, target->initialHp,
source.events.attackDirection}); source.events.attackDirection });
}
auto player = players.find("player1");
if (!interactiveObjectsLocked &&
!location->targetInteractiveObjectId.empty() &&
player != players.end() && player->second && player->second->enabled) {
auto target = interactiveObjects.find(location->targetInteractiveObjectId);
const auto object =
target == interactiveObjects.end() ? nullptr : target->second;
if (object && !object->isAnimating) {
const Eigen::Vector3f& approachTarget = object->hasInteractionPosition
? object->interactionPosition : object->position;
if ((player->second->position - approachTarget).norm() <= object->approachRadius) {
result.interactiveObjects.push_back(
{location->targetInteractiveObjectId, {}, true,
object->activateFunctionName});
location->targetInteractiveObjectId.clear();
}
}
}
if (!location->targetInteractNpcId.empty() &&
player != players.end() && player->second && player->second->enabled &&
npcs.count(location->targetInteractNpcId) != 0 &&
npcs.at(location->targetInteractNpcId)) {
const auto& npc = npcs.at(location->targetInteractNpcId);
const float distance =
(player->second->position - npc->position).norm();
if (distance <= NPC_TALK_DISTANCE) {
player->second->stopInPlace();
npc->faceTargetId = "player1";
result.npcInteractionId = location->targetInteractNpcId;
location->targetInteractNpcId.clear();
}
} }
return result; return result;
+3 -8
View File
@@ -27,7 +27,7 @@ struct CharacterDamageEvent {
float amount = 0.f; float amount = 0.f;
float hp = 0.f; float hp = 0.f;
float maxHp = 0.f; float maxHp = 0.f;
Eigen::Vector3f direction = Eigen::Vector3f::Zero(); Vec3fp direction;
}; };
struct InteractiveObjectSimulationEvent { struct InteractiveObjectSimulationEvent {
@@ -64,20 +64,15 @@ struct LocationGameState {
CharacterState* findCharacter(const std::string& id, PlayerStateMap& players); CharacterState* findCharacter(const std::string& id, PlayerStateMap& players);
const CharacterState* findCharacter( const CharacterState* findCharacter(
const std::string& id, const PlayerStateMap& players) const; const std::string& id, const PlayerStateMap& players) const;
std::vector<PathFinder::DynamicObstacle> collectPathObstacles(
const std::string& requesterId,
const PlayerStateMap& players,
const std::string& forcedObstacleId = {}) const;
bool setCharacterDestination( bool setCharacterDestination(
const std::string& id, const std::string& id,
const Eigen::Vector3f& target, const Vec3fp& target,
std::string callbackName, std::string callbackName,
PlayerStateMap& players, PlayerStateMap& players,
const std::string& forcedObstacleId = {}); const std::string& forcedObstacleId = {});
bool replanCharacter( bool replanCharacter(
const std::string& id, const std::string& id,
PlayerStateMap& players, PlayerStateMap& players);
const std::string& forcedObstacleId = {});
void startTaxi(); void startTaxi();
void resetTaxi(); void resetTaxi();
LocationSimulationEvents update( LocationSimulationEvents update(
+2 -2
View File
@@ -123,7 +123,7 @@ void loadSinglePlayerPersistence(
// Entity ownership stays above this navigation-only layer. // Entity ownership stays above this navigation-only layer.
std::vector<Eigen::Vector3f> LocationNetworkState::planPathExceptMyself( /**td::vector<Eigen::Vector3f> LocationNetworkState::planPathExceptMyself(
const Eigen::Vector3f& start, const Eigen::Vector3f& start,
const Eigen::Vector3f& end, const Eigen::Vector3f& end,
const std::string& entityId, const std::string& entityId,
@@ -135,7 +135,7 @@ void loadSinglePlayerPersistence(
return {}; return {};
return navigationMaps[activeNavigationIndex].findPathToNearest( return navigationMaps[activeNavigationIndex].findPathToNearest(
start, end, dynamicObstacles); start, end, dynamicObstacles);
} }*/
void LocationNetworkState::initNavigation( void LocationNetworkState::initNavigation(
const std::vector<std::string>& paths, const std::string& zipFile) const std::vector<std::string>& paths, const std::string& zipFile)
+2 -2
View File
@@ -41,11 +41,11 @@ struct LocationNetworkState : public ISaveable {
//This value is populated from the JSON configs and should not be changed during the regular gameplay. //This value is populated from the JSON configs and should not be changed during the regular gameplay.
//It might be changed only in the editor mode during development. //It might be changed only in the editor mode during development.
std::vector<PathFinder> navigationMaps; std::vector<PathFinder> navigationMaps;
std::vector<Eigen::Vector3f> planPathExceptMyself( /*std::vector<Eigen::Vector3f> planPathExceptMyself(
const Eigen::Vector3f& start, const Eigen::Vector3f& start,
const Eigen::Vector3f& end, const Eigen::Vector3f& end,
const std::string& entityId, const std::string& entityId,
const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles = {}) const; const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles = {}) const;*/
void initNavigation( void initNavigation(
const std::vector<std::string>& paths, const std::vector<std::string>& paths,
const std::string& zipFile = std::string()); const std::string& zipFile = std::string());
+3 -7
View File
@@ -17,10 +17,6 @@
namespace FRG { namespace FRG {
#ifdef __EMSCRIPTEN__
//extern Game* Game::s_instance;
#endif
// Localization // Localization
static const std::string EMPTY_LANGUAGE_RU = u8"(пусто)"; static const std::string EMPTY_LANGUAGE_RU = u8"(пусто)";
static const std::string EMPTY_LANGUAGE_EN = "(empty)"; static const std::string EMPTY_LANGUAGE_EN = "(empty)";
@@ -81,7 +77,7 @@ namespace FRG {
pushWord(currentWord); pushWord(currentWord);
if (!currentLine.empty()) flushLine(); if (!currentLine.empty()) flushLine();
return max(1, lines); return std::max(1, lines);
} }
static std::string trimChatPreview(const std::string& msg, int maxChars = 22) { static std::string trimChatPreview(const std::string& msg, int maxChars = 22) {
@@ -2257,9 +2253,9 @@ namespace FRG {
float MenuManager::ToastEntry::currentAlpha() const { float MenuManager::ToastEntry::currentAlpha() const {
switch (state) { switch (state) {
case State::FadeIn: return min(timer / TOAST_FADE_MS, 1.0f); case State::FadeIn: return std::min(timer / TOAST_FADE_MS, 1.0f);
case State::Visible: return 1.0f; case State::Visible: return 1.0f;
case State::FadeOut: return max(1.0f - timer / TOAST_FADE_MS, 0.0f); case State::FadeOut: return std::max(1.0f - timer / TOAST_FADE_MS, 0.0f);
} }
return 0.0f; return 0.0f;
} }
+419
View File
@@ -0,0 +1,419 @@
#pragma once
#include "navigation/PathFinder.h"
#include <algorithm>
#include <string>
#include <vector>
namespace FRG {
// Линейное ограничение (полуплоскость допустимых скоростей ORCA)
struct OrcaLine {
Vec2fp point; // Точка на граничной прямой
Vec2fp direction; // Единичный вектор направления прямой (допустимая полуплоскость СЛЕВА)
};
struct OrcaAgent {
std::string id;
CharacterState* state = nullptr;
Vec2fp pos;
Vec2fp vel;
Vec2fp prefVel;
fp radius;
fp maxSpeed;
bool isStationary = false;
bool isGhost = false; // Враги-духи (парят в воздухе, легко расталкиваются)
bool constantSpeed = true; // Люди (ходят только с полной скоростью либо стоят)
Vec2fp newVel;
};
class OrcaSolver {
public:
// Горизонт упреждения 0.6 сек (~1.8 метра при скорости 3 м/с)
static fp timeHorizon() { return fp(0.6f); }
static void step(
std::vector<OrcaAgent>& agents,
fp dtSec,
const PathFinder* navMesh)
{
if (dtSec <= fp(0) || agents.empty()) return;
// 1. Обязательная сортировка по ID для 100% детерминизма Lockstep
std::sort(agents.begin(), agents.end(), [](const OrcaAgent& a, const OrcaAgent& b) {
return a.id < b.id;
});
const fp invTimeHorizon = fp(1) / timeHorizon();
const fp invDt = fp(1) / dtSec;
const fp neighborDist = fp(4.0f);
const fp neighborDistSq = neighborDist * neighborDist;
std::vector<OrcaLine> orcaLines;
orcaLines.reserve(16);
// 2. Для каждого агента строим линии ORCA относительно соседей
for (std::size_t i = 0; i < agents.size(); ++i) {
OrcaAgent& agent = agents[i];
if (agent.maxSpeed <= fp(0) || agent.prefVel.lengthSq() <= fp(0.001f)) {
agent.newVel = Vec2fp(fp(0), fp(0));
continue;
}
orcaLines.clear();
// ВАЖНО: Если человек стартует с места, используем prefVel как базу для построения VO,
// чтобы стоящие рядом NPC не запирали его в нулевой скорости!
const Vec2fp effectiveAgentVel = (agent.constantSpeed && agent.vel.lengthSq() < fp(0.1f))
? agent.prefVel
: agent.vel;
for (std::size_t j = 0; j < agents.size(); ++j) {
if (i == j) continue;
const OrcaAgent& other = agents[j];
// Человек НЕ строит тормозящих линий ORCA об духов —
// духи сами расступаются перед человеком и расталкиваются в resolveHardCollisions!
if (!agent.isGhost && other.isGhost) {
continue;
}
const Vec2fp relativePos = other.pos - agent.pos;
const fp distSq = relativePos.lengthSq();
if (distSq > neighborDistSq) continue;
const Vec2fp relativeVel = effectiveAgentVel - other.vel;
// Для проверки конуса обхода используем чуть меньший радиус (85%),
// чтобы персонажи легче протискивались мимо друг друга
const fp combinedRadius = (agent.radius + other.radius) * fp(0.85f);
const fp combinedRadiusSq = combinedRadius * combinedRadius;
OrcaLine line;
Vec2fp u;
if (distSq > combinedRadiusSq) {
const Vec2fp w = relativeVel - relativePos * invTimeHorizon;
const fp wLengthSq = w.lengthSq();
const fp dotProduct1 = w.dot(relativePos);
if (dotProduct1 < fp(0) && dotProduct1 * dotProduct1 > combinedRadiusSq * wLengthSq) {
const fp wLength = fpm::sqrt(wLengthSq);
const Vec2fp unitW = wLength > fp(0.001f) ? (w / wLength) : Vec2fp(fp(1), fp(0));
line.direction = Vec2fp(unitW.y, -unitW.x);
u = unitW * (combinedRadius * invTimeHorizon - wLength);
}
else {
const fp leg = fpm::sqrt(std::max(fp(0), distSq - combinedRadiusSq));
if (relativePos.cross(w) > fp(0)) {
line.direction = Vec2fp(
relativePos.x * leg - relativePos.y * combinedRadius,
relativePos.x * combinedRadius + relativePos.y * leg
) / distSq;
}
else {
line.direction = Vec2fp(
-(relativePos.x * leg + relativePos.y * combinedRadius),
-(-relativePos.x * combinedRadius + relativePos.y * leg)
) / distSq;
}
const fp dotProduct2 = relativeVel.dot(line.direction);
u = line.direction * dotProduct2 - relativeVel;
}
}
else {
const Vec2fp w = relativeVel - relativePos * invDt;
const fp wLength = w.length();
const Vec2fp unitW = wLength > fp(0.001f)
? (w / wLength)
: ((i < j) ? Vec2fp(fp(1), fp(0)) : Vec2fp(fp(-1), fp(0)));
line.direction = Vec2fp(unitW.y, -unitW.x);
u = unitW * (combinedRadius * invDt - wLength);
}
fp responsibility = fp(0.5f);
if (agent.isGhost && !other.isGhost) {
responsibility = fp(0.95f); // Дух берет 95% уклонения от человека
}
else if (other.isStationary && !agent.isStationary) {
responsibility = fp(1.0f); // Обходящий берет 100% маневра мимо стоящего
}
else if (!other.isStationary && agent.isStationary) {
responsibility = fp(0.0f);
}
line.point = effectiveAgentVel + u * responsibility;
orcaLines.push_back(line);
}
// Решаем стандартную задачу ORCA
Vec2fp resultVel = agent.prefVel;
int lineFail = linearProgram2(orcaLines, agent.maxSpeed, agent.prefVel, false, resultVel);
if (lineFail < static_cast<int>(orcaLines.size())) {
linearProgram3(orcaLines, lineFail, agent.maxSpeed, resultVel);
}
// Если это человек (constantSpeed == true), ищем угол обхода с сохранением полной скорости:
const fp prefSpeedSq = agent.prefVel.lengthSq();
if (agent.constantSpeed && prefSpeedSq > fp(0.01f)) {
const fp prefSpeed = fpm::sqrt(prefSpeedSq);
resultVel = findConstantSpeedVelocity(orcaLines, agent.prefVel, prefSpeed, resultVel);
}
agent.newVel = resultVel;
}
// 3. Применяем скорости и корректируем жесткие наложения
for (auto& agent : agents) {
agent.pos = agent.pos + agent.newVel * dtSec;
}
resolveHardCollisions(agents);
// 4. Удерживаем всех внутри NavMesh
if (navMesh && navMesh->isReady()) {
for (auto& agent : agents) {
Vec3fp pt3D(agent.pos.x, navMesh->getFloorY(), agent.pos.y);
if (!navMesh->isWalkable(pt3D)) {
int dummyTri = -1;
agent.pos = navMesh->clampToNavMesh(agent.pos, dummyTri);
}
}
}
}
private:
// Подбирает вектор скорости строго длины prefSpeed, который огибает препятствие без замедления
static Vec2fp findConstantSpeedVelocity(
const std::vector<OrcaLine>& orcaLines,
const Vec2fp& prefVel,
fp prefSpeed,
const Vec2fp& orcaVel)
{
if (orcaLines.empty()) return prefVel;
const Vec2fp forwardDir = prefVel / prefSpeed;
// Перпендикуляр для отступа влево/вправо при лобовом столкновении
const Vec2fp rightDir(forwardDir.y, -forwardDir.x);
// Перебираем веер углов отклонения от прямого курса (0°, ±15°, ±30°, ±45°, ±60°, ±75°, ±90°)
// Синусы и косинусы заданы константами для скорости и детерминизма
struct AngleOffset { fp cosA; fp sinA; };
static const AngleOffset offsets[] = {
{ fp(1.000f), fp(0.000f) },
{ fp(0.966f), fp(0.259f) }, { fp(0.966f), fp(-0.259f) }, // ±15°
{ fp(0.866f), fp(0.500f) }, { fp(0.866f), fp(-0.500f) }, // ±30°
{ fp(0.707f), fp(0.707f) }, { fp(0.707f), fp(-0.707f) }, // ±45°
{ fp(0.500f), fp(0.866f) }, { fp(0.500f), fp(-0.866f) }, // ±60°
{ fp(0.259f), fp(0.966f) }, { fp(0.259f), fp(-0.966f) }, // ±75°
{ fp(0.000f), fp(1.000f) }, { fp(0.000f), fp(-1.000f) } // ±90°
};
// Определяем, в какую сторону (влево или вправо) нам выгоднее обходить по подсказке от ORCA
const fp sideHint = rightDir.dot(orcaVel);
for (std::size_t k = 0; k < sizeof(offsets) / sizeof(offsets[0]); ++k) {
// Если k нечетный (пара ±угол), проверяем сначала ту сторону, куда склоняется ORCA
std::size_t idx = k;
if (k >= 1 && (k % 2 == 1) && sideHint < fp(0)) {
idx = k + 1;
}
else if (k >= 1 && (k % 2 == 0) && sideHint < fp(0)) {
idx = k - 1;
}
const Vec2fp candidateDir = forwardDir * offsets[idx].cosA + rightDir * offsets[idx].sinA;
const Vec2fp candidateVel = candidateDir * prefSpeed;
bool valid = true;
for (const auto& line : orcaLines) {
// Допускаем погрешность 0.1f м/с
if (line.direction.cross(line.point - candidateVel) > fp(0.1f)) {
valid = false;
break;
}
}
if (valid) {
return candidateVel;
}
}
// Если ни один угол до ±90° не свободен (проход полностью заблокирован вплотную) — стоим
return Vec2fp(fp(0), fp(0));
}
static void resolveHardCollisions(std::vector<OrcaAgent>& agents) {
const std::size_t n = agents.size();
for (int iter = 0; iter < 2; ++iter) {
for (std::size_t i = 0; i < n; ++i) {
for (std::size_t j = i + 1; j < n; ++j) {
Vec2fp delta = agents[j].pos - agents[i].pos;
fp distSq = delta.lengthSq();
fp minDist = agents[i].radius + agents[j].radius;
if (distSq < minDist * minDist) {
fp dist = fpm::sqrt(distSq);
Vec2fp normal = (dist > fp(0.001f))
? (delta / dist)
: Vec2fp(fp(1), fp(0));
fp penetration = (minDist - dist) * fp(0.5f);
fp weightI = fp(0.5f);
fp weightJ = fp(0.5f);
if (!agents[i].isGhost && agents[j].isGhost) {
weightI = fp(0.0f); weightJ = fp(1.0f); // Человек на 100% выталкивает духа
}
else if (agents[i].isGhost && !agents[j].isGhost) {
weightI = fp(1.0f); weightJ = fp(0.0f);
}
else {
weightI = agents[i].isStationary ? fp(0.1f) : fp(0.5f);
weightJ = agents[j].isStationary ? fp(0.1f) : fp(0.5f);
fp sumW = weightI + weightJ;
weightI /= sumW;
weightJ /= sumW;
}
agents[i].pos = agents[i].pos - normal * (penetration * weightI);
agents[j].pos = agents[j].pos + normal * (penetration * weightJ);
}
}
}
}
}
static bool linearProgram1(
const std::vector<OrcaLine>& lines,
int lineNo,
fp radius,
const Vec2fp& optVelocity,
bool directionOpt,
Vec2fp& result)
{
const fp dotProduct = lines[lineNo].point.dot(lines[lineNo].direction);
const fp discriminant = dotProduct * dotProduct + radius * radius - lines[lineNo].point.lengthSq();
if (discriminant < fp(0)) {
return false;
}
const fp sqrtDiscriminant = fpm::sqrt(discriminant);
fp tLeft = -dotProduct - sqrtDiscriminant;
fp tRight = -dotProduct + sqrtDiscriminant;
for (int i = 0; i < lineNo; ++i) {
const fp denominator = lines[lineNo].direction.cross(lines[i].direction);
const fp numerator = lines[i].direction.cross(lines[lineNo].point - lines[i].point);
if (fpm::abs(denominator) <= fp(0.001f)) {
if (numerator < fp(0)) return false;
continue;
}
const fp t = numerator / denominator;
if (denominator >= fp(0)) {
tRight = std::min(tRight, t);
}
else {
tLeft = std::max(tLeft, t);
}
if (tLeft > tRight) return false;
}
if (directionOpt) {
if (optVelocity.dot(lines[lineNo].direction) > fp(0)) {
result = lines[lineNo].point + lines[lineNo].direction * tRight;
}
else {
result = lines[lineNo].point + lines[lineNo].direction * tLeft;
}
}
else {
const fp t = lines[lineNo].direction.dot(optVelocity - lines[lineNo].point);
const fp tClamped = std::max(tLeft, std::min(tRight, t));
result = lines[lineNo].point + lines[lineNo].direction * tClamped;
}
return true;
}
static int linearProgram2(
const std::vector<OrcaLine>& lines,
fp radius,
const Vec2fp& optVelocity,
bool directionOpt,
Vec2fp& result)
{
if (directionOpt) {
result = optVelocity * radius;
}
else if (optVelocity.lengthSq() > radius * radius) {
result = (optVelocity / optVelocity.length()) * radius;
}
else {
result = optVelocity;
}
for (int i = 0; i < static_cast<int>(lines.size()); ++i) {
if (lines[i].direction.cross(lines[i].point - result) > fp(0)) {
const Vec2fp tempResult = result;
if (!linearProgram1(lines, i, radius, optVelocity, directionOpt, result)) {
result = tempResult;
return i;
}
}
}
return static_cast<int>(lines.size());
}
static void linearProgram3(
const std::vector<OrcaLine>& lines,
int beginLine,
fp radius,
Vec2fp& result)
{
fp distance = fp(0);
for (int i = beginLine; i < static_cast<int>(lines.size()); ++i) {
if (lines[i].direction.cross(lines[i].point - result) > distance) {
std::vector<OrcaLine> projLines;
projLines.reserve(i);
for (int j = 0; j < i; ++j) {
OrcaLine line;
fp determinant = lines[i].direction.cross(lines[j].direction);
if (fpm::abs(determinant) <= fp(0.001f)) {
if (lines[i].direction.dot(lines[j].direction) > fp(0)) continue;
line.point = (lines[i].point + lines[j].point) * fp(0.5f);
}
else {
line.point = lines[i].point + lines[i].direction *
(lines[j].direction.cross(lines[i].point - lines[j].point) / determinant);
}
Vec2fp dirDiff = lines[j].direction - lines[i].direction;
fp dirLen = dirDiff.length();
if (dirLen <= fp(0.001f)) continue;
line.direction = dirDiff / dirLen;
projLines.push_back(line);
}
const Vec2fp tempResult = result;
const Vec2fp optDir(-lines[i].direction.y, lines[i].direction.x);
if (linearProgram2(projLines, radius, optDir, true, result) < static_cast<int>(projLines.size())) {
result = tempResult;
}
distance = lines[i].direction.cross(lines[i].point - result);
}
}
}
};
} // namespace FRG
+19 -14
View File
@@ -13,6 +13,9 @@
namespace FRG { namespace FRG {
constexpr float kPi = 3.14159265358979323846f;
namespace { namespace {
// Reverse-looks-up the global name a Lua function is registered under, by // Reverse-looks-up the global name a Lua function is registered under, by
@@ -126,9 +129,9 @@ namespace FRG {
std::cerr << "[script] npc_walk_to: on_arrived must be a global Lua function\n"; std::cerr << "[script] npc_walk_to: on_arrived must be a global Lua function\n";
} }
loc->gameServer().submit(SetCharacterHomePositionCommand{ loc->gameServer().submit(SetCharacterHomePositionCommand{
loc->locationId(), npc->entityId(), Eigen::Vector3f(x, 0.f, z)}); loc->locationId(), npc->entityId(), Vec3fp(static_cast<fp>(x), static_cast<fp>(0.f), static_cast<fp>(z))});
loc->setCharacterTarget( loc->setCharacterTarget(
*npc, Eigen::Vector3f(x, y, z), std::move(cbName)); *npc, Vec3fp(static_cast<fp>(x), static_cast<fp>(y), static_cast<fp>(z)), std::move(cbName));
}); });
@@ -140,16 +143,16 @@ namespace FRG {
return; return;
} }
const float rad = angle * static_cast<float>(M_PI) / 180.f; const fp rad = static_cast<fp>(angle) * static_cast<fp>(kPi) / static_cast < fp>(180.f);
logger() << "[script] npc_rotate_to: " << npcId << " angle " << angle << " degrees (" << rad << " radians)" << std::endl; logger() << "[script] npc_rotate_to: " << npcId << " angle " << angle << " degrees (" << static_cast<float>(rad) << " radians)" << std::endl;
loc->gameServer().submit(SetCharacterFacingAngleCommand{ loc->gameServer().submit(SetCharacterFacingAngleCommand{
loc->locationId(), npc->entityId(), rad, false}); loc->locationId(), npc->entityId(), rad, false});
}); });
api.set_function("player_rotate_to", api.set_function("player_rotate_to",
[loc](float angle) { [loc](float angle) {
const float rad = angle * static_cast<float>(M_PI) / 180.f; const fp rad = static_cast<fp>(angle) * static_cast<fp>(kPi) / static_cast <fp>(180.f);
logger() << "[script] player_rotate_to: angle " << angle << " degrees (" << rad << " radians)" << std::endl; logger() << "[script] player_rotate_to: angle " << angle << " degrees (" << static_cast<float>(rad) << " radians)" << std::endl;
loc->gameServer().submit(SetCharacterFacingAngleCommand{ loc->gameServer().submit(SetCharacterFacingAngleCommand{
loc->locationId(), loc->player->entityId(), rad, false}); loc->locationId(), loc->player->entityId(), rad, false});
}); });
@@ -188,9 +191,9 @@ namespace FRG {
} }
loc->gameServer().submit(SetCharacterHomePositionCommand{ loc->gameServer().submit(SetCharacterHomePositionCommand{
loc->locationId(), loc->player->entityId(), loc->locationId(), loc->player->entityId(),
Eigen::Vector3f(x, 0.f, z)}); Vec3fp(static_cast<fp>(x), static_cast<fp>(0.f), static_cast<fp>(z))});
loc->setCharacterTarget( loc->setCharacterTarget(
*loc->player, Eigen::Vector3f(x, y, z), std::move(cbName)); *loc->player, Vec3fp(static_cast<fp>(x), static_cast<fp>(y), static_cast<fp>(z)), std::move(cbName));
}); });
@@ -424,12 +427,12 @@ namespace FRG {
api.set_function("get_player_x", api.set_function("get_player_x",
[loc]() -> float { [loc]() -> float {
auto state = loc->player ? loc->player->stateSnapshot() : nullptr; auto state = loc->player ? loc->player->stateSnapshot() : nullptr;
return state ? state->position.x() : 0.0f; return state ? static_cast<float>(state->position.x) : 0.0f;
}); });
api.set_function("get_player_z", api.set_function("get_player_z",
[loc]() -> float { [loc]() -> float {
auto state = loc->player ? loc->player->stateSnapshot() : nullptr; auto state = loc->player ? loc->player->stateSnapshot() : nullptr;
return state ? state->position.z() : 0.0f; return state ? static_cast<float>(state->position.z) : 0.0f;
}); });
api.set_function("refill_current_player_hp", api.set_function("refill_current_player_hp",
@@ -593,7 +596,7 @@ namespace FRG {
std::cerr << "[script] npc_set_position: unknown NPC id '" << npcId << "'\n"; std::cerr << "[script] npc_set_position: unknown NPC id '" << npcId << "'\n";
return; return;
} }
Eigen::Vector3f pos(x, y, z); Vec3fp pos(static_cast<fp>(x), static_cast<fp>(y), static_cast<fp>(z));
loc->gameServer().submit(SetCharacterPositionCommand{ loc->gameServer().submit(SetCharacterPositionCommand{
loc->locationId(), npc->entityId(), pos, true}); loc->locationId(), npc->entityId(), pos, true});
loc->setCharacterTarget(*npc, pos); loc->setCharacterTarget(*npc, pos);
@@ -607,7 +610,8 @@ namespace FRG {
std::cerr << "[script] npc_set_rotation: unknown NPC id '" << npcId << "'\n"; std::cerr << "[script] npc_set_rotation: unknown NPC id '" << npcId << "'\n";
return; return;
} }
const float rad = angle * static_cast<float>(M_PI) / 180.f; const fp rad = static_cast<fp>(angle) * static_cast<fp>(kPi) / static_cast <fp>(180.f);
loc->gameServer().submit(SetCharacterFacingAngleCommand{ loc->gameServer().submit(SetCharacterFacingAngleCommand{
loc->locationId(), npc->entityId(), rad, true}); loc->locationId(), npc->entityId(), rad, true});
}); });
@@ -626,7 +630,7 @@ namespace FRG {
api.set_function("player_set_position", api.set_function("player_set_position",
[loc](float x, float y, float z) { [loc](float x, float y, float z) {
Eigen::Vector3f pos(x, y, z); Vec3fp pos(static_cast<fp>(x), static_cast<fp>(y), static_cast<fp>(z));
loc->gameServer().submit(SetCharacterPositionCommand{ loc->gameServer().submit(SetCharacterPositionCommand{
loc->locationId(), loc->player->entityId(), pos, true}); loc->locationId(), loc->player->entityId(), pos, true});
loc->setCharacterTarget(*loc->player, pos); loc->setCharacterTarget(*loc->player, pos);
@@ -634,7 +638,8 @@ namespace FRG {
api.set_function("player_set_rotation", api.set_function("player_set_rotation",
[loc](float angle) { [loc](float angle) {
const float rad = angle * static_cast<float>(M_PI) / 180.f; const fp rad = static_cast<fp>(angle) * static_cast<fp>(kPi) / static_cast <fp>(180.f);
loc->gameServer().submit(SetCharacterFacingAngleCommand{ loc->gameServer().submit(SetCharacterFacingAngleCommand{
loc->locationId(), loc->player->entityId(), rad, true}); loc->locationId(), loc->player->entityId(), rad, true});
}); });
+16 -13
View File
@@ -6,6 +6,7 @@
#include <cstring> #include <cstring>
#include <stdexcept> #include <stdexcept>
#include <unordered_set> #include <unordered_set>
#include <math.h>
namespace FRG { namespace FRG {
namespace { namespace {
@@ -172,7 +173,7 @@ CharacterState NpcData::toCharacterState() const
CharacterState state; CharacterState state;
state.npcName = name; state.npcName = name;
state.position = {positionX, positionY, positionZ}; state.position = {positionX, positionY, positionZ};
state.homePosition = {positionX, 0.f, positionZ}; state.homePosition = {positionX, static_cast<fp>(0.f), positionZ};
state.stopInPlace(); state.stopInPlace();
state.facingAngle = facingAngle; state.facingAngle = facingAngle;
state.walkSpeed = walkSpeed; state.walkSpeed = walkSpeed;
@@ -183,9 +184,9 @@ CharacterState NpcData::toCharacterState() const
state.visibleRange = visibleRange; state.visibleRange = visibleRange;
state.canAttack = canAttack; state.canAttack = canAttack;
state.enabled = enabled; state.enabled = enabled;
state.collisionRadius = navigationAvoidanceRadius; //state.collisionRadius = navigationAvoidanceRadius;
state.movementContactRadius = //state.movementContactRadius =
(std::min)(movementContactRadius, navigationAvoidanceRadius); // (std::min)(movementContactRadius, navigationAvoidanceRadius);
state.interactionRadius = interactionRadius; state.interactionRadius = interactionRadius;
state.configureNavigationRetryCooldowns( state.configureNavigationRetryCooldowns(
id, navigationRetryCooldownTicks); id, navigationRetryCooldownTicks);
@@ -309,8 +310,8 @@ LocationDefinition LocationContentLoader::load(
data.meshTextures[name] = path.get<std::string>(); data.meshTextures[name] = path.get<std::string>();
} }
} }
data.interactionRadius = data.interactionRadius = static_cast<fp>(
item.value("interactionRadius", 0.f); item.value<double>("interactionRadius", 0.0));
data.animationIdlePath = data.animationIdlePath =
item.value("animationIdlePath", ""); item.value("animationIdlePath", "");
data.animationWalkPath = data.animationWalkPath =
@@ -327,11 +328,11 @@ LocationDefinition LocationContentLoader::load(
item.value("animationActionToDeathPath", ""); item.value("animationActionToDeathPath", "");
data.animationDeathIdlePath = data.animationDeathIdlePath =
item.value("animationDeathIdlePath", ""); item.value("animationDeathIdlePath", "");
data.positionX = item.value("positionX", 0.f); data.positionX = static_cast<fp>(item.value<double>("positionX", 0.0));
data.positionY = item.value("positionY", 0.f); data.positionY = static_cast<fp>(item.value<double>("positionY", 0.0));
data.positionZ = item.value("positionZ", 0.f); data.positionZ = static_cast<fp>(item.value<double>("positionZ", 0.0));
data.walkSpeed = item.value("walkSpeed", 1.5f); data.walkSpeed = static_cast<fp>(item.value<double>("walkSpeed", 1.5));
data.rotationSpeed = item.value("rotationSpeed", 8.f); data.rotationSpeed = static_cast<fp>(item.value<double>("rotationSpeed", 8.0));
data.modelScale = item.value("modelScale", 0.01f); data.modelScale = item.value("modelScale", 0.01f);
const float degrees = kPi / 180.f; const float degrees = kPi / 180.f;
data.modelCorrectionRotX = data.modelCorrectionRotX =
@@ -341,12 +342,13 @@ LocationDefinition LocationContentLoader::load(
data.modelCorrectionRotZ = data.modelCorrectionRotZ =
item.value("modelCorrectionRotZ", 0.f) * degrees; item.value("modelCorrectionRotZ", 0.f) * degrees;
data.facingAngle = data.facingAngle =
item.value("facingAngle", 0.f) * degrees; static_cast<fp>(item.value<double>("facingAngle", 0.0)) * static_cast<fp>(kPi) / static_cast<fp>(180.0);
data.hp = item.value("hp", 100.f); data.hp = item.value("hp", 100.f);
data.visibleRange = data.visibleRange =
item.value("visibleRange", VISIBLE_RANGE); static_cast<fp>(item.value<double>("visibleRange", static_cast<double>(VISIBLE_RANGE)));
data.canAttack = item.value("canAttack", false); data.canAttack = item.value("canAttack", false);
data.enabled = item.value("enabled", true); data.enabled = item.value("enabled", true);
/*
data.navigationAvoidanceRadius = item.value( data.navigationAvoidanceRadius = item.value(
"navigationAvoidanceRadius", "navigationAvoidanceRadius",
item.value("collisionRadius", 0.45f)); item.value("collisionRadius", 0.45f));
@@ -359,6 +361,7 @@ LocationDefinition LocationContentLoader::load(
0.05f, (std::min)( 0.05f, (std::min)(
data.movementContactRadius, data.movementContactRadius,
data.navigationAvoidanceRadius)); data.navigationAvoidanceRadius));
*/
const char* cooldownKey = const char* cooldownKey =
item.contains("navigationRetryCooldownTicks") item.contains("navigationRetryCooldownTicks")
? "navigationRetryCooldownTicks" ? "navigationRetryCooldownTicks"
+10 -10
View File
@@ -58,23 +58,23 @@ struct NpcData {
std::string animationActionToStandPath; std::string animationActionToStandPath;
std::string animationActionToDeathPath; std::string animationActionToDeathPath;
std::string animationDeathIdlePath; std::string animationDeathIdlePath;
float positionX = 0.f; fp positionX = static_cast<fp>(0.f);
float positionY = 0.f; fp positionY = static_cast<fp>(0.f);
float positionZ = 0.f; fp positionZ = static_cast<fp>(0.f);
float walkSpeed = 1.5f; fp walkSpeed = static_cast<fp>(1.5f);
float rotationSpeed = 8.f; fp rotationSpeed = static_cast<fp>(8.f);
float modelScale = 0.01f; float modelScale = 0.01f;
float modelCorrectionRotX = 0.f; float modelCorrectionRotX = 0.f;
float modelCorrectionRotY = 0.f; float modelCorrectionRotY = 0.f;
float modelCorrectionRotZ = 0.f; float modelCorrectionRotZ = 0.f;
float facingAngle = 0.f; fp facingAngle = static_cast<fp>(0.f);
float hp = 100.f; float hp = 100.f;
float visibleRange = VISIBLE_RANGE; fp visibleRange = static_cast<fp>(VISIBLE_RANGE);
bool canAttack = false; bool canAttack = false;
bool enabled = true; bool enabled = true;
float navigationAvoidanceRadius = 0.45f; //fp navigationAvoidanceRadius = static_cast<fp>(0.45f);
float movementContactRadius = 0.32f; //fp movementContactRadius = static_cast<fp>(0.32f);
float interactionRadius = 0.f; fp interactionRadius = static_cast<fp>(0.f);
std::vector<int> navigationRetryCooldownTicks; std::vector<int> navigationRetryCooldownTicks;
CharacterState toCharacterState() const; CharacterState toCharacterState() const;
+6 -5
View File
@@ -7,6 +7,7 @@
#include <string> #include <string>
#include <variant> #include <variant>
#include <vector> #include <vector>
#include "../navigation/PathFinder.h"
namespace FRG { namespace FRG {
@@ -110,7 +111,7 @@ struct SetNavigationAreaAvailableCommand {
struct AddNavigationObstacleCommand { struct AddNavigationObstacleCommand {
std::string locationId; std::string locationId;
std::string name; std::string name;
std::vector<Eigen::Vector2f> polygon; std::vector<Vec2fp> polygon;
}; };
struct AddInventoryItemCommand { std::string itemId; }; struct AddInventoryItemCommand { std::string itemId; };
struct RemoveInventoryItemCommand { std::string itemId; }; struct RemoveInventoryItemCommand { std::string itemId; };
@@ -136,19 +137,19 @@ struct SetGlobalFloatCommand { std::string key; float value; };
struct SetCharacterPositionCommand { struct SetCharacterPositionCommand {
std::string locationId; std::string locationId;
std::string characterId; std::string characterId;
Eigen::Vector3f position; Vec3fp position;
bool updateHomePosition = false; bool updateHomePosition = false;
}; };
struct SetCharacterFacingAngleCommand { struct SetCharacterFacingAngleCommand {
std::string locationId; std::string locationId;
std::string characterId; std::string characterId;
float angle; fp angle;
bool setCurrentAngle = false; bool setCurrentAngle = false;
}; };
struct SetCharacterHomePositionCommand { struct SetCharacterHomePositionCommand {
std::string locationId; std::string locationId;
std::string characterId; std::string characterId;
Eigen::Vector3f position; Vec3fp position;
}; };
struct SetCharacterTargetCommand { struct SetCharacterTargetCommand {
std::string locationId; std::string locationId;
@@ -172,7 +173,7 @@ struct ClearCharacterAttackTargetCommand {
struct SetCharacterDestinationCommand { struct SetCharacterDestinationCommand {
std::string locationId; std::string locationId;
std::string characterId; std::string characterId;
Eigen::Vector3f target; Vec3fp target;
std::string callbackName; std::string callbackName;
}; };
struct StopCharacterCommand { struct StopCharacterCommand {
+3 -4
View File
@@ -70,11 +70,11 @@ void InternalGameServer::updateBattleZoneTestBot(std::int64_t deltaMs)
battleZoneTestBotElapsedMs_ %= kBattleZoneTestBotIntervalMs; battleZoneTestBotElapsedMs_ %= kBattleZoneTestBotIntervalMs;
static std::mt19937 randomEngine(std::random_device{}()); static std::mt19937 randomEngine(std::random_device{}());
static std::uniform_real_distribution<float> coordinate(-10.0f, 10.0f); static std::uniform_int_distribution<int> coordinate(-100, 100); //Divide by 10 then.
submit(SetCharacterDestinationCommand{ submit(SetCharacterDestinationCommand{
state_.currentLocationName, state_.currentLocationName,
kBattleZonePlayer2Id, kBattleZonePlayer2Id,
Eigen::Vector3f(coordinate(randomEngine), 0.f, coordinate(randomEngine)), Vec3fp(static_cast<fp>(coordinate(randomEngine)) / static_cast<fp>(10), static_cast<fp>(0.f), coordinate(randomEngine) / static_cast<fp>(10)),
std::string()}); std::string()});
} }
@@ -520,8 +520,7 @@ void InternalGameServer::update(std::int64_t deltaMs)
if (!event.events.navigationRequested) continue; if (!event.events.navigationRequested) continue;
if (event.events.forceNavigationReplan) if (event.events.forceNavigationReplan)
stored->second.replanCharacter( stored->second.replanCharacter(
event.characterId, state_.players, event.characterId, state_.players);
event.events.blockingCharacterId);
else else
stored->second.setCharacterDestination( stored->second.setCharacterDestination(
event.characterId, event.events.navigationTarget, {}, event.characterId, event.events.navigationTarget, {},
+43 -5
View File
@@ -91,6 +91,44 @@ bool readTarget(
return true; return true;
} }
bool readTargetFp(
const nlohmann::json& value, FRG::Vec3fp& target, std::string& error)
{
if (!value.is_array() || value.size() != 3) {
error = "target must contain three coordinates";
return false;
}
FRG::fp coords[3];
for (std::size_t index = 0; index < 3; ++index) {
if (!value[index].is_number()) {
error = "target coordinates must be numbers";
return false;
}
const double coordinate = value[index].get<double>();
if (!std::isfinite(coordinate) || std::abs(coordinate) > 100000.0) {
error = "target coordinate is not finite or is out of range";
return false;
}
// Округляем до сотых в double и конвертируем в Q20.12 (fp)
coords[index] = FRG::fp(coordinate);
}
target.x = coords[0];
target.y = coords[1];
target.z = coords[2];
return true;
}
nlohmann::json targetJsonFp(const FRG::Vec3fp& target)
{
return nlohmann::json::array({
static_cast<double>(target.x),
static_cast<double>(target.y),
static_cast<double>(target.z)
});
}
nlohmann::json targetJson(const Eigen::Vector3f& target) nlohmann::json targetJson(const Eigen::Vector3f& target)
{ {
return nlohmann::json::array({target.x(), target.y(), target.z()}); return nlohmann::json::array({target.x(), target.y(), target.z()});
@@ -161,7 +199,7 @@ bool parseInitialStateMessage(
} }
std::uint64_t LockstepTimeline::schedule( std::uint64_t LockstepTimeline::schedule(
const std::string& playerId, const Eigen::Vector3f& target) const std::string& playerId, const Vec3fp& target)
{ {
const std::uint64_t sequence = nextCommandSequence_++; const std::uint64_t sequence = nextCommandSequence_++;
pending_[currentTick_ + inputDelayTicks_].push_back( pending_[currentTick_ + inputDelayTicks_].push_back(
@@ -206,7 +244,7 @@ std::string makeDestinationInputMessage(const DestinationInputMessage& message)
{ {
return envelope("destination_input", { return envelope("destination_input", {
{"clientSequence", message.clientSequence}, {"clientSequence", message.clientSequence},
{"target", targetJson(message.target)} {"target", targetJsonFp(message.target)}
}); });
} }
@@ -219,7 +257,7 @@ bool parseDestinationInputMessage(
return false; return false;
if (!payload.contains("target")) if (!payload.contains("target"))
return error = "target is required", false; return error = "target is required", false;
return readTarget(payload["target"], message.target, error); return readTargetFp(payload["target"], message.target, error);
} }
std::string makeCharacterActionInputMessage( std::string makeCharacterActionInputMessage(
@@ -265,7 +303,7 @@ std::string makeTickMessage(const TickMessage& message)
{"type", "destination"}, {"type", "destination"},
{"commandSequence", command.commandSequence}, {"commandSequence", command.commandSequence},
{"playerId", command.playerId}, {"playerId", command.playerId},
{"target", targetJson(command.target)} {"target", targetJsonFp(command.target)}
}); });
} else { } else {
if constexpr (std::is_same_v<T, ScheduledCharacterEnabledCommand>) { if constexpr (std::is_same_v<T, ScheduledCharacterEnabledCommand>) {
@@ -320,7 +358,7 @@ bool parseTickMessage(
destination.commandSequence = previousSequence; destination.commandSequence = previousSequence;
if (!value.contains("playerId") || !value["playerId"].is_string() || if (!value.contains("playerId") || !value["playerId"].is_string() ||
!value.contains("target") || !value.contains("target") ||
!readTarget(value["target"], destination.target, error)) { !readTargetFp(value["target"], destination.target, error)) {
if (error.empty()) error = "invalid destination command"; if (error.empty()) error = "invalid destination command";
return false; return false;
} }
+4 -3
View File
@@ -7,6 +7,7 @@
#include <string> #include <string>
#include <variant> #include <variant>
#include <vector> #include <vector>
#include "../navigation/PathFinder.h"
namespace FRG { namespace FRG {
@@ -25,7 +26,7 @@ struct InitialStateMessage {
struct DestinationInputMessage { struct DestinationInputMessage {
std::uint64_t clientSequence = 0; std::uint64_t clientSequence = 0;
Eigen::Vector3f target = Eigen::Vector3f::Zero(); Vec3fp target;
}; };
struct CharacterActionInputMessage { struct CharacterActionInputMessage {
@@ -37,7 +38,7 @@ struct CharacterActionInputMessage {
struct ScheduledDestinationCommand { struct ScheduledDestinationCommand {
std::uint64_t commandSequence = 0; std::uint64_t commandSequence = 0;
std::string playerId; std::string playerId;
Eigen::Vector3f target = Eigen::Vector3f::Zero(); Vec3fp target;
}; };
struct ScheduledCharacterEnabledCommand { struct ScheduledCharacterEnabledCommand {
@@ -73,7 +74,7 @@ public:
std::uint64_t currentTick() const { return currentTick_; } std::uint64_t currentTick() const { return currentTick_; }
std::uint64_t schedule( std::uint64_t schedule(
const std::string& playerId, const Eigen::Vector3f& target); const std::string& playerId, const Vec3fp& target);
std::uint64_t scheduleCharacterEnabled( std::uint64_t scheduleCharacterEnabled(
const std::string& locationId, const std::string& characterId, const std::string& locationId, const std::string& characterId,
bool enabled); bool enabled);
+12 -9
View File
@@ -13,6 +13,9 @@
namespace FRG { namespace FRG {
constexpr float kPi = 3.14159265358979323846f;
using json = nlohmann::json; using json = nlohmann::json;
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@@ -341,7 +344,7 @@ namespace FRG {
data.meshTextures[it.key()] = it.value().get<std::string>(); data.meshTextures[it.key()] = it.value().get<std::string>();
} }
} }
data.interactionRadius = item.value("interactionRadius", 0.0f); data.interactionRadius = static_cast<fp>(item.value<double>("interactionRadius", 0.0));
data.animationIdlePath = item.value("animationIdlePath", ""); data.animationIdlePath = item.value("animationIdlePath", "");
data.animationWalkPath = item.value("animationWalkPath", ""); data.animationWalkPath = item.value("animationWalkPath", "");
data.animationActionIdlePath = item.value("animationActionIdlePath", ""); data.animationActionIdlePath = item.value("animationActionIdlePath", "");
@@ -350,18 +353,18 @@ namespace FRG {
data.animationActionToStandPath = item.value("animationActionToStandPath", ""); data.animationActionToStandPath = item.value("animationActionToStandPath", "");
data.animationActionToDeathPath = item.value("animationActionToDeathPath", ""); data.animationActionToDeathPath = item.value("animationActionToDeathPath", "");
data.animationDeathIdlePath = item.value("animationDeathIdlePath", ""); data.animationDeathIdlePath = item.value("animationDeathIdlePath", "");
data.positionX = item.value("positionX", 0.0f); data.positionX = static_cast<fp>(item.value<double>("positionX", 0.0));
data.positionY = item.value("positionY", 0.0f); data.positionY = static_cast<fp>(item.value<double>("positionY", 0.0));
data.positionZ = item.value("positionZ", 0.0f); data.positionZ = static_cast<fp>(item.value<double>("positionZ", 0.0));
data.walkSpeed = item.value("walkSpeed", 1.5f); data.walkSpeed = static_cast<fp>(item.value<double>("walkSpeed", 1.5));
data.rotationSpeed = item.value("rotationSpeed", 8.0f); data.rotationSpeed = static_cast<fp>(item.value<double>("rotationSpeed", 8.0));
data.modelScale = item.value("modelScale", 0.01f); data.modelScale = item.value("modelScale", 0.01f);
data.modelCorrectionRotX = item.value("modelCorrectionRotX", 0.0f) * static_cast<float>(M_PI) / 180.0f; data.modelCorrectionRotX = item.value("modelCorrectionRotX", 0.0f) * static_cast<float>(M_PI) / 180.0f;
data.modelCorrectionRotY = item.value("modelCorrectionRotY", 0.0f) * static_cast<float>(M_PI) / 180.0f; data.modelCorrectionRotY = item.value("modelCorrectionRotY", 0.0f) * static_cast<float>(M_PI) / 180.0f;
data.modelCorrectionRotZ = item.value("modelCorrectionRotZ", 0.0f) * static_cast<float>(M_PI) / 180.0f; data.modelCorrectionRotZ = item.value("modelCorrectionRotZ", 0.0f) * static_cast<float>(M_PI) / 180.0f;
data.facingAngle = item.value("facingAngle", 0.0f) * static_cast<float>(M_PI) / 180.0f; data.facingAngle = static_cast<fp>(item.value("facingAngle", 0.0f)) * static_cast<fp>(kPi) / static_cast<fp>(180.0f);
data.hp = item.value("hp", 100.0f); data.hp = item.value("hp", 100.0f);
data.visibleRange = item.value("visibleRange", VISIBLE_RANGE); data.visibleRange = static_cast<fp>(item.value<double>("visibleRange", static_cast<double>(VISIBLE_RANGE)));
data.canAttack = item.value("canAttack", false); data.canAttack = item.value("canAttack", false);
data.enabled = item.value("enabled", true); data.enabled = item.value("enabled", true);
@@ -401,7 +404,7 @@ namespace FRG {
renderer, zipPath.c_str()); renderer, zipPath.c_str());
if (npc) { if (npc) {
logger() << "Successfully loaded NPC: " << npcData.name << " at (" logger() << "Successfully loaded NPC: " << npcData.name << " at ("
<< npcData.positionX << ", " << npcData.positionY << ", " << npcData.positionZ << ")" << std::endl; << static_cast<float>(npcData.positionX) << ", " << static_cast<float>(npcData.positionY) << ", " << static_cast<float>(npcData.positionZ) << ")" << std::endl;
npcs.push_back(std::move(npc)); npcs.push_back(std::move(npc));
} }
} catch (const std::exception& e) { } catch (const std::exception& e) {
+64 -42
View File
@@ -12,8 +12,27 @@
namespace FRG { namespace FRG {
using json = nlohmann::json; using json = nlohmann::json;
Vec3fp operator-(Vec3fp a, Vec3fp b)
{
return Vec3fp{
a.x - b.x,
a.y - b.y,
a.z - b.z
};
}
Vec3fp operator+(Vec3fp a, Vec3fp b)
{
return Vec3fp{
a.x + b.x,
a.y + b.y,
a.z + b.z
};
}
namespace { namespace {
std::vector<Eigen::Vector2f> readPolygon(const json& item) std::vector<Eigen::Vector2f> readPolygon(const json& item)
{ {
std::vector<Eigen::Vector2f> polygon; std::vector<Eigen::Vector2f> polygon;
@@ -79,7 +98,7 @@ namespace {
} }
} }
void PathFinder::build(const std::string& configPath, void PathFinderOld::build(const std::string& configPath,
const std::string& zipPath) const std::string& zipPath)
{ {
ready = false; ready = false;
@@ -115,22 +134,23 @@ void PathFinder::build(const std::string& configPath,
<< ", cell=" << cellSize << ", areas=" << areas.size() << "\n"; << ", cell=" << cellSize << ", areas=" << areas.size() << "\n";
} }
std::vector<Eigen::Vector3f> PathFinder::findPath(const Eigen::Vector3f& start, std::vector<Eigen::Vector3f> PathFinderOld::findPath(const Eigen::Vector3f& start,
const Eigen::Vector3f& end) const const Eigen::Vector3f& end) const
{ {
const std::lock_guard<std::mutex> lock(*queryMutex); //const std::lock_guard<std::mutex> lock(*queryMutex);
return runSearch(start, end, 0, false); //return runSearch(start, end, 0, false);
return std::vector<Eigen::Vector3f>{start, end};
} }
std::vector<Eigen::Vector3f> PathFinder::findPath(const Eigen::Vector3f& start, /*
std::vector<Eigen::Vector3f> PathFinderOld::findPath(const Eigen::Vector3f& start,
const Eigen::Vector3f& end, const Eigen::Vector3f& end,
const std::vector<DynamicObstacle>& dynamicObstacles) const const std::vector<DynamicObstacle>& dynamicObstacles) const
{ {
const std::lock_guard<std::mutex> lock(*queryMutex); const std::lock_guard<std::mutex> lock(*queryMutex);
return runSearch(start, end, buildDynamicOverlay(dynamicObstacles), false); return runSearch(start, end, buildDynamicOverlay(dynamicObstacles), false);
} }*/
std::uint32_t PathFinderOld::buildDynamicOverlay(
std::uint32_t PathFinder::buildDynamicOverlay(
const std::vector<DynamicObstacle>& dynamicObstacles) const const std::vector<DynamicObstacle>& dynamicObstacles) const
{ {
const size_t cellCount = walkable.size(); const size_t cellCount = walkable.size();
@@ -172,7 +192,7 @@ std::uint32_t PathFinder::buildDynamicOverlay(
return dynamicGeneration; return dynamicGeneration;
} }
bool PathFinder::isCellWalkableOverlay( bool PathFinderOld::isCellWalkableOverlay(
const Cell& cell, std::uint32_t generation) const const Cell& cell, std::uint32_t generation) const
{ {
if (!isCellWalkable(cell)) return false; if (!isCellWalkable(cell)) return false;
@@ -180,7 +200,7 @@ bool PathFinder::isCellWalkableOverlay(
dynamicBlockedGeneration[static_cast<size_t>(indexOf(cell))] != generation; dynamicBlockedGeneration[static_cast<size_t>(indexOf(cell))] != generation;
} }
bool PathFinder::findNearestWalkableCellOverlay( bool PathFinderOld::findNearestWalkableCellOverlay(
const Eigen::Vector3f& point, Cell& out, std::uint32_t generation) const const Eigen::Vector3f& point, Cell& out, std::uint32_t generation) const
{ {
Cell origin; Cell origin;
@@ -212,7 +232,7 @@ bool PathFinder::findNearestWalkableCellOverlay(
return false; return false;
} }
bool PathFinder::hasLineOfSightOverlay( bool PathFinderOld::hasLineOfSightOverlay(
const Cell& from, const Cell& to, std::uint32_t generation) const const Cell& from, const Cell& to, std::uint32_t generation) const
{ {
int x = from.x; int x = from.x;
@@ -244,7 +264,7 @@ bool PathFinder::hasLineOfSightOverlay(
} }
} }
std::vector<PathFinder::Cell> PathFinder::smoothCellsOverlay( std::vector<PathFinderOld::Cell> PathFinderOld::smoothCellsOverlay(
const std::vector<Cell>& cells, std::uint32_t generation) const const std::vector<Cell>& cells, std::uint32_t generation) const
{ {
if (cells.size() <= 2) return cells; if (cells.size() <= 2) return cells;
@@ -263,7 +283,7 @@ std::vector<PathFinder::Cell> PathFinder::smoothCellsOverlay(
return result; return result;
} }
std::vector<Eigen::Vector3f> PathFinder::runSearch( std::vector<Eigen::Vector3f> PathFinderOld::runSearch(
const Eigen::Vector3f& start, const Eigen::Vector3f& start,
const Eigen::Vector3f& end, const Eigen::Vector3f& end,
std::uint32_t obstacleGeneration, std::uint32_t obstacleGeneration,
@@ -385,23 +405,25 @@ std::vector<Eigen::Vector3f> PathFinder::runSearch(
return path; return path;
} }
std::vector<Eigen::Vector3f> PathFinder::findPathToNearest( std::vector<Eigen::Vector3f> PathFinderOld::findPathToNearest(
const Eigen::Vector3f& start, const Eigen::Vector3f& end) const const Eigen::Vector3f& start, const Eigen::Vector3f& end) const
{ {
const std::lock_guard<std::mutex> lock(*queryMutex); //const std::lock_guard<std::mutex> lock(*queryMutex);
return runSearch(start, end, 0, true); // return runSearch(start, end, 0, true);
return std::vector<Eigen::Vector3f>{start, end};
} }
std::vector<Eigen::Vector3f> PathFinder::findPathToNearest( /*
std::vector<Eigen::Vector3f> PathFinderOld::findPathToNearest(
const Eigen::Vector3f& start, const Eigen::Vector3f& end, const Eigen::Vector3f& start, const Eigen::Vector3f& end,
const std::vector<DynamicObstacle>& dynamicObstacles) const const std::vector<DynamicObstacle>& dynamicObstacles) const
{ {
const std::lock_guard<std::mutex> lock(*queryMutex); const std::lock_guard<std::mutex> lock(*queryMutex);
return runSearch( return runSearch(
start, end, buildDynamicOverlay(dynamicObstacles), true); start, end, buildDynamicOverlay(dynamicObstacles), true);
} }*/
bool PathFinder::setAreaAvailable(const std::string& areaName, bool available) bool PathFinderOld::setAreaAvailable(const std::string& areaName, bool available)
{ {
bool found = false; bool found = false;
bool changed = false; bool changed = false;
@@ -420,13 +442,13 @@ bool PathFinder::setAreaAvailable(const std::string& areaName, bool available)
return found; return found;
} }
bool PathFinder::isWalkable(const Eigen::Vector3f& point) const bool PathFinderOld::isWalkable(const Eigen::Vector3f& point) const
{ {
Cell cell; Cell cell;
return worldToCell(point, cell) && isCellWalkable(cell, walkable); return worldToCell(point, cell) && isCellWalkable(cell, walkable);
} }
void PathFinder::addObstaclePolygon(const ObstaclePolygon& polygon) void PathFinderOld::addObstaclePolygon(const ObstaclePolygon& polygon)
{ {
obstaclePolygons.push_back(polygon); obstaclePolygons.push_back(polygon);
if (ready) { if (ready) {
@@ -434,7 +456,7 @@ void PathFinder::addObstaclePolygon(const ObstaclePolygon& polygon)
} }
} }
bool PathFinder::saveConfig(const std::string& path) const bool PathFinderOld::saveConfig(const std::string& path) const
{ {
json root; json root;
root["cellSize"] = cellSize; root["cellSize"] = cellSize;
@@ -481,7 +503,7 @@ bool PathFinder::saveConfig(const std::string& path) const
return saveJsonToFile(root, path); return saveJsonToFile(root, path);
} }
bool PathFinder::saveGrid(const std::string& path) const bool PathFinderOld::saveGrid(const std::string& path) const
{ {
#if defined(_WIN32) #if defined(_WIN32)
if (!ready) return false; if (!ready) return false;
@@ -510,7 +532,7 @@ bool PathFinder::saveGrid(const std::string& path) const
#endif #endif
} }
bool PathFinder::loadGrid(const std::string& path, const std::string& zipPath) bool PathFinderOld::loadGrid(const std::string& path, const std::string& zipPath)
{ {
std::string content; std::string content;
try { try {
@@ -565,7 +587,7 @@ bool PathFinder::loadGrid(const std::string& path, const std::string& zipPath)
return true; return true;
} }
void PathFinder::loadConfig(const std::string& configPath, const std::string& zipPath) void PathFinderOld::loadConfig(const std::string& configPath, const std::string& zipPath)
{ {
cellSize = 0.4f; cellSize = 0.4f;
agentRadius = 0.45f; agentRadius = 0.45f;
@@ -640,7 +662,7 @@ void PathFinder::loadConfig(const std::string& configPath, const std::string& zi
boundaryPadding = (std::max)(boundaryPadding, 0.0f); boundaryPadding = (std::max)(boundaryPadding, 0.0f);
} }
void PathFinder::resetGridBounds() void PathFinderOld::resetGridBounds()
{ {
if (areas.empty()) { if (areas.empty()) {
gridWidth = 0; gridWidth = 0;
@@ -682,14 +704,14 @@ void PathFinder::resetGridBounds()
gridDepth = static_cast<int>(std::ceil((maxZ - minZ) / cellSize)); gridDepth = static_cast<int>(std::ceil((maxZ - minZ) / cellSize));
} }
void PathFinder::rebuildWalkableGrid() void PathFinderOld::rebuildWalkableGrid()
{ {
walkable.assign(static_cast<size_t>(gridWidth * gridDepth), 0); walkable.assign(static_cast<size_t>(gridWidth * gridDepth), 0);
markAvailableAreasWalkable(); markAvailableAreasWalkable();
markObstaclePolygonsBlocked(); markObstaclePolygonsBlocked();
} }
void PathFinder::markAvailableAreasWalkable() void PathFinderOld::markAvailableAreasWalkable()
{ {
const auto insideAvailableArea = [this](const Eigen::Vector2f& point) { const auto insideAvailableArea = [this](const Eigen::Vector2f& point) {
for (const NavigationArea& area : areas) { for (const NavigationArea& area : areas) {
@@ -763,7 +785,7 @@ void PathFinder::markAvailableAreasWalkable()
} }
} }
void PathFinder::markObstaclePolygonsBlocked() void PathFinderOld::markObstaclePolygonsBlocked()
{ {
if (obstaclePolygons.empty()) { if (obstaclePolygons.empty()) {
return; return;
@@ -800,7 +822,7 @@ void PathFinder::markObstaclePolygonsBlocked()
} }
} }
bool PathFinder::worldToCell(const Eigen::Vector3f& point, Cell& out) const bool PathFinderOld::worldToCell(const Eigen::Vector3f& point, Cell& out) const
{ {
if (gridWidth <= 0 || gridDepth <= 0) { if (gridWidth <= 0 || gridDepth <= 0) {
out = {}; out = {};
@@ -812,7 +834,7 @@ bool PathFinder::worldToCell(const Eigen::Vector3f& point, Cell& out) const
return isInsideGrid(out); return isInsideGrid(out);
} }
Eigen::Vector3f PathFinder::cellCenter(const Cell& cell) const Eigen::Vector3f PathFinderOld::cellCenter(const Cell& cell) const
{ {
return { return {
minX + (static_cast<float>(cell.x) + 0.5f) * cellSize, minX + (static_cast<float>(cell.x) + 0.5f) * cellSize,
@@ -821,17 +843,17 @@ Eigen::Vector3f PathFinder::cellCenter(const Cell& cell) const
}; };
} }
bool PathFinder::isInsideGrid(const Cell& cell) const bool PathFinderOld::isInsideGrid(const Cell& cell) const
{ {
return cell.x >= 0 && cell.z >= 0 && cell.x < gridWidth && cell.z < gridDepth; return cell.x >= 0 && cell.z >= 0 && cell.x < gridWidth && cell.z < gridDepth;
} }
bool PathFinder::isCellWalkable(const Cell& cell) const bool PathFinderOld::isCellWalkable(const Cell& cell) const
{ {
return isCellWalkable(cell, walkable); return isCellWalkable(cell, walkable);
} }
bool PathFinder::isCellWalkable(const Cell& cell, const std::vector<unsigned char>& walkableGrid) const bool PathFinderOld::isCellWalkable(const Cell& cell, const std::vector<unsigned char>& walkableGrid) const
{ {
const int idx = indexOf(cell); const int idx = indexOf(cell);
return isInsideGrid(cell) && return isInsideGrid(cell) &&
@@ -840,17 +862,17 @@ bool PathFinder::isCellWalkable(const Cell& cell, const std::vector<unsigned cha
walkableGrid[static_cast<size_t>(idx)] != 0; walkableGrid[static_cast<size_t>(idx)] != 0;
} }
int PathFinder::indexOf(const Cell& cell) const int PathFinderOld::indexOf(const Cell& cell) const
{ {
return cell.z * gridWidth + cell.x; return cell.z * gridWidth + cell.x;
} }
bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out) const bool PathFinderOld::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out) const
{ {
return findNearestWalkableCell(point, out, walkable); return findNearestWalkableCell(point, out, walkable);
} }
bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out, const std::vector<unsigned char>& walkableGrid) const bool PathFinderOld::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out, const std::vector<unsigned char>& walkableGrid) const
{ {
Cell origin; Cell origin;
if (!worldToCell(point, origin)) { if (!worldToCell(point, origin)) {
@@ -885,12 +907,12 @@ bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out
return false; return false;
} }
bool PathFinder::hasLineOfSight(const Cell& from, const Cell& to) const bool PathFinderOld::hasLineOfSight(const Cell& from, const Cell& to) const
{ {
return hasLineOfSight(from, to, walkable); return hasLineOfSight(from, to, walkable);
} }
bool PathFinder::hasLineOfSight(const Cell& from, const Cell& to, const std::vector<unsigned char>& walkableGrid) const bool PathFinderOld::hasLineOfSight(const Cell& from, const Cell& to, const std::vector<unsigned char>& walkableGrid) const
{ {
const Eigen::Vector3f a = cellCenter(from); const Eigen::Vector3f a = cellCenter(from);
const Eigen::Vector3f b = cellCenter(to); const Eigen::Vector3f b = cellCenter(to);
@@ -909,12 +931,12 @@ bool PathFinder::hasLineOfSight(const Cell& from, const Cell& to, const std::vec
return true; return true;
} }
std::vector<PathFinder::Cell> PathFinder::smoothCells(const std::vector<Cell>& cells) const std::vector<PathFinderOld::Cell> PathFinderOld::smoothCells(const std::vector<Cell>& cells) const
{ {
return smoothCells(cells, walkable); return smoothCells(cells, walkable);
} }
std::vector<PathFinder::Cell> PathFinder::smoothCells(const std::vector<Cell>& cells, const std::vector<unsigned char>& walkableGrid) const std::vector<PathFinderOld::Cell> PathFinderOld::smoothCells(const std::vector<Cell>& cells, const std::vector<unsigned char>& walkableGrid) const
{ {
if (cells.size() <= 2) { if (cells.size() <= 2) {
return cells; return cells;
@@ -937,7 +959,7 @@ std::vector<PathFinder::Cell> PathFinder::smoothCells(const std::vector<Cell>& c
return result; return result;
} }
bool PathFinder::pointInPolygon(float x, float z, const std::vector<Eigen::Vector2f>& polygon) bool PathFinderOld::pointInPolygon(float x, float z, const std::vector<Eigen::Vector2f>& polygon)
{ {
bool inside = false; bool inside = false;
size_t previous = polygon.size() - 1; size_t previous = polygon.size() - 1;
+577 -5
View File
@@ -1,15 +1,587 @@
#pragma once #pragma once
#include <Eigen/Core> #include <Eigen/Core>
#include <algorithm>
#include <cstdint> #include <cstdint>
#include <limits>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <queue>
#include <string> #include <string>
#include <unordered_map>
#include <vector> #include <vector>
#ifdef min
#undef min
#endif
#ifdef max
#undef max
#endif
#include "../../thirdparty/fpm-1.1.0/include/fpm/fixed.hpp"
#include "../../thirdparty/fpm-1.1.0/include/fpm/math.hpp"
// Сторонние библиотеки для геометрии (Header-only / легко подключаемые):
// 1. Clipper2: https://github.com/PolyClipping/Clipper2/releases/tag/Clipper2_2.0.1
// 2. CDT (Constrained Delaunay Triangulation): https://github.com/artem-ogre/CDT
#include "../../thirdparty/Clipper2-Clipper2_2.0.1/CPP/build/Release/include/clipper2/clipper.h"
#include "../../thirdparty/CDT-2.0.2/CDT/build/release/include/CDT.h"
namespace FRG { namespace FRG {
class PathFinder {
//using fp = fpm::fixed<std::int32_t, std::int64_t, 16>;
inline constexpr unsigned int FP_FRACTION_BITS = 12;
using fp = fpm::fixed<std::int32_t, std::int64_t, FP_FRACTION_BITS>;
struct Vec2fp {
fp x{ 0 };
fp y{ 0 };
Vec2fp() = default;
Vec2fp(fp _x, fp _y) : x(_x), y(_y) {}
// Конвертация из float только на этапе загрузки/ввода
static Vec2fp fromFloat(float fx, float fy) {
return Vec2fp(fp(fx), fp(fy));
}
Vec2fp operator+(const Vec2fp& o) const { return { x + o.x, y + o.y }; }
Vec2fp operator-(const Vec2fp& o) const { return { x - o.x, y - o.y }; }
Vec2fp operator*(fp s) const { return { x * s, y * s }; }
Vec2fp operator/(fp s) const { return { x / s, y / s }; }
bool operator==(const Vec2fp& o) const { return x == o.x && y == o.y; }
bool operator!=(const Vec2fp& o) const { return !(*this == o); }
fp dot(const Vec2fp& o) const { return x * o.x + y * o.y; }
// 2D Векторное произведение (Z-компонента): определяет, с какой стороны лежит точка
fp cross(const Vec2fp& o) const { return x * o.y - y * o.x; }
fp lengthSq() const { return x * x + y * y; }
fp length() const { return fpm::sqrt(lengthSq()); }
Eigen::Vector2f toEigen() const {
return Eigen::Vector2f(static_cast<float>(x), static_cast<float>(y));
}
static Vec2fp fromEigen(const Eigen::Vector2f& v) {
return Vec2fp(fp(v.x()), fp(v.y()));
}
};
struct Vec3fp {
fp x{ 0 }, y{ 0 }, z{ 0 };
Vec3fp() = default;
Vec3fp(fp _x, fp _y, fp _z) : x(_x), y(_y), z(_z) {}
static Vec3fp fromEigen(const Eigen::Vector3f& v) {
return { fp(v.x()), fp(v.y()), fp(v.z()) };
}
Eigen::Vector3f toEigen() const {
return Eigen::Vector3f(static_cast<float>(x), static_cast<float>(y), static_cast<float>(z));
}
fp normSquared() const
{
return x * x + y * y + z * z;
}
Vec3fp operator/(fp value) const
{
if (value == static_cast<fp>(0.0))
{
throw std::runtime_error("Vec3fp error: division by zero");
return Vec3fp();
}
Vec3fp r;
r.x = x / value;
r.y = y / value;
r.z = z / value;
return r;
}
Vec3fp operator*(const fp value) const
{
Vec3fp r;
r.x = x * value;
r.y = y * value;
r.z = z * value;
return r;
}
};
Vec3fp operator-(Vec3fp a, Vec3fp b);
Vec3fp operator+(Vec3fp a, Vec3fp b);
inline std::vector<Eigen::Vector2f> toEigenVector(const std::vector<FRG::Vec2fp>& src) {
std::vector<Eigen::Vector2f> dst;
dst.reserve(src.size());
for (const auto& pt : src) {
dst.emplace_back(static_cast<float>(pt.x), static_cast<float>(pt.y));
}
return dst;
}
inline std::vector<FRG::Vec2fp> fromEigenVector(const std::vector<Eigen::Vector2f>& src) {
std::vector<FRG::Vec2fp> dst;
dst.reserve(src.size());
for (const auto& pt : src) {
dst.emplace_back(FRG::fp(pt.x()), FRG::fp(pt.y()));
}
return dst;
}
class PathFinder {
public:
struct NavigationArea {
std::string name;
bool available = true;
std::vector<Vec2fp> polygon;
};
struct ObstaclePolygon {
std::string name;
std::vector<Vec2fp> polygon;
};
struct NavTriangle {
int id = -1;
Vec2fp vertices[3];
Vec2fp center;
int neighbors[3] = { -1, -1, -1 }; // Индексы соседних треугольников по ребрам (0-1, 1-2, 2-0)
};
struct Portal {
Vec2fp left;
Vec2fp right;
};
// Масштаб для Clipper2 (так как Clipper работает с целыми int64_t,
// сохраняем точность в 10000 единиц на 1 метр)
static constexpr int64_t CLIPPER_SCALE = 10000;
void build(const std::string& configPath = "", const std::string& zipPath = "") {
// Здесь может быть парсинг configPath в area и obstaclePolygons.
// После заполнения геометрии строим NavMesh:
area.name = "main";
area.polygon = {
Vec2fp::fromFloat(-100.f, -100.f),
Vec2fp::fromFloat(-100.f, 100.f),
Vec2fp::fromFloat(100.f, 100.f),
Vec2fp::fromFloat(100.f, -100.f)
};
area.available = true;
obstaclePolygons.push_back({
"p1", {Vec2fp::fromFloat(-10.f, -10.f),
Vec2fp::fromFloat(-10.f, 10.f),
Vec2fp::fromFloat(10.f, 10.f),
Vec2fp::fromFloat(10.f, -10.f)}
});
//ready = true;
generateNavMesh();
}
// Построение NavMesh: Объединение полигонов (Clipper2) + Триангуляция (CDT)
void generateNavMesh() {
navMesh.clear();
if (area.polygon.size() < 3) return;
using namespace Clipper2Lib;
// Вспомогательная лямбда: безопасно переводит fp в координаты Clipper2 (int64_t)
auto toClipperCoord = [](fp val) -> int64_t {
return (static_cast<int64_t>(val.raw_value()) * CLIPPER_SCALE) / (1LL << FP_FRACTION_BITS);
};
// 1. Переводим контур локации (NavigationArea) в формат Clipper2
Paths64 subject;
Path64 outerPath;
for (const auto& pt : area.polygon) {
outerPath.push_back(Point64(toClipperCoord(pt.x), toClipperCoord(pt.y)));
}
subject.push_back(outerPath);
// 2. Собираем все препятствия
Paths64 clips;
for (const auto& obs : obstaclePolygons) {
if (obs.polygon.size() < 3) continue;
Path64 clipPath;
for (const auto& pt : obs.polygon) {
clipPath.push_back(Point64(toClipperCoord(pt.x), toClipperCoord(pt.y)));
}
clips.push_back(clipPath);
}
// 3. Булево объединение пересекающихся препятствий и вычитание их из локации
// Clipper автоматически объединяет пересекающиеся полигоны внутри Difference
PolyTree64 solTree;
Clipper64 clipper;
clipper.AddSubject(subject);
clipper.AddClip(clips);
//clipper.Execute(ClipType::Difference, FillRule::NonZero, solTree);
clipper.Execute(ClipType::Difference, FillRule::EvenOdd, solTree);
// 4. Подготовка данных для Constrained Delaunay Triangulation (CDT)
std::vector<CDT::V2d<double>> cdtVertices;
std::vector<CDT::Edge> cdtEdges;
auto extractPolygons = [&](const PolyPath64* node, auto& selfRef) -> void {
if (!node->Polygon().empty()) {
const Path64& path = node->Polygon();
std::size_t startIdx = cdtVertices.size();
std::size_t count = path.size();
for (std::size_t i = 0; i < count; ++i) {
double x = static_cast<double>(path[i].x) / CLIPPER_SCALE;
double y = static_cast<double>(path[i].y) / CLIPPER_SCALE;
cdtVertices.push_back({ x, y });
cdtEdges.push_back(CDT::Edge(startIdx + i, startIdx + (i + 1) % count));
}
}
for (std::size_t i = 0; i < node->Count(); ++i) {
selfRef(node->Child(i), selfRef);
}
};
for (std::size_t i = 0; i < solTree.Count(); ++i) {
extractPolygons(solTree.Child(i), extractPolygons);
}
if (cdtVertices.empty()) return;
CDT::RemoveDuplicatesAndRemapEdges(cdtVertices, cdtEdges);
// 5. Выполняем триангуляцию и удаляем треугольники внутри дыр (препятствий)
CDT::Triangulation<double> cdt;
cdt.insertVertices(cdtVertices);
cdt.insertEdges(cdtEdges);
cdt.eraseOuterTrianglesAndHoles();
// 6. Конвертируем результат в детерминированный граф NavTriangle (Q32.32)
const auto& triangles = cdt.triangles;
const auto& vertices = cdt.vertices;
navMesh.resize(triangles.size());
for (std::size_t i = 0; i < triangles.size(); ++i) {
NavTriangle& navTri = navMesh[i];
navTri.id = static_cast<int>(i);
for (int j = 0; j < 3; ++j) {
const auto& v = vertices[triangles[i].vertices[j]];
navTri.vertices[j] = Vec2fp(fp(v.x), fp(v.y));
// Соседи в CDT хранятся напротив вершин, перепривязываем к ребрам (j, j+1)
auto neighborIdx = triangles[i].neighbors[j];
navTri.neighbors[j] = (neighborIdx != CDT::noNeighbor) ? static_cast<int>(neighborIdx) : -1;
}
navTri.center = (navTri.vertices[0] + navTri.vertices[1] + navTri.vertices[2]) / fp(3);
}
ready = !navMesh.empty();
}
// Основной метод поиска пути (плоскость XZ, Y — высота)
std::vector<Vec3fp> findPathToNearest(const Vec3fp& start3D, const Vec3fp& end3D) const {
if (!ready || !area.available) return { start3D };
Vec2fp startPt(start3D.x, start3D.z);
Vec2fp targetPt(end3D.x, end3D.z);
// 1. Находим треугольники для старта и финиша.
// Если точка попала в препятствие (вне NavMesh), ищем ближайшую точку на ребрах NavMesh
int startTri = findTriangleContaining(startPt);
if (startTri == -1) {
startPt = findClosestPointOnNavMesh(startPt, startTri);
}
int endTri = findTriangleContaining(targetPt);
if (endTri == -1) {
targetPt = findClosestPointOnNavMesh(targetPt, endTri);
}
if (startTri == -1 || endTri == -1) return { start3D };
// Если старт и конец в одном треугольнике — идем по прямой
if (startTri == endTri) {
return {
start3D,
Vec3fp(targetPt.x, floorY, targetPt.y)
};
}
// 2. Поиск коридора из треугольников через A*
std::vector<int> triangleCorridor = runAStar(startTri, endTri, startPt, targetPt);
if (triangleCorridor.empty()) return { start3D };
// 3. Сглаживание пути через Funnel Algorithm (Натянутая нить)
std::vector<Vec2fp> smoothPath2D = stringPullFunnel(startPt, targetPt, triangleCorridor);
// 4. Формируем выходной массив 3D точек
std::vector<Vec3fp> result;
result.reserve(smoothPath2D.size());
for (const auto& pt : smoothPath2D) {
result.emplace_back(pt.x, floorY, pt.y);
}
return result;
}
// Перегрузка для совместимости со старым Eigen-интерфейсом
std::vector<Eigen::Vector3f> findPathToNearest(const Eigen::Vector3f& start, const Eigen::Vector3f& end) const {
auto pathFp = findPathToNearest(Vec3fp::fromEigen(start), Vec3fp::fromEigen(end));
std::vector<Eigen::Vector3f> out;
out.reserve(pathFp.size());
for (const auto& p : pathFp) out.push_back(p.toEigen());
return out;
}
bool isWalkable(const Vec3fp& point) const {
return findTriangleContaining(Vec2fp(point.x, point.z)) != -1;
}
bool isWalkable(const Eigen::Vector3f& point) const {
return isWalkable(Vec3fp::fromEigen(point));
}
bool setAreaAvailable(const std::string& areaName, bool available) {
if (area.name == areaName) {
area.available = available;
return true;
}
return false;
}
bool isReady() const { return ready; }
const NavigationArea& getArea() const { return area; }
void setArea(const NavigationArea& a) { area = a; }
fp getFloorY() const { return floorY; }
void setFloorY(fp y) { floorY = y; }
void addObstaclePolygon(const ObstaclePolygon& polygon) {
obstaclePolygons.push_back(polygon);
}
const std::vector<ObstaclePolygon>& getObstaclePolygons() const { return obstaclePolygons; }
bool saveConfig(const std::string& path) const { return true; }
bool saveGrid(const std::string& path) const { return true; }
// Внутри public: класса PathFinder
Vec2fp clampToNavMesh(const Vec2fp& pt, int& outTriId) const {
return findClosestPointOnNavMesh(pt, outTriId);
}
// Удобная перегрузка без outTriId, если индекс треугольника не нужен:
Vec2fp clampToNavMesh(const Vec2fp& pt) const {
int dummyTri = -1;
return findClosestPointOnNavMesh(pt, dummyTri);
}
protected:
NavigationArea area;
std::vector<ObstaclePolygon> obstaclePolygons;
std::vector<NavTriangle> navMesh;
fp floorY{ 0 };
bool ready = false;
// Проверка нахождения точки внутри треугольника (через знаки векторных произведений)
static bool isPointInTriangle(const Vec2fp& p, const Vec2fp& a, const Vec2fp& b, const Vec2fp& c) {
fp d1 = (b - a).cross(p - a);
fp d2 = (c - b).cross(p - b);
fp d3 = (a - c).cross(p - c);
bool hasNeg = (d1 < fp(0)) || (d2 < fp(0)) || (d3 < fp(0));
bool hasPos = (d1 > fp(0)) || (d2 > fp(0)) || (d3 > fp(0));
return !(hasNeg && hasPos);
}
int findTriangleContaining(const Vec2fp& pt) const {
for (const auto& tri : navMesh) {
if (isPointInTriangle(pt, tri.vertices[0], tri.vertices[1], tri.vertices[2])) {
return tri.id;
}
}
return -1;
}
// Проекция точки на отрезок AB
static Vec2fp closestPointOnSegment(const Vec2fp& p, const Vec2fp& a, const Vec2fp& b) {
Vec2fp ab = b - a;
fp lenSq = ab.lengthSq();
if (lenSq == fp(0)) return a;
fp t = (p - a).dot(ab) / lenSq;
t = std::max(fp(0), std::min(fp(1), t));
return a + ab * t;
}
Vec2fp findClosestPointOnNavMesh(const Vec2fp& pt, int& outTriId) const {
fp minDistSq = std::numeric_limits<fp>::max();
Vec2fp bestPt = pt;
outTriId = -1;
for (const auto& tri : navMesh) {
for (int i = 0; i < 3; ++i) {
// В CDT neighbors[i] — это сосед через ребро (vertices[i], vertices[(i + 1) % 3])
if (tri.neighbors[i] == -1) {
const Vec2fp& a = tri.vertices[i];
const Vec2fp& b = tri.vertices[(i + 1) % 3];
Vec2fp proj = closestPointOnSegment(pt, a, b);
fp distSq = (pt - proj).lengthSq();
if (distSq < minDistSq) {
minDistSq = distSq;
bestPt = proj;
outTriId = tri.id;
}
}
}
}
return bestPt;
}
// Поиск пути A* по центрам треугольников
std::vector<int> runAStar(int startTri, int endTri, const Vec2fp& startPt, const Vec2fp& endPt) const {
struct NodeRecord {
int triId;
fp fScore;
// В случае равенства fScore сортируем по triId для 100% детерминизма
bool operator>(const NodeRecord& o) const {
if (fScore != o.fScore) return fScore > o.fScore;
return triId > o.triId;
}
};
std::priority_queue<NodeRecord, std::vector<NodeRecord>, std::greater<NodeRecord>> openSet;
std::vector<fp> gScore(navMesh.size(), std::numeric_limits<fp>::max());
std::vector<int> cameFrom(navMesh.size(), -1);
std::vector<bool> closed(navMesh.size(), false);
gScore[startTri] = fp(0);
openSet.push({ startTri, (navMesh[startTri].center - endPt).length() });
while (!openSet.empty()) {
int current = openSet.top().triId;
openSet.pop();
if (current == endTri) {
std::vector<int> path;
for (int curr = endTri; curr != -1; curr = cameFrom[curr]) {
path.push_back(curr);
}
std::reverse(path.begin(), path.end());
return path;
}
if (closed[current]) continue;
closed[current] = true;
const NavTriangle& currTri = navMesh[current];
for (int i = 0; i < 3; ++i) {
int neighbor = currTri.neighbors[i];
if (neighbor == -1 || closed[neighbor]) continue;
fp stepCost = (currTri.center - navMesh[neighbor].center).length();
fp tentativeG = gScore[current] + stepCost;
if (tentativeG < gScore[neighbor]) {
cameFrom[neighbor] = current;
gScore[neighbor] = tentativeG;
fp h = (navMesh[neighbor].center - endPt).length();
openSet.push({ neighbor, tentativeG + h });
}
}
}
return {};
}
// Извлечение общих вершин (портала) между двумя соседними треугольниками
// Извлечение общих вершин (портала) между двумя соседними треугольниками
Portal getPortal(int fromTriId, int toTriId) const {
const NavTriangle& t1 = navMesh[fromTriId];
for (int i = 0; i < 3; ++i) {
if (t1.neighbors[i] == toTriId) {
// При обходе треугольника CCW и движении изнутри наружу через ребро i -> (i + 1) % 3:
// вершина i находится СЛЕВА, а вершина (i + 1) % 3 — СПРАВА
return Portal{ t1.vertices[i], t1.vertices[(i + 1) % 3] };
}
}
return Portal{ t1.center, t1.center };
}
// Алгоритм натянутой нити (Simple Stupid Funnel Algorithm)
std::vector<Vec2fp> stringPullFunnel(const Vec2fp& startPt, const Vec2fp& endPt, const std::vector<int>& corridor) const {
std::vector<Portal> portals;
portals.reserve(corridor.size() + 2);
portals.push_back({ startPt, startPt });
for (std::size_t i = 0; i + 1 < corridor.size(); ++i) {
portals.push_back(getPortal(corridor[i], corridor[i + 1]));
}
portals.push_back({ endPt, endPt });
std::vector<Vec2fp> pts;
pts.push_back(startPt);
Vec2fp portalApex = startPt;
Vec2fp portalLeft = startPt;
Vec2fp portalRight = startPt;
int apexIndex = 0, leftIndex = 0, rightIndex = 0;
for (int i = 1; i < static_cast<int>(portals.size()); ++i) {
const Vec2fp& left = portals[i].left;
const Vec2fp& right = portals[i].right;
// Обновляем правую границу воронки
if ((portalRight - portalApex).cross(right - portalApex) <= fp(0)) {
if (portalApex == portalRight || (portalLeft - portalApex).cross(right - portalApex) > fp(0)) {
portalRight = right;
rightIndex = i;
}
else {
pts.push_back(portalLeft);
portalApex = portalLeft;
apexIndex = leftIndex;
portalLeft = portalApex;
portalRight = portalApex;
leftIndex = apexIndex;
rightIndex = apexIndex;
i = apexIndex;
continue;
}
}
// Обновляем левую границу воронки
if ((portalLeft - portalApex).cross(left - portalApex) >= fp(0)) {
if (portalApex == portalLeft || (portalRight - portalApex).cross(left - portalApex) < fp(0)) {
portalLeft = left;
leftIndex = i;
}
else {
pts.push_back(portalRight);
portalApex = portalRight;
apexIndex = rightIndex;
portalLeft = portalApex;
portalRight = portalApex;
leftIndex = apexIndex;
rightIndex = apexIndex;
i = apexIndex;
continue;
}
}
}
if (pts.back() != endPt) {
pts.push_back(endPt);
}
return pts;
}
};
class PathFinderOld {
public: public:
struct ObstaclePolygon { struct ObstaclePolygon {
@@ -39,18 +611,18 @@ public:
std::vector<Eigen::Vector3f> findPath(const Eigen::Vector3f& start, std::vector<Eigen::Vector3f> findPath(const Eigen::Vector3f& start,
const Eigen::Vector3f& end) const; const Eigen::Vector3f& end) const;
std::vector<Eigen::Vector3f> findPath(const Eigen::Vector3f& start, /*std::vector<Eigen::Vector3f> findPath(const Eigen::Vector3f& start,
const Eigen::Vector3f& end, const Eigen::Vector3f& end,
const std::vector<DynamicObstacle>& dynamicObstacles) const; const std::vector<DynamicObstacle>& dynamicObstacles) const;
*/
// Like findPath, but when the destination is unreachable the character // Like findPath, but when the destination is unreachable the character
// walks as close as possible instead of not moving at all. // walks as close as possible instead of not moving at all.
std::vector<Eigen::Vector3f> findPathToNearest(const Eigen::Vector3f& start, std::vector<Eigen::Vector3f> findPathToNearest(const Eigen::Vector3f& start,
const Eigen::Vector3f& end) const; const Eigen::Vector3f& end) const;
std::vector<Eigen::Vector3f> findPathToNearest(const Eigen::Vector3f& start, /*std::vector<Eigen::Vector3f> findPathToNearest(const Eigen::Vector3f& start,
const Eigen::Vector3f& end, const Eigen::Vector3f& end,
const std::vector<DynamicObstacle>& dynamicObstacles) const; const std::vector<DynamicObstacle>& dynamicObstacles) const;
*/
bool setAreaAvailable(const std::string& areaName, bool available); bool setAreaAvailable(const std::string& areaName, bool available);
bool isReady() const { return ready; } bool isReady() const { return ready; }
bool isWalkable(const Eigen::Vector3f& point) const; bool isWalkable(const Eigen::Vector3f& point) const;
+2 -2
View File
@@ -109,7 +109,7 @@ namespace FRG {
const size_t firstWaypoint = (std::min)( const size_t firstWaypoint = (std::min)(
state->currentWaypointIndex, state->pathWaypoints.size()); state->currentWaypointIndex, state->pathWaypoints.size());
for (size_t i = firstWaypoint; i < state->pathWaypoints.size(); ++i) { for (size_t i = firstWaypoint; i < state->pathWaypoints.size(); ++i) {
const Eigen::Vector3f& waypoint = state->pathWaypoints[i]; const Eigen::Vector3f& waypoint = state->pathWaypoints[i].toEigen();
route.push_back(Eigen::Vector3f(waypoint.x(), 0.0f, waypoint.z())); route.push_back(Eigen::Vector3f(waypoint.x(), 0.0f, waypoint.z()));
} }
const Eigen::Vector3f clickedPosition( const Eigen::Vector3f clickedPosition(
@@ -140,7 +140,7 @@ namespace FRG {
constexpr float kPlayerClearanceMs = 20.0f; constexpr float kPlayerClearanceMs = 20.0f;
constexpr float kPlayerMaximumSpeedMultiplier = 1.5f; constexpr float kPlayerMaximumSpeedMultiplier = 1.5f;
const float playerSpeed = (std::max)( const float playerSpeed = (std::max)(
state->walkSpeed * kPlayerMaximumSpeedMultiplier, 0.01f); static_cast<float>(state->walkSpeed) * kPlayerMaximumSpeedMultiplier, 0.01f);
const float playerRouteTimeMs = totalLength / playerSpeed * 1000.0f; const float playerRouteTimeMs = totalLength / playerSpeed * 1000.0f;
const float revealDurationMs = (std::min)(kMaximumRevealMs, playerRouteTimeMs / 3.0f); const float revealDurationMs = (std::min)(kMaximumRevealMs, playerRouteTimeMs / 3.0f);
const float waveSpeed = totalLength / (revealDurationMs / 1000.0f); const float waveSpeed = totalLength / (revealDurationMs / 1000.0f);