1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-02-03 15:15:34 +00:00

aiming to moving target in ranged combat ai

1) Taking into account target move vector and speed. However aiming is not
ideal, since attack strength can't be controlled directly. I did achieve
almost 100% accuracy updating it everyframe but then thought it would be
unfair, cause AI should mimic human targetting.
2) Also added in this commit func to measure real attack durations for
weapon.
This commit is contained in:
mrcheko 2014-06-08 20:59:26 +04:00
parent 8fa7fcdbee
commit 67abc60264
5 changed files with 278 additions and 100 deletions

View file

@ -1,8 +1,6 @@
#include "aicombat.hpp"
#include <OgreMath.h>
#include <OgreVector3.h>
#include "../mwworld/class.hpp"
#include "../mwworld/timestamp.hpp"
@ -14,6 +12,8 @@
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/dialoguemanager.hpp"
#include "../mwrender/animation.hpp"
#include "creaturestats.hpp"
#include "steering.hpp"
@ -30,7 +30,12 @@ namespace
}
//chooses an attack depending on probability to avoid uniformity
void chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement);
ESM::Weapon::AttackType chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement);
void getMinMaxAttackDuration(const MWWorld::Ptr& actor, float (*fMinMaxDurations)[2]);
Ogre::Vector3 AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const Ogre::Vector3& vLastTargetPos,
float duration, int weapType, float strength);
float getZAngleToDir(const Ogre::Vector3& dir, float dirLen = 0.0f)
{
@ -76,18 +81,20 @@ namespace
namespace MWMechanics
{
static const float MAX_ATTACK_DURATION = 0.35f;
static const float DOOR_CHECK_INTERVAL = 1.5f; // same as AiWander
// NOTE: MIN_DIST_TO_DOOR_SQUARED is defined in obstacle.hpp
AiCombat::AiCombat(const MWWorld::Ptr& actor) :
mTargetActorId(actor.getClass().getCreatureStats(actor).getActorId()),
mLastTargetPos(actor.getRefData().getPosition().pos),
mTimerAttack(0),
mTimerReact(0),
mTimerCombatMove(0),
mFollowTarget(false),
mReadyToAttack(false),
mAttack(false),
mStrength(0),
mMinMaxAttackDuration(),
mCombatMove(false),
mMovement(),
mForceNoShortcut(false),
@ -158,9 +165,9 @@ namespace MWMechanics
return true;
if (!actor.getClass().isNpc() && target == MWBase::Environment::get().getWorld()->getPlayerPtr() &&
(actor.getClass().canSwim(actor) && !actor.getClass().canWalk(actor) // pure water creature
&& !MWBase::Environment::get().getWorld()->isSwimming(target)) // Player moved out of water
|| (!actor.getClass().canSwim(actor) && MWBase::Environment::get().getWorld()->isSwimming(target))) // creature can't swim to Player
(actor.getClass().canSwim(actor) && !actor.getClass().canWalk(actor) // 1. pure water creature and Player moved out of water
&& !MWBase::Environment::get().getWorld()->isSwimming(target))
|| (!actor.getClass().canSwim(actor) && MWBase::Environment::get().getWorld()->isSwimming(target))) // 2. creature can't swim to Player
{
actor.getClass().getCreatureStats(actor).setHostile(false);
actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false);
@ -194,6 +201,27 @@ namespace MWMechanics
}
mTimerAttack -= duration;
//TODO: Some skills affect period of strikes.For berserk-like style period ~ 0.25f
float attacksPeriod = 1.0f;
ESM::Weapon::AttackType attackType;
if(mReadyToAttack)
{
if (mMinMaxAttackDuration[0][0] == 0)
{
getMinMaxAttackDuration(actor, mMinMaxAttackDuration);
}
if (mTimerAttack <= 0) mAttack = false;
}
else
{
mTimerAttack = -attacksPeriod;
mAttack = false;
}
actor.getClass().getCreatureStats(actor).setAttackingOrSpell(mAttack);
float tReaction = 0.25f;
@ -213,40 +241,6 @@ namespace MWMechanics
mCell = actor.getCell();
}
//actual attacking logic
//TODO: Some skills affect period of strikes.For berserk-like style period ~ 0.25f
float attacksPeriod = 1.0f;
if(mReadyToAttack)
{
if(mTimerAttack <= -attacksPeriod)
{
//TODO: should depend on time between 'start' to 'min attack'
//for better controlling of NPCs' attack strength.
//Also it seems that this time is different for slash/thrust/chop
mTimerAttack = MAX_ATTACK_DURATION * static_cast<float>(rand())/RAND_MAX;
mAttack = true;
//say a provoking combat phrase
if (actor.getClass().isNpc())
{
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
int chance = store.get<ESM::GameSetting>().find("iVoiceAttackOdds")->getInt();
int roll = std::rand()/ (static_cast<double> (RAND_MAX) + 1) * 100; // [0, 99]
if (roll < chance)
{
MWBase::Environment::get().getDialogueManager()->say(actor, "attack");
}
}
}
else if (mTimerAttack <= 0)
mAttack = false;
}
else
{
mTimerAttack = -attacksPeriod;
mAttack = false;
}
const MWWorld::Class &actorCls = actor.getClass();
const ESM::Weapon *weapon = NULL;
MWMechanics::WeaponType weaptype;
@ -270,9 +264,9 @@ namespace MWMechanics
if (weaptype == WeapType_HandToHand)
{
const MWWorld::Store<ESM::GameSetting> &gmst =
MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
weapRange = gmst.find("fHandToHandReach")->getFloat();
static float fHandToHandReach =
MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fHandToHandReach")->getFloat();
weapRange = fHandToHandReach;
}
else if (weaptype != WeapType_PickProbe && weaptype != WeapType_Spell)
{
@ -289,6 +283,49 @@ namespace MWMechanics
weapRange = 150; //TODO: use true attack range (the same problem in Creature::hit)
}
float rangeAttack;
float rangeFollow;
bool distantCombat = false;
if (weaptype == WeapType_BowAndArrow || weaptype == WeapType_Crossbow || weaptype == WeapType_Thrown)
{
rangeAttack = 1000; // TODO: should depend on archer skill
rangeFollow = 0; // not needed in ranged combat
distantCombat = true;
}
else
{
rangeAttack = weapRange;
rangeFollow = 300;
}
// start new attack
if(mReadyToAttack)
{
if(mTimerAttack <= -attacksPeriod)
{
mAttack = true; // attack starts just now
if (!distantCombat) attackType = chooseBestAttack(weapon, mMovement);
else attackType = ESM::Weapon::AT_Chop; // cause it's =0
mStrength = static_cast<float>(rand()) / RAND_MAX;
mTimerAttack = mMinMaxAttackDuration[attackType][0] +
(mMinMaxAttackDuration[attackType][1] - mMinMaxAttackDuration[attackType][0]) * mStrength;
//say a provoking combat phrase
if (actor.getClass().isNpc())
{
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
int chance = store.get<ESM::GameSetting>().find("iVoiceAttackOdds")->getInt();
int roll = std::rand()/ (static_cast<double> (RAND_MAX) + 1) * 100; // [0, 99]
if (roll < chance)
{
MWBase::Environment::get().getDialogueManager()->say(actor, "attack");
}
}
}
}
/*
* Some notes on meanings of variables:
@ -319,21 +356,6 @@ namespace MWMechanics
* target even if LOS is not achieved)
*/
float rangeAttack;
float rangeFollow;
bool distantCombat = false;
if (weaptype == WeapType_BowAndArrow || weaptype == WeapType_Crossbow || weaptype == WeapType_Thrown)
{
rangeAttack = 1000; // TODO: should depend on archer skill
rangeFollow = 0; // not needed in ranged combat
distantCombat = true;
}
else
{
rangeAttack = weapRange;
rangeFollow = 300;
}
ESM::Position pos = actor.getRefData().getPosition();
Ogre::Vector3 vActorPos(pos.pos);
Ogre::Vector3 vTargetPos(target.getRefData().getPosition().pos);
@ -342,40 +364,46 @@ namespace MWMechanics
bool isStuck = false;
float speed = 0.0f;
if(mMovement.mPosition[1] && (Ogre::Vector3(mLastPos.pos) - vActorPos).length() < (speed = actorCls.getSpeed(actor)) * tReaction / 2)
if(mMovement.mPosition[1] && (mLastActorPos - vActorPos).length() < (speed = actorCls.getSpeed(actor)) * tReaction / 2)
isStuck = true;
mLastPos = pos;
mLastActorPos = vActorPos;
// check if actor can move along z-axis
bool canMoveByZ = (actorCls.canSwim(actor) && MWBase::Environment::get().getWorld()->isSwimming(actor))
|| MWBase::Environment::get().getWorld()->isFlying(actor);
// determine vertical angle to target
// if actor can move along z-axis it will control movement dir
// if can't - it will control correct aiming
mMovement.mRotation[0] = getXAngleToDir(vDirToTarget, distToTarget);
// (within strike dist) || (not quite strike dist while following)
// (within attack dist) || (not quite attack dist while following)
if(distToTarget < rangeAttack || (distToTarget <= rangeFollow && mFollowTarget && !isStuck) )
{
//Melee and Close-up combat
// if we preserve dir.z then horizontal angle can be inaccurate
// getXAngleToDir determines vertical angle to target:
// if actor can move along z-axis it will control movement dir
// if can't - it will control correct aiming.
// note: in getZAngleToDir if we preserve dir.z then horizontal angle can be inaccurate
if (distantCombat)
{
Ogre::Vector3 vAimDir = AimDirToMovingTarget(actor, target, mLastTargetPos, tReaction, weaptype, mStrength);
mLastTargetPos = vTargetPos;
mMovement.mRotation[0] = getXAngleToDir(vAimDir);
mMovement.mRotation[2] = getZAngleToDir(Ogre::Vector3(vAimDir.x, vAimDir.y, 0));
}
else
{
mMovement.mRotation[0] = getXAngleToDir(vDirToTarget, distToTarget);
mMovement.mRotation[2] = getZAngleToDir(Ogre::Vector3(vDirToTarget.x, vDirToTarget.y, 0));
}
// (not quite strike dist while following)
// (not quite attack dist while following)
if (mFollowTarget && distToTarget > rangeAttack)
{
//Close-up combat: just run up on target
mMovement.mPosition[1] = 1;
}
else // (within strike dist)
else // (within attack dist)
{
mMovement.mPosition[1] = 0;
// set slash/thrust/chop attack
if (mAttack && !distantCombat) chooseBestAttack(weapon, mMovement);
if (!mAttack) mMovement.mPosition[1] = 0;
if(mMovement.mPosition[0] || mMovement.mPosition[1])
{
@ -479,9 +507,9 @@ namespace MWMechanics
//special run attack; it shouldn't affect melee combat tactics
if(actorCls.getMovementSettings(actor).mPosition[1] == 1)
{
//check if actor can overcome the distance = distToTarget - attackerWeapRange
//less than in time of playing weapon anim from 'start' to 'hit' tags (t_swing)
//then start attacking
/* check if actor can overcome the distance = distToTarget - attackerWeapRange
less than in time of swinging with weapon (t_swing), then start attacking
*/
float speed1 = actorCls.getSpeed(actor);
float speed2 = target.getClass().getSpeed(target);
if(target.getClass().getMovementSettings(target).mPosition[0] == 0
@ -491,13 +519,16 @@ namespace MWMechanics
float s1 = distToTarget - weapRange;
float t = s1/speed1;
float s2 = speed2 * t;
float t_swing = (MAX_ATTACK_DURATION/2) / weapSpeed;//instead of 0.17 should be the time of playing weapon anim from 'start' to 'hit' tags
float t_swing =
mMinMaxAttackDuration[ESM::Weapon::AT_Thrust][0] +
(mMinMaxAttackDuration[ESM::Weapon::AT_Thrust][1] - mMinMaxAttackDuration[ESM::Weapon::AT_Thrust][0]) * static_cast<float>(rand()) / RAND_MAX;
if (t + s2/speed1 <= t_swing)
{
mReadyToAttack = true;
if(mTimerAttack <= -attacksPeriod)
{
mTimerAttack = MAX_ATTACK_DURATION * static_cast<float>(rand())/RAND_MAX;
mTimerAttack = t_swing;
mAttack = true;
}
}
@ -650,8 +681,10 @@ namespace MWMechanics
namespace
{
void chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement)
ESM::Weapon::AttackType chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement)
{
ESM::Weapon::AttackType attackType;
if (weapon == NULL)
{
//hand-to-hand deal equal damage for each type
@ -660,17 +693,21 @@ void chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement
{
movement.mPosition[0] = (static_cast<float>(rand())/RAND_MAX < 0.5f)? 1: -1;
movement.mPosition[1] = 0;
attackType = ESM::Weapon::AT_Slash;
}
else if(roll <= 0.666f) //forward punch
{
movement.mPosition[1] = 1;
attackType = ESM::Weapon::AT_Thrust;
}
else
{
movement.mPosition[1] = movement.mPosition[0] = 0;
attackType = ESM::Weapon::AT_Chop;
}
return;
}
else
{
//the more damage attackType deals the more probability it has
int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2;
int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2;
@ -683,11 +720,134 @@ void chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement
{
movement.mPosition[0] = (static_cast<float>(rand())/RAND_MAX < 0.5f)? 1: -1;
movement.mPosition[1] = 0;
attackType = ESM::Weapon::AT_Slash;
}
else if(roll <= (static_cast<float>(slash) + static_cast<float>(thrust))/total)
{
movement.mPosition[1] = 1;
attackType = ESM::Weapon::AT_Thrust;
}
else
{
movement.mPosition[1] = movement.mPosition[0] = 0;
attackType = ESM::Weapon::AT_Chop;
}
}
return attackType;
}
void getMinMaxAttackDuration(const MWWorld::Ptr& actor, float (*fMinMaxDurations)[2])
{
if (!actor.getClass().hasInventoryStore(actor)) // creatures
{
fMinMaxDurations[0][0] = fMinMaxDurations[0][1] = 0.1f;
fMinMaxDurations[1][0] = fMinMaxDurations[1][1] = 0.1f;
fMinMaxDurations[2][0] = fMinMaxDurations[2][1] = 0.1f;
return;
}
// get weapon information: type and speed
const ESM::Weapon *weapon = NULL;
MWMechanics::WeaponType weaptype;
MWWorld::ContainerStoreIterator weaponSlot =
MWMechanics::getActiveWeapon(actor.getClass().getCreatureStats(actor), actor.getClass().getInventoryStore(actor), &weaptype);
float weapSpeed;
if (weaptype != MWMechanics::WeapType_HandToHand)
{
weapon = weaponSlot->get<ESM::Weapon>()->mBase;
weapSpeed = weapon->mData.mSpeed;
}
else weapSpeed = 1.0f;
MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(actor);
std::string weapGroup;
MWMechanics::getWeaponGroup(weaptype, weapGroup);
weapGroup = weapGroup + ": ";
bool bRangedWeap = (weaptype >= MWMechanics::WeapType_BowAndArrow && weaptype <= MWMechanics::WeapType_Thrown);
const char *attackType[] = {"chop ", "slash ", "thrust ", "shoot "};
std::string textKey = "start";
std::string textKey2;
// get durations for each attack type
for (int i = 0; i < (bRangedWeap ? 1 : 3); i++)
{
float start1 = anim->getStartTime(weapGroup + (bRangedWeap ? attackType[3] : attackType[i]) + textKey, false);
textKey2 = "min attack";
float start2 = anim->getStartTime(weapGroup + (bRangedWeap ? attackType[3] : attackType[i]) + textKey2, false);
fMinMaxDurations[i][0] = (start2 - start1) / weapSpeed;
textKey2 = "max attack";
start1 = anim->getStartTime(weapGroup + (bRangedWeap ? attackType[3] : attackType[i]) + textKey2, false);
fMinMaxDurations[i][1] = fMinMaxDurations[i][0] + (start1 - start2) / weapSpeed;
}
}
Ogre::Vector3 AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const Ogre::Vector3& vLastTargetPos,
float duration, int weapType, float strength)
{
float projSpeed;
// get projectile speed (depending on weapon type)
if (weapType == ESM::Weapon::MarksmanThrown)
{
static float fThrownWeaponMinSpeed =
MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fThrownWeaponMinSpeed")->getFloat();
static float fThrownWeaponMaxSpeed =
MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fThrownWeaponMaxSpeed")->getFloat();
projSpeed =
fThrownWeaponMinSpeed + (fThrownWeaponMaxSpeed - fThrownWeaponMinSpeed) * strength;
}
else
{
static float fProjectileMinSpeed =
MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fProjectileMinSpeed")->getFloat();
static float fProjectileMaxSpeed =
MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fProjectileMaxSpeed")->getFloat();
projSpeed =
fProjectileMinSpeed + (fProjectileMaxSpeed - fProjectileMinSpeed) * strength;
}
// idea: perpendicular to dir to target speed components of target move vector and projectile vector should be the same
Ogre::Vector3 vActorPos = Ogre::Vector3(actor.getRefData().getPosition().pos);
Ogre::Vector3 vTargetPos = Ogre::Vector3(target.getRefData().getPosition().pos);
Ogre::Vector3 vDirToTarget = vTargetPos - vActorPos;
float distToTarget = vDirToTarget.length();
Ogre::Vector3 vTargetMoveDir = vTargetPos - vLastTargetPos;
vTargetMoveDir /= duration; // |vTargetMoveDir| is target real speed in units/sec now
Ogre::Vector3 vPerpToDir = vDirToTarget.crossProduct(Ogre::Vector3::UNIT_Z);
float velPerp = vTargetMoveDir.dotProduct(vPerpToDir.normalisedCopy());
float velDir = vTargetMoveDir.dotProduct(vDirToTarget.normalisedCopy());
// time to collision between target and projectile
float t_collision;
float projVelDirSquared = projSpeed * projSpeed - velPerp * velPerp;
float projDistDiff = vDirToTarget.dotProduct(vTargetMoveDir.normalisedCopy());
projDistDiff = sqrt(distToTarget * distToTarget - projDistDiff * projDistDiff);
if (projVelDirSquared > 0)
t_collision = projDistDiff / (sqrt(projVelDirSquared) - velDir);
else t_collision = 0; // speed of projectile is not enough to reach moving target
return vTargetPos + vTargetMoveDir * t_collision - vActorPos;
}
}

View file

@ -8,6 +8,8 @@
#include "movement.hpp"
#include "obstacle.hpp"
#include <OgreVector3.h>
#include "../mwworld/cellstore.hpp" // for Doors
#include "../mwbase/world.hpp"
@ -48,12 +50,17 @@ namespace MWMechanics
bool mCombatMove;
bool mBackOffDoor;
float mStrength; // this is actually make sense only in ranged combat
float mMinMaxAttackDuration[3][2]; // slash, thrust, chop has different durations
bool mForceNoShortcut;
ESM::Position mShortcutFailPos;
ESM::Position mLastPos;
Ogre::Vector3 mLastActorPos;
MWMechanics::Movement mMovement;
int mTargetActorId;
Ogre::Vector3 mLastTargetPos;
const MWWorld::CellStore* mCell;
ObstacleCheck mObstacleCheck;

View file

@ -896,16 +896,27 @@ bool Animation::getInfo(const std::string &groupname, float *complete, float *sp
return true;
}
float Animation::getStartTime(const std::string &groupname) const
float Animation::getStartTime(const std::string &groupname, bool onlyGroup) const
{
AnimSourceList::const_iterator iter(mAnimSources.begin());
for(;iter != mAnimSources.end();iter++)
{
const NifOgre::TextKeyMap &keys = (*iter)->mTextKeys;
if (onlyGroup)
{
NifOgre::TextKeyMap::const_iterator found = findGroupStart(keys, groupname);
if(found != keys.end())
return found->first;
}
else
{
for(NifOgre::TextKeyMap::const_iterator iter(keys.begin()); iter != keys.end(); ++iter)
{
if(iter->second.compare(0, groupname.size(), groupname) == 0)
return iter->first;
}
}
}
return -1.f;
}

View file

@ -275,7 +275,7 @@ public:
bool getInfo(const std::string &groupname, float *complete=NULL, float *speedmult=NULL) const;
/// Get the absolute position in the animation track of the first text key with the given group.
float getStartTime(const std::string &groupname) const;
float getStartTime(const std::string &groupname, bool onlyGroup) const;
/// Get the current absolute position in the animation track for the animation that is currently playing from the given group.
float getCurrentTime(const std::string& groupname) const;

View file

@ -32,7 +32,7 @@ float WeaponAnimationTime::getValue() const
void WeaponAnimationTime::setGroup(const std::string &group)
{
mWeaponGroup = group;
mStartTime = mAnimation->getStartTime(mWeaponGroup);
mStartTime = mAnimation->getStartTime(mWeaponGroup, true);
}
void WeaponAnimationTime::updateStartTime()