NPCs, Horses, and you

From the Oblivion ConstructionSet Wiki
Jump to navigation Jump to search


This how-to was not designed for beginners but for people who have been banging their head against the same bloody wall since they created their first NPC ... When the phrases "MOUNT DA$#@#!" or the equally agitated 'GET OFF THE D^@# HORSE, B#@^H!" issued forth in abundance. If this is you, as it was me, then perhaps this HOW-TO may grant you a measure of sanity.

Purpose[edit | edit source]

The purpose of this how-to is...

  • To get your NPC to mount when you mount.
  • To get your NPC to dismount when you dismount.

It is not:

  • The best solution to the problem, I'm sure.
  • A particular fast or optimized solution.
  • Nor, I'm sure, is it the best conceptual or compliant solution.

With this in mind, proceed at your own risk....

Important Factors[edit | edit source]

After hours of tinkering I was able to infer a few things about how horses and NPCs interact. I expect there may be some inaccuracies or perhaps they are simply wrong; regardless, for me they were the secrets to unlocking the mysteries of NPCs, horses, and you. ;)

  • NPCs respond to the TRAVEL package with the 'Use Horse' flag correctly and mount on the horse. The FOLLOW package with the 'Use Horse' flag did not appear to cause the NPC to mount the horse.
  • Likewise, NPCs respond to the TRAVEL package without the horse flag correctly and dismount, if mounted; Again, the FOLLOW package seemingly does not have this effect.
  • Unlike mounting, dismounting cannot be triggered with the addScriptPackage function. Instead, the dismount has to be triggered by an evaluatePackage (evp) command.

The Packages[edit | edit source]

You need to create four AI packages for your NPC, as follows:

  • PACKAGE 1 - TRAVEL
    • NAME: 'travelToHorse'
    • PURPOSE: To get the NPC moving to the horse. When the NPC is close enough to the horse, the USE HORSE bit will kick in and the NPC will mount.
    • STORAGE: Outside the NPCs Package List
    • FLAGS
      • Use Horse
    • LOCATION: HorseType (ex:E3Horse)
  • PACKAGE 2 - FOLLOW
    • NAME: 'followOnHorse'
    • PURPOSE: After the NPC has mounted this package will be activated, causing the NPC to follow the player.
    • STORAGE: Outside the NPCs Package List
    • FLAGS
      • Use Horse
    • TARGET: ('Abandoned Mine','PlayerRef')
      • DISTANCE: 350 (Main Quests use this)
  • PACKAGE 3 - TRAVEL
    • NAME: 'travelToPlayer'
    • PURPOSE: Causes the NPC to dismount and travel to the player.
    • STORAGE: First in NPCs package list
    • CONDITION: getScriptVariable( CELL , dismountFlag )==1 and isRidingHorse==1
      • Where CELL is the initial cell the NPC was created in, view the USE INFO for the NPC to see where that is.
    • LOCATION: Player
  • PACKAGE 4 - FOLLOW
    • NAME: 'followOnFoot'
    • PURPOSE: Causes the NPC to follow the player on foot.
    • STORAGE: Outside the NPCs Package List
    • TARGET: Player
      • DISTANCE: 0 (default)

The Script[edit | edit source]

This script should be incorporated into your NPC's primary script.

float mountTimer
short isMountTimerOn
short mounted
short dismountFlag

begin gamemode
    ;dismount section
    if mounted == 1 
        if Player.IsRidingHorse == 0
            if IsRidingHorse == 0
                Message "NPC has dismounted"
                set dismountFlag to 0
                set mounted to 0
                set isMountTimerOn to 0
                set mountTimer to 0
                addScriptPackage followOnFoot
            else
                if isMountTimerOn == 0
                    Message "Trying to get NPC to dismount"
                    set isMountTimerOn to 1
                    set mountTimer to 3
                    set dismountFlag to 1
                    evp
                else
                    if mountTimer <= 0
                        Message "dismountTimer expired"
                        set mountTimer to 3
                        evp 
                    else
                        set mountTimer to mountTimer - getSecondsPassed
                    endif
                endif
            endif
        else ;Timing issues
            if isRidingHorse == 0 ;NPC managed to dismount
                Message "Player has mounted out of turn, NPC dismounted"
                addScriptPackage followOnFoot
                set isMountTimerOn to 0
                set mountTimer to 0
                set dismountFlag to 0
                set mounted to 0
            else
                if isMountTimerOn == 1 ;NPC still trying to dismount
                    Message "Player has mounted out of turn, in the timer."
                    removeScriptPackage
                    addScriptPackage followOnHorse
                    set isMountTimerOn to 0
                    set mountTimer to 0
                    set dismountFlag to 0
                endif
            endif
        endif
    endif

    ;mount section
    if mounted == 0 
        if Player.isRidingHorse==1
            if isRidingHorse==1
                Message "NPC is Mounted"
                set mounted to 1
                set isMountTimerOn to 0
                set mountTimer to 0
                addScriptPackage followOnHorse
            else
                if isMountTimerOn == 0
                    Message "Trying to get NPC to Mount"
                    set isMountTimerOn to 1
                    set mountTimer to 3
                    addScriptPackage travelToHorse
                else
                    if isMountTimerOn <= 0
                        Message "mountTimer expired"
                        set mountTimer to 3
                        addScriptPackage travelToHorse
                    else
                        set mountTimer to mountTimer - getSecondsPassed
                    endif
                endif
            endif
        else ;Timing issues
            if isRidingHorse == 1 ;NPC managed to mount
                Message "Player has dismounted out of turn, NPC mounted"
                addScriptPackage followOnHorse
                set isMountTimerOn to 0
                set mountTimer to 0
                set mounted to 1
            else
                if isMountTimerOn == 1 ;caught while waiting for NPC to mount
                    Message "Player has dismounted out of turn, in the timer."
                    removeScriptPackage
                    addScriptPackage followOnFoot
                    set isMountTimerOn to 0
                    set mountTimer to 0
                endif
            endif
        endif
    endif		
End

Caveats and Notes[edit | edit source]

Caveats[edit | edit source]

  • This script assumes that the NPC's horse is nearby, you could be in for long wait if its far away. You could use the getDistance function to determine if the NPC bothers to mount or not.
  • Obviously the NPC must own the horse either by faction or direct ownership.
  • I used a Unique Horse ID 'E3Horse' so the NPC couldn't be confused by other horses nearby.
  • Do not use 'continue when PC is near' in 'Package 2' or the NPC will never dismount.

Notes[edit | edit source]

  • The 'mounted' variable in the script is of type short created at the beginning and set to 0. Its just a toggle so that I know to change the package.
  • While mounted is used for mounting and dismounting, the dismountFlag is a special indicator to cause only a particular package to get evaluated, and is only used when dismounting.
  • You may wonder why I have a timer for mounting and dismounting...hehe...its because the darn NPCs keep forgetting what their supposed to be doing. Sometimes you can actually see that they will move for a bit and then stop and then when the "timer up" message appears they will start moving again. Think of it as pretty little cattle prod designed just for NPC fannies. ;)

Hope this was helpful. -- Breave Apr 26, 2006

Another way to get horse and NPC following[edit | edit source]

The solution above inspired me to work on a, let's say, more reliable solution using a slightly different coding approach. As said before, this is not really for beginners, as you should know, how to use quest topics to set script variables amongst other things.

Introduction[edit | edit source]

The following how-to will lead to an NPC that will follow riding or walking, depending on the players action. In combat, the NPC will leave his horse, if he is attacked. You may need quest topics, that set the appropriate flags in the script. The NPC has also the ability to call his horse, if it is not near.

  1. The FollowPlayer script variable
    • Setting the FollowPlayer script variable to 1, will cause the NPC to follow by horse or walking, depending on the players action. Setting it to 2, will cause the NPC to follow always walking. Setting it to 3 will make the NPC wait for the player.
  2. The SneakAsPlayer script variable
    • Setting the SneakAsPlayer script variable to 1, will cause the NPC to sneak, whenever the player is sneaking. Setting it to 2 will cause the NPC to sneak always.
  3. The CallHorse script variable
    • Setting the CallHorse script variable to 1 will cause the NPC to call his horse.

The other related objects I've used where a01HorseC01 (with reference a01HorseC01Ref) for the horse and a01Companion01 (with reference a01C01Ref) for the companion.

The Packages[edit | edit source]

The packages, directly related to the NPC need to have conditions set, to avoid, that they overrule the follow packages. For the wander package I have set the condition IsInCombat == 1. The eat and sleep packages I have set to FollowPlayer == 0 OR FollowPlayer == 3 (using GetScriptVariable, Reference a01Companion01). You may set an additional wander package to the same conditions.

Now lets move to the follow packages:

  1. a01AIC01FollowWalking - FOLLOW package type
    • Flags set: Skip Fallout Behavior, Allow Swimming, Allow Falls, Defensive Combat
    • Conditions: NONE
    • Locations: NONE
    • Target: Player, Distance 150
    • Package Location: Not related to any object
  2. a01AIC01FollowRiding - FOLLOW package type
    • Flags set: Skip Fallout Behavior, Use Horse, Allow Swimming, Allow Falls, Defensive Combat
    • Conditions: NONE
    • Locations: NONE
    • Target: Player, Distance 350
    • Package Location: Not related to any object
  3. a01AIC01MountHorse - TRAVEL package type
    • Flags set: Skip Fallout Behavior, Use Horse, Allow Swimming, Allow Falls, Defensive Combat
    • Conditions: NONE
    • Locations: HorseRef
    • Target: NONE
    • Package Location: Not related to any object
  4. a01AIC01DismountHorse - TRAVEL package type
    • Flags set: Skip Fallout Behavior, Allow Swimming, Allow Falls, Defensive Combat
    • Conditions: NONE
    • Locations: NONE
    • Target: Player
    • Package Location: Not related to any object
  5. a01AIC01Wait - WANDER package type
    • Flags set: Skip Fallout Behavior, Allow Swimming, Allow Falls, Defensive Combat
    • Conditions: NONE
    • Locations: Near Current Location 350
    • Target: NONE
    • Package Location: Not related to any object

The Script[edit | edit source]

ScriptName a01ScrCompanion01

; Script for companion with follow functions.
;
; The companion should follow the player and mimic his actions, e.g.
; walk when player is walking, ride when player is riding, sneak when
; player is sneaking

;==============================================================================
;==============================================================================
; After lots of testing and despair I came to the conclusion, that script
; execution is prevented, if a follow package is running and relying on
; script variables in the condition. The script is run once, and, if the
; conditions are met for a follow package it will run and never allow the
; script to be executed again.

; So, first, the AI-Packages should not be related directly to script variables
; Second, the running quest package script runs ALWAYS every 5 seconds, if the
; fQuestDelayTime var is not set to another value
; Third, thus the Quest script can be used as an control instance to avoid
; package overruling script execution due to variables
; Fourth, the most secure option is to use the AI packages to be loaded
; WITHOUT ANY condition, while the direct related packages have conditions

; This is considered for AI-Packages directly related to the NPC
; thus the NPC should only have a wander as well as an eat and sleep package. 
; If you add other packages, you have to test, if they interfere with the
; follow packages. Every package, directly related to the NPC should have proper 
; conditions to avoid problems with the follow packages
;==============================================================================
;==============================================================================

; This AI packages are needed for follow functionality
; a01AIC01FollowWalking
; a01AIC01FollowRiding
; a01AIC01MountHorse
; a01AIC01DismountHorse
; a01AIC01Wait

; If we want the companion to follow on horse, he should have a
; horse. This horse is referenced in this script to get the distance
; and on need, activate the beam function (using MoveTo).
; Current EditorID is a01HorseC01Ref, NPC is a01C01Ref

; C01 stands for Companion01

; init vars
short DoOnce

; Follow function vars
short FollowPlayer
; can be 0 (no follow action)
;        1 (follow player)
;        2 (follow player always walking)
;        3 (wait for player)
short SneakAsPlayer
; can be 0 (no sneak action)
;        1 (sneak, when player's sneaking)
;        2 (always sneak when walking)
short FollowAction
; can be 0 (no follow action)
;        1 (follow walking)
;        2 (follow riding)
;        3 (mount horse)
;        4 (dismount horse)
;        5 (wait for player)
;        6 (call horse action - special handling)
short LoadAction
; can be 0 (no load action)
;        1 (load a01AIC01FollowWalking)
;        2 (load a01AIC01FollowRiding)
;        3 (load a01AIC01MountHorse)
;        4 (load a01AIC01DismountHorse)
;        5 (load a01AIC01Wait)
;        6 (activate MoveTo of horse - special handling as no
;           LoadedPackage exists, comparing the FollowAction 6
;           to LoadedPackage will always be false and thus executed)
short LoadedPackage
; can be 0 (not identified)
;        1 (loaded a01AIC01FollowWalking)
;        2 (loaded a01AIC01FollowRiding)
;        3 (loaded a01AIC01MountHorse)
;        4 (loaded a01AIC01DismountHorse)
;        5 (loaded a01AIC01Wait)
short SneakAction
; can be 0 (no action)
;        1 (start sneaking)
;        2 (stop sneaking)
short CallHorse
; can be 0 (don't call horse)
;        1 (call horse)
short WasInCombat
; can be 0 (no combat detected)
;      > 0 (combat detected and working as a timer to discover
;           the end of combat - as this state may change from
;           one frame to another)
short HorseTimer
; can be 0 (no call for horse)
;      > 0 (horse called and timer started to finish process)

long HorseDistance

Begin GameMode

   ; ====================Init section====================
   If ( DoOnce == 0 )
      Set DoOnce To 1
      Set FollowPlayer To 0
      Set SneakAsPlayer To 0
      Set UseTorch To 0
      Set FollowAction To 0
      Set LoadAction To 0
      Set LoadedPackage To 0
      Set SneakAction To 0
      Set TorchAction To 0
      Set CallHorse To 0
      Set WasInCombat To 0
      Set HorseTimer To 0
      Set HorseDistance To 0
   EndIf

   ; ====================Start Follow player section====================
   If ( FollowPlayer > 0 )
      ; ====================get status and decide what to do====================
      ; this vars are always reset if follow player is active
      Set LoadAction To 0
      Set LoadedPackage To 0
      Set HorseDistance To 0
      Set SneakAction To 0

      ; loaded package needs to be identified
      If ( GetIsCurrentPackage "a01AIC01FollowWalking" )
         Set LoadedPackage To 1
      ElseIf ( GetIsCurrentPackage "a01AIC01FollowRiding" )
         Set LoadedPackage To 2
      ElseIf ( GetIsCurrentPackage "a01AIC01MountHorse" )
         Set LoadedPackage To 3
      ElseIf ( GetIsCurrentPackage "a01AIC01DismountHorse" )
         Set LoadedPackage To 4
      ElseIf ( GetIsCurrentPackage "a01AIC01Wait" )
         Set LoadedPackage To 5
      EndIf

      ; this might include riding
      If ( FollowPlayer == 1 )
         ; decide, if we should ride also
         If ( Player.IsRidingHorse )
            ; we should only ride, if not in combat
            If ( IsInCombat || Player.IsInCombat || ( WasInCombat > 0 ) )
               ; first time, we notice combat situation
               If ( WasInCombat == 0 )
                  Set FollowAction To 1
                  Set WasInCombat To 1
               ; wait for combat to finish (combat state may flicker!!!)
               Else
                  Set WasInCombat To WasInCombat + 1
                  ; check combat state
                  If ( WasInCombat == 60 )
                     If ( ( IsInCombat == 0 ) && ( Player.IsInCombat == 0 ) )
                        Set WasInCombat To 0
                        Set FollowAction To 2
                     EndIf
                  ElseIf ( WasInCombat == 120 )
                     If ( ( IsInCombat == 0 ) && ( Player.IsInCombat == 0 ) )
                        Set WasInCombat To 0
                        Set FollowAction To 2
                     EndIf
                  ElseIf ( WasInCombat == 180 )
                     ; too long, reset combat status
                     Set WasInCombat To 0
                     Set FollowAction To 2
                  EndIf
               EndIf
            ; no combat, normal processing
            Else
               ; we are riding, that's fine
               If IsRidingHorse
                  Set FollowAction To 2
                  Set HorseTimer To 0
                  Set CallHorse To 0
               ; we have to convince the NPC to ride
               Else
                  ; we may need this information
                  Set HorseDistance To ( GetDistance "a01HorseC01Ref" )
                  ; if calling the horse, we need special processing
                  If CallHorse
                     If ( HorseTimer == 0 )
                        Set HorseTimer To 1
                        Set FollowAction To 6
                     Else
                        ; wait until horse arrives
                        If ( GetInSameCell "a01HorseC01Ref" )
                           Set HorseTimer To HorseTimer + 1
                           If ( HorseTimer == 30 )
                              ; set LoadedPackage to -1 to force package reload
                              Set LoadedPackage To -1
                              If ( HorseDistance > 350 )
                                 Set FollowAction To 3
                              Else
                                 Set FollowAction To 2
                              EndIf
                           ElseIf ( HorseTimer == 60 )
                              ; set LoadedPackage to -1 to force package reload
                              Set LoadedPackage To -1
                              If ( HorseDistance > 350 )
                                 Set FollowAction To 3
                              Else
                                 Set FollowAction To 2
                              EndIf
                           ElseIf ( HorseTimer == 90 )
                              ; waited long enough
                              ; set LoadedPackage to -1 to force package reload
                              Set LoadedPackage To -1
                              If ( HorseDistance > 350 )
                                 Set FollowAction To 3
                              Else
                                 Set FollowAction To 2
                              EndIf
                              Set HorseTimer To 0
                              Set CallHorse To 0
                           EndIf
                        Else
                           ; freeze HorseTimer until Horse arrived
                           Set HorseTimer To 2
                        EndIf
                     EndIf
                  ; not calling horse
                  Else
                     ; check if horse is near
                     If ( HorseDistance < 500 )
                        ; activate the follow package immediately
                        Set FollowAction To 2
                     ElseIf ( ( HorseDistance > 499 ) && ( HorseDistance < 2000 ) )
                        ; try to mount first
                        Set FollowAction To 3
                     ElseIf ( HorseDistance > 1999 )
                        ; to avoid any load action
                        ; set the FollowAction to LoadedPackage for this frame
                        Set FollowAction To LoadedPackage
                        ; ask player, if we should call the horse
                        ; quest topics should set either CallHorse to 1
                        ; or FollowPlayer to 2
                        StartConversation Player "0poQMC01CallHorse"
                     EndIf
                  EndIf ; end condition CallHorse or not
               EndIf ; end condition IsRidingHorse or not
            EndIf ; end condition combat or not
         ; we should walk
         Else
            ; clear CallHorse and Combat state
            Set CallHorse To 0
            Set HorseTimer To 0
            Set WasInCombat To 0
            ; we are riding, that's bad
            If IsRidingHorse
               ; first dismount
               Set FollowAction To 4
            Else
               ; we're walking as it should be
               Set FollowAction To 1
            EndIf
            ; check if sneak mode is on
            If ( SneakAsPlayer != 0 )
               ;normal sneak mode, sneak like player
               If ( SneakAsPlayer == 1 )
                  If Player.IsSneaking
                     ; player sneaks but not me
                     If ( IsSneaking == 0 )
                        Set SneakAction To 1
                     EndIf
                  Else
                     ; player doesn't sneak but I
                     If IsSneaking
                        Set SneakAction To 2
                     EndIf
                  EndIf
               ElseIf ( SneakAsPlayer == 2 )
                  ; always sneak
                  If ( IsSneaking == 0 )
                     Set SneakAction To 1
                  EndIf
               EndIf
            EndIf
         EndIf ; end condition ride or walk

         ; determine LoadAction
         If ( FollowAction != LoadedPackage )
            ; if not loaded, so load it!
            Set LoadAction To FollowAction
         EndIf

      ; this means always walking
      ElseIf ( FollowPlayer == 2 )
         ; if for any reason riding, we should dismount
         If IsRidingHorse
            Set FollowAction To 4
         Else
            Set FollowAction To 1
            ; check if sneak mode is on
            If ( SneakAsPlayer != 0 )
               ;normal sneak mode, sneak like player
               If ( SneakAsPlayer == 1 )
                  If Player.IsSneaking
                     ; player sneaks but not me
                     If ( IsSneaking == 0 )
                        Set SneakAction To 1
                     EndIf
                  Else
                     ; player doesn't sneak but I
                     If IsSneaking
                        Set SneakAction To 2
                     EndIf
                  EndIf
               ElseIf ( SneakAsPlayer == 2 )
                  ; always sneak
                  If ( IsSneaking == 0 )
                     Set SneakAction To 1
                  EndIf
               EndIf
            EndIf
         EndIf
         ; determine LoadAction
         ; if we have loaded the correct package
         If ( FollowAction != LoadedPackage )
            Set LoadAction To FollowAction
         EndIf
         ; clear any riding state
         Set WasInCombat To 0
         Set HorseTimer To 0

      ; this means waiting for the player
      ElseIf ( FollowPlayer == 3 )
         ; if for any reason riding, we should dismount
         If IsRidingHorse
            Set FollowAction To 4
         Else
            Set FollowAction To 5
         EndIf
         ; determine LoadAction
         ; if we have loaded the correct package
         If ( FollowAction != LoadedPackage )
            Set LoadAction To FollowAction
         EndIf
         ; clear any riding state
         Set WasInCombat To 0
         Set HorseTimer To 0

      EndIf ; Follow Player analysis

      ; in follow mode, decide which package to load, if any
      ; RemoveScriptPackage is only used, if one of the special
      ; packages are loaded - to avoid removing standard packages
      If ( LoadAction == 1 )
         If ( ( LoadedPackage != 0 ) && ( LoadedPackage != 1 ) )
            RemoveScriptPackage
         EndIf
         AddScriptPackage "a01AIC01FollowWalking"
         EvaluatePackage
      ElseIf ( LoadAction == 2 )
         If ( ( LoadedPackage != 0 ) && ( LoadedPackage != 2 ) )
            RemoveScriptPackage
         EndIf
         AddScriptPackage "a01AIC01FollowRiding"
         EvaluatePackage
      ElseIf ( LoadAction == 3 )
         If ( ( LoadedPackage != 0 ) && ( LoadedPackage != 3 ) )
            RemoveScriptPackage
         EndIf
         AddScriptPackage "a01AIC01MountHorse"
         EvaluatePackage
      ElseIf ( LoadAction == 4 )
         If ( ( LoadedPackage != 0 ) && ( LoadedPackage != 4 ) )
            RemoveScriptPackage
         EndIf
         AddScriptPackage "a01AIC01DismountHorse"
         EvaluatePackage
      ElseIf ( LoadAction == 5 )
         If ( ( LoadedPackage != 0 ) && ( LoadedPackage != 5 ) )
            RemoveScriptPackage
         EndIf
         AddScriptPackage "a01AIC01Wait"
         EvaluatePackage
      ElseIf ( LoadAction == 6 )
         ; get horse closer
         "a01HorseC01Ref".MoveTo "a01C01Ref" 200 0 0
         "a01HorseC01Ref".EvaluatePackage
      EndIf

      ; check sneak actions
      If ( SneakAction == 1 )
         SetForceSneak 1
      ElseIf ( SneakAction == 2 )
         SetForceSneak 0
      EndIf

   EndIf ; end condition follow mode active
   ; ====================End Follow player section====================

End

Caveats and Notes[edit | edit source]

  • The MoveTo command is pretty dangerous. It was necessary to freeze the timer until GetInSameCell function reports that the horse has arrived. If you try to load any package before the horse arrives in the cell, Oblivion will in most of the cases crash.
  • The combat situation needed special treatment, to avoid, that the NPC can't decide, if he should load a package or start to fight.
  • The timer used, do not count seconds, they count every time, the script runs. If the NPC is near the player, the scripts runs usually according to the frame rate, that means about 30 times every second. You may play with the timer settings, to get the NPC acting more directly.
  • The NPC starts to act, with a little delay, as I favorized the Follow packages instead of the travel packages. I use them only, if the follow packages don't work. You may change the order to your favors. This means for my version, that you have to move riding up to a distance of 350 until the NPC mounts the horse and follows.
  • The script tried to minimize function calls, whereever possible, as checking conditions with variables is usually faster. You may delete the comments in your final version, to get the interpreter run the script faster.