开始之前
理解 table、Player 和服务器脚本。新建 Lesson19,不依赖旧金币系统。
Roblox Studio · 第 19 课
用 Attributes 记录每位玩家的收集进度,让任务目标和奖励规则清楚可查。
Course overview
金币适合反复收集,任务物品则通常只计算一次。这次你要做一个明确的目标:找到三颗不同的能量晶石。
Attributes 像贴在对象上的小标签。我们在 Player 上记录 Crystals 和 QuestComplete,让进度既容易读取,也容易在 Properties 中检查。
理解 table、Player 和服务器脚本。新建 Lesson19,不依赖旧金币系统。
三颗每人各可收集一次的晶石,集齐后将 QuestComplete 设为 true。
反复碰同一颗不加进度;三个不同晶石才完成;两人互不抢走任务物品。

Learning route
每完成一小步,就测试一次。先说出你预计会看到什么,再用 Play 和 Output 检查,不用急着一次写完所有代码。
建立一个只装晶石 Part 的文件夹,脚本就能一次找到它们。不要把灯、路标和脚本也放进去让程序误认。
Crystals 保存已收集数量,QuestComplete 保存是否完成。SetAttribute 设置一个值,GetAttribute 读取它;它们和内置的 Position、Color 不同,是自己定义的标签。
collected[player] 是这个玩家的领取表,里面用 crystal 对象作键。重复触碰同一对象会马上返回,而碰到另一颗会继续处理。
同一颗晶石对 A 可能已领取,对 B 却仍然可领取,所以共享外观不能简单代表所有人的任务状态。
任务完成不是碰三次,而是碰到三颗不同晶石。先把这句话写下来,再检查代码是否遵守。
Step by step
建立一个只装晶石 Part 的文件夹,脚本就能一次找到它们。不要把灯、路标和脚本也放进去让程序误认。
为了简单,本课保持晶石可见。每位玩家是否收集过,由服务器的记录判断,不通过全局隐藏晶石来决定。

Step by step
Crystals 保存已收集数量,QuestComplete 保存是否完成。SetAttribute 设置一个值,GetAttribute 读取它;它们和内置的 Position、Color 不同,是自己定义的标签。
收集记录必须由服务器修改。客户端以后可以用 GetAttributeChangedSignal 更新 HUD,但不能把自己改出来的数字当作真实任务进度。
Step by step
collected[player] 是这个玩家的领取表,里面用 crystal 对象作键。重复触碰同一对象会马上返回,而碰到另一颗会继续处理。
所有判断都在增加进度之前完成。距离检查给小型晶石留出 8 studs 范围;改成巨大物体时,需要重新设计这个范围。
local Players = game:GetService("Players")
local folder = workspace:WaitForChild("Crystals")
local TARGET = 3
local collected = {}
local function setup(player)
collected[player] = {}
player:SetAttribute("Crystals", 0)
player:SetAttribute("QuestComplete", false)
end
Players.PlayerAdded:Connect(setup)
for _, player in ipairs(Players:GetPlayers()) do setup(player) end
Players.PlayerRemoving:Connect(function(player) collected[player] = nil end)
for _, crystal in ipairs(folder: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)
local root = character and character:FindFirstChild("HumanoidRootPart")
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
if not player or not root or not humanoid or humanoid.Health <= 0 then return end
if (root.Position - crystal.Position).Magnitude > 8 then return end
local record = collected[player]
if not record or record[crystal] then return end
record[crystal] = true
local count = (player:GetAttribute("Crystals") or 0) + 1
player:SetAttribute("Crystals", count)
player:SetAttribute("QuestComplete", count >= TARGET)
print(player.Name, "Crystals:", count, "/", TARGET)
end)
end
Step by step
同一颗晶石对 A 可能已领取,对 B 却仍然可领取,所以共享外观不能简单代表所有人的任务状态。
本课用 Output 与个人 Attribute 验证。综合项目会加入个人 HUD;若要让已领取晶石只对自己淡出,还需要客户端的个人显示逻辑。
Step by step
任务完成不是碰三次,而是碰到三颗不同晶石。先把这句话写下来,再检查代码是否遵守。
如果后来动态创建新晶石,这段启动时 GetChildren 的连接不会自动覆盖它们。本课物品在开始前就摆好;动态生成需要额外处理 ChildAdded。
Homework
设计三条不同路线,验证无论先找到哪颗都能完成。
写上“收集三颗不同晶石”,避免让玩家误以为同一颗可刷三次。
增加 CrystalD 并把 TARGET 改为 4,重新做重复领取测试。
Exit questions
不是,是通过 SetAttribute 定义的自定义属性。
本课规定每人各有一份,服务器删除会让其他玩家也失去它。
collected[player][crystal] 记录了这个玩家已经领取过的对象。
本课代码不会;它只在启动时遍历已有子对象。
Explore further
正文代码可以直接复制。先确认脚本类型、放置位置,以及这段代码是完整版本还是小实验;不要把同一脚本的多个版本叠加运行。界面参考图已标注来源,图中的对象名可能与本课不同,请以操作步骤为准。