Fixing "Left-Turn" Direction in Procedural Road Generation

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!
    I want to make that the left Turn direction is correct (btw I am pretty sure i next up the name for the models, but that shouldn’t really matter and plus I am to lazy to fix them)

  2. What is the issue? Include screenshots / videos if possible!

  3. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    I have tried using just on turnModel and both didn’t work, rn only the model named rightTurn works. Tried using math.rad(-90) for the LEFT_TURN_ANGLE but it didn’t change much

This is the rightTurnModel

local module = {}

--[[
	Constants and probabilities
		LEFT_TURN_ANGLE  : Angle (in radians) for a left turn.
		RIGHT_TURN_ANGLE : Angle (in radians) for a right turn.
		TURN_CHANCE      : Probability that a turn occurs when allowed.
		TURN_COOLDOWN    : Number of straight segments forced after a turn.
]]
local LEFT_TURN_ANGLE = math.rad(90)
local RIGHT_TURN_ANGLE = math.rad(-90)
local TURN_CHANCE = 0.5
local TURN_COOLDOWN = 10

local TemplateModels = {
	initial  = roadPrefabs:WaitForChild("Road_Initial"),
	straight = roadPrefabs:WaitForChild("Road_Straight"),
	leftTurn = roadPrefabs:WaitForChild("Road_LeftTurn"),
	rightTurn = roadPrefabs:WaitForChild("Road_RightTurn")
}

--[[
	alignModel(newModel, snapCFrame)
		Aligns the newModel so that its BackConnector is exactly at snapCFrame.
		This ensures each road segment attaches to the previous segment's FrontConnector.
]]
local function alignModel(newModel, snapCFrame)
	local backConnector = newModel:WaitForChild("BackConnector")
	local offset = backConnector.CFrame:ToObjectSpace(newModel.PrimaryPart.CFrame)
	newModel:SetPrimaryPartCFrame(snapCFrame * offset)
end

--[[
	spawnSegment(templateType, snapCFrame, overridePOI)
		Creates a physical road segment of the specified templateType 
		and snaps its BackConnector to snapCFrame.
		
		Arguments:
			templateType (string) : "initial", "straight", "leftTurn", or "rightTurn"
			snapCFrame   (CFrame) : The position/orientation for the segment's BackConnector
			overridePOI  (bool)   : Whether to force a point of interest on this segment
		
		Returns:
			table {
				model       : The spawned Model
				type        : The templateType
				hasPOI      : Whether the segment contains a POI
				frontCFrame : CFrame of the segment's FrontConnector
				finish2D    : { x, y } 2D position of FrontConnector (for path logic)
			}
]]
function module.spawnSegment(templateType, snapCFrame, overridePOI)
	local templateModel = TemplateModels[templateType]
	if not templateModel then
		error("Unknown template type: " .. tostring(templateType))
	end

	local newModel = templateModel:Clone()
	newModel.Parent = workspace.Roads

	-- Align the new road piece
	alignModel(newModel, snapCFrame)

	local frontConnector = newModel:WaitForChild("FrontConnector")
	local frontPos = frontConnector.Position
	local finish2D = { x = frontPos.X, y = frontPos.Z }

	local hasPOI = overridePOI or (math.random() < 0.2)

	local segment = {
		model       = newModel,
		type        = templateType,
		hasPOI      = hasPOI,
		frontCFrame = frontConnector.CFrame,
		finish2D    = finish2D
	}
	return segment
end

--[[
	getRotationChange(templateType)
		Returns how much we rotate the direction (in radians) for each segment type.
		"leftTurn"   => +LEFT_TURN_ANGLE
		"rightTurn"  => -RIGHT_TURN_ANGLE
		"straight"   => 0
		"initial"    => 0
]]
local function getRotationChange(templateType)
	if templateType == "leftTurn" then
		return LEFT_TURN_ANGLE
	elseif templateType == "rightTurn" then
		return -RIGHT_TURN_ANGLE
	else
		return 0
	end
end

--[[
	generateRandomRoad(start2D, direction, numSegments)
		Generates a sequence of road segments in 3D space, starting from a given 
		2D coordinate (start2D) at a certain direction. The result is numSegments
		segments (including the initial one). Periodically spawns turns based on
		TURN_CHANCE, with a cooldown period that forces straight segments after a turn.
		
		Arguments:
			start2D    : { x, y } initial 2D point
			direction  : (number) heading in radians
			numSegments: (number) how many total segments to spawn
			
		Returns:
			segments   : table of spawned segment data
]]
function module.generateRandomRoad(start2D, direction, numSegments)
	local segments = {}
	local cooldown = 0

	local function getSnapCFrame(pos2D, dir)
		return CFrame.new(pos2D.x, 0, pos2D.y) * CFrame.Angles(0, dir, 0)
	end


	local segType = "initial"
	local snapCFrame = getSnapCFrame(start2D, direction)
	local segment = module.spawnSegment(segType, snapCFrame, true)
	table.insert(segments, segment)

	segment.model.Name = string.format("%s_Segment_%d", segType, 1)

	-- Update the 2D position and direction for the next piece
	local current2D = segment.finish2D
	local currentDirection = direction + getRotationChange(segType)

	-- Generate subsequent segments
	for i = 2, numSegments do
		if cooldown > 0 then
			segType = "straight"
			cooldown -= 1
		else
			if math.random() < TURN_CHANCE then
				segType = (math.random() < 0.5) and "leftTurn" or "rightTurn"
				cooldown = TURN_COOLDOWN
			else
				segType = "straight"
			end
		end

		snapCFrame = getSnapCFrame(current2D, currentDirection)
		local newSeg = module.spawnSegment(segType, snapCFrame)
		table.insert(segments, newSeg)

		newSeg.model.Name = string.format("%s_Segment_%d", segType, i)

		-- Update to next
		current2D = newSeg.finish2D
		currentDirection = currentDirection + getRotationChange(segType)
	end

	generationFinished:Fire(segments)

	return segments
end

return module

Thanks for the help

1 Like

I’m noob and I’m probably wrong, but do you have BackConnector on correct end of that template?

1 Like

there should be 2 separate models for left and right turns because they are mirrors of each other so you cant make the other with just rotation

1 Like

There separate models for the turns

yeah they are the right way!
Fixed the Problem, just remade the turns

1 Like

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