SteamVR/Environments/Scripting/Custom Tool Creation
The SteamVR Workshop Tools allow authors to create tool props that players can pick up and use. Tools can be used to manipulate the world, spawn other items, weaponry and even for new types of locomotion. See the Flashlight Tool Tutorial for the basic tutorial on how to set up tools.
Contents
Modeling
The tool prop requires a model with a collision mesh set up.
When a tool is picked up, the model is aligned to the motion controller the attachment point vr_controller_root
, or by the model origin if the attachment point does not exist.
To aid with aligning the tool model, the Vive motion controller model can be imported to the modeling software from \SteamApps\common\SteamVR\resources\rendermodels\vr_controller_vive_1_5\vr_controller_vive_1_5.obj
The origin of the Vive controller under the menu button will match up with the origin of the custom model or the vr_controller_root
if it is specified.
Scripting
For reference, the flashlight tutorial script can be found here, and official tool script can be found in the /scripts/vscripts/tools
directory of the archive \SteamApps\common\SteamVR\tools\steamvr_environments\game\steamtours\pak01_dir.vpk
The .vpk archive can be opened using the third-party GCFScape utility.
Basics
The tool script relies on having hook functions in it to receive input from the game code.
The OnHandleInput()
function periodically receives the state of the motion controller buttons, and can be used to control the tool functionality.
The SetEquipped()
and SetUnequipped()
functions get called when the player picks up or drops the tool respectively. When the game calls the SetEquipped()
function, it passes on information about what player and hand picked it up, as well as a reference to the hand attachment entity. The hand attachment is the model entity that is visible while the tool is held. Parenting and position calculation should always be done to the hand attachment instead of the tool entity while the tool is held. The variables passed in to SetEquipped()
should be copied over to script scope variables in the entity script to allow access from other functions.
Additionally, there are hook functions available for all entity scripts. Precache()
is needed to to load in any custom assets that are spawned by the entity. Spawn()
and Activate()
are called during and after the entity spawns respectively. UpdateOnRemove()
is called when the entity is killed, just before it is removed from the game. It can be used to clean up any resources that aren't needed anymore.
This script skeleton can be used as a base to start writing your own script.
local m_bIsEquipped = false; -- keep track of whether we are equipped or not
local m_hHand = nil; -- keep a handle to the hand that is holding the tool
local m_nHandID = -1; -- keep track of the hand index that is holding the tool (0==right, 1==left)
local m_hHandAttachment = nil; -- this is the handle to the tool attachment which displays the actual model in the hand
local m_hPlayer = nil; -- handle to the player holding the tool
---------------------------------------------------------------------------
-- SetEquipped
-- Called by code when the tool is picked up.
---------------------------------------------------------------------------
function SetEquipped( self, pHand, nHandID, pHandAttachment, pPlayer )
-- store these into our global scope so we can use them in other functions
m_hHand = pHand;
m_nHandID = nHandID;
m_hHandAttachment = pHandAttachment;
m_hPlayer = pPlayer;
m_bIsEquipped = true;
return true;
end
---------------------------------------------------------------------------
-- SetUnequipped
-- Called when the player drops the tool.
---------------------------------------------------------------------------
function SetUnequipped()
m_hHand = nil;
m_nHandID = -1;
m_hHandAttachment = nil;
m_hPlayer = nil;
m_bIsEquipped = false;
return true;
end
function OnHandleInput( input )
-- this is lua's ugly version of a ternary operator
-- we do this because we only care about the input from the hand that is holding us
-- but you could check input from the other hand if you want
local nIN_TRIGGER = IN_USE_HAND1; if (m_nHandID == 0) then nIN_TRIGGER = IN_USE_HAND0 end;
local nIN_GRIP = IN_GRIP_HAND1; if (m_nHandID == 0) then nIN_GRIP = IN_GRIP_HAND0 end;
local nIN_PAD = IN_PAD_HAND1; if (m_nHandID == 0) then nIN_PAD = IN_PAD_HAND0 end;
return input;
end
Controlling how the tool is dropped
To enable the player to keep the tool in-hand without having the trigger pressed, the OnHandleInput()
hook function can be set up to force the game to ignore the input of buttons. In particular, the trigger release event need to be suppressed to keep the tool in-hand.
The tool can be dropped programmatically from any script using the ForceDropTool() method of the tool entity.
This code demostrates how to set up OnHandleInput()
to keep the tool in-hand until the grip is pressed.
function OnHandleInput( input )
local nIN_TRIGGER = IN_USE_HAND1; if (m_nHandID == 0) then nIN_TRIGGER = IN_USE_HAND0 end;
local nIN_GRIP = IN_GRIP_HAND1; if (m_nHandID == 0) then nIN_GRIP = IN_GRIP_HAND0 end;
-- this checks if the TRIGGER has just been released
if ( input.buttonsReleased:IsBitSet( nIN_TRIGGER ) ) then
-- Clearing the trigger release prevents the tool from being dropped after the tool as been picked up.
-- Removing this makes the tool drop once the player releases the trigger.
input.buttonsReleased:ClearBit( nIN_TRIGGER );
end
-- checks to see if the GRIP has been pressed this update
if ( input.buttonsReleased:IsBitSet( nIN_GRIP ) ) then
input.buttonsReleased:ClearBit( nIN_GRIP );
-- This drops the tool directly when the grip is pressed. It can be triggered by other parts of the code as well.
thisEntity:ForceDropTool();
end
return input;
end
Spawning new entities
New entities can be spawned from the script using the SpawnEntity
family of global functions. If the entity uses art assets (like models), the asset needs to first be precached from the Precache()
function.
These script excerpts show how to spawn a balloonicorn in front of the tool.
function Precache( context )
--Cache the models
PrecacheModel("models/props/toys/balloonicorn.vmdl", context );
end
function OnHandleInput( input )
...
-- Spawns the ballonicorn when the trigger is pressed.
if ( input.buttonsPressed:IsBitSet( nIN_TRIGGER ) ) then
SpawnBalloonicorn();
input.buttonsPressed:ClearBit( nIN_TRIGGER );
end
...
end
function SpawnBalloonicorn()
-- Abort spawning the prop if the tool isn't equipped.
if not m_bIsEquipped then
return;
end
-- Find the point where to spawn the prop from the attachment "prop_spawnpoint" on the tool model.
-- Create the attachment on the tool model in the model editor first.
local nAttachmentID = m_hHandAttachment:ScriptLookupAttachment( "prop_spawnpoint" );
local vSpawnPosition = m_hHandAttachment:GetAttachmentOrigin( nAttachmentID );
local balloonKeyvalues = {
targetname = "spawned_prop";
model = "models/props/toys/balloonicorn.vmdl";
origin = vSpawnPosition; -- The origin found from the attachment.
angles = m_hHandAttachment:GetAngles() -- Give the prop the same angles as the tool.
}
-- This function spawns the entity in.
SpawnEntityFromTableSynchronous( "prop_destinations_physics", balloonKeyvalues );
end
Parenting other models to the tool
While the tool prop entity does not support playing animations on itself, animated prop_dynamic entities can be parented to the tool.
When the tool entity is picked up, it is turned invisible and the position and angles of it are not updated properly. Instead a a hand attachment entity is spawned and supplied to the SetEquipped()
function. The hand attachment entity is the tool model visible in the players hand. If the entity is parented to the tool instead of the attachment, the entity position will not reliably update, leading to desynchronised movement. Conversely, when the tool is dropped, the hand attachment is despawned, and the entity needs to be parented to the tool entity.
This script excerpt shows how to spawn a prop_dynamic and keep it attached to the tool. The tool model needs the attachment point "knob_attach" set from the model editor.
local m_hKnobAttachment = nil -- Global reference to the attached prop.
function Precache( context )
--Cache the models
PrecacheModel( "models/props_gameplay/cache_finder001_attachment.vmdl", context );
end
function Activate()
-- Find the point where to spawn the prop from the attachment "knob_attach" on the tool model.
-- Create the attachment on the tool model in the model editor first.
local nAttachmentID = thisEntity:ScriptLookupAttachment( "knob_attach" );
local vSpawnPosition = thisEntity:GetAttachmentOrigin( nAttachmentID );
local knobKeyvalues = {
targetname = "knob";
model = "models/props_gameplay/cache_finder001_attachment.vmdl";
origin = vSpawnPosition; -- The origin found from the attachment.
angles = thisEntity:GetAngles(); -- Give the prop the same angles as the tool.
solid = 0; -- Make sure the collision is disabled.
}
-- This function spawns the entity in.
m_hKnobAttachment = SpawnEntityFromTableSynchronous( "prop_dynamic", knobKeyvalues );
-- Parents the prop to the tool and the attachment point.
m_hKnobAttachment:SetParent( thisEntity, "knob_attach" );
-- Set the origin and angles of the prop. Since the prop is parented to the attachment point, aligning it to the local origin lines it up with the attachment point itself.
m_hKnobAttachment:SetLocalOrigin( Vector(0, 0, 0) );
m_hKnobAttachment:SetLocalAngles( 0, 0, 0 );
end
function SetEquipped( self, pHand, nHandID, pHandAttachment, pPlayer )
...
-- Parents the prop to the hand attachment while held.
m_hKnobAttachment:SetParent( m_hHandAttachment, "knob_attach" );
m_hKnobAttachment:SetLocalOrigin( Vector(0, 0, 0) );
m_hKnobAttachment:SetLocalAngles( 0, 0, 0 );
...
end
function SetUnequipped()
...
-- Parents the prop to the tool again when dropped.
m_hKnobAttachment:SetParent( thisEntity, "knob_attach" );
m_hKnobAttachment:SetLocalOrigin( Vector(0, 0, 0) );
m_hKnobAttachment:SetLocalAngles( 0, 0, 0 );
...
end