We use cookies on this site to enhance your user experience

Top Down Action: Pie Launcher

Top Down Action: Pie Launcher

15 min

It’s time to give our player a way to defeat the robots in our game. Equipment and weapons can be implemented in Roblox with the Tool instance. Tools can be equipped, unequipped, and listen to input events. In StarterPack you will find a Tool called PieLauncher. Since the Tool is in StarterPack, it will automatically be given to every player when their character spawns.

Right now the tool is just cosmetic and not functional; we will need several scripts to get it working. We will need a LocalScript to play animations and listen for player input, a Script to create new pies for the launcher to throw, and a ModuleScript to store various settings for the launcher that the other two scripts can refer to.

TDS_PieLauncher.png

Configurations

Let’s start with the Configurations ModuleScript. A ModuleScript is a script-like object containing code that can be shared between several regular scripts. One use of ModuleScripts is to declare variables that other scripts can all read from so they don’t have to be declared in the individual scripts.

Right click on PieLauncher and insert a ModuleScript called PieLauncherConfigurations. Insert the following code:

local PieLauncherConfigurations = {}

PieLauncherConfigurations.LaunchSpeed = 80
PieLauncherConfigurations.LaunchTime = .15
PieLauncherConfigurations.Cooldown = .3
PieLauncherConfigurations.Damage = 30

return PieLauncherConfigurations

As you can see, this ModuleScript simply creates a table, stores some variables, and then returns the table. When our other scripts access this module script, they will have access to this table so they can read the values stored in it.

LocalScript and Animations

The next script we will setup is the LocalScript. This script will be responsible for listening to the player’s input, playing an animation for the launch, launching the pie, and checking when that pie hits something. Technically, some of these tasks (with the exception of input) could be handled by a Script on the server. The problem with that is there will always be a slight delay between when the server performs actions and when a client sees them due to network latency. When working with a Tool, you want feedback and actions to be instantaneous. In this case, we can have the client handle these actions and then tell the server about them which will provide a seamless experience for the player.

Before we get ahead of ourselves though, let’s get the basic framework for the LocalScript. Insert a LocalScript called PieLauncherLocalScript into PieLauncher and insert the following code:

local tool = script.Parent

local function onActivate()
	
end

local function onEquip()
	
end

tool.Equipped:connect(onEquip)
tool.Activated:connect(onActivate)

This code simply creates a variable for the tool and binds functions to the Tool/Equipped|Equipped and Tool/Activated|Activated events. These fire when a Tool is first worn and when the user clicks the mouse respectively.

Next let’s make the launcher animate when the tool is activated. PieLauncher already has a launch animation inside of it called Animation. To play an animation in Roblox, it must first be loaded into a Humanoid using the Humanoid/LoadAnimation|LoadAnimation function which will return an AnimationTrack. This track can then be used to start and stop the animation.

local tool = script.Parent
local player = game.Players.LocalPlayer
local animationTrack = nil

local function onActivate()
	animationTrack:Play()
end

local function onEquip()
	local humanoid = player.Character.Humanoid
	animationTrack = humanoid:LoadAnimation(tool.Animation)
end

tool.Equipped:connect(onEquip)
tool.Activated:connect(onActivate)

The player and animationTrack are declared at the top as they will be used in various places throughout the script. In onEquip the animation is loaded into the player character’s Humanoid, and in onActivate the track is played.

If you test this now, you’ll notice that there is no limit to how often a player can activate the tool. We need to implement a cooldown to prevent the player from launching too frequently.

local tool = script.Parent
local player = game.Players.LocalPlayer
local animationTrack = nil
local canLaunch = true

local configurations = require(tool.PieLauncherConfigurations)

local function onActivate()
	if canLaunch then
		canLaunch = false
		
		animationTrack:Play()
		
		wait(configurations.Cooldown)
		
		canLaunch = true
	end
end

local function onEquip()
	local humanoid = player.Character.Humanoid
	animationTrack = humanoid:LoadAnimation(tool.Animation)
end

tool.Equipped:connect(onEquip)
tool.Activated:connect(onActivate)

We first have to make a variable for the configurations because we want to grab the cooldown time that we set there. The require keyword is needed when loading a ModuleScript. We then setup a variable canLaunch which limits when the code in onActivate can run.

Server Script and Pies

Now that our launcher animates, we need to create some pies for it to launch. With a few exceptions, changes made on the client via a LocalScript will not copy to the server. Animations that play on the player character or tools the player is holding will copy just fine, but creating objects will not. So let’s create a Script to create the pies. Insert a Script inside of PieLauncher and name it PieLauncherServerScript. Enter the following code:

-- Server Script

local tool = script.Parent

local function onEquip()
	
end

local function onUnequip()

end

tool.Equipped:connect(onEquip)
tool.Unequipped:connect(onUnequip)

Again we just setup the basic framework for the tool. Just like a LocalScript, a Server Script can listen to the Tool/Equipped|Equipped event of a tool. We are also listening to the Tool/Unequipped|Unequipped event of the tool. After we have this framework we can add a function to create a pie for the player when the tool is equipped:

-- Server Script

local tool = script.Parent
local activePie = nil

local function createPie(player)
	activePie = game.ServerStorage.Models.Pie:Clone()
	activePie.Name = "Pie" .. player.UserId
	activePie.Parent = game.Workspace.Pies
	activePie:SetNetworkOwner(player)
end

local function onEquip()
	local character = tool.Parent
	local player = game.Players:GetPlayerFromCharacter(character)
	createPie(player)
end

local function onUnequip()
	if activePie then
		activePie:Destroy()
	end
end

tool.Equipped:connect(onEquip)
tool.Unequipped:connect(onUnequip)

First we make a variable to keep track of which pie will be launched next called activePie. Next we declare a function to create a pie for a given player. There is a pie model in ServerStorage that we will copy to create a new pie, which will then be stored in the Pies folder in Workspace. We will later need to directly reference this pie in the LocalScript, so we give it a name based off the player’s Player/UserId|UserId. We then set the network ownership of the pie to the player so that the LocalScript can later manipulate the position and velocity of the pie. If we did not set the network ownership, the server would own the pie and the LocalScript could not change physical properties of the pie.

We then use the onEquip function to call create. Notice that we can’t get the player from game.Players.LocalPlayer like we can in our LocalScript. The server doesn’t have a local player since it is running on a Roblox machine. To get the player who owns the tool, we instead have to check the tool’s parent.

Lastly, we need to clean up the activePie when the player unequips the tool. In the onUnequip function, we simply check to see if activePie exists. If it does, then we can safely destroy it.

Launching the Pie

Now that we have a pie to launch, let’s head back to our LocalScript.

-- Local Script

local tool = script.Parent
local player = game.Players.LocalPlayer
local animationTrack = nil
local canLaunch = true
local pieFolder = game.Workspace.Pies
local activePie = pieFolder:WaitForChild('Pie' .. player.UserId)

local configurations = require(tool.PieLauncherConfigurations)

local function onActivate()
	if canLaunch then
		canLaunch = false
		
		animationTrack:Play()
		
		local direction = tool.Handle.CFrame.lookVector
		activePie.CFrame = tool.Pie.CFrame	
		activePie.Velocity = direction * configurations.LaunchSpeed
		
		wait(configurations.Cooldown)
		
		canLaunch = true
	end
end

local function onEquip()
	local humanoid = player.Character.Humanoid
	animationTrack = humanoid:LoadAnimation(tool.Animation)
end

tool.Equipped:connect(onEquip)
tool.Activated:connect(onActivate)

We first setup a variable for the active pie. Using Instance/WaitForChild|WaitForChild allows the script to pause while the server creates the pie via the Script we added in the previous step. In our onActivate function, we can now launch the pie during the animation. First, we get the direction the pie will travel in based on the Handle of the Tool. The Handle is the part of the Tool that is directly attached to the player’s arm. We can use the lookVector of that part’s DataType/CFrame to see which direction the character is facing. We then position the activePie at the same position and orientation of the pie model that is contained in the Tool. Lastly, we set the velocity of the activePie by multiplying the direction value we just got by the LaunchSpeed defined in the configurations ModuleScript.

Reloading the Launcher

If you test the launcher you might notice that it reuses the same pie on every launch. We should instead create a new pie after every launch so the player can have several pies in the air at once. We first need to modify the LocalScript to fire an event every launch which our server script will later listen to to create new pies:

-- Local Script

local tool = script.Parent
local player = game.Players.LocalPlayer
local animationTrack = nil
local canLaunch = true
local pieFolder = game.Workspace.Pies
local activePie = pieFolder:WaitForChild('Pie' .. player.UserId)

local configurations = require(tool.PieLauncherConfigurations)

local function onActivate()
	if canLaunch then
		canLaunch = false
		
		tool.LaunchPie:FireServer()		
		
		animationTrack:Play()
		
		local direction = tool.Handle.CFrame.lookVector
		activePie.CFrame = tool.Pie.CFrame	
		activePie.Velocity = direction * configurations.LaunchSpeed
		activePie.Name = 'oldPie'
		activePie = pieFolder:WaitForChild('Pie' .. player.UserId)	
		
		wait(configurations.Cooldown)
		
		canLaunch = true
	end
end

local function onEquip()
	local humanoid = player.Character.Humanoid
	animationTrack = humanoid:LoadAnimation(tool.Animation)
end

tool.Equipped:connect(onEquip)
tool.Activated:connect(onActivate)

Now when the LocalScript launches a pie it fires the LaunchPie RemoteEvent which is inside the Tool. Also, after the pie is launched, the activePie is renamed and the script waits for a new active pie to be added to the folder.

Now we can modify the server Script to create a new pie with the LaunchPie event fires.

-- Server Script
 
local tool = script.Parent
local activePie = nil
local configurations = require(tool.PieLauncherConfigurations)
 
local function createPie(player)
	activePie = game.ServerStorage.Models.Pie:Clone()
	activePie.Parent = game.Workspace.Pies
	activePie.Name = "Pie" .. player.UserId
	activePie:SetNetworkOwner(player)
end
 
local function onLaunch(player)
	local oldPie = activePie
	oldPie.Name = "oldPie"
	createPie(player)
	wait(configurations.PieLifetime)
	oldPie:Destroy()
end

local function onUnequip()
	if activePie then
		activePie:Destroy()
	end
end
 
local function onEquip()
	local character = tool.Parent
	local player = game.Players:GetPlayerFromCharacter(character)
	createPie(player)
end
 
tool.Equipped:connect(onEquip)
tool.Unequipped:connect(onUnequip)
tool.LaunchPie.OnServerEvent:connect(onLaunch)

When the event fires, the activePie is renamed and stored in the variable oldPie. A new pie is then created. The function then waits the amount of time defined by PieLifetime in configurations, and the old pie is cleaned up.

Pie collisions

Now that our launcher is flinging pies, let’s add code to check when the pies hit a robot so we can damage them. Since we want the experience to be seamless for the player who is wielding the launcher, we will have the LocalScript check for collisions with the launched pie.

-- Local Script

local tool = script.Parent
local player = game.Players.LocalPlayer
local animationTrack = nil
local canLaunch = true
local pieFolder = game.Workspace.Pies
local activePie = pieFolder:WaitForChild('Pie' .. player.UserId)

local configurations = require(tool.PieLauncherConfigurations)

local function findCharacter(object)
	if object == game.Workspace then return nil end
	if object:FindFirstChild('Humanoid') then
		return object
	end
	return findCharacter(object.Parent)
end

local function bindPieHit(pie)
	local hasHit = false
	pie.Touched:connect(function(otherPart)
		if not hasHit then
			local character = findCharacter(otherPart)
			if character and not game.Players:GetPlayerFromCharacter(character) then
				hasHit = true
				tool.PieDamage:FireServer(character, pie)
			end
		end
	end)
end

local function onActivate()
	if canLaunch then
		canLaunch = false
		
		tool.LaunchPie:FireServer()		
		
		animationTrack:Play()
		
  		bindPieHit(activePie)
		local direction = tool.Handle.CFrame.lookVector
		activePie.CFrame = tool.Pie.CFrame	
		activePie.Velocity = direction * configurations.LaunchSpeed
		activePie.Name = 'oldPie'
		activePie = pieFolder:WaitForChild('Pie' .. player.UserId)	
		
		wait(configurations.Cooldown)
		
		canLaunch = true
	end
end

local function onEquip()
	local humanoid = player.Character.Humanoid
	animationTrack = humanoid:LoadAnimation(tool.Animation)
end

tool.Equipped:connect(onEquip)
tool.Activated:connect(onActivate)

We first define the function findCharacter. This function checks if the passed in object contains a Humanoid. If it is, then it is either an NPC like the robots or a player character. If the object doesn’t contain a Humanoid, it recursively checks the object’s parent to see if that has a Humanoid.

The next function bindPieHit takes a pie as an argument. It binds a function to the BasePart/Touched|Touched event of the pie. This function checks if the hit object is a character and makes sure the character is not a player (and therefore a robot). If it is hitting a robot, it fires the PieDamage RemoteEvent which our server will listen to and damage the hit robot.

Speaking of the server listening to the RemoteEvent, let’s add a function to damage a robot when this event fires:

-- Server Script
 
local tool = script.Parent
local activePie = nil
local configurations = require(tool.PieLauncherConfigurations)
 
local function createPie(player)
	activePie = game.ServerStorage.Models.Pie:Clone()
	activePie.Name = "Pie" .. player.UserId
	activePie.Parent = game.Workspace.Pies
	activePie:SetNetworkOwner(player)
end
 
local function onLaunch(player)
	local oldPie = activePie
	oldPie.Name = "oldPie"
	createPie(player)	
	wait(configurations.PieLifetime)
	oldPie:Destroy()
end
 
local function onPieHit(player, robot, pie)
	robot.Humanoid:TakeDamage(configurations.Damage)
	pie:Destroy()
end
 
local function onEquip()
	local character = tool.Parent
	local player = game.Players:GetPlayerFromCharacter(character)
	createPie(player)
end

local function onUnequip()
	if activePie then
		activePie:Destroy()
	end
end 

tool.Equipped:connect(onEquip)
tool.Unequipped:connect(onUnequip)
tool.LaunchPie.OnServerEvent:connect(onLaunch)
tool.PieDamage.OnServerEvent:connect(onPieHit)

Now when a pie hits a robot the client will tell the server about the collision. The server will then damage the robot with the Humanoid/TakeDamage|TakeDamage function, and then destroy the pie that hit the robot.

Animation syncing

We have almost completed the launcher, there is just a bit of polish left. Firstly, the pie launches as soon as the animation starts, which doesn’t look very realistic. Also, the tool has a built in pie that should disappear when the pie is launched and reappear when the pie is reloaded. Let’s start by modifying the LocalScript:

-- Local Script

local tool = script.Parent
local player = game.Players.LocalPlayer
local animationTrack = nil
local canLaunch = true
local pieFolder = game.Workspace.Pies
local activePie = pieFolder:WaitForChild('Pie' .. player.UserId)

local configurations = require(tool.PieLauncherConfigurations)

local function findCharacter(object)
	if object == game.Workspace then return nil end
	if object:FindFirstChild('Humanoid') then
		return object
	end
	return findCharacter(object.Parent)
end

local function bindPieHit(pie)
	local hasHit = false
	pie.Touched:connect(function(otherPart)
		if not hasHit then
			local character = findCharacter(otherPart)
			if character and not game.Players:GetPlayerFromCharacter(character) then
				hasHit = true
				tool.PieDamage:FireServer(character, pie)
			end
		end
	end)
end

local function onActivate()
	if canLaunch then
		canLaunch = false
		
		tool.LaunchPie:FireServer()		
		animationTrack:Play()
		
		wait(configurations.LaunchTime)
		
		tool.Pie.LocalTransparencyModifier = 1
		bindPieHit(activePie)
		local direction = tool.Handle.CFrame.lookVector
		activePie.CFrame = tool.Pie.CFrame	
		activePie.Velocity = direction * configurations.LaunchSpeed
		activePie.Name = 'oldPie'
		activePie = pieFolder:WaitForChild('Pie' .. player.UserId)	
		
		wait(configurations.Cooldown)
		
		tool.Pie.LocalTransparencyModifier = 0
		canLaunch = true
	end
end

local function onEquip()
	local humanoid = player.Character.Humanoid
	animationTrack = humanoid:LoadAnimation(tool.Animation)
end

tool.Equipped:connect(onEquip)
tool.Activated:connect(onActivate)

Now instead of launching the pie as soon as the Tool is activated, the script waits a bit of time. It also sets the BasePart/LocalTransparencyModifier|LocalTransparencyModifier of the tool’s pie to 1 when the Tool launches, and then back to 0 when the Tool is ready to launch again.

We also need to edit the Server Script, as setting the BasePart/LocalTransparencyModifier|LocalTransparencyModifier will only make the pie transparent for the launching player:

-- Server Script
 
local tool = script.Parent
local activePie = nil
local configurations = require(tool.PieLauncherConfigurations)
 
local function createPie(player)
	activePie = game.ServerStorage.Models.Pie:Clone()
	activePie.Name = "Pie" .. player.UserId
	activePie.Parent = game.Workspace.Pies
	activePie:SetNetworkOwner(player)
end
 
local function onLaunch(player)
	wait(configurations.LaunchTime)
	tool.Pie.Transparency = 1
 
	local oldPie = activePie
	oldPie.Name = "oldPie"
	createPie(player)	
 
	wait(configurations.Cooldown)
 
	tool.Pie.Transparency = 0	
 
	wait(configurations.PieLifetime)
	oldPie:Destroy()
end
 
local function onPieHit(player, robot, pie)
	robot.Humanoid:TakeDamage(configurations.Damage)
	pie:Destroy()
end

local function onUnequip()
	if activePie then
		activePie:Destroy()
	end
end
 
local function onEquip()
	local character = tool.Parent
	local player = game.Players:GetPlayerFromCharacter(character)
	createPie(player)
end
 
tool.Equipped:connect(onEquip)
tool.Unequipped:connect(onUnequip)
tool.LaunchPie.OnServerEvent:connect(onLaunch)
tool.PieDamage.OnServerEvent:connect(onPieHit)

Now our player has a complete tool to dispatch the robots!

TDS_PieLauncher_Animation.png