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.
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!
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!
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:
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! 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:
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:
And here are the model files I used. Crate01_Combined.mdl is the one that is ready-to-use with this script:
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/
-
2
3 Comments
Recommended Comments