Research:NPC AI Behaviour: Difference between revisions

From OpenMW Wiki
Jump to navigation Jump to search
(Shielding face from storm added)
(Update research)
 
(7 intermediate revisions by the same user not shown)
Line 9: Line 9:
|The main point for actor behaviour selection
|The main point for actor behaviour selection
|
|
|{{StatusCol|orange|Early, incomplete research}}}}
|{{StatusCol|orange|Incomplete}}}}
====Weight functions====
====Weight functions====
<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
Line 15: Line 15:
if npc is creature: return 0
if npc is creature: return 0
if actor is not player: 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 incapacitated: return 0
if player is invisible or player.chameleon >= 75: return 0
if player is invisible or player.chameleon >= 75: return 0
if npc.hello == 0: return 0
if npc.hello == 0: return 0
if npc.currentAIPackage not in { AIWander, AITravel, AIPursue }: return 0
if npc.currentAIPackage is in { AIEscort, AIFollow, AIActivate }: return 0


x = iGreetDistanceMultiplier * npc.hello
x = iGreetDistanceMultiplier * npc.hello
Line 36: Line 37:
if npc is creature: return 0
if npc is creature: return 0
if npc is incapacitated: return 0
if npc is incapacitated: return 0
if npc is werewolf: return 0
if npc.alarm == 0: return 0
if npc.alarm == 0: return 0
if not npc.isGuard: return 0
if not npc.isGuard: return 0
Line 41: Line 43:
if player is incapacitated: return 0
if player is incapacitated: return 0
if player is invisible or player.chameleon >= 75: 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 player is resisting arrest: return 0
if bounty < iCrimeThreshold: return 0
if bounty < iCrimeThreshold: return 0
Line 51: Line 54:
<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
actionFightWeighting :: (npc, actor) -> weight
actionFightWeighting :: (npc, actor) -> weight
if actor is in combat with an npc escort: return 100
if npc is in combat with actor: return 100
if actor is in combat with an actor escorted by npc: return 100
npc resets in-combat flag and attack state
 
if actor is invisible or actor.chameleon >= 75: return 0
fightTerm = npc.fight
if actor is npc or npc ally: return 0
if actor.isWerewolf or (actor is player and player.isKnownWerewolf): fightTerm += iWerewolfFightMod
if actor is in combat with npc ally: return 100
dist = distanceAttack(npc, actor)    # dist function that ignores vertical distance if actor is flying or swimming
if npc is npc and npc.isGuard and actor is in combat:
 
    if actor is creature and has no allies: return 100
weight = iFightDistanceBase - fFightDistanceMultiplier * distance + fFightDispMult * (50 - disp)
    if actor.isWerewolf or (actor is player and player.isKnownWerewolf): return 100
if weight < 100: return 0
    if npc object has unknown flag and actor is player with a death warrant and the game is not in chargen state: return 100
return weight
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>


<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
actionLookAtWeighting :: (npc, actor) -> weight
actionLookAtWeighting :: (npc, actor) -> weight
if actor is incapacitated: return 0
if npc is in combat: return 0
if actor is invisible or player.chameleon >= 75: return 0
if npc is incapacitated: return 0
if npc current AI package prevents proximity check: return 0   # unclear conditions
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 distance(npc, actor) >= 640: return 0
if actor is an npc escort: return 0
if npc is actor or actor ally or actor enemy: return 0
if actor is being escorted by npc: return 0
return 100
return 100
</syntaxhighlight>
</syntaxhighlight>
Line 81: Line 95:


for each actor in environment: # player is examined first
for each actor in environment: # player is examined first
     if actor is dead: continue
     if actor is dying or dead: continue
     if distance(npc, actor) > 7168: continue
     if distance(npc, actor) > 7168: continue
     # 5-10 more conditions
     if current cell is not actor cell: continue
      
      
     greetWeight = 0
     greetWeight = 0
     crimeWeight = 0
     crimeWeight = 0
     if actor is player:
     if actor is player:
         if npc meets condition x:   # under research
         if npc is not in greet state:
             greetWeight = actionGreetWeighting(npc, actor)
             greetWeight = actionGreetWeighting(npc, actor)
         if npc meets condition y:   # under research
         if npc is not in pursue state:
             crimeWeight = actionCrimeWeighting(npc, actor)
             crimeWeight = actionCrimeWeighting(npc, actor)
     fightWeight = actionFightWeighting(npc, actor)
     fightWeight = actionFightWeighting(npc, actor)
Line 112: Line 126:
         actionWeight = int(crimeWeight)
         actionWeight = int(crimeWeight)
         action = uphold the law
         action = uphold the law
   
 
     if actionWeight != 0 and action != none and npc.target != none:
     if actionWeight != 0 and action != none and npc.target != none:
         if some 20 page function: # includes expensive functions like line of sight and sneak
         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
             npc.target = 0
             actionWeight = 0
             actionWeight = 0
Line 125: Line 139:


====Comments====
====Comments====
With the lookAt action, the NPC looks at its target's head. The anim controller blends the head pose over time.
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.




Line 160: Line 175:


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



Latest revision as of 19:51, 9 October 2019


General behaviour

Decision scan

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

Weight functions

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

Actor scan

For a single actor, labelled npc

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

Comments

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

Shielding face from storm

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

For a non-werewolf humanoid:

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

Comments

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

NPC awareness check

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

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)

NPC side

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

Final check

target = x - y
roll 100, win if roll < target


Comments

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

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

AI Tendencies

Fight

Affected by calm and frenzy spells.

Flee

Affected by rally and demoralize spells.

Alarm

Hello