บทความนี้บอกตามตรงว่ามีที่มาพอสมควรครับ จะพูดว่าดองมานานอยู่เหมือนกัน บทความนี้ต่อจากบทความนึงที่เขียนหลายเดือนที่แล้วโดยเพื่อนผมเองครับ
บทความที่ว่าเนี้ยเขียนอธิบายการติดตั้ง SFML มาใช้กับ Visual Studio 2019 ซึ่งบอกตรงๆว่าหนักหน่วงอยู่พอสมควร แต่บังเอิญว่าบทความนี้ก็ยังเขียนแค่วิธีการติดตั้ง ผมกับเจ้าของบทความที่แล้วเลยคุยๆกันว่า เฮ้ย เราไม่ทำต่อวะ เขียนวิธีทำเกมด้วย SFML เลย ผมก็เห็นดีด้วยเลยตกลงว่าจะเขียน
วันนี้ก็เลยจากวันนั้นมา หลายเดือนพอสมควรครับ พูดง่ายๆก็คือดองมาหลายเดือน 555 ก็ถ้าใครอยากสร้างเกมแล้วยังไม่ติดตั้งก็กดบทความข้างต้นไปแล้วทำตามเลยนะครับ
ผมจะบอก Objective ที่ตั้งใจจะเขียนในวันนี้ก่อน เกมที่เราจะเขียนในวันนี้มีความสามารถง่ายๆอยู่ 3 อย่างคือ
- สามารถกด W A S D แล้วทำให้ตัวละครเคลื่อนที่ได้
- เมื่อตัวละครเคลื่อนที่ตัวละครจะขยับโดยมี animation ด้วย
- เมื่อตัวละครไปชนวัตถุในแมพ มันจะย้อนกลับมาที่จุดเริ่มต้น
อาจจะเห็นว่าง่าย แต่บอกตรงๆว่าปาดเหงื่อปาดน้ำตาไปหลายรอบอยู่ครับ 5555
เริ่มกันก่อน สิ่งแรกที่เราจะทำก็คือโครงสร้างของเกมครับ โดยโค้ดข้างบนเนี่ยสิ่งที่มันจะทำก็คือแสดงหน้าต่างดำๆขึ้นมาอันนึง ซึ่งหน้าต่างนี้ก็เหมือนหน้าต่างในโปรแกรมต่างๆครับ ที่มีให้กด minimize close restore นั่นแหละ
จะเห็นว่าในบรรทัดที่ 4 เราประกาศ object ตัวนึงขึ้นมาชื่อ window โดย window รับค่าเข้าไป 2 ค่า ค่าแรกคือ sf::VideoMode(1080, 720)
ค่าที่สองคือ “Game from scratch!”
ค่าแรกจะเป็นการกำหนดว่าหน้าต่างของเรามันจะกว้าง สูงเท่าไร โดยมีหน่วยเป็น pixels ครับ ส่วนค่าที่สองจะเป็น Title ที่แสดงบนหน้าต่างบอกว่าโปรแกรมนั้นชื่ออะไร
ในบรรทัดที่ 7 จนถึง 15 จะเป็น loop ซึ่ง loop นี้ปกติจะเรียกกันว่า Game Loop ครับ ถ้าเคยได้ยินคำว่า FPS หรือ Frames Per Second มันก็มาจากเจ้า loop นี้นี่แหละครับ โดยทุกๆรอบที่ Loop นี้ทำงาน จะนับเป็น 1 Frame ซึ่งถ้าคอมพิวเตอร์ใครแรงๆหน่อยก็จะรันได้หลาย Frame นั่นเอง
ในการออกแบบเกม Game Loop เป็นส่วนที่ Main หลักของมันเลยครับ ในแต่ละ loop เราจะรับค่า input อะไรแล้วจะ output อะไรก็จะทำใน loop
ในบรรทัดที่ 9–12 ก็เรียกว่าเป็นส่วนของการรับ input แล้วมาแสดงผล โดยสิ่งที่เราทำคือเช็คว่า input เป็นปุ่ม Escape หรือเปล่า ถ้าใช่เราจะปิดหน้าต่างนี้ลง
ส่วนบรรทัดที่ 14 window.clear() จะทำหน้าที่เคลียร์หน้าจอที่ค้างอยู่ใน frame นี้ก่อนที่จะวาดใหม่ใน frame หน้า
จบกันไปแล้วสำหรับส่วนของการแสดงหน้าต่างขึ้นมา!! ปาดเหงื่อไปหลายหยดครับแต่ยังไม่จบครับ สิ่งต่อไปที่เราจะทำคือแสดงอะไรซักอย่างขึ้นมาในหน้าจอของเรา ซึ่งในที่นี้ผมจะขอเลือกแสดงวงกลมขึ้นมาครับ
อนึ่ง เราไม่จำเป็นต้องทำตาม flow ข้างบนก็ได้ครับ แต่ก็ควรจะทำเป็น Best Practice
ในการวาดวงกลมขึ้นมาผมเพิ่มโค้ดไป 4 บรรทัด
บรรทัดที่ 8 เป็นการ Initialize Object ขึ้นมาตัวนึง เป็น วงกลมเส้นผ่านศูนย์กลาง 100 pixel ชื่อว่า collision
บรรทัดที่ 9 คือย้ายเจ้าวงกลมเนี่ยไปที่พิกัด (200, 200)
บรรทัดที่ 10 เพิ่มสีให้มัน ผมตั้งให้เป็นสีแดง
ส่วนบรรทัดที่ 14 เป็นบรรทัดที่สำคัญมากครับ ลืมไม่ได้และต้องนำไปวางให้ถูกที่ด้วย เราจะต้องสั่งให้ window ของเราวาด Object วงกลมนี้บนหน้าต่างครับ (ถ้าลืมมันก็จะไม่ขึ้นบนจอ) แล้วก็เอาวางไว้ก่อน window.display() ด้วยครับ
ต่อไปเราจะ render ตัวละครของเราออกมาซักทีครับ
ในบรรทัดที่ 2 ผม include iostream เข้ามาเพราะต้องการใช้ cout
บรรทัดที่ 14 เป็นบรรทัดที่เราสร้าง object ขึ้นมาเก็บ Texture ครับ
ในบรรทัดที่ 15 เราจะโหลด texture เข้ามา แต่ที่ใส่ if ไว้เช็คเพราะว่าอยากจะรู้ว่าโหลดสำเร็จมั้ย ถ้าโหลดไม่สำเร็จก็จะ cout ออกมาว่าเราโหลดไม่ได้นะ
ในบรรทัด sprite จะเป็นบรรทัดที่ทุกอย่างดูซับซ้อนขึ้นครับ
sprite ใน sfml เนี่ย คือกรอบๆนึง ซึ่งจะเอาไปชี้พื้นที่ใน texture นั้นว่าจะให้มันแสดงอะไรออกมา ทีนี้ถ้าเราอยากให้ sprite เราแสดงอะไรเราจึงต้องกำหนดกรอบให้มันครับ
บรรทัดที่ 21 ประกาศ sprite ขึ้นมา
บรรทัดที่ 22 สั่งให้มันไปชี้เจ้า texture ที่เราโหลดไว้
บรรทัดที่ 23 คือสั่งให้มันไปชี้ที่จุด 0, 0 ของ texture และกรอบของมันกว้างยาว 32, 38 ตามลำดับ
ไอ้กรอบกว้างยาวเท่าไร นี่เราสามารถหาได้หลายวิธีครับ แต่เนื่องจากว่าตอนนี้เป็นเวลาดึกแล้ว ผมง่วงแล้ว 5555 เลยมั่วเลขเอาเลยดีกว่า ง่ายดี
ทีนี้เราก็ไปสั่งให้มัน draw ก่อนที่จะ display
อันนี้คือผลที่ผมได้ครับ จะเห็นว่า sprite ของเรามันยืนหันไปข้างหลังซึ่งถ้ามาดูในไฟล์ texture ที่ผมโหลดเข้าไป
จะเห็นว่าชี้จาก (0,0) ของกว้าง 32 ยาว 38 ก็คือมันแสดงถูกครับ
ทีนี้เรามีสไปรต์แล้ว แต่มันไม่เห็นเคลื่อนที่ได้เลย ทำยังไงดี
ผมเพิ่มโค้ดบรรทัดที่ 27–42 ซึ่งจริงๆแล้วมันทำหน้าที่เหมือนกันเกือบทุกอันครับ
การทำงานของมันเหมือนกับเช็คปุ่ม Escape เราตอนแรกเลย เพียงแต่เราเปลี่ยนปุ่มที่ใช้เช็คเท่านั้น
เมื่อผมกดปุ่ม A ผมจะสั่งให้ sprite ของเราเคลื่อนที่ไปทางซ้ายด้วยการเรียกใช้ เมธอด move() ซึ่งจะรับค่าเป็น sf::Vector2f หรือถ้าขี้เกียจจะเขียนเป็น {(x),(y)} ไปก็ได้ครับ
ต่อไปจะเป็นการทำ animation ให้เจ้า sprite ของเราโดยแนวคิดของเราเนี่ยจะคล้ายกับหนังฟิล์มครับ
เวลาเราไปดูหนังที่เห็นว่าเป็นภาพเคลื่อนไหว จริงๆแล้วมันคือภาพนิ่งหลายๆภาพฉายต่อๆกัน จนเราเห็นว่าเหมือนมันเป็นภาพเคลื่อนไหว
สิ่งที่เราจะทำคือเลื่อนกรอบที่ sprite ของเราชี้บน texture ไปเรื่อยๆนั่นเองครับ
โดยจากตอนแรกที่เราชี้จาก 0, 0 เราก็เลื่อนไป 32 (ความกว้าง 1 ช่อง) เรื่อยๆ จนหมดสไปรต์แล้วก็เลื่อนมาช่องแรกใหม่แล้วก็วนไปเรื่อยๆ
บรรทัดที่ 24, 25 ผมตั้งขึ้นมาเพื่อหาระยะกว้างยาวของช่องแต่ละช่องครับ สังเกตจาก texture ที่ผมโหลดเข้ามาใช้ในแกน x มี 3 ตัว ส่วนแกน y มี 4 ตัว ก็หารไปตามจำนวนตัวเลย
จากนั้นผมสร้างตัวแปรชื่อ animationFrame ขึ้นมาเพื่อเก็บว่าตอนนี้เราเล่น animation อยู่ที่ frame ไหน
ในบรรทัดที่ 39, 44, 49, 54 จะเป็นช่วงที่ซับซ้อนครับ เพราะเป็นส่วนที่เราใช้เล่น animation จริงๆ
จากภาพจะเห็นว่าแต่ละช่องเราก็แค่ย้ายช่องชี้ไปเรื่อยๆ เริ่มจาก 0, 0 ไป (1 * spriteSize, 0) ไป (2 * spriteSize, 0)
ถ้า spriteSize เท่ากับ 32 ก็จะเป็นจาก 0, 0 ไป 32, 0 ไป 64, 0 ไปเรื่อยๆ
ซึ่งไอ้ตัวเลข 0 1 2 นี่แหละครับ ก็คือ animationFrame ของเรา เราจะย้ายช่องไปเรื่อยๆจนช่องหมดก็กลับไป 0, 0 ใหม่
แต่ถ้าเราเล่น animation อื่นที่ไม่ใช่ที่ y = 0 เราก็แค่ต้องเพิ่มค่า y เข้าไป
อย่างถ้าดูเวลาไปทางซ้ายของผม แกน y ผมเริ่มจาก spriteSizeY * 3 เพราะว่า animation ไปทางซ้ายของผมมันเป็นตัวที่ 4 ใน texture (เริ่มนับจาก 0)
ส่วนต่อไปจะเป็นส่วนสุดท้ายที่เราจะทำในบทความนี้แล้วครับ ซึ่งก็คือการ check collision หรือตรวจจับการชนนั่นเอง
โดยสิ่งที่เราต้องการคือเมื่อชนวงกลมแดงๆแล้วตัวเราจะเด้งกลับไปที่จุดเกิดของเรา
บรรทัดที่ 29, 30 ผมตั้งจุดเกิดใหม่ให้เป็นจุด 0, 0 แล้วก็ setPosition สไปรต์ไป 0, 0 ซะ (จริงๆ default ก็เป็นแบบนี้อยู่แล้วแหละครับ ทำทำไม่ก็ไม่รู้)
ในบรรทัดที่ 69 ถึงจะเป็นบรรทัดที่เราเช็ค collision จริงๆโดยเราตั้งเงื่อนไขว่า
ซึ่งเป็นเงื่อนไขที่หน้ากลัวมาก 5555
เมธอด getGlobalBounds() ใช้สำหรับคืนค่าพิกัดของกรอบของออปเจคต์นั้นๆครับ ส่วน intersect() ก็ตามชื่อใช้เช็คว่าวัตถุมันซ้อนกันหรือเปล่า
ซึ่งการชนกันก็หมายถึงว่ามีจุดใดจุดหนึ่งของกรอบมีพิกัดเดียวกัน หรือซ้อนกันครับ
ถ้าหากเงื่อนไขข้างต้นเป็นจริง Player ของเราจะโดนดีดไปจุด 0, 0 ซึ่งคือจุดเกิดที่เราตั้งไว้ครับ
ก็เป็นว่าเราทำ objective ทั้งหมดที่เราตั้งใจจะทำเสร็จจนหมดแล้วครับ แต่อันที่จริงยังมีอีกหลายส่วนที่สามารถแก้ได้ อย่างเช่น animation ของเรามันแสดงเร็วเกินไป ทำยังไงให้มันช้าลง หรือส่วนของการรับ input ซึ่งสามารถเขียนได้ดีกว่านี้
แต่เนื่องจากตอนนี้ดึกแล้ว ผมง่วงแล้วครับ เอาเป็นว่าขอจบบทความแค่นี้แหละครับ 5555