From b5161790e56b6fa98939c14ab9f94c4bdcfc817e Mon Sep 17 00:00:00 2001 From: David Markowitz Date: Sat, 26 Sep 2026 00:32:59 -0700 Subject: [PATCH] feat: working rotations --- dGame/dComponents/ModelComponent.cpp | 83 +++++++----- dGame/dComponents/ModelComponent.h | 41 +++++- dGame/dComponents/SimplePhysicsComponent.cpp | 27 ++-- dGame/dComponents/SimplePhysicsComponent.h | 9 +- dGame/dPropertyBehaviors/Strip.cpp | 126 +++++-------------- dGame/dPropertyBehaviors/Strip.h | 11 +- resources/worldconfig.ini | 3 + 7 files changed, 145 insertions(+), 155 deletions(-) diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp index 046126a8e..fccd565f2 100644 --- a/dGame/dComponents/ModelComponent.cpp +++ b/dGame/dComponents/ModelComponent.cpp @@ -1,8 +1,13 @@ #include "ModelComponent.h" + +#include + #include "Entity.h" #include "Game.h" #include "Logger.h" +#include "dConfig.h" +#include "dMath.h" #include "BehaviorStates.h" #include "ControlBehaviorMsgs.h" @@ -19,7 +24,6 @@ ModelComponent::ModelComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { m_OriginalPosition = m_Parent->GetDefaultPosition(); m_OriginalRotation = m_Parent->GetDefaultRotation(); - LOG("%f %f %f %f", m_OriginalRotation.x, m_OriginalRotation.y, m_OriginalRotation.z, m_OriginalRotation.w); m_IsPaused = false; m_NumListeningInteract = 0; @@ -43,10 +47,7 @@ bool ModelComponent::OnResetModelToDefaults(GameMessages::ResetModelToDefaults& if (reset.bResetPos) m_Parent->SetPosition(m_OriginalPosition); if (reset.bResetRot) m_Parent->SetRotation(m_OriginalRotation); m_Parent->SetVelocity(NiPoint3Constant::ZERO); - GameMessages::SetAngularVelocity setAngVel; - setAngVel.target = m_Parent->GetObjectID(); - setAngVel.angVelocity = NiPoint3Constant::ZERO; - setAngVel.Send(); + ResetRotationState(m_Parent->GetRotation()); m_Speed = 3.0f; m_NumListeningInteract = 0; @@ -317,36 +318,60 @@ void ModelComponent::SetVelocity(const NiPoint3& velocity) const { m_Parent->SetVelocity(velocity); } -bool ModelComponent::TrySetAngularVelocity(const NiPoint3& angularVelocity) const { - GameMessages::GetAngularVelocity getAngVel{}; - getAngVel.target = m_Parent->GetObjectID(); - if (!getAngVel.Send()) { - LOG("Couldn't get angular velocity for %llu", m_Parent->GetObjectID()); - return false; +bool ModelComponent::TryStartRotation(const int axis, const float direction) { + if (axis < 0 || axis > 2 || direction == 0.0f) return false; + if (m_RotationDirection[axis] != 0.0f) return false; + + // Rebase only when nothing is rotating so simultaneous rotations stay relative to the same base + if (m_RotationDirection == NiPoint3Constant::ZERO) { + ResetRotationState(m_Parent->GetRotation()); + m_SendAngularVelocity = Game::config->GetValue("model_rotation_send_angular_velocity") != "0"; } + m_RotationDegrees[axis] = std::fmod(m_RotationDegrees[axis], 360.0f); + m_RotationActionStart[axis] = m_RotationDegrees[axis]; + m_RotationDirection[axis] = direction > 0.0f ? 1.0f : -1.0f; + SyncAngularVelocity(); + return true; +} + +void ModelComponent::SetRotationProgress(const int axis, const float degrees) { + if (axis < 0 || axis > 2) return; + m_RotationDegrees[axis] = m_RotationActionStart[axis] + degrees; + + // Whole turns wrap to exactly 0 so e.g. 720 degrees yields exactly the base rotation + const NiPoint3 radians( + Math::DegToRad(std::fmod(m_RotationDegrees.x, 360.0f)), + Math::DegToRad(std::fmod(m_RotationDegrees.y, 360.0f)), + Math::DegToRad(std::fmod(m_RotationDegrees.z, 360.0f)) + ); + m_Parent->SetRotation(QuatUtils::FromEuler(radians) * m_RotationBase); +} + +void ModelComponent::StopRotation(const int axis) { + if (axis < 0 || axis > 2) return; + m_RotationDirection[axis] = 0.0f; + SyncAngularVelocity(); +} + +void ModelComponent::SetSpeed(const float newSpeed) { + m_Speed = newSpeed; + if (m_RotationDirection != NiPoint3Constant::ZERO) SyncAngularVelocity(); +} + +void ModelComponent::SyncAngularVelocity() const { GameMessages::SetAngularVelocity setAngVel{}; setAngVel.target = m_Parent->GetObjectID(); - if (angularVelocity != NiPoint3Constant::ZERO) { - setAngVel.angVelocity = getAngVel.angVelocity; - const auto [x, y, z] = angularVelocity * m_Speed; - if (x != 0.0f) { - if (getAngVel.angVelocity.x != 0.0f) return false; - setAngVel.angVelocity.x = x; - } else if (y != 0.0f) { - if (getAngVel.angVelocity.y != 0.0f) return false; - setAngVel.angVelocity.y = y; - } else if (z != 0.0f) { - if (getAngVel.angVelocity.z != 0.0f) return false; - setAngVel.angVelocity.z = z; - } - } else { - setAngVel.angVelocity = angularVelocity; - } - LOG("Setting angular velocity to %f %f %f", setAngVel.angVelocity.x, setAngVel.angVelocity.y, setAngVel.angVelocity.z); + setAngVel.angVelocity = m_SendAngularVelocity ? m_RotationDirection * Math::DegToRad(GetAngularSpeed()) : NiPoint3Constant::ZERO; setAngVel.Send(); +} - return true; +void ModelComponent::ResetRotationState(const NiQuaternion& newBase) { + m_RotationBase = newBase; + m_RotationDegrees = NiPoint3Constant::ZERO; + m_RotationActionStart = NiPoint3Constant::ZERO; + m_RotationDirection = NiPoint3Constant::ZERO; + SyncAngularVelocity(); } void ModelComponent::OnChatMessageReceived(const std::string& sMessage) { diff --git a/dGame/dComponents/ModelComponent.h b/dGame/dComponents/ModelComponent.h index 29395eb50..3a1dec0f4 100644 --- a/dGame/dComponents/ModelComponent.h +++ b/dGame/dComponents/ModelComponent.h @@ -147,17 +147,25 @@ public: // Force sets the velocity to a value. void SetVelocity(const NiPoint3& velocity) const; - // Attempts to set the angular velocity of the model. - // If the axis currently has a velocity of zero, returns true. - // If the axis is currently controlled by a behavior, returns false. - bool TrySetAngularVelocity(const NiPoint3& angularVelocity) const; + // Attempts to claim a world axis (0 = x, 1 = y, 2 = z) for rotation in direction (+1 or -1). + // Returns false if the axis is already controlled by a behavior. + bool TryStartRotation(const int axis, const float direction); + + // Sets how many signed degrees the active rotation on axis has progressed and updates the entity. + void SetRotationProgress(const int axis, const float degrees); + + // Releases the axis so another behavior can rotate it. + void StopRotation(const int axis); + + // Degrees per second for rotation actions at the current speed. + float GetAngularSpeed() const noexcept { return BASE_ANGULAR_SPEED * m_Speed; } void OnChatMessageReceived(const std::string& sMessage); void OnHit(); // Sets the speed of the model - void SetSpeed(const float newSpeed) { m_Speed = newSpeed; } + void SetSpeed(const float newSpeed); // Whether or not to restart at the end of the frame void RestartAtEndOfFrame() { m_RestartAtEndOfFrame = true; } @@ -172,6 +180,14 @@ public: float GetSpeed() const noexcept { return m_Speed; } private: + // Degrees per second per unit of m_Speed + static constexpr float BASE_ANGULAR_SPEED = 15.0f; + + // Sends the client-side angular velocity for the currently active rotation axes. + void SyncAngularVelocity() const; + + // Clears all rotation state and makes rotation relative to newBase. + void ResetRotationState(const NiQuaternion& newBase); // Loads a behavior from the database. void LoadBehavior(const LWOOBJID behaviorID, const size_t index, const bool isIndexed); @@ -220,4 +236,19 @@ private: // Whether or not to restart at the end of the frame. bool m_RestartAtEndOfFrame{ false }; + + // The rotation that m_RotationDegrees is applied on top of + NiQuaternion m_RotationBase = QuatUtils::IDENTITY; + + // Accumulated signed degrees per world axis since m_RotationBase was set + NiPoint3 m_RotationDegrees{}; + + // m_RotationDegrees at the moment the current rotation on each axis started + NiPoint3 m_RotationActionStart{}; + + // Per axis -1, 0 or 1. Non-zero means a behavior currently owns rotation on that axis. + NiPoint3 m_RotationDirection{}; + + // Whether the client is sent an angular velocity to extrapolate rotation between updates + bool m_SendAngularVelocity{ true }; }; diff --git a/dGame/dComponents/SimplePhysicsComponent.cpp b/dGame/dComponents/SimplePhysicsComponent.cpp index b3f3c07aa..f699938c5 100644 --- a/dGame/dComponents/SimplePhysicsComponent.cpp +++ b/dGame/dComponents/SimplePhysicsComponent.cpp @@ -40,20 +40,11 @@ SimplePhysicsComponent::~SimplePhysicsComponent() { } void SimplePhysicsComponent::Update(const float deltaTime) { - if (m_Velocity != NiPoint3Constant::ZERO) { - m_Position += m_Velocity * deltaTime; - m_DirtyPosition = true; - Game::entityManager->SerializeEntity(m_Parent); - } - - if (m_AngularVelocity != NiPoint3Constant::ZERO) { - m_Rotation.Normalize(); - const auto vel = NiQuaternion::FromEulerAngles(m_AngularVelocity * deltaTime); - m_Rotation *= vel; - const auto euler = m_Rotation.GetEulerAngles(); - m_DirtyPosition = true; - Game::entityManager->SerializeEntity(m_Parent); - } + // Rotation is driven by ModelComponent; angular velocity is only relayed to clients. + if (m_Velocity == NiPoint3Constant::ZERO) return; + m_Position += m_Velocity * deltaTime; + m_DirtyPosition = true; + Game::entityManager->SerializeEntity(m_Parent); } void SimplePhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { @@ -102,17 +93,15 @@ bool SimplePhysicsComponent::OnGetObjectReportInfo(GameMessages::GetObjectReport return true; } -bool SimplePhysicsComponent::OnSetAngularVelocity(GameMessages::GameMsg& msg) { - auto& setAngVel = static_cast(msg); +bool SimplePhysicsComponent::OnSetAngularVelocity(GameMessages::SetAngularVelocity& setAngVel) { m_DirtyVelocity |= setAngVel.bForceFlagDirty || (m_AngularVelocity != setAngVel.angVelocity); m_AngularVelocity = setAngVel.angVelocity; - LOG("Velocity is now %f %f %f", m_AngularVelocity.x, m_AngularVelocity.y, m_AngularVelocity.z); + LOG_DEBUG("Angular velocity is now %f %f %f", m_AngularVelocity.x, m_AngularVelocity.y, m_AngularVelocity.z); Game::entityManager->SerializeEntity(m_Parent); return true; } -bool SimplePhysicsComponent::OnGetAngularVelocity(GameMessages::GameMsg& msg) { - auto& getAngVel = static_cast(msg); +bool SimplePhysicsComponent::OnGetAngularVelocity(GameMessages::GetAngularVelocity& getAngVel) { getAngVel.angVelocity = m_AngularVelocity; return true; } diff --git a/dGame/dComponents/SimplePhysicsComponent.h b/dGame/dComponents/SimplePhysicsComponent.h index e686a0db6..bf9b7f176 100644 --- a/dGame/dComponents/SimplePhysicsComponent.h +++ b/dGame/dComponents/SimplePhysicsComponent.h @@ -22,6 +22,11 @@ enum class eClimbableType : int32_t { CLIMBABLE_TYPE_WALL_STICK }; +namespace GameMessages { + struct SetAngularVelocity; + struct GetAngularVelocity; +} + /** * Component that serializes locations of entities to the client @@ -61,8 +66,8 @@ public: */ void SetAngularVelocity(const NiPoint3& value) { m_AngularVelocity = value; m_DirtyVelocity = true; } - bool OnSetAngularVelocity(GameMessages::GameMsg& msg); - bool OnGetAngularVelocity(GameMessages::GameMsg& msg); + bool OnSetAngularVelocity(GameMessages::SetAngularVelocity& setAngVel); + bool OnGetAngularVelocity(GameMessages::GetAngularVelocity& getAngVel); /** * Returns the physics motion state diff --git a/dGame/dPropertyBehaviors/Strip.cpp b/dGame/dPropertyBehaviors/Strip.cpp index 4b94d59af..a85ffd989 100644 --- a/dGame/dPropertyBehaviors/Strip.cpp +++ b/dGame/dPropertyBehaviors/Strip.cpp @@ -9,7 +9,6 @@ #include "PropertyManagementComponent.h" #include "PlayerManager.h" #include "SimplePhysicsComponent.h" -#include "dMath.h" #include "dChatFilter.h" @@ -108,6 +107,8 @@ void Strip::HandleMsg(GameMessages::ResetModelToDefaults& msg) { m_NextActionIndex = 0; m_InActionTranslation = NiPoint3Constant::ZERO; m_PreviousFramePosition = NiPoint3Constant::ZERO; + m_InActionRotation = NiPoint3Constant::ZERO; + m_RotationProgress = 0.0f; } void Strip::OnChatMessageReceived(const std::string& sMessage) { @@ -173,7 +174,6 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent, Up // TODO replace with switch case and nextActionType with enum /* BEGIN Move */ if (nextActionType == "MoveRight" || nextActionType == "MoveLeft") { - m_IsRotating = false; // X axis bool isMoveLeft = nextActionType == "MoveLeft"; int negative = isMoveLeft ? -1 : 1; @@ -183,7 +183,6 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent, Up m_InActionTranslation.x = isMoveLeft ? -number : number; } } else if (nextActionType == "FlyUp" || nextActionType == "FlyDown") { - m_IsRotating = false; // Y axis bool isFlyDown = nextActionType == "FlyDown"; int negative = isFlyDown ? -1 : 1; @@ -194,7 +193,6 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent, Up } } else if (nextActionType == "MoveForward" || nextActionType == "MoveBackward") { - m_IsRotating = false; // Z axis bool isMoveBackward = nextActionType == "MoveBackward"; int negative = isMoveBackward ? -1 : 1; @@ -208,40 +206,25 @@ void Strip::ProcNormalAction(float deltaTime, ModelComponent& modelComponent, Up /* BEGIN Rotate */ else if (nextActionType == "Spin" || nextActionType == "SpinNegative") { - const float radians = Math::DegToRad(number); - bool isSpinNegative = nextActionType == "SpinNegative"; - float negative = isSpinNegative ? -0.261799f : 0.261799f; - - // Default angular velocity is 3 units per second. - if (modelComponent.TrySetAngularVelocity(NiPoint3Constant::UNIT_Y * negative)) { - m_IsRotating = true; - m_InActionTranslation.y = isSpinNegative ? -number : number; - m_PreviousFrameRotation = entity.GetRotation(); - // d/vi = t - // radians/velocity = time - // only care about the time, direction is irrelevant here + // Y axis + const float direction = nextActionType == "SpinNegative" ? -1.0f : 1.0f; + if (number != 0.0 && modelComponent.TryStartRotation(1, direction)) { + m_InActionRotation.y = direction * number; + m_RotationProgress = 0.0f; } } else if (nextActionType == "Tilt" || nextActionType == "TiltNegative") { - const float radians = Math::DegToRad(number); - bool isRotateLeft = nextActionType == "TiltNegative"; - float negative = isRotateLeft ? -0.261799f : 0.261799f; - - // Default angular velocity is 3 units per second. - if (modelComponent.TrySetAngularVelocity(NiPoint3Constant::UNIT_X * negative)) { - m_IsRotating = true; - m_InActionTranslation.x = isRotateLeft ? -number : number; - m_PreviousFrameRotation = entity.GetRotation(); + // X axis + const float direction = nextActionType == "TiltNegative" ? -1.0f : 1.0f; + if (number != 0.0 && modelComponent.TryStartRotation(0, direction)) { + m_InActionRotation.x = direction * number; + m_RotationProgress = 0.0f; } } else if (nextActionType == "Roll" || nextActionType == "RollNegative") { - const float radians = Math::DegToRad(number); - bool isRotateDown = nextActionType == "RollNegative"; - float negative = isRotateDown ? -0.261799f : 0.261799f; - - // Default angular velocity is 3 units per second. - if (modelComponent.TrySetAngularVelocity(NiPoint3Constant::UNIT_Z * negative)) { - m_IsRotating = true; - m_InActionTranslation.z = isRotateDown ? -number : number; - m_PreviousFrameRotation = entity.GetRotation(); + // Z axis + const float direction = nextActionType == "RollNegative" ? -1.0f : 1.0f; + if (number != 0.0 && modelComponent.TryStartRotation(2, direction)) { + m_InActionRotation.z = direction * number; + m_RotationProgress = 0.0f; } } /* END Rotate */ @@ -346,8 +329,6 @@ void Strip::RemoveStates(ModelComponent& modelComponent) const { } bool Strip::CheckMovement(float deltaTime, ModelComponent& modelComponent) { - if (m_IsRotating) return true; - auto& entity = *modelComponent.GetParent(); const auto& currentPos = entity.GetPosition(); const auto diff = currentPos - m_PreviousFramePosition; @@ -393,65 +374,24 @@ bool Strip::CheckMovement(float deltaTime, ModelComponent& modelComponent) { } bool Strip::CheckRotation(float deltaTime, ModelComponent& modelComponent) { - if (!m_IsRotating) return true; - GameMessages::GetAngularVelocity getAngVel{}; - getAngVel.target = modelComponent.GetParent()->GetObjectID(); - getAngVel.Send(); - const auto curRotation = modelComponent.GetParent()->GetRotation(); - const auto diff = m_PreviousFrameRotation.Diff(curRotation).GetEulerAngles(); - LOG("Diff: x=%f, y=%f, z=%f", std::abs(Math::RadToDeg(diff.x)), std::abs(Math::RadToDeg(diff.y)), std::abs(Math::RadToDeg(diff.z))); - LOG("Velocity: x=%f, y=%f, z=%f", Math::RadToDeg(getAngVel.angVelocity.x) * deltaTime, Math::RadToDeg(getAngVel.angVelocity.y) * deltaTime, Math::RadToDeg(getAngVel.angVelocity.z) * deltaTime); - m_PreviousFrameRotation = curRotation; - auto angVel = diff; - angVel.x = std::abs(Math::RadToDeg(angVel.x)); - angVel.y = std::abs(Math::RadToDeg(angVel.y)); - angVel.z = std::abs(Math::RadToDeg(angVel.z)); - const auto [rotateX, rotateY, rotateZ] = m_InActionTranslation; - bool rotateFinished = true; - NiPoint3 finalRotationAdjustment = NiPoint3Constant::ZERO; - if (rotateX != 0.0f) { - m_InActionTranslation.x -= angVel.x; - rotateFinished = std::signbit(m_InActionTranslation.x) != std::signbit(rotateX); - finalRotationAdjustment.x = Math::DegToRad(m_InActionTranslation.x); - } else if (rotateY != 0.0f) { - m_InActionTranslation.y -= angVel.y; - rotateFinished = std::signbit(m_InActionTranslation.y) != std::signbit(rotateY); - finalRotationAdjustment.y = Math::DegToRad(m_InActionTranslation.y); - } else if (rotateZ != 0.0f) { - m_InActionTranslation.z -= angVel.z; - rotateFinished = std::signbit(m_InActionTranslation.z) != std::signbit(rotateZ); - finalRotationAdjustment.z = Math::DegToRad(m_InActionTranslation.z); + for (int axis = 0; axis < 3; axis++) { + const float target = m_InActionRotation[axis]; + if (target == 0.0f) continue; + + // Snapping to the target keeps the final angle exact regardless of speed or frame time + const float step = modelComponent.GetAngularSpeed() * deltaTime; + if (std::abs(target - m_RotationProgress) <= step) m_RotationProgress = target; + else m_RotationProgress += std::copysign(step, target); + + modelComponent.SetRotationProgress(axis, m_RotationProgress); + if (m_RotationProgress != target) return false; + + modelComponent.StopRotation(axis); + m_InActionRotation = NiPoint3Constant::ZERO; + m_RotationProgress = 0.0f; } - if (rotateFinished && m_InActionTranslation != NiPoint3Constant::ZERO) { - LOG("Rotation finished, zeroing angVel"); - - angVel.x = Math::DegToRad(angVel.x); - angVel.y = Math::DegToRad(angVel.y); - angVel.z = Math::DegToRad(angVel.z); - - if (rotateX != 0.0f) getAngVel.angVelocity.x = 0.0f; - else if (rotateY != 0.0f) getAngVel.angVelocity.y = 0.0f; - else if (rotateZ != 0.0f) getAngVel.angVelocity.z = 0.0f; - - GameMessages::SetAngularVelocity setAngVel{}; - setAngVel.target = modelComponent.GetParent()->GetObjectID(); - setAngVel.angVelocity = getAngVel.angVelocity; - setAngVel.Send(); - - // Do the final adjustment so we will have rotated exactly the requested units - auto currentRot = modelComponent.GetParent()->GetRotation(); - NiQuaternion finalAdjustment = NiQuaternion::FromEulerAngles(finalRotationAdjustment); - currentRot *= finalAdjustment; - currentRot.Normalize(); - modelComponent.GetParent()->SetRotation(currentRot); - - m_InActionTranslation = NiPoint3Constant::ZERO; - m_IsRotating = false; - } - - LOG("angVel: x=%f, y=%f, z=%f", m_InActionTranslation.x, m_InActionTranslation.y, m_InActionTranslation.z); - return rotateFinished; + return true; } void Strip::Update(float deltaTime, ModelComponent& modelComponent, UpdateResult& updateResult) { diff --git a/dGame/dPropertyBehaviors/Strip.h b/dGame/dPropertyBehaviors/Strip.h index e61543e95..142dfeb1d 100644 --- a/dGame/dPropertyBehaviors/Strip.h +++ b/dGame/dPropertyBehaviors/Strip.h @@ -52,9 +52,6 @@ private: // Indicates this Strip is waiting for an action to be taken upon it to progress to its actions bool m_WaitingForAction{ false }; - // True if this strip is currently rotating - bool m_IsRotating{ false }; - // The amount of time this strip is paused for. Any interactions with this strip should be bounced if this is greater than 0. // Actions that do not use time do not use this (ex. positions). float m_PausedTime{ 0.0f }; @@ -75,11 +72,11 @@ private: // The position of the parent model on the previous frame NiPoint3 m_PreviousFramePosition{}; - NiPoint3 m_RotationRemaining{}; + // The signed target degrees of the current rotation action. Only 1 axis is active at once for any given strip. + NiPoint3 m_InActionRotation{}; - NiQuaternion m_PreviousFrameRotation{}; - - NiPoint3 m_SavedVelocity{}; + // The signed degrees the current rotation action has progressed so far + float m_RotationProgress{ 0.0f }; }; #endif //!__STRIP__H__ diff --git a/resources/worldconfig.ini b/resources/worldconfig.ini index 5d23a32f6..415db64b3 100644 --- a/resources/worldconfig.ini +++ b/resources/worldconfig.ini @@ -105,3 +105,6 @@ hardcore_coin_keep= # save pre-split lxfmls to disk for debugging save_lxfmls=0 + +# Send models' angular velocity to clients so they extrapolate rotation between updates (1) or only send rotation (0) +model_rotation_send_angular_velocity=1