How To Make a Dynamic Line

  1. What do you want to achieve?
    I’ve recently come across the following youtube video: https://www.youtube.com/watch?v=2OR9IdAUiFQ . I found the frontline they used (the black line) very interesting and wanted to imitate it but couldn’t figure out how.

  2. What is the issue?
    I’ve come up with a script that splits my line into nodes/segments then it detects whenever the units (blocks) move near the line and it adjusts accordingly. The issues with this script is that it is very janky, completely different from the goal of the line in the video and also it fails to create new segments/nodes so some nodes become super long and rotated. The final issue I have is that sometimes the script has a hard time figuring out the direction a block is moving from/started at, this leads to issues of sometimes the line moves towards the unit that is advancing and not away from it, and it will also sometimes lead to the line moving backwards with a unit when the unit retreats (the line should only adjust when a unit advances not retreats).

  3. What solutions have you tried so far? Did you look for solutions on the Creator Hub?
    I have tried asking some friends but they couldn’t help me, and I did try look on the creator hub but nothing that helped came up for me.

  4. Extra Features That Are Missing:
    As in the video you can see how when a unit is encircled it will automatically destroy, I want it to detect if a unit is encircled and if it is don’t destroy it, only destroy the line if there is no unit inside. And also the script is unable to create encirclements properly right now since the line is always connected. Finally, there is also the issue that the line ignores enemy units right now, so if there is a unit advancing and another unit on the opposite side, the line will just go past the opposite side’s unit.

Here is my current script for the line:

local Workspace = game:GetService("Workspace")
local RunService = game:GetService("RunService")

local FRONTLINE_NAME = "Frontline"
local BLOCKS_FOLDER_NAME = "Blocks"
local NODE_COUNT = 50
local NODE_SPACING = 2
local MAX_AFFECT_RADIUS = 20
local PUSH_SCALE = 0.25
local DAMPING = 0.95
local MAX_DISTANCE_FROM_BLOCK = 5

local original = Workspace:FindFirstChild(FRONTLINE_NAME)
local blocksFolder = Workspace:FindFirstChild(BLOCKS_FOLDER_NAME)
assert(original and blocksFolder, "Missing Frontline or Blocks")

local baseCFrame = original.CFrame
local nodeHeight = original.Size.Y
original:Destroy()

-- NODE CONTAINER
local nodeFolder = Instance.new("Folder")
nodeFolder.Name = "Frontline"
nodeFolder.Parent = Workspace

-- NODE STRUCTURE
local nodes = {}
for i = 0, NODE_COUNT - 1 do
	local part = Instance.new("Part")
	part.Size = Vector3.new(NODE_SPACING, nodeHeight, 1)
	part.Anchored = true
	part.CanCollide = false
	part.Material = Enum.Material.SmoothPlastic
	part.Color = Color3.new(0, 0, 0)
	part.Parent = nodeFolder

	table.insert(nodes, {
		Part = part,
		XOffset = 0,
		ZOffset = 0,
		XVelocity = 0,
		ZVelocity = 0,
	})
end

local function positionNodes()
	local start = baseCFrame.Position - baseCFrame.RightVector * ((#nodes - 1) * NODE_SPACING / 2)

	for i, node in ipairs(nodes) do
		local basePos = start + baseCFrame.RightVector * ((i - 1) * NODE_SPACING)
		local offset = Vector3.new(node.XOffset, 0, node.ZOffset)
		local pos = basePos + offset
		node.Part.Position = pos
	end

	for i = 1, #nodes - 1 do
		local current = nodes[i]
		local nextNode = nodes[i + 1]
		local vec = (nextNode.Part.Position - current.Part.Position)
		local dist = vec.Magnitude
		current.Part.Size = Vector3.new(current.Part.Size.X, current.Part.Size.Y, dist)
		current.Part.CFrame = CFrame.lookAt(current.Part.Position, nextNode.Part.Position) * CFrame.new(0, 0, -dist / 2)
	end

	-- LAST NODE LOOKS AT PREVIOUS
	if #nodes > 1 then
		local last = nodes[#nodes]
		local prev = nodes[#nodes - 1]
		local vec = (last.Part.Position - prev.Part.Position)
		last.Part.Size = Vector3.new(last.Part.Size.X, last.Part.Size.Y, vec.Magnitude)
		last.Part.CFrame = CFrame.lookAt(last.Part.Position, prev.Part.Position) * CFrame.new(0, 0, -vec.Magnitude / 2)
	end
end

local lastPositions = {}

local function isPushingToward(nodePos, blockPos, velocity)
	local toNode = (nodePos - blockPos).Unit
	local speedTowardNode = velocity:Dot(toNode)
	return speedTowardNode > 0, speedTowardNode
end

local FRICTION = 0.9
local MASS = 1  -- YOU CAN TUNE THIS TO MAKE IT MORE/LESS SLUGGISH

local MAX_PUSH_DISTANCE = 1  -- NODES WON'T GO CLOSER THAN THIS TO A PUSHING BLOCK
local NEW_NODE_DISTANCE = NODE_SPACING * 1.25 -- WHEN NEEDED, ADD A NEW NODE TO EXTEND THE LINE
local MAX_TOTAL_SPREAD = NODE_COUNT * NODE_SPACING * 2 -- PREVENT ARTIFICIAL LIMIT

-- OVERRIDE APPLYPUSHFORCES WITH THESE CHANGES:
local function applyPushForces()
	local newPositions = {}
	local blockVelocities = {}

	-- CALCULATE BLOCK VELOCITIES
	for _, block in ipairs(blocksFolder:GetChildren()) do
		if block:IsA("BasePart") then
			local last = lastPositions[block] or block.Position
			local velocity = (block.Position - last)
			blockVelocities[block] = velocity
			newPositions[block] = block.Position
		end
	end

	local totalWidth = (#nodes - 1) * NODE_SPACING

	for i, node in ipairs(nodes) do
		local nodePos = node.Part.Position
		local forceX, forceZ = 0, 0

		for block, velocity in pairs(blockVelocities) do
			local blockPos = newPositions[block]
			local planarDistVec = Vector3.new(blockPos.X, 0, blockPos.Z) - Vector3.new(nodePos.X, 0, nodePos.Z)
			local planarDist = planarDistVec.Magnitude

			if planarDist <= MAX_AFFECT_RADIUS then
				local toNode = (nodePos - blockPos).Unit
				local speedTowardNode = velocity:Dot(toNode)

				-- ONLY PUSH IF MOVING TOWARD THE NODE
				if speedTowardNode > 0 then
					local influence = (1 - (planarDist / MAX_AFFECT_RADIUS))

					-- DON'T PUSH NODE PAST THE BLOCK'S POSITION MINUS 1 STUD BUFFER
					local futureOffset = Vector3.new(node.XOffset + node.XVelocity, 0, node.ZOffset + node.ZVelocity)
					local futurePos = node.Part.Position + futureOffset
					local stopVec = Vector3.new(blockPos.X, 0, blockPos.Z) - futurePos

					if stopVec.Magnitude > MAX_PUSH_DISTANCE then
						forceX += toNode.X * speedTowardNode * influence
						forceZ += toNode.Z * speedTowardNode * influence
					end
				end
			end
		end

		-- APPLY VELOCITY WITH DAMPING
		node.XVelocity = (node.XVelocity + (forceX / MASS)) * FRICTION
		node.ZVelocity = (node.ZVelocity + (forceZ / MASS)) * FRICTION

		-- UPDATE OFFSETS
		node.XOffset += node.XVelocity
		node.ZOffset += node.ZVelocity
	end

	lastPositions = newPositions
end

RunService.Heartbeat:Connect(function()
	applyPushForces()
	positionNodes()
end)

I would appreciate if someone could find out where the math in my script is going wrong, where and how I can add the size limit of the nodes (and then adding new nodes when needed) and also preventing the lines from going too far out and so they can detect enemy units.

Here is a gif (I’m using gyazo) of how the line currently behaves:
b995eae77ef17fb2752f5b2ef5063eb7

Here is the current studio file if you want the place I’m testing it in:
FrontlineTest.rbxl (58.7 KB)

1 Like