เกมแนว roguelike ที่ดีที่สุดไม่ได้แค่ฆ่าคุณ แต่พวกมันทำให้คุณอยากตาย

ฟังดูแปลก แต่ใครก็ตามที่เพิ่งแพ้ในรอบดึกตอนเที่ยงคืนแล้วเริ่มรอบใหม่ตอน 12:03 น. ย่อมเข้าใจความรู้สึกนี้ดี ความตายนั้นเจ็บปวด พลังชีวิตของคุณลดลงเหลือศูนย์ หน้าจอเต็มไปด้วยความล้มเหลว ทว่านิ้วของคุณกลับวางค้างอยู่เหนือปุ่มเล่นเกมเสียแล้ว บางอย่างในช่วงยี่สิบนาทีที่ผ่านมานั้นมีความหมายมากพอจนคุณไม่สามารถทิ้งเกมไว้ในจุดที่มันจบลงได้

นี่คือลูป "ขออีกรอบเดียว" (one more run loop) และมันไม่ใช่เรื่องบังเอิญ แต่มันคือการออกแบบ หากคุณกำลังสร้างเกมแนว roguelike หรือเกมใดก็ตามที่มีระบบตายแล้วเริ่มใหม่ (permadeath) งานทั้งหมดของคุณคือการสร้างสมดุลระหว่างแรงผลักดันสองอย่างที่สวนทางกัน ผู้เล่นต้องแพ้มากพอที่จะรู้สึกถึงความตึงเครียด แต่ต้องรักษาบางอย่างไว้ได้มากพอที่จะรู้สึกถึงความหวัง

สกุลเงินแห่งความล้มเหลว

ในโปรเจกต์ล่าสุดของผม Neon Survivor ผมต้องการสร้างความตึงเครียดแบบนั้นพอดี เมื่อผู้เล่นตาย ทุกอย่างที่เก็บสะสมมาในรอบนั้นจะหายไปหมด ยกเว้นสิ่งเดียวคือ: ทอง ทองนั้นจะถูกนำเข้าธนาคารโดยอัตโนมัติ เมื่อกลับมาที่เมนู พวกเขาสามารถใช้มันเพื่ออัปเกรดความสามารถแบบถาวร จากนั้นพวกเขาก็จะกระโดดกลับเข้าไปเล่นอีกครั้ง โดยที่แข็งแกร่งกว่าเดิมเล็กน้อย

ลูปที่เรียบง่ายนี้คือหัวใจหลักของเกม หากไม่มีมัน ความตายคือจุดฟูลสตอป (full stop) ผู้เล่นจะเดินจากไปเพราะการเล่นรอบล่าสุดไม่ได้อะไรกลับมาเลย แต่ถ้ามีมัน ความตายจะเป็นเพียงแค่เครื่องหมายจุลภาค (comma) การเล่นรอบนั้นจะกลายเป็นการออกไปฟาร์มของ แม้ความสูญเสียจะเจ็บปวด แต่มันก็ได้จ่ายค่าตอบแทนสำหรับวันพรุ่งนี้แล้ว

เคล็ดลับนั้นง่ายมาก คือคุณต้องเหลือบางอย่างไว้ให้ผู้เล่นแม้ว่าพวกเขาจะสูญเสียทุกอย่างไปแล้วก็ตาม ความยากคือการทำให้แน่ใจว่าสิ่งที่เหลือไว้นั้นมีความสำคัญ โดยไม่ทำลายความท้าทายของเกม หากการอัปเกรดอ่อนแอเกินไป ผู้เล่นจะเลิกใส่ใจ หากมันแข็งแกร่งเกินไป เกมก็จะเล่นด้วยตัวมันเอง ไม่ว่าทางไหนลูปนี้ก็จะพังทลายลง

นาฬิกาสองเรือน ความสับสนเป็นศูนย์

ในการสร้างสิ่งนี้ให้ถูกต้อง ผมต้องจัดการกับไทม์ไลน์สองเส้นที่แยกจากกัน

Run Clock จะรีเซ็ตทุกครั้งที่คุณกดเล่น มันจะติดตามค่าพลังชีวิต, คะแนนปัจจุบัน, จำนวนเวฟของศัตรู และไอเทมเพิ่มพลังชั่วคราว (power-ups) ใดๆ ที่เก็บได้ในเซสชันนั้น เมื่อตัวละครตาย นาฬิกานี้จะหมุนกลับไปที่ศูนย์

Meta Clock จะไม่มีวันรีเซ็ต มันจะเก็บจำนวนทองทั้งหมดที่หาได้จากการเล่นทุกครั้ง, เวฟที่สูงที่สุดที่เคยไปถึง และการอัปเกรดถาวรทุกอย่างที่ซื้อไป นาฬิกานี้จะยังคงเดินต่อไปไม่ว่าคุณจะรีเฟรชเบราว์เซอร์กี่ครั้งก็ตาม

เมื่อนำสองสิ่งนี้มาผสมกัน คุณจะได้บั๊กที่ติดตามยากและแก้ไขได้ลำบาก ผมเคยเห็นนักพัฒนาเผลอลบความคืบหน้าของผู้เล่นโดยไม่ตั้งใจระหว่างการรีเซ็ตฉากตามปกติ เพราะฟังก์ชันทำความสะอาด (cleanup function) ไปแตะต้องที่เก็บข้อมูลผิดที่ ข้อมูลของ Meta Clock ระเหยหายไป ผู้เล่นกลับไปสู่จุดที่มีทองเป็นศูนย์และไม่มีการอัปเกรดใดๆ ณ จุดนั้น ความสัมพันธ์ระหว่างคุณกับผู้เล่นจะขาดสะบั้น พวกเขาไม่ได้กำลังเริ่มเล่นรอบใหม่ แต่พวกเขากำลังเริ่มสะสมความแค้นใหม่

การแยกนาฬิกาทั้งสองออกจากกันไม่ใช่แค่ทางเลือกด้านสไตล์ แต่มันคือกลยุทธ์เพื่อความอยู่รอด

Phaser v4 จัดการกับการแยกส่วนนี้อย่างไร

ผมสร้าง Neon Survivor ด้วย Phaser v4 ซึ่งมีเครื่องมือเฉพาะสองอย่างสำหรับปัญหานี้

Registry จะเก็บข้อมูลสดไว้ในหน่วยความจำสำหรับเซสชันปัจจุบัน มันรวดเร็ว มันเรียบง่าย และมันจะระเหยหายไปทันทีที่ผู้เล่นรีเฟรชหน้าเว็บ

LocalStorage จะบันทึกข้อมูลลงในเบราว์เซอร์โดยตรง มันสามารถอยู่รอดได้แม้จะปิดแท็บ, รีสตาร์ทเบราว์เซอร์ หรือไฟดับ แต่มันก็ช้ากว่าและมีความน่าเชื่อถือน้อยกว่า เบราว์เซอร์อาจบล็อกมัน, จำกัดความเร็ว หรือลบมันทิ้งหากพื้นที่จัดเก็บเต็ม

การตัดสินใจในการออกแบบของผมนั้นเข้มงวดมาก Registry คือแหล่งข้อมูลที่ถูกต้องเพียงหนึ่งเดียว (source of truth) ในระหว่างการเล่นเกม ตัวเกมจะอ่านข้อมูลจากมัน เขียนข้อมูลลงในมัน และเชื่อใจมันอย่างสมบูรณ์ LocalStorage ไม่ได้ทำหน้าที่เป็นผู้เขียนร่วม (co-author) แต่มันทำหน้าที่เป็นกระจกเงา (mirror)

นี่คือขั้นตอนการทำงาน: เกมจะเขียนการซื้ออัปเกรดลงใน Registry จากนั้นจะมีคลาส Manager เพียงคลาสเดียวที่คอยเฝ้าดู Registry เมื่อถึงเวลาที่เหมาะสม Manager นั้นจะทำการสะท้อน (mirror) ข้อมูลจาก Registry ไปยัง LocalStorage หากเบราว์เซอร์บล็อกการเขียน ข้อมูลในเกมก็จะไม่กระตุก หากการจัดเก็บข้อมูลล้มเหลว เซสชันปัจจุบันก็ยังคงเล่นได้อย่างสมบูรณ์ ผู้เล่นอาจสูญเสียความคืบหน้าก็ต่อเมื่อพวกเขาปิดแท็บในวินาทีเดียวกันเป๊ะๆ เท่านั้น แต่ตัวเซสชันเองจะไม่แครชเลย

รูปแบบนี้ช่วยป้องกันหายนะที่แนบเนียน หากคุณปล่อยให้ทุกระบบเขียนข้อมูลลงใน LocalStorage โดยตรง คุณกำลังสร้างความพึ่งพาต่อ API ที่เปราะบาง ผู้เล่นที่ตั้งค่าความเป็นส่วนตัวไว้สูงหรือใช้อุปกรณ์ที่มีพื้นที่จัดเก็บน้อยอาจพบว่าเกมช้าลงหรือค้างระหว่างการต่อสู้ เพราะมีฟังก์ชันเบื้องหลังบางอย่างพยายามบันทึกค่าสถิติ การทำให้ Registry เป็นแหล่งข้อมูลเพียงหนึ่งเดียวจะช่วยให้การต่อสู้ยังคงรวดเร็วและควบคุมความเสี่ยงได้

ปล่อยให้โค้ดได้หายใจ

ผมยังใช้ event เพื่อแยกส่วนระบบออกจากกัน (decouple the systems) เมื่อการเล่นรอบนั้นสิ้นสุดลง GameScene จะไม่จัดการงานศพของตัวเอง มันจะไม่เรียกฟังก์ชันบันทึกข้อมูล (save function) และไม่นำเข้าเครื่องมือจัดเก็บข้อมูล (storage utility) แต่มันจะเพียงแค่ส่ง (emit) event "run-ended" พร้อมกับ payload ของข้อมูลที่เกี่ยวข้องออกไปเท่านั้น

Listener แยกต่างหากจะทำหน้าที่จัดการข้อมูล โดยจะรับเหตุการณ์ (event) อัปเดต Meta Clock และสั่งให้ manager ทำการคัดลอกยอดรวมใหม่ไปยัง LocalStorage

การแยกส่วนนี้ให้ผลลัพธ์ที่คุ้มค่าทันที ผมสามารถเขียน GameScene ใหม่ทั้งหมด เปลี่ยนตัวละครผู้เล่น เปลี่ยนมุมกล้อง หรือแม้แต่เปลี่ยนแนวเกมจาก survival เป็น bullet hell ได้โดยไม่ต้องแตะต้องระบบบันทึกข้อมูลเลย ระบบต่างๆ ทำงานเป็นอิสระต่อกัน พวกมันสื่อสารกันผ่าน event ไม่ใช่การเรียกใช้ function โดยตรง ซึ่งหมายถึงการเกิด merge conflict ที่น้อยลง บั๊กที่น้อยลง และ codebase ที่ไม่กลายเป็นโค้ดที่พันกันยุ่งเหยิง (spaghetti) หลังจากผ่านไปหกเดือน

การอัปเกรดที่เปลี่ยนรูปแบบการเล่น

การมีโครงสร้างทางเทคนิคที่ดีจะไม่มีประโยชน์เลยหากรางวัลที่ได้รับให้ความรู้สึกเหมือนแค่ตัวเลขในสเปรดชีต ผมจึงใช้เวลาอย่างมากไปกับการทำให้การอัปเกรดนั้นให้ความรู้สึกที่แตกต่างเมื่อนำไปเล่นจริง

การอัปเกรดบางอย่างนั้นปลอดภัย เช่น เพิ่มความเร็วในการเคลื่อนที่ เพิ่มพลังชีวิต หรือรีโหลดกระสุนได้เร็วขึ้น สิ่งเหล่านี้ช่วยเพิ่มโอกาสให้ผู้เล่นเมื่อเกิดข้อผิดพลาด มันทำให้รู้สึกอุ่นใจ และช่วยให้เกมง่ายขึ้นโดยไม่ต้องเปลี่ยนกฎเกณฑ์เดิม

แต่การอัปเกรดอื่นๆ จะเป็นการเขียนกฎขึ้นมาใหม่ทั้งหมด ในเกม Neon Survivor ผมได้เพิ่ม "Piercing Rounds" เข้าไป ก่อนที่จะมีการอัปเกรดนี้ กระสุนจะหยุดลงเมื่อกระทบศัตรูตัวแรก แต่หลังจากอัปเกรดแล้ว มันจะพุ่งทะลุผ่านศัตรูไปได้ ซึ่งอาจกวาดล้างศัตรูได้ทั้งแถวในการยิงเพียงนัดเดียว

ความแตกต่างนั้นชัดเจนมาก ความเร็วและพลังชีวิตอาจช่วยให้คุณอยู่รอดได้นานขึ้น แต่ Piercing Rounds จะเปลี่ยนวิธีที่คุณจัดตำแหน่งตัวเอง คุณจะเริ่มวางแผนล่อศัตรูมาเรียงกัน คุณจะเลิกวิ่งวนล่อศัตรู (kiting) อยู่แค่ขอบจอแล้วเริ่มบุกทะลวงผ่านตรงกลางแทน พื้นที่ในการตัดสินใจของเกมจึงขยายกว้างขึ้น

การพัฒนาตัวละครที่ดีควรเปลี่ยนการตัดสินใจของผู้เล่น ไม่ใช่แค่เพิ่มตัวเลข หากทุกการอัปเกรดเป็นเพียงการเพิ่มค่าเป็นเปอร์เซ็นต์ ผู้เล่นก็จะเลิกอ่านคำอธิบาย พวกเขาแค่คลิก อัปเกรด แล้วก็ลืมไป แต่ถ้าการอัปเกรดทำให้พวกเขาต้องคิดทบทวนกลยุทธ์ใหม่ พวกเขาจะจดจำมัน พวกเขาจะพูดถึงมัน และจะกลับมาดูว่ามีอะไรอีกที่จะสามารถพลิกโฉมการเล่นของเกมได้

ผลลัพธ์ที่แท้จริง

ลูป "ขออีกรอบ" (one more run) ไม่ใช่ระบบเดี่ยวๆ แต่มันคือความสัมพันธ์ระหว่างความสูญเสียและการได้รับ ซึ่งถูกสร้างขึ้นบน clean architecture และรางวัลที่มีความหมาย

จงสร้างสองไทม์ไลน์ที่แยกจากกัน และปกป้อง Meta Clock ราวกับว่ามันคือความไว้วางใจของผู้เล่น เพราะมันเป็นเช่นนั้นจริงๆ ใช้เครื่องมือใน engine ของคุณเพื่อให้ข้อมูลแบบ live data ทำงานได้รวดเร็ว และข้อมูลแบบ persistent data มีความปลอดภัย แยกส่วน (decouple) scene ออกจากระบบจัดเก็บข้อมูล เพื่อที่คุณจะได้พัฒนา (iterate) ต่อไปได้โดยไม่ต้องกังวล และเมื่อคุณออกแบบการอัปเกรด ให้ถามตัวเองว่าสิ่งนั้นช่วยให้ผู้เล่นมีเวลามากขึ้น หรือช่วยให้พวกเขามีทางเลือกที่น่าสนใจมากขึ้นกันแน่

หากคุณทำสิ่งนี้ได้ถูกต้อง ผู้เล่นจะไม่ใช่แค่ยอมรับความตาย แต่พวกเขาจะโหยหามัน ทุกการเล่นจะกลายเป็นการลงทุนเพื่อการเล่นในรอบถัดไป เกมจะไม่ใช่แค่การเริ่มใหม่ซ้ำแล้วซ้ำเล่า แต่จะกลายเป็นการปีนป่ายที่ต่อเนื่องและเป็นหนึ่งเดียว