กรณีศึกษา - Inside World Wide Maze

World Wide Maze เป็นเกมที่คุณใช้สมาร์ทโฟนเพื่อบังคับลูกบอลกลิ้งผ่านเขาวงกต 3 มิติที่สร้างจากเว็บไซต์ต่างๆ เพื่อไปยังจุดหมาย

World Wide Maze

เกมใช้ฟีเจอร์ HTML5 เป็นจำนวนมาก เช่น เหตุการณ์ DeviceOrientation จะดึงข้อมูลการเอียงจากสมาร์ทโฟน จากนั้นส่งไปยัง PC ผ่าน WebSocket ซึ่งผู้เล่นจะหาทางผ่านพื้นที่ 3 มิติที่สร้างขึ้นโดย WebGL และ Web Workers

ในบทความนี้ เราจะอธิบายวิธีใช้ฟีเจอร์เหล่านี้อย่างละเอียด กระบวนการพัฒนาโดยรวม และประเด็นสําคัญสําหรับการเพิ่มประสิทธิภาพ

DeviceOrientation

เหตุการณ์ DeviceOrientation (ตัวอย่าง) ใช้ดึงข้อมูลการเอียงจากสมาร์ทโฟน เมื่อใช้ addEventListener กับเหตุการณ์ DeviceOrientation ระบบจะเรียกใช้การเรียกกลับที่มีออบเจ็กต์ DeviceOrientationEvent เป็นอาร์กิวเมนต์เป็นระยะๆ โดยช่วงเวลาจะแตกต่างกันไปตามอุปกรณ์ที่ใช้ เช่น ใน iOS + Chrome และ iOS + Safari ระบบจะเรียกใช้การเรียกกลับทุกๆ 1/20 วินาที ส่วนใน Android 4 + Chrome ระบบจะเรียกใช้ทุกๆ 1/10 วินาที

window.addEventListener('deviceorientation', function (e) {
  // do something here..
});

ออบเจ็กต์ DeviceOrientationEvent มีข้อมูลการเอียงสำหรับแกน X, Y และ Z แต่ละแกนเป็นองศา (ไม่ใช่เรเดียน) (อ่านเพิ่มเติมใน HTML5Rocks) อย่างไรก็ตาม ค่าที่แสดงผลจะแตกต่างกันไปตามการผสมผสานของอุปกรณ์และเบราว์เซอร์ที่ใช้ ช่วงที่แสดงผลลัพธ์จริงจะแสดงอยู่ในตารางด้านล่าง

การวางแนวของอุปกรณ์

ค่าที่ไฮไลต์สีน้ำเงินด้านบนคือค่าที่ระบุไว้ในข้อกำหนดของ W3C รายการที่ไฮไลต์ด้วยสีเขียวตรงกับข้อกําหนดเหล่านี้ ส่วนรายการที่ไฮไลต์ด้วยสีแดงจะเบี่ยงเบน สิ่งที่น่าประหลาดใจคือมีเพียงการรวม Android-Firefox เท่านั้นที่แสดงค่าที่ตรงกับข้อกําหนด อย่างไรก็ตาม เมื่อพูดถึงการใช้งาน การปรับค่าที่พบบ่อยจะเหมาะสมกว่า World Wide Maze จึงใช้ค่าที่ iOS แสดงผลเป็นค่ามาตรฐานและปรับค่าสำหรับอุปกรณ์ Android ให้สอดคล้องกัน

if android and event.gamma > 180 then event.gamma -= 360

อย่างไรก็ตาม ฟีเจอร์นี้ยังไม่รองรับ Nexus 10 แม้ว่า Nexus 10 จะแสดงค่าในช่วงเดียวกับอุปกรณ์ Android เครื่องอื่นๆ แต่ก็มีข้อบกพร่องที่แสดงค่าเบต้าและค่าแกมมากลับกัน เรากำลังดำเนินการแก้ไขเรื่องนี้แยกต่างหาก (หรือระบบตั้งค่าเริ่มต้นเป็นแนวนอน)

ดังที่แสดงให้เห็น แม้ว่า API ที่เกี่ยวข้องกับอุปกรณ์จริงจะมีข้อกำหนดเฉพาะที่กำหนดไว้ แต่ก็ไม่ได้รับประกันว่าค่าที่แสดงจะตรงกับข้อกำหนดเหล่านั้น ดังนั้น การทดสอบแอปในอุปกรณ์ที่เป็นไปได้ทั้งหมดจึงเป็นสิ่งที่สําคัญ และยังหมายความว่าอาจมีการป้อนค่าที่ไม่คาดคิด ซึ่งจะต้องหาวิธีแก้ปัญหา World Wide Maze จะแจ้งให้ผู้เล่นที่เล่นเป็นครั้งแรกปรับเทียบอุปกรณ์เป็นขั้นตอนที่ 1 ของบทแนะนำ แต่จะไม่ปรับเทียบเป็นตำแหน่ง 0 อย่างถูกต้องหากได้รับค่าการเอียงที่ไม่คาดคิด ดังนั้นจึงมีการจำกัดเวลาภายในและแจ้งให้ผู้เล่นเปลี่ยนไปใช้การควบคุมด้วยแป้นพิมพ์หากไม่สามารถปรับเทียบภายในเวลาดังกล่าว

WebSocket

ใน World Wide Maze สมาร์ทโฟนและ PC จะเชื่อมต่อกันผ่าน WebSocket กล่าวอย่างถูกต้องคืออุปกรณ์ต่างๆ เชื่อมต่อกันผ่านเซิร์ฟเวอร์รีเลย์ เช่น สมาร์ทโฟนกับเซิร์ฟเวอร์กับ PC เนื่องจาก WebSocket ไม่สามารถเชื่อมต่อเบราว์เซอร์เข้าด้วยกันโดยตรง (การใช้ช่องทางข้อมูล WebRTC ช่วยให้เชื่อมต่อแบบ peer-to-peer ได้และไม่จำเป็นต้องใช้เซิร์ฟเวอร์รีเลย์ แต่ ณ เวลาที่ติดตั้งใช้งาน วิธีนี้ใช้ได้กับ Chrome Canary และ Firefox Nightly เท่านั้น)

เราเลือกใช้ไลบรารีชื่อ Socket.IO (v0.9.11) ซึ่งมีฟีเจอร์สำหรับเชื่อมต่ออีกครั้งในกรณีที่การเชื่อมต่อหมดเวลาหรือถูกตัดการเชื่อมต่อ เราใช้ Socket.IO ร่วมกับ NodeJS เนื่องจากชุดค่าผสม NodeJS + Socket.IO นี้แสดงประสิทธิภาพฝั่งเซิร์ฟเวอร์ที่ดีที่สุดในการทดสอบการใช้งาน WebSocket หลายครั้ง

การจับคู่ตามตัวเลข

  1. PC เชื่อมต่อกับเซิร์ฟเวอร์
  2. เซิร์ฟเวอร์จะสร้างตัวเลขแบบสุ่มให้กับ PC ของคุณและจดจำชุดค่าผสมของตัวเลขและ PC
  3. ระบุหมายเลขและเชื่อมต่อกับเซิร์ฟเวอร์จากอุปกรณ์เคลื่อนที่
  4. หากหมายเลขที่ระบุตรงกับหมายเลขจาก PC ที่เชื่อมต่ออยู่ แสดงว่าอุปกรณ์เคลื่อนที่จับคู่กับ PC เครื่องนั้น
  5. หากไม่มี PC ที่กําหนดไว้ ระบบจะแสดงข้อผิดพลาด
  6. เมื่ออุปกรณ์เคลื่อนที่ได้รับข้อมูล ระบบจะส่งข้อมูลดังกล่าวไปยัง PC ที่จับคู่ไว้ และในทางกลับกัน

หรือจะเชื่อมต่อครั้งแรกจากอุปกรณ์เคลื่อนที่แทนก็ได้ ในกรณีนี้ อุปกรณ์จะสลับกัน

การซิงค์แท็บ

ฟีเจอร์การซิงค์แท็บเฉพาะของ Chrome จะช่วยให้กระบวนการจับคู่ง่ายขึ้น ซึ่งช่วยให้เปิดหน้าเว็บที่เปิดอยู่ใน PC บนอุปกรณ์เคลื่อนที่ได้ง่ายๆ (และในทางกลับกัน) PC จะนำหมายเลขการเชื่อมต่อที่เซิร์ฟเวอร์ออกและต่อท้าย URL ของหน้าเว็บโดยใช้ history.replaceState

history.replaceState(null, null, '/maze/' + connectionNumber)

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

เวลาในการตอบสนอง

เนื่องจากเซิร์ฟเวอร์รีเลย์ตั้งอยู่ในสหรัฐอเมริกา การเข้าถึงเซิร์ฟเวอร์จากญี่ปุ่นจึงทำให้เกิดความล่าช้าประมาณ 200 มิลลิวินาทีก่อนที่ข้อมูลการเอียงของสมาร์ทโฟนจะไปถึง PC เวลาที่ใช้ในการตอบสนองช้ากว่าอย่างชัดเจนเมื่อเทียบกับสภาพแวดล้อมในเครื่องที่ใช้ระหว่างการพัฒนา แต่การแทรกสิ่งต่างๆ เช่น ตัวกรอง Low Pass (ฉันใช้ EMA) ช่วยปรับปรุงเรื่องนี้ให้อยู่ในระดับที่ไม่รบกวน (ในทางปฏิบัติแล้ว จำเป็นต้องใช้ตัวกรอง Low Pass เพื่อวัตถุประสงค์ในการนำเสนอด้วย เนื่องจากค่าที่แสดงผลจากเซ็นเซอร์การเอียงมีสัญญาณรบกวนจำนวนมาก และการใช้ค่าเหล่านั้นกับหน้าจอทำให้ภาพสั่นมาก) แต่การกระโดดไม่ทำงาน ซึ่งเห็นได้ชัดว่าทำงานช้า แต่เราไม่สามารถแก้ปัญหานี้ได้

เนื่องจากเราคาดไว้ว่าจะเกิดปัญหาเกี่ยวกับเวลาในการตอบสนองตั้งแต่ต้น เราจึงพิจารณาตั้งค่ารีเลย์เซิร์ฟเวอร์ทั่วโลกเพื่อให้ไคลเอ็นต์เชื่อมต่อกับเซิร์ฟเวอร์ที่ใกล้ที่สุดได้ (ซึ่งจะช่วยลดเวลาในการตอบสนอง) แต่สุดท้ายเราใช้ Google Compute Engine (GCE) ซึ่งในขณะนั้นมีให้บริการในสหรัฐอเมริกาเท่านั้น จึงทำไม่ได้

ปัญหาเกี่ยวกับอัลกอริทึมของ Nagle

โดยทั่วไป อัลกอริทึมของ Nagle จะรวมอยู่ในระบบปฏิบัติการเพื่อการสื่อสารที่มีประสิทธิภาพโดยการบัฟเฟอร์ที่ระดับ TCP แต่เราพบว่าไม่สามารถส่งข้อมูลแบบเรียลไทม์ได้ขณะที่เปิดใช้อัลกอริทึมนี้ (โดยเฉพาะเมื่อใช้ร่วมกับการตอบกลับที่ล่าช้าของ TCP แม้ว่า ACK จะไม่มีการเลื่อนเวลา แต่ปัญหาเดียวกันนี้อาจเกิดขึ้นหาก ACK เลื่อนเวลาไปในระดับหนึ่งเนื่องจากปัจจัยต่างๆ เช่น เซิร์ฟเวอร์ตั้งอยู่ต่างประเทศ)

ปัญหาเวลาในการตอบสนองของ Nagle ไม่ได้เกิดขึ้นกับ WebSocket ใน Chrome สำหรับ Android ซึ่งมีตัวเลือก TCP_NODELAY สำหรับปิดใช้ Nagle แต่เกิดขึ้นกับ WebKit WebSocket ที่ใช้ใน Chrome สำหรับ iOS ซึ่งไม่ได้เปิดใช้ตัวเลือกนี้ (Safari ซึ่งใช้ WebKit เดียวกันก็มีปัญหานี้เช่นกัน) ปัญหานี้ได้รับการรายงานไปยัง Apple ผ่าน Google และดูเหมือนว่าได้รับการแก้ไขแล้วใน WebKit เวอร์ชันสำหรับนักพัฒนาซอฟต์แวร์

เมื่อเกิดปัญหานี้ขึ้น ระบบจะรวมข้อมูลการเอียงที่ส่งทุกๆ 100 มิลลิวินาทีเป็นกลุ่มที่จะส่งถึง PC ทุกๆ 500 มิลลิวินาทีเท่านั้น เกมจะทำงานไม่ได้ภายใต้เงื่อนไขเหล่านี้ ดังนั้นจึงหลีกเลี่ยงเวลาในการตอบสนองนี้โดยให้ฝั่งเซิร์ฟเวอร์ส่งข้อมูลเป็นช่วงๆ สั้นๆ (ทุกๆ 50 มิลลิวินาทีหรือประมาณนั้น) เราเชื่อว่าการรับ ACK ในช่วงเวลาสั้นๆ จะหลอกลวงอัลกอริทึม Nagle ให้คิดว่าสามารถส่งข้อมูลได้

อัลกอริทึมของ Nagle 1

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

อัลกอริทึมของ Nagle 2

ในทางตรงกันข้าม กราฟนี้แสดงผลลัพธ์ของการใช้เซิร์ฟเวอร์ในสหรัฐอเมริกา ขณะที่ช่วงเวลาของเอาต์พุตสีเขียวคงที่อยู่ที่ 100 มิลลิวินาที ช่วงเวลาของอินพุตจะผันผวนระหว่างต่ำสุด 0 มิลลิวินาทีและสูงสุด 500 มิลลิวินาที ซึ่งบ่งชี้ว่า PC กำลังรับข้อมูลเป็นกลุ่ม

ALT_TEXT_HERE

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

หากพบข้อบกพร่อง

แม้ว่าเบราว์เซอร์เริ่มต้นใน Android 4 (ICS) จะมี WebSocket API แต่ก็ยังเชื่อมต่อไม่ได้ ซึ่งส่งผลให้เกิดเหตุการณ์ connect_failed ของ Socket.IO การเชื่อมต่อหมดเวลาภายใน และฝั่งเซิร์ฟเวอร์ก็ยืนยันการเชื่อมต่อไม่ได้เช่นกัน (เรายังไม่ได้ทดสอบกับ WebSocket เพียงอย่างเดียว จึงอาจเป็นปัญหาเกี่ยวกับ Socket.IO)

การปรับขนาดเซิร์ฟเวอร์รีเลย์

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

ฟิสิกส์

การเคลื่อนไหวของลูกบอลในเกม (กลิ้งลงเนิน ชนกับพื้น ชนกับผนัง เก็บไอเทม ฯลฯ) ทั้งหมดทำด้วยโปรแกรมจำลองฟิสิกส์ 3 มิติ เราใช้ Ammo.js ซึ่งเป็นพอร์ตของเครื่องมือฟิสิกส์ Bullet ที่ใช้กันอย่างแพร่หลายมาไว้ใน JavaScript โดยใช้ Emscripten พร้อมกับ Physijs เพื่อใช้เป็น "Web Worker"

Web Worker

Web Worker คือ API สําหรับเรียกใช้ JavaScript ในเธรดแยกต่างหาก JavaScript ที่เปิดเป็น Web Worker จะทํางานเป็นเธรดแยกต่างหากจากเธรดที่เรียกใช้ครั้งแรก เพื่อให้สามารถทํางานหนักได้ในขณะที่หน้าเว็บยังคงตอบสนอง Physijs ใช้ Web Worker อย่างมีประสิทธิภาพเพื่อช่วยให้เอ็นจิ้นฟิสิกส์ 3 มิติที่ทำงานหนักตามปกติทำงานได้อย่างราบรื่น World Wide Maze จะจัดการกับเอ็นจิ้นฟิสิกส์และการเรนเดอร์ภาพ WebGL ที่อัตราเฟรมแตกต่างกันโดยสิ้นเชิง ดังนั้นแม้ว่าอัตราเฟรมจะลดลงในเครื่องที่มีประสิทธิภาพต่ำเนื่องจากภาระการเรนเดอร์ WebGL มาก แต่เอ็นจิ้นฟิสิกส์จะยังคงรักษาอัตราเฟรมไว้ที่ 60 fps และไม่ส่งผลต่อการควบคุมเกม

FPS

รูปภาพนี้แสดงอัตราเฟรมที่ได้ใน Lenovo G570 ช่องด้านบนแสดงอัตราเฟรมของ WebGL (การแสดงผลภาพ) และช่องด้านล่างแสดงอัตราเฟรมของเอ็นจิ้นฟิสิกส์ GPU เป็นชิป Intel HD Graphics 3000 แบบรวม ดังนั้นอัตราเฟรมการแสดงผลภาพจึงไม่ถึง 60 fps ที่คาดไว้ อย่างไรก็ตาม เนื่องจากเอนจิ้นฟิสิกส์มีอัตราเฟรมตามที่คาดไว้ เกมเพลย์จึงไม่ได้แตกต่างจากประสิทธิภาพบนเครื่องที่มีประสิทธิภาพสูงมากนัก

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

Service Worker

Chrome เวอร์ชันล่าสุดให้คุณตั้งจุดหยุดพักเมื่อเปิดใช้งาน Web Worker ซึ่งมีประโยชน์สำหรับการแก้ไขข้อบกพร่องด้วย ซึ่งอยู่ในแผง "Workers" ในเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์

ประสิทธิภาพ

บางครั้งสเตจที่มีจำนวนรูปหลายเหลี่ยมสูงจะมีรูปหลายเหลี่ยมมากกว่า 100,000 รูป แต่ประสิทธิภาพก็ไม่ได้ลดลงมากนัก แม้ว่าจะสร้างเป็น Physijs.ConcaveMesh (btBvhTriangleMeshShape ใน Bullet) ทั้งหมดก็ตาม

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

วัตถุที่มองไม่เห็น

ออบเจ็กต์ที่มีการตรวจจับการชนแต่ไม่มีการชนและไม่มีผลกระทบต่อออบเจ็กต์อื่นๆ จะเรียกว่า "ออบเจ็กต์ผี" ใน Bullet แม้ว่า Physijs จะไม่รองรับออบเจ็กต์ผีอย่างเป็นทางการ แต่คุณก็สร้างออบเจ็กต์ผีในนั้นได้โดยการปรับแต่ง Flag หลังจากสร้าง Physijs.Mesh World Wide Maze ใช้วัตถุผีสําหรับการตรวจจับการชนของไอเทมและจุดหมาย

hit = new Physijs.SphereMesh(geometry, material, 0)
hit._physijs.collision_flags = 1 | 4
scene.add(hit)

สำหรับ collision_flags 1 คือ CF_STATIC_OBJECT และ 4 คือ CF_NO_CONTACT_RESPONSE ลองค้นหาข้อมูลเพิ่มเติมในฟอรัมของ Bullet, Stack Overflow หรือเอกสารประกอบของ Bullet เนื่องจาก Physijs เป็น Wrapper สำหรับ Ammo.js และ Ammo.js นั้นเหมือนกับ Bullet เกือบทั้งหมด สิ่งที่ทำใน Bullet ได้ก็สามารถทำได้ใน Physijs ด้วย

ปัญหาเกี่ยวกับ Firefox 18

การอัปเดต Firefox จากเวอร์ชัน 17 เป็น 18 เปลี่ยนวิธีแลกเปลี่ยนข้อมูลของ Web Worker และส่งผลให้ Physijs หยุดทํางาน ปัญหาดังกล่าวได้รับการรายงานใน GitHub และได้รับการแก้ไขแล้วหลังจากผ่านไป 2-3 วัน แม้ว่าประสิทธิภาพของโอเพนซอร์สนี้จะสร้างความประทับใจให้ฉัน แต่เหตุการณ์นี้ยังทำให้ฉันนึกขึ้นได้ว่า World Wide Maze ประกอบด้วยเฟรมเวิร์กโอเพนซอร์สหลายแบบ เราเขียนบทความนี้ขึ้นเพื่อหวังจะให้ความคิดเห็นบางอย่าง

asm.js

แม้ว่าเรื่องนี้จะไม่เกี่ยวข้องกับ World Wide Maze โดยตรง แต่ Ammo.js รองรับ asm.js ที่ Mozilla เพิ่งประกาศไป (ไม่น่าแปลกใจเนื่องจาก asm.js สร้างขึ้นเพื่อเพิ่มความเร็วให้กับ JavaScript ที่ Emscripten สร้างขึ้น และ Emscripten ก็เป็นผลงานของผู้สร้าง Ammo.js ด้วย) หาก Chrome รองรับ asm.js ด้วย ภาระการประมวลผลของโปรแกรมฟิสิกส์ก็ควรลดลงอย่างมาก ความเร็วเร็วขึ้นอย่างเห็นได้ชัดเมื่อทดสอบด้วย Firefox Nightly เราคิดว่าคุณควรเขียนส่วนที่ต้องทำงานเร็วขึ้นใน C/C++ แล้วพอร์ตไปยัง JavaScript โดยใช้ Emscripten

WebGL

สําหรับการติดตั้งใช้งาน WebGL เราใช้ไลบรารี three.js (r53) ที่พัฒนาอยู่อย่างต่อเนื่อง แม้ว่าการแก้ไข 57 จะเผยแพร่ไปแล้วในช่วงท้ายของการพัฒนา แต่ API ก็มีการเปลี่ยนแปลงที่สำคัญ เราจึงใช้การแก้ไขเวอร์ชันเดิมในการเผยแพร่

เอฟเฟกต์เรืองแสง

เราได้เพิ่มเอฟเฟกต์เรืองแสงลงในแกนกลางของลูกบอลและไอเทมโดยใช้ "Kawase Method MGF" เวอร์ชันง่ายๆ อย่างไรก็ตาม ในขณะที่วิธีการของ Kawase ทําให้บริเวณที่สว่างทั้งหมดสว่างขึ้น World Wide Maze จะสร้างเป้าหมายการเรนเดอร์แยกต่างหากสําหรับบริเวณที่ต้องเปล่งแสง เนื่องจากต้องใช้ภาพหน้าจอของเว็บไซต์สำหรับพื้นผิวของฉาก และการนำเฉพาะบริเวณที่สว่างทั้งหมดออกจะทำให้ทั้งเว็บไซต์สว่างขึ้น เช่น หากมีพื้นหลังสีขาว เราเคยพิจารณาประมวลผลทุกอย่างเป็น HDR แต่ตัดสินใจไม่ทำในครั้งนี้เนื่องจากการติดตั้งใช้งานจะค่อนข้างซับซ้อน

เปล่งประกาย

ด้านบนซ้ายแสดงการผ่านครั้งแรก ซึ่งระบบจะแสดงผลพื้นที่เรืองแสงแยกต่างหาก จากนั้นจึงเบลอ ด้านล่างขวาแสดงการผ่านครั้งที่ 2 ซึ่งลดขนาดรูปภาพลง 50% แล้วเบลอ ด้านบนขวาแสดงการผ่านที่ 3 ซึ่งมีการลดขนาดรูปภาพลง 50% อีกครั้งแล้วเบลอ จากนั้นจึงวางซ้อนภาพทั้ง 3 ภาพเพื่อสร้างภาพรวมสุดท้ายที่แสดงอยู่ด้านล่างซ้าย สำหรับเบลอ เราใช้ VerticalBlurShader และ HorizontalBlurShader ซึ่งรวมอยู่ใน three.js ดังนั้นจึงยังมีพื้นที่สำหรับการเพิ่มประสิทธิภาพเพิ่มเติม

ลูกบอลสะท้อนแสง

การสะท้อนบนลูกบอลอิงตามตัวอย่างจาก three.js ระบบจะแสดงผลทุกทิศทางจากตำแหน่งของลูกบอลและใช้เป็นแผนที่สภาพแวดล้อม แผนที่สภาพแวดล้อมต้องได้รับการอัปเดตทุกครั้งที่ลูกบอลเคลื่อนไหว แต่เนื่องจากการอัปเดตที่ 60 fps นั้นใช้ทรัพยากรมาก ระบบจึงอัปเดตแผนที่ทุกๆ 3 เฟรมแทน ผลลัพธ์ที่ได้จะไม่ราบรื่นเท่ากับการอัปเดตทุกเฟรม แต่ความแตกต่างนั้นแทบจะมองไม่เห็นเลย เว้นแต่จะมีการชี้ให้เห็น

Shader, Shader, Shader…

WebGL ต้องใช้ Shader (Vertex Shader, Fragment Shader) สำหรับการเรนเดอร์ทั้งหมด แม้ว่าเชดเดอร์ที่รวมอยู่ใน three.js จะมีเอฟเฟกต์ที่หลากหลายอยู่แล้ว แต่คุณก็จำเป็นต้องเขียนเชดเดอร์ของคุณเองเพื่อให้ได้แสงและเงาที่ซับซ้อนมากขึ้นและเพิ่มประสิทธิภาพ เนื่องจาก World Wide Maze ทำให้ CPU ทำงานหนักด้วยโปรแกรมฟิสิกส์ เราจึงพยายามใช้ GPU แทนโดยเขียนด้วยภาษา Shading (GLSL) ให้ได้มากที่สุด แม้ว่าการประมวลผลของ CPU (ผ่าน JavaScript) จะง่ายกว่าก็ตาม เอฟเฟกต์คลื่นทะเลใช้ชิเดอร์เป็นพื้นฐานอยู่แล้ว เช่นเดียวกับพลุที่จุดจุดสิ้นสุดการแข่งขันและเอฟเฟกต์เมชที่ใช้เมื่อลูกบอลปรากฏขึ้น

ลูกบอลแรเงา

ด้านบนเป็นการทดสอบเอฟเฟกต์ตาข่ายที่ใช้เมื่อลูกบอลปรากฏขึ้น รูปทางด้านซ้ายคือรูปที่ใช้ในเกม ซึ่งประกอบด้วยรูปหลายเหลี่ยม 320 รูป รูปภาพตรงกลางใช้รูปหลายเหลี่ยมประมาณ 5,000 รูป ส่วนรูปภาพด้านขวาใช้รูปหลายเหลี่ยมประมาณ 300,000 รูป แม้จะมีรูปหลายเหลี่ยมจํานวนมากขนาดนี้ แต่การประมวลผลด้วยเชดเดอร์จะรักษาอัตราเฟรมที่ 30 fps ไว้ได้

เมช Shader

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

poly2tri

ระยะจะสร้างขึ้นตามข้อมูลเค้าโครงที่ได้รับจากเซิร์ฟเวอร์ จากนั้น JavaScript จะเปลี่ยนเป็นรูปหลายเหลี่ยม การใช้สามเหลี่ยมซึ่งเป็นส่วนสําคัญของกระบวนการนี้ทําได้ไม่ดีใน three.js และมักจะไม่สําเร็จ เราจึงตัดสินใจผสานรวมไลบรารีการแบ่งพื้นที่เป็นสามเหลี่ยมอื่นชื่อ poly2tri ด้วยตนเอง ปรากฏว่า three.js เคยพยายามทำสิ่งเดียวกันนี้ในอดีตแล้ว เราจึงทำให้โค้ดทำงานได้โดยเพียงแค่ใส่ความคิดเห็นบางส่วนออก ส่งผลให้ข้อผิดพลาดลดลงอย่างมาก ซึ่งทำให้เล่นด่านได้มากขึ้น ข้อผิดพลาดยังคงเกิดขึ้นเป็นครั้งคราว และด้วยเหตุผลบางอย่าง poly2tri จึงจัดการข้อผิดพลาดโดยการออกการแจ้งเตือน เราจึงแก้ไขให้แสดงข้อยกเว้นแทน

poly2tri

ด้านบนแสดงวิธีแบ่งขอบเขตสีน้ำเงินออกเป็นสามเหลี่ยมและสร้างรูปหลายเหลี่ยมสีแดง

การกรองแบบแอนไอโซทรอปิก

เนื่องจาก MIP Mapping แบบ Isotropic มาตรฐานจะลดขนาดรูปภาพทั้งบนแกนแนวนอนและแนวตั้ง การดูรูปหลายเหลี่ยมจากมุมเอียงจะทำให้พื้นผิวที่ส่วนท้ายสุดของด่าน World Wide Maze ดูเหมือนพื้นผิวที่มีความละเอียดต่ำซึ่งยืดออกในแนวนอน รูปภาพด้านขวาบนในหน้า Wikipedia นี้แสดงตัวอย่างที่ดีของกรณีนี้ ในทางปฏิบัติ ต้องใช้ความละเอียดแนวนอนมากกว่า ซึ่ง WebGL (OpenGL) จะแก้ปัญหานี้โดยใช้วิธีการที่เรียกว่าการกรองแบบแอนไอโซทรอปิก ใน three.js การตั้งค่า THREE.Texture.anisotropy เป็นค่าที่มากกว่า 1 จะเปิดใช้การกรองแบบ anisotropic อย่างไรก็ตาม ฟีเจอร์นี้เป็นส่วนขยายและ GPU บางรุ่นอาจไม่รองรับ

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

ดังที่บทความแนวทางปฏิบัติแนะนำสำหรับ WebGL นี้กล่าวถึงด้วยว่าวิธีสำคัญที่สุดในการปรับปรุงประสิทธิภาพของ WebGL (OpenGL) คือลดจำนวนการเรียกใช้การวาด ในช่วงแรกของการพัฒนา World Wide Maze เกาะ สะพาน และราวกั้นทั้งหมดในเกมเป็นวัตถุแยกกัน ซึ่งบางครั้งส่งผลให้มีการเรียกใช้การวาดมากกว่า 2,000 ครั้ง ทำให้ใช้สเตจที่ซับซ้อนได้ยาก อย่างไรก็ตาม เมื่อฉันรวมออบเจ็กต์ประเภทเดียวกันทั้งหมดไว้ในเมชเดียว การเรียกใช้การวาดลดลงเหลือประมาณ 50 ครั้ง ซึ่งช่วยปรับปรุงประสิทธิภาพได้อย่างมาก

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

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

ด้านบนคือผลการติดตามจากการสร้างแผนที่สภาพแวดล้อมสำหรับการสะท้อนของบอล การวาง console.time และ console.timeEnd ไว้ในตำแหน่งที่เกี่ยวข้องใน three.js จะให้กราฟที่มีลักษณะดังนี้ เวลาจะไหลจากซ้ายไปขวา และแต่ละเลเยอร์จะคล้ายกับสแต็กการเรียกใช้ การฝัง console.time ภายใน console.time จะช่วยให้วัดผลเพิ่มเติมได้ กราฟด้านบนคือก่อนการเพิ่มประสิทธิภาพ และกราฟด้านล่างคือหลังการเพิ่มประสิทธิภาพ ดังที่กราฟด้านบนแสดง มีการเรียก updateMatrix (แม้ว่าคำจะถูกตัดให้สั้นลง) สําหรับการแสดงผล 0-5 แต่ละรายการในระหว่างการเพิ่มประสิทธิภาพก่อน เราแก้ไขให้เรียกใช้เพียงครั้งเดียว เนื่องจากกระบวนการนี้จําเป็นเฉพาะเมื่อวัตถุเปลี่ยนตําแหน่งหรือการวางแนวเท่านั้น

กระบวนการติดตามก็ใช้ทรัพยากรเช่นกัน ดังนั้นการแทรก console.time มากเกินไปอาจทําให้ประสิทธิภาพจริงเบี่ยงเบนไปอย่างมาก ซึ่งทําให้ระบุพื้นที่ที่ต้องการเพิ่มประสิทธิภาพได้ยาก

ตัวปรับประสิทธิภาพ

เนื่องจากลักษณะของอินเทอร์เน็ต เกมจึงมีแนวโน้มที่จะเล่นในระบบที่มีสเปคแตกต่างกันไปอย่างมาก Find Your Way to Oz ที่เผยแพร่ไปเมื่อต้นเดือนกุมภาพันธ์ใช้คลาสชื่อ IFLAutomaticPerformanceAdjust เพื่อลดระดับเอฟเฟกต์ตามความผันผวนของอัตราเฟรม ซึ่งช่วยให้การเล่นราบรื่น World Wide Maze สร้างขึ้นจากคลาส IFLAutomaticPerformanceAdjust เดียวกันและลดระดับเอฟเฟกต์ตามลำดับต่อไปนี้เพื่อให้เกมเพลย์ราบรื่นที่สุด

  1. หากอัตราเฟรมลดลงต่ำกว่า 45 fps แผนที่สภาพแวดล้อมจะหยุดอัปเดต
  2. หากยังคงต่ำกว่า 40 fps ระบบจะลดความละเอียดในการเรนเดอร์ลงเหลือ 70% (50% ของอัตราส่วนพื้นผิว)
  3. หากยังคงต่ำกว่า 40 fps ระบบจะปิดใช้ FXAA (การลดรอยหยัก)
  4. หากยังคงต่ำกว่า 30 fps ระบบจะไม่ใช้เอฟเฟกต์แสง

หน่วยความจํารั่วไหล

การนําวัตถุออกอย่างเรียบร้อยนั้นค่อนข้างยุ่งยากเมื่อใช้ three.js แต่การปล่อยไว้เฉยๆ ย่อมจะทำให้เกิดการรั่วไหลของหน่วยความจำอย่างแน่นอน เราจึงคิดค้นวิธีการด้านล่างนี้ @renderer หมายถึง THREE.WebGLRenderer (การแก้ไขล่าสุดของ three.js ใช้วิธีการเพิ่มพื้นที่ว่างที่แตกต่างออกไปเล็กน้อย ดังนั้นวิธีนี้อาจใช้ไม่ได้กับ three.js)

destructObjects: (object) =>
  switch true
    when object instanceof THREE.Object3D
      @destructObjects(child) for child in object.children
      object.parent?.remove(object)
      object.deallocate()
      object.geometry?.deallocate()
      @renderer.deallocateObject(object)
      object.destruct?(this)

    when object instanceof THREE.Material
      object.deallocate()
      @renderer.deallocateMaterial(object)

    when object instanceof THREE.Texture
      object.deallocate()
      @renderer.deallocateTexture(object)

    when object instanceof THREE.EffectComposer
      @destructObjects(object.copyPass.material)
      object.passes.forEach (pass) =>
        @destructObjects(pass.material) if pass.material
        @renderer.deallocateRenderTarget(pass.renderTarget) if pass.renderTarget
        @renderer.deallocateRenderTarget(pass.renderTarget1) if pass.renderTarget1
        @renderer.deallocateRenderTarget(pass.renderTarget2) if pass.renderTarget2

HTML

เราคิดว่าสิ่งที่ดีที่สุดเกี่ยวกับแอป WebGL คือความสามารถในการออกแบบเลย์เอาต์หน้าเว็บใน HTML การสร้างอินเทอร์เฟซ 2 มิติ เช่น คะแนนหรือการแสดงข้อความใน Flash หรือ openFrameworks (OpenGL) นั้นค่อนข้างยุ่งยาก Flash มี IDE เป็นอย่างน้อย แต่ openFrameworks นั้นใช้ยากหากคุณไม่คุ้นเคย (การใช้เครื่องมืออย่าง Cocos2D อาจทําให้ง่ายขึ้น) ในทางกลับกัน HTML ช่วยให้ควบคุมแง่มุมการออกแบบฟรอนต์เอนด์ทั้งหมดได้อย่างแม่นยำด้วย CSS เช่นเดียวกับการสร้างเว็บไซต์ แม้ว่าเอฟเฟกต์ที่ซับซ้อน เช่น อนุภาคที่รวมกันเป็นโลโก้ จะเป็นไปไม่ได้ แต่เอฟเฟกต์ 3 มิติบางอย่างที่ทำได้ภายในความสามารถของ CSS Transforms ก็เป็นไปได้ เอฟเฟกต์ข้อความ "GOAL" และ "TIME IS UP" ของ World Wide Maze เคลื่อนไหวโดยใช้การปรับขนาดในการเปลี่ยน CSS (ติดตั้งใช้งานด้วย Transit) (แน่นอนว่าการไล่สีพื้นหลังใช้ WebGL)

แต่ละหน้าในเกม (ชื่อ ผลลัพธ์ อันดับ ฯลฯ) จะมีไฟล์ HTML ของตัวเอง และเมื่อโหลดหน้าเหล่านี้เป็นเทมเพลต ระบบจะเรียกใช้ $(document.body).append() ด้วยค่าที่เหมาะสมในเวลาที่เหมาะสม ปัญหาหนึ่งคือไม่สามารถตั้งค่าเหตุการณ์เมาส์และแป้นพิมพ์ก่อนการต่อท้ายได้ ดังนั้นการพยายามใช้ el.click (e) -> console.log(e) ก่อนการต่อท้ายจึงไม่ได้ผล

การทำให้เป็นสากล (i18n)

การทำงานใน HTML ยังสะดวกต่อการสร้างเวอร์ชันภาษาอังกฤษด้วย เราเลือกใช้ i18next ซึ่งเป็นไลบรารี i18n บนเว็บเพื่อรองรับหลายภาษา ซึ่งเรานำไปใช้ได้โดยไม่ต้องแก้ไข

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

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

RequireJS

ฉันเลือก RequireJS เป็นระบบโมดูล JavaScript โค้ดต้นฉบับ 10,000 บรรทัดของเกมแบ่งออกเป็นคลาสประมาณ 60 คลาส (= ไฟล์ Coffee) และคอมไพล์เป็นไฟล์ js แต่ละไฟล์ RequireJS จะโหลดไฟล์แต่ละไฟล์เหล่านี้ตามลําดับที่เหมาะสมโดยอิงตามข้อกําหนด

define ->
  class Hoge
    hogeMethod: ->

คลาสที่กําหนดไว้ด้านบน (hoge.coffee) สามารถใช้ได้ดังนี้

define ['hoge'], (Hoge) ->
  class Moge
    constructor: ->
      @hoge = new Hoge()
      @hoge.hogeMethod()

หากต้องการให้ทํางาน ระบบต้องโหลดไฟล์ hoge.js ก่อน moge.js และเนื่องจาก "hoge" ได้รับการกําหนดให้เป็นอาร์กิวเมนต์แรกของ "define" ระบบจึงจะโหลดไฟล์ hoge.js ก่อนเสมอ (เรียกกลับเมื่อโหลดไฟล์ hoge.js เสร็จแล้ว) กลไกนี้เรียกว่า AMD และสามารถใช้ไลบรารีของบุคคลที่สามสําหรับการเรียกกลับประเภทเดียวกันได้ ตราบใดที่ไลบรารีรองรับ AMD แม้แต่โมดูลที่ไม่ได้ใช้ (เช่น three.js) ก็จะทํางานในลักษณะเดียวกัน ตราบใดที่ระบุทรัพยากรที่ต้องพึ่งพาล่วงหน้า

ซึ่งคล้ายกับการนําเข้า AS3 จึงไม่น่าแปลกใจ หากมีไฟล์ที่เกี่ยวข้องมากขึ้น วิธีนี้อาจเป็นวิธีแก้ปัญหาได้

r.js

RequireJS มีเครื่องมือเพิ่มประสิทธิภาพที่เรียกว่า r.js ซึ่งจะรวมไฟล์ JS หลักกับไฟล์ JS ทั้งหมดที่เกี่ยวข้องไว้ในไฟล์เดียว จากนั้นจะบีบอัดโดยใช้ UglifyJS (หรือ Closure Compiler) ซึ่งจะช่วยลดจำนวนไฟล์และปริมาณข้อมูลทั้งหมดที่เบราว์เซอร์ต้องโหลด ขนาดไฟล์ JavaScript ทั้งหมดของ World Wide Maze อยู่ที่ประมาณ 2 MB และสามารถลดขนาดลงเหลือประมาณ 1 MB ด้วยการเพิ่มประสิทธิภาพ r.js หากเผยแพร่เกมได้โดยใช้ gzip ขนาดจะลดลงเหลือ 250 KB (GAE มีปัญหาที่ไม่อนุญาตให้ส่งไฟล์ gzip ที่มีขนาดใหญ่กว่า 1 MB ดังนั้นขณะนี้ระบบจะเผยแพร่เกมแบบไม่บีบอัดเป็นข้อความธรรมดาขนาด 1 MB)

ตัวสร้างระยะ

ระบบจะสร้างข้อมูลระยะการทำงานดังนี้ ซึ่งดำเนินการทั้งหมดบนเซิร์ฟเวอร์ GCE ในสหรัฐอเมริกา

  1. ระบบจะส่ง URL ของเว็บไซต์ที่จะแปลงเป็นระยะผ่าน WebSocket
  2. PhantomJS จะจับภาพหน้าจอ และดึงข้อมูลตําแหน่งแท็ก div และ img เพื่อแสดงผลในรูปแบบ JSON
  3. โปรแกรม C++ (OpenCV, Boost) ที่กําหนดเองจะลบพื้นที่ที่ไม่จําเป็น สร้างเกาะ เชื่อมต่อเกาะด้วยสะพาน คํานวณตำแหน่งของรั้วและสินค้า กำหนดจุดเป้าหมาย ฯลฯ โดยอิงตามภาพหน้าจอจากขั้นตอนที่ 2 และข้อมูลตำแหน่งขององค์ประกอบ HTML ผลลัพธ์จะแสดงผลเป็นรูปแบบ JSON และส่งกลับไปยังเบราว์เซอร์

PhantomJS

PhantomJS เป็นเบราว์เซอร์ที่ไม่ต้องใช้หน้าจอ ซึ่งสามารถโหลดหน้าเว็บได้โดยไม่ต้องเปิดหน้าต่าง จึงนำไปใช้ในการทดสอบอัตโนมัติหรือจับภาพหน้าจอฝั่งเซิร์ฟเวอร์ได้ เครื่องมือเบราว์เซอร์ของ Edge คือ WebKit ซึ่งเป็นเครื่องมือเดียวกับที่ Chrome และ Safari ใช้ ดังนั้นเลย์เอาต์และผลลัพธ์ของการดำเนินการ JavaScript จะคล้ายกับเบราว์เซอร์มาตรฐาน

เมื่อใช้ PhantomJS ระบบจะใช้ JavaScript หรือ CoffeeScript เพื่อเขียนกระบวนการที่คุณต้องการให้ดำเนินการ การจับภาพหน้าจอนั้นง่ายมาก ดังที่แสดงในตัวอย่างนี้ ฉันทํางานบนเซิร์ฟเวอร์ Linux (CentOS) จึงต้องติดตั้งแบบอักษรเพื่อแสดงภาษาญี่ปุ่น (M+ FONTS) อย่างไรก็ตาม การจัดการการแสดงผลแบบอักษรจะแตกต่างออกไปจากใน Windows หรือ Mac OS ดังนั้นแบบอักษรเดียวกันจึงอาจดูแตกต่างออกไปในเครื่องอื่นๆ (แต่ความแตกต่างนั้นน้อยมาก)

โดยทั่วไปแล้ว การรับตำแหน่งแท็ก img และ div จะจัดการในลักษณะเดียวกับในหน้ามาตรฐาน นอกจากนี้ คุณยังใช้ jQuery ได้โดยที่ไม่มีปัญหา

stage_builder

ตอนแรกเราพิจารณาที่จะใช้แนวทางแบบ DOM มากขึ้นเพื่อสร้างระยะ (คล้ายกับ Firefox 3D Inspector) และลองใช้สิ่งที่คล้ายกับการวิเคราะห์ DOM ใน PhantomJS แต่สุดท้ายแล้ว เราเลือกใช้วิธีการประมวลผลรูปภาพ ด้วยเหตุนี้ เราจึงเขียนโปรแกรม C++ ที่ใช้ OpenCV และ Boost ชื่อ "stage_builder" โดยจะทําสิ่งต่อไปนี้

  1. โหลดภาพหน้าจอและไฟล์ JSON
  2. แปลงรูปภาพและข้อความเป็น "เกาะ"
  3. สร้างสะพานเพื่อเชื่อมต่อเกาะต่างๆ
  4. กำจัดบริดจ์ที่ไม่จำเป็นเพื่อสร้างเขาวงกต
  5. วางสิ่งของขนาดใหญ่
  6. วางสิ่งของเล็กๆ
  7. ติดตั้งราวกันชน
  8. ส่งออกข้อมูลตำแหน่งในรูปแบบ JSON

โปรดดูรายละเอียดของแต่ละขั้นตอนด้านล่าง

กำลังโหลดภาพหน้าจอและไฟล์ JSON

cv::imread ปกติจะใช้เพื่อโหลดภาพหน้าจอ เราทดสอบไลบรารีหลายรายการสำหรับไฟล์ JSON แต่ picojson ดูเหมือนจะใช้งานได้ง่ายที่สุด

การเปลี่ยนรูปภาพและข้อความเป็น "เกาะ"

การสร้างระยะ

ด้านบนคือภาพหน้าจอของส่วนข่าวใน aid-dcc.com (คลิกเพื่อดูขนาดจริง) องค์ประกอบรูปภาพและข้อความต้องแปลงเป็นองค์ประกอบเดี่ยว หากต้องการแยกส่วนเหล่านี้ เราควรลบสีพื้นหลังสีขาว หรือก็คือสีที่พบบ่อยที่สุดในภาพหน้าจอ ลักษณะของหน้าเว็บเมื่อดำเนินการเสร็จแล้วมีดังนี้

การสร้างระยะ

ส่วนที่เป็นสีขาวคือเกาะที่เป็นไปได้

ข้อความมีขนาดเล็กและคมเกินไป เราจึงจะปรับให้หนาขึ้นด้วย cv::dilate, cv::GaussianBlur และ cv::threshold เนื้อหารูปภาพก็หายไปด้วย ดังนั้นเราจะเติมพื้นที่เหล่านั้นด้วยสีขาวตามเอาต์พุตข้อมูลแท็ก img จาก PhantomJS รูปภาพที่ได้จะมีลักษณะดังนี้

การสร้างระยะ

ตอนนี้ข้อความอยู่ในรูปแบบที่เหมาะสมแล้ว และรูปภาพแต่ละรูปเป็นเกาะที่เหมาะสม

การสร้างสะพานเพื่อเชื่อมต่อเกาะต่างๆ

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

การสร้างระยะ

การยกเลิกบริดจ์ที่ไม่จำเป็นเพื่อสร้างเขาวงกต

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

การสร้างระยะ

การวางสินค้าขนาดใหญ่

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

การสร้างระยะ

จากจุดที่เป็นไปได้ทั้งหมดเหล่านี้ ระบบจะตั้งค่าจุดที่ด้านซ้ายบนเป็นจุดเริ่มต้น (วงกลมสีแดง) จุดที่ด้านขวาล่างเป็นเป้าหมาย (วงกลมสีเขียว) และเลือกจุดที่เหลือไม่เกิน 6 จุดสำหรับตำแหน่งของสินค้าขนาดใหญ่ (วงกลมสีม่วง)

การวางสิ่งของเล็กๆ

การสร้างระยะ

วางวัตถุขนาดเล็กจํานวนที่เหมาะสมตามเส้นในระยะห่างจากขอบเกาะที่กำหนด รูปภาพด้านบน (ไม่ใช่จาก aid-dcc.com) แสดงเส้นตำแหน่งที่คาดการณ์เป็นสีเทา ซึ่งมีการเลื่อนและวางเป็นระยะๆ จากขอบของเกาะ จุดสีแดงแสดงตำแหน่งของสิ่งของเล็กๆ เนื่องจากรูปภาพนี้มาจากเวอร์ชันที่กำลังพัฒนา รายการต่างๆ จึงวางเรียงกันเป็นเส้นตรง แต่เวอร์ชันสุดท้ายจะกระจายรายการต่างๆ ออกไปไม่สม่ำเสมอกันมากนักทางด้านข้างของเส้นสีเทา

การวางขอบเขต

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

การสร้างระยะ

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

การแสดงผลข้อมูลตำแหน่งในรูปแบบ JSON

เราใช้ picojson สำหรับเอาต์พุตด้วย โดยจะเขียนข้อมูลไปยังเอาต์พุตมาตรฐาน ซึ่งผู้เรียกใช้ (Node.js) จะได้รับข้อมูลดังกล่าว

การสร้างโปรแกรม C++ ใน Mac เพื่อเรียกใช้ใน Linux

เกมนี้พัฒนาใน Mac และติดตั้งใช้งานใน Linux แต่เนื่องจาก OpenCV และ Boost มีให้บริการสำหรับทั้ง 2 ระบบปฏิบัติการ การพัฒนาจึงไม่ใช่เรื่องยากเมื่อสร้างสภาพแวดล้อมการคอมไพล์แล้ว เราใช้เครื่องมือบรรทัดคำสั่งใน Xcode เพื่อแก้ไขข้อบกพร่องของบิลด์ใน Mac จากนั้นสร้างไฟล์กำหนดค่าโดยใช้ automake/autoconf เพื่อให้คอมไพล์บิลด์ใน Linux ได้ จากนั้นฉันก็ใช้ "configure && make" ใน Linux เพื่อสร้างไฟล์ที่เรียกใช้งานได้ เราพบข้อบกพร่องบางอย่างเฉพาะสำหรับ Linux เนื่องจากความแตกต่างของเวอร์ชันคอมไพเลอร์ แต่ก็สามารถแก้ไขได้อย่างง่ายดายโดยใช้ gdb

บทสรุป

เกมลักษณะนี้สร้างได้ด้วย Flash หรือ Unity ซึ่งจะมีประโยชน์มากมาย อย่างไรก็ตาม เวอร์ชันนี้ไม่ต้องใช้ปลั๊กอิน และฟีเจอร์เลย์เอาต์ของ HTML5 + CSS3 พิสูจน์แล้วว่ามีประสิทธิภาพมาก การมีเครื่องมือที่เหมาะสมกับแต่ละงานเป็นสิ่งสำคัญอย่างยิ่ง ส่วนตัวแล้วฉันประหลาดใจกับผลลัพธ์ของเกมที่สร้างขึ้นด้วย HTML5 ทั้งหมด และแม้ว่าเกมจะยังขาดอะไรหลายอย่าง แต่เราก็หวังว่าจะได้เห็นการพัฒนาเกมนี้ในอนาคต