Roblox EP5 เกมลาวาขึ้น: สร้างทั้งด่านด้วยโค้ด และทดสอบกฎเกมนอก Studio
เกมที่สองของซีรีส์ เกมเป็นรอบที่ลาวาค่อย ๆ ขึ้น ใครถึงยอดหอคอยได้แต้ม ทั้งด่านสร้างจากโค้ดด้วย Rojo และทดสอบกฎว่ากระโดดถึงทุกขั้นด้วย Lune
- ด่านที่สร้างจากโค้ด แก้ตัวเลขที่เดียวแล้วทั้งด่านเปลี่ยนตาม และเก็บประวัติใน Git ได้
- แยกกฎของเกมเป็นฟังก์ชันที่ไม่แตะวัตถุของ Roblox แล้วทดสอบนอก Studio ได้ว่ายุติธรรมไหม
- เกมเป็นรอบมีโครงเดียวกันแทบทุกเกม รอ → เล่น → ตัดสิน → กลับไปรอ และเซิร์ฟเวอร์เป็นคนตัดสินเสมอ
EP1-4 เราวางพื้นและบันไดด้วยมือใน Studio แล้วเขียนโค้ดให้ชิ้นส่วนทำงาน บทนี้ทำเกมที่สอง ลาวาขึ้น เกมเป็นรอบที่เล่นกับเพื่อนได้ และเปลี่ยนวิธีทำ คือสร้างทั้งด่านจากโค้ด แล้วทดสอบกฎเกมนอก Studio ก่อนกด Play จริง วิธีนี้คือแบบที่ทีมทำเกมจริงใช้เมื่อเกมเริ่มใหญ่
บทนี้ยากขึ้นจากเดิม ถ้ายังไม่ได้อ่าน EP3 เรื่องเซิร์ฟเวอร์กับเครื่องผู้เล่น ควรอ่านก่อน
เกมที่จะได้
- ผู้เล่นเกิดบนเกาะห้องรอ นับถอยหลัง 15 วินาที
- ทุกคนถูกวาร์ปไปรอบฐานหอคอยบันไดวน 24 ขั้น
- 5 วินาทีต่อมา ลาวาเริ่มขึ้น ขึ้นเป็นระลอก ช้าสลับพุ่ง
- ใครเท้าแตะลาวาตาย กลับไปห้องรอ
- ครบ 75 วินาที ใครยืนบนยอดสีทองได้ Wins +1 แล้วเริ่มรอบใหม่
ระหว่างทางมีขั้นบันไดสองแบบพิเศษ ขั้นสีฟ้าขยับขึ้นลง และขั้นสีน้ำตาลที่สั่นแล้วถล่มเมื่อเหยียบ
ทำไมสร้างด่านด้วยโค้ด
| วางด้วยมือใน Studio | สร้างจากโค้ด | |
|---|---|---|
| อยากให้บันไดสูงขึ้นทุกขั้น | แก้ทีละชิ้น 24 ชิ้น | แก้ตัวเลขเดียว |
| เก็บประวัติการแก้ | ยาก ไฟล์เกมเป็นก้อนเดียว | ไฟล์ข้อความ ใช้ Git ได้ |
| ตรวจว่ากระโดดถึงทุกขั้น | ต้องเล่นเองทุกครั้ง | เขียนเทสต์ให้คำนวณ |
| ตอนแก้ไขเห็นฉากไหม | เห็น | ไม่เห็น ต้องกด Play |
| ด่านศิลปะ ปั้นละเอียด | เหมาะ | ไม่เหมาะ |
เกมนี้เป็นรูปทรงซ้ำ ๆ ตามสูตร บันไดวนรอบเสา ระยะเท่ากัน จึงเหมาะกับโค้ดมาก ส่วนด่านที่ต้องจัดวางสวยทีละมุม ใช้มือใน Studio ดีกว่า หลายเกมใช้ผสมกัน
เครื่องมือสองตัว
- Rojo เชื่อมไฟล์โค้ดในเครื่องกับ Studio เราเขียนโค้ดเป็นไฟล์
.luauธรรมดา แล้วให้ Rojo สร้างไฟล์เกม.rbxlxที่เปิดใน Studio ได้ - Lune รันภาษา Luau นอก Roblox ใช้รันเทสต์กฎเกมในเครื่องเรา ไม่ต้องเปิด Studio
ทั้งสองตัวเป็นโอเพนซอร์ส โหลดจากหน้า Releases บน GitHub ของแต่ละโครงการ บทนี้ใช้ Rojo 7.7.1 และ Lune 0.11.0 บน Windows
ไฟล์ในโปรเจกต์ไปอยู่ตรงไหนใน Studio:
| ไฟล์ | อยู่ใน Studio ที่ | ประเภท |
|---|---|---|
src/shared/Config.luau | ReplicatedStorage › Shared | ModuleScript ตัวเลขทั้งเกม |
src/shared/RoundLogic.luau | ReplicatedStorage › Shared | ModuleScript กฎของเกม |
src/server/MapBuilder.luau | ServerScriptService › Server | ModuleScript สร้างด่าน |
src/server/Main.server.luau | ServerScriptService › Server | Script ลูปของเกม |
src/client/StatusUI.client.luau | StarterPlayerScripts › Client | LocalScript แถบสถานะบนจอ |
Rojo ดูนามสกุลไฟล์ .server.luau เป็น Script, .client.luau เป็น LocalScript, ที่เหลือเป็น ModuleScript และจับคู่โฟลเดอร์กับที่ใน Studio จากไฟล์ default.project.json:
{
"name": "lava-rising",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": { "Shared": { "$path": "src/shared" } },
"ServerScriptService": { "Server": { "$path": "src/server" } },
"StarterPlayer": {
"StarterPlayerScripts": { "Client": { "$path": "src/client" } }
}
}
}
สร้างไฟล์เกมด้วยคำสั่งเดียว แล้วดับเบิลคลิกไฟล์ที่ได้เพื่อเปิดใน Studio:
rojo build -o build/lava-rising.rbxlx
ตัวเลขทั้งเกมอยู่ไฟล์เดียว
Config.luau เก็บทุกตัวเลขที่กำหนดหน้าตาและความยากของเกม ตัดมาบางส่วน:
local Config = {
INTERMISSION = 15, -- วินาทีที่รอในห้องรอ
ROUND_TIME = 75, -- ความยาวรอบ
LAVA_DELAY = 5, -- ลาวาเริ่มขยับหลังเริ่มรอบกี่วินาที
TOWER_STEPS = 24, -- จำนวนขั้นบันได
STEP_RISE = 5, -- ความสูงต่อขั้น (กระโดดปกติได้ราว 7)
STEP_ANGLE = 30, -- หมุนกี่องศาต่อขั้น
TOWER_RADIUS = 22, -- ระยะจากเสากลางถึงขั้นบันได
PLATFORM_SIZE = 8,
SURGE_WAVES = 3, -- ลาวาขึ้นเป็นระลอก กี่ระลอกต่อรอบ
SURGE_STRENGTH = 0.6 -- 0 = ขึ้นคงที่, 0.6 = เร็วช้าระหว่าง 0.4 ถึง 1.6 เท่า
}
return Config
อยากให้เกมยากขึ้น ลด ROUND_TIME อยากให้หอคอยสูงขึ้น เพิ่ม TOWER_STEPS ทั้งด่านเปลี่ยนตามเอง
กฎของเกม เขียนเป็นฟังก์ชันล้วน
จุดสำคัญของบทนี้ คือแยกกฎของเกมออกจากวัตถุใน Roblox RoundLogic.luau รับตัวเลขเข้าไป คืนตัวเลขออกมา ไม่แตะ Part ไม่แตะผู้เล่น เช่น ตำแหน่งขั้นบันไดแต่ละขั้น:
function RoundLogic.towerLayout(cfg)
local points = {}
for i = 1, cfg.TOWER_STEPS do
local a = math.rad(i * cfg.STEP_ANGLE)
table.insert(points, {
x = cfg.TOWER_RADIUS * math.cos(a),
y = i * cfg.STEP_RISE,
z = cfg.TOWER_RADIUS * math.sin(a),
})
end
return points
end
ขั้นที่ i หมุนไป i × 30 องศา สูงขึ้น i × 5 studs ได้บันไดวนรอบเสา ส่วน MapBuilder ค่อยเอาตำแหน่งพวกนี้ไปสร้าง Part จริง
กฎว่าใครโดนลาวาก็สั้นมาก ตำแหน่งที่วัดได้คือ HumanoidRootPart ซึ่งอยู่สูงกว่าเท้าราว 3 studs:
function RoundLogic.isBurned(rootY, lavaTop, cfg)
return rootY - cfg.BURN_MARGIN <= lavaTop -- BURN_MARGIN = 3
end
ระดับลาวาไม่ได้ขึ้นเป็นเส้นตรง แต่ขึ้นเป็นระลอก ช่วงช้าให้หายใจ ช่วงพุ่งให้ตื่นเต้น สูตรออกแบบให้ลาวาขึ้นอย่างเดียว ไม่ลง และถึงจุดสูงสุดพอดีตอนหมดเวลา ใต้ยอดหอคอย 4 studs คนบนยอดจึงรอดเสมอ
ทดสอบกฎนอก Studio
เพราะกฎไม่แตะ Roblox เราจึงรันมันด้วย Lune ได้ ไฟล์ tests/round_logic.luau ถามคำถามที่ตอบด้วยการคำนวณ (ชื่อเทสต์เป็นภาษาอังกฤษตามโค้ดจริง) เช่น
check("a step is jumpable (rise under 7)", Config.STEP_RISE < 7)
local gap = RoundLogic.stepGap(Config)
check("gap between steps is positive and short (0-6 studs)", gap > 0 and gap < 6)
-- แม้ช่วงลาวาพุ่งเร็วที่สุด ก็ต้องเหลือเวลาอย่างน้อย 1.5 วินาทีต่อขั้น
local secondsPerStep = Config.STEP_RISE / RoundLogic.lavaPeakSpeed(Config)
check("lava gives at least 1.5 s per step, even in a surge", secondsPerStep >= 1.5)
check("standing on the summit at the end is safe", not RoundLogic.isBurned(summitTop + 3, endY, Config))
รันในเครื่อง:
lune run tests/round_logic
lava
ok still during the head start
ok lava only ever goes up
...
climb time
ok lava gives at least 1.5 s per step, even in a surge
...
34 passed, 0 failed
ตัวอย่างการคำนวณ ช่องว่างระหว่างขั้น ระยะระหว่างจุดกลางของสองขั้นที่ห่างกัน 30 องศาบนวงรัศมี 22 คือ 2 × 22 × sin 15° ≈ 11.4 studs หักความกว้างขั้น 8 studs เหลือช่องว่างจริง ≈ 3.4 studs กระโดดข้ามสบาย
ประโยชน์จริงคือ วันที่แก้ STEP_RISE เป็น 8 เพื่อให้หอคอยสูงขึ้น เทสต์จะฟ้องทันทีว่ากระโดดไม่ถึง ก่อนที่ผู้เล่นคนไหนจะเจอ
ทดสอบสิ่งที่คำนวณได้ด้วยโค้ด เก็บเวลาเล่นจริงไว้ตัดสินสิ่งที่วัดด้วยตัวเลขไม่ได้ คือสนุกไหม
ลูปของเกม: รอ เล่น ตัดสิน
Main.server.luau เป็น Script ฝั่งเซิร์ฟเวอร์ตัวเดียวที่คุมทั้งเกม โครงทั้งหมดคือ
while true do
intermission() -- นับถอยหลังในห้องรอ
playRound() -- วาร์ป ลาวาขึ้น ตัดสิน
end
ใน playRound มีสามเรื่องที่ควรดู
1. จดว่าใครยังอยู่ ตอนวาร์ป ใส่ชื่อผู้เล่นลงตาราง alive และฟัง Died ของแต่ละคน
alive[player.Name] = true
humanoid.Died:Connect(function()
alive[player.Name] = false
end)
2. ขยับลาวาและตรวจทุกเฟรม ใช้ RunService.Heartbeat ซึ่งทำงานทุกเฟรมบนเซิร์ฟเวอร์
RunService.Heartbeat:Connect(function()
lavaTop = RoundLogic.lavaHeight(os.clock() - started, Config)
MapBuilder.setLava(lava, lavaTop)
for _, player in Players:GetPlayers() do
local root = if alive[player.Name] then rootOf(player) else nil
if root and RoundLogic.isBurned(root.Position.Y, lavaTop, Config) then
player.Character:FindFirstChildWhichIsA("Humanoid").Health = 0
end
end
end)
ทำไมไม่ใช้ Touched แบบลาวาใน EP2 เพราะลาวาก้อนนี้ถูกย้ายตำแหน่งทุกเฟรม และกินพื้นที่ทั้งสนาม การเทียบความสูงตรง ๆ แน่นอนกว่าและอ่านง่ายกว่า
บรรทัด if alive[player.Name] มาจากบั๊กจริงที่เจอก่อนส่งให้เล่น ลาวาขึ้นสูงกว่าระดับเกาะห้องรอ ถ้าตรวจทุกคนในเซิร์ฟเวอร์ คนที่รออยู่ในห้องรอจะตายไปด้วยทั้งที่ไม่ได้อยู่ใกล้ลาวาเลย
3. ตัดสินตอนจบ ใครยังอยู่ในตาราง alive ได้ Wins +1 แล้ววาร์ปกลับห้องรอ ทุกอย่างนี้เกิดบนเซิร์ฟเวอร์ เครื่องผู้เล่นแค่แสดงข้อความ ตามกติกาเดิมจาก EP3 ไม่เชื่อเครื่องผู้เล่น
แถบสถานะบนจอใช้วิธีง่ายที่สุด เซิร์ฟเวอร์เขียนข้อความลง StringValue ชื่อ Status ใน ReplicatedStorage และ LocalScript ฝั่งผู้เล่นแค่อ่านค่ามาแสดง ไม่ต้องใช้ RemoteEvent
ทำให้สนุกขึ้น
รุ่นแรกเล่นได้ แต่เรียบไป ปีนวนขึ้นไปเรื่อย ๆ รุ่นที่สองเพิ่มสามอย่าง
- ลาวาเป็นระลอก มี 3 ระลอกต่อรอบ ช่วงพุ่งแถบสถานะเปลี่ยนเป็นสีแดง ขึ้นว่า
LAVA SURGE! - ขั้นพิเศษ ตามแพตเทิร์น ปกติ › ขยับ › ปกติ › ถล่ม วนไปจนสุดหอคอย ขั้นที่ขยับใช้
TweenServiceให้ลอยขึ้นลง ขั้นที่ถล่มจะสั่น 0.6 วินาทีแล้วหายไป 3 วินาที - มาตรวัดความสูงเหนือลาวา บนจอผู้เล่น และจอแดงขึ้นเรื่อย ๆ เมื่อลาวาใกล้ เป็นแค่การแสดงผลฝั่งผู้เล่น ไม่ได้ตัดสินอะไร
ความสนุกต้องไม่แลกกับความยุติธรรม จึงมีกฎเพิ่มในเทสต์ และเทสต์ทั้งหมดต้องผ่านก่อนสร้างไฟล์เกม
- 3 ขั้นแรกเป็นขั้นปกติเสมอ ทุกคนได้เริ่มอย่างสบาย
- ไม่มีขั้นพิเศษสองขั้นติดกัน
- ขั้นสุดท้ายก่อนยอดเป็นขั้นปกติ
- ขั้นที่ขยับ บวกระยะขยับแล้วยังสูงไม่ถึง 7 studs
ลองเล่นในเครื่อง
- สร้างไฟล์เกมด้วย
rojo buildแล้วดับเบิลคลิกbuild/lava-rising.rbxlx - Studio เปิดขึ้นมาจะเห็นแค่ท้องฟ้า เป็นเรื่องปกติ
- กด Play (▶ ฟ้า มุมซ้ายบน) หรือ F5 เล่นคนเดียวได้ เพราะ
MIN_PLAYERS = 1 - ลองหลายคน ไปที่ Test › Clients and Servers ตั้ง 2-3 คน แล้วกด Start
- เปิดหน้าต่าง Output จากเมนู Window ไว้ดู error
ถ้าเปิดไฟล์จาก Studio แทนการดับเบิลคลิก ไปที่ File › Open from File
ข้อผิดพลาดที่เจอบ่อย
- ตรวจลาวากับผู้เล่นทุกคนในเซิร์ฟเวอร์ คนในห้องรอตายไปด้วย
- ไม่ยกเลิก
Connectตอนจบรอบ รอบถัดไปมีตัวตรวจลาวาซ้อนสองชุด แก้ด้วยการเก็บ connection ไว้ในตารางแล้วDisconnectทั้งหมดตอนจบ - ตั้งลาวาให้ขึ้นถึงยอด ไม่มีใครชนะได้เลย ต้องหยุดใต้ยอดเสมอ
- แก้ตัวเลขใน Config แล้วไม่รันเทสต์ ได้ด่านที่กระโดดไม่ถึงโดยไม่รู้ตัว
- ใส่ LocalScript ไว้ผิดที่ ตามที่เล่าใน EP3 มันจะไม่ทำงานเลยโดยไม่มี error
ตอนหน้า
ตอนนี้ Wins หายทุกครั้งที่ออกจากเกม EP6 จะบันทึกข้อมูลผู้เล่นข้ามรอบด้วย DataStore พร้อมวิธีจัดการตอนบันทึกล้มเหลว และการ Publish เกมขึ้น Roblox ซึ่งต้องทำก่อนใช้ DataStore ได้
คำถามที่พบบ่อย
ต้องใช้ Rojo ไหม ทำใน Studio อย่างเดียวได้ไหม
ได้ โค้ดทุกไฟล์ในบทนี้วางเป็น Script และ ModuleScript ใน Studio ตามตารางในบทได้เลย Rojo ช่วยตอนโปรเจกต์ใหญ่ขึ้น เพราะเขียนโค้ดในเครื่องมือที่ถนัด ใช้ Git และให้ AI ช่วยแก้ไฟล์ได้
ทำไมเปิดไฟล์เกมแล้วเห็นแค่ท้องฟ้าว่าง
เพราะด่านถูกสร้างตอนเซิร์ฟเวอร์เริ่มทำงาน กด Play แล้วฉากจะขึ้นมา เป็นข้อแลกเปลี่ยนของการสร้างด่านด้วยโค้ด
Lune คืออะไร ปลอดภัยไหม
Lune เป็นโปรแกรมรันภาษา Luau นอก Roblox เป็นโอเพนซอร์ส โหลดจาก GitHub ของโครงการ ใช้รันเทสต์กฎเกมในเครื่องเราเอง ไม่ได้ส่งอะไรขึ้น Roblox
คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ