How To Build A Catapult

From Goodblox Wiki
Revision as of 17:09, 22 June 2020 by Pizzaboxer (talk | contribs) (Pizzaboxer moved page Catapult to How To Build A Catapult)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigationJump to search


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.

Introduction

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.

Tips:

  • 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)

WarRock Script:

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


Going Advanced

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)