I’ve extended example#18 (with external file BoneCollisions.lua, content below) so that Jack is attacked by a ninja when pressing key B
Modifications to file 18_CharacterDemo.lua:
require “LuaScripts/BoneCollisions”
in HandleUpdate function:
if input:GetKeyPress(KEY_B) then CreateEnemy() end – Create ennemy (ninja)
hitTest(characterNode:GetChild(“Bip01_Head”, true).worldPosition) – Check Jack’s head collisions
What’s strange is that using “self.node:LookAt(dir, Vector3(0,1,0))” to rotate the ninja toward Jack produces a Bullet AABB overflow.
-- BoneCollisions.lua
-- Demonstrates:
-- - accurate bone collisions
-- - enemy head always facing the main character
-- - playing context animation
local BRAKE_FORCE = 0.3 -- Set slightly upper than Jack so that Jack is faster
function boundingBoxescollision(bbox1, bbox2) -- Check if 2 BoundingBoxes overlap (3D AABB collision test)
if bbox2.min.x >= bbox1.max.x -- too much @left
or bbox2.max.x <= bbox1.min.x -- too much @right
or bbox2.min.y >= bbox1.max.y -- too high
or bbox2.max.y <= bbox1.min.y -- too low
or bbox2.min.z >= bbox1.max.z -- too forward
or bbox2.max.z <= bbox1.min.z -- too backward
then return false
else return true end
end
function customBoundingBox(position, size, visible)
local bboxSize = Vector3(size, size, size) -- Size of the BoundingBox
local bbox = BoundingBox(position -bboxSize, position + bboxSize) -- Set BoundingBox position & size
if visible == true then scene_:GetComponent("DebugRenderer"):AddBoundingBox(bbox, Color(0.5, 0.5, 0.5)) end -- Draw BoundingBox
return bbox
end
function hitTest(hitPos)
local bbox = customBoundingBox(hitPos, 0.1, true) -- Get BoundingBox from hitPos & size and draw it
local result = scene_:GetComponent("Octree"):GetDrawables(bbox, DRAWABLE_GEOMETRY)
-- print("Octree bbox hits: " .. result:Size() .. " at position " .. hitPos:ToString())
for i = 0, result:Size() -1 do
if result[i].node.name ~= "Jack" then -- Exclude Self (Jack)
if result[i].node:GetComponent("StaticModel") ~= nil then scene_:GetComponent("DebugRenderer"):AddBoundingBox(result[i].node:GetComponent("StaticModel").worldBoundingBox, Color(1, 1, 1)) end -- Draw node BoundingBox
if result[i].node:GetComponent("AnimatedModel") ~= nil then -- For AnimatedModel, check if specified bones are colliding with the BoundingBox
print(boundingBoxescollision(customBoundingBox(result[i].node:GetChild("Joint13", true).worldPosition, 0.1, true), bbox)) -- Check collision between the 2 BoundingBoxes & draw them
if boundingBoxescollision(customBoundingBox(result[i].node:GetChild("Joint13", true).worldPosition, 0.1, true), bbox) then
--headShot()
end
end
-- print("Result " .. i .. " Drawable type: " .. result[i].drawable.typeName .. " node name: " .. result[i].node.name .. " node pos: " .. result[i].node.position:ToString())
end
end
end
function headShot()
-- do something with Jack (context animation, move backward, rotate head backward...)
end
function CreateEnemy()
local EnemyNode = scene_:CreateChild("Enemy")
EnemyNode.position = Vector3(0, 0, 10)
local object = EnemyNode:CreateComponent("AnimatedModel")
object.model = cache:GetResource("Model", "Models/Ninja.mdl")
object.material = cache:GetResource("Material", "Materials/Ninja.xml")
object.castShadows = true
local body = EnemyNode:CreateComponent("RigidBody")
body.mass = 1 -- Set non-zero mass so that the body becomes dynamic
body.angularFactor = Vector3(0, 0, 0) -- Keep Enemy always upright
local shape = EnemyNode:CreateComponent("CollisionShape")
shape:SetCapsule(0.7, 1.8, Vector3(0, 0.9, 0)) -- diameter/height/position
shape.offset = Vector3(0,1,0)
EnemyNode:CreateScriptObject("Enemy") -- Create the Enemy logic object, which takes care of steering the rigidbody
EnemyNode:CreateComponent("AnimationController") -- Create the animation controller
end
-- Enemy script object class
Enemy = ScriptObject()
function Enemy:Start()
self.isFighting = false
self:SubscribeToEvent(self.node, "NodeCollision", "Enemy:HandleNodeCollision") -- Subscribe to NodeCollision physics event
end
function Enemy:Load(deserializer)
self.isFighting = deserializer:ReadBool()
end
function Enemy:Save(serializer)
serializer:WriteBool(self.isFighting)
end
function Enemy:HandleNodeCollision(eventType, eventData)
local otherBody = eventData:GetPtr("RigidBody", "OtherBody") -- Get the other colliding body
local otherNode = eventData:GetPtr("Node", "OtherNode") -- Get the other colliding node
if otherNode.name == "Jack" then self.isFighting = true else self.isFighting = false end -- Check if fighting to trigger appropriate animation
end
function Enemy:FixedPostUpdate(timeStep) -- NB: FixedPostUpdate in reaction to Jack's FixedUpdate
local body = self.node:GetComponent("RigidBody")
if not self.isFighting then -- When fighting, don't move Enemy
local dir = characterNode.position - self.node.position
--self.node:LookAt(dir, Vector3(0,1,0)) -- Uncommenting this line produces an AABB overflow
-- Rotate Enemy head toward Jack
local skeleton = self.node:GetComponent("AnimatedModel").skeleton
skeleton:GetBone("Joint8").animated = false -- Disable head animation
local headNode = self.node:GetChild("Joint8", true)
headNode:LookAt(characterNode.position + Vector3(0, 1.7, 0), Vector3(0, 1 ,0)) -- Look at head level
--Todo: restrict lateral rotation
-- Update movement
local velocity = body.linearVelocity
local planeVelocity = Vector3(velocity.x, 0, velocity.z) -- Velocity on the XZ plane
local brakeForce = planeVelocity * -BRAKE_FORCE
body:ApplyImpulse(dir:Normalized())
body:ApplyImpulse(brakeForce)
end
-- Update Animations
local animCtrl = self.node:GetComponent("AnimationController")
if self.isFighting then animCtrl:PlayExclusive("Models/Ninja_Attack3.ani", 0, true, 0.2)
else animCtrl:PlayExclusive("Models/Ninja_Walk.ani", 0, true, 0.3) end
animCtrl:SetSpeed("Models/Ninja_Walk.ani", 1)
end