กรณีศึกษา - Bouncy Mouse

บทนำ

Bouncy Mouse

หลังจากเผยแพร่ Bouncy Mouse ใน iOS และ Android เมื่อช่วงสิ้นปีที่ผ่านมา เราได้รับบทเรียนสำคัญ 2-3 ข้อ สิ่งสำคัญอย่างหนึ่งคือ การเจาะตลาดที่มีชื่อเสียงนั้นเป็นเรื่องยาก ในตลาด iPhone ที่เต็มไปด้วยแอปมากมาย การดึงดูดความสนใจนั้นยากมาก แต่ในตลาด Android Marketplace ที่แอปมีจำนวนไม่มากนัก การพัฒนาก็ง่ายขึ้น แต่ก็ยังไม่ง่าย ประสบการณ์นี้ทำให้ฉันเห็นโอกาสที่น่าสนใจใน Chrome เว็บสโตร์ แม้ว่า Web Store จะไม่ว่างเปล่า แต่แคตตาล็อกเกมคุณภาพสูงที่ใช้ HTML5 เพิ่งจะเริ่มเติบโตขึ้น สําหรับนักพัฒนาแอปรายใหม่ การเปลี่ยนแปลงนี้หมายความว่าการสร้างอันดับในชาร์ตและการเพิ่มระดับการเข้าถึงจะง่ายขึ้นมาก เมื่อเห็นโอกาสนี้ เราจึงเริ่มพอร์ต Bouncy Mouse ไปยัง HTML5 หวังว่าจะมอบประสบการณ์การเล่นเกมล่าสุดให้กับฐานผู้ใช้ใหม่ที่น่าตื่นเต้น ในกรณีศึกษานี้ เราจะพูดถึงกระบวนการทั่วไปในการพอร์ต Bouncy Mouse ไปยัง HTML5 จากนั้นจะเจาะลึกอีกเล็กน้อยใน 3 ด้านที่น่าสนใจ ได้แก่ เสียง ประสิทธิภาพ และการสร้างรายได้

การพอร์ตเกม C++ ไปยัง HTML5

ปัจจุบัน Bouncy Mouse พร้อมให้บริการใน Android(C++), iOS (C++), Windows Phone 7 (C#) และ Chrome (Javascript) บางครั้งคำถามที่ตามมาคือ คุณเขียนเกมที่ย้ายข้อมูลไปยังแพลตฟอร์มต่างๆ ได้อย่างง่ายดายได้อย่างไร เราเข้าใจดีว่าผู้ใช้ต้องการวิธีแก้ปัญหาแบบสำเร็จรูปที่จะช่วยให้โอนข้อมูลได้ในระดับนี้โดยไม่ต้องใช้การโอนด้วยตนเอง ขออภัย เรายังไม่แน่ใจว่ามีโซลูชันดังกล่าวหรือไม่ (สิ่งที่ใกล้เคียงที่สุดน่าจะเป็น เฟรมเวิร์ก PlayN ของ Google หรือเอ็นจิ้น Unity แต่ทั้ง 2 อย่างนี้ก็ไม่ได้ตรงกับเป้าหมายทั้งหมดที่เราสนใจ) แนวทางของฉันคือการพอร์ตด้วยตนเอง ก่อนอื่นเราเขียนเวอร์ชัน iOS/Android เป็น C++ แล้วพอร์ตโค้ดนี้ไปยังแพลตฟอร์มใหม่แต่ละแพลตฟอร์ม แม้ว่าการดำเนินการนี้อาจฟังดูยุ่งยาก แต่เวอร์ชัน WP7 และ Chrome แต่ละเวอร์ชันใช้เวลาไม่เกิน 2 สัปดาห์ คำถามคือมีวิธีใดบ้างที่จะทำให้โค้ดเบสพกพาได้ง่ายๆ สิ่งที่เราทําซึ่งช่วยแก้ปัญหานี้ได้มีดังนี้

เก็บฐานโค้ดให้เล็ก

แม้ว่าเรื่องนี้อาจดูชัดเจน แต่นี่เป็นเหตุผลหลักที่ทำให้ฉันพอร์ตเกมได้อย่างรวดเร็ว โค้ดไคลเอ็นต์ของ Bouncy Mouse มีโค้ด C++ เพียงประมาณ 7,000 บรรทัด โค้ด 7,000 บรรทัดไม่ใช่จำนวนน้อยๆ แต่ก็ยังจัดการได้ โค้ดไคลเอ็นต์ทั้งเวอร์ชัน C# และ JavaScript มีขนาดใกล้เคียงกัน การรักษาโค้ดให้เล็กนั้นอาศัยหลักปฏิบัติสำคัญ 2 ข้อ คือ อย่าเขียนโค้ดเกินความจำเป็น และดำเนินการในโค้ดการประมวลผลก่อนการเรียกใช้ (ไม่ใช่รันไทม์) ให้ได้มากที่สุด การไม่เขียนโค้ดเกินความจำเป็นอาจดูเป็นเรื่องธรรมดา แต่นี่เป็นสิ่งหนึ่งที่ฉันพยายามทำอยู่เสมอ ฉันมักจะอยากเขียนคลาส/ฟังก์ชันตัวช่วยสำหรับทุกอย่างที่รวมไว้ในตัวช่วยได้ อย่างไรก็ตาม โดยทั่วไปแล้ว การใช้ตัวช่วยหลายครั้งจะทําให้โค้ดมีขนาดใหญ่ขึ้น เว้นแต่คุณจะวางแผนที่จะใช้ตัวช่วยหลายครั้งจริงๆ สำหรับ Bouncy Mouse เราระมัดระวังที่จะไม่เขียนตัวช่วย เว้นแต่ว่าจะใช้อย่างน้อย 3 ครั้ง เมื่อเขียนคลาสตัวช่วย ฉันพยายามทำให้คลาสนั้นสะอาด นำไปใช้ได้กับโปรเจ็กต์อื่นๆ และนํากลับมาใช้ใหม่ได้ ในทางกลับกัน เมื่อเขียนโค้ดสําหรับ Bouncy Mouse เท่านั้น ซึ่งมีโอกาสน้อยที่จะนําไปใช้งานซ้ำ เป้าหมายของฉันคือการทํางานเขียนโค้ดให้เสร็จสมบูรณ์อย่างง่ายดายและรวดเร็วที่สุด แม้ว่านี่จะไม่ใช่วิธี "เรียบร้อย" ที่สุดในการแต่งโค้ด ส่วนที่สองและสำคัญกว่าในการทำให้โค้ดฐานมีขนาดเล็กคือการเพิ่มขั้นตอนเตรียมข้อมูลให้มากที่สุด หากย้ายงานรันไทม์ไปยังงานเตรียมการล่วงหน้าได้ เกมจะทำงานได้เร็วขึ้นและคุณไม่จําเป็นต้องพอร์ตโค้ดไปยังแพลตฟอร์มใหม่แต่ละแพลตฟอร์ม ตัวอย่างเช่น เดิมเราจัดเก็บข้อมูลเรขาคณิตของเลเวลในรูปแบบที่ยังไม่ได้ประมวลผลมากนัก โดยประกอบบัฟเฟอร์เวิร์กเท็กซ์ OpenGL/WebGL จริงในรันไทม์ ซึ่งต้องใช้เวลาในการตั้งค่าและโค้ดรันไทม์หลายร้อยบรรทัด ต่อมา เราย้ายโค้ดนี้ไปยังขั้นตอนการเตรียมข้อมูลขั้นต้น โดยเขียนบัฟเฟอร์เวิร์กเท็กซ์ OpenGL/WebGL ที่แพ็กเต็มรูปแบบออกมาในเวลาคอมไพล์ โค้ดจริงมีจำนวนเท่าเดิม แต่มีการย้ายโค้ดหลายร้อยบรรทัดเหล่านั้นไปยังขั้นตอนการประมวลผลก่อน ซึ่งหมายความว่าฉันไม่ต้องพอร์ตโค้ดไปยังแพลตฟอร์มใหม่เลย ตัวอย่างของกรณีนี้ใน Bouncy Mouse มีมากมาย และสิ่งที่เป็นไปได้จะแตกต่างกันไปในแต่ละเกม แต่ให้คอยสังเกตสิ่งที่ไม่จำเป็นที่จะต้องทำขณะรันไทม์

อย่าใช้ Dependency ที่ไม่จำเป็น

อีกเหตุผลหนึ่งที่ทำให้ Bouncy Mouse ย้ายข้อมูลได้ง่ายคือแทบไม่มีทรัพยากรที่ต้องพึ่งพา แผนภูมิต่อไปนี้สรุปไลบรารีหลักที่ต้องพึ่งพาของ Bouncy Mouse ตามแพลตฟอร์ม

Android iOS HTML5 WP7
กราฟิก OpenGL ES OpenGL ES WebGL XNA
เสียง OpenSL ES OpenAL เสียงบนเว็บ XNA
ฟิสิกส์ Box2D Box2D Box2D.js Box2D.xna

เท่านี้เอง ไม่ได้ใช้ไลบรารีขนาดใหญ่ของบุคคลที่สาม ยกเว้น Box2D ซึ่งนำไปใช้กับทุกแพลตฟอร์มได้ สำหรับกราฟิก ทั้ง WebGL และ XNA จะแมปกับ OpenGL เกือบ 1:1 จึงไม่ใช่ปัญหาใหญ่ เฉพาะในส่วนของเสียงเท่านั้นที่ไลบรารีจริงจะแตกต่างกัน อย่างไรก็ตาม โค้ดเสียงใน Bouncy Mouse มีขนาดเล็ก (โค้ดเฉพาะแพลตฟอร์มประมาณ 100 บรรทัด) จึงไม่ใช่ปัญหาใหญ่ การทำให้ Bouncy Mouse ไม่มีไลบรารีขนาดใหญ่ที่ย้ายไม่ได้หมายความว่าตรรกะของโค้ดรันไทม์จะเกือบเหมือนกันระหว่างเวอร์ชันต่างๆ (แม้จะมีการเปลี่ยนแปลงภาษา) นอกจากนี้ ยังช่วยให้เราไม่ต้องยึดติดกับเชนเครื่องมือที่ย้ายไม่ได้ เราได้รับคำถามว่าการเขียนโค้ดกับ OpenGL/WebGL โดยตรงทำให้เกิดความซับซ้อนมากขึ้นเมื่อเทียบกับการใช้ไลบรารีอย่าง Cocos2D หรือ Unity หรือไม่ (มีเครื่องมือช่วยสำหรับ WebGL อยู่ด้วย) อันที่จริงแล้ว เราเชื่อว่าตรงกันข้าม เกมบนโทรศัพท์มือถือ / HTML5 ส่วนใหญ่ (อย่างน้อยเกมอย่าง Bouncy Mouse) นั้นเล่นง่ายมาก ในกรณีส่วนใหญ่ เกมจะวาดสไปรท์เพียงไม่กี่รายการและอาจมีเรขาคณิตที่มีพื้นผิว ผลรวมของโค้ดเฉพาะ OpenGL ใน Bouncy Mouse น่าจะน้อยกว่า 1,000 บรรทัด เราประหลาดใจหากการใช้ไลบรารีตัวช่วยจะลดจำนวนนี้ลงได้ แม้ว่าจะลดจำนวนนี้ลงครึ่งหนึ่งได้ แต่ฉันก็ต้องใช้เวลาอย่างมากในการเรียนรู้ไลบรารี/เครื่องมือใหม่เพื่อประหยัดโค้ด 500 บรรทัด นอกจากนี้ เรายังไม่พบไลบรารีตัวช่วยที่นำไปใช้ได้กับทุกแพลตฟอร์มที่เราสนใจ ดังนั้นการใช้ไลบรารีดังกล่าวจะส่งผลเสียต่อความสามารถในการใช้งานข้ามแพลตฟอร์มอย่างมาก หากฉันเขียนเกม 3 มิติที่ต้องใช้ไลท์แมป, LOD แบบไดนามิก, ภาพเคลื่อนไหวแบบสกิน และอื่นๆ คำตอบของฉันจะเปลี่ยนไปอย่างแน่นอน ในกรณีนี้ ฉันจะต้องเขียนโค้ดทั้งเครื่องมือด้วยตนเองเพื่อใช้กับ OpenGL สิ่งที่เราต้องการจะสื่อคือเกมบนอุปกรณ์เคลื่อนที่/HTML5 ส่วนใหญ่ยังไม่อยู่ในหมวดหมู่นี้ จึงไม่จำเป็นต้องทำให้เรื่องยุ่งยากจนกว่าจำเป็น

อย่าประเมินความคล้ายคลึงระหว่างภาษาต่ำไป

เคล็ดลับสุดท้ายที่ช่วยให้ฉันประหยัดเวลาได้มากในการพอร์ตโค้ด C++ ไปยังภาษาใหม่คือการตระหนักว่าโค้ดส่วนใหญ่เกือบจะเหมือนกันทุกภาษา แม้ว่าองค์ประกอบหลักบางอย่างอาจเปลี่ยนแปลง แต่องค์ประกอบเหล่านี้มีจำนวนน้อยกว่าองค์ประกอบที่ไม่เปลี่ยนแปลง ความจริงแล้ว การเปลี่ยนจาก C++ เป็น JavaScript สำหรับฟังก์ชันหลายรายการนั้นทำได้ง่ายๆ เพียงเรียกใช้การเปลี่ยนนิพจน์ทั่วไป 2-3 รายการในโค้ดฐาน C++

สรุปการย้ายข้อมูล

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

เสียง

ปัญหาอย่างหนึ่งที่ทำให้เรา (และดูเหมือนว่าทุกคน) ลำบากใจคือเสียง ใน iOS และ Android มีตัวเลือกเสียงที่ยอดเยี่ยมหลายรายการ (OpenSL, OpenAL) แต่ในโลกของ HTML5 สถานการณ์ดูจะแย่กว่า แม้ว่าเสียง HTML5 จะพร้อมใช้งาน แต่เราพบว่าเสียงดังกล่าวมีปัญหาบางอย่างที่ทำให้ใช้งานไม่ได้เมื่อใช้ในเกม ฉันพบปัญหาการทำงานที่แปลกประหลาดบ่อยครั้ง แม้จะใช้เบราว์เซอร์เวอร์ชันล่าสุดก็ตาม เช่น Chrome ดูเหมือนจะจํากัดจํานวนองค์ประกอบเสียง (แหล่งที่มา) ที่คุณสร้างพร้อมกันได้ นอกจากนี้ บางครั้งเสียงที่เล่นก็อาจบิดเบี้ยวอย่างไม่มีเหตุผล โดยรวมแล้ว เราค่อนข้างกังวล การค้นหาทางออนไลน์แสดงให้เห็นว่าเกือบทุกคนพบปัญหาเดียวกัน โซลูชันที่เราพบในตอนแรกคือ API ชื่อ SoundManager2 API นี้ใช้เสียง HTML5 หากมี และจะใช้ Flash ในกรณีที่มีปัญหา แม้ว่าวิธีนี้จะใช้งานได้ แต่ก็ยังมีข้อบกพร่องและคาดเดาไม่ได้ (น้อยกว่าเสียง HTML5 ล้วนๆ) 1 สัปดาห์หลังจากเปิดตัว ฉันได้พูดคุยกับเจ้าหน้าที่ที่ช่วยเหลือดีคนหนึ่งของ Google ซึ่งแนะนำให้ฉันใช้ Web Audio API ของ Webkit ตอนแรกเราเคยพิจารณาที่จะใช้ API นี้ แต่กลับไม่ใช้เนื่องจากความซับซ้อนที่ไม่จำเป็น (สำหรับเรา) ของ API ฉันแค่ต้องการเล่นเสียง 2-3 รายการ: โดยใช้เสียง HTML5 จะใช้ JavaScript เพียง 2-3 บรรทัด อย่างไรก็ตาม เมื่อได้ดู Web Audio คร่าวๆ แล้ว ฉันรู้สึกประหลาดใจกับข้อกำหนดที่ยาวมาก (70 หน้า) ตัวอย่างเพลงบนเว็บมีจำนวนน้อย (เป็นเรื่องปกติสำหรับ API ใหม่) และไม่มีฟังก์ชัน "เล่น" "หยุดชั่วคราว" หรือ "หยุด" ในข้อกำหนด เมื่อ Google รับรองว่าความกังวลของฉันไม่มีมูลความจริง ฉันจึงศึกษา API นี้อีกครั้ง หลังจากดูตัวอย่างเพิ่มเติมและทำการวิจัยเพิ่มเติม พบว่า Google พูดถูก API นี้ตอบโจทย์ความต้องการของฉันได้อย่างแน่นอน และทำงานได้โดยไม่มีข้อบกพร่องที่รบกวน API อื่นๆ บทความที่มีประโยชน์อย่างยิ่งคือบทความเริ่มต้นใช้งาน Web Audio API ซึ่งเป็นแหล่งข้อมูลที่ยอดเยี่ยมหากคุณต้องการทําความเข้าใจ API ให้ดียิ่งขึ้น ปัญหาที่แท้จริงของฉันคือ แม้จะทำความเข้าใจและใช้ API แล้ว แต่ฉันก็ยังรู้สึกว่า API นี้ไม่ได้ออกแบบมาเพื่อ "เล่นเสียงเพียงไม่กี่เสียง" ฉันจึงเขียนคลาสตัวช่วยเล็กๆ ขึ้นมาเพื่อให้ใช้ API ได้ตรงกับความต้องการของฉัน ไม่ว่าจะเป็นการเล่น หยุดชั่วคราว หยุด และค้นหาสถานะของเสียง เราตั้งชื่อคลาสตัวช่วยนี้ว่า AudioClip แหล่งที่มาแบบเต็มมีใน GitHub ภายใต้สัญญาอนุญาต Apache 2.0 และเราจะพูดถึงรายละเอียดของคลาสด้านล่าง แต่ก่อนอื่น เราขออธิบายข้อมูลเบื้องต้นเกี่ยวกับ Web Audio API

กราฟ Web Audio

สิ่งแรกที่ทําให้ Web Audio API ซับซ้อนกว่า (และมีประสิทธิภาพมากกว่า) องค์ประกอบเสียง HTML5 คือความสามารถในการประมวลผล / มิกซ์เสียงก่อนที่จะส่งออกไปยังผู้ใช้ แม้ว่าจะมีประสิทธิภาพ แต่การที่การเล่นเสียงใดๆ ก็ตามเกี่ยวข้องกับกราฟก็ทําให้สถานการณ์ที่ซับซ้อนเล็กน้อยมีความซับซ้อนมากขึ้น กราฟต่อไปนี้แสดงให้เห็นถึงประสิทธิภาพของ Web Audio API

กราฟ Web Audio พื้นฐาน
กราฟเสียงบนเว็บพื้นฐาน

แม้ว่าตัวอย่างข้างต้นจะแสดงให้เห็นถึงความสามารถของ Web Audio API แต่ฉันไม่จำเป็นต้องใช้ความสามารถส่วนใหญ่นี้ในสถานการณ์ของฉัน ฉันแค่อยากเล่นเสียง แม้ว่าจะต้องใช้กราฟ แต่กราฟก็มีความซับซ้อนน้อยมาก

กราฟไม่จำเป็นต้องซับซ้อน

สิ่งแรกที่ทําให้ Web Audio API ซับซ้อนกว่า (และมีประสิทธิภาพมากกว่า) องค์ประกอบเสียง HTML5 คือความสามารถในการประมวลผล / มิกซ์เสียงก่อนที่จะส่งออกไปยังผู้ใช้ แม้ว่าจะมีประสิทธิภาพ แต่การที่การเล่นเสียงใดๆ ก็ตามเกี่ยวข้องกับกราฟก็ทําให้สถานการณ์ที่ซับซ้อนเล็กน้อยมีความซับซ้อนมากขึ้น กราฟต่อไปนี้แสดงให้เห็นถึงประสิทธิภาพของ Web Audio API

กราฟ Web Audio ที่ไม่สําคัญ
กราฟเสียงในเว็บแบบไม่ซับซ้อน

กราฟที่แสดงด้านบนสามารถทําทุกอย่างที่จําเป็นในการเล่น หยุดชั่วคราว หรือหยุดเสียง

แต่ไม่ต้องกังวลเรื่องกราฟ

แม้ว่าการทำความเข้าใจกราฟจะเป็นเรื่องที่ดี แต่ฉันไม่ต้องการต้องจัดการกับกราฟทุกครั้งที่เล่นเสียง เราจึงเขียนคลาส Wrapper ที่เรียบง่ายชื่อ "AudioClip" คลาสนี้จะจัดการกราฟนี้ภายใน แต่แสดง API ที่แสดงต่อผู้ใช้ได้ง่ายกว่ามาก

AudioClip
AudioClip

คลาสนี้เป็นเพียงกราฟ Web Audio และสถานะตัวช่วยบางอย่าง แต่ช่วยให้ฉันใช้โค้ดที่ง่ายกว่ามากเมื่อเทียบกับกรณีที่ต้องสร้างกราฟ Web Audio เพื่อเล่นเสียงแต่ละรายการ

// At startup time
var sound = new AudioClip("ping.wav");

// Later
sound.play();

รายละเอียดการใช้งาน

มาดูโค้ดของคลาสตัวช่วยกันอย่างรวดเร็ว Constructor – Constructor จะจัดการการโหลดข้อมูลเสียงโดยใช้ XHR แม้ว่าจะไม่แสดงที่นี่ (เพื่อให้ตัวอย่างเข้าใจง่าย) แต่คุณก็ใช้เอลิเมนต์เสียง HTML5 เป็นโหนดแหล่งที่มาได้เช่นกัน ซึ่งจะเป็นประโยชน์อย่างยิ่งสำหรับกลุ่มตัวอย่างขนาดใหญ่ โปรดทราบว่า Web Audio API กำหนดให้เราดึงข้อมูลนี้เป็น "arraybuffer" เมื่อได้รับข้อมูลแล้ว เราจะสร้างบัฟเฟอร์ Web Audio จากข้อมูลนี้ (ถอดรหัสจากรูปแบบเดิมเป็นรูปแบบ PCM รันไทม์)

/**
* Create a new AudioClip object from a source URL. This object can be played,
* paused, stopped, and resumed, like the HTML5 Audio element.
*
* @constructor
* @param {DOMString} src
* @param {boolean=} opt_autoplay
* @param {boolean=} opt_loop
*/
AudioClip = function(src, opt_autoplay, opt_loop) {
// At construction time, the AudioClip is not playing (stopped),
// and has no offset recorded.
this.playing_ = false;
this.startTime_ = 0;
this.loop_ = opt_loop ? true : false;

// State to handle pause/resume, and some of the intricacies of looping.
this.resetTimout_ = null;
this.pauseTime_ = 0;

// Create an XHR to load the audio data.
var request = new XMLHttpRequest();
request.open("GET", src, true);
request.responseType = "arraybuffer";

var sfx = this;
request.onload = function() {
// When audio data is ready, we create a WebAudio buffer from the data.
// Using decodeAudioData allows for async audio loading, which is useful
// when loading longer audio tracks (music).
AudioClip.context.decodeAudioData(request.response, function(buffer) {
    sfx.buffer_ = buffer;
    
    if (opt_autoplay) {
    sfx.play();
    }
});
}

request.send();
}

เล่น – การเล่นเสียงของเรามี 2 ขั้นตอน ได้แก่ การตั้งค่ากราฟการเล่น และการเรียกใช้ "noteOn" เวอร์ชันต่างๆ ในแหล่งที่มาของกราฟ แหล่งที่มาเล่นซ้ำได้เพียงครั้งเดียว เราจึงต้องสร้างแหล่งที่มา/กราฟใหม่ทุกครั้งที่เล่น ความซับซ้อนส่วนใหญ่ของฟังก์ชันนี้มาจากข้อกำหนดที่จำเป็นต่อการกลับมาเล่นคลิปที่หยุดชั่วคราว (this.pauseTime_ > 0) หากต้องการเล่นคลิปที่หยุดชั่วคราวต่อ เราจะใช้ noteGrainOn ซึ่งอนุญาตให้เล่นพื้นที่ย่อยของบัฟเฟอร์ ขออภัย noteGrainOn ไม่ได้โต้ตอบกับการวนซ้ำในลักษณะที่ต้องการสำหรับสถานการณ์นี้ (จะวนซ้ำเฉพาะส่วนย่อย ไม่ใช่ทั้งบัฟเฟอร์) เราจึงต้องแก้ปัญหานี้ด้วยการเล่นคลิปที่เหลือด้วย noteGrainOn จากนั้นเริ่มเล่นคลิปอีกครั้งจากต้นโดยเปิดใช้การวนซ้ำ

/**
* Recreates the audio graph. Each source can only be played once, so
* we must recreate the source each time we want to play.
* @return {BufferSource}
* @param {boolean=} loop
*/
AudioClip.prototype.createGraph = function(loop) {
var source = AudioClip.context.createBufferSource();
source.buffer = this.buffer_;
source.connect(AudioClip.context.destination);

// Looping is handled by the Web Audio API.
source.loop = loop;

return source;
}

/**
* Plays the given AudioClip. Clips played in this manner can be stopped
* or paused/resumed.
*/
AudioClip.prototype.play = function() {
if (this.buffer_ && !this.isPlaying()) {
// Record the start time so we know how long we've been playing.
this.startTime_ = AudioClip.context.currentTime;
this.playing_ = true;
this.resetTimeout_ = null;

// If the clip is paused, we need to resume it.
if (this.pauseTime_ > 0) {
    // We are resuming a clip, so it's current playback time is not correctly
    // indicated by startTime_. Correct this by subtracting pauseTime_.
    this.startTime_ -= this.pauseTime_;
    var remainingTime = this.buffer_.duration - this.pauseTime_;

    if (this.loop_) {
    // If the clip is paused and looping, we need to resume the clip
    // with looping disabled. Once the clip has finished, we will re-start
    // the clip from the beginning with looping enabled
    this.source_ = this.createGraph(false);
    this.source_.noteGrainOn(0, this.pauseTime_, remainingTime)

    // Handle restarting the playback once the resumed clip has completed.
    // *Note that setTimeout is not the ideal method to use here. A better 
    // option would be to handle timing in a more predictable manner,
    // such as tying the update to the game loop.
    var clip = this;
    this.resetTimeout_ = setTimeout(function() { clip.stop(); clip.play() },
                                    remainingTime * 1000);
    } else {
    // Paused non-looping case, just create the graph and play the sub-
    // region using noteGrainOn.
    this.source_ = this.createGraph(this.loop_);
    this.source_.noteGrainOn(0, this.pauseTime_, remainingTime);
    }

    this.pauseTime_ = 0;
} else {
    // Normal case, just creat the graph and play.
    this.source_ = this.createGraph(this.loop_);
    this.source_.noteOn(0);
}
}
}

เล่นเป็นเสียงประกอบ - ฟังก์ชันเล่นด้านบนไม่อนุญาตให้เล่นคลิปเสียงซ้ำซ้อนกันหลายครั้ง (การเล่นครั้งที่ 2 จะเป็นไปได้ก็ต่อเมื่อเล่นคลิปจนจบหรือหยุดเล่นแล้วเท่านั้น) บางครั้งเกมอาจต้องการเล่นเสียงหลายครั้งโดยไม่ต้องรอให้การเล่นแต่ละครั้งเล่นจนจบ (การเก็บเหรียญในเกม ฯลฯ) หากต้องการเปิดใช้ คลาส AudioClip จะมีเมธอด playAsSFX() เนื่องจากการเล่นหลายรายการอาจเกิดขึ้นพร้อมกันได้ การกลับมาเล่นจาก playAsSFX() จึงไม่ได้เชื่อมโยงกับ AudioClip แบบ 1:1 ดังนั้นจึงไม่สามารถหยุด หยุดชั่วคราว หรือค้นหาสถานะการเล่นได้ นอกจากนี้ ระบบจะปิดใช้การเล่นซ้ำด้วย เนื่องจากไม่มีวิธีหยุดเสียงที่เล่นซ้ำในลักษณะนี้

/**
* Plays the given AudioClip as a sound effect. Sound Effects cannot be stopped
* or paused/resumed, but can be played multiple times with overlap.
* Additionally, sound effects cannot be looped, as there is no way to stop
* them. This method of playback is best suited to very short, one-off sounds.
*/
AudioClip.prototype.playAsSFX = function() {
if (this.buffer_) {
var source = this.createGraph(false);
source.noteOn(0);
}
}

หยุด หยุดชั่วคราว และสถานะการค้นหา – ฟังก์ชันที่เหลือค่อนข้างตรงไปตรงมาและไม่จําเป็นต้องอธิบายมากนัก

/**
* Stops an AudioClip , resetting its seek position to 0.
*/
AudioClip.prototype.stop = function() {
if (this.playing_) {
this.source_.noteOff(0);
this.playing_ = false;
this.startTime_ = 0;
this.pauseTime_ = 0;
if (this.resetTimeout_ != null) {
    clearTimeout(this.resetTimeout_);
}
}
}

/**
* Pauses an AudioClip. The offset into the stream is recorded to allow the
* clip to be resumed later.
*/
AudioClip.prototype.pause = function() {
if (this.playing_) {
this.source_.noteOff(0);
this.playing_ = false;
this.pauseTime_ = AudioClip.context.currentTime - this.startTime_;
this.pauseTime_ = this.pauseTime_ % this.buffer_.duration;
this.startTime_ = 0;
if (this.resetTimeout_ != null) {
    clearTimeout(this.resetTimeout_);
}
}
}

/**
* Indicates whether the sound is playing.
* @return {boolean}
*/
AudioClip.prototype.isPlaying = function() {
var playTime = this.pauseTime_ +
            (AudioClip.context.currentTime - this.startTime_);

return this.playing_ && (this.loop_ || (playTime < this.buffer_.duration));
}

เสียงสรุป

เราหวังว่าคลาสตัวช่วยนี้จะเป็นประโยชน์ต่อนักพัฒนาแอปที่พบปัญหาเกี่ยวกับเสียงเช่นเดียวกับเรา นอกจากนี้ ชั้นเรียนแบบนี้ยังเหมาะที่จะเริ่มต้นใช้งานด้วย แม้ว่าคุณจะต้องเพิ่มฟีเจอร์ที่มีประสิทธิภาพมากขึ้นของ Web Audio API ไม่ว่าจะด้วยวิธีใด โซลูชันนี้ก็ได้ตอบโจทย์ความต้องการของ Bouncy Mouse และทำให้เกมเป็นเกม HTML5 อย่างแท้จริง

ประสิทธิภาพ

อีกเรื่องหนึ่งที่ฉันกังวลเกี่ยวกับพอร์ต JavaScript คือประสิทธิภาพ หลังจากพอร์ตเวอร์ชัน 1 เสร็จแล้ว เราพบว่าทุกอย่างทำงานได้ตามปกติบนเดสก์ท็อปแบบ Quad-Core แต่น่าเสียดายที่ประสบการณ์การใช้งานบนเน็ตบุ๊กหรือ Chromebook นั้นไม่ค่อยดีนัก ในกรณีนี้ เครื่องมือวิเคราะห์ของ Chrome ช่วยฉันได้โดยแสดงว่าโปรแกรมทั้งหมดใช้เวลาไปที่ไหนบ้าง ประสบการณ์ของฉันเน้นย้ำถึงความสำคัญของการสร้างโปรไฟล์ก่อนทำการเพิ่มประสิทธิภาพ เราคาดว่าฟิสิกส์ของ Box2D หรือโค้ดการแสดงผลจะเป็นสาเหตุหลักที่ทำให้ช้าลง แต่เวลาส่วนใหญ่กลับหมดไปกับฟังก์ชัน Matrix.clone() เนื่องจากเกมของฉันมีเนื้อหาทางคณิตศาสตร์มาก ฉันจึงรู้ว่าต้องสร้าง/โคลนเมทริกซ์เป็นจำนวนมาก แต่ไม่เคยคาดคิดว่านี่จะเป็นจุดคอขวด สุดท้ายแล้ว การเปลี่ยนแปลงที่ง่ายมากนี้ช่วยให้เกมลดการใช้ CPU ได้มากกว่า 3 เท่า จาก 6-7% บนเดสก์ท็อปเป็น 2% ข้อมูลนี้อาจเป็นสิ่งที่นักพัฒนาซอฟต์แวร์ JavaScript ทราบกันดี แต่ในฐานะนักพัฒนาซอฟต์แวร์ C++ ปัญหานี้ทำให้ฉันประหลาดใจ ฉันจึงจะอธิบายรายละเอียดเพิ่มเติม โดยพื้นฐานแล้ว คลาสเมทริกซ์เดิมของฉันคือเมทริกซ์ 3x3: อาร์เรย์ 3 องค์ประกอบ โดยแต่ละองค์ประกอบมีอาร์เรย์ 3 องค์ประกอบ แต่ข้อเสียคือเมื่อถึงเวลาโคลนเมทริกซ์ ฉันต้องสร้างอาร์เรย์ใหม่ 4 รายการ การเปลี่ยนแปลงเพียงอย่างเดียวที่ฉันต้องทำคือย้ายข้อมูลนี้ไปยังอาร์เรย์ 9 องค์ประกอบรายการเดียวและอัปเดตคณิตศาสตร์ให้สอดคล้องกัน การเปลี่ยนแปลงเพียงอย่างเดียวนี้ส่งผลต่อการลดการใช้ CPU 3 เท่าที่เราเห็น และหลังจากการเปลี่ยนแปลงนี้ ประสิทธิภาพของฉันก็อยู่ในระดับที่ยอมรับได้สำหรับอุปกรณ์ทดสอบทั้งหมด

การเพิ่มประสิทธิภาพเพิ่มเติม

แม้ว่าประสิทธิภาพจะยอมรับได้ แต่ก็ยังพบปัญหาเล็กๆ น้อยๆ หลังจากทำโปรไฟล์เพิ่มเติม เราพบว่าปัญหานี้เกิดจากการเก็บขยะของ JavaScript แอปของฉันทำงานที่ 60 FPS ซึ่งหมายความว่าแต่ละเฟรมมีเวลาวาดเพียง 16 มิลลิวินาที อย่างไรก็ตาม เมื่อมีการเรียกใช้การเก็บขยะในเครื่องที่ช้ากว่า บางครั้งอาจใช้เวลาประมาณ 10 มิลลิวินาที ส่งผลให้เกมกระตุกทุกๆ 2-3 วินาที เนื่องจากเกมต้องใช้เกือบ 16 มิลลิวินาทีในการวาดเฟรมเต็ม เราใช้เครื่องมือตรวจสอบกองขยะของ Chrome เพื่อให้ทราบสาเหตุที่ทำให้เกิดขยะจำนวนมาก เรารู้สึกสิ้นหวังมากเมื่อพบว่าขยะส่วนใหญ่ (มากกว่า 70%) เกิดจาก Box2D การกำจัดขยะใน JavaScript เป็นงานที่ยาก และเราไม่สามารถเขียน Box2D ขึ้นมาใหม่ได้ จึงพบว่าตัวเองกำลังตกอยู่ในสถานการณ์ที่ลำบาก แต่โชคดีที่ยังมีเคล็ดลับเก่าๆ อยู่อย่างหนึ่ง ซึ่งก็คือเมื่อเล่นที่ 60fps ไม่ได้ ให้เล่นที่ 30fps เป็นที่ยอมรับกันโดยทั่วไปว่าการเล่นเกมที่ 30fps สม่ำเสมอนั้นดีกว่าการเล่นเกมที่ 60fps กระตุก อันที่จริงเรายังไม่ได้รับคำร้องเรียนหรือความคิดเห็นใดๆ ว่าเกมทำงานที่ 30fps (ซึ่งบอกได้ยากมาก เว้นแต่คุณจะเปรียบเทียบทั้ง 2 เวอร์ชันควบคู่กัน) การเพิ่ม 16 มิลลิวินาทีต่อเฟรมนี้หมายความว่าแม้ในกรณีที่มีการรวบรวมขยะอย่างน่าเกลียด แต่เราก็ยังมีเวลาเหลือเฟือในการแสดงผลเฟรม แม้ว่า Timing API ที่ใช้ (requestAnimationFrame ที่ยอดเยี่ยมของ WebKit) จะไม่เปิดใช้การทำงานที่ 30 fps อย่างชัดเจน แต่ก็สามารถดำเนินการดังกล่าวได้ง่ายๆ แม้ว่าอาจไม่มีประสิทธิภาพเท่ากับ API ที่ชัดเจน แต่คุณก็ทำให้เฟรมเรต 30 fps ได้โดยการทำความเข้าใจว่าช่วงเวลาของ RequestAnimationFrame สอดคล้องกับ VSYNC ของจอภาพ (โดยปกติคือ 60 fps) ซึ่งหมายความว่าเราจะต้องละเว้นการเรียกกลับอื่นๆ ทั้งหมด โดยพื้นฐานแล้ว หากคุณมี "Tick" ที่เป็นคอลแบ็กซึ่งเรียกใช้ทุกครั้งที่ "RequestAnimationFrame" เริ่มทํางาน คุณจะทําสิ่งต่อไปนี้ได้

var skip = false;

function Tick() {
skip = !skip;
if (skip) {
return;
}

// OTHER CODE
}

หากต้องการระมัดระวังเป็นพิเศษ คุณควรตรวจสอบว่า VSYNC ของคอมพิวเตอร์ไม่ได้อยู่ที่หรือต่ำกว่า 30 fps เมื่อเริ่มต้น และปิดใช้การข้ามในกรณีนี้ อย่างไรก็ตาม เรายังไม่พบปัญหานี้ในการกําหนดค่าเดสก์ท็อป/แล็ปท็อปที่ทดสอบ

การจัดจำหน่ายและการสร้างรายได้

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

Bouncy Mouse เปิดตัวไปเมื่อช่วงสิ้นเดือนตุลาคมใน Chrome เว็บสโตร์ การเผยแพร่ใน Chrome เว็บสโตร์ช่วยให้ฉันใช้ประโยชน์จากระบบที่มีอยู่สำหรับความสามารถในการค้นพบ การมีส่วนร่วมของชุมชน การจัดอันดับ และฟีเจอร์อื่นๆ ที่คุ้นเคยในแพลตฟอร์มอุปกรณ์เคลื่อนที่ สิ่งที่ทำให้ฉันประหลาดใจคือความครอบคลุมของร้านค้า ภายใน 1 เดือนหลังจากเปิดตัว เรามียอดติดตั้งเกือบ 4 แสนครั้งและได้รับประโยชน์จากการมีส่วนร่วมของชุมชนแล้ว (การรายงานข้อบกพร่อง ความคิดเห็น) อีกสิ่งหนึ่งที่ทำให้ฉันประหลาดใจคือศักยภาพในการสร้างรายได้ของเว็บแอป

Bouncy Mouse มีวิธีสร้างรายได้ง่ายๆ เพียงวิธีเดียวคือโฆษณาแบนเนอร์ข้างเนื้อหาเกม อย่างไรก็ตาม เมื่อพิจารณาถึงการเข้าถึงที่กว้างขวางของเกม เราพบว่าโฆษณาแบนเนอร์นี้สามารถสร้างรายได้จำนวนมาก และในช่วงที่ได้รับความนิยมสูงสุด แอปสร้างรายได้เทียบเท่ากับแพลตฟอร์มที่ประสบความสำเร็จมากที่สุดของเราอย่าง Android ปัจจัยหนึ่งที่ทำให้รายได้เพิ่มขึ้นคือโฆษณา AdSense ขนาดที่ใหญ่กว่าซึ่งแสดงในเวอร์ชัน HTML5 สร้างรายได้ต่อการแสดงผลสูงกว่าโฆษณา AdMob ขนาดที่เล็กกว่าซึ่งแสดงใน Android อย่างมาก ไม่เพียงเท่านั้น โฆษณาแบนเนอร์ในเวอร์ชัน HTML5 ยังรบกวนสายตาน้อยกว่าในเวอร์ชัน Android มาก ซึ่งช่วยให้ได้รับประสบการณ์การเล่นเกมที่ราบรื่นยิ่งขึ้น โดยรวมแล้ว เรารู้สึกประหลาดใจกับผลลัพธ์นี้

รายได้มาตรฐานในช่วงระยะเวลาหนึ่ง
รายได้ที่ปรับตามมาตรฐานตามช่วงเวลา

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

บทสรุป

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