TF2-Style Payload: Stopping and Resuming the Cart Based on Nearby Players

I’m creating a gamemode similar to TF2 payload.

Everything works fine, however I am unable to come up with a good method of stopping the payload and resuming it based on the number of players of each team are near the cart.

This has had me stumped for a long while, so any help at all is appreciated.

local tweenService = game:GetService("TweenService")
local pointsChildren = script.Parent.Parent.Waypoints:GetDescendants()
local StartPart = script.Parent.Parent.Waypoints.StartPart


local OrderByDistance = {}

local Part = script.Parent.Parent.BasePart
local Module = require(script.ModuleScript)

local CanMove = false

local tweenInfo = TweenInfo.new(
	10,
	Enum.EasingStyle.Linear,
	Enum.EasingDirection.Out,
	0,
	false,
	0
)

while #pointsChildren > 0 do
	local lastIndex = nil
	local closestPart = nil
	local PreviousDistance = nil
	
	for i, v in ipairs(pointsChildren) do
		if v:IsA("Part") and v ~= StartPart then
			local distance = (StartPart.Position - v.Position).Magnitude
			
			if closestPart == nil then
				closestPart = v
				PreviousDistance = distance
				lastIndex = i
			else 
				if distance < PreviousDistance then
					closestPart = v
					PreviousDistance = distance
					lastIndex = i
				end
			end
		end
	end
	
	StartPart = closestPart
	table.insert(OrderByDistance, closestPart)
	table.remove(pointsChildren, lastIndex)
	
	
	
end

local check = coroutine.wrap(function()
	while wait(0.1) do
		if #Module:ATTACKERSNUM() > 0 and #Module:DEFENDERSNUM() <= 0 then
			CanMove = true
		elseif #Module:ATTACKERSNUM() <= 0 and #Module:DEFENDERSNUM() > 0 then
			CanMove = false
		elseif #Module:ATTACKERSNUM() == 0 and #Module:DEFENDERSNUM() == 0 then
			CanMove = false
		end
		print(#Module:ATTACKERSNUM())
		print(#Module:DEFENDERSNUM())
	end
	

	
end)

check()

while true do
	for _, v in ipairs(OrderByDistance) do
		v.Material = Enum.Material.Neon
		task.wait(0.1)
		v.Material = Enum.Material.Plastic

		local Goal = {}
		Goal.Position = v.Position 
		Goal.Orientation = v.Orientation
		local tween = tweenService:Create(Part,tweenInfo,Goal)

		if CanMove then
			tween:Play()
		else
			tween:Pause()
		end

		tween.Completed:Wait() 

	end
end

The stop/resume mechanic isn’t really your problem — the problem is that CanMove gets read once per waypoint and then never again.

Look at the order in your main loop:

if CanMove then
	tween:Play()
else
	tween:Pause()
end

tween.Completed:Wait()

CanMove is sampled at the moment the tween is created, and then you block on Completed:Wait(). Two things fall out of that:

  • If CanMove is false when a segment starts, you call Pause() on a tween that was never played. It never runs, Completed never fires, and that :Wait() blocks forever. The cart doesn’t stop — the loop dies.
  • If CanMove flips to false during a segment, nothing re-reads it, so the cart finishes the segment anyway.

Your check() coroutine is also missing the case you actually care about. The chain covers attackers-only, defenders-only and nobody — but there’s no branch for both teams present, so CanMove silently keeps whatever it already was. That contested state is the whole mechanic.

Read your two counts into locals at the top of the poll and write the rule as a single expression, so there’s no gap for a case to fall through:

-- your two team-count calls, hoisted out of the branches
local attackers = #(...)
local defenders = #(...)

CanMove = attackers > 0 and defenders == 0

That also stops you calling both module functions two or three times per tick just to evaluate the chain.

On stop/resume specifically: Tween:Pause() and Tween:Play() do behave the way you want — Pause freezes at the current position, Play resumes from there, and tween.PlaybackState tells you which state you’re in. So you could keep tweens and just move the check out of the segment loop into your existing 0.1s poll:

if CanMove and tween.PlaybackState ~= Enum.PlaybackState.Playing then
	tween:Play()
elseif not CanMove and tween.PlaybackState == Enum.PlaybackState.Playing then
	tween:Pause()
end

I’d still move off tweens for a payload though, because of the other issue: your TweenInfo is a flat 10 seconds per segment no matter how far apart the waypoints are, so the cart crawls between close ones and rockets between distant ones. And a payload that speeds up with more attackers pushing is something a fixed-duration tween can’t express at all.

Driving it yourself is less code than it sounds:

local RunService = game:GetService("RunService")
local speed = 8 -- studs/sec
local index, alpha = 1, 0

RunService.Heartbeat:Connect(function(dt)
	if not CanMove or index >= #OrderByDistance then return end

	local a, b = OrderByDistance[index], OrderByDistance[index + 1]
	local segment = (b.Position - a.Position).Magnitude

	alpha += (speed * dt) / segment
	while alpha >= 1 do
		alpha -= 1
		index += 1
		if index >= #OrderByDistance then return end
		a, b = OrderByDistance[index], OrderByDistance[index + 1]
		segment = (b.Position - a.Position).Magnitude
	end

	Part.CFrame = CFrame.new(a.Position:Lerp(b.Position, alpha), b.Position)
end)

Stopping is now just CanMove going false — no pause state to manage, nothing to cancel, and resuming happens on its own. Speed is constant regardless of waypoint spacing, and speed becomes a number you can scale: something like speed = 6 + math.min(#attackers, 3) * 2 gets you push-rate behaviour directly.

One separate thing in your path sort, because it will bite you later:

local pointsChildren = script.Parent.Parent.Waypoints:GetDescendants()

GetDescendants() returns everything under Waypoints, not just Parts. Your loop only removes an entry when it finds a Part, so once nothing but non-Parts is left, closestPart and lastIndex stay nil — table.remove(t, nil) then drops the last element instead of the one you meant, StartPart becomes nil, and the next StartPart.Position throws. If there’s a Folder, an Attachment or a script sitting under Waypoints right now, it’s already misbehaving.

Past that, greedy nearest-neighbour isn’t really a path — it’s a chain, and it will happily cut across a hairpin when two waypoints on opposite legs happen to be near each other. For a fixed track just name them 1, 2, 3… and sort by name, or put an Index attribute on each. Deterministic, and you can read the order straight out of the Explorer.

1 Like

Thank you so much, you’re a life saver!

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.