Difference between revisions of "Running Scripts On Arrows"
imported>Scruggs (Adding to Useful Code) |
imported>Scruggs |
||
Line 178: | Line 178: | ||
set baseObject to trigRef.getBaseObject | set baseObject to trigRef.getBaseObject | ||
player.addItem baseObject 1 | player.addItem baseObject 1 | ||
else | |||
set trigRef to 0 | |||
endif | endif | ||
end | end |
Revision as of 13:42, 9 October 2006
One of the limitations of the Construction Set is that it won't allow you to attach scripts to arrows. This is an obstacle for many mod ideas. A partial workaround is to add a Script Effect enchantment to the arrow; the script will run when the arrow hits an actor. But there are times when you want to detect or control the behavior of an arrow while it is in flight, or trigger some action before the arrow hits an actor. This tutorial will show you how to do that.
The first step is to get a reference to the arrow. This is done by placing a trigger zone at the player's location. When an arrow collides with the trigger zone, an OnTriggerMob block will run. The reference to the triggering object can then be returned with GetActionRef.
We'll create a new trigger zone object using the TrigZone01.nif and place a reference to it somewhere in the gameworld. Make sure it is a persistent reference, and has a unique reference ID. We also want to scale it to 0.5 scale. We'll call the reference acTrigZoneREF.
Now, how do we keep the trigger zone glued to the player's location? SetPos and MoveTo will be useful here. We will use a quest script to handle the MoveTo calls for when the trigger zone is not in the same cell as the player, and update its position in the current cell with SetPos.
scriptName acQuestSCR ; attached to the control quest ; moves the trigger zone to the player when he changes cells ; updates trigger zone's position within the player's cell float xp float yp ; player coordinates float zp float fQuestDelayTime ; controls processing speed of this script short relocate ; set this to 1 when we need to call moveTo if ( fQuestDelayTime != 0.001 ) set fQuestDelayTime to 0.001 ; process every frame endif if ( relocate == 1 ) acTrigZoneREF.moveTo player set relocate to 2 return elseif ( relocate == 2 ) acTrigZoneRef.disable set relocate to 3 return elseif ( relocate == 3 ) acTrigZoneRef.enable set relocate to 4 return elseif ( relocate == 4 ) set xp to player.getPos x acTrigZoneREF.setpos x xp set relocate to 0 endif if ( player.getDistance taTrigNoisemakerRef < 200 ) ; trigZone is in same cell as player set xp to player.getPos x set yp to player.getPos y ; store player's coordinates set zp to player.getPos z acTrigZoneRef.setpos x xp acTrigZoneRef.setpos y yp ; and move trigZone to those coordinates acTrigZoneRef.setpos z zp else ; player has changed cells set relocate to 1 endif
Notice that the use of the relocate variable splits the movement from one cell to the next into four stages. This is to make sure that the trigger zone retains its collision properties after it is moved.
That script works...sort of. The problem is that the trigger zone is always at the player's feet. We need to adjust the Z position of the trigger zone so that it will always be at the correct height to collide with an arrow fired by the player. Three factors are involved here: the player's height, which is defined by his race; the player's vertical rotation (X angle); and whether or not the player is sneaking. We'll add a section to the script to calculate the necessary adjustment and apply it to the trigger zone's coordinates:
scriptName acQuestSCR ; attached to the control quest ; moves the trigger zone to the player when he changes cells ; updates trigger zone's position within the player's cell float xp float yp ; player coordinates float zp float pxRot ; player's vertical rotation float zOffset ; adjustment to zPos float fQuestDelayTime ; controls processing speed of this script short relocate ; set this to 1 when we need to call moveTo if ( fQuestDelayTime != 0.001 ) set fQuestDelayTime to 0.001 ; process every frame endif if ( relocate == 1 ) acTrigZoneREF.moveTo player set relocate to 2 return elseif ( relocate == 2 ) acTrigZoneRef.disable set relocate to 3 return elseif ( relocate == 3 ) acTrigZoneRef.enable set relocate to 4 return elseif ( relocate == 4 ) set xp to player.getPos x acTrigZoneREF.setpos x xp set relocate to 0 endif set zOffset to player.getScale ; get player's height set zOffset to ( zOffset * ( 115 - ( player.IsSneaking * 20 ) ) ) set pxRot to player.getAngle x ; get player's vertical facing set pxRot to ( pxRot / -1.5 ) set zOffset to ( zOffset + pxRot ) if ( player.getDistance taTrigNoisemakerRef < 200 ) ; trigZone is in same cell as player set xp to player.getPos x set yp to player.getPos y ; store player's coordinates set zp to player.getPos z set zp to ( zp + zOffset ) ; adjust for zOffset acTrigZoneRef.setpos x xp acTrigZoneRef.setpos y yp ; and move trigZone to those coordinates acTrigZoneRef.setpos z zp else ; player has changed cells set relocate to 1 endif
Here, GetScale returns the player's height as a proportion of the default racial height of 128. For the default height, we want the trigger zone placed 115 units above the player's feet when standing, and 95 units above when sneaking. Then we modify the offset to account for the player's vertical facing.
Great, now we have a trigger zone glued to the player's location, capable of intercepting any arrows he fires. We can attach a demo script to the trigger zone itself to show how this works:
scriptName acDemoTrigSCR ; Causes any projectile fired by the player to disappear ref trigRef ; reference to the triggering object begin onTriggerMob ; a mobile object has collided with the trigger zone set trigRef to getActionRef if ( trigRef.IsActor == 0 ) ; it's not an NPC trigRef.disable ; so make it disappear endif end
That's fun, but not entirely useful. Also, a serious problem arises: the onTriggerMob block will run when any projectile collides with the trigger zone, including arrows and spells fired at the player. What we need is a method to determine whether the projectile is an arrow, and whether it was fired by the player.
If we know the ID of the arrows we want to run scripts on, this is easy. Just check GetIsID on the triggering reference:
scriptName acTrigZoneSCR ref trigRef begin onTriggerMob set trigRef to getActionRef if ( trigRef.getIsID "myArrowID" ) trigRef.disable endif end
This assumes that only the player has this type of arrow, so we know it wasn't fired at the player by an NPC.
Detecting any arrow is more complicated. We will rely on OBSE functions for this:
scriptName acTrigZoneSCR ref trigRef begin onTriggerMob set trigRef to getActionRef if ( trigRef.IsAmmo ) ; OBSE, returns 1 if ref is an arrow trigRef.disable endif end
Another more useful example results in a bottomless quiver of arrows:
scriptName acTrigZoneInfiniteArrowsSCR ref trigRef ref baseObject short triggered ; flag to avoid repeated triggering by the same object begin onTriggerMob if ( triggered ) if ( getActionRef == trigRef ) ; same reference as last frame return else set triggered to 0 endif endif set trigRef to getActionRef if ( trigRef.IsAmmo ) set baseObject to trigRef.getBaseObject player.addItem baseObject 1 else set trigRef to 0 endif end
We still haven't eliminated the possibility that the arrow was fired by an NPC. We'll tackle that later.
At this point, we have a means of obtaining a reference to an arrow, and we can run scripts on that reference. However, it would be useful to know what the arrow is doing. We can figure this out by tracking its position and rotation over the course of its flight:
- If the arrow's rotation changes and it continues moving, it has bounced off of a surface;
- If its rotation changes and then it stops moving, it is lying on a flat surface, or on the ground;
- If it stops moving, but its rotation remains the same, and its coordinates do not return 0, it has stuck into a surface, such as a wooden door;
- If its coordinates all return 0, it has hit an actor and no longer exists in the gameworld.
Using these guidelines, we can write a slightly more complicated script. This script Simply reports on the state of the arrow over the course of its flight. We use a doOnce variable triggered to make sure we only track one arrow at a time:
scriptName acTrigZoneSummonSCR ref trigRef float xp float yp ; the arrow's current coordinates float zp float ox float oy ; coordinates from previous frame float oz float ax float ay ; arrow's starting angles float az short triggered ; set to 1 while an arrow is being tracked begin onTriggerMob if ( triggered == 0 ) set trigRef to getActionRef if ( trigRef.IsAmmo ) set triggered to 1 set ax to trigRef.getAngle x set ay to trigRef.getAngle y set az to trigRef.getAngle z message "Arrow fired." endif endif end begin gameMode if ( triggered ) ; currently tracking an arrow set xp to trigRef.getPos x set yp to trigRef.getPos y ; get current coordinates of arrow set zp to trigRef.getPos z if ( ( xp + yp + zp ) == 0 ) ; arrow is not in gameworld message "Arrow has hit an actor!" set triggered to 0 return elseif ( trigRef.getAngle x != ax || trigRef.getAngle y != ay || trigRef.getAngle z != az ) message "Arrow has bounced off of something!" set triggered to -1 ; remember that the arrow bounced set ax to trigRef.getAngle x set ay to trigRef.getAngle y set az to trigRef.getAngle z elseif ( xp == ox && yp == oy && zp == oz ) ; arrow stopped moving if ( triggered == 1 ) ; arrow never bounced message "Arrow stuck in a surface!" else ; arrow bounced message "Arrow is lying on the ground!" endif set triggered to 0 endif endif end
Admittedly, that script isn't terribly useful in itself, but it should give you a starting point in setting up your own scripts on arrows.