Research:NPC AI Behaviour

From OpenMW Wiki
Jump to navigation Jump to search



General behaviour[edit]

Decision scan[edit]

Actions affected AI decision check, every 5.0 seconds
Description The main point for actor behaviour selection
Implementation status
Analysis status Incomplete

Weight functions[edit]

<syntaxhighlight lang="python"> actionGreetWeighting :: (npc, actor) -> weight if npc is creature: return 0 if actor is not player: return 0 if player is in combat: return 0 if player is incapacitated: return 0 if player is invisible or player.chameleon >= 75: return 0 if npc.hello == 0: return 0 if npc.currentAIPackage is in { AIEscort, AIFollow, AIActivate }: return 0

x = iGreetDistanceMultiplier * npc.hello dist = distance(npc, actor) if npc has already greeted and dist > 2 * x:

   npc resets greeting flag = none

elif npc has not greeted and dist < x: = actor
   return 100


   return 0


<syntaxhighlight lang="python"> actionCrimeWeighting :: (npc, actor) -> weight if npc is creature: return 0 if npc is incapacitated: return 0 if npc is werewolf: return 0 if npc.alarm == 0: return 0 if not npc.isGuard: return 0 if actor is not player: return 0 if player is incapacitated: return 0 if player is invisible or player.chameleon >= 75: return 0 if bounty is 0: return 0 if player is resisting arrest: return 0 if bounty < iCrimeThreshold: return 0

weight = npc.alarm + 0.1 * bounty if weight < 100: return 0 return weight </syntaxhighlight>

<syntaxhighlight lang="python"> actionFightWeighting :: (npc, actor) -> weight if npc is in combat with actor: return 100 npc resets in-combat flag and attack state if actor is invisible or actor.chameleon >= 75: return 0 if actor is npc or npc ally: return 0 if actor is in combat with npc ally: return 100 if npc is npc and npc.isGuard and actor is in combat:

   if actor is creature and has no allies: return 100
   if actor.isWerewolf or (actor is player and player.isKnownWerewolf): return 100
   if npc object has unknown flag and actor is player with a death warrant and the game is not in chargen state: return 100

if target is player or player ally:

   fightTerm = npc.fight
   if actor.isWerewolf or (actor is player and player.isKnownWerewolf): fightTerm += iWerewolfFightMod
   dist = distance(npc, actor)
   if npc.isFlying or npc.isSwimming: dist -= absHeightDiff(npc, actor)
   fightTerm += iFightDistanceBase - fFightDistanceMultiplier * dist
   if npc is not creature:
       fightTerm += fFightDispMult * (50 - npc.disposition)
   if fightTerm >= 100: return fightTerm

return 0 </syntaxhighlight>

<syntaxhighlight lang="python"> actionLookAtWeighting :: (npc, actor) -> weight if npc is in combat: return 0 if npc is incapacitated: return 0 if actor is invisible or actor.chameleon >= 75: return 0 if npc is not idle: return 0 # needs verification if npc is completely idle: return 0 # also unclear condition if actor is not in combat: return 0 if distance(npc, actor) >= 640: return 0 if npc is actor or actor ally or actor enemy: return 0 return 100 </syntaxhighlight>

Actor scan[edit]

For a single actor, labelled npc <syntaxhighlight lang="python"> actionWeight = 0 selectedAction = none

for each actor in environment: # player is examined first

   if actor is dying or dead: continue
   if distance(npc, actor) > 7168: continue
   if current cell is not actor cell: continue
   greetWeight = 0
   crimeWeight = 0
   if actor is player:
       if npc is not in greet state:
           greetWeight = actionGreetWeighting(npc, actor)
       if npc is not in pursue state:
           crimeWeight = actionCrimeWeighting(npc, actor)
   fightWeight = actionFightWeighting(npc, actor)
   lookAtWeight = actionLookAtWeighting(npc, actor)
   action = none
   if lookAtWeight > actionWeight: = actor
       actionWeight = int(lookAtWeight)
       action = lookAt
   if greetWeight > actionWeight: = actor
       actionWeight = int(greetWeight)
       action = greet
   if fightWeight > actionWeight: = actor
       actionWeight = int(fightWeight)
       action = fight
   if crimeWeight > actionWeight: = actor
       actionWeight = int(crimeWeight)
       action = uphold the law
   if actionWeight != 0 and action != none and != none:
       if not actor.aiActive or not npc.aiActive or actor and npc are not enabled or actor is dying or dead or npc cannot detect actor: 
  = 0
           actionWeight = 0
           action = none
   if action != none: selectedAction = action

if action != none: npc switches to selectedAction </syntaxhighlight>


Hrnchamd: With the lookAt action, the NPC looks at its target's head. The anim controller blends the head pose over time.

Capostrophic, August 2019: It's not really clear what so called "look at" action actually means now that its conditions are updated. NullCascade suggested that it might be avoid/flee action, but for now it remains a mystery.

Idle behaviour[edit]

Shielding face from storm[edit]

Actions affected Every frame.
Description Part of AI animation behaviour.
Implementation status
Analysis status Verified

For a non-werewolf humanoid: <syntaxhighlight lang="python"> facing = Y column of orientation matrix (forward facing vector) windVelocity = interpolated wind velocity (see Weather page)

windSpeed = length(windVelocity)

if windSpeed > fStromWindSpeed and not (actor.weaponDrawn or actor.spellReadied):

   f = normalize(facing)
   w = normalize(windVelocity)
   fAlt = (-f.x, f.y, f.z)
   if dot(fAlt, w) < -0.5:
       if upper body animation is not IdleStorm:
           immediately change upper body animation to IdleStorm, animation loops
       if upper body animation is IdleStorm:
           change IdleStorm loop mode to play once until end key
           set next upper body animation to standard idle for player state



The use of fAlt is inexplicable. The test should probably be dot(f, w) < -0.5. The storm idle applies to the upper body bone group, and has higher priority than movement animations, but lower priority than weapon animations.

Combat behaviour[edit]

NPC awareness check[edit]

Actions affected AI decision check, every 5.0 seconds
Description Part of the NPC perception state.
Implementation status implemented
Analysis status Verified

This check runs every 5 seconds for each NPC. It occurs whether you are sneaking or not, but isn't the same as the combat distance check.

Player side[edit]

<syntaxhighlight lang="python"> if sneaking:

   sneakTerm = fSneakSkillMult * sneak + 0.2 * agility + 0.1 * luck + bootWeight * fSneakBootMult


   sneakTerm = 0

fatigueTerm = fFatigueBase - fFatigueMult*(1 - normalisedFatigue) where normalisedFatigue is a function of fatigue. empty fatigue bar -> 0.0, full fatigue bar -> 1.0

distTerm = fSneakDistanceBase + fSneakDistanceMultiplier*dist x = sneakTerm * distTerm * fatigueTerm + chameleon (+ 100 if invisible) </syntaxhighlight>

NPC side[edit]

<syntaxhighlight lang="python"> npcTerm = npcSneak + 0.2 * npcAgility + 0.1 * npcLuck - npcBlind npcFatigueTerm = fFatigueBase - fFatigueMult * (1 - normalisedFatigue)

using NPC normalisedFatigue

if PC is behind NPC (180 degrees):

   y = npcTerm * npcFatigueTerm * fSneakNoViewMult


   y = npcTerm * npcFatigueTerm * fSneakViewMult


Final check[edit]

<syntaxhighlight lang="python"> target = x - y roll 100, win if roll < target </syntaxhighlight>


Appears straightforward and bug-free. NPCs can take up to five seconds to notice you even if you are not sneaking. This function precedes the combat distance check. I have not identified if there is a line of sight check occuring before or after.

According to this formula, NPCs can still detect you when invisible? That seems wrong. Scrawl (talk) 00:25, 7 January 2014 (CET)

Attack frequency[edit]

Actions affected Upon initiating combat, and after each swing/missile
Description The semi-random delay inserted between physical attacks.
Implementation status Implemented
Analysis status Function contains other side effects

<syntaxhighlight lang="python"> if actor is an npc:

   baseDelay = fCombatDelayNPC


   baseDelay = fCombatDelayCreature

delay = min(baseDelay + 0.01 * rand 100, baseDelay + 0.9)

if actor is in range to attack with current weapon:

   actor will initiate a swing/fire a missile if time since end of last attack >= delay


AI Tendencies[edit]


Affected by calm and frenzy spells.


Affected by rally and demoralize spells.