Merge branch 'mobiusdoublereacharound' into 'master'

Rewrite melee hit target selection (bug #3438)

Closes #3438

See merge request OpenMW/openmw!3274
macos_ci_fix
psi29a 1 year ago
commit 84019e7998

@ -2,6 +2,7 @@
------
Bug #2623: Snowy Granius doesn't prioritize conjuration spells
Bug #3438: NPCs can't hit bull netch with melee weapons
Bug #3842: Body part skeletons override the main skeleton
Bug #4127: Weapon animation looks choppy
Bug #4204: Dead slaughterfish doesn't float to water surface after loading saved game

@ -252,13 +252,6 @@ namespace MWBase
virtual float getMaxActivationDistance() const = 0;
/// Returns a pointer to the object the provided object would hit (if within the
/// specified distance), and the point where the hit occurs. This will attempt to
/// use the "Head" node, or alternatively the "Bip01 Head" node as a basis.
virtual std::pair<MWWorld::Ptr, osg::Vec3f> getHitContact(
const MWWorld::ConstPtr& ptr, float distance, std::vector<MWWorld::Ptr>& targets)
= 0;
virtual void adjustPosition(const MWWorld::Ptr& ptr, bool force) = 0;
///< Adjust position after load to be on ground. Must be called after model load.
/// @param force do this even if the ptr is flying
@ -546,9 +539,6 @@ namespace MWBase
const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target, bool isRangedCombat)
= 0;
/// Return the distance between actor's weapon and target's collision box.
virtual float getHitDistance(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target) = 0;
virtual void addContainerScripts(const MWWorld::Ptr& reference, MWWorld::CellStore* cell) = 0;
virtual void removeContainerScripts(const MWWorld::Ptr& reference) = 0;

@ -242,24 +242,10 @@ namespace MWClass
if (!weapon.isEmpty())
dist *= weapon.get<ESM::Weapon>()->mBase->mData.mReach;
// For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit
// result.
std::vector<MWWorld::Ptr> targetActors;
getCreatureStats(ptr).getAiSequence().getCombatTargets(targetActors);
std::pair<MWWorld::Ptr, osg::Vec3f> result
= MWBase::Environment::get().getWorld()->getHitContact(ptr, dist, targetActors);
const std::pair<MWWorld::Ptr, osg::Vec3f> result = MWMechanics::getHitContact(ptr, dist);
if (result.first.isEmpty()) // Didn't hit anything
return true;
const MWWorld::Class& othercls = result.first.getClass();
if (!othercls.isActor()) // Can't hit non-actors
return true;
MWMechanics::CreatureStats& otherstats = othercls.getCreatureStats(result.first);
if (otherstats.isDead()) // Can't hit dead actors
return true;
// Note that earlier we returned true in spite of an apparent failure to hit anything alive.
// This is because hitting nothing is not a "miss" and should be handled as such character controller-side.
victim = result.first;

@ -569,25 +569,10 @@ namespace MWClass
* (!weapon.isEmpty() ? weapon.get<ESM::Weapon>()->mBase->mData.mReach
: store.find("fHandToHandReach")->mValue.getFloat());
// For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit
// result.
std::vector<MWWorld::Ptr> targetActors;
if (ptr != MWMechanics::getPlayer())
getCreatureStats(ptr).getAiSequence().getCombatTargets(targetActors);
// TODO: Use second to work out the hit angle
std::pair<MWWorld::Ptr, osg::Vec3f> result = world->getHitContact(ptr, dist, targetActors);
const std::pair<MWWorld::Ptr, osg::Vec3f> result = MWMechanics::getHitContact(ptr, dist);
if (result.first.isEmpty()) // Didn't hit anything
return true;
const MWWorld::Class& othercls = result.first.getClass();
if (!othercls.isActor()) // Can't hit non-actors
return true;
MWMechanics::CreatureStats& otherstats = othercls.getCreatureStats(result.first);
if (otherstats.isDead()) // Can't hit dead actors
return true;
// Note that earlier we returned true in spite of an apparent failure to hit anything alive.
// This is because hitting nothing is not a "miss" and should be handled as such character controller-side.
victim = result.first;

@ -23,6 +23,7 @@
#include "actorutil.hpp"
#include "aicombataction.hpp"
#include "character.hpp"
#include "combat.hpp"
#include "creaturestats.hpp"
#include "movement.hpp"
#include "pathgrid.hpp"
@ -242,7 +243,7 @@ namespace MWMechanics
const osg::Vec3f vActorPos(pos.asVec3());
const osg::Vec3f vTargetPos(target.getRefData().getPosition().asVec3());
float distToTarget = MWBase::Environment::get().getWorld()->getHitDistance(actor, target);
float distToTarget = getDistanceToBounds(actor, target);
storage.mReadyToAttack = (currentAction->isAttackingOrSpell() && distToTarget <= rangeAttack && storage.mLOS);

@ -551,4 +551,102 @@ namespace MWMechanics
return distanceIgnoreZ(lhs, rhs);
return distance(lhs, rhs);
}
float getDistanceToBounds(const MWWorld::Ptr& actor, const MWWorld::Ptr& target)
{
osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3());
osg::Vec3f targetPos(target.getRefData().getPosition().asVec3());
MWBase::World* world = MWBase::Environment::get().getWorld();
float dist = (targetPos - actorPos).length();
dist -= world->getHalfExtents(actor).y();
dist -= world->getHalfExtents(target).y();
return dist;
}
std::pair<MWWorld::Ptr, osg::Vec3f> getHitContact(const MWWorld::Ptr& actor, float reach)
{
// Lasciate ogne speranza, voi ch'entrate
MWWorld::Ptr result;
osg::Vec3f hitPos;
float minDist = std::numeric_limits<float>::max();
MWBase::World* world = MWBase::Environment::get().getWorld();
const MWWorld::Store<ESM::GameSetting>& store = world->getStore().get<ESM::GameSetting>();
const ESM::Position& posdata = actor.getRefData().getPosition();
const osg::Vec3f actorPos(posdata.asVec3());
// Morrowind uses body orientation or camera orientation if available
// The difference between that and this is subtle
osg::Quat actorRot
= osg::Quat(posdata.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(posdata.rot[2], osg::Vec3f(0, 0, -1));
const float fCombatAngleXY = store.find("fCombatAngleXY")->mValue.getFloat();
const float fCombatAngleZ = store.find("fCombatAngleZ")->mValue.getFloat();
const float combatAngleXYcos = std::cos(osg::DegreesToRadians(fCombatAngleXY));
const float combatAngleZcos = std::cos(osg::DegreesToRadians(fCombatAngleZ));
// The player can target any active actor, non-playable actors only target their targets
std::vector<MWWorld::Ptr> targets;
if (actor != getPlayer())
actor.getClass().getCreatureStats(actor).getAiSequence().getCombatTargets(targets);
else
MWBase::Environment::get().getMechanicsManager()->getActorsInRange(
actorPos, Settings::game().mActorsProcessingRange, targets);
for (MWWorld::Ptr& target : targets)
{
if (actor == target || target.getClass().getCreatureStats(target).isDead())
continue;
float dist = getDistanceToBounds(actor, target);
osg::Vec3f targetPos(target.getRefData().getPosition().asVec3());
osg::Vec3f dirToTarget = targetPos - actorPos;
if (dist >= reach || dist >= minDist || std::abs(dirToTarget.z()) >= reach)
continue;
dirToTarget.normalize();
// The idea is to use fCombatAngleXY and fCombatAngleZ as tolerance angles
// in XY and YZ planes of the coordinate system where the actor's orientation
// corresponds to (0, 1, 0) vector. This is not exactly what Morrowind does
// but Morrowind does something (even more) stupid here
osg::Vec3f hitDir = actorRot.inverse() * dirToTarget;
if (combatAngleXYcos * std::abs(hitDir.x()) > hitDir.y())
continue;
// Nice cliff racer hack Todd
if (combatAngleZcos * std::abs(hitDir.z()) > hitDir.y() && !MWMechanics::canActorMoveByZAxis(target))
continue;
// Gotta use physics somehow!
if (!world->getLOS(actor, target))
continue;
minDist = dist;
result = target;
}
// This hit position is currently used for spawning the blood effect.
// Morrowind does this elsewhere, but roughly at the same time
// and it would be hard to track the original hit results outside of this function
// without code duplication
// The idea is to use a random point on a plane in front of the target
// that is defined by its width and height
if (!result.isEmpty())
{
osg::Vec3f resultPos(result.getRefData().getPosition().asVec3());
osg::Vec3f dirToActor = actorPos - resultPos;
dirToActor.normalize();
hitPos = resultPos + dirToActor * world->getHalfExtents(result).y();
// -25% to 25% of width
float xOffset = Misc::Rng::deviate(0.f, 0.25f, world->getPrng());
// 20% to 100% of height
float zOffset = Misc::Rng::deviate(0.6f, 0.4f, world->getPrng());
hitPos.x() += world->getHalfExtents(result).x() * 2.f * xOffset;
hitPos.z() += world->getHalfExtents(result).z() * 2.f * zOffset;
}
return std::make_pair(result, hitPos);
}
}

@ -1,6 +1,8 @@
#ifndef OPENMW_MECHANICS_COMBAT_H
#define OPENMW_MECHANICS_COMBAT_H
#include <utility>
namespace osg
{
class Vec3f;
@ -59,6 +61,11 @@ namespace MWMechanics
float getAggroDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs);
// Cursed distance calculation used for combat proximity and hit checks in Morrowind
float getDistanceToBounds(const MWWorld::Ptr& actor, const MWWorld::Ptr& target);
// Similarly cursed hit target selection
std::pair<MWWorld::Ptr, osg::Vec3f> getHitContact(const MWWorld::Ptr& actor, float reach);
}
#endif

@ -192,93 +192,6 @@ namespace MWPhysics
return true;
}
std::pair<MWWorld::Ptr, osg::Vec3f> PhysicsSystem::getHitContact(const MWWorld::ConstPtr& actor,
const osg::Vec3f& origin, const osg::Quat& orient, float queryDistance, std::vector<MWWorld::Ptr>& targets)
{
// First of all, try to hit where you aim to
int hitmask = CollisionType_World | CollisionType_Door | CollisionType_HeightMap | CollisionType_Actor;
RayCastingResult result = castRay(origin, origin + (orient * osg::Vec3f(0.0f, queryDistance, 0.0f)), actor,
targets, hitmask, CollisionType_Actor);
if (result.mHit)
{
reportCollision(Misc::Convert::toBullet(result.mHitPos), Misc::Convert::toBullet(result.mHitNormal));
return std::make_pair(result.mHitObject, result.mHitPos);
}
// Use cone shape as fallback
const MWWorld::Store<ESM::GameSetting>& store
= MWBase::Environment::get().getESMStore()->get<ESM::GameSetting>();
btConeShape shape(osg::DegreesToRadians(store.find("fCombatAngleXY")->mValue.getFloat() / 2.0f), queryDistance);
shape.setLocalScaling(btVector3(
1, 1, osg::DegreesToRadians(store.find("fCombatAngleZ")->mValue.getFloat() / 2.0f) / shape.getRadius()));
// The shape origin is its center, so we have to move it forward by half the length. The
// real origin will be provided to getFilteredContact to find the closest.
osg::Vec3f center = origin + (orient * osg::Vec3f(0.0f, queryDistance * 0.5f, 0.0f));
btCollisionObject object;
object.setCollisionShape(&shape);
object.setWorldTransform(btTransform(Misc::Convert::toBullet(orient), Misc::Convert::toBullet(center)));
const btCollisionObject* me = nullptr;
std::vector<const btCollisionObject*> targetCollisionObjects;
const Actor* physactor = getActor(actor);
if (physactor)
me = physactor->getCollisionObject();
if (!targets.empty())
{
for (MWWorld::Ptr& target : targets)
{
const Actor* targetActor = getActor(target);
if (targetActor)
targetCollisionObjects.push_back(targetActor->getCollisionObject());
}
}
DeepestNotMeContactTestResultCallback resultCallback(
me, targetCollisionObjects, Misc::Convert::toBullet(origin));
resultCallback.m_collisionFilterGroup = CollisionType_Actor;
resultCallback.m_collisionFilterMask
= CollisionType_World | CollisionType_Door | CollisionType_HeightMap | CollisionType_Actor;
mTaskScheduler->contactTest(&object, resultCallback);
if (resultCallback.mObject)
{
PtrHolder* holder = static_cast<PtrHolder*>(resultCallback.mObject->getUserPointer());
if (holder)
{
reportCollision(resultCallback.mContactPoint, resultCallback.mContactNormal);
return std::make_pair(holder->getPtr(), Misc::Convert::toOsg(resultCallback.mContactPoint));
}
}
return std::make_pair(MWWorld::Ptr(), osg::Vec3f());
}
float PhysicsSystem::getHitDistance(const osg::Vec3f& point, const MWWorld::ConstPtr& target) const
{
btCollisionObject* targetCollisionObj = nullptr;
const Actor* actor = getActor(target);
if (actor)
targetCollisionObj = actor->getCollisionObject();
if (!targetCollisionObj)
return 0.f;
btTransform rayFrom;
rayFrom.setIdentity();
rayFrom.setOrigin(Misc::Convert::toBullet(point));
auto hitpoint = mTaskScheduler->getHitPoint(rayFrom, targetCollisionObj);
if (hitpoint)
return (point - Misc::Convert::toOsg(*hitpoint)).length();
// didn't hit the target. this could happen if point is already inside the collision box
return 0.f;
}
RayCastingResult PhysicsSystem::castRay(const osg::Vec3f& from, const osg::Vec3f& to,
const MWWorld::ConstPtr& ignore, const std::vector<MWWorld::Ptr>& targets, int mask, int group) const
{

@ -207,15 +207,6 @@ namespace MWPhysics
const MWWorld::ConstPtr& ptr, int collisionGroup, int collisionMask) const;
osg::Vec3f traceDown(const MWWorld::Ptr& ptr, const osg::Vec3f& position, float maxHeight);
std::pair<MWWorld::Ptr, osg::Vec3f> getHitContact(const MWWorld::ConstPtr& actor, const osg::Vec3f& origin,
const osg::Quat& orientation, float queryDistance, std::vector<MWWorld::Ptr>& targets);
/// Get distance from \a point to the collision shape of \a target. Uses a raycast to find where the
/// target vector hits the collision shape and then calculates distance from the intersection point.
/// This can be used to find out how much nearer we need to move to the target for a "getHitContact" to be
/// successful. \note Only Actor targets are supported at the moment.
float getHitDistance(const osg::Vec3f& point, const MWWorld::ConstPtr& target) const override;
/// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all
/// other actors.
RayCastingResult castRay(const osg::Vec3f& from, const osg::Vec3f& to,

@ -23,12 +23,6 @@ namespace MWPhysics
public:
virtual ~RayCastingInterface() = default;
/// Get distance from \a point to the collision shape of \a target. Uses a raycast to find where the
/// target vector hits the collision shape and then calculates distance from the intersection point.
/// This can be used to find out how much nearer we need to move to the target for a "getHitContact" to be
/// successful. \note Only Actor targets are supported at the moment.
virtual float getHitDistance(const osg::Vec3f& point, const MWWorld::ConstPtr& target) const = 0;
/// @param me Optional, a Ptr to ignore in the list of results. targets are actors to filter for, ignoring all
/// other actors.
virtual RayCastingResult castRay(const osg::Vec3f& from, const osg::Vec3f& to,

@ -1037,44 +1037,6 @@ namespace MWWorld
return osg::Matrixf::translate(actor.getRefData().getPosition().asVec3());
}
std::pair<MWWorld::Ptr, osg::Vec3f> World::getHitContact(
const MWWorld::ConstPtr& ptr, float distance, std::vector<MWWorld::Ptr>& targets)
{
const ESM::Position& posdata = ptr.getRefData().getPosition();
osg::Quat rot
= osg::Quat(posdata.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(posdata.rot[2], osg::Vec3f(0, 0, -1));
osg::Vec3f halfExtents = mPhysics->getHalfExtents(ptr);
// the origin of hitbox is an actor's front, not center
distance += halfExtents.y();
// special cased for better aiming with the camera
// if we do not hit anything, will use the default approach as fallback
if (ptr == getPlayerPtr())
{
osg::Vec3f pos = getActorHeadTransform(ptr).getTrans();
std::pair<MWWorld::Ptr, osg::Vec3f> result = mPhysics->getHitContact(ptr, pos, rot, distance, targets);
if (!result.first.isEmpty())
return std::make_pair(result.first, result.second);
}
osg::Vec3f pos = ptr.getRefData().getPosition().asVec3();
// general case, compatible with all types of different creatures
// note: we intentionally do *not* use the collision box offset here, this is required to make
// some flying creatures work that have their collision box offset in the air
pos.z() += halfExtents.z();
std::pair<MWWorld::Ptr, osg::Vec3f> result = mPhysics->getHitContact(ptr, pos, rot, distance, targets);
if (result.first.isEmpty())
return std::make_pair(MWWorld::Ptr(), osg::Vec3f());
return std::make_pair(result.first, result.second);
}
void World::deleteObject(const Ptr& ptr)
{
if (!ptr.getRefData().isDeleted() && ptr.getContainerStore() == nullptr)
@ -3010,12 +2972,12 @@ namespace MWWorld
}
else
{
// For actor targets, we want to use hit contact with bounding boxes.
// For actor targets, we want to use melee hit contact.
// This is to give a slight tolerance for errors, especially with creatures like the Skeleton that would
// be very hard to aim at otherwise. For object targets, we want the detailed shapes (rendering
// raycast). If we used the bounding boxes for static objects, then we would not be able to target e.g.
// objects lying on a shelf.
std::pair<MWWorld::Ptr, osg::Vec3f> result1 = getHitContact(actor, fCombatDistance, targetActors);
const std::pair<Ptr, osg::Vec3f> result1 = MWMechanics::getHitContact(actor, fCombatDistance);
// Get the target to use for "on touch" effects, using the facing direction from Head node
osg::Vec3f origin = getActorHeadTransform(actor).getTrans();
@ -3728,15 +3690,6 @@ namespace MWWorld
return (targetPos - weaponPos);
}
float World::getHitDistance(const ConstPtr& actor, const ConstPtr& target)
{
osg::Vec3f weaponPos = actor.getRefData().getPosition().asVec3();
osg::Vec3f halfExtents = mPhysics->getHalfExtents(actor);
weaponPos.z() += halfExtents.z();
return mPhysics->getHitDistance(weaponPos, target) - halfExtents.y();
}
void preload(MWWorld::Scene* scene, const ESMStore& store, const ESM::RefId& obj)
{
if (obj.empty())

@ -345,12 +345,6 @@ namespace MWWorld
float getDistanceToFacedObject() override;
/// Returns a pointer to the object the provided object would hit (if within the
/// specified distance), and the point where the hit occurs. This will attempt to
/// use the "Head" node as a basis.
std::pair<MWWorld::Ptr, osg::Vec3f> getHitContact(
const MWWorld::ConstPtr& ptr, float distance, std::vector<MWWorld::Ptr>& targets) override;
/// @note No-op for items in containers. Use ContainerStore::removeItem instead.
void deleteObject(const Ptr& ptr) override;
@ -627,9 +621,6 @@ namespace MWWorld
osg::Vec3f aimToTarget(
const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target, bool isRangedCombat) override;
/// Return the distance between actor's weapon and target's collision box.
float getHitDistance(const MWWorld::ConstPtr& actor, const MWWorld::ConstPtr& target) override;
bool isPlayerInJail() const override;
void setPlayerTraveling(bool traveling) override;

Loading…
Cancel
Save