Roblox Studio · 第 29 课

综合项目:完成能量岛寻宝游戏

把晶石收集、任务进度、终点和界面接起来,做一个有开始、有目标、有结果的小游戏。

10–15 岁 90–120 分钟 综合项目 / 游戏闭环 动手实践 测试与保存

Course overview

把分散的技能连起来,做成一款有目标的能量岛小游戏。

你的目标很清楚:从营地出发,找到三颗不同晶石,再回到终端完成任务。屏幕会告诉你进度,终端会检查你是否真的集齐。

这节课从全新的 Baseplate 开始,使用下面列出的对象和两份完整脚本。前面的知识会帮助你读懂它,但不需要把二十多课的旧脚本全部搬进来。

01 · BEFORE

开始之前

掌握第 10、13、15–19 课,能读懂服务器与客户端分工。建议分两次完成。

02 · MAKE

你的作品

可双人独立游玩的寻宝游戏:收集、进度、终点校验与完成提示完整连接。

03 · CHECK

完成标志

没集齐不能完成;重复晶石不计数;两位玩家进度独立;完成奖励只发一次。

能量岛启动画面,任务面板显示晶石 0/3,场景中摆放三颗晶石
图 29-1 · Studio 实拍:两份脚本运行后的起点画面。左上角显示晶石 0/3 和目标,场景里的三颗晶石等待收集;这张图展示启动状态,不是通关画面。

Learning route

先准备,再动手;看到结果后,记得保存。

每完成一小步,就测试一次。先说出你预计会看到什么,再用 Play 和 Output 检查,不用急着一次写完所有代码。

1

01 · 先确定最小可玩的规则

第一版只做三颗晶石和一个终端。没有倒计时、商店和云端存档,避免多个系统一起出错。先走完一轮,再选择一项扩展。

2

02 · 按对象清单搭建场景

严格一致的对象名字,是把场景和代码接起来的第一步。你可以改变颜色和装饰,但先不要改变脚本使用的名称与层级。

3

03 · 服务器记录进度,并检查终点

IslandCount 是晶石数,IslandFinished 表示是否已完成,IslandMessage 给界面提供提示。collected 表记录具体领过哪些晶石,防止重复触碰刷进度。

4

04 · 让玩家看见自己的进度

HUD 只读取服务器状态,不发奖也不判定任务完成。初始化时先监听变化,再 render 一次,以免界面打开时已经错过早先的状态。

5

05 · 按验收表完整走一遍

“通关了一次”还不足以证明任务可靠。你也要尝试没集齐、重复领取和重复提交,确认规则在这些情况下仍成立。

6

06 · 一次只加入一个扩展

先保存 v01,再挑一项扩展:更清楚的路标、完成粒子或温和夜景。不要同时加入计时、存档和商店。

Step by step

01 · 先确定最小可玩的规则

第一版只做三颗晶石和一个终端。没有倒计时、商店和云端存档,避免多个系统一起出错。先走完一轮,再选择一项扩展。

本次会话中,角色重生保留已收集进度;离开后重置。完成时得到一枚本次游戏奖章,重复使用终端不会继续增加。

  1. 新建并保存“姓名缩写_Lesson29_v01”。
  2. 写下游戏规则:三颗不同晶石 → 终端确认 → 得到一次奖章。
  3. 保留 Baseplate 作为安全地面,用低平台和路标做岛屿主题。

Step by step

02 · 按对象清单搭建场景

严格一致的对象名字,是把场景和代码接起来的第一步。你可以改变颜色和装饰,但先不要改变脚本使用的名称与层级。

把晶石排在能看见的地方,测试成功后再藏到路线边上。过早隐藏目标会让你分不清是玩法太难还是代码没响应。

  1. Workspace 下创建 Crystals 文件夹,内含 CrystalA、CrystalB、CrystalC 三个 Part。
  2. 晶石 Size=3,3,3;位置分别为 -12,3,-12;0,3,-26;12,3,-12;Anchored=true、CanCollide=false、CanTouch=true。
  3. 创建 Terminal:Size=6,4,4,Position=0,2,-40,Anchored=true。
  4. 在 Terminal 内创建 ProximityPrompt,ActionText=完成任务、ObjectText=能量终端、HoldDuration=0.5、MaxActivationDistance=10。
  5. 建立 ServerScriptService > IslandQuest(Script)和 StarterPlayerScripts > IslandHud(LocalScript)。
必须一致的 Explorer 结构 · 不是 Luau 代码Explorer
Workspace
├─ Crystals (Folder)
│  ├─ CrystalA (Part)
│  ├─ CrystalB (Part)
│  └─ CrystalC (Part)
└─ Terminal (Part)
   └─ ProximityPrompt
ServerScriptService
└─ IslandQuest (Script)
StarterPlayer > StarterPlayerScripts
└─ IslandHud (LocalScript)

Step by step

03 · 服务器记录进度,并检查终点

IslandCount 是晶石数,IslandFinished 表示是否已完成,IslandMessage 给界面提供提示。collected 表记录具体领过哪些晶石,防止重复触碰刷进度。

终端只读服务器自己的 count,不接收客户端上报的“我集齐了”。状态检查、距离检查和一次性标记都通过后,才增加奖章。

  1. 把完整代码放进 IslandQuest。
  2. 先 Play 并用 Output 或 Player 属性验证数量,再接入 HUD。
  3. 本项目只保留这一份任务服务器脚本,不叠加第 19 课的 CrystalQuest。
ServerScriptService > IslandQuest · 完整服务器脚本Luau
local Players = game:GetService("Players")
local crystals = workspace:WaitForChild("Crystals")
local terminal = workspace:WaitForChild("Terminal")
local prompt = terminal:WaitForChild("ProximityPrompt")
local TARGET = 3
local collected, lastUse = {}, {}

local function setup(player)
    if collected[player] then return end
    collected[player] = {}
    player:SetAttribute("IslandCount", 0)
    player:SetAttribute("IslandFinished", false)
    player:SetAttribute("IslandMessage", "找到 3 颗不同晶石,再前往能量终端。")
    local stats = Instance.new("Folder")
    stats.Name = "leaderstats"
    local medals = Instance.new("IntValue")
    medals.Name = "Medals"
    medals.Value = 0
    medals.Parent = stats
    stats.Parent = player
end
local function near(player, part, distance)
    local character = player.Character
    local root = character and character:FindFirstChild("HumanoidRootPart")
    local humanoid = character and character:FindFirstChildOfClass("Humanoid")
    return root and humanoid and humanoid.Health > 0
        and (root.Position - part.Position).Magnitude <= distance
end
Players.PlayerAdded:Connect(setup)
for _, player in ipairs(Players:GetPlayers()) do setup(player) end
Players.PlayerRemoving:Connect(function(player)
    collected[player], lastUse[player] = nil, nil
end)

for _, crystal in ipairs(crystals:GetChildren()) do
    if not crystal:IsA("BasePart") then continue end
    crystal.Touched:Connect(function(hit)
        local character = hit:FindFirstAncestorOfClass("Model")
        local player = character and Players:GetPlayerFromCharacter(character)
        if not player or not near(player, crystal, 8) then return end
        local record = collected[player]
        if not record or record[crystal] or player:GetAttribute("IslandFinished") then return end
        record[crystal] = true
        local count = math.min(TARGET, (player:GetAttribute("IslandCount") or 0) + 1)
        player:SetAttribute("IslandCount", count)
        if count == TARGET then
            player:SetAttribute("IslandMessage", "集齐啦!前往能量终端,按提示完成任务。")
        else
            player:SetAttribute("IslandMessage", "找到 " .. crystal.Name .. ",继续寻找其他晶石。")
        end
    end)
end

prompt.Triggered:Connect(function(player)
    if not collected[player] or not near(player, terminal, 12) then return end
    local now = os.clock()
    if now - (lastUse[player] or -math.huge) < 0.5 then return end
    lastUse[player] = now
    if player:GetAttribute("IslandFinished") then return end
    local count = player:GetAttribute("IslandCount") or 0
    if count < TARGET then
        player:SetAttribute("IslandMessage", "还差 " .. (TARGET - count) .. " 颗不同晶石。")
        return
    end
    player:SetAttribute("IslandFinished", true)
    player.leaderstats.Medals.Value += 1
    player:SetAttribute("IslandMessage", "任务完成!你获得了一枚探险奖章。")
    print(player.Name, "finished Energy Island")
end)

Step by step

04 · 让玩家看见自己的进度

HUD 只读取服务器状态,不发奖也不判定任务完成。初始化时先监听变化,再 render 一次,以免界面打开时已经错过早先的状态。

面板宽度取屏幕 84%,再限制最大宽度;文字保持固定字号并允许换行。这样比把一长串提示硬塞进一个很小的框更容易读。

  1. 把完整代码放入 IslandHud。
  2. Play 后应该看到“晶石 0/3”和任务说明。
  3. 收集与使用终端时,确认文字跟随状态变化。
StarterPlayer > StarterPlayerScripts > IslandHud · 完整客户端脚本Luau
local player = game:GetService("Players").LocalPlayer
local gui = Instance.new("ScreenGui")
gui.Name = "IslandGui"
gui.ResetOnSpawn = false
local panel = Instance.new("Frame")
panel.Position = UDim2.fromOffset(16, 16)
panel.Size = UDim2.new(0.84, 0, 0, 132)
panel.BackgroundColor3 = Color3.fromRGB(18, 35, 55)
panel.Parent = gui
local limit = Instance.new("UISizeConstraint")
limit.MaxSize = Vector2.new(420, 132)
limit.Parent = panel
local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 12)
corner.Parent = panel
local label = Instance.new("TextLabel")
label.Position = UDim2.fromOffset(12, 10)
label.Size = UDim2.new(1, -24, 1, -20)
label.BackgroundTransparency = 1
label.TextColor3 = Color3.fromRGB(240, 247, 255)
label.TextSize = 20
label.TextWrapped = true
label.TextXAlignment = Enum.TextXAlignment.Left
label.Parent = panel
gui.Parent = player:WaitForChild("PlayerGui")

local function render()
    local count = player:GetAttribute("IslandCount")
    local finished = player:GetAttribute("IslandFinished")
    local message = player:GetAttribute("IslandMessage") or "等待任务数据…"
    local heading = finished and "探险完成!" or "能量岛探险"
    label.Text = heading .. "\n晶石 " .. tostring(count or 0) .. "/3\n" .. message
end
for _, name in ipairs({"IslandCount", "IslandFinished", "IslandMessage"}) do
    player:GetAttributeChangedSignal(name):Connect(render)
end
render()

Step by step

05 · 按验收表完整走一遍

“通关了一次”还不足以证明任务可靠。你也要尝试没集齐、重复领取和重复提交,确认规则在这些情况下仍成立。

两名玩家可以共用相同晶石,但每个人各有进度。晶石不会全局消失,这是这份项目有意选择的规则。

  1. 0 颗就使用终端:提示还差 3,Medals=0。
  2. A、A、B、C:进度依次 1、1、2、3。
  3. 集齐再提交:Medals=1;重复提交仍为 1。
  4. 中途重生:进度保留;Stop 重开:进度为 0。
  5. 双人分别完成:双方各得 1,不互相影响。
综合项目双人验收使用的测试入口
图 29-2:官方双人测试入口:按验收表检查双方进度独立,以及重复提交不会再次发奖。Roblox Creator Documentation · CC BY 4.0 · 原图未修改 · 许可说明

Step by step

06 · 一次只加入一个扩展

先保存 v01,再挑一项扩展:更清楚的路标、完成粒子或温和夜景。不要同时加入计时、存档和商店。

本课的成品是课堂原型,不承诺完整线上反作弊或持久奖励。下一课会把测试、设备体验和安全分享收尾。

  1. 请一个没有看过代码的人尝试找三颗晶石,观察哪里会迷路。
  2. 只修正最影响体验的一点,保存 v02。
  3. 截取起点目标、收集进度、完成结果三张真实游戏画面,不使用参考图冒充自己的成果。

Homework

轮到你来改一改,做出自己的版本。

1 完成一轮实测

按验收表记录六种情况,发现问题先修复再美化。

2 一个自己的设计

给能量岛加入独特路标或故事背景,但不改变核心对象名。

3 选做:完成粒子

在服务器确认首次完成后播放一次粒子,不在每次 HUD 刷新时播放。

完成标准:先在 Playtest 中验证作品,再点击 Stop,保存为“姓名缩写_Lesson29”或对应 Homework 副本;不要覆盖上一课的可运行版本。

Exit questions

先自己回答,再展开看看。

客户端能直接宣布任务完成吗?

不能,终端由服务器读取它自己的进度并验证条件。

为什么重复提交只得一次奖章?

IslandFinished 在发奖前设为 true,之后的提交会提前返回。

重生与 Stop 重开有什么区别?

重生保留同一个 Player 的任务记录;Stop 后新会话重新初始化。

第一版要同时加上所有旧课功能吗?

不需要,先完成最小闭环,再一次增加一个经过验证的功能。

Explore further

需要查资料时,从这里出发。

正文代码可以直接复制。先确认脚本类型、放置位置,以及这段代码是完整版本还是小实验;不要把同一脚本的多个版本叠加运行。界面参考图已标注来源,图中的对象名可能与本课不同,请以操作步骤为准。