iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Software Development

Roblox Studio AI 協作開發大全系列 第 17

第 17 章:商店與升級系統

  • 分享至 

  • xImage
  •  

第 14 章讓玩家完成任務並取得 Gold。第 16 章讓玩家取得 WindDash 能力。現在要把這兩件事接起來:玩家用任務獎勵購買能力升級。

這一章的商店不是 Roblox 付費商城,也不是 Developer Product。它是遊戲內 progression shop:玩家在遊戲裡賺到貨幣,再用貨幣解鎖或強化能力。

本章的核心問題是:按鈕可以在 client,但購買結果必須由 server 決定。玩家按下「Buy」只是在提出請求,server 要檢查價格、Gold、升級是否存在、是否已經買過,然後才扣款與套用升級。

本章目標

完成本章後,你會得到:

  • 一個最小商店 UI。
  • 兩個 WindDash 升級項目。
  • 一個 ShopConfig ModuleScript。
  • 一個 PurchaseUpgrade RemoteEvent。
  • 一個 ShopUpdate RemoteEvent。
  • 一個 server-side ShopService
  • Gold 足夠時可以購買升級。
  • Gold 不足或重複購買時會被 server 拒絕。
  • AbilityService 可以讀取升級狀態,調整 WindDash 效果。

遊戲商店中的推薦商品介面範例。

圖 17-1 商店 UI 必須清楚呈現商品、價格與效果;購買判定和升級狀態則由 server 驗證。

Context Pack

本章使用下列 Roblox 官方文件作為 context:

本章採用這些文件中的幾個原則:

  • UI button 可以觸發購買流程,但不決定購買結果。
  • server 是 game state 的 authority。
  • RemoteEvent 適合做非同步 request / update 流程。
  • ModuleScript 適合集中價格、升級數值與顯示資料。
  • client 傳進 server 的 item id、價格或數值都必須被驗證。

這不是 Robux Shop

本章建立的是使用遊戲內 Gold 的 progression shop。Roblox 官方文件中的 Shop 則會呈現 Pass 與符合資格的 Developer Product,並使用平台購買流程。兩者名稱相近,但交易責任完全不同:

項目 本章 Gold 商店 Roblox 官方 Shop
貨幣 遊戲內 Gold Robux 或符合資格的平台付款方式
商品 WindDash 升級 Pass、Developer Product 等
交易入口 PurchaseUpgrade Roblox 平台購買流程
交付 ShopService ownership 或 server-side receipt flow
本章是否實作 否,留到第 33–34 章

因此本章不使用 MarketplaceServiceProcessReceipt,也不修改 ExperienceShop 或 Creator Hub 的 Monetization 設定。第八部會從第 32 章開始完整處理平台商品、交易交付、分析與廣告。

官方升級範例使用 RemoteFunction 讓 client 同步取得購買結果。本書前面已經用 QuestUpdateAbilityUpdate 建立了 RemoteEvent update pattern,所以本章會用兩個 RemoteEvent

PurchaseUpgrade: client -> server
ShopUpdate: server -> client

這樣做能保持本書架構一致,也能避免 UI script 因等待 server 回傳而卡住。

開場情境

玩家完成 Guide 的水晶任務後得到 25 Gold。如果之後任務或收集物讓玩家累積更多 Gold,就需要一個消耗 Gold 的地方。最自然的第一個消耗點,就是升級第 16 章剛建立的 WindDash

本章先做兩個一次性升級:

DashCooldown1
Cost = 40 Gold
Effect = WindDash cooldown from 6.0 to 4.5 seconds

DashSpeed1
Cost = 60 Gold
Effect = WindDash speed from 28 to 34

這兩個升級故意很小。現在的目標不是做龐大的技能樹,而是建立一條可以驗證的商店管線。

Button Activated
-> PurchaseUpgrade:FireServer(upgradeId)
-> ShopService validates
-> deduct leaderstats.Gold
-> set player attribute
-> ShopUpdate:FireClient(...)

第一個 Prompt

請 Assistant 建立最小可用商店與升級流程。

【Ch17 主任務|建立升級商店】
我們要繼續開發 Roblox Studio 專案「AI Adventure Island」。

現有背景:
- 玩家已經有 leaderstats.Gold。
- 第 16 章建立了 WindCharm 與 WindDash。
- ReplicatedStorage.Remotes 已經存在。
- StarterGui.AdventureHUD 已經存在。
- ServerScriptService.AbilityService 負責處理 WindDash。
- 目前不要加入 DataStore;這會留到第 18 章。
- 不要加入 Roblox Marketplace、Developer Products、Robux 購買或任何真實貨幣營利功能。

請建立第一套遊戲內商店與升級系統:
1. 在 ReplicatedStorage 建立名為 ShopConfig 的 ModuleScript。
2. ShopConfig 應定義兩個一次性升級:
   - DashCooldown1:
     - Cost = 40
     - AttributeName = "WindDashCooldownLevel"
     - AttributeValue = 1
     - DisplayName = "風之衝刺冷卻"
     - Description = "將 WindDash cooldown 從 6.0 秒縮短為 4.5 秒。"
   - DashSpeed1:
     - Cost = 60
     - AttributeName = "WindDashSpeedLevel"
     - AttributeValue = 1
     - DisplayName = "風之衝刺速度"
     - Description = "將 WindDash 速度從 28 提高到 34。"
3. 在 ReplicatedStorage.Remotes 建立名為 PurchaseUpgrade 的 RemoteEvent。
4. 在 ReplicatedStorage.Remotes 建立名為 ShopUpdate 的 RemoteEvent。
5. 建立 ServerScriptService.ShopService。
6. ShopService 必須在 server 驗證:
   - upgradeId 是 string
   - ShopConfig 中存在這個 upgradeId
   - 玩家擁有 leaderstats.Gold
   - 玩家有足夠的 Gold
   - 玩家尚未購買過這個一次性升級
7. 購買成功時:
   - 從 leaderstats.Gold 扣除 Cost
   - 根據 ShopConfig.AttributeName 與 ShopConfig.AttributeValue 設定玩家 attribute
   - 透過 ShopUpdate:FireClient 傳送成功狀態、upgradeId、剩餘 Gold 與訊息
8. 購買失敗時:
   - 不要扣除 Gold
   - 透過 ShopUpdate:FireClient 傳送失敗原因與訊息
9. 更新 ServerScriptService.AbilityService,讓 WindDash 使用以下數值:
   - 玩家 WindDashCooldownLevel 為 1 時,cooldown 設為 4.5
   - 玩家 WindDashSpeedLevel 為 1 時,速度設為 34
   - 否則沿用第 16 章的原始數值
10. 在 StarterGui.AdventureHUD 裡建立最小可用的 ShopPanel:
   - BuyDashCooldownButton
   - BuyDashSpeedButton
   - ShopStatusLabel
11. 新增或更新 LocalScript,讓兩個按鈕分別傳送:
   - PurchaseUpgrade:FireServer("DashCooldown1")
   - PurchaseUpgrade:FireServer("DashSpeed1")
12. client 不得傳送價格、等級、速度、cooldown 或 Gold 數量。

請維持小型 prototype 的規模。目前不要加入 DataStore、可捲動商店頁面、多種貨幣或升級樹。

這個 prompt 對商店很重要。你不是只要求 Assistant 「做一個商店」,而是指定 server 要驗證哪些事情,以及 client 不能傳哪些資料。

尤其這句不能省:

client 不得傳送價格、等級、速度、cooldown 或 Gold 數量。

client 只傳 upgradeId。其他所有價格與效果都由 server 從 ShopConfig 讀取。

Assistant 可能產生的結果

Assistant 可能會建立這樣的結構:

ReplicatedStorage
├── ShopConfig (ModuleScript)
└── Remotes
    ├── PurchaseUpgrade (RemoteEvent)
    └── ShopUpdate (RemoteEvent)

ServerScriptService
├── AbilityService
└── ShopService

StarterGui
└── AdventureHUD
    └── ShopPanel
        ├── BuyDashCooldownButton
        ├── BuyDashSpeedButton
        └── ShopStatusLabel

ShopConfig 可能長得像這樣:

local ShopConfig = {}

ShopConfig.Upgrades = {
	DashCooldown1 = {
		Cost = 40,
		AttributeName = "WindDashCooldownLevel",
		AttributeValue = 1,
		DisplayName = "Wind Dash Cooldown",
		Description = "Reduce WindDash cooldown from 6.0 to 4.5 seconds.",
	},
	DashSpeed1 = {
		Cost = 60,
		AttributeName = "WindDashSpeedLevel",
		AttributeValue = 1,
		DisplayName = "Wind Dash Speed",
		Description = "Increase WindDash speed from 28 to 34.",
	},
}

return ShopConfig

這份 config 同時給 server 和 client 讀取是可以的,因為價格與描述不是秘密。重點是:就算 client 讀得到,也不能由 client 決定購買是否成功。

ShopService 的核心流程可能像這樣:

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local remotes = ReplicatedStorage:WaitForChild("Remotes")
local purchaseUpgrade = remotes:WaitForChild("PurchaseUpgrade")
local shopUpdate = remotes:WaitForChild("ShopUpdate")

local ShopConfig = require(ReplicatedStorage:WaitForChild("ShopConfig"))

local function sendShopUpdate(player, success, upgradeId, message, remainingGold)
	shopUpdate:FireClient(player, {
		success = success,
		upgradeId = upgradeId,
		message = message,
		remainingGold = remainingGold,
	})
end

local function getGoldValue(player)
	local leaderstats = player:FindFirstChild("leaderstats")
	if not leaderstats then
		return nil
	end

	return leaderstats:FindFirstChild("Gold")
end

local function hasPurchased(player, upgrade)
	return player:GetAttribute(upgrade.AttributeName) == upgrade.AttributeValue
end

local function purchase(player, upgradeId)
	if typeof(upgradeId) ~= "string" then
		sendShopUpdate(player, false, nil, "Invalid upgrade request.", nil)
		return
	end

	local upgrade = ShopConfig.Upgrades[upgradeId]
	if not upgrade then
		sendShopUpdate(player, false, upgradeId, "Upgrade does not exist.", nil)
		return
	end

	if hasPurchased(player, upgrade) then
		sendShopUpdate(player, false, upgradeId, "Already purchased.", nil)
		return
	end

	local gold = getGoldValue(player)
	if not gold then
		sendShopUpdate(player, false, upgradeId, "Gold stat is missing.", nil)
		return
	end

	if gold.Value < upgrade.Cost then
		sendShopUpdate(player, false, upgradeId, "Not enough Gold.", gold.Value)
		return
	end

	gold.Value -= upgrade.Cost
	player:SetAttribute(upgrade.AttributeName, upgrade.AttributeValue)

	sendShopUpdate(player, true, upgradeId, "Purchased " .. upgrade.DisplayName .. ".", gold.Value)
end

purchaseUpgrade.OnServerEvent:Connect(purchase)

這段程式的重點是順序:

  1. 先檢查 upgradeId 型別。
  2. 再檢查 config 是否存在。
  3. 再檢查是否已購買。
  4. 再檢查 Gold 是否存在。
  5. 再檢查 Gold 是否足夠。
  6. 最後才扣款與設定 attribute。

不要先扣款再檢查,也不要讓 client 告訴 server 價格。

AbilityService 需要讀取升級狀態。第 16 章原本有:

local WIND_DASH_COOLDOWN = 6
local WIND_DASH_WALK_SPEED = 28

本章可以改成 helper:

local function getWindDashCooldown(player)
	if player:GetAttribute("WindDashCooldownLevel") == 1 then
		return 4.5
	end

	return 6
end

local function getWindDashWalkSpeed(player)
	if player:GetAttribute("WindDashSpeedLevel") == 1 then
		return 34
	end

	return 28
end

然後在使用能力時改成:

local cooldown = getWindDashCooldown(player)
local dashSpeed = getWindDashWalkSpeed(player)

這樣 ShopService 不需要 require AbilityServiceAbilityService 也不需要 require ShopService。兩者透過 player attribute 交接狀態。

client UI 則只送出 upgrade id:

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local remotes = ReplicatedStorage:WaitForChild("Remotes")
local purchaseUpgrade = remotes:WaitForChild("PurchaseUpgrade")
local shopUpdate = remotes:WaitForChild("ShopUpdate")

local shopPanel = script.Parent:WaitForChild("ShopPanel")
local cooldownButton = shopPanel:WaitForChild("BuyDashCooldownButton")
local speedButton = shopPanel:WaitForChild("BuyDashSpeedButton")
local statusLabel = shopPanel:WaitForChild("ShopStatusLabel")

cooldownButton.Activated:Connect(function()
	purchaseUpgrade:FireServer("DashCooldown1")
end)

speedButton.Activated:Connect(function()
	purchaseUpgrade:FireServer("DashSpeed1")
end)

shopUpdate.OnClientEvent:Connect(function(result)
	statusLabel.Text = result.message
end)

這段 client code 很短,這是好事。商店 UI 不應該塞滿價格計算、扣款邏輯、升級狀態判斷。

元件拆解

ShopConfig

ShopConfig 是本章最重要的整理點。

如果不用 config,Assistant 很可能把資料寫成這樣:

Button text says 40 Gold
LocalScript checks 40
ShopService checks 40
AbilityService knows cooldown becomes 4.5

這會造成未來調整價格時要改很多地方。只要有一個地方漏改,UI 顯示、server 扣款和實際效果就會不一致。

集中成 ShopConfig 後,價格與效果只有一份來源:

ShopConfig.Upgrades.DashCooldown1.Cost

TextButton

TextButton 是玩家觸發購買的 UI。它可以處理 click、tap 和 gamepad UI selection activation。

本章用 Activated

button.Activated:Connect(function()
	purchaseUpgrade:FireServer("DashCooldown1")
end)

Activated 是 UI 入口,不是購買完成。按鈕被按下,只代表玩家提出購買請求。

RemoteEvent

本章用兩個 RemoteEvent:

PurchaseUpgrade
ShopUpdate

PurchaseUpgrade 是 client -> server:

purchaseUpgrade:FireServer("DashCooldown1")

ShopUpdate 是 server -> client:

shopUpdate:FireClient(player, result)

官方升級範例使用 RemoteFunction,那也是合理選擇。RemoteFunction 的優點是 client 可以直接拿到回傳值;缺點是 client script 會等待 server 回覆。本書目前多數系統採用 RemoteEvent request / update pattern,所以本章沿用它,讓讀者看到一致的資料流。

Explorer 中放在 ReplicatedStorage 的升級用 RemoteFunction。

圖 17-2 RemoteFunction 必須放在 client 與 server 都能存取的位置。無論使用 RemoteFunction 或 RemoteEvent,價格與升級結果都要由 server 驗證。

Server Validation

商店系統最容易犯的錯,是相信 client。

錯誤設計:

purchaseUpgrade:FireServer("DashCooldown1", 40, 4.5)

server 如果相信第二個與第三個參數,就等於讓 client 決定價格與效果。

正確設計:

purchaseUpgrade:FireServer("DashCooldown1")

server 自己查:

local upgrade = ShopConfig.Upgrades[upgradeId]

client 只傳 id。id 本身也要驗證,必須存在於 ShopConfig.Upgrades

Gold

第 14 章已建立 leaderstats.Gold。本章直接沿用它,先不引入新的貨幣系統。

扣 Gold 的順序必須清楚:

if gold.Value < upgrade.Cost then
	return
end

gold.Value -= upgrade.Cost
player:SetAttribute(upgrade.AttributeName, upgrade.AttributeValue)

先確認足夠,再扣款,再套用升級。不要把扣款放在 client,也不要讓 UI 顯示的 Gold 當作實際 Gold。

Player Attributes

本章用 player attribute 暫存升級狀態:

player:SetAttribute("WindDashCooldownLevel", 1)

這很適合 prototype。它讓 AbilityService 可以讀取目前玩家升級,而不需要和 ShopService 互相 require。

第 18 章會把這些狀態接到 DataStore。現在先不要混進存檔,否則很難判斷商店 bug 是來自購買流程還是資料保存流程。

Playtest

本章至少做四輪測試。

第一輪:Gold 不足

  1. 按 Play。
  2. 確認玩家 Gold 小於 40。
  3. BuyDashCooldownButton
  4. HUD 應顯示 Gold 不足。
  5. Gold 不應改變。
  6. player attribute 不應新增 WindDashCooldownLevel = 1

第二輪:Gold 足夠

  1. 在測試中暫時把 Gold 設成 40 或更多。
  2. BuyDashCooldownButton
  3. Gold 應扣除 40。
  4. player attribute 應變成 WindDashCooldownLevel = 1
  5. HUD 應顯示購買成功。

第三輪:重複購買

  1. 已購買 DashCooldown1 後再次按同一按鈕。
  2. server 應拒絕。
  3. Gold 不應再次扣除。
  4. HUD 應顯示已購買。

第四輪:能力效果

  1. 購買 DashCooldown1
  2. 使用 WindDash
  3. cooldown 應從 6 秒變成 4.5 秒。
  4. 購買 DashSpeed1
  5. 使用 WindDash
  6. dash speed 應從 28 變成 34。

測試時切換到 Server 檢查:

  • ShopService 是否在 server。
  • Gold 是否由 server 扣除。
  • player attributes 是否在 server 設定。
  • Output 是否有購買成功或失敗訊息。

修正 Prompt

如果 client 傳了價格或效果,使用:

【Ch17 修正 1|移除 Client 商品資料】
商店 LocalScript 目前會將價格、cooldown、速度或 Gold 數量傳送到 server。

預期結果:
- client 只能傳送 PurchaseUpgrade:FireServer(upgradeId)。
- ShopService 應該從 ReplicatedStorage.ShopConfig 讀取價格與效果。
- server 不得信任 client 傳入的價格或效果數值。

請更新商店請求流程。
不要修改 upgrade id 或 UI 按鈕名稱。

如果 Gold 不足仍然能購買,使用:

【Ch17 修正 2|玩家在 Gold 不足時仍然可以購買升級】
玩家在 Gold 不足時仍然可以購買升級。

預期結果:
- ShopService 在 server 檢查 leaderstats.Gold。
- 如果 Gold.Value < ShopConfig.Upgrades[upgradeId].Cost,購買應該失敗。
- Gold 不應改變。
- 不應設定任何玩家升級 attribute。

請只檢查 ServerScriptService.ShopService 與 ShopConfig。

如果同一升級可以重複買,使用:

【Ch17 修正 3|阻止重複購買升級】
同一個一次性升級目前可以重複購買。

預期結果:
- DashCooldown1 與 DashSpeed1 都是一次性升級。
- 如果玩家的 attribute 已經等於升級的 AttributeValue,ShopService 應該拒絕購買。
- 不應再次扣除 Gold。

請在 ShopService 加入防止重複購買的檢查。

如果購買成功但 WindDash 沒有變強,使用:

【Ch17 修正 4|套用 WindDash 升級】
商店購買成功,但 WindDash 仍然使用舊的 cooldown 或速度。

預期結果:
- AbilityService 應該讀取以下玩家 attributes:
  - WindDashCooldownLevel
  - WindDashSpeedLevel
- WindDashCooldownLevel 為 1 時,cooldown 應該是 4.5。
- WindDashSpeedLevel 為 1 時,Dash 速度應該是 34。

請只更新 AbilityService。
不要讓 ShopService require AbilityService。
使用玩家 attributes 作為兩個 service 之間的界線。

工程整理

完成本章後,Explorer 應接近這樣:

ReplicatedStorage
├── ShopConfig
└── Remotes
    ├── RequestGuideHint
    ├── QuestUpdate
    ├── RequestAbility
    ├── AbilityUpdate
    ├── PurchaseUpgrade
    └── ShopUpdate

StarterGui
└── AdventureHUD
    ├── DashButton
    ├── DashCooldownBar
    └── ShopPanel
        ├── BuyDashCooldownButton
        ├── BuyDashSpeedButton
        └── ShopStatusLabel

ServerScriptService
├── QuestService
├── EnemyAIService
├── AbilityService
└── ShopService

整理時檢查:

  • ShopConfig 是價格與效果的單一來源。
  • ShopService 不相信 client 傳來的價格、Gold、cooldown 或 speed。
  • ShopService 不 require AbilityService
  • AbilityService 不 require ShopService
  • 兩個 service 透過 player attributes 交換升級狀態。
  • 本章沒有 DataStore。
  • 本章沒有 Robux、Developer Product 或 Marketplace。

如果要把這套 UI 延伸成真實 Robux 商品,不能只把 Gold 按鈕換成價格。請接續第 32–34 章,重新建立商品型別、平台購買流程、receipt、權益與可靠交付。

這個邊界能讓第 18 章接存檔時更清楚:DataStore 只需要保存玩家 Gold 與 upgrade attributes,不需要重寫商店邏輯。

本章總結

本章把 Gold 變成有用途的資源,也讓 WindDash 從固定能力變成可以升級的系統。

你建立了:

  • ShopConfig
  • ShopService
  • PurchaseUpgrade / ShopUpdate
  • 最小商店 UI。
  • 兩個 WindDash 一次性升級。
  • server-side 購買驗證。

本章最重要的觀念是:

Button 只是入口。
Config 是資料來源。
Server 才能完成購買。

下一章會處理這套系統目前最大的缺口:玩家離開後,Gold 與已購買升級都會消失。第 18 章會把任務進度、Gold 與升級狀態接到 DataStore。


關於 Wolke

嗨!我是 Wolke,曾任 Google Developer Expert(GDE,2019–2023)LINE API Expert

我熱衷於研究 AI Agent、n8n 自動化工作流與全端開發架構,致力於將 AI 技術轉化為真正能落地的生產力工具。

如果你喜歡這篇文章,歡迎透過以下方式與我交流:

📚 技術著作
《實用的 Gemini API 開發點子書》:帶你運用 Gemini App、Google AI Studio、Gemini CLI 與 Antigravity IDE,打造 AI Agent 與實用產品。

📝 技術部落格
歡迎追蹤我的 Medium,我會持續分享 Agentic Automation、架構設計與實際開發的踩坑心得。

🎤 技術講座與合作
我持續受邀至技術社群及研討會,分享 AI Agent、自動化工作流、DevOps 與全端開發實戰。

我曾於 DevOpsDays Taipei 2026 主講「不再只是寫腳本!讓 AI 代理人成為你的 SRE 最佳夥伴」工作坊。

如果你的企業、社群或學校正在尋找相關主題講者,歡迎私訊聯繫,洽談講座與工作坊合作!

🎮 我的 Roblox 遊戲

🎁 免費贈送 OpenAI 或 Claude AI 額度

為了鼓勵大家實際動手打造自己的 Roblox 體驗,我每個月會開放:

  • 10 個名額
  • 每人 50 點 AI 額度
  • 名額送完為止

參加方式:

  1. 訂閱本系列文章。
  2. 分享任一篇系列文章。
  3. 私訊分享截圖及你的 AI 帳號 Email。

確認完成後,我會邀請你加入並設定 50 點額度。名額有限,歡迎把握機會!


上一篇
第 16 章:道具、能力與冷卻時間
下一篇
第 18 章:存檔系統
系列文
Roblox Studio AI 協作開發大全23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言