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:
+7
-2
@@ -21,14 +21,19 @@ to owner + son. Cars persist between sessions. CI green on a self-hosted runner.
|
|||||||
(driver owns network physics). **Needs a Studio playtest to tune feel.**
|
(driver owns network physics). **Needs a Studio playtest to tune feel.**
|
||||||
- [ ] Import the **real RX-7 mesh**; weld the shell onto the chassis `Body`.
|
- [ ] Import the **real RX-7 mesh**; weld the shell onto the chassis `Body`.
|
||||||
See `docs/MESH_SWAP.md` for the exact steps (needs Studio for asset upload).
|
See `docs/MESH_SWAP.md` for the exact steps (needs Studio for asset upload).
|
||||||
- [ ] Tune acceleration curve, grip, and the chase camera (after first playtest).
|
- [ ] Tune acceleration curve and grip (after first playtest).
|
||||||
|
- [x] **Chase camera**: scriptable follow cam while seated — damped chase with
|
||||||
|
look-ahead, flat horizon (ignores car pitch/roll), speed-based FOV kick.
|
||||||
|
`src/client/CameraController.luau`. Tune constants after playtest.
|
||||||
- [ ] Sound: engine loop, tire screech, boost.
|
- [ ] Sound: engine loop, tire screech, boost.
|
||||||
**Exit test:** the RX-7 is genuinely fun to just drive around. Ship nothing else
|
**Exit test:** the RX-7 is genuinely fun to just drive around. Ship nothing else
|
||||||
until this is true.
|
until this is true.
|
||||||
|
|
||||||
## M2 — More cars + polish (still no racing)
|
## M2 — More cars + polish (still no racing)
|
||||||
- [ ] Import a few more real car meshes.
|
- [ ] Import a few more real car meshes.
|
||||||
- [ ] Better map: more expressway sections, ramps, tunnels, scenery.
|
- [x] Better map v2: 3200-stud expressway with gantry arches, a neon-lit
|
||||||
|
tunnel, jump ramps (right lane; left stays flat), end wall, and a
|
||||||
|
procedural neon skyline flanking the route.
|
||||||
- [ ] Day/night, weather mood.
|
- [ ] Day/night, weather mood.
|
||||||
|
|
||||||
## M3 — Racing (the first "game" layer)
|
## M3 — Racing (the first "game" layer)
|
||||||
|
|||||||
@@ -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 Remotes = require(Shared.Remotes)
|
||||||
local Cars = require(Shared.Cars)
|
local Cars = require(Shared.Cars)
|
||||||
local ChassisController = require(script.ChassisController)
|
local ChassisController = require(script.ChassisController)
|
||||||
|
local CameraController = require(script.CameraController)
|
||||||
|
|
||||||
ChassisController.init()
|
ChassisController.init()
|
||||||
|
CameraController.init()
|
||||||
|
|
||||||
local player = Players.LocalPlayer
|
local player = Players.LocalPlayer
|
||||||
local playerGui = player:WaitForChild("PlayerGui")
|
local playerGui = player:WaitForChild("PlayerGui")
|
||||||
|
|||||||
+273
-65
@@ -1,13 +1,22 @@
|
|||||||
--!strict
|
--!strict
|
||||||
--[[
|
--[[
|
||||||
MapBuilder.luau (SERVER)
|
MapBuilder.luau (SERVER)
|
||||||
Builds the MVP "Tokyo expressway" vertical slice entirely from code so the
|
Builds the "Tokyo expressway" map entirely from code — ZERO manual Studio
|
||||||
game is playable with ZERO manual Studio building. This is a straight,
|
building. v2 (M2 groundwork): the straight highway now runs through a
|
||||||
banked-guardrail highway segment with a start pad, a finish line, neon-ish
|
sequence of FEATURES so driving it is actually interesting:
|
||||||
side pylons, and invisible kill walls so you can't drive off the edge.
|
|
||||||
|
|
||||||
Later you replace this with a proper modeled map; the rest of the game
|
seg 0-1 open start (spawn pad)
|
||||||
(spawns, races) reads named parts so it keeps working.
|
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")
|
local Workspace = game:GetService("Workspace")
|
||||||
@@ -16,8 +25,12 @@ local MapBuilder = {}
|
|||||||
|
|
||||||
local ROAD_WIDTH = 60
|
local ROAD_WIDTH = 60
|
||||||
local SEGMENT_LENGTH = 400
|
local SEGMENT_LENGTH = 400
|
||||||
local NUM_SEGMENTS = 6 -- total length = 2400 studs of highway
|
local NUM_SEGMENTS = 8 -- total length = 3200 studs of highway
|
||||||
local WALL_HEIGHT = 20
|
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 function part(props: { [string]: any }): Part
|
||||||
local p = Instance.new("Part")
|
local p = Instance.new("Part")
|
||||||
@@ -30,6 +43,242 @@ local function part(props: { [string]: any }): Part
|
|||||||
return p
|
return p
|
||||||
end
|
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
|
function MapBuilder.build(): Folder
|
||||||
local existing = Workspace:FindFirstChild("Map")
|
local existing = Workspace:FindFirstChild("Map")
|
||||||
if existing then
|
if existing then
|
||||||
@@ -45,63 +294,12 @@ function MapBuilder.build(): Folder
|
|||||||
|
|
||||||
local totalLength = SEGMENT_LENGTH * NUM_SEGMENTS
|
local totalLength = SEGMENT_LENGTH * NUM_SEGMENTS
|
||||||
|
|
||||||
-- Road deck (segmented so you can later bank/curve individual pieces) ------
|
buildRoad(roadFolder)
|
||||||
for i = 0, NUM_SEGMENTS - 1 do
|
buildBarriers(map, totalLength)
|
||||||
local z = -(i * SEGMENT_LENGTH) - SEGMENT_LENGTH / 2
|
buildGantries(map)
|
||||||
local road = part({
|
buildTunnel(map)
|
||||||
Name = "Road_" .. i,
|
buildRamps(map)
|
||||||
Size = Vector3.new(ROAD_WIDTH, 2, SEGMENT_LENGTH),
|
buildSkyline(map, totalLength)
|
||||||
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
|
|
||||||
|
|
||||||
-- Spawn pad (where cars appear) at the start ------------------------------
|
-- Spawn pad (where cars appear) at the start ------------------------------
|
||||||
local pad = part({
|
local pad = part({
|
||||||
@@ -125,6 +323,16 @@ function MapBuilder.build(): Folder
|
|||||||
})
|
})
|
||||||
finish.Parent = map
|
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) ------
|
-- A floor far below to catch anything that clips through (safety net) ------
|
||||||
local floor = part({
|
local floor = part({
|
||||||
Name = "SafetyFloor",
|
Name = "SafetyFloor",
|
||||||
|
|||||||
Reference in New Issue
Block a user