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

I do it this way, haven’t had any issues.

1 Like

Would be nice if this had a bullet penetration option.

Though, I’ve already implemented it myself and it seems to be working fine for the most part. However, I don’t feel too confident about how I’ve scripted bullet penetration.

May I ask how you programmed bullet penetration?

You could fire the bullet again, but adding the part it just hit to the ignore list.

1 Like

I am using your module for my game, which is about space combat. My first weapons prototype will be a defense satellite. I basically copied your gun example, removed the client side stuff (mouse clicking and mouse.hit.p), and made it shoot automatically in bursts (with separate delay timers for individual shots and bursts).

As far as I know I am inputting the correct values that the module needs, and I changed nothing in the FastCast module. I am also up to date as I tried replacing the FastCast module a few times. This is what happens:
Screenshot_84
The bullet stays in position, with no velocity being applied to it. The bullet is not hitting something because it prints (“hit”) if it hits something (as seen in the code later on).

It’s aiming is accurate though…?
Screenshot_85

I don’t know what I am doing wrong! According to the prints it is supposed to be working. (The Vector3 is “ModifiedBulletVelocity” and “firesound” is when the sound function is called.)
Screenshot_86
Note:
Every ship has a “Core” where values and scripts are stored.

Help would be appreciated!

-- Latest update 11 June 2019 - Added bullet spread factor as suggested by https://devforum.roblox.com/t/making-a-combat-game-with-ranged-weapons-fastcast-may-be-the-module-for-you/133474/88?u=etithespirit

local FirePointObject = script.Parent
local FastCast = require(script.Parent.FastCast)

local Debris = game:GetService("Debris")

-- REMEMBER: THERE'S RESOURCES TO HELP YOU AT https://github.com/XanTheDragon/FastCastAPIDocs/wiki/API
local RNG = Random.new()							-- Set up a randomizer.
local BULLET_SPEED = 500							-- Studs/second - the speed of the bullet
local BULLET_MAXDIST = 1500							-- The furthest distance the bullet can travel 
local BULLET_DROP_GRAVITY = 0						-- The amount of gravity applied to the bullet. Default place gravity is 196.2 (see workspace.Gravity)
local BULLET_WIND_OFFSET = Vector3.new(0, 0, 0)	-- The amount of force applied to the bullet in world space. Useful for wind effects.
local MIN_BULLET_SPREAD_ANGLE = 0.3					-- 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.7					-- 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.5								-- The amount of time that must pass after firing the gun before we can fire again.
local BURST_COUNT = 3
local BURST_DELAY = 2
local DAMAGE = {EMP=15,NORMAL=12}
local TAU = math.pi * 2							-- Set up mathematical constant Tau (pi * 2)

local CanFire = true -- Used for a cooldown.
local CurrentShot = 0

-- Now we set the caster values.
local Caster = FastCast.new() --Create a new caster object.
Caster.Gravity = BULLET_DROP_GRAVITY --Set the values accordingly.
Caster.ExtraForce = BULLET_WIND_OFFSET
Caster.IgnoreDescendantsInstance = workspace.Effects

-- Make a base cosmetic bullet object. This will be cloned every time we fire off a ray.
local CosmeticBullet = Instance.new("Part")
CosmeticBullet.Name = "Bolt"
CosmeticBullet.Material = Enum.Material.Neon
CosmeticBullet.Color = Color3.fromRGB(110, 153, 202)
CosmeticBullet.CanCollide = false
CosmeticBullet.Anchored = true
CosmeticBullet.Size = Vector3.new(0.25, 0.25, 1)

-- Bonus points: If you're going to be slinging a ton of bullets in a short period of time, you may see it fit to use PartCache.
-- https://devforum.roblox.com/t/partcache-for-all-your-quick-part-creation-needs/246641 

-- And a function to play fire sounds.
function PlayFireSound()
	print("firesound")
end

-- Create the spark effect for the bullet impact
function MakeParticleFX(Position, Normal)
	-- This is a trick I do with attachments all the time.
	-- Parent attachments to the Terrain - It counts as a part, and setting position/rotation/etc. of it will be in world space.
	-- UPD 11 JUNE 2019 - Attachments now have a "WorldPosition" value, but despite this, I still see it fit to parent attachments to terrain since its position never changes.
	local Attachment = Instance.new("Attachment")
	Attachment.CFrame = CFrame.new(Position, Position + Normal)
	Attachment.Parent = workspace.Terrain
	local Particle = ImpactParticle:Clone()
	Particle.Parent = Attachment
	Debris:AddItem(Attachment, Particle.Lifetime.Max) -- Automatically delete the particle effect after its maximum lifetime.
	
	-- A potentially better option in favor of this would be to use the Emit method (Particle:Emit(numParticles)) though I prefer this since it adds some natural spacing between the particles.
	Particle.Enabled = true
	wait(0.05)
	Particle.Enabled = false
end

function Fire(Direction)
	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 ModifiedBulletSpeed = Direction * BULLET_SPEED
	
	-- Prepare a new cosmetic bullet
	local Bullet = CosmeticBullet:Clone()
	Bullet.CFrame = CFrame.new(FirePointObject.Position, FirePointObject.Position + Direction)
	Bullet.Parent = workspace.Effects
	
	-- NOTE: It may be a good idea to make a Folder in your workspace named "CosmeticBullets" (or something of that nature) and use FireWithBlacklist on the descendants of this folder!
	-- Quickly firing bullets in rapid succession can cause the caster to hit other casts' bullets from the same gun (The caster only ignores the bullet of that specific shot, not other bullets).
	-- Do note that if you do this, you will need to remove the Equipped connection that sets IgnoreDescendantsInstance, as this property is not used with FireWithBlacklist
	
	-- Fire the caster
	Caster:FireWithBlacklist(FirePointObject.Position, Direction * BULLET_MAXDIST, ModifiedBulletSpeed, script.Parent.Parent:GetChildren(), Bullet)
	print(ModifiedBulletSpeed)
	-- Play the sound
	PlayFireSound()
end

function OnRayHit(HitPart, HitPoint, Normal, Material, CosmeticBulletObject)
	-- This function will be connected to the Caster's "RayHit" event.
	print("hit")
	CosmeticBulletObject:Destroy()
	if HitPart and HitPart.Parent then
		local Core = HitPart.Parent:FindFirstChildOfClass("Core")
		if Core then
			if Core.Shield.Value ~= 0 then
				Core.Shield.Value = Core.Shield.Value - DAMAGE.EMP
				if Core.Shield.Value < 0 then
					Core.Shield.Value = 0
				end
			else
				Core.Hull.Value = Core.Hull.Value - DAMAGE.NORMAL
				if Core.Hull.Value < 0 then
					Core.Hull.Value = 0
				end
			end
		else
			print("hit does not have core")
		end
		--MakeParticleFX(HitPoint, Normal) -- Particle FX
	end
end

function OnRayUpdated(CastOrigin, SegmentOrigin, SegmentDirection, Length, 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.
	local BulletLength = CosmeticBulletObject.Size.Z / 2 -- This is used to move the bullet to the right spot based on a CFrame offset
	CosmeticBulletObject.CFrame = CFrame.new(SegmentOrigin, SegmentOrigin + SegmentDirection) * CFrame.new(0, 0, -(Length - BulletLength))
end
	
-- Before I make the connections, I will check for proper values. In production scripts that you are writing that you know you will write properly, you should not do this.
-- This is included exclusively as a result of this being an example script, and users may tweak the values incorrectly.
assert(MAX_BULLET_SPREAD_ANGLE >= MIN_BULLET_SPREAD_ANGLE, "Error: MAX_BULLET_SPREAD_ANGLE cannot be less than MIN_BULLET_SPREAD_ANGLE!")
if (MAX_BULLET_SPREAD_ANGLE > 180) then
	warn("Warning: MAX_BULLET_SPREAD_ANGLE is over 180! This will not pose any extra angular randomization. The value has been changed to 180 as a result of this.")
	MAX_BULLET_SPREAD_ANGLE = 180
end

while wait() do
	if CanFire and CurrentShot < BURST_COUNT then
		for i, player in pairs(workspace.Players:GetChildren()) do
			if (player.Core.Position - script.Parent.Position).magnitude < BULLET_MAXDIST-BULLET_SPEED then
				print("shooting at: " .. player.Name)
				local Direction = (player.Core.CFrame.p - script.Parent.Position).Unit
				Fire(Direction)
				CurrentShot = CurrentShot+1
				wait(FIRE_DELAY)
			end
		end
	else
		CanFire = false
		CurrentShot = 0
		wait(BURST_DELAY)
		CanFire = true
	end
end

Caster.LengthChanged:Connect(OnRayUpdated)
Caster.RayHit:Connect(OnRayHit)
2 Likes

The following part of the code is faulty (the very end):

while wait() do
	...
end

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

Move the connections so that they are before the loop then it’ll work fine. It can’t connect because the code will never go anywhere after the loop.

5 Likes

ok I ended up using this for some guns i made and it was super useful, so thanks 4 that. the only suggestion I can offer is remove the LengthChanged and RayHit events and replace them as parameters for when the :Fire method is called on a caster. I was using a single caster for all weapons but some weapons needed different things to happen when it hit something, so the best thing I could think of without modifying your code was inserting a value inside of the bullet object itself and checking what it was to do things such as explosions/different particle effects. Im not a fan of this approach but it worked. However, I think replacing the custom events could improve your module and make some things easier.

5 Likes

I’m using FastCast but the ray is intersecting with CanCollide=false objects.

Is there a way to ignore objects with CanCollide=false? Or do I have to recast the ray manually?

3 Likes

Rays will intersect with parts that have CanCollide false.

One option is to move the parts to another collision group. So long as this collision group can’t collide with the Default group, the ray will pass through it.

3 Likes

Do you mean you had say, 3 different weapons each with unique functionality? If this is the case, you should make 3 different casters, one for each weapon.

1 Like

yea but im rendering projectiles client side, so replicating projectiles that way didnt seem too practical for me at the time. i’d need the client to have a caster for each weapon on the client and it didnt seem like a clean way to do it. but now that i think about it, I think I couldve maybe just passed the caster from the client in a remote event and used that, but not too sure if i can do that. but its too late anyways, i ended up making a seperate module thats based off of your work that can spawn projectiles something like this for use in the future.

projectile.new({
origin = v3,
direction = v3,
velocity = v3,
acceleration = v3, (for acceleration, wind, “ExtraForce”)
gravity = num,
blacklist = table,
stopped = function,
update = function
})

stopped basically replaced RayHit and update replaced LengthChanged

and by based on ur work i mean copied all ur math and just changed it up a bit : P

2 Likes

Perfect solution to my problem, thank you.

1 Like

Can you elaborate on this? It doesn’t seem to work for me.

1 Like

This would help you.

coroutine.resume(coroutine.create(function()
    while wait() do
        ...
    end
end))

Caster.LengthChanged:Connect(OnRayUpdated)
Caster.RayHit:Connect(OnRayHit)
1 Like

Hello!

I’ve implemented fastcast projectiles in my own game (projectiles are visual only for clients and do damage on the server). I’m using a part with a PointLight in it, and i’ve noticed sometimes the projectiles tend to stay mid-path without moving (I am not sure whether the actual projectile is there, but the PointLight stays active right in the air). Did this happen to anyone? Is there a fix. i’m willing to provide code, i just don’t know which part of it.

Edit: i just realized i replied this to someone instead of the post creator. Sorry for that.

Do you have a onHit(or something similar) function in your projectile system that deletes the bullet upon intersection? Maybe the bullet is intersecting with an invisible part?

1 Like

i have something similiar to onHit (except with particles) that does just that. if it did intersect, it’d stop the bullet in the air instead of keeping the pointlight active for multiple seconds.

1 Like

Does anyone else have problems with the cosmetic bullet staying visible after the death? The bullet freezes and is still there once the player respawns. I have read all of the comments and the issue seems to have not been addressed.

1 Like

I’m pretty sure you have to :Destroy() the cosmetic bullet yourself once it impacts, that’s what I did at least.

2 Likes

My bullets disappear just fine on hit. My problem is that if the player dies, any fastcast bullets stop in mid air and stay there.

2 Likes