Jump to content

Breakable Objects


I love the physics in games like Amnesia and the Half-Life series. Realistic interactive physics add a degree of interactivity that immerses the player in the world like nothing else, and I plan on physics being a key feature in our SCP game. In this article, I will walk you through the steps I took to make a breakable crate.

The Breakable Model

As usual, I like to start with the artwork, because it's key to everything. I usually learn so much just by using actual game-ready artwork, rather than blocky "programmer art". Plus, at the end I get something I am proud of showing off. @reepblue created this model and provided it for use with Leadwerks, and @Rich D textured it and broke it up into fragments, for your destructive enjoyment.

image.thumb.png.927415219c3fe4a4989c66835f602f94.png

In fact, breakable objects are primarily an art problem. The basic concept is very simple: When an object is damaged, hide it and replace it with a series of fragments that represent the object broken into pieces. This is not a complicated programming problem at all, but let's dig into the details and see what happens.

Colliders

The first question is how the colliders for the model fragments should be made.

If your colliders fit too perfectly, the broken object will form a strong interlocking structure, and won't fall apart unless a large force is applied to it. Even worse, if your fragment colliders intersect the moment they are enabled, the object will form explosive forces that could send the crate or the player flying! We definitely don't want this happening.

The main question is, should fragments of the same object even intersect at all? I was curious how Half-Life 2 handled this, so I fired it up, broke a crate, picked up one of the pieces, and saw that fragments of a broken object don't collide with each other at all! Here I am holding one piece, and it freely intersects the other fragments with no collision, even if I drop it. I have 36 hours of play time in this game, and I never noticed this until now!

debugging23.gif.d2055399f5b87c9d3920426b0e153341.gif

Well, if this solution is good enough for Valve, then I don't feel a need to make it more complicated than it needs to be. This simplifies our goals because we can just set the collision type of the fragments to "Debris", which will collide with the Prop and Scene collision types, but not with the player, and not with other broken pieces. That makes it perfect for this use, and we don't have to worry too much about the exact shape of the colliders for each piece of our broken object. If the fragment colliders overlap, it won't cause explosive forces or lock the pieces into place like a jigsaw puzzle.

The next question is how should the content be structured? I have a single-mesh unbroken version of the model, as well as a version that contains all the fragments separated into child limbs. I could save each limb as a separate model and maybe use some naming scheme to identify them, like crate_frag1.mdl, crate_frag2.mdl, etc. However, I think the simplest way to handle this is to make the unbroken version of the model the top-level entity in the hierarchy, and then just add the fragments as children in the model file. The script will just make the assumption that all children in the model are fragments, and hide them when the game starts:

  • Unbroken mesh
    • Fragment 1
    • Fragment 2
    • Fragment 3

I think this design accounts for the most likely use case, and making it more complicated would be more trouble than its worth.

I noticed that in the current build of Leadwerks Editor, the model editor only allows assigning a collider to the whole object. Let's fix that, so we can assign a separate collider to any object in the hierarchy!

image.thumb.png.820aa553120fc8fb557904b2b78b7d5b.png

I performed that for each limb in the broken crate model.

I then opened the unbroken crate model, and assigned a box collider to it, and saved it:

image.thumb.png.138e64de77474b28327942578d889462.png

Merging the Models

Now how to we combine these two models? An artist might want to export the two models to glTF and combine them in Blender or another program, but since I am a programmer, the easiest solution for me is to just write a simple script.

local mdl1 = LoadModel(nil, "Models/Crate/crate01.mdl")
local mdl2 = LoadModel(nil, "Models/Crate/cratebroken01.mdl")

while #mdl2.kids > 0 do
    local child = mdl2.kids[1]
    child:SetParent(mdl1)
end

mdl1:Save("Models/Crate/Crate01_Combined.mdl")

The only thing to watch out for is that when were call SetParent we are modifying the list of objects we are iterating through. Therefore, it's best to use a while loop and just get the first child each time. This tripped me up for a few minutes because I started with a simple for loop! :blink: Here's the wrong way that I tried first:

--Don't do this!
for n = 1, #mdl2.kids do
    local child = mdl2.kids[n]
    child:SetParent(mdl1)
end

I just copied and pasted my script code into the editor console, pressed enter, and it saves the model with the unbroken single mesh as the top-level limb, and all the fragments as children. Separate colliders for each limb are retained correctly:

image.thumb.png.abadc21552eb70c255d34017d96a41b3.png

Awesome!

Now that the artwork is correctly prepared, the actual code this requires will be very easy.

Breakable Object Script

Let's create a new entity script called "Breakable" in the Physics folder. We'll add a property for health, and another for the speed of a collision that will cause the object to break:

Breakable = {}
Breakable.health = 10 --"Health"
Breakable.breakspeed = 10 --"Break speed"

In the Start function, we'll just hide all the child limbs, since we have decided already that those will form the fragments of the object when it breaks:

function Breakable:Start()	
	local n
	for n = 1, #self.kids do --No problems using a for loop here since we are not modifying the entity limbs ;)
		self.kids[n]:SetMass(0)--This should already be zero, but if not let's set it automatically
		self.kids[n]:SetCollisionType(COLLISION_NONE)
		self.kids[n]:SetHidden(true)
		--self.kids[n]:SetColor(0,4,0,1) --optional visual hint for testing
	end
end

We will add a function so that the object will accept damage if something hits it. Most likely this will be caused by the player shooting or hitting object with a melee weapon, but if an enemy attacks the crate for any reason, it will also work.

function Breakable:Damage(amount)
    if self.health > 0 then
        self.health = self.health - amount
        if self.health <= 0 then
            self:Break()
        end
    end
end

In the collide function, we can add a check to see if the break speed was hit, and break the object if it was:

function Breakable:Collide(entity, position, normal, speed)
    if speed > self.breakspeed then
        self:Break()
    end
end

Finally, we add our Break function, where the object finally gets broken apart into pieces:

function Breakable:Break()
    if self:GetHidden() then return end --Make sure this only gets broken once
    if #self.kids == 0 then return end --If there are no kids then don't do anything

    --Hide the object
    self:SetHidden(true)
	
    --Calculate simple mass for each piece
    local mass = self:GetMass()
    if mass == 0 then
        mass = 5
    else
        mass = mass / #self.kids
    end
	
    --Create a table to store the fragments in
    self.fragments = {}
    while #self.kids > 0 do
        local child = self.kids[1]
        child:SetParent(nil)
        child:SetMass(mass)
        child:SetHidden(false)		
        child:SetCollisionType(COLLISION_DEBRIS)
        table.insert(self.fragments, child)
    end
end

Running the Game

I was very excited when I ran the game for the first time, in a level with a crate hooked up to this script. What would happen if I shot the object? Would it break into pieces and look natural, like I hoped it would? Here is the result:

Wow, on the first try it looks great! Adding sounds for collision and breaking would enhance the feel of it even more, and I plan to add this next. I also plan to try adding some velocity to the broken fragments, so the crate has more of an explosive appearance, instead of just falling apart in place.

Here is the complete script, with the accompanying properties definition file. The script requires a game created with the current "beta-e" branch on Steam:

Breakable.luaBreakable.json

And here are the model files I used. Crate01_Combined.mdl is the one that is ready-to-use with this script:

Crate01.zip

I even find the code itself for this very beautiful, for some reason. The weird comments with the @ signs are annotations that give Lua Language Server a hint about what type each parameter is, for nice auto-completion in our script editor. These comments are automatically generated when the script is first created.

---@class Breakable : Entity
Breakable = {}
Breakable.health = 10 --"Health"
Breakable.breakspeed = 10 --"Break speed"

---@param self Breakable
function Breakable:Start()	
    local n
    for n = 1, #self.kids do --No problems using a for loop here since we are not modifying the entity limbs ;)
        self.kids[n]:SetMass(0)--This should already be zero, but if not let's set it automatically
        self.kids[n]:SetCollisionType(COLLISION_NONE)
        self.kids[n]:SetHidden(true)
        --self.kids[n]:SetColor(0,4,0,1) --optional visual hint for testing
    end
end

---@param self Breakable
---@param entity Entity
---@param position Vec3
---@param normal Vec3
---@param speed number
function Breakable:Collide(entity, position, normal, speed)
    if speed > self.breakspeed then
        self:Break()
    end
end

---@param self Breakable
---@param amount number
function Breakable:Damage(amount)
    if self.health > 0 then
        self.health = self.health - amount
        if self.health <= 0 then
            self:Break()
        end
    end
end

---@param self Breakable
function Breakable:Break()
    if self:GetHidden() then return end --Make sure this only gets broken once
    if #self.kids == 0 then return end --If there are no kids then don't do anything

    --Hide the object
    self:SetHidden(true)
	
    --Calculate simple mass for each piece
    local mass = self:GetMass()
    if mass == 0 then
        mass = 5
    else
        mass = mass / #self.kids
    end
	
    --Create a table to store the fragments in
    self.fragments = {}
    while #self.kids > 0 do
        local child = self.kids[1]
        child:SetParent(nil)
        child:SetMass(mass)
        child:SetHidden(false)		
        child:SetCollisionType(COLLISION_DEBRIS)
        table.insert(self.fragments, child)
    end
end

If you are looking for custom artwork for your game, I can wholeheartedly recommend Rich DiGiovanni's services. I have worked with Rich on many projects, and I always get great results at a reasonable price: https://richdigiovanni.com/

  • Upvote 2

3 Comments


Recommended Comments

Josh

Posted

Here I just assigned a team value to the crate, and now the monster will attack and break them.

function Breakable:Start()

	self.team = 2 --Monsters hate this!

	local n
	for n = 1, #self.kids do --No problems using a for loop here since we are not modifying the entity limbs ;)
		self.kids[n]:SetMass(0)--This should already be zero, but if not let's set it automatically
		self.kids[n]:SetCollisionType(COLLISION_NONE)
		self.kids[n]:SetHidden(true)
		--self.kids[n]:SetColor(0,4,0,1) --optional visual hint for testing
	end
end

Here is the result: :lol:

 

Josh

Posted

This is what happens when you add many crates:

  • Haha 1
Josh

Posted

Here is what happened when I added a physics force to the monster's attack. Amazing to see this kind of thing come to life!

 

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...