มาสร้างคอมพิวเตอร์จำลองไว้เล่นเกมกันเถอะ [Chip-8 emulator]
หลาย ๆ คนคงคุ้นชื่อเกมใน console ไม่ว่าจะเป็น Pokémon Sword & Shield, Final Fantasy VII Remake เก่ากว่านั้นหน่อยก็ Pokémon Sun & Moon เก่ากว่านั้นอีก Pokémon White & Black วันนี้เราจะย้อนกลับไปแบบเก่าสุด ๆ เอาตั้งแต่ภาพยังเป็นพิกเซลอยู่เลย ไปดูกันว่าเราจะจำลองมันขึ้นมาได้ยังไง
Emulator คืออะไร
Emulator คือ การจำลองให้คอมพิวเตอร์เครื่องหนึ่งสามารถรันโปรแกรมทุกอย่างเหมือนคอมพิวเตอร์อีกเครื่องได้ยกตัวอย่างเช่น ถ้าเราอยากเล่น Mario kart : super circuit ในคอมของเรา แทนที่เราจะไปนั่งเล่นจนเข้าใจว่าเกมมีกี่ด่าน มีไอเทมอะไรบ้าง ตัวละครกี่ตัวแล้วมาสร้างเกมให้เหมือนกับของเค้า เรามาสร้างระบบที่พอใส่เกมเข้าไปแล้ว โปรแกรมทำงานเหมือนเครื่องเกมนั้นเลยดีกว่า
ทำความรู้จักกับ Chip-8
Chip-8 ถูกสร้างโดย Joe Weisbecker ในปี 1977 เป็น Programming language ที่เหมาะกับการเขียนเกม คอมพิวเตอร์หลายเครื่องจึงนำไปใช้เช่น Cosmac VIP และ Telmac 1800 หลังจากนั้นก็เกิด Chip-8 virtual machine ขึ้นเพื่อใช้รันโปรแกรม chip-8 โดยเฉพาะ
ทำไมต้อง Chip-8
Chip-8 เป็น emulator ที่สร้างได้ง่ายที่สุด เหมาะสำหรับเป็นโปรเจกต์ระยะสั้นใช้เวลาประมาณ 1 อาทิตย์ก็เสร็จแล้ว แต่ผู้ทำต้องมีความรู้พื้นฐานเกี่ยวกับการเขียนโปรแกรมมาก่อนแล้ว (ไม่ควรเริ่มเรียนเขียนโปรแกรมด้วยโปรเจกต์นี้)
Note: เพื่อความเข้าใจที่ตรงกันตัวเลขที่เป็นเลขฐาน 16 ผู้เขียนจะเขียนในรูป 0xnnnn เช่น FF ก็จะเขียนเป็น 0xFF ถ้าเป็นเลขฐาน 2 จะเป็น 0bnnnn (n เป็นตัวเลข)
Chip-8 ประกอบด้วยอะไรบ้าง
เพื่อที่จะอธิบายว่า Chip-8 ทำงานได้อย่างไรเราจำเป็นต้องรู้ก่อนว่าข้างใน chip-8 มีอะไรบ้าง
- RAM (random access memory) ขนาด 4Kb (4,096 bytes) : โดยเริ่มจากตำแหน่งที่ 0x000 จนถึง 0xFFF (แต่ละตำแหน่งขนาด 1 byte จึงมีขนาด 4,096 พอดี)
- Registers 16 ตัว: หน่วยความจำขนาดเล็กที่ทำงานได้เร็วมากอยู่ในตัว CPU ของ chip-8 ส่วนมากจะเอาไว้ใช้เก็บที่อยู่ของ memory
- Program counter (PC) : เป็นตัวชี้ว่าโปรแกรมต้องอ่านคำสั่งไหน เช่นถ้า PC = 0x202 แสดงว่าให้โปรแกรมอ่านคำสั่งที่ตำแหน่ง 0x202 ใน RAM
- Stack 16 ชั้น : ในบางครั้งคำสั่งบางอันจะสั่งให้โปรแกรมไปทำคำสั่งย่อยก่อน (subroutine) แล้วจึงกลับมาทำคำสั่งที่ทำอยู่ต่อ โดยก่อนที่จะไปทำคำสั่งย่อยโปรแกรมจะเก็บที่อยู่ที่เราอยู่ตอนนี้ไว้ใน Stack แล้วจึงไปทำคำสั่งย่อย เมื่อทำคำสั่งย่อยเสร็จตัวโปรแกรมก็จะดึงข้อมูลที่อยู่ใน Stack มาแล้วเปลี่ยน PC (ตัวชี้ว่าเราต้องทำคำสั่งไหน) ให้เท่ากับค่านั้น
- Stack counter : ลักษณะการทำงานเหมือนกับ PC แต่อันนี้ชี้ใน Stack แทนว่าต่อไปต้องดึงที่อยู่ไหนมาแล้วเปลี่ยน PC ให้เป็นค่านั้น
- Delay timer : เป็น register เช่นเดียวกับ 16 ตัวด้านบนแต่เมื่อถูกเซ็ตค่าที่ไม่ใช่ 0 แล้ว ค่าจะลดลงเรื่อย ๆ จนเป็นศูนย์หรือก็คือเอาไว้จับเวลานั่นเอง
- Sound timer : ลักษณะเหมือน delay timer แต่เมื่อถูกเซ็ตค่าแล้วค่าลดลงถึง 0 จะส่งเสียงออกมา
Chip-8 ทำงานยังไง
เริ่มแรกเรามาดูไฟล์เกมกันก่อนว่าหน้าตาเป็นยังไง
อย่าพึ่งตกใจว่าเลขสุดวุ่นวายกับตัวอักษรนั่นคืออะไร หายใจเข้าลึก ๆ ซักรอบหนึ่งก่อน มันไม่ได้น่ากลัวขนาดนั้น (มั้ง)
ROMs(read-only memory) หรือไฟล์เกมที่นำมาใส่ใน chip-8 จะประกอบด้วยชุดคำสั่งที่เรียกว่า opcode มีให้เลือกทั้งหมด 35 คำสั่งซึ่งคำสั่งเหล่านี้จะเป็นคนคอยบอกคอมพิวเตอร์จำลองที่เราสร้างขึ้นว่าต้องทำอะไรบ้างเช่น สั่งวาดรูป สั่งเคลียร์จอ สั่งให้ตัวละครเดินด้านซ้าย ด้านขวา
Opcode จะแสดงผลอยู่ในเลขฐาน 16 เราจึงเห็นตัวเลขของเรามีภาษาอังกฤษอยู่ด้วยทีนี้ Opcode หนึ่งคำสั่งจะประกอบด้วยตัวอักษร 4 ตัว แต่แต่ละช่องมีแค่สองตัวเอง
ดังนั้นเวลาที่เราอ่าน opcode หนึ่งอันเราจะเอาสองช่องมาประกอบกันอย่างเช่น
- คำสั่งแรก 0x1225(ในรูปคือบนซ้าย) นำ 12 กับ 25 มาต่อกันและเมื่อไปลองเทียบกับคู่มือเราจะพบว่ามันตรงกับ opcode 0x1nnn ซึ่งแปลคร่าว ๆ ได้ว่าให้ตัวโปรแกรมกระโดดไปทำ opcode ที่ตำแหน่ง 0x225เลย ไม่ต้องดูคำสั่งต่อไป (“0x5350”)
- คำสั่งที่สอง 0x5350 ประกอบจาก 53 กับ 50 ที่หมายความว่าให้ข้ามคำสั่งถัดไป(0x4143) ถ้า ค่าใน register ตัวที่ 3 เท่ากับค่าใน register ตัวที่ 5 เป็นต้น
โดยปกติเมื่อเริ่มโปรแกรมตัว chip-8 จะก๊อบปี้ตัว ROM ทั้งหมดไปไว้ใน RAM เริ่มจากตำแหน่งที่ 0x200 เพราะฉะนั้นถ้าโปรแกรมสั่งให้กระโดดไปโค้ดที่ตำแหน่ง 0x225 (ดังเช่นคำสั่งแรก) แสดงว่าเป็นโค้ดตัวที่ 0x25 ใน ROM นั่นเอง (ในรูปคือ 0x60 และเพื่อประกอบเป็น opcode จึงเป็น 0x6000)
แล้วทำไมไม่โหลดที่ 0x000 ไปเลยล่ะ
นั่นเป็นเพราะตำแหน่งที่ 0x000 ถึง 0x1FF จองไว้ให้ตัว chip-8 โหลดข้อมูลพื้นฐานของตัวมันเองอาทิเช่น Fonts, ตัวแปลคำสั่ง (interpreter)
Flow of program
สิ่งที่เราต้องเขียนให้โปรแกรมทำได้นั่นก็คือ
- 1.โหลด Fonts และ ROM เข้า RAM
- 2. อ่านคำสั่ง (opcode) จาก RAM (ครั้งแรกเริ่มที่ตำแหน่ง 0x200) (fetch)
- 3.แปลคำสั่งนั้นว่าต้องการให้โปรแกรมทำอะไร (decode)
- 4.รันคำสั่งที่แปลแล้ว (execute)
- 5.แสดงผลออกหน้าจอและรับอินพุต (I/O)
- 6.วนกลับไปอ่านคำสั่งถัดไป (เพิ่มค่าตัว PC แล้วกลับไปทำข้อที่ 2)
จะเห็นได้ว่าสุดท้ายแล้วโปรแกรมนั้นจะเป็นลูปโดยวนระหว่างข้อ 2 ถึง 6 เราเรียกสิ่งนี้ว่า game loop
ส่วนที่ทำนานที่สุดก็คงเป็นส่วน execute เพราะเราต้องมานั่งเขียนฟังก์ชันให้รองรับทั้ง 35 คำสั่งของ opcode ซึ่งถือว่าน้อยที่สุดในบรรดา Emulator ทั้งหมดทั้งมวลแล้ว (แต่ก็ยังแอบเยอะอยู่ดี😩) นอกจากนั้นยังต้องมานั่งงงกับบั๊กที่เกิดเพราะคำสั่งไหนก็ไม่รู้อีก (ฮา)
สุดท้ายนี้ผู้เขียนได้ความรู้หลายอย่างจากการทำโปรเจกต์นี้ไม่ว่าจะเป็น computer architecture, bit manipulation, binary and hexadecimal number systems นอกจากนี้ผู้เขียนเลือกที่จะเขียน chip-8 ใน Java Script ซึ่งไม่ค่อยรู้จัก Frameworks และเป็นภาษาที่ใหม่สำหรับผู้เขียน ทำให้ต้องนั่งหาความรู้อยู่นานพอสมควร😓 แต่ทุกอย่างก็ผ่านพ้นไปด้วยดีและออกมาเป็นแบบที่เห็นนั่นแหละ (กด Demo ด้านล่างไปเล่นกันได้ เลือก ROM ด้านขวาล่าง เลือกเสร็จวิธีเล่นก็จะขึ้นแถวๆนั้นด้วย^^)
Technical reference:
Guide to make Chip-8: