Menu
CoddyTech

질주

Coddy의 Roblox 게임: 어둠에서 탈출하기 코스 레슨. 20개 중 17번째.

때로는 달려야 할 때가 있습니다. Shift 키(또는 휴대폰 버튼)를 누르고 있으면 스태미나가 있는 동안 Settings.SprintSpeed의 속도로 전력 질주합니다. 전력 질주하는 동안 스태미나가 줄어들고, 걸을 때 회복됩니다. 스태미나가 바닥나면 다시 누를 때까지 전력 질주가 멈춥니다.

이 기능은 전부 클라이언트에서 처리합니다. 플레이어 자신의 캐릭터는 그 플레이어의 소유입니다. Roblox에서는 LocalScript가 Humanoid의 WalkSpeed를 설정할 수 있고, 움직임은 그에 따라 바뀝니다. ContextActionService는 키를 누를 때(Begin)와 뗄 때(End) 스크립트에 알려 줍니다.

ContextActionService:BindAction("Sprint", function(_, state)
	sprinting = state == Enum.UserInputState.Begin
end, true, Enum.KeyCode.LeftShift)

매 프레임마다 dt는 이전 프레임 이후 지난 시간(초)이므로, 스태미나는 변화율에 dt를 곱한 만큼 변합니다.

challenge icon

챌린지

어려움

DarkClient에서 ContextActionService를 가져오고, sprinting과 stamina(초깃값은 Settings.StaminaMax)를 유지합니다. 터치 버튼과 함께 LeftShift에 "Sprint"를 바인딩합니다. Heartbeat에서는 스태미나가 남아 있는 동안 질주하면 Settings.StaminaDrain * dt만큼 소모하고 WalkSpeed를 Settings.SprintSpeed로 설정합니다(스태미나가 0이 되면 질주를 중지합니다). 그렇지 않으면 최댓값까지 Settings.StaminaRegen * dt만큼 회복하고 WalkSpeed를 Settings.WalkSpeed로 설정합니다. 스태미나 값에 맞게 hud.Stamina.Fill의 크기를 설정합니다.

플레이를 누르고 Shift 키를 길게 누릅니다. 그런 다음 확인을 누릅니다.

직접 해보기

local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local Lighting = game:GetService("Lighting")
local TweenService = game:GetService("TweenService")
local PathfindingService = game:GetService("PathfindingService")
local Settings = require(game.ReplicatedStorage.Settings)

local remotes = game.ReplicatedStorage.Remotes
local rooms = workspace.Rooms
local lastRoom = rooms.Lobby -- 가장 최근에 생성된 방
local count = 0 -- 지금까지 생성된 방의 수
local monster = nil
local codes = {} -- 금고 문 -> 해당 문의 코드
local lastTry = {} -- 플레이어 -> 마지막으로 코드를 입력한 시각

-- 밤이 찾아온다: 태양은 사라지고, 사방에 희미한 빛과 안개가 깔린다
Lighting.ClockTime = Settings.ClockTime
Lighting.Ambient = Settings.Ambient
Lighting.OutdoorAmbient = Settings.Ambient
Lighting.FogColor = Color3.new(0, 0, 0)
Lighting.FogEnd = Settings.FogEnd
print("Lights out")

-- 플레이어가 지닌 도구: 손에 들고 있거나 Backpack에 들어 있다
local function findTool(player, name)
	return (player.Character and player.Character:FindFirstChild(name)) or player.Backpack:FindFirstChild(name)
end

-- 손전등은 켜져 있는 동안 배터리가 줄어들고, 다 닳으면 꺼진다
task.spawn(function()
	while true do
		task.wait(0.25)
		for _, player in Players:GetPlayers() do
			local flashlight = player.Character and player.Character:FindFirstChild("Flashlight")
			if flashlight and flashlight.Handle.Beam.Enabled then
				local battery = math.max(0, flashlight:GetAttribute("Battery") - Settings.BatteryDrain * 0.25)
				flashlight:SetAttribute("Battery", battery)
				if battery == 0 then
					flashlight.Handle.Beam.Enabled = false
				end
			end
		end
	end
end)

-- 경첩을 축으로 문을 돌려 연다
local function openDoor(door)
	door:SetAttribute("Open", true)
	door.Panel.Prompt.Enabled = false
	local hinge, panel = door.Hinge, door.Panel
	local goal = hinge.CFrame * CFrame.Angles(0, math.rad(90), 0) * hinge.CFrame:ToObjectSpace(panel.CFrame)
	TweenService:Create(panel, TweenInfo.new(0.6), { CFrame = goal }):Play()
end

-- MonsterSight 범위 안에서 잡을 수 있는 가장 가까운 살아 있는 플레이어(숨어 있지 않은 플레이어)와 그 거리
local function nearestCharacter(position)
	local best, bestDistance = nil, Settings.MonsterSight
	for _, player in Players:GetPlayers() do
		local character = player.Character
		local root = character and character:FindFirstChild("HumanoidRootPart")
		if root and character.Humanoid.Health > 0 and not character:GetAttribute("Hidden") then
			local distance = (root.Position - position).Magnitude
			if distance < bestDistance then
				best, bestDistance = character, distance
			end
		end
	end
	return best, bestDistance
end

-- 괴물이 이 캐릭터를 볼 수 있는가? 머리에서 쏜 광선이 캐릭터에 닿는지 확인한다
local function canSee(shadow, character)
	local from = shadow.Head.Position
	local params = RaycastParams.new()
	params.FilterDescendantsInstances = { shadow }
	local hit = workspace:Raycast(from, character.HumanoidRootPart.Position - from, params)
	return hit ~= nil and hit.Instance:IsDescendantOf(character)
end

-- 지점을 향해 이동한다: 길이 트여 있으면 직진하고, 아니면 경로를 따라간다
local function walkToward(shadow, goal, path)
	local root = shadow.HumanoidRootPart
	local params = RaycastParams.new()
	params.FilterDescendantsInstances = { shadow }
	local hit = workspace:Raycast(root.Position, goal - root.Position, params)
	if not hit or hit.Instance.Parent:FindFirstChildOfClass("Humanoid") then
		shadow.Humanoid:MoveTo(goal)
		return
	end
	path:ComputeAsync(root.Position, goal)
	local points = path:GetWaypoints()
	if path.Status == Enum.PathStatus.Success and points[2] then
		shadow.Humanoid:MoveTo(points[2].Position)
	end
end

-- 플레이어들이 있는 방보다 `back`개의 방만큼 앞선 방의 중앙(그 방은
-- 가장 최근 방의 앞 방), 몬스터의 순찰을 위해
local function patrolPoint(back)
	for _, room in rooms:GetChildren() do
		if room:GetAttribute("Number") == count - 1 - back then
			return (room.Entrance.CFrame * CFrame.new(0, 3.1, -12)).Position
		end
	end
	return nil
end

-- 몬스터의 행동: 0.25초마다 누군가를 쫓는다
local function runMonster(shadow)
	local humanoid = shadow.Humanoid
	local root = shadow.HumanoidRootPart
	local path = PathfindingService:CreatePath()
	local lastSeen = nil -- 마지막으로 누군가를 본 위치
	local back = 0 -- 순찰 경로: 0은 플레이어들이 있는 방, 2는 두 방 뒤
	while shadow.Parent do
		local character = nearestCharacter(root.Position)
		if character and canSee(shadow, character) then
			lastSeen = character.HumanoidRootPart.Position
		else
			character = nil
		end
		if character and (character.HumanoidRootPart.Position - root.Position).Magnitude <= Settings.CatchDistance then
			-- 붙잡음: 플레이어가 쓰러지고, 몬스터는 다음 문까지 이동한다
			character.Humanoid.Health = 0
			shadow:Destroy()
			monster = nil
			return
		end
		local goal = character and character.HumanoidRootPart.Position or lastSeen
		if goal then
			walkToward(shadow, goal, path)
			-- 마지막으로 플레이어를 본 곳에 도착했는데 플레이어가 없으면 추적을 포기한다
			if not character and ((goal - root.Position) * Vector3.new(1, 0, 1)).Magnitude < 1.5 then
				lastSeen = nil
			end
		else
			-- 아무도 보이지 않으면 앞뒤로 순찰한다
			local point = patrolPoint(back) or patrolPoint(0)
			if point then
				walkToward(shadow, point, path)
				if ((point - root.Position) * Vector3.new(1, 0, 1)).Magnitude < 2 then
					back = back == 0 and 2 or 0
				end
			end
		end
		task.wait(0.25)
	end
end

-- 몬스터가 플레이어 뒤에서 나타난다: 가장 최근 방보다 두 방 앞의 입구에서
local function spawnMonster()
	local behind = nil
	for _, room in rooms:GetChildren() do
		if room:GetAttribute("Number") == count - 2 then
			behind = room
		end
	end
	if not behind then
		return
	end
	monster = ServerStorage.Shadow:Clone()
	monster:PivotTo(behind.Entrance.CFrame + Vector3.new(0, 3.1, 0))
	monster.Humanoid.WalkSpeed = Settings.MonsterSpeed
	monster.Parent = workspace
	task.spawn(runMonster, monster)
end

local addRoom -- 아래에 작성됨: 문은 방을 추가하고, 방에는 문이 있다

-- 플레이어가 문을 열 때 실행되는 동작
local function setupDoor(door)
	door.Panel.Prompt.Triggered:Connect(function(player)
		if door:GetAttribute("Open") then
			return
		end
		if door:GetAttribute("Vault") and door:GetAttribute("Locked") then
			return -- 금고는 키패드에 코드를 입력하면 열린다
		end
		if door:GetAttribute("Locked") then
			local key = findTool(player, "Key")
			if not key then
				return
			end
			key:Destroy()
			door:SetAttribute("Locked", nil)
		end
		if door.Parent == lastRoom then
			addRoom()
		end
		openDoor(door)
	end)
end
setupDoor(workspace.Rooms.Lobby.Door)

-- 방에 들어가면 그 방이 플레이어의 Room이자 체크포인트가 된다
local function setupEntry(room)
	room.Entry.Touched:Connect(function(hit)
		local player = Players:GetPlayerFromCharacter(hit.Parent)
		if player then
			local number = room:GetAttribute("Number")
			player.leaderstats.Room.Value = number
			player.RespawnLocation = room.Checkpoint
		end
	end)
end
setupEntry(workspace.Rooms.Lobby)

-- 옷장: E를 누르면 안에 숨고, 다시 E를 누르면 나온다
local function setupCloset(closet)
	closet.Front.Prompt.Triggered:Connect(function(player)
		local character = player.Character
		if character:GetAttribute("Hidden") then
			character:PivotTo(closet.Front.CFrame * CFrame.new(-3, -1, 0) * CFrame.Angles(0, math.rad(90), 0))
			character:SetAttribute("Hidden", nil)
		else
			character:PivotTo(closet.Inside.CFrame + Vector3.new(0, 2.5, 0))
			character:SetAttribute("Hidden", true)
		end
	end)
end

-- 가장 최근 방의 출구에 새 방 생성
function addRoom()
	count += 1
	local name = Settings.Order[(count - 1) % #Settings.Order + 1]
	if count >= Settings.RoomsToEscape then
		name = "Exit"
	end
	local room = ServerStorage.Rooms[name]:Clone()
	room:PivotTo(lastRoom.Exit.CFrame)
	room:SetAttribute("Number", count)
	room.Sign.SurfaceGui.Number.Text = "Room " .. count
	room.Parent = rooms
	lastRoom = room
	setupDoor(room.Door)
	setupEntry(room)
	-- 잠긴 방의 열쇠는 그 방 안에 있다
	local keySpot = room:FindFirstChild("KeySpot")
	if keySpot then
		local key = ServerStorage.Key:Clone()
		key.Handle.CFrame = keySpot.CFrame
		key.Parent = room
		key.Handle.Prompt.Triggered:Connect(function(player)
			key.Handle.Prompt.Enabled = false
			key.Parent = player.Backpack
		end)
	end
	local closet = room:FindFirstChild("Closet")
	if closet then
		setupCloset(closet)
	end
	-- 일부 방에는 배터리가 있다
	local batterySpot = room:FindFirstChild("BatterySpot")
	if batterySpot then
		local battery = ServerStorage.Battery:Clone()
		battery.CFrame = batterySpot.CFrame
		battery.Parent = room
		battery.Prompt.Triggered:Connect(function(player)
			local flashlight = findTool(player, "Flashlight")
			if flashlight and battery.Parent then
				flashlight:SetAttribute("Battery", math.min(Settings.BatteryMax, flashlight:GetAttribute("Battery") + Settings.BatteryRefill))
				battery:Destroy()
			end
		end)
	end
	-- 금고의 코드는 금고 안의 쪽지에 적혀 있다
	if room.Door:GetAttribute("Vault") then
		local code = tostring(math.random(1000, 9999))
		codes[room.Door] = code
		room.Note.SurfaceGui.Code.Text = "Code " .. code
	end
	-- 가장 최근 방과 그 뒤의 KeepRooms개 방을 유지한다
	for _, old in rooms:GetChildren() do
		if old:GetAttribute("Number") < count - Settings.KeepRooms then
			old:Destroy()
		end
	end
	if count >= Settings.MonsterFrom and not monster then
		spawnMonster()
	end
	return room
end

-- 옷장에서 나오면 더 이상 숨어 있지 않다
task.spawn(function()
	while true do
		task.wait(0.25)
		for _, player in Players:GetPlayers() do
			local character = player.Character
			if character and character:GetAttribute("Hidden") then
				local closest = math.huge
				for _, inside in workspace.Rooms:GetDescendants() do
					if inside.Name == "Inside" then
						closest = math.min(closest, (inside.Position - character.HumanoidRootPart.Position).Magnitude)
					end
				end
				if closest > 4 then
					character:SetAttribute("Hidden", nil)
				end
			end
		end
	end
end)

-- 금고 키패드에 입력한 코드. 서버가 확인하는 조건: 근처에 실제 금고가 있고
-- 플레이어가 가까이 있으며, CodeWait마다 한 번만 시도하고, 코드가 맞아야 한다
remotes.TryCode.OnServerInvoke = function(player, door, code)
	if typeof(door) ~= "Instance" or not codes[door] or typeof(code) ~= "string" then
		return false
	end
	local root = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
	if not root or (root.Position - door.Panel.Position).Magnitude > 12 then
		return false
	end
	local now = time()
	if lastTry[player] and now - lastTry[player] < Settings.CodeWait then
		return false
	end
	lastTry[player] = now
	if code ~= codes[door] then
		return false
	end
	codes[door] = nil
	door:SetAttribute("Locked", nil)
	if door.Parent == lastRoom then
		addRoom()
	end
	openDoor(door)
	return true
end

-- 모든 플레이어: 리더보드에 Room을 표시하고, 부활할 때마다 손전등을 지급한다
Players.PlayerAdded:Connect(function(player)
	local leaderstats = Instance.new("Folder")
	leaderstats.Name = "leaderstats"
	leaderstats.Parent = player

	local room = Instance.new("IntValue")
	room.Name = "Room"
	room.Parent = leaderstats

	player.CharacterAdded:Connect(function()
		local flashlight = ServerStorage.Flashlight:Clone()
		flashlight:SetAttribute("Battery", Settings.BatteryMax)
		flashlight.Parent = player.Backpack
		local beam = flashlight.Handle.Beam
		flashlight.Activated:Connect(function()
			if beam.Enabled then
				beam.Enabled = false
			elseif flashlight:GetAttribute("Battery") > 0 then
				beam.Enabled = true
			end
		end)
	end)
end)
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

Roblox 게임: 어둠에서 탈출하기의 모든 레슨