6.6.1 Cannon Script

With that out of the way, it is time to design the scripts.

The cannon module needs a cannon barrel to fire from and a cannonball.

I will just use a Part named ‘Cannon’ to represent the barrel. This can easily be replaced with a modeled cannon mesh during production

I placed mine on an elevated platform. It is possible to place the cannon at surface level or even below surface level, provided you adjust the firing angle to compensate for the lower elevation.

Anchor the part then disable CanCollide and CanTouch so the cannonball cannot make contact and explode when it fires.

To fire the cannon, the module will implement a single function that takes a target position or target.

In the function, a cannonball is cloned and place at the cannon barrel position and then fired at the velocity calculated from our equation.


First, let’s define the constants and initialize the script.

Lua
local DebrisService = game:GetService("Debris")

local GRAVITY = game.Workspace.Gravity
local LAUNCH_ANGLE = math.pi / 4

local CANNON = game.Workspace.Cannon
local CANNONBALL = Instance.new("Part")
local FIRE_SOUND = Instance.new("Sound")

local COLLIDE_WAV = 'rbxassetid://12221984'
local FIRE_WAV = 'rbxassetid://4812797067'

CANNONBALL.Shape = Enum.PartType.Ball
CANNONBALL.Size = Vector3.one * 4
CANNONBALL.Material = Enum.Material.Neon
CANNONBALL.BrickColor = BrickColor.new("Flame yellowish orange")

FIRE_SOUND.SoundId = FIRE_WAV
FIRE_SOUND.Volume = 5
FIRE_SOUND.Parent = CANNON

do
	local explodSound = Instance.new("Sound")
	explodSound.SoundId = COLLIDE_WAV
	explodSound.Volume = 5
	explodSound.Parent = CANNONBALL
	
	local fire = Instance.new("Fire")
	fire.Parent = CANNONBALL
end

A sound and fire are parented to the cannonball, however the cannonball does not need to be parented to anything. As long as we keep a reference to it, the garbage collector won’t clean it up.

When the cannon fires, it will clone the cannonball and then parent it to the workspace. Then debris service will clean up the fired cannonball.


Next we’ll define the function for launch velocity. Since several of these are constant, you can define them ahead of time if you so chose.

Lua
local function getLaunchVelocity(range, height)
	local cosAngle2 = 2 * math.cos(LAUNCH_ANGLE) * math.cos(LAUNCH_ANGLE)
	local tanAngle = math.tan(LAUNCH_ANGLE)
	
	local numer = GRAVITY * (range * range)
	local denom = (height + (range * tanAngle)) * cosAngle2

	return math.sqrt(numer / denom)
end

Finally, we’ll define the attack function. An external script will call this function to attack a player.

Lua
function CannonModule:attackPlayer(character)
	if not character or not character.PrimaryPart then return false end
	
	local pos0 = CANNON.Position
	local pos1 = character.PrimaryPart.Position
	
	local direction = pos1 - pos0
	local rotationY = math.atan2(-direction.X, -direction.Z)
	local height = -direction.Y
	local distance = (direction * Vector3.new(1, 0, 1)).Magnitude
	CANNON.CFrame = CFrame.fromEulerAnglesYXZ(LAUNCH_ANGLE, rotationY, 0) + CANNON.Position

	local launchV = getLaunchVelocity(distance, height)
	
	local newCannonball = CANNONBALL:Clone()
	local mass = newCannonball.Mass
	local f = mass * launchV
	local launchCf = CANNON.CFrame
	
	newCannonball.CFrame = launchCf
	newCannonball.Parent = game.Workspace
	newCannonball:SetNetworkOwner(nil)
	
	newCannonball:ApplyImpulse(launchCf.LookVector * f)
	
	FIRE_SOUND:Play()
	
	newCannonball.Touched:Once(function()
		newCannonball.Anchored = true
		newCannonball.Transparency = 1
		newCannonball.CanCollide = false
		newCannonball.CanTouch = false

		local explosion = Instance.new("Explosion")
		explosion.Position = newCannonball.Position
		explosion.Parent = newCannonball

		newCannonball.Fire:Destroy()
		newCannonball.Sound:Play()
		DebrisService:AddItem(newCannonball, 3)

	end)
end

The first thing is to localize the target to the cannon by subtracting their positions. This can then be used to calculate the rotation (not launch angle) of the cannon.

If you are looking directly from above, then X and Z represent a 2D vector where the angle of rotation is about the y-axis.

The launch velocity is calculated assuming a horizontal distance. However the direction magnitude to the target is in 3D which takes into account elevation differences and in turn, results in a magnitude that is no longer than one that is purely horizontal.

The ‘distance’ variable removes the elevation difference by multiplying the direction with the vector (1, 0, 1) to remove the height component.

The CFrame is created using the ‘fromEulerAnglesYXZ’ constructor . This rotates the cannon about the y-axis and then applies the launch angle to orient the cannon barrel.

Note that this is the ‘YXZ’ version and not the ‘XYZ’ version. This is because the cannon needs to be rotated first and then the launch angle is set.


Thus far, whenever we wanted to move a part in script, we did so by manually setting the part’s position.

This was done mostly out of necessity because we have not gotten to ‘constraints’ yet. However, this manner of movement has several limitations.

One of those limitations is the nullification of physics based simulation and interaction, which includes the touch event that we kind of need in order to explode a cannonball in the player’s face.

We will return learn about constraints in the next chapter, but for the purposes of this section, the simplest way to move a part under physics simulation is to manually set the part’s velocity or give it an impulse.

Setting the velocity works but according to the API,

Setting the velocity directly may lead to unrealistic motion

So instead, we’ll use the ApplyImpulse method. All BaseParts have this method for setting a part in motion.

The amount of force required to attain certain velocity can be calculated by multiplying the part (or collection of parts) mass by the desired speed.


But before sending it off, let’s pause here and take a moment to talk about physics and networks.

In games, every part that is not anchored and therefore subject to physical simulation must have a host to perform physics simulations. In general, that host is the server.

However, the problem with having the server handle physics is that it takes time for changes to replicate to clients.

In certain circumstances, this delay can be considerable and degrades the user’s experience.

For instance, if characters were handled on the server and then replicated the players, this latency would make characters feel unacceptably laggy and unresponsive.

Game engines address this by strategically delegating those duties to players.

In Roblox, this is called ‘network ownership.’

By default, each player has network ownership of their own character and physics for their character is handled on their local machine.

On the other hand, unanchored parts are owned by the server.

However, depending on a character’s proximity to a part, this can trigger the server to assign network ownership to the player.

This would make sense. If a player is within interaction distance of a part, they are most likely to come contact with it so they should be the one to handle the physics for the best fidelity.

This isn’t without its pitfalls, however.

If a part is moving during the switchover, that transition from the server to the client and vice versa can cause jittery motion.

In addition, because a client owned part also has its touch event handled on the client, this desyncing of positions can cause poorly timed events.

The ‘SetNetworkOwner’ method called with a nil argument forces the server to maintain ownership of the part.

In this script, this is called on the unanchored fireball to make the server keep control of it.


Finally, the cannonball is launched.

The CFrame look vector is a unit vector. Multiplying it by the force will send that cannonball flying in that direction at the required speed.

Then all that’s left is to connect the touch event.

If your cannonball is exploding at the cannon, you may need to disable the ‘CanTouch’ property on the cannon.