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

เกริ่นนำ

หนูเด้ง

หลังจากเผยแพร่ Bouncy Mouse บน iOS และ Android เมื่อปลายปีที่แล้ว ผมก็ได้บทเรียนที่สำคัญมาก สิ่งสำคัญอย่างหนึ่งคือ การเจาะตลาดที่มั่นคงนั้นเป็นเรื่องยาก ในตลาด iPhone ที่อิ่มตัวแบบทั่วถึง การได้รับความสนใจนั้นเป็นเรื่องที่ยากมาก ส่วนใน Android Marketplace ที่อิ่มตัวน้อย ความคืบหน้าก็ทำได้ง่ายกว่าแต่ก็ไม่ง่ายนัก จากประสบการณ์นี้ ผมเห็นโอกาสที่น่าสนใจใน Chrome เว็บสโตร์ แม้ว่าเว็บสโตร์จะว่างเปล่าอยู่บ่อยๆ แต่แคตตาล็อกเกมคุณภาพสูงที่ใช้ 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 สัปดาห์จึงจะเสร็จสิ้น ตอนนี้คำถามก็คือ คุณทำอะไรได้บ้างเพื่อให้โค้ดเบสพกพาสะดวก มี 2 สิ่งที่ฉันทำซึ่งช่วยเราได้ในเรื่องนี้

ทำให้ฐานโค้ดมีขนาดเล็ก

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

อย่าใช้การขึ้นต่อกันที่ไม่จำเป็น

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

Android iOS HTML5 WP7
กราฟิก OpenGL ES OpenGL ES WebGL XNA
เสียง OpenSL ES OpenAL เว็บออดิโอ XNA
ฟิสิกส์ กล่อง 2 มิติ กล่อง 2 มิติ Box2D.js Box2D.xna

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

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

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

สรุปการโอน

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

เสียง

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

กราฟเสียงในเว็บ

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

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

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

กราฟอาจเป็นที่เรียบง่าย

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

กราฟเสียงในเว็บเพียงเล็กน้อย
กราฟเสียงบนเว็บที่สำคัญ

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

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

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

AudioClip
AudioClip

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

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

// Later
sound.play();

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

ลองมาดูโค้ดของคลาสตัวช่วยกัน: ตัวสร้าง – ตัวสร้างจะจัดการการโหลดข้อมูลเสียงโดยใช้ 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() จึงไม่เชื่อมโยงแบบ 1:1 กับ AudioClip ดังนั้นจึงหยุดการเล่น หยุดชั่วคราว หรือค้นหาสถานะไม่ได้ การวนซ้ำยังถูกปิดใช้ด้วย เนื่องจากจะไม่มีวิธีหยุดเสียงวนลูปที่เล่นในลักษณะนี้

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

เพิ่มประสิทธิภาพได้มากขึ้น

แม้ว่าประสิทธิภาพของฉันจะเหมาะสม แต่ฉันก็ยังพบอุปสรรคเล็กน้อย หลังจากทำโปรไฟล์อีกเล็กน้อย ฉันก็พบว่านั่นเป็นเพราะระบบเก็บขยะของ JavaScript แอปของฉันทำงานที่ 60 FPS ซึ่งหมายความว่าแต่ละเฟรมมีเวลาวาดเพียง 16 มิลลิวินาที ขออภัย เมื่อเก็บขยะในเครื่องที่ช้ากว่า ก็อาจกินพื้นที่ประมาณ 10 มิลลิวินาที ส่งผลให้ภาพกระตุกในไม่กี่วินาที เนื่องจากเกมใช้เวลาเกือบ 16 มิลลิวินาทีเต็มเพื่อวาดเฟรมทั้งเฟรม ฉันจึงใช้เครื่องมือสร้างโปรไฟล์ฮีปของ Chrome เพื่อทำความเข้าใจว่าทำไมฉันถึงสร้างขยะจำนวนมากได้ ที่น่าเสียดายมากคือผลปรากฏว่าขยะส่วนใหญ่ (มากกว่า 70%) นั้นสร้างขึ้นโดย Box2D การกำจัดขยะใน JavaScript เป็นงานที่ซับซ้อน และการเขียน Box2D ใหม่ก็ไม่ได้มาจากคำถามอีกต่อไป ผมจึงนึกขึ้นได้ว่าตัวเองเจอเรื่องยาก โชคดีที่ยังมีเทคนิคที่เก่าที่สุดเล่มหนึ่งในหนังสือที่เราหาได้ ซึ่งก็คือเมื่อคุณทำสถิติได้ไม่ถึง 60fps ให้เล่นที่ 30fps เป็นที่ตกลงกันค่อนข้างดีว่าการเรียกใช้ที่อัตรา 30 FPS อย่างสม่ำเสมอดีกว่าการใช้ที่ Jitter ที่ 60 FPS อันที่จริงฉันยังไม่ได้รับการร้องเรียนหรือความคิดเห็นเลยว่าเกมนี้เล่นที่ 30fps (ยากที่จะบอก ถ้าคุณไม่เปรียบเทียบสองเวอร์ชันควบคู่กันไป) เวลาอีก 16 มิลลิวินาทีต่อเฟรมทำให้ฉันมีเวลาเหลือเฟือในการแสดงภาพเฟรมจนเก็บขยะไม่สวยงาม ขณะที่ API เวลาที่ฉันกำลังใช้ไม่ได้เปิดใช้งานที่ 30fps อย่างชัดเจน (requestAnimationFrame ยอดเยี่ยมของ WebKit) ก็ทำได้ไม่ยุ่งยาก แม้ว่าอาจจะไม่สวยเท่า 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 ทำให้ฉันเชื่อมโยงถึงกันได้ โปรดส่งอีเมลมาหาฉันหากมีคำถามเพิ่มเติม หรือเพียงแค่แสดงความคิดเห็นด้านล่าง ฉันจะพยายามตรวจสอบความคิดเห็นเหล่านี้เป็นประจำ