Making a combat game with ranged weapons? FastCast may be the module for you!

Update: When it goes out of range the ray terminates but not the projectile part, how do I fix this?

1 Like

Couldn’t you connect to CastTerminating, and destroy it there?

2 Likes

Well how would I get the projectile instance from CastTerminating?

Yep this is a common question, because the api doesn’t explicitly provide the cosmetic bullet object in cast terminating as one of the parameters so it’s not as obvious.

However it’s possible through the activecast object like so:

Cast terminating bullet clean up with PartCache
		local function cleanUpBullet(activeCast)
			local bullet = activeCast.RayInfo.CosmeticBulletObject -- API wise it's pretty weird compared to OnRayHit event.
			--Debris:AddItem(bullet,1)--normal instance.new clone
			local trail = bullet:FindFirstChildWhichIsA("Trail")
			if trail then
				trail.Enabled = false
			end
--If you use part cache to clean up
			projectileCache:ReturnPart(bullet)
		end

		castComponent.CastTerminating:Connect(cleanUpBullet)
1 Like

Thank you for this! I can finally get past the range mechanic and continue progressing on my game!

I’ve been working on a rebuild of FC on and off recently. I will address this in the new API once it’s all released. No ETA, since my job takes precedence.

2 Likes

Hi Eti, I wanted to ask, will you add support for bigger hitboxes anytime soon? I’ve been using your module for a while, but for projects that require ranged abilities with bigger hitboxes, I can’t use it, and it’s a huge bummer since it’s the only thing i can think of using for ranged attacks or weapons.

What’s the recommended way to add a “lifetime” for the bullet (i.e. to purposefully proc the CastTerminating event after x seconds)?

I’ve decided to use FastCast in a project but the issue is it seems really expensive. I have multiple weapons in my game so in the server I’m looping through the data for the weapons and creating casters and cast behaviors for each one as well as establishing their respective events. I have a single remote event for when any weapon fires and find the caster setup for that weapon and use it to fire the weapon. This feels expensive though. Is there not some better way to do this?

Do the caster setup entirely client sided. Attach caster objects to instances via a dictionary to keep track and avoid having to create a new caster object.

On the server use projectile logic based sanity checks

1 Like

Hi, Excellent module! Yielding good results for my project right now.

On another note, I am curious, why do you use your own website for the API instead of GitHub, I see the github page you had set up is obsolete now. Just curious.

Thank you for the contribution to the community!

Is there no support for multipart projectiles when using PartCache? My bullet consists of two mesh parts but I can’t seem to use a model because PartCache only accepts a BasePart.

I’ve faced the same issue, I had to change my projectile model to a single mesh. I don’t think there’s any support for Model projectiles yet.

1 Like

Hey! I just want my CosmeticBullet to go “straight” (like a knife should go) but it gets thrown from aside.

Like this:

I tried to change the CosmeticBullet orientation directly from the Properties in the union (my CosmeticBullet is an union) and also via script (tried out with .fromMatrix and some lookAt functions but nothing works). Why can’t I change the orientation and how can I fix it?

ty!

You’d need to modify PartCache to support models. It’s not too hard, considering its just two tables and a cloning function.

I think I help you with that, can you send your length changed event code?

sry idk what that exactly is so imma send u my whole server script fastcast code (i’ve undone the part i changed CosmeticBullet Orientation/CFrame bcs it didn’t work as i said)

local knifeThrow = game.ReplicatedStorage:WaitForChild("KnifeThrow")
local tweenService = game:GetService("TweenService")
local health_module = require(game:GetService("ServerStorage").ModuleLibrary.HealthModule)

local DEBUG = false									-- Whether or not to use debugging features of FastCast, such as cast visualization.
local BULLET_SPEED = 250							-- Studs/second - the speed of the bullet
local BULLET_MAXDIST = 1000							-- The furthest distance the bullet can travel 
local BULLET_GRAVITY = Vector3.new(0, -7.5, 0)		-- The amount of gravity applied to the bullet in world space (so yes, you can have sideways gravity)
local MIN_BULLET_SPREAD_ANGLE = 0					-- THIS VALUE IS VERY SENSITIVE. Try to keep changes to it small. The least accurate the bullet can be. This angle value is in degrees. A value of 0 means straight forward. Generally you want to keep this at 0 so there's at least some chance of a 100% accurate shot.
local MAX_BULLET_SPREAD_ANGLE = 0.25					-- THIS VALUE IS VERY SENSITIVE. Try to keep changes to it small. The most accurate the bullet can be. This angle value is in degrees. A value of 0 means straight forward. This cannot be less than the value above. A value of 90 will allow the gun to shoot sideways at most, and a value of 180 will allow the gun to shoot backwards at most. Exceeding 180 will not add any more angular varience.
local FIRE_DELAY = 0								-- The amount of time that must pass after firing the gun before we can fire again.
local BULLETS_PER_SHOT = 1							-- The amount of bullets to fire every shot. Make this greater than 1 for a shotgun effect.
local PIERCE_DEMO = true

local Tool = script.Parent
local Handle = Tool.Handle
local FastCast = require(script.FastCastRedux)
local CanFire = true
local RNG = Random.new()							-- Set up a randomizer.
local TAU = math.pi * 2	
local MouseEvent = Tool.MouseEvent

FastCast.DebugLogging = DEBUG
FastCast.VisualizeCasts = DEBUG

--caster--

local CosmeticBulletsFolder = workspace:FindFirstChild("CosmeticBulletsFolder") or Instance.new("Folder", workspace)
CosmeticBulletsFolder.Name = "CosmeticBulletsFolder"

local Caster = FastCast.new()

local CosmeticBullet = game.ServerStorage.Knife
CosmeticBullet.CanCollide = false
CosmeticBullet.Anchored = true

local CastParams = RaycastParams.new()
CastParams.IgnoreWater = true
CastParams.FilterType = Enum.RaycastFilterType.Blacklist
CastParams.FilterDescendantsInstances = {}

local CastBehavior = FastCast.newBehavior()
CastBehavior.RaycastParams = CastParams
CastBehavior.MaxDistance = BULLET_MAXDIST
CastBehavior.HighFidelityBehavior = FastCast.HighFidelityBehavior.Default
CastBehavior.CosmeticBulletContainer = CosmeticBulletsFolder
CastBehavior.Acceleration = BULLET_GRAVITY
CastBehavior.AutoIgnoreContainer = false

CastBehavior.CosmeticBulletTemplate = CosmeticBullet
--fire--

function Fire(direction)
	if Tool.Parent:IsA("Backpack") then return end
	local directionalCF = CFrame.new(Vector3.new(), direction)
	local direction = (directionalCF * CFrame.fromOrientation(0, 0, RNG:NextNumber(0, TAU)) * CFrame.fromOrientation(math.rad(RNG:NextNumber(MIN_BULLET_SPREAD_ANGLE, MAX_BULLET_SPREAD_ANGLE)), 0, 0)).LookVector

	local simBullet = Caster:Fire(Handle.Position, direction, BULLET_SPEED, CastBehavior)
end

--events--

function OnRayHit(cast, raycastResult, segmentVelocity, cosmeticBulletObject)
	-- This function will be connected to the Caster's "RayHit" event.
	local hitPart = raycastResult.Instance
	local hitPoint = raycastResult.Position
	local normal = raycastResult.Normal
	if hitPart ~= nil and hitPart.Parent ~= nil then
		local humanoid = hitPart.Parent:FindFirstChildOfClass("Humanoid")
		if humanoid then
			health_module.TakeClampedDamage(humanoid, 28)
		end
	end
end

function OnRayUpdated(cast, segmentOrigin, segmentDirection, length, segmentVelocity, cosmeticBulletObject)
	-- Whenever the caster steps forward by one unit, this function is called.
	-- The bullet argument is the same object passed into the fire function.
	if cosmeticBulletObject == nil then return end
	local bulletLength = cosmeticBulletObject.Size.Z / 2
	local baseCFrame = CFrame.new(segmentOrigin, segmentOrigin + segmentDirection)
	cosmeticBulletObject.CFrame = baseCFrame * CFrame.new(0, 0, -(length - bulletLength))
end

function OnRayTerminated(cast)
	local cosmeticBullet = cast.RayInfo.CosmeticBulletObject
	if cosmeticBullet ~= nil then
		-- This code here is using an if statement on CastBehavior.CosmeticBulletProvider so that the example gun works out of the box.
		-- In your implementation, you should only handle what you're doing (if you use a PartCache, ALWAYS use ReturnPart. If not, ALWAYS use Destroy.
		if CastBehavior.CosmeticBulletProvider ~= nil then
			CastBehavior.CosmeticBulletProvider:ReturnPart(cosmeticBullet)
		else
			cosmeticBullet:Destroy()
		end
	end
end

MouseEvent.OnServerEvent:Connect(function (clientThatFired, mousePoint)
	if not CanFire then
		return
	end
	CanFire = false
	local mouseDirection = (mousePoint - Handle.Position).Unit
	for i = 1, BULLETS_PER_SHOT do
		Fire(mouseDirection)
	end
	if FIRE_DELAY > 0.8 then wait(FIRE_DELAY) end
	CanFire = true
end)

Caster.RayHit:Connect(OnRayHit)
Caster.LengthChanged:Connect(OnRayUpdated)
Caster.CastTerminating:Connect(OnRayTerminated)

Tool.Equipped:Connect(function ()
	CastParams.FilterDescendantsInstances = {Tool.Parent, CosmeticBulletsFolder}
end)
1 Like

First make sure your bullets’ front face is actually the front of the bullet. Then try changing this line
cosmeticBulletObject.CFrame = baseCFrame * CFrame.new(0, 0, -(length - bulletLength)) for this: cosmeticBulletObject.CFrame = CFrame.lookAt(baseCFrame * CFrame.new(0, 0, -(length - bulletLength)), segmentOrigin + segmentDirection)

Thanks! I’ve changed the front face and that was the only problem here!

1 Like

Hey! I’m back here with a different problem. I just finished my previous weapon (throwing knife) successfully but this new one (crossbow) has a little problem: bullet goes way up the mouse position.

VIDEO: Desktop 2021.10.06 - 18.50.31.01

SERVER SCRIPT:

local tweenService = game:GetService("TweenService")
local health_module = require(game:GetService("ServerStorage").ModuleLibrary.HealthModule)

local DEBUG = false									-- Whether or not to use debugging features of FastCast, such as cast visualization.
local BULLET_SPEED = 300							-- Studs/second - the speed of the bullet
local BULLET_MAXDIST = 1000							-- The furthest distance the bullet can travel 
local BULLET_GRAVITY = Vector3.new(0, -5, 0)		-- The amount of gravity applied to the bullet in world space (so yes, you can have sideways gravity)
local MIN_BULLET_SPREAD_ANGLE = 0					-- THIS VALUE IS VERY SENSITIVE. Try to keep changes to it small. The least accurate the bullet can be. This angle value is in degrees. A value of 0 means straight forward. Generally you want to keep this at 0 so there's at least some chance of a 100% accurate shot.
local MAX_BULLET_SPREAD_ANGLE = 0					-- THIS VALUE IS VERY SENSITIVE. Try to keep changes to it small. The most accurate the bullet can be. This angle value is in degrees. A value of 0 means straight forward. This cannot be less than the value above. A value of 90 will allow the gun to shoot sideways at most, and a value of 180 will allow the gun to shoot backwards at most. Exceeding 180 will not add any more angular varience.
local FIRE_DELAY = 0								-- The amount of time that must pass after firing the gun before we can fire again.
local BULLETS_PER_SHOT = 1							-- The amount of bullets to fire every shot. Make this greater than 1 for a shotgun effect.
local PIERCE_DEMO = true
local COOLDOWN = true

local Tool = script.Parent
local Handle = Tool.Handle
local Arrow = Tool.Arrow							--i use the arrow to get the fire position
local FastCast = require(script.FastCastRedux)
local CanFire = true
local RNG = Random.new()							-- Set up a randomizer.
local TAU = math.pi * 2	
local MouseEvent = Tool.MouseEvent
local ReloadEvent = Tool.ReloadEvent

FastCast.DebugLogging = DEBUG
FastCast.VisualizeCasts = DEBUG

Tool.Equipped:Connect(function ()
	Handle.CanCollide = false
	Arrow.CanCollide = false
	Arrow.Spikes.CanCollide = false
	Arrow.Tail.CanCollide = false
	Arrow.Spike.CanCollide = false
	Tool.Metal.CanCollide = false
	Tool.Rope.CanCollide = false
	Tool.Fabric.CanCollide = false
end)

--caster--

local CosmeticBulletsFolder = workspace:FindFirstChild("CosmeticBulletsFolder") or Instance.new("Folder", workspace)
CosmeticBulletsFolder.Name = "CosmeticBulletsFolder"

local Caster = FastCast.new()

local CosmeticBullet = game.ServerStorage.Arrow
CosmeticBullet.CanCollide = false
CosmeticBullet.Anchored = true

local CastParams = RaycastParams.new()
CastParams.IgnoreWater = true
CastParams.FilterType = Enum.RaycastFilterType.Blacklist
CastParams.FilterDescendantsInstances = {}

local CastBehavior = FastCast.newBehavior()
CastBehavior.RaycastParams = CastParams
CastBehavior.MaxDistance = BULLET_MAXDIST
CastBehavior.HighFidelityBehavior = FastCast.HighFidelityBehavior.Default
CastBehavior.CosmeticBulletContainer = CosmeticBulletsFolder
CastBehavior.Acceleration = BULLET_GRAVITY
CastBehavior.AutoIgnoreContainer = false

CastBehavior.CosmeticBulletTemplate = CosmeticBullet
--fire--

function Fire(direction)
	if Tool.Parent:IsA("Backpack") then return end
	local directionalCF = CFrame.new(Vector3.new(), direction)
	local direction = (directionalCF * CFrame.fromOrientation(0, 0, RNG:NextNumber(0, TAU)) * CFrame.fromOrientation(math.rad(RNG:NextNumber(MIN_BULLET_SPREAD_ANGLE, MAX_BULLET_SPREAD_ANGLE)), 0, 0)).LookVector

	local simBullet = Caster:Fire(Handle.Position, direction, BULLET_SPEED, CastBehavior)
end

--events--

function OnRayHit(cast, raycastResult, segmentVelocity, cosmeticBulletObject)
	-- This function will be connected to the Caster's "RayHit" event.
	local cosmeticBullet = cast.RayInfo.CosmeticBulletObject
	local hitPart = raycastResult.Instance
	local hitPoint = raycastResult.Position
	local normal = raycastResult.Normal
	cosmeticBullet.CFrame = cosmeticBullet.CFrame + cosmeticBullet.CFrame.LookVector
	local weld = Instance.new("WeldConstraint")
	weld.Part0 = hitPart
	weld.Part1 = cosmeticBullet
	weld.Parent = cosmeticBullet
	if hitPart ~= nil and hitPart.Parent ~= nil then
		local humanoid = hitPart.Parent:FindFirstChildOfClass("Humanoid")
		if humanoid then
			
			health_module.TakeClampedDamage(humanoid, 55)
			cosmeticBullet.Anchored = false
		elseif hitPart.Parent:IsA("Accoutrement") then
			health_module.TakeClampedDamage(hitPart.Parent.Parent.Humanoid, 55)
			cosmeticBullet.Anchored = false
		end
	end
	wait(4)
	cosmeticBullet:Destroy()
end

function OnRayUpdated(cast, segmentOrigin, segmentDirection, length, segmentVelocity, cosmeticBulletObject)
	-- Whenever the caster steps forward by one unit, this function is called.
	-- The bullet argument is the same object passed into the fire function.
	if cosmeticBulletObject == nil then return end
	local bulletLength = cosmeticBulletObject.Size.Z / 2
	local baseCFrame = CFrame.new(segmentOrigin, segmentOrigin + segmentDirection)
	cosmeticBulletObject.CFrame = baseCFrame * CFrame.new(0, 0, -(length - bulletLength))
end

function OnRayTerminated(cast)
	local cosmeticBullet = cast.RayInfo.CosmeticBulletObject
	if cosmeticBullet ~= nil then
		-- This code here is using an if statement on CastBehavior.CosmeticBulletProvider so that the example gun works out of the box.
		-- In your implementation, you should only handle what you're doing (if you use a PartCache, ALWAYS use ReturnPart. If not, ALWAYS use Destroy.
		if CastBehavior.CosmeticBulletProvider ~= nil then
			CastBehavior.CosmeticBulletProvider:ReturnPart(cosmeticBullet)
		end
	end
end

function Transparency()
	Arrow.Transparency = 1
	Arrow.Spike.Transparency = 1
	Arrow.Spikes.Transparency = 1
	Arrow.Tail.Transparency = 1
end

function UnTransparency()
	Arrow.Transparency = 0
	Arrow.Spike.Transparency = 0
	Arrow.Spikes.Transparency = 0
	Arrow.Tail.Transparency = 0
end


ReloadEvent.OnServerEvent:Connect(function()
	CanFire = true
	UnTransparency()
end)

MouseEvent.OnServerEvent:Connect(function (clientThatFired, mousePoint)
	if CanFire == false then return end
	CanFire = false
	local mouseDirection = (mousePoint - Arrow.Spike.Position).Unit
	for i = 1, BULLETS_PER_SHOT do
		Fire(mouseDirection)
	end
	Transparency()
end)

Caster.RayHit:Connect(OnRayHit)
Caster.LengthChanged:Connect(OnRayUpdated)
Caster.CastTerminating:Connect(OnRayTerminated)

Tool.Equipped:Connect(function ()
	CastParams.FilterDescendantsInstances = {Tool.Parent, CosmeticBulletsFolder}
end)

LOCAL SCRIPT:

local Tool = script.Parent
local Handle = Tool:WaitForChild("Handle")
local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local MouseEvent = Tool:WaitForChild("MouseEvent")
local ReloadEvent = Tool:WaitForChild("ReloadEvent")
local player = game.Players.LocalPlayer
local character = player.Character
local cooldown = 0.8
local debounce = false 
local charged = true
local Mouse = nil
local mouseget = player:GetMouse()
local Mousepl = nil
local ExpectingInput = false
local Camera = workspace.CurrentCamera
local IsMouseDown = false
local animation1 = Instance.new("Animation")
animation1.AnimationId = "http://www.roblox.com/asset/?id=7659776195"
local animationIdle
local animation2 = Instance.new("Animation")
animation2.AnimationId = "http://www.roblox.com/asset/?id=7659853751"
local animationReload
local reloadanimOn = false


local function LoadAnim(character)
	local humanoid = character.Humanoid
	animationIdle = humanoid.Animator:LoadAnimation(animation1)
	animationReload  = humanoid.Animator:LoadAnimation(animation2)
end

local connection

if player.Character then
	LoadAnim(player.Character)
end


function OnEquipped(playerMouse)
	Mouse = playerMouse
	ExpectingInput = true
	IsMouseDown = false
	animationIdle:Play()
end

function OnUnequipped()
	ExpectingInput = false
	IsMouseDown = false
	animationIdle:Stop()
	animationIdle:Destroy()
	animationReload:Stop()
	animationReload:Destroy()
	reloadanimOn = false
	if character then
		character.runn.Disabled = false
	end
	script.Disabled = true
	script.Disabled = false
end

Tool.Equipped:Connect(OnEquipped)
Tool.Unequipped:Connect(OnUnequipped)

Tool.Activated:Connect(function()
	if player.Character then
		local humanoid = player.Character:FindFirstChild("Humanoid")
		if humanoid then
			if charged == true then
				MouseEvent:FireServer(Mouse.Hit.Position)
				charged = false
			elseif charged == false then
				if reloadanimOn == false then
					animationReload:Play()
					reloadanimOn = true
					if character then
						character.runn.Disabled = true
					end
					wait(3)
					if character then
						character.runn.Disabled = false
					end
					ReloadEvent:FireServer()
					reloadanimOn = false
					charged = true
				end
			end
		end
	end 
end)

I think the problem is about how I get the mouse.Hit.Position (line 70 local script), in the throwing knife I get it from the “Mouse” variable in OnEquipped function and it works perfectly (I can’t do that in this script).