Just to show off what I came up with. See it as a poor proof of concept:
Things to note:
- It is a hack, it has bugs, it is slow and therefore should not be used in production, at least not the way I implemented it.
- The good thing about it is that it is pretty scalable technique and doesn’t need any mesh setup, and it can probably work with any skeleton you throw at it.
- It will kill draw calls because each detached limb is a separate AnimatedModel.
- The code is a mess, sorry, so don’t try to understand it just run it.
- Decals disabled.
- Left mouse throws ball, Right mouse dismembers.
- Dismember only occurs when ragdoll is on.
!!! VIRTUAL GORE AHEAD !!!
LUA Code:
require "LuaScripts/Utilities/Sample"
function Start()
cache.autoReloadResources = true
-- Execute the common startup for samples
SampleStart()
-- Create the scene content
CreateScene()
-- Create the UI content
CreateInstructions()
-- Setup the viewport for displaying the scene
SetupViewport()
-- Set the mouse mode to use in the sample
SampleInitMouseMode(MM_RELATIVE)
-- Hook up to the frame update and render post-update events
SubscribeToEvents()
end
function CreateScene()
scene_ = Scene()
-- Create octree, use default volume (-1000, -1000, -1000) to (1000, 1000, 1000)
-- Create a physics simulation world with default parameters, which will update at 60fps. Like the Octree must
-- exist before creating drawable components, the PhysicsWorld must exist before creating physics components.
-- Finally, create a DebugRenderer component so that we can draw physics debug geometry
scene_:CreateComponent("Octree")
scene_:CreateComponent("PhysicsWorld")
scene_:CreateComponent("DebugRenderer")
-- Create a Zone component for ambient lighting & fog control
local zoneNode = scene_:CreateChild("Zone")
local zone = zoneNode:CreateComponent("Zone")
zone.boundingBox = BoundingBox(-1000.0, 1000.0)
zone.ambientColor = Color(0.15, 0.15, 0.15)
zone.fogColor = Color(0.5, 0.5, 0.7)
zone.fogStart = 100.0
zone.fogEnd = 300.0
-- Create a directional light to the world. Enable cascaded shadows on it
local lightNode = scene_:CreateChild("DirectionalLight")
lightNode.direction = Vector3(0.6, -1.0, 0.8)
local light = lightNode:CreateComponent("Light")
light.lightType = LIGHT_DIRECTIONAL
light.castShadows = true
light.shadowBias = BiasParameters(0.00025, 0.5)
-- Set cascade splits at 10, 50 and 200 world units, fade shadows out at 80% of maximum shadow distance
light.shadowCascade = CascadeParameters(10.0, 50.0, 200.0, 0.0, 0.8)
-- Create a floor object, 500 x 500 world units. Adjust position so that the ground is at zero Y
local floorNode = scene_:CreateChild("Floor")
floorNode.position = Vector3(0.0, -0.5, 0.0)
floorNode.scale = Vector3(500.0, 1.0, 500.0)
local floorObject = floorNode:CreateComponent("StaticModel")
floorObject.model = cache:GetResource("Model", "Models/Box.mdl")
floorObject.material = cache:GetResource("Material", "Materials/StoneTiled.xml")
-- Make the floor physical by adding RigidBody and CollisionShape components
local body = floorNode:CreateComponent("RigidBody")
-- We will be spawning spherical objects in this sample. The ground also needs non-zero rolling friction so that
-- the spheres will eventually come to rest
local shape = floorNode:CreateComponent("CollisionShape")
-- Set a box shape of size 1 x 1 x 1 for collision. The shape will be scaled with the scene node scale, so the
-- rendering and physics representation sizes should match (the box model is also 1 x 1 x 1.)
shape:SetBox(Vector3(1.0, 1.0, 1.0))
-- Create animated models
for z = -1, 1 do
for x = -4, 4 do
local modelNode = scene_:CreateChild("Jack")
modelNode.position = Vector3(x * 5.0, 0.0, z * 5.0)
modelNode.rotation = Quaternion(0.0, 180.0, 0.0)
local modelObject = modelNode:CreateComponent("AnimatedModel")
local model = cache:GetResource("Model", "Models/Jack.mdl")
modelObject.model = model
modelObject.material = cache:GetResource("Material", "Materials/Jack.xml")
modelObject.castShadows = true
-- Set the model to also update when invisible to avoid staying invisible when the model should come into
-- view, but does not as the bounding box is not updated
modelObject.updateInvisible = true
-- Create a rigid body and a collision shape. These will act as a trigger for transforming the
-- model into a ragdoll when hit by a moving object
local body = modelNode:CreateComponent("RigidBody")
-- The trigger mode makes the rigid body only detect collisions, but impart no forces on the
-- colliding objects
body.trigger = true
local shape = modelNode:CreateComponent("CollisionShape")
-- Create the capsule shape with an offset so that it is correctly aligned with the model, which
-- has its origin at the feet
shape:SetCapsule(0.7, 2.0, Vector3(0.0, 1.0, 0.0))
-- Create a custom script object that reacts to collisions and creates the ragdoll
modelNode:CreateScriptObject("CreateRagdoll")
end
end
-- Create the camera. Limit far clip distance to match the fog. Note: now we actually create the camera node outside
-- the scene, because we want it to be unaffected by scene load / save
cameraNode = Node()
local camera = cameraNode:CreateComponent("Camera")
camera.farClip = 300.0
-- Set an initial position for the camera scene node above the floor
cameraNode.position = Vector3(0.0, 5.0, -20.0)
end
function CreateInstructions()
-- Construct new Text object, set string to display and font to use
local instructionText = ui.root:CreateChild("Text")
instructionText:SetText(
"Use WASD keys and mouse to move\n"..
"LMB to spawn physics objects\n"..
"F5 to save scene, F7 to load\n"..
"Space to toggle physics debug geometry")
instructionText:SetFont(cache:GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15)
-- The text has multiple rows. Center them in relation to each other
instructionText.textAlignment = HA_CENTER
-- Position the text relative to the screen center
instructionText.horizontalAlignment = HA_CENTER
instructionText.verticalAlignment = VA_CENTER
instructionText:SetPosition(0, ui.root.height / 4)
end
function SetupViewport()
-- Set up a viewport to the Renderer subsystem so that the 3D scene can be seen
local viewport = Viewport:new(scene_, cameraNode:GetComponent("Camera"))
renderer:SetViewport(0, viewport)
end
function SubscribeToEvents()
-- Subscribe HandleUpdate() function for processing update events
SubscribeToEvent("Update", "HandleUpdate")
-- Subscribe HandlePostRenderUpdate() function for processing the post-render update event, during which we request
-- debug geometry
SubscribeToEvent("PostRenderUpdate", "HandlePostRenderUpdate")
end
function DrawDecalAt(ray)
local result = scene_:GetComponent("Octree"):RaycastSingle(ray, RAY_TRIANGLE, 10000, DRAWABLE_GEOMETRY)
if not result then
return
end
local node = result.drawable.node
local ds = node:GetComponent("DecalSet")
if not ds then
ds = node:CreateComponent("DecalSet")
ds:SetMaterial(cache:GetResource("Material","Materials/wound1.xml"))
ds:SetMaxIndices(1024 * 100)
ds:SetMaxVertices(1024 * 100)
end
ds:AddDecal(result.drawable, result.position, cameraNode.rotation, 0.5, 1, 0.5, Vector2(0,0), Vector2(1,1), 0, 0.1)
end
function PushRay(dismember)
local ray = cameraNode:GetComponent("Camera"):GetScreenRay(0.5,0.5)
local resultPhys = scene_:GetComponent("PhysicsWorld"):RaycastSingle(ray, 10000)
if resultPhys then
local node = resultPhys.body.node
if not resultPhys.body.active then
resultPhys.body:Activate()
end
local force = 3
resultPhys.body:ApplyImpulse(ray.direction * force, node:WorldToLocal(resultPhys.position))
if dismember and not node:HasTag("dismembered") then
DismemberLimb(node)
end
--DrawDecalAt(ray)
end
end
function DismemberLimb(node)
local constraint = node:GetComponent("Constraint")
if constraint then
constraint.enabled = false
SeparateLimb1(node)
end
end
function SeparateLimb1(node)
node:AddTag("dismembered") -- add tag so we dont do process this bone again
local nodeClone = node:Clone()
local limbNode = scene_:CreateChild("limb") -- contains the limb
local bodiesNode = limbNode:CreateChild("bodies") -- contains rigid bodies
nodeClone.parent = bodiesNode
-- ORIGINAL MODEL TWEAK
local children = node:GetChildrenWithComponent("RigidBody",true)
for i=1, #children, 1 do
children[i]:RemoveAllComponents()
end
node:RemoveAllComponents()
node:SetScale(0)
-- END ORIGINAL MODEL TWEAK
local skelNode = nodeClone:CreateChild("skeleton") -- node that holds the skel bones
local am = skelNode:CreateComponent("AnimatedModel") -- create the skell bones
local model = cache:GetResource("Model", "Models/Jack.mdl")
am.model = model
local skelBone = skelNode:GetChild(node.name, true) -- get same node as our cloned node
local reparentNode = skelNode:CreateChild("reparent") -- this is the node that stores the bones that get scaled to 0
-- reparent
children = skelNode:GetChildren(false)
for i=1, #children, 1 do
children[i].parent = reparentNode
end
FixSkeleton(bodiesNode, skelNode) -- Fix and assigns each new bone to it repective rigidbodied bone that was cloned (nodeClone)
reparentNode:SetScale(0) -- set the rest of the body to scale 0
skelBone.parent = skelNode -- take detached limbBone out of the reparentNode
-- so basically in the end we probably have this
-- limbNode (Main node where the detached limbs resides)
-- bodiesNode (has the rigidbodies tha we cloned)
-- nodeClone (the clonned node)
-- skelNode<AnimatedModel> (has the new skeleton + animated model)
-- skelBone (aka the actual detached limb)
-- reparentNode (every bone that is not the detached limb should be here)
end
function FixSkeleton(bodiesNode, skelNode) -- goes through each rigidbody, gets the respective node of our new AnimatedModel that matches rigidbodies name, and set it as a child of the rigidibody, so it can follow it around. This whole process could be avoided if I could 'retarget' or 'replace' bone nodes, but since I cant, we need to grab the automatically created bone node and set it to be child of its respective 'rigidbody'
local bodies = bodiesNode:GetChildren()
for i = 1, #bodies, 1 do
FixSkeleton(bodies[i], skelNode)
local bone = skelNode:GetChild(bodies[i].name, true)
if bone then
bone.parent = bodies[i]
bone:SetTransform(Vector3.ZERO, Quaternion.IDENTITY)
bone:SetScale(1)
end
end
end
function MoveCamera(timeStep)
-- Do not move if the UI has a focused element (the console)
if ui.focusElement ~= nil then
return
end
-- Movement speed as world units per second
local MOVE_SPEED = 20.0
-- Mouse sensitivity as degrees per pixel
local MOUSE_SENSITIVITY = 0.1
-- Use this frame's mouse motion to adjust camera node yaw and pitch. Clamp the pitch between -90 and 90 degrees
local mouseMove = input.mouseMove
yaw = yaw + MOUSE_SENSITIVITY * mouseMove.x
pitch = pitch +MOUSE_SENSITIVITY * mouseMove.y
pitch = Clamp(pitch, -90.0, 90.0)
-- Construct new orientation for the camera scene node from yaw and pitch. Roll is fixed to zero
cameraNode.rotation = Quaternion(pitch, yaw, 0.0)
-- Read WASD keys and move the camera scene node to the corresponding direction if they are pressed
if input:GetKeyDown(KEY_W) then
cameraNode:Translate(Vector3(0.0, 0.0, 1.0) * MOVE_SPEED * timeStep)
end
if input:GetKeyDown(KEY_S) then
cameraNode:Translate(Vector3(0.0, 0.0, -1.0) * MOVE_SPEED * timeStep)
end
if input:GetKeyDown(KEY_A) then
cameraNode:Translate(Vector3(-1.0, 0.0, 0.0) * MOVE_SPEED * timeStep)
end
if input:GetKeyDown(KEY_D) then
cameraNode:Translate(Vector3(1.0, 0.0, 0.0) * MOVE_SPEED * timeStep)
end
-- "Shoot" a physics object with left mousebutton
if input:GetMouseButtonPress(MOUSEB_LEFT) then
SpawnObject()
end
if input:GetMouseButtonPress(MOUSEB_RIGHT) then
nClock = os.clock()
PushRay(true)
print("Elapsed time is: " .. os.clock()-nClock)
end
-- Check for loading/saving the scene. Save the scene to the file Data/Scenes/Physics.xml relative to the executable
-- directory
if input:GetKeyPress(KEY_F5) then
scene_:SaveXML(fileSystem:GetProgramDir().."Data/Scenes/Ragdolls.xml")
end
if input:GetKeyPress(KEY_F7) then
scene_:LoadXML(fileSystem:GetProgramDir().."Data/Scenes/Ragdolls.xml")
end
-- Toggle debug geometry with space
if input:GetKeyPress(KEY_SPACE) then
drawDebug = not drawDebug
end
end
function SpawnObject()
local boxNode = scene_:CreateChild("Sphere")
boxNode.position = cameraNode.position
boxNode.rotation = cameraNode.rotation
boxNode:SetScale(0.25)
local boxObject = boxNode:CreateComponent("StaticModel")
boxObject.model = cache:GetResource("Model", "Models/Sphere.mdl")
boxObject.material = cache:GetResource("Material", "Materials/StoneSmall.xml")
boxObject.castShadows = true
local body = boxNode:CreateComponent("RigidBody")
body.mass = 1.0
body.rollingFriction = 0.15
local shape = boxNode:CreateComponent("CollisionShape")
shape:SetSphere(1.0)
local OBJECT_VELOCITY = 10.0
-- Set initial velocity for the RigidBody based on camera forward vector. Add also a slight up component
-- to overcome gravity better
body.linearVelocity = cameraNode.rotation * Vector3(0.0, 0.25, 1.0) * OBJECT_VELOCITY
end
function HandleUpdate(eventType, eventData)
-- Take the frame time step, which is stored as a float
local timeStep = eventData["TimeStep"]:GetFloat()
-- Move the camera, scale movement with time step
MoveCamera(timeStep)
end
function HandlePostRenderUpdate(eventType, eventData)
-- If draw debug mode is enabled, draw physics debug geometry. Use depth test to make the result easier to interpret
if drawDebug then
renderer:DrawDebugGeometry(true)
scene_:GetComponent("PhysicsWorld"):DrawDebugGeometry(true)
end
end
-- CreateRagdoll script object class
CreateRagdoll = ScriptObject()
function CreateRagdoll:Start()
-- Subscribe physics collisions that concern this scene node
self:SubscribeToEvent(self.node, "NodeCollision", "CreateRagdoll:HandleNodeCollision")
end
function CreateRagdoll:HandleNodeCollision(eventType, eventData)
-- Get the other colliding body, make sure it is moving (has nonzero mass)
local otherBody = eventData["OtherBody"]:GetPtr("RigidBody")
if otherBody.mass > 0.0 then
-- We do not need the physics components in the AnimatedModel's root scene node anymore
self.node:RemoveComponent("RigidBody")
self.node:RemoveComponent("CollisionShape")
-- Create RigidBody & CollisionShape components to bones
self:CreateRagdollBone("Bip01_Pelvis", SHAPE_BOX, Vector3(0.3, 0.2, 0.25), Vector3(0.0, 0.0, 0.0),
Quaternion(0.0, 0.0, 0.0))
self:CreateRagdollBone("Bip01_Spine1", SHAPE_BOX, Vector3(0.35, 0.2, 0.3), Vector3(0.15, 0.0, 0.0),
Quaternion(0.0, 0.0, 0.0))
self:CreateRagdollBone("Bip01_L_Thigh", SHAPE_CAPSULE, Vector3(0.175, 0.45, 0.175), Vector3(0.25, 0.0, 0.0),
Quaternion(0.0, 0.0, 90.0))
self:CreateRagdollBone("Bip01_R_Thigh", SHAPE_CAPSULE, Vector3(0.175, 0.45, 0.175), Vector3(0.25, 0.0, 0.0),
Quaternion(0.0, 0.0, 90.0))
self:CreateRagdollBone("Bip01_L_Calf", SHAPE_CAPSULE, Vector3(0.15, 0.55, 0.15), Vector3(0.25, 0.0, 0.0),
Quaternion(0.0, 0.0, 90.0))
self:CreateRagdollBone("Bip01_R_Calf", SHAPE_CAPSULE, Vector3(0.15, 0.55, 0.15), Vector3(0.25, 0.0, 0.0),
Quaternion(0.0, 0.0, 90.0))
self:CreateRagdollBone("Bip01_Head", SHAPE_CAPSULE, Vector3(0.2, 0.2, 0.2), Vector3(0.1, 0.0, 0.0),
Quaternion(0.0, 0.0, 0.0))
self:CreateRagdollBone("Bip01_L_UpperArm", SHAPE_CAPSULE, Vector3(0.15, 0.35, 0.15), Vector3(0.1, 0.0, 0.0),
Quaternion(0.0, 0.0, 90.0))
self:CreateRagdollBone("Bip01_R_UpperArm", SHAPE_CAPSULE, Vector3(0.15, 0.35, 0.15), Vector3(0.1, 0.0, 0.0),
Quaternion(0.0, 0.0, 90.0))
self:CreateRagdollBone("Bip01_L_Forearm", SHAPE_CAPSULE, Vector3(0.125, 0.4, 0.125), Vector3(0.2, 0.0, 0.0),
Quaternion(0.0, 0.0, 90.0))
self:CreateRagdollBone("Bip01_R_Forearm", SHAPE_CAPSULE, Vector3(0.125, 0.4, 0.125), Vector3(0.2, 0.0, 0.0),
Quaternion(0.0, 0.0, 90.0))
self:CreateRagdollBone("Bip01_L_Hand", SHAPE_BOX, Vector3(0.1, 0.2, 0.1), Vector3(0.1, 0.0, 0.0),
Quaternion(0.0, 0.0, 90.0))
self:CreateRagdollBone("Bip01_R_Hand", SHAPE_BOX, Vector3(0.1, 0.2, 0.1), Vector3(0.1, 0.0, 0.0),
Quaternion(0.0, 0.0, 90.0))
self:CreateRagdollBone("Bip01_L_Foot", SHAPE_BOX, Vector3(0.125, 0.3, 0.07), Vector3(0.1, 0.075, 0.0),
Quaternion(0.0, 0.0, 0.0))
self:CreateRagdollBone("Bip01_R_Foot", SHAPE_BOX, Vector3(0.125, 0.3, 0.07), Vector3(0.1, 0.075, 0.0),
Quaternion(0.0, 0.0, 0.0))
-- Create Constraints between bones
self:CreateRagdollConstraint("Bip01_L_Thigh", "Bip01_Pelvis", CONSTRAINT_CONETWIST, Vector3(0.0, 0.0, -1.0),
Vector3(0.0, 0.0, 1.0), Vector2(45.0, 45.0), Vector2(0.0, 0.0), true)
self:CreateRagdollConstraint("Bip01_R_Thigh", "Bip01_Pelvis", CONSTRAINT_CONETWIST, Vector3(0.0, 0.0, -1.0),
Vector3(0.0, 0.0, 1.0), Vector2(45.0, 45.0), Vector2(0.0, 0.0), true)
self:CreateRagdollConstraint("Bip01_L_Calf", "Bip01_L_Thigh", CONSTRAINT_HINGE, Vector3(0.0, 0.0, -1.0),
Vector3(0.0, 0.0, -1.0), Vector2(90.0, 0.0), Vector2(0.0, 0.0), true)
self:CreateRagdollConstraint("Bip01_R_Calf", "Bip01_R_Thigh", CONSTRAINT_HINGE, Vector3(0.0, 0.0, -1.0),
Vector3(0.0, 0.0, -1.0), Vector2(90.0, 0.0), Vector2(0.0, 0.0), true)
self:CreateRagdollConstraint("Bip01_Spine1", "Bip01_Pelvis", CONSTRAINT_HINGE, Vector3(0.0, 0.0, 1.0),
Vector3(0.0, 0.0, 1.0), Vector2(45.0, 0.0), Vector2(-10.0, 0.0), true)
self:CreateRagdollConstraint("Bip01_Head", "Bip01_Spine1", CONSTRAINT_CONETWIST, Vector3(-1.0, 0.0, 0.0),
Vector3(-1.0, 0.0, 0.0), Vector2(0.0, 30.0), Vector2(0.0, 0.0), true)
self:CreateRagdollConstraint("Bip01_L_UpperArm", "Bip01_Spine1", CONSTRAINT_CONETWIST, Vector3(0.0, -1.0, 0.0),
Vector3(0.0, 1.0, 0.0), Vector2(45.0, 45.0), Vector2(0.0, 0.0), false)
self:CreateRagdollConstraint("Bip01_R_UpperArm", "Bip01_Spine1", CONSTRAINT_CONETWIST, Vector3(0.0, -1.0, 0.0),
Vector3(0.0, 1.0, 0.0), Vector2(45.0, 45.0), Vector2(0.0, 0.0), false)
self:CreateRagdollConstraint("Bip01_L_Forearm", "Bip01_L_UpperArm", CONSTRAINT_HINGE, Vector3(0.0, 0.0, -1.0),
Vector3(0.0, 0.0, -1.0), Vector2(90.0, 0.0), Vector2(0.0, 0.0), true)
self:CreateRagdollConstraint("Bip01_R_Forearm", "Bip01_R_UpperArm", CONSTRAINT_HINGE, Vector3(0.0, 0.0, -1.0),
Vector3(0.0, 0.0, -1.0), Vector2(90.0, 0.0), Vector2(0.0, 0.0), true)
self:CreateRagdollConstraint("Bip01_L_Hand", "Bip01_L_Forearm", CONSTRAINT_CONETWIST, Vector3(0.0, 0.0, -1.0),
Vector3(0.0, 0.0, -1.0), Vector2(45, 0), Vector2(0, 0), true);
self:CreateRagdollConstraint("Bip01_R_Hand", "Bip01_R_Forearm", CONSTRAINT_CONETWIST, Vector3(0.0, 0.0, -1.0),
Vector3(0.0, 0.0, -1.0), Vector2(45, 0), Vector2(0, 0), true);
self:CreateRagdollConstraint("Bip01_L_Foot", "Bip01_L_Calf", CONSTRAINT_CONETWIST, Vector3(0.0, 0.0, -1.0),
Vector3(0.0, 0.0, -1.0), Vector2(45, 0), Vector2(0, 0), true);
self:CreateRagdollConstraint("Bip01_R_Foot", "Bip01_R_Calf", CONSTRAINT_CONETWIST, Vector3(0.0, 0.0, -1.0),
Vector3(0.0, 0.0, -1.0), Vector2(45, 0), Vector2(0, 0), true);
-- Disable keyframe animation from all bones so that they will not interfere with the ragdoll
local model = self.node:GetComponent("AnimatedModel")
local skeleton = model.skeleton
for i = 0, skeleton.numBones - 1 do
skeleton:GetBone(i).animated = false
end
-- Finally remove self (the ScriptInstance which holds this script object) from the scene node. Note that this must
-- be the last operation performed in the function
self.instance:Remove()
end
end
function CreateRagdoll:CreateRagdollBone(boneName, type, size, position, rotation)
-- Find the correct child scene node recursively
local boneNode = self.node:GetChild(boneName, true)
if boneNode == nil then
print("Could not find bone " .. boneName .. " for creating ragdoll physics components\n")
return
end
local body = boneNode:CreateComponent("RigidBody")
-- Set mass to make movable
body.mass = 1.0
body.restitution = 1
body.friction = 1
-- Set damping parameters to smooth out the motion
body.rollingFriction = 0.01
body.linearDamping = 0.05
body.angularDamping = 0.2
-- Set rest thresholds to ensure the ragdoll rigid bodies come to rest to not consume CPU endlessly
body.linearRestThreshold = 2
body.angularRestThreshold = 3
local shape = boneNode:CreateComponent("CollisionShape")
-- We use either a box or a capsule shape for all of the bones
if type == SHAPE_BOX then
shape:SetBox(size, position, rotation)
else
shape:SetCapsule(size.x, size.y, position, rotation)
end
end
function CreateRagdoll:CreateRagdollConstraint(boneName, parentName, type, axis, parentAxis, highLimit, lowLimit, disableCollision)
local boneNode = self.node:GetChild(boneName, true)
local parentNode = self.node:GetChild(parentName, true)
if boneNode == nil then
print("Could not find bone " .. boneName .. " for creating ragdoll constraint\n")
return
end
if parentNode == nil then
print("Could not find bone " .. parentName .. " for creating ragdoll constraint\n")
return
end
local constraint = boneNode:CreateComponent("Constraint")
constraint.constraintType = type
-- Most of the constraints in the ragdoll will work better when the connected bodies don't collide against each other
constraint.disableCollision = disableCollision
-- The connected body must be specified before setting the world position
constraint.otherBody = parentNode:GetComponent("RigidBody")
-- Position the constraint at the child bone we are connecting
constraint.worldPosition = boneNode.worldPosition
-- Configure axes and limits
constraint.axis = axis
constraint.otherAxis = parentAxis
constraint.highLimit = highLimit
constraint.lowLimit = lowLimit
end
-- Create XML patch instructions for screen joystick layout specific to this sample app
function GetScreenJoystickPatchString()
return
"<patch>" ..
" <remove sel=\"/element/element[./attribute[@name='Name' and @value='Button0']]/attribute[@name='Is Visible']\" />" ..
" <replace sel=\"/element/element[./attribute[@name='Name' and @value='Button0']]/element[./attribute[@name='Name' and @value='Label']]/attribute[@name='Text']/@value\">Spawn</replace>" ..
" <add sel=\"/element/element[./attribute[@name='Name' and @value='Button0']]\">" ..
" <element type=\"Text\">" ..
" <attribute name=\"Name\" value=\"MouseButtonBinding\" />" ..
" <attribute name=\"Text\" value=\"LEFT\" />" ..
" </element>" ..
" </add>" ..
" <remove sel=\"/element/element[./attribute[@name='Name' and @value='Button1']]/attribute[@name='Is Visible']\" />" ..
" <replace sel=\"/element/element[./attribute[@name='Name' and @value='Button1']]/element[./attribute[@name='Name' and @value='Label']]/attribute[@name='Text']/@value\">Debug</replace>" ..
" <add sel=\"/element/element[./attribute[@name='Name' and @value='Button1']]\">" ..
" <element type=\"Text\">" ..
" <attribute name=\"Name\" value=\"KeyBinding\" />" ..
" <attribute name=\"Text\" value=\"SPACE\" />" ..
" </element>" ..
" </add>" ..
"</patch>"
end
How it works, let’s say I shot an Arm:
PS: you will see me using the word bone and node interchangeably, because they kinda are.
1- Get the node that was hit, an arm for example.
2- Clone the node. This will also clone children, great.
3- Remove unnecessary components from original node (rigid bodies, contraints, etc.)
4- Scale original node to 0, now there is no arm.
5- Reparent cloned node for a cleaner hierarchy.
What we have so far is:
- Original AnimatedModel without the arm, because it got scaled to 0 and has no components.
- A cloned node that has no other component but the rigidbodies and constraints, therefore, no way to render it.
And this is where the dirty hack begins. The most correct way to attach geometry to the arm would be something like this:
6- Create a new AnimatedModel, obvious step.
Now we need the arm of this AnimatedModel to follow our cloned arm right ? in other words, we need to bind our cloned arm node to the AnimatedModel. So we would do something like
7- Use a function like “ReplaceBone(originalBoneNode, newBoneNode)” that could recursively replace each original bone node of the AnimatedModel with our cloned arm bone nodes.
But the above 2 steps did not happen exactly like that, because there is no such function that replaces a bone with another (at least not in LUA). When you create an AnimatedModel, it automatically creates the bone nodes for you, and there is no way to replace them with custom ones. So there is no way to ‘bind’ our cloned arm node to the AnimatedModel we just created…
So I had to hack this out, and the way I did it was by, pay attention: Reparenting each new bone to their respective cloned arm bone. So each new bone gets to be a child of their respective cloned arm bone.
8- Scale the rest of the body in relation to the cloned arm (the inverse of step 4), so that there is no body, but jut the arm.