CameraController (client): - Scriptable chase cam only while in the DriveSeat; restores default on exit - Frame-rate independent damped follow with look-ahead aim point - Flat horizon (ignores car pitch/roll) so bumps don't shake the view - Speed-based FOV kick (quadratic ramp) for sense of pace - Cleans up on Died/despawn MapBuilder v2: - Highway extended 2400 -> 3200 studs with distinct feature zones - Gantry arches (seg 2), enclosed neon-lit tunnel (seg 3), jump ramp + landing pair on the right lane (seg 4, left lane stays flat) - Procedural neon skyline towers flanking both sides (deterministic seed, same city every server) - End kill-wall so you can't sail off the far edge
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
--!strict
|
||||
--[[
|
||||
CameraController.luau (CLIENT)
|
||||
Chase camera for driving. Replaces the default Roblox camera ONLY while the
|
||||
local player sits in a DriveSeat; hands the camera straight back when they
|
||||
get out.
|
||||
|
||||
Feel decisions (tunables at the top):
|
||||
- The camera chases a point behind/above the car with critically-damped
|
||||
smoothing (frame-rate independent lerp), so it lags into corners and
|
||||
catches up on straights — classic arcade racer feel.
|
||||
- It looks ahead of the car (not at it), which reads speed better.
|
||||
- FOV widens with speed for a cheap but very effective sense of pace.
|
||||
- Yaw follows the car's facing, but the position smoothing means drifts
|
||||
and slides read visually (car rotates inside the frame first).
|
||||
]]
|
||||
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local Workspace = game:GetService("Workspace")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Shared = ReplicatedStorage.Shared
|
||||
local Chassis = require(Shared.Chassis)
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
|
||||
local CameraController = {}
|
||||
|
||||
-- Tunables --------------------------------------------------------------------
|
||||
local CAM_DISTANCE = 24 -- studs behind the car
|
||||
local CAM_HEIGHT = 9 -- studs above the car
|
||||
local LOOK_AHEAD = 14 -- aim this far in front of the car
|
||||
local POSITION_RESPONSIVENESS = 6 -- higher = tighter follow (1/sec)
|
||||
local BASE_FOV = 70
|
||||
local MAX_FOV_KICK = 18 -- extra FOV at (chassis) top speed
|
||||
local FOV_RESPONSIVENESS = 3
|
||||
|
||||
-- State -------------------------------------------------------------------------
|
||||
local renderConn: RBXScriptConnection? = nil
|
||||
local seatedConn: RBXScriptConnection? = nil
|
||||
local savedCameraType: Enum.CameraType? = nil
|
||||
local savedFov: number? = nil
|
||||
|
||||
-- Frame-rate independent smoothing factor: fraction of remaining distance
|
||||
-- covered this frame. alpha = 1 - e^(-k*dt).
|
||||
local function smoothAlpha(k: number, dt: number): number
|
||||
return 1 - math.exp(-k * dt)
|
||||
end
|
||||
|
||||
local function startChase(seat: VehicleSeat)
|
||||
local model = seat:FindFirstAncestorOfClass("Model")
|
||||
local body = model and model.PrimaryPart
|
||||
if not body then
|
||||
return
|
||||
end
|
||||
local bodyPart = body :: BasePart
|
||||
|
||||
local camera = Workspace.CurrentCamera
|
||||
savedCameraType = camera.CameraType
|
||||
savedFov = camera.FieldOfView
|
||||
camera.CameraType = Enum.CameraType.Scriptable
|
||||
|
||||
-- Start from wherever the car is so there's no first-frame snap.
|
||||
local camPos = bodyPart.Position + bodyPart.CFrame.LookVector * -CAM_DISTANCE + Vector3.new(0, CAM_HEIGHT, 0)
|
||||
|
||||
renderConn = RunService.RenderStepped:Connect(function(dt)
|
||||
if not bodyPart.Parent then
|
||||
return -- car despawned; SeatPart signal will clean us up
|
||||
end
|
||||
|
||||
local look = bodyPart.CFrame.LookVector
|
||||
local flatLook = Vector3.new(look.X, 0, look.Z)
|
||||
if flatLook.Magnitude < 0.01 then
|
||||
flatLook = Vector3.new(0, 0, -1)
|
||||
else
|
||||
flatLook = flatLook.Unit
|
||||
end
|
||||
|
||||
-- Where the camera WANTS to be: behind and above, following flat yaw
|
||||
-- (ignoring car pitch/roll so bumps don't shake the horizon).
|
||||
local desired = bodyPart.Position - flatLook * CAM_DISTANCE + Vector3.new(0, CAM_HEIGHT, 0)
|
||||
camPos = camPos:Lerp(desired, smoothAlpha(POSITION_RESPONSIVENESS, dt))
|
||||
|
||||
local lookTarget = bodyPart.Position + flatLook * LOOK_AHEAD
|
||||
|
||||
camera.CFrame = CFrame.lookAt(camPos, lookTarget)
|
||||
|
||||
-- Speed-based FOV kick.
|
||||
local speed = bodyPart.AssemblyLinearVelocity.Magnitude
|
||||
-- Normalise against a nominal fast top speed; Chassis gives per-car
|
||||
-- values but a fixed reference keeps slow cars feeling slow.
|
||||
local speedFrac = math.clamp(speed / 190, 0, 1)
|
||||
local targetFov = BASE_FOV + MAX_FOV_KICK * speedFrac * speedFrac
|
||||
camera.FieldOfView = camera.FieldOfView + (targetFov - camera.FieldOfView) * smoothAlpha(FOV_RESPONSIVENESS, dt)
|
||||
end)
|
||||
end
|
||||
|
||||
local function stopChase()
|
||||
if renderConn then
|
||||
renderConn:Disconnect()
|
||||
renderConn = nil
|
||||
end
|
||||
local camera = Workspace.CurrentCamera
|
||||
if camera then
|
||||
camera.CameraType = savedCameraType or Enum.CameraType.Custom
|
||||
camera.FieldOfView = savedFov or BASE_FOV
|
||||
end
|
||||
savedCameraType = nil
|
||||
savedFov = nil
|
||||
end
|
||||
|
||||
function CameraController.init()
|
||||
local function onCharacter(character: Model)
|
||||
local humanoid = character:WaitForChild("Humanoid") :: Humanoid
|
||||
if seatedConn then
|
||||
seatedConn:Disconnect()
|
||||
end
|
||||
seatedConn = humanoid:GetPropertyChangedSignal("SeatPart"):Connect(function()
|
||||
stopChase()
|
||||
local seatPart = humanoid.SeatPart
|
||||
if seatPart and seatPart:IsA("VehicleSeat") and seatPart.Name == Chassis.SEAT_NAME then
|
||||
startChase(seatPart)
|
||||
end
|
||||
end)
|
||||
-- Character died/respawned while driving: make sure the camera is back.
|
||||
humanoid.Died:Connect(stopChase)
|
||||
end
|
||||
|
||||
if player.Character then
|
||||
onCharacter(player.Character)
|
||||
end
|
||||
player.CharacterAdded:Connect(onCharacter)
|
||||
end
|
||||
|
||||
return CameraController
|
||||
@@ -16,8 +16,10 @@ local Shared = ReplicatedStorage.Shared
|
||||
local Remotes = require(Shared.Remotes)
|
||||
local Cars = require(Shared.Cars)
|
||||
local ChassisController = require(script.ChassisController)
|
||||
local CameraController = require(script.CameraController)
|
||||
|
||||
ChassisController.init()
|
||||
CameraController.init()
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
local playerGui = player:WaitForChild("PlayerGui")
|
||||
|
||||
+273
-65
@@ -1,13 +1,22 @@
|
||||
--!strict
|
||||
--[[
|
||||
MapBuilder.luau (SERVER)
|
||||
Builds the MVP "Tokyo expressway" vertical slice entirely from code so the
|
||||
game is playable with ZERO manual Studio building. This is a straight,
|
||||
banked-guardrail highway segment with a start pad, a finish line, neon-ish
|
||||
side pylons, and invisible kill walls so you can't drive off the edge.
|
||||
Builds the "Tokyo expressway" map entirely from code — ZERO manual Studio
|
||||
building. v2 (M2 groundwork): the straight highway now runs through a
|
||||
sequence of FEATURES so driving it is actually interesting:
|
||||
|
||||
Later you replace this with a proper modeled map; the rest of the game
|
||||
(spawns, races) reads named parts so it keeps working.
|
||||
seg 0-1 open start (spawn pad)
|
||||
seg 2 overhead gantry arches
|
||||
seg 3 TUNNEL (enclosed, neon-striped ceiling)
|
||||
seg 4 jump ramps (side-by-side launch + landing, optional — road
|
||||
continues flat around them)
|
||||
seg 5-6 open run to the finish line
|
||||
seg 7 finish + runoff
|
||||
|
||||
Neon skyline blocks flank the whole route outside the guardrails for the
|
||||
night-city mood. Everything is still named parts (SpawnPad, FinishLine,
|
||||
KillWall_*) so gameplay code keeps working when this is replaced by a
|
||||
modeled map later.
|
||||
]]
|
||||
|
||||
local Workspace = game:GetService("Workspace")
|
||||
@@ -16,8 +25,12 @@ local MapBuilder = {}
|
||||
|
||||
local ROAD_WIDTH = 60
|
||||
local SEGMENT_LENGTH = 400
|
||||
local NUM_SEGMENTS = 6 -- total length = 2400 studs of highway
|
||||
local WALL_HEIGHT = 20
|
||||
local NUM_SEGMENTS = 8 -- total length = 3200 studs of highway
|
||||
local WALL_HEIGHT = 26
|
||||
|
||||
local NEON_PINK = Color3.fromRGB(255, 40, 120)
|
||||
local NEON_BLUE = Color3.fromRGB(40, 160, 255)
|
||||
local NEON_TEAL = Color3.fromRGB(40, 230, 200)
|
||||
|
||||
local function part(props: { [string]: any }): Part
|
||||
local p = Instance.new("Part")
|
||||
@@ -30,6 +43,242 @@ local function part(props: { [string]: any }): Part
|
||||
return p
|
||||
end
|
||||
|
||||
-- Deterministic pseudo-random for scenery (same city every server) ------------
|
||||
local function det(seed: number): number
|
||||
local x = math.sin(seed * 12.9898) * 43758.5453
|
||||
return x - math.floor(x)
|
||||
end
|
||||
|
||||
-- Road deck ------------------------------------------------------------------
|
||||
local function buildRoad(parent: Folder)
|
||||
for i = 0, NUM_SEGMENTS - 1 do
|
||||
local z = -(i * SEGMENT_LENGTH) - SEGMENT_LENGTH / 2
|
||||
local road = part({
|
||||
Name = "Road_" .. i,
|
||||
Size = Vector3.new(ROAD_WIDTH, 2, SEGMENT_LENGTH),
|
||||
Position = Vector3.new(0, 0, z),
|
||||
Color = Color3.fromRGB(28, 28, 34),
|
||||
Material = Enum.Material.Asphalt,
|
||||
})
|
||||
road.Parent = parent
|
||||
|
||||
local line = part({
|
||||
Name = "Lane_" .. i,
|
||||
Size = Vector3.new(1, 2.1, SEGMENT_LENGTH),
|
||||
Position = Vector3.new(0, 0.1, z),
|
||||
Color = Color3.fromRGB(230, 200, 60),
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
line.Parent = parent
|
||||
end
|
||||
end
|
||||
|
||||
-- Guardrails, kill walls, pylons ----------------------------------------------
|
||||
local function buildBarriers(map: Folder, totalLength: number)
|
||||
for _, side in ipairs({ -1, 1 }) do
|
||||
local x = side * (ROAD_WIDTH / 2)
|
||||
local rail = part({
|
||||
Name = "Guardrail_" .. (side == -1 and "L" or "R"),
|
||||
Size = Vector3.new(1.5, 4, totalLength),
|
||||
Position = Vector3.new(x, 2, -totalLength / 2),
|
||||
Color = Color3.fromRGB(180, 180, 190),
|
||||
Material = Enum.Material.Metal,
|
||||
})
|
||||
rail.Parent = map
|
||||
|
||||
local wall = part({
|
||||
Name = "KillWall_" .. (side == -1 and "L" or "R"),
|
||||
Size = Vector3.new(2, WALL_HEIGHT, totalLength),
|
||||
Position = Vector3.new(x + side * 2, WALL_HEIGHT / 2, -totalLength / 2),
|
||||
Transparency = 1,
|
||||
CanCollide = true,
|
||||
})
|
||||
wall.Parent = map
|
||||
|
||||
for i = 0, math.floor(totalLength / 200) do
|
||||
local pylon = part({
|
||||
Name = "Pylon",
|
||||
Size = Vector3.new(1, 14, 1),
|
||||
Position = Vector3.new(x + side * 3, 7, -(i * 200)),
|
||||
Color = side == -1 and NEON_PINK or NEON_BLUE,
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
pylon.Parent = map
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Overhead gantry arches (segment 2) -------------------------------------------
|
||||
local function buildGantries(map: Folder)
|
||||
local folder = Instance.new("Folder")
|
||||
folder.Name = "Gantries"
|
||||
folder.Parent = map
|
||||
|
||||
local segStart = 2 * SEGMENT_LENGTH
|
||||
for g = 0, 3 do
|
||||
local z = -(segStart + 60 + g * 90)
|
||||
for _, side in ipairs({ -1, 1 }) do
|
||||
local post = part({
|
||||
Name = "GantryPost",
|
||||
Size = Vector3.new(2, 24, 2),
|
||||
Position = Vector3.new(side * (ROAD_WIDTH / 2 - 2), 12, z),
|
||||
Color = Color3.fromRGB(60, 60, 70),
|
||||
Material = Enum.Material.DiamondPlate,
|
||||
})
|
||||
post.Parent = folder
|
||||
end
|
||||
local beam = part({
|
||||
Name = "GantryBeam",
|
||||
Size = Vector3.new(ROAD_WIDTH, 2.5, 2.5),
|
||||
Position = Vector3.new(0, 24, z),
|
||||
Color = g % 2 == 0 and NEON_TEAL or NEON_PINK,
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
beam.Parent = folder
|
||||
end
|
||||
end
|
||||
|
||||
-- Tunnel (segment 3): walls + ceiling with neon strips ---------------------------
|
||||
local function buildTunnel(map: Folder)
|
||||
local folder = Instance.new("Folder")
|
||||
folder.Name = "Tunnel"
|
||||
folder.Parent = map
|
||||
|
||||
local zCenter = -(3 * SEGMENT_LENGTH) - SEGMENT_LENGTH / 2
|
||||
local tunnelHeight = 22
|
||||
|
||||
for _, side in ipairs({ -1, 1 }) do
|
||||
local wall = part({
|
||||
Name = "TunnelWall_" .. (side == -1 and "L" or "R"),
|
||||
Size = Vector3.new(3, tunnelHeight, SEGMENT_LENGTH),
|
||||
Position = Vector3.new(side * (ROAD_WIDTH / 2 + 1.5), tunnelHeight / 2, zCenter),
|
||||
Color = Color3.fromRGB(38, 38, 46),
|
||||
Material = Enum.Material.Concrete,
|
||||
})
|
||||
wall.Parent = folder
|
||||
end
|
||||
|
||||
local ceiling = part({
|
||||
Name = "TunnelCeiling",
|
||||
Size = Vector3.new(ROAD_WIDTH + 6, 2, SEGMENT_LENGTH),
|
||||
Position = Vector3.new(0, tunnelHeight + 1, zCenter),
|
||||
Color = Color3.fromRGB(30, 30, 38),
|
||||
Material = Enum.Material.Concrete,
|
||||
})
|
||||
ceiling.Parent = folder
|
||||
|
||||
-- Neon ceiling strips light the tunnel (one every 50 studs, alternating).
|
||||
for i = 0, math.floor(SEGMENT_LENGTH / 50) do
|
||||
local z = -(3 * SEGMENT_LENGTH) - i * 50
|
||||
local strip = part({
|
||||
Name = "TunnelLight",
|
||||
Size = Vector3.new(ROAD_WIDTH * 0.5, 0.6, 2),
|
||||
Position = Vector3.new(0, tunnelHeight - 0.5, z),
|
||||
Color = i % 2 == 0 and NEON_BLUE or NEON_TEAL,
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
strip.Parent = folder
|
||||
end
|
||||
end
|
||||
|
||||
-- Jump ramps (segment 4): launch + landing pair on the right half ---------------
|
||||
local function buildRamps(map: Folder)
|
||||
local folder = Instance.new("Folder")
|
||||
folder.Name = "Ramps"
|
||||
folder.Parent = map
|
||||
|
||||
local zStart = 4 * SEGMENT_LENGTH + 100
|
||||
local rampX = ROAD_WIDTH / 4 -- right half of the road; left half stays flat
|
||||
|
||||
-- Launch ramp: a wedge sloping up in the direction of travel (-Z).
|
||||
local launch = Instance.new("WedgePart")
|
||||
launch.Anchored = true
|
||||
launch.Name = "RampLaunch"
|
||||
launch.Size = Vector3.new(ROAD_WIDTH / 2 - 4, 10, 48)
|
||||
-- WedgePart slopes from its -Z (tall) to +Z (thin edge); car travels -Z,
|
||||
-- so face the thin edge toward the approaching car (+Z toward car = yaw 180).
|
||||
launch.CFrame = CFrame.new(rampX, 5, -zStart) * CFrame.Angles(0, math.rad(180), 0)
|
||||
launch.Color = Color3.fromRGB(50, 50, 62)
|
||||
launch.Material = Enum.Material.Metal
|
||||
launch.TopSurface = Enum.SurfaceType.Smooth
|
||||
launch.Parent = folder
|
||||
|
||||
local edge = part({
|
||||
Name = "RampEdgeGlow",
|
||||
Size = Vector3.new(ROAD_WIDTH / 2 - 4, 0.8, 1),
|
||||
Position = Vector3.new(rampX, 10.4, -zStart - 24),
|
||||
Color = NEON_PINK,
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
edge.Parent = folder
|
||||
|
||||
-- Landing ramp: mirrored wedge further down so clean jumps land smooth.
|
||||
local landing = Instance.new("WedgePart")
|
||||
landing.Anchored = true
|
||||
landing.Name = "RampLanding"
|
||||
landing.Size = Vector3.new(ROAD_WIDTH / 2 - 4, 10, 60)
|
||||
landing.CFrame = CFrame.new(rampX, 5, -(zStart + 150))
|
||||
landing.Color = Color3.fromRGB(50, 50, 62)
|
||||
landing.Material = Enum.Material.Metal
|
||||
landing.TopSurface = Enum.SurfaceType.Smooth
|
||||
landing.Parent = folder
|
||||
end
|
||||
|
||||
-- Neon skyline: procedural building blocks flanking the route -------------------
|
||||
local function buildSkyline(map: Folder, totalLength: number)
|
||||
local folder = Instance.new("Folder")
|
||||
folder.Name = "Skyline"
|
||||
folder.Parent = map
|
||||
|
||||
local accents = { NEON_PINK, NEON_BLUE, NEON_TEAL, Color3.fromRGB(200, 120, 255) }
|
||||
|
||||
for _, side in ipairs({ -1, 1 }) do
|
||||
local n = 0
|
||||
local z = -80
|
||||
while z > -totalLength do
|
||||
n += 1
|
||||
local seed = side * 1000 + n
|
||||
local width = 40 + det(seed) * 60
|
||||
local depth = 40 + det(seed + 1) * 60
|
||||
local height = 40 + det(seed + 2) * 160
|
||||
local gap = 30 + det(seed + 3) * 70
|
||||
local x = side * (ROAD_WIDTH / 2 + 50 + det(seed + 4) * 90 + width / 2)
|
||||
|
||||
local tower = part({
|
||||
Name = "Tower",
|
||||
Size = Vector3.new(width, height, depth),
|
||||
Position = Vector3.new(x, height / 2 - 10, z - depth / 2),
|
||||
Color = Color3.fromRGB(16, 16, 24),
|
||||
Material = Enum.Material.Slate,
|
||||
})
|
||||
tower.Parent = folder
|
||||
|
||||
-- Lit window band + rooftop accent (2 parts per tower, cheap).
|
||||
local accent = accents[(n % #accents) + 1]
|
||||
local band = part({
|
||||
Name = "TowerBand",
|
||||
Size = Vector3.new(width + 0.4, 2 + det(seed + 5) * 4, depth + 0.4),
|
||||
Position = Vector3.new(x, height * (0.4 + det(seed + 6) * 0.4), z - depth / 2),
|
||||
Color = accent,
|
||||
Material = Enum.Material.Neon,
|
||||
Transparency = 0.2,
|
||||
})
|
||||
band.Parent = folder
|
||||
|
||||
local roof = part({
|
||||
Name = "TowerRoof",
|
||||
Size = Vector3.new(width * 0.3, 1.5, depth * 0.3),
|
||||
Position = Vector3.new(x, height - 9, z - depth / 2),
|
||||
Color = accent,
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
roof.Parent = folder
|
||||
|
||||
z = z - depth - gap
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function MapBuilder.build(): Folder
|
||||
local existing = Workspace:FindFirstChild("Map")
|
||||
if existing then
|
||||
@@ -45,63 +294,12 @@ function MapBuilder.build(): Folder
|
||||
|
||||
local totalLength = SEGMENT_LENGTH * NUM_SEGMENTS
|
||||
|
||||
-- Road deck (segmented so you can later bank/curve individual pieces) ------
|
||||
for i = 0, NUM_SEGMENTS - 1 do
|
||||
local z = -(i * SEGMENT_LENGTH) - SEGMENT_LENGTH / 2
|
||||
local road = part({
|
||||
Name = "Road_" .. i,
|
||||
Size = Vector3.new(ROAD_WIDTH, 2, SEGMENT_LENGTH),
|
||||
Position = Vector3.new(0, 0, z),
|
||||
Color = Color3.fromRGB(28, 28, 34),
|
||||
Material = Enum.Material.Asphalt,
|
||||
})
|
||||
road.Parent = roadFolder
|
||||
|
||||
-- Center lane line
|
||||
local line = part({
|
||||
Name = "Lane_" .. i,
|
||||
Size = Vector3.new(1, 2.1, SEGMENT_LENGTH),
|
||||
Position = Vector3.new(0, 0.1, z),
|
||||
Color = Color3.fromRGB(230, 200, 60),
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
line.Parent = roadFolder
|
||||
end
|
||||
|
||||
-- Guardrails + invisible kill walls on both sides -------------------------
|
||||
for _, side in ipairs({ -1, 1 }) do
|
||||
local x = side * (ROAD_WIDTH / 2)
|
||||
local rail = part({
|
||||
Name = "Guardrail_" .. (side == -1 and "L" or "R"),
|
||||
Size = Vector3.new(1.5, 4, totalLength),
|
||||
Position = Vector3.new(x, 2, -totalLength / 2),
|
||||
Color = Color3.fromRGB(180, 180, 190),
|
||||
Material = Enum.Material.Metal,
|
||||
})
|
||||
rail.Parent = map
|
||||
|
||||
-- Invisible wall: solid, tall, non-rendered — the "can't leave" barrier.
|
||||
local wall = part({
|
||||
Name = "KillWall_" .. (side == -1 and "L" or "R"),
|
||||
Size = Vector3.new(2, WALL_HEIGHT, totalLength),
|
||||
Position = Vector3.new(x + side * 2, WALL_HEIGHT / 2, -totalLength / 2),
|
||||
Transparency = 1,
|
||||
CanCollide = true,
|
||||
})
|
||||
wall.Parent = map
|
||||
|
||||
-- Neon pylons for that cyberpunk Tokyo glow (cheap: one every 200 studs)
|
||||
for i = 0, NUM_SEGMENTS * 2 do
|
||||
local pylon = part({
|
||||
Name = "Pylon",
|
||||
Size = Vector3.new(1, 14, 1),
|
||||
Position = Vector3.new(x + side * 3, 7, -(i * 200)),
|
||||
Color = side == -1 and Color3.fromRGB(255, 40, 120) or Color3.fromRGB(40, 160, 255),
|
||||
Material = Enum.Material.Neon,
|
||||
})
|
||||
pylon.Parent = map
|
||||
end
|
||||
end
|
||||
buildRoad(roadFolder)
|
||||
buildBarriers(map, totalLength)
|
||||
buildGantries(map)
|
||||
buildTunnel(map)
|
||||
buildRamps(map)
|
||||
buildSkyline(map, totalLength)
|
||||
|
||||
-- Spawn pad (where cars appear) at the start ------------------------------
|
||||
local pad = part({
|
||||
@@ -125,6 +323,16 @@ function MapBuilder.build(): Folder
|
||||
})
|
||||
finish.Parent = map
|
||||
|
||||
-- End wall so you can't sail off the far edge ------------------------------
|
||||
local endWall = part({
|
||||
Name = "KillWall_End",
|
||||
Size = Vector3.new(ROAD_WIDTH + 8, WALL_HEIGHT, 2),
|
||||
Position = Vector3.new(0, WALL_HEIGHT / 2, -totalLength - 1),
|
||||
Transparency = 1,
|
||||
CanCollide = true,
|
||||
})
|
||||
endWall.Parent = map
|
||||
|
||||
-- A floor far below to catch anything that clips through (safety net) ------
|
||||
local floor = part({
|
||||
Name = "SafetyFloor",
|
||||
|
||||
Reference in New Issue
Block a user