Research:NPC AI Behaviour

From OpenMW Wiki
Jump to navigation Jump to search

{{#switch:|subgroup|child=|none=|#default=

}}


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
   npc.target = none

elif npc has not greeted and dist < x:

   npc.target = actor
   return 100

else:

   return 0

</syntaxhighlight>

<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:
       npc.target = actor
       actionWeight = int(lookAtWeight)
       action = lookAt
   if greetWeight > actionWeight:
       npc.target = actor
       actionWeight = int(greetWeight)
       action = greet
   if fightWeight > actionWeight:
       npc.target = actor
       actionWeight = int(fightWeight)
       action = fight
   if crimeWeight > actionWeight:
       npc.target = actor
       actionWeight = int(crimeWeight)
       action = uphold the law
   if actionWeight != 0 and action != none and npc.target != 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: 
           npc.target = 0
           actionWeight = 0
           action = none
   
   if action != none: selectedAction = action

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

Comments[edit]

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
   else:
       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

</syntaxhighlight>

Comments[edit]

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

else:

   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

else:

   y = npcTerm * npcFatigueTerm * fSneakViewMult

</syntaxhighlight>

Final check[edit]

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


Comments[edit]

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

else:

   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

</syntaxhighlight>

AI Tendencies[edit]

Fight[edit]

Affected by calm and frenzy spells.

Flee[edit]

Affected by rally and demoralize spells.

Alarm[edit]

Hello[edit]

{{#switch:|subgroup|child=|none=|#default=

}}