Script Attachments
Basics
Script attachments allow attaching any model to any game object or even other script attachments.
This opens up a lot of possibilities like weapon attachments no longer having to be part of the weapon model and much more.
Script attachments are managed entirely by scripts, the engine does not store any attachment data in save files.
This means you'll have to keep track of your attachments and store serialized data of them in mdata if you want them to persist throughout saves.
Or find other means to reliably attach/remove them, like actor_item_to_slot
and actor_item_to_ruck
callbacks.
Creating a script attachment
All you need to create a script attachment is a parent game_object/attachment and the model you want to use.
One simple line will create the script attachment and attach it to its parent:
local test_att = db.actor:add_attachment(0, "path\\to\\model")
This adds a new script attachment into slot 0 of the actor game_object using the model we specified.
Attachment slots range from 0 to 65535.
Creating an attachment in a slot that is already used by an attachment will delete the previous attachment.
You can also attach attachments to other attachments:
local child_att = test_att:add_attachment(0, "path\\to\\other_model")
local child_child_att = child_attachment:add_attachment(0, "path\\to\\another_model")
How do I remove them?
If you want to remove an attachment, just remove it from its parent:
test_att:remove_attachment(0)
child_att = nil
child_child_att = nil
This will remove the child_att
we created in slot 0 of test_att
.
Any child attachments of child_att
will be removed as well.
Make sure you don't keep references to deleted attachments in your scripts or you might get pure virtual function errors, similar to storing a reference to a destroyed game_object.
Accessing script attachments without a reference
It's also possible to access a script attachment without it being stored in a variable. You can access it like this:
db.actor:get_attachment(0)
Getting and setting parent
If you need the parent object or parent attachment of an attachment for whatever reason, it's possible to access it like this:
local parent_tbl = test_att:get_parent()
We’ll receive a table, that looks like this:
parent_tbl = {
["object"] = userdata, -- (reference to db.actor)
["attachment"] = nil
}
Now we know that the attachment is attached to a game_object and we have a reference to said game_object.
local parent_tbl = test_att:get_parent()
if parent_tbl.object then
-- will print "parent is actor"
printf("parent is %s", parent_tbl.object:section())
elseif parent_tbl.attachment then
printf("parent is attachment")
end
Maybe you want to transfer your script attachment to another parent game_object or attachment?
Just use set_parent
, it will transfer the attachment, as well as any child attachments to the new parent:
test_att:set_parent(npc)
The attachment will overwrite an existing attachment of the new parent object, if it uses the same slot! (Might get changed in the future)
Parent Bone
By default the script attachment will attach to the root bone (bone 0) of the parent game_object or attachment. You can change the parent bone at any time using
test_att:set_parent_bone(15)
In this case it will set the parent bone to bip01_head
of the parent npc object.
Changing to a bone id that doesn't exist will default to the root bone.
To get the current parent bone id you can use
local bone_id = test_att:get_parent_bone()
Attachment position, rotation and scale
Position and rotation work as an offset to the parent bone's position and rotation.
Functions support both vectors and three numbers.
Scale can even be set with a single number, if the scale should be the same in all directions.
Position
get_position() -- returns vector
set_position(vector)
set_position(number, number, number)
Rotation
get_rotation() -- returns vector
set_rotation(vector)
set_rotation(number, number, number)
Scale
get_scale() -- returns vector
set_scale(vector)
set_scale(number, number, number)
set_scale(number)
Attachment origin
get_origin() -- returns vector
set_origin(vector)
set_origin(number, number, number)
It's also possible to change the origin point of an attachment.
By default the root bone of the attachment will be used as its origin point.
Say, you want to rotate the attachment around its center, instead of its root bone:
local center = test_att:get_center()
test_att:set_origin(center)
Flags
At the moment there are three flags that can be set for script attachments.
["eSA_RenderHUD"] = 1,
["eSA_RenderWorld"] = 2,
["eSA_CamAttached"] = 4,
By default the flags value is set to 2
, which means a new attachment will only render on the world model of its parent object.
Using getter/setter we can read and change the flags value:
get_flags()
set_flags(number)
If we want the attachment to render both in world and hud mode, we can do it as follows:
test_att:set_flags(3)
Cam attached?
Another very cool aspect of script attachments is the ability to have them attach to the player camera.
Setting the flags value to 4
(eSA_CamAttached
) will enable this behavior.
When attached to the camera, the attachment will follow the position and direction of the first person camera and render at a fixed fov, so it should look similar on all resolutions and fov values. This is especially useful for things like 3D UIs or visible 3D helmets and gasmasks.
Attachment model
To get the path of the model that is currently used for a script attachment use
local path = test_att:get_model()
The model can be changed at any time
test_att:set_model("path\\to\\new_model", false)
The boolean tells the engine if existing bone callbacks should be kept in place.
You can find out more about bone callbacks down below.
Model bones
A bone's visibility can be read and changed using
get_bone_visible(number)
set_bone_visible(number, boolean)
At the moment only bone IDs are supported (instead of names).
Animations
If the model contains any motions or motion references, you can play them on the attachment model.
By default the engine will try to play the idle
motion, so it's advised to always add one to your model if you're exporting it as a dynamic model.
-- Name MixIn Speed
play_motion(string, boolean, number)
Example
-- returns animation length in milliseconds
local anm_length = test_att:play_motion("reload", false, 1)
-- if you want to stop the motion, just play "idle"
-- if the animation is not looped, it will automatically play "idle" at the end
test_att:play_motion("idle", true, 1)
Bone Callbacks
Bone callbacks are a powerful feature, they allow you to change the position of any bone of the attachment model in real time.
There are two types of bone callbacks.
Copy Bone Callback
The first type of bone callbacks simply copies the position of a bone of the parent game_object/attachment and moves the selected attachment bone to that position. It's mainly useful if you want to attach something with multiple bones to a parent object and have it follow its animations.
Usage
test_att:bone_callback(id1,id2,true)
-- id1 is the ID of the bone of the attachment model we want to modify
-- id2 is the ID of the bone of the parent model we want to copy
-- setting the boolean to true tells the engine that we don't need to calculate
-- animations for this bone (on the attachment model) - mainly for performance reasons
For example we can add an exoskeleton to the player model like this
-- attach exo model
local exo_n = db.actor:add_attachment(3202, ogf_file_npc)
-- set bone callbacks
-- you can do for i=0,46
-- i'm just copying the bones that are actually used by the exo here
for i=0,14 do
exo_n:bone_callback(i,i,true)
end
for i=20,22 do
exo_n:bone_callback(i,i,true)
end
for i=33,35 do
exo_n:bone_callback(i,i,true)
end
You have to set the callback for bone 0 on actor, stalker and mutant models, otherwise the attachment will awkwardly shift around sometimes due to foot IK.
Attaching something to the first person hands is a little more involved.
Since left and right arms use seperate models, we have to attach two script attachments:
local exo_r = db.actor:add_attachment(3200, ogf_file)
-- an attachment for the right arm has to use bone 21 as its parent bone
exo_r:set_parent_bone(21)
-- set hud mode
exo_r:set_flags(1)
-- the model I'm using is for both arms so we have to hide the left part
exo_r:set_bone_visible(1, false)
-- we also need a bone callback to copy the root bone
exo_r:bone_callback(0,0,true)
-- copy the rest of the right arm bones
for i=21,41 do
exo_r:bone_callback(i,i,true)
end
-- attaching to the left arm is simpler
local exo_l = db.actor:add_attachment(3201, ogf_file)
-- set hud mode
exo_l:set_flags(1)
-- hide right part
exo_l:set_bone_visible(22, false)
-- copy left arm bones
for i=0,20 do
exo_l:bone_callback(i,i,true)
end
Script Bone Callback
The second type of bone callbacks uses a lua function to modify the final position of a bone.
It is called each frame so don't use too many of them if you can avoid it :)
We need a function for our callback.
It takes the matrix of the bone and does calculations on it to modify the final bone matrix.
See the following example from the lockpick UI
function cylinder_callback(mat)
-- temp copy of position since setHPB overwrites it
local temp = mat.c
local hpb = mat:getHPB()
-- apply our rotation
mat:setHPB(hpb.x, hpb.y + cylinder_rotation, hpb.z)
-- revert to saved position
mat.c = temp
-- return modified matrix to the engine
return mat
end
Since we want the cylinder_rotation to be applied on top of an animated lock, we don't disable animation calculation this time (third argument is false
).
lockpicker:bone_callback(1, cylinder_callback, false)
Removing Bone Callbacks
Bone callbacks can be removed like this
-- remove bone callback from bone with ID 1
lockpicker:remove_bone_callback(1)
Persisting through model change
As mentioned before, you can tell the engine to keep existing bone callbacks in place when changing the attachment model by setting the second argument to true in set_model
lockpicker:set_model("path\\to\\other_lock", true)
Attaching Script UI
Another nice to have feature, you can attach script 3D UI to attachments (UI like on the dosimeter or rf receiver)
Check gamedata\scripts\ui_dosimeter.script
to see how script 3D UI works.
Available functions:
get_ui()
get_ui_bone()
get_ui_rotation()
set_ui(string)
set_ui_bone(number)
set_ui_position(vector)
set_ui_position(number, number, number)
set_ui_position()
set_ui_rotation(vector)
set_ui_rotation(number, number, number)
Let's say, we want to display the dosimeter UI on the attachment model:
test_att:set_ui("ui_dosimeter.get_UI")
test_att:set_ui_bone(5)
test_att:set_ui_position(0,0.1,0)
test_att:set_ui_rotation(90,0,0)
You can even change the scale of the UI using bone callbacks!
Just change the scale of the bone the UI is attached to.
(Better use a bone that is not actually used by the model, or you'll scale a part of it.)
test_att:bone_callback(5, scale_func, false)
local scale_mat = matrix():scale(1.5, 1.5, 1.5)
function scale_func(mat)
mat:mulA_43(scale_mat)
return mat
end