หัวใจแห่งการทำเกม หากเข้าใจสามารถเขียนเกมได้ทุกภาษา

เกริ่นนำ

สวัสดีครับ ผมขอแนะนำตัวซักเล็กน้อย กระผม Pagon Game Dev (เรียกภากร หรือ พาก้อนก็ได้) โดยผมจบ ป.ตรี สาขาแอนนิเมชั่น วิชาเอกเกม และ ปวช. สาขาอิเล็กทรอนิกส์ จบมาทำงานด้านเกมมา 5 ปี

ปัจจุบันย้ายสายมาทำงานเขียนเวปหลังบ้าน (ห่างทำเกมซักพัก~) แต่ด้วยความที่มีความรู้ด้านการทำเกม และมีความรู้อิเล็กทรอนิกส์ จึงสนใจและหลงไหลงาน Interactive เป็นพิเศษ (คือการเขียนโปรแกรมทีใกล้ชิดมนุษย์มากกว่าเดิม มากกว่าการใช้เม้าท์และคีย์บอร์ด เช่น Kinect , Senser , XR , AR , MR , VR) และตอนนี้กำลังเดินหน้าไปกับบริษัทของตัวเองชื่อว่า บริษัท ต่างโลก อินเตอร์แอคทีฟ จำกัด

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

  • คำเตือน บทความนี้เข้มข้นและยาวมาก แนะนำให้ค่อยๆอ่าน

โดยเรื่องนี้ จะอธิบายว่า ผมมีแนวคิดอย่างไรในการเขียนเกมขึ้นมาจากภาษาหรืออุปกรณ์ที่ไม่ได้เกิดมาเพื่อการทำเกมได้อย่างไร

โดยผมมีตัวอย่างที่เคยทำมาก่อนหน้าดังนี้

เขียนเกมบน HTML5 ในปี 2013 (สมัย HTML5 พึ่งออก)
เขียนเกม Tetris บน Arduino ด้วย c++ อ่านบทความจิ้มเบาๆตรงนี้ ดูซอร์สโค๊ดคลิกตรงนี้
เขียนเกมงู บน ESP8266 ด้วย c++ ดูวิดีโอคลิกตรงนี้ ดูซอร์สโค๊ดคลิกตรงนี้

เอาล่ะ ก่อนเข้าสู่เนื้อหาหัวใจแห่งการทำเกม เรามาทำความเข้าใจก่อนว่า เกม คืออะไร และเกิดขึ้นมาได้อย่างไร

เกมคืออะไร

โดยเราต้องแยกให้ออกระหว่างคำว่า เกม กับ เกมส์

เกมส์​ ไว้ใช้เรียกกิจกรรมสันทนาการ ที่มีหลายๆกิจกรรม หรือกีฬาหลายๆชนิดรวมกัน เช่น เอเชี่ยนเกมส์ โอลิมปิกเกมส์ โดยการทำเกมจะไม่ใช้คำว่าเกมส์

เกม คือ การละเล่น หรือ วิดีโอเกม ที่เรากำลังจะพูดถึง

ทั้งนี้เกม ก็มีองค์ประกอบอยู่ หากมีไม่ครบ จะไม่เป็นเกม แต่จะกลายเป็นกิจกรรมเฉยๆ

องค์ประกอบของเกม

องค์ประกอบของเกม

ประกอบไปด้วย

1. มีผู้เล่น 1 คนขึ้นไป
2. มีกฏ กติกา มีผลตัดสิน เงื่อนไขของการแพ้ ชนะ
3. มีความท้าทาย คือต้องเป็นสิ่งที่ไม่ใช่ใครๆก็ทำได้หรือทำได้เป็นประจำ

ยกตัวอย่างการกระโดดเชือก ถ้าเรากระโดดเชือกธรรมดาๆ เราจะยังไม่นับว่าเป็นเกม แต่พอใส่กติกาว่า “กระโดดเชือกให้ครบ 30 ครั้ง ใน 1 นาที ทำครบชนะ แพ้จะถูกทำโทษ” จากเดิมที่เป็นกิจกรรม พอมีกติกา ผลการตัดสิน และความท้าทาย ก็จะกลายเป็นอยู่ในรูปแบบเกมทันที

ถ้าเราดูหนัง 2 ชม. ให้จบ จะเห็นว่าเป็นกิจกรรม แต่ถ้าใส่กติกาว่า “แข่งกันดูหนังสารคดีน่าเบื่อ 2 ชม โดยต้องห้ามหลับ ห้ามเข้าห้องน้ำ ใครแพ้ต้องเลี้ยงน้ำ” ก็กลายเป็นเกมได้เหมือนกัน

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

เกมเกิดขึ้นได้อย่างไร

เกมคอมพิวเตอร์นั้นถ้าให้นับคือเป็น Animation แบบหนึ่ง ดังนั้นจึงมีรากฐานต้นกำเนิดเช่นเดียวกัน นั่นก็คือสิ่งที่เรียกว่า Flipbook ที่เราชอบวาดอยู่มุมกระดาษหนังสือหรือสมุดเรียนในสมัยเด็ก
(Animation คือ การทำภาพเคลื่อนไหว หรือถ้าเรียกแบบชาวบ้านคือหนังการ์ตูน)

ตัวอย่าง flipbook ริมสมุด (ที่มา https://youtu.be/GO9NshFE6Yc)
ตัวอย่าง flipbook (ที่มา https://youtu.be/kXx96ToJ2xk)

Flipbook คือ การวาดภาพนิ่งทีละแผ่น และเปิดด้วยความเร็ว เมื่อมีความเร็วมากพอก็จะกลายเป็นภาพติดตา เหมือนภาพเกิดการเคลื่อนไหวจริงๆ เกิดเป็นสิ่งที่เรียกว่า Animation

ตัวอย่าง Animation (ที่มา https://youtu.be/npMreLeVD6o)

ถ้าพอเข้าใจรากฐาน Animation ก็จะเริ่มเข้าใจแล้วว่า คือการทำภาพนิ่งทีละภาพ แล้ว ค่อยมาเล่นต่อกันโดยเราเรียกว่าอัตราภาพ (Framerate) โดยมีหน่วยเป็น จำนวนภาพต่อวินาที (FPS : Frame Per Second)

ตัวอย่าง FPS (ที่มา https://connarjamesmills.wordpress.com)

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

ตัวอย่างการเคลื่อนไหวในเกม (http://database.valkyrieuprising.com/)
ตัวอย่างการเคลื่อนที่ในเกม Stardew Valley

ในที่สุดก็ได้เข้าเนื้อหาเรื่องหัวใจของการทำเกม

หัวใจของการทำเกม

หัวใจแห่งการทำเกม

มีอยู่ 4 อย่างด้วยกัน ถ้ามีครบ 4 องค์ประกอบ จะสามารถสร้างเกมขึ้นมาได้

1. Game Loop : การทำงานที่รองรับการแสดงผลถี่ๆ
2. Delta Time : การนับเวลาที่ทำให้ทุกเครื่องทำงานเร็วเท่ากัน
3. Output : ส่วนที่สามารถแสดงผลเกมออกมาได้
4. Input : ส่วนที่สามารถรับค่าเพื่อที่จะเล่นเกมได้

1.Game Loop

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

function main(){
delay(1000); // เขียนฟังก์ชั่นรอเวลา 1 วินาที
alert('Hello'); // เมื่อครบแจ้งเตือน Hello
}

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

เราจึงใช้หลักการ GameLoop มาใช้ โดยประกอบด้วย 2 ส่วน คือ

1 Initial : คือจะทำงานครั้งเดียวเมื่อเริ่มต้นโปรแกรม เป็นขั้นตอนสำหรับเตรียมการตั้งค่าตัวแปรเริ่มต้นให้พร้อมใช้ หรือเรียกทรัพยากร เช่น รูปภาพ เสียง โมเดล 3 มิติ ก่อนทำการเข้าส่วนของ GameLoop

2 GameLoop : จะทำงานเป็นลูป ทำงานวนไปเรื่อยๆ จนกว่าปิดโปรแกรม โดยมีหน้าที่คือ ทำการอัพเดทค่าต่างๆ ของเกม และทำงานล้างภาพที่วาดจากรอบก่อนหน้า และทำการวาดขึ้นมาใหม่ เพื่อให้เกิดภาพเคลื่อนไหวขึ้น

function main()
{
initial(); // initial ทำงานครั้งเดียวเมื่อเริ่มต้นโปรแกรม
setInterval(loop, 1000/50); //gameloop จะทำการวนการทำงานไปเรื่อยๆ
}
// ประกาศตัวแปร global จะนับว่าเป็น Initial ได้เหมือนกัน
const hero = { x: 50, y: 50}
const speed = 10;
const goal = 2500;
function initial()
{
// เราจะทำการกำหนดค่าเริ่มต้น หรือโหลดรูปภาพตรงนี้
// เนื่องจากในบางภาษา ไม่รองรับการโหลดทรัพยากร
// หรือ คำนวนตัวแปร เช่น a = x+y ใน global ได้
hero.x = 0;
}
function loop()
{
// ทำการวนลูปอัพเดทค่าต่างๆ
hero.x += speed; // อัพเดทค่าตำแหน่งตัวละคร
if(hero.x >= goal)
{
alert("Game Win!!); // ถ้าตำแหน่งตัวละครถึงจุดหมายให้แสดง Game Win!!
}

DrawHero(hero.x , hero.y); //ทำการวาดภาพตัวละครในแต่ละรอบ
}

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

จากตัวอย่าง GameLoop ข้างบน เพื่อให้เห็นภาพ และเข้าใจมากยิ่งขึ้น

ผมขอยกตัวอย่าง GameLoop จาก XNA Framework ที่ผมคิดว่าเป็นพื้นฐาน GameLoop ที่ดีที่สุดในการเรียนรู้ดังภาพข้างล่าง

(XNA เป็น Framework ของ Microsoft ที่ใช้เขียนเกมแบบ Cross-Platform ปัจจุบันเปลี่ยนชื่อมาเป็น Mono Game)

ตัวอย่าง Gameloop XNA (ที่มา https://blogs.msdn.microsoft.com/tess/)

โดย XNA มี GameLoop อยู่ 5 ขั้นตอนคือ

1 Initial : ตั้งค่าตัวแปรเริ่มต้น

2 Load Content : โหลดทรัพยากรที่จำเป็นต้องใช้เตรียมไว้ เช่นเสียงหรือรูปภาพ

3 Update : ใช้ในการเขียน Update Logic ของเกมในแต่ละเฟรม เช่น เพิ่ม/ลด HP ย้ายตำแหน่งของตัวละคร เช็คปุ่มที่กด เป็นต้น

4 Draw : ทำการลบภาพจากเฟรมที่แล้ว และทำการแสดงผล(วาดภาพ)​ขึ้นมาใหม่ โดย Logic ของการวาดภาพทั้งหมดจะอยู่ในส่วนนี้ พอเสร็จ ก็จะกลับเข้าไปยัง Update และทำการวนแบบนี้ต่อไปเรื่อยๆ

5 : Unload Content เมื่อปิดโปรแกรมจะทำการคืนทรัพยากร ที่จองไว้

จะสังเกตุเห็นว่า GameLoop ของ XNA จะขยายมาจาก GameLoop ปกติ เพื่อให้แยกออกมาดูเป็นระเบียบ และ ง่ายต่อการใช้งาน

โดย Initial จะถูกแบ่งออกเป็น Initial และ Load Content ส่วน GameLoop ถูกแบ่งออกเป็น Update และ Draw ส่วน Unload Content โปรแกรมส่วนใหญ่ทำให้ตอนปิดโปรแกรมอยู่แล้ว

2. Delta Time

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

ดังนั้นจึงต้องใช้ Delta Time เข้ามาช่วย หรือหากใครเรียนคณิตศาสตร์จะรู้จักในนาม Δ Time หรือ Time2 - Time1 หรือให้แปลง่ายๆว่า ผลต่างของเวลา

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

การอธิบายเรื่อง Delta Time

แล้วถ้าภาษาที่เราใช้ไมมี DeltaTime แล้วเราจะสามารถหาได้จากที่ใหน โดยพื้นฐานแล้วถ้าเราไม่ได้ใช้ Framework หรือ Game Engine ก็คงไม่มี DeltaTime มาให้

แต่เดี่ยวก่อน Delta Time เราสามารถ​สร้างขึ้นมาเองได้ไม่ยาก ขอแค่มีหน่วยเวลาที่อ้างอิงจาก Timestamp หรืออ้างอิงจากเวลาเริ่มนับตอนเปิดเครื่อง (อย่าง Arduino มีคำสั่ง millis()) แล้วนำมาคำนวน โดยการเอาเวลาที่รับได้ของเฟรมปัจจุบัน ลบกับ เฟรมก่อนหน้า ก็จะได้ค่า Delta Time ออกมานั่นเอง

ยกตัวอย่างการเคลือนที่ตัวละคร โดยมีฟังก์ชั่น DrawHero ทำการวาดตัวละคร โดยรับค่า พิกัด x,y ขึ้นมา

ตัวอย่างที่ไม่ดี แบบแรก แบบไม่ใช้ Delta Time

function loop()
{
while(isPlayGame)
{
hero.x += 5; //เคลื่อนที่ hero ในแกน x ที่ 5 พิกเซล ต่อเฟรม
DrawHero(hero.x , hero.y);
}
}

โดยวิธีนี้ เราจะกำหนดความเร็วของตัวละคร ด้วยการกำหนดค่าคงที่ขึ้นมาซักตัว โดยเราทดสอบแล้วว่าเครื่องของเราสามารถรันลูปอย่างคงที่ อยู่ที่ 50 รอบต่อวินาที จึงเขียนโปรแกรมเพิ่มค่า hero.x ทีละ 5 ในแต่ละรอบ เมื่อครบ 1 วินาที จะได้ 5 * 50 = 250 พิกเซล ต่อ วินาที

ทั้งนี้วิธีนี้จะมีข้อเสียคือ เราไม่รู้เลยว่าเครื่องอื่นมีความเร็วที่เท่าไหร่ หากมีเครื่องรันได้ 100 รอบต่อวินาที ความเร็วจะเปลี่ยนเป็น 5 * 100 = 500 พิกเซล ต่อ วินาที หรือ ถ้ามีเครื่องรันได้ 20 รอบต่อวินาที จะกลายเป็น 5 * 20 = 100 พิกเซล ต่อ วินาที แทน ซึ่งเราจะเห็นได้อย่างชัดเจนว่า แต่ละเครื่องตัวละครเราเคลื่อนที่ด้วยความเร็วที่ไม่เท่ากัน

ตัวอย่างที่ไม่ดีแบบที่สอง แบบที่ใช้ setInterval

function main()
{
// ทำการ Set Interval ให้ฟังก์ชั่น loop ทำงาน 50 ครั้ง ต่อ 1 วินาที
// โดย Interval มีหน่วยเป็น มิลลิวินาที จึงทำให้ 1 วินาทีมีค่าเป็น 1000 มิลลิวินาที
setInterval(loop, 1000/50);
}
function loop()
{
// เคลื่อนที่ hero ในแกน x ที่ 250 พิกเซล ต่อ วินาที
// โดย 1/50 คือเวลาที่ใช้ต่อรอบ มีค่าเป็น 0.02 วินาที
hero.x += 250 * (1/50);
DrawHero(hero.x , hero.y);
}
}

หากภาษารองรับคำสั่ง setInterval จะช่วยให้เราสามารถกำหนดจำนวนรอบการทำงานต่อวินาทีได้ โดยตามคำสั่งในตัวอย่างจะตั้งอยู่ที่ 50 รอบต่อนาที เราจึงกำหนดค่าคงที่ได้คร่าวๆว่า 1/50 (50 รอบ หาร 1 วินาที) มีค่าต่อรอบเป็น 0.02 วินาทีต่อรอบ

วิธีนี้เกือบจะดีแล้ว ตรงที่ ถ้าเครื่องเรารันอยู่ที่ 50 รอบก็จะรันที่ 50 รอบต่อวินาที แต่ถ้าเครื่องที่เร็วกว่า เช่น รันได้ 100–200 รอบต่อวินาที ก็จะถูกจำกัดความเร็วอยู่ที่ 50 รอบต่อวินาที ทำให้ตัวละคร เคลื่อนที่ด้วยความเร็ว 250 พิกเซลต่อวินาที

แต่วิธีนี้ก็มีข้อเสียอยู่เหมือนเดิมคือ ถ้าเครื่องที่มีความเร็วต่ำกว่า 50 รอบต่อวินาที การแสดงผลก็จะเริ่มผิดเพี้ยน เช่นเครื่องที่รันที่ 20 รอบต่อวินาที จะมีความเร็วอยู่ที่ 250 * (1/50) * 20 = 100 พิกเซลต่อวินาทีแทนอยู่ดี

ตัวอย่างการใช้ DeltaTime

function main()
{
// ทำการ Set Interval ให้ฟังก์ชั่น loop ทำงาน 50 ครั้ง ต่อ 1 วินาที
// โดย Interval มีหน่วยเป็น มิลลิวินาที จึงทำให้ 1 วินาทีมีค่าเป็น 1000 มิลลิวินาที
setInterval(loop, 1000/50);
}
let deltaTime = 0; // ไว้เก็บผลต่างของเวลา
let timeBefore = new Date().getTime(); // เก็บค่าเวลาเฟรมก่อนหน้า
let timeNow = new Date().getTime(); // เก็บค่าเวลาเฟรมปัจจุบัน
function updateDeltaTime()
{
timeBefore = timeNow; //รับเวลาเฟรมก่อนหน้า
timeNow = new Date().getTime(); //รับเวลาเฟรมปัจจุบัน
deltaTime = ( timeNow - timeBefore )/1000; // หาผลต่างของเวลา
}
function loop()
{
updateDeltaTime(); //ทำการอัพเดท DeltaTime เมื่อวนลูป
// เคลื่อนที่ hero ในแกน x ที่ 250 พิกเซล ต่อ วินาที
hero.x += 250 * deltaTime;
DrawHero(hero.x , hero.y);
}

วิธีการใช้ DeltaTime โดยวิธีนี้จะใช้ setInterval หรือไม่ใช้ก็ได้ หรือจะเขียนวิธีหน่วงขึ้นมาเองก็ได้ โดยตัว DeltaTime ก็จะมาช่วยคอยชดเชยผลต่างของเวลาให้อยู่แล้ว

อย่างเครื่องของเราสามารถรันอยู่ที่ 50 รอบต่อวินาที จะได้ DeltaTime 0.02 วินาที ตัวละครจะเคลื่อนที่ด้วยความเร็วที่ 250 * ( 0.02 * 50 ) = 250 พิกเซลต่อวินาที

หากเครื่องความเร็ว 100 รอบต่อวินาที จะได้ DeltaTime ที่น้อยลง อยู่ที่ 0.01 วินาที ตัวละครจะเคลื่อนที่ด้วยความเร็ว 250 * ( 0.01 * 100 ) = 250 พิกเซลต่อวินาที และ

หากเครื่องความเร็ว 20 รอบต่อวินาที จะได้ DeltaTime ที่มากขึ้น อยู่ที่ 0.05 วินาที ตัวละครจะเคลื่อนที่ด้วยความเร็ว 250 * ( 0.05 * 20 ) = 250 พิกเซลต่อวินาที เช่นกัน

หรือหากเราต้องการจะจับเวลาเพื่อทำอะไรบางอย่าง เช่น นับเวลาจบเกม เราสามารถเขียนได้แบบตัวอย่างข้างล่าง

ตัวอย่างการเขียนโค๊ด นับเวลา ด้วย deltaTime

let isTimeOut = false;    // เก็บค่าเช็คเตือนหมดเวลา
let timeCount = 0; // เก็บค่านับเวลา
let timeMax = 3; // เก็บค่าเวลาที่จะเริ่มเตือน
function loop()
{
updateDeltaTime(); //ทำการอัพเดท DeltaTime เมื่อวนลูป
timeCount += deltaTime; // ทำการนับเวลา
if(timeCount >= timeMax && !isTimeOut) //การเช็คว่าหมดเวลาหรือยัง
{
alert("Time Out!!"); // ถ้าหมดเวลาให้เตือน Time Out!!
isTimeOut = true;
}
}

3. Output

การทำเกมต้องมีสิ่งที่แสดงผลออกมาโดยทำการวาด และ ลบ อยู่ตลอด ดังนั้นเราต้องดูว่าภาษาหรือเครื่องมือที่เราใช้ มีคำสั่งช่วยในการแสดงผลอยู่หรือเปล่า อย่าง HTML5 สามารถเขียนเกมได้ เพราะมีคำสั่ง <canvas> ที่เป็นเหมือนกระดานวาดภาพ ที่ทำให้สามารถสั่งแสดงรูปได้ หรือ ใน Arduino เองแม้ว่าไม่มีส่วนแสดงผล แต่ก็สามารถต่อจอ Dot Matrix หรือจอ OLED และทำการวาดภาพโดยการส่ง Array 2 มิติ เข้าไป หรือถ้าภาษาไม่รองรับการแสดงผลรูป เราก็ยังสามารถทำเกมบน command line โดยการล้างจอแล้วพิมพ์ใหม่ก็ได้

ตัวอย่างคำสั่งการวาดใน HTML5

<canvas id="myCanvas" width="250" height="300"/> // กรอบสำหรับวาดภาพ<script>
var c = document.getElementById("myCanvas"); // เรียกกรอบสำหรับวาด
var ctx = c.getContext("2d"); // เรียกคำสั่งสำหรับวาด
var img = document.getElementById("scream"); // ดึงค่ารูปจากแท็ก
ctx.clearRect(0, 0, c.width, c.height); // ทำการล้างกรอบวาดภาพ
ctx.drawImage(img, 10, 10); // ทำการวาดภาพเข้าไปใหม่
</script>

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

ตัวอย่างแกนพิกัดในคอมพิวเตอร์ (ซ้าย) และ แกนพิกัดที่คนธรรมดาเข้าใจ (ขวา)

จากที่กล่าวไปปกติแล้วจะใช้แกนคอมพิวเตอร์ในการเขียนโค๊ด แต่ผมเคยเจอกรณีที่ต้องแปลงแกนไปมาในโปรเจ็กท์เกม Tetris เนื่องจากจอแนวนอน แต่ทำเกมแนวตั้ง และไม่มีซอฟต์แวร์แปลงแกนให้ รวมทั้งตัวภาษา และ ตัวจอเองมีพิกัดแกนไม่เหมือนกัน เลยต้องคำนวนเปลี่ยนแกนเอง โดยเปลี่ยนพิกัดจาก พิกัดในอาเรย์ เป็น แกนพิกัดคอมพิวเตอร์ แล้วแปลงเป็นแกนพิกัดจออีกทีเป็นต้น

ตัวอย่างการคิดแกนพิกัด ในเกม tetris สามารถดูเพิ่มเติมใน บทความ

4. Input

แน่นอนว่าการเขียนเกมเราจะต้องมีการรับค่าเข้ามาโดยพื้นฐานแล้วจะรับจาก เมาส์ คีย์บอร์ด หรือจอย และให้ดูว่าภาษาที่เราใช้เขียนรองรับการรับค่าปุ่มกดหรือเปล่า หรือจะใช้การรับค่าเป็นอย่างอื่นก็ได้ เช่น arduino มีการรับค่าจากขา digital / analog หรือรับค่าจากอุปกรณ์อย่างอื่นเช่น kinect , leap motion , gyroscope , sensor เพื่อรับค่าการจับการเคลื่อนไหวก็ได้

ตัวอย่างการรับค่าปุ่มกดบน HTML5

// ทำการเขียน Event รับค่า keydown , keyup document.addEventListener('keydown',getKeyDown,false);
document.addEventListener('keyup',getKeyUp,false);
let keyDown = null; // ตัวแปรที่ใช้รับค่าปุ่มกดfunction getKeyDown(e) // event ถูกเรียกเมื่อมีการกดปุ่มลง
{
keyDown = e.keyCode; // รับค่าเมื่อกดปุ่มลง
}
function getKeyUp(e) // event ถูกเรียกเมื่อมีการกดปุ่มขึ้น
{
keyDown = null; // ล้างค่าเมื่อยกปุ่มขึ้น
}
const hero = { x: 50 , y: 250 }
const speed = 50;
function loop()
{
// เมื่อทำการกด w หรือปุ่มลูกศรขึ้นค้าง จะทำการเคลื่อนที่ตัวละครขึ้นข้างบน
if(keyDown === 38 || keyDown === 87)
{
hero.y -= speed * deltaTime;
}
DrawHero(hero.x , hero.y);
}

สิ่งที่ควรรู้เพิ่มเติมในการทำเกม

Collision detection

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

ตัวอย่างเช็คชน ที่ไม่สามารถทะลุได้ (ที่มา https://tvtropes.org/pmwiki/pmwiki.php/Main/InsurmountableWaistHeightFence)

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

ตัวอย่างการเช็คชน (ที่มา http://treadgaming.blogspot.com)

True Random

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

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

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

ตัวอย่าง สุ่มแบบมีรูปแบบ (ซ้าย) และ สุ่มที่แท้จริง (ขวา) (ที่มา https://puzzlewocky.com/brain-teasers/randomness-and-patterns/)

Roulette

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

ตัวอย่างวงแหวนแห่งการสุ่ม (ที่มา https://apexcharts.com)

ตัวอย่างการเขียนวงแหวนแห่งการสุ่ม

function gachaLife()
{
const rand = Math.random() * 100;
if(rand < 50) { alert("You Get Salt."); }
else if(rand < 99) { alert("You Still Get Salt."); }
else if(rand < 99.999){ alert("You Get SR Card."); }
else { alert("You Get SSR Card."); }
}

Vector Physics

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

แต่ทั้งนี้ ให้ระวังภาษาที่ใช้ด้วยว่า คำสั่งในการคำนวนคณิตศาสตร์นั้น้มีการรับค่าองศามีหน่วยเป็น Radian หรือ Degree กันแน่ และอย่าลืมแปลงหน่วยให้ถูกต้อง

แปลงหน่วยจาก Degree เป็น Radian

degree = radian * ( 180 / PI )

แปลงหน่วยจาก Radian เป็น Degree

radian = degree * ( PI / 180 )

สูตรในการหาระยะระหว่าง 2 จุด

c^2 = (xA − xB)^2 + (yA − yB)^2

ตัวอย่างการเขียนโค๊ด

const A = { x: 5  , y: 4  };
const B = { x: 10 , y: 20 };
const distance = Math.sqrt( Math.pow(A.x - B.x , 2)
+ Math.pow(A.y - B.y , 2));

สูตรในการหาองศาระหว่าง 2 จุด

θ = atan2 (yB − yA , xB - xA) // ใช้ yA − yB ในกรณีแกน y ชี้ลง

ตัวอย่างการเขียนโค๊ด

const A = { x: 5  , y: 4  };
const B = { x: 10 , y: 20 };
const angle = Math.atan2(A.y - B.y, B.x- A.x) * (180 / Math.PI);

สูตรในการแตกแรง

xA = E cos θ
yA = E sin θ

ตัวอย่างการเขียนโค๊ด

const speed = 50;
const angle = 60 * ( Math.PI / 180); // แปลง Degree เป็น Radian
const forceX = Math.cos(angle) * speed;
const forceY = Math.sin(angle) * speed;

Projectile Physics

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

ตัวอย่างของการเคลื่อนที่ ​Projectile (ที่มา http://www.ncsec.org/team5/students/objectives.html)

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

พอถึงจุดนี้เราสามารถเขียนโค๊ดการเคลื่อนที่แบบโปรเจกไตล์แบบไม่ต้องใช้สูตรฟิสิกส์จริงๆได้

ตัวอย่างการเขียนโปรเจกไตล์ในแนวดิ่ง โดยไม่ได้ใช้สูตรฟิสิกส์จริงๆ

const hero = {x:50,y:250}
const forceOnStart = 500; // กำหนดแรงเริ่มต้น
const gravity = 9.81; // แรงดึงดูด
let force = forceOnStart; // แรงที่กระทำต่อวัตถุ
function loop()
{
updateDeltaTime();
hero.y -= force * deltaTime; // แรงที่กระทำต่อวัถตุ
force -= gravity; // แรงจะลดลงเรื่อยๆจากแรงดึงดูด
DrawHero(hero.x , hero.y);
}

หรือเราสามารถใช้การแตกแรงเข้ามาช่วยในการเคลื่อนที่แบบโปรเจกไตล์

const hero = { x: 50 , y: 250 }
const forceOnStart = 500; // กำหนดแรงเริ่มต้น
const gravity = 9.81; // แรงดึงดูด
const angle = 40 * ( Math.PI / 180 ); // องศา ของแรง
let forceX = forceOnStart * Math.cos(angle); // แรงที่กระทำในแนวราบ
let forceY = forceOnStart * Math.sin(angle); // แรงที่กระทำในแนวดิ่ง
function loop()
{
updateDeltaTime();
hero.x += forceX * deltaTime; // แรงที่กระทำต่อวัถตุในแนวราบ
hero.y -= forceY * deltaTime; // แรงที่กระทำต่อวัถตุในแนวดิ่ง
forceY -= gravity; // แรงจะลดลงเรื่อยๆจากแรงดึงดูด
DrawHero(hero.x , hero.y);
}
ตัวอย่างผลลัพธ์การเขียนโปรแกรมการเคลื่อนที่แบบโปรเจกไตล์

ส่งท้าย

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

สำหรับใครที่ชื่นชอบ หรือคิดว่าบทความนี้มีประโยชน์ ฝากกดติดตาม medium หรือ facebook ของ บริษัทต่างโลก อินเตอร์แอคทีฟ ด้วยนะครับ

หรือใครมีความสนใจด้าน XR (VR MR AR) และ Interactive สามารถพูดคุยกันได้ที่กลุ่มนี้ครับ XR (VR MR AR) and Interactive Developers Thailand (XRIDT)

สำหรับใครสนใจการเขียน GameLoop เอง ผมยังมีซอร์สโค๊ดที่ทำเกมงูด้วย ESP8266 สามารถเข้าไปดูได้ คลิกตรงนี้ ครับ

หรือหากใครสนใจอยากศึกษาต่อยอดพื้นฐานการทำเกม ผมแนะนำหนังสือที่อาจารย์ผมเขียน สร้างเกม ด้วย HTML 5 น่าจะยังมีขายอยู่ตามร้านขายหนังสือทั่วไป (ปล. อันนี้ผมไม่ได้มีส่วนได้ส่วนเสียนะครับ แค่มาแนะนำเฉยๆครับ)

หนังสือ สร้างเกมด้วย html5 ของอาจารย์ผม

--

--

Pagon GameDev
บริษัทต่างโลก อินเตอร์แอคทีฟ จำกัด

Interactive Developer ผู้สนใจการทำเกม และ Interactive XR VR MR AR มากว่า 10 ปี ปัจจุบันเป็น Co-Founder บริษัท ต่างโลก อินเตอร์แอคทีฟ จำกัด