How To Build A Catapult
|This is an Advanced, War Device related tutorial.|
Who this Tutorial is for
This tutorial is for people who are interested in the inner workings of my Catapult model. If you just want to put a catapult in your level, you do not need to read this tutorial. Just do a free model search for "Catapult".
I will be discussing OnTouch triggers, BodyForce objects, and the rather arcane but very powerful RunService. If you understand all this stuff, building moving mechanisms in GoodBlox becomes much easier. Anyone can read this tutorial, but I am going to assume you are a coder and know your way around GoodBlox Studio.
Let's spec out the catapult that we want to build. A catapult is basically a big lever that chucks rocks, but there are many ways to build one in GoodBlox.
What we are trying to accomplish
Our catapult will:
- Fling rocks at least 512 studs (the width of the largest possible baseplate)
- Be hand-loaded by players with the Grab tool.
- Be fired by players pressing a lever on the catapult.
- Shoot rocks that explode when they hit something (this one is tricky).
The Mechanical Mechanism
Go build a catapult - just the mechanical mechanism part of it. Come back when you're done.
- The lever arm of the catapult should be longer on the side the ammo goes in (see my screenshots). This can be a precise science (there are equations that tell you what your mechanical advantage of the arm is, predict how much impulse you can impart to a projectile, and thus how far you can fling it), but I just eye-balled something that looked good for mine.
- The lever arm needs to be constrained so that it chucks the projectile Up into the air, preferably at close to a 45 degree angle for maximum range. I did this by placing a block under the lever arm so that it can only go down so far when shooting.
- Use two hinge joints - one on each side of the arm. Using only one hinge is probably not a good idea.
- Make the ammo carrier first, then size the rest of the catapult to whatever sized carrier you made. Make sure that the bottom of the carrier is only one piece, this will be important to our scripting effort later.
- Anchor your support beams to the ground and your hinge joints to the support beams. The force of the catapult is quite large, and your entire catapult can "hop" if you don't.
Making it Fire
Once you have the catapult built, now we need to script its function. Actually making the thing fire is pretty easy. All we want to do is apply a massive force down on the short end of the arm to send the rock flying. Enter the BodyForce object.
The BodyForce object can be used to apply a force vector to the part that it is placed under (in the GoodBlox Studio treeview).
All we need to do is:
1. Create a BodyForce object under a part.
2. Set its force vector to (0,0,0) - i.e. it is off by default.
3. Place the part on the short end of the arm.
4. Write a script that detects when the firing lever is touched by a player.
5. When this happens, apply a huge downward force.
6. Wait a couple of seconds.
7. Reset force vector to (0,0,0)
If you look carefully at the first catapult screenshot at the top of this article, you will see a little red 2x2 that I placed at the end of the catapult arm. This is where I put the BodyForce object.
Here is some sample source code. You will probably need to edit the variables to fit your own part names:
local powerblock = script.Parent.Parent.PowerBlock local bodyforce = powerblock.BodyForce local debounce = false function onTouched(hit) local humanoid = hit.Parent:findFirstChild("Humanoid") if humanoid~=nil and debounce == false then debounce = true bodyforce.force = Vector3.new(0,1000000,0) wait(3) bodyforce.force = Vector3.new(0,0,0) wait(3) debounce = false end end script.Parent.Touched:connect(onTouched)
Q & A
You might have a couple of questions about this script:
What does debounce do? - In my experiments I found that that Touched event happens multiple times whenever a player touches a brick. I use the debounce flag to make sure I only handle one Touched event per firing cycle.
How did you pick the force vector? - Trial and error. In my catapult, I have tested forces up to 3 million units (GBX Newtons?) without a problem. Obviously, the more force you apply the farther the projectile is flung. Making a long range catapult is a less interesting problem than making the most efficient catapult possible, since you can always make something shoot further by upping the force you apply.
Why is the force vector upside down? - Honestly, I have no idea. I think I have probably found a bug in the BodyForce object. In my experiments, this is what worked.
Tip: The more efficient your catapult design, the better. An efficient design requires less force to chuck the rock the same distance. The GoodBlox physics engine doesn't like dealing with huge forces - it can make your simulation unstable (you will see jittering). This becomes especially important if you plan on mounting your catapult on a hinge, swivel, or other un-anchored base.
Making Rocks Blow Up When they Hit Stuff
The last part of the puzzle, and the hardest. If you have gotten to this step, you have a cool catapult that can launch rocks into orbit. But they don't do much damage when they hit something, do they? That's just not satisfying.
This problem is actually two problems:
1. How do you make projectiles explode when they hit something?
2. How do you make projectiles not explode when you are loading them into the Catapult?
Number 1 is easy - just look at the GoodBlox weapon scripts for inspiration. They basically all work the same way - there is a script that is copied and attached to every projectile a weapon fires that handles doing damage OnTouch.
Number 2 is hard. Here is one solution:
1. Name all of your ammo rocks a special name - I choose "WarRock" (As in Homer's War Rocks, kudos if you get the reference)
2. Make the bottom of your ammo carrier listen to the OnTouch event. When it detects that a WarRock has touched it, the WarRock goes "hot". This means a projectile script is attached to the rock that will make it blow up when hit.
3. Make the projectile script periodically (like 30 times a second), check the WarRock's position and compare it to the WarRock's previous position. Go "critical" when the WarRock's velocity exceeds a sustained velocity of 16 studs per second over .5 seconds. Once a WarRock has gone critical it will explode the next time it touches anything.
The beauty of this scheme: The WarRock "knows" when it has been launched because it is checking its own velocity. There is no need for the trigger to communite with the WarRock's projectile script. When I was originally trying to figure this out, some people on the forums suggested using a wait() timer instead to make the WarRock go critical X seconds after the trigger was flipped. I have not tried this solution, but I would bet that there would be issue with lag, making the rock explode too late. It also requires Trigger To Rock communication - which is fugly.
I'm going to give my final code, just as an example of what I did. I doubt you can plug it into your catapult without mods.
Catapult Trigger Script:
local powerblock = script.Parent.Parent.PowerBlock local bodyforce = powerblock.BodyForce local hotplate = script.Parent.Parent.HotPlate local debounce = false function onTouched(hit) local humanoid = hit.Parent:findFirstChild("Humanoid") if humanoid~=nil and debounce == false then debounce = true bodyforce.force = Vector3.new(0,1000000,0) --[[you may have to adjust this number, depending on the design of your catapult --]] wait(3) bodyforce.force = Vector3.new(0,0,0) wait(3) debounce = false end end function onHotPlateTouched(hit) if (hit.Name ~= "WarRock") then return end if (hit:findFirstChild("WarRockScript") ~= nil) then return end -- debounce hit.BrickColor = BrickColor.new(105) local code = script.Parent.WarRockScript:clone() code.Disabled = false code.Parent = hit end script.Parent.Touched:connect(onTouched) hotplate.Touched:connect(onHotPlateTouched)
I got a litle fancy in this script, so not all the code is necessary for a bare-bones implementation. The trick here is that the velocity-tracking code runs 30 times per second, on the physics engine heartbeat, using the RunService to do a SteppedWait. The GoodBlox Rocket Launcher does a similar thing in order to override gravity. I can't take full credit for figuring this out, Telamon had to explain it to me.
-- attached to WarRock print("WarRock script running") r = game:service("RunService") local warRock = script.Parent local flightTime = 0 -- rock is only hot after we detect it has been flying for .5 seconds local hotRock = false local lastPos = nil function onTouched(hit) -- We hit something, time to blow up if (hotRock == true) then blowUp() end end function getExplosionRadius(mass) if (mass > 50) then return 16 end if (mass > 40) then return 13 end if (mass > 5) then return 10 end return 6 end function getExplosionPressure(mass) if (mass > 50) then return 2000000 end if (mass > 40) then return 1000000 end if (mass > 5) then return 500000 end return 200000 end function blowUp() local sound = Instance.new("Sound") sound.SoundId = "rbxasset://sounds\\Rocket shot.wav" sound.Parent = warRock sound.Volume = 1 sound:play() explosion = Instance.new("Explosion") local mass = warRock:getMass() -- in default war rocks, mass varies from 1 to 57 explosion.BlastRadius = getExplosionRadius(mass) explosion.BlastPressure = getExplosionPressure(mass) explosion.Position = warRock.Position explosion.Parent = game.Workspace warRock.Parent = nil end function getDistance(a, b) local dx = a.x - b.x --[[change in x distance from a to b--]] local dy = a.y - b.y --[[change in y distance from a to b--]] local dz = a.z - b.z --[[change in z distance from a to b--]] return math.sqrt(dx * dx + dy * dy + dz * dz) --[[Sphere: x^2 + y^2 + z^2 = radius^2, therefore we need the sqrt of x^2 + y^2 + z^2 to get the radius. --]] end function checkFlying(dt) -- "flying" is movement of over 16 studs per second -- when this condition holds, increment the flightTime -- when flightTime > .5, WarRock goes hot! if (lastPos == nil) then lastPos = warRock.Position return end local velocity = getDistance(lastPos, warRock.Position) / dt --[[rate of change of distance over time, derivative --]] if (velocity > 16) then flightTime = flightTime + dt else flightTime = 0 end if (flightTime > .5) then hotRock = true warRock.BrickColor = BrickColor.new(21) end lastPos = warRock.Position end warRock.Touched:connect(onTouched) local lastTime = r.Stepped:wait() local curTime while true do curTime = r.Stepped:wait() -- should be 1/30th a second, but take no chances if (hotRock == false) then checkFlying(curTime - lastTime) lastTime = curTime else break end end
Make sure that all the scripts in your WarRocks have been named to "WarRockScript".
Now, this is an onClick script. That means you will need to insert > object > clickdetector in your catapult.
Now enter the following script into your catapult.
local powerblock = script.Parent.Parent.PowerBlock local bodyforce = powerblock.BodyForce local hotplate = script.Parent.Parent.HotPlate function onClicked() bodyforce.force = Vector3.new(0,1000000,0) wait(3) bodyforce.force = Vector3.new(0,0,0) wait(3) end function onHotPlateTouched(hit) if (hit.Name ~= "WarRock") then return end if (hit:findFirstChild("WarRockScript") ~= nil) then return end -- debounce hit.BrickColor = BrickColor.new(105) local code = script.Parent.WarRockScript:clone() code.Disabled = false code.Parent = hit end script.Parent.ClickDetector.MouseClick:connect(onClicked) hotplate.Touched:connect(onHotPlateTouched)