libGDX เขียนเกม part 3: Life Cycle & Main
เพื่อนๆได้เขียน Kotlin ไปบ้างแล้ว โอกาสนี้จะเคลื่อนที่จาก desktop มาที่ core และเริ่มต้นเขียน Kotlin จริงจังขึ้น ทำความเข้าใจ libGDX Life Cycle และ ApplicationListener Interface ครับ
จากโครงสร้างไฟล์ข้างต้น มันคือ folder ชื่อ core ที่ที่เราจะนำ game logic, รูปภาพ, เสียง, กล้องและอื่นๆมารวมกันเป็นเกมของเรา ภายใน folder นี้ประกอบด้วย
- assets เก็บ resources ต่างๆ เช่น ไฟล์รูปภาพ, ไฟล์เสียงและอื่นๆ
- build เกิดจากการทำงานของ Compiler ไม่ต้องสนใจ
- src พื้นที่เขียนโปรแกรม
- build.gradle มีเพราะเราเขียนโปรแกรมภาษา Kotlin ไม่ต้องสนใจ
ภายใน src ประกอบด้วย
- Packages
- Class & Interface และอื่นๆ
Packages ง่ายๆเลยคือแยกแยะหรือจัดกลุ่ม Class & Interface (และอื่นๆ) ออกจากกันหรือรวมกันเพื่อให้เกิดความคิดในการจัดการ มักเล่าเรื่องจากใหญ่ไปหาเล็ก ตัวอย่าง บ้านหนึ่งหลังประกอบด้วยหลายห้อง ดังนั้น packages จะเล่าว่าเรามีบ้านก่อน จากนั้นจึงมีห้อง แต่ละห้องจำแนกต่างกัน ห้องนอน, ห้องน้ำ, ห้องครัว, ห้องเก็บของ, โรงรถ, สนามหญ้า เป็นต้น
แต่ในทางการเขียนโปรแรกรม packages เป็นมากกว่านั้น มันอาจเป็นส่วนประกอบของ Design Patterns เช่น MVC เพื่อแยกส่วนของ Model ออกจาก View และออกจาก Controller หรือไว้จัดกลุ่มของคลาสธรรมดา (Normal Classes) กลุ่มของคลาสช่วยเหลือ (Helper Classes) ประมาณนั้น
Class ก็คือคลาส เป็นการนิยามองค์ประกอบและพฤติกรรมที่วัตถุพึงมี คลาสจะนำไปสร้างเป็นวัตถุ (หรือเรียกว่า ออบเจ็กต์ (Object)) ได้มากเท่าต้องการ เช่น คลาสยานอวกาศ เราก็นิยามว่าต้องเป็นรูปทรงสามเหลี่ยม มีห้องบัญชาการ ติดจรวจ ปืนกล เลเซอร์ สามารถบินได้อย่างอิสระ เร่งหรือลดความเร็วได้ ทีนี้เราก็มาสร้างเกมต่อสู้ระหว่างดวงดาวที่ผู้เล่นต้องสวมบทบาทเป็นนักบินขับยานอวกาศ ฮัดซ่า! อูตะเร้! เมื่อผู้เล่นหนึ่งคนออนไลน์เข้ามาในเกม เขาจะกลายเป็นนักบินและเลือกยานอวกาศของตนเอง ติดอาวุธแล้วประจัญบานร่วมรบไปกับผู้เล่นคนอื่นๆ เห็นไหม จากนิยามคลาสเป็นยานอวกาศ จากนั้นนำมาสร้างเป็นวัตถุยานอวกาศภายในเกมสัก 500 ลำก็เพลิดเพลินจิตแล้ว
“นักบินคุณลุงได้เลือกเรือดำน้ำแทนยานอวกาศ!? เกิด bug ซะแล้ว!”
Interface ก็คืออินเตอร์เฟซ เป็นการนิยามความสามารถหรือการสื่อสารระหว่างโปรแกรม เช่น เราสร้าง interface ชื่อ flyable หมายถึง บินได้ ให้กับคลาสยานอวกาศ นั่นหมายความว่า ยานอวกาศนั้นสามารถบินได้ ยานอวกาศลำไหนไม่มี flyable interface ก็จะบินไม่ได้ ดั่งนักบินที่เลือกเรือดำน้ำก็จะบินไม่ได้ เพราะใช้ interface อื่นที่ไม่ใช่ flyable และไม่สามารถเล่นเกมนี้ได้
ใครเพิ่งมาอ่าน นี่คือความเดิมตอนที่แล้ว
libGDX Life Cycle
วงจรชีวิตของ libGDX หลังจากถูก run เกิดขึ้นเป็นปกติของมันอยู่แล้ว แต่ละ state มีความหมายในตัวเอง แต่ละ state เราสามารถเพิ่มโค้ดเข้าไปได้เพื่อให้มันกลายเป็นเกมที่เราต้องการ ขอแนะนำการแทรกโค้ดเข้าไปในแต่ละ state ด้วย interface ที่ชื่อ ApplicationListener
ApplicationListener States
ประกอบด้วย state ต่อไปนี้
- create สร้างและประกาศ
- resize เปลี่ยนขนาดจอภาพ
- render วาด เล่นเกม
- pause หยุดชั่วคราว
- resume เล่นต่อ
- dispose คืนทรัพยากร
ซึ่งปกติแล้วคลาส Main ที่เราได้สร้างขึ้นก็จะใช้งาน interface นี้ครับ
libGDX Life Cycle
วงจรชีวิตของ libGXD ดูภาพ
เมื่อโปรแกรมหรือเกมของเราเริ่มต้นขึ้น state แรกที่จะพบคือ create ณ state นี้จะใช้กำหนดค่าเริ่มต้นให้กับตัวแปร โหลด resources ที่อยู่ใน assets folder ไปไว้ใน memory รวมไปถึงการจัดวางตำแหน่งวัตถุต่างๆในโลกของเกม
ตามมาติดๆกับ resize ณ state นี้เกมของเราจะได้รับผลจากการปรับขนาดจอภาพซึ่งมีขนาดเท่ากับ กว้าง x ยาว มีหน่วยเป็น pixel
ถัดจากนั้น libGDX จะเข้าสู่การ handle system events หรือการจัดการสิ่งต่างๆที่เกิดขึ้นจากระบบ เช่น กดปุ่ม home, รับสายโทรศัพท์, แจ้งเตือน battery ใกล้หมดและอื่นๆ เหล่านี้จะเกิดขึ้นกับระบบ android ทำให้เกมต้องหยุดชั่วคราวจะเข้าสู่ pause ณ state นี้เราจะบันทึกค่าตัวแปรที่สำคัญเก็บไว้ เมื่อผู้เล่นเล่นต่อ resume จะถูกเรียก ณ state นี้ค่าตัวแปรที่ได้บันทึกไว้ก็จะโหลดเข้าสู่ตัวเกมอีกครั้ง และวนกลับไปที่ handle system events
สำหรับระบบที่ไม่ใช่ android หากออกจากเกมหรือเกมถูก terminate วงจรจะทำงาน pause ณ state นี้ก็ควรบันทึกค่าตัวแปรที่สำคัญเก็บไว้หรือจะ save game ก่อนจะปิดเกมให้อัตโนมัติก็ได้ จากนั้น dispose จะถูกเรียก ณ state นี้จะคืนทรัพยากรที่เรียกใช้ตลอดทั้งเกมคืนแก่ระบบไป (cleanup to free all the resources)
กลับมาพูดที่ handle system events ถ้าไม่ปรากฏเหตุการณ์ใดๆก็ตามที่จะทำให้เข้าสู่ pause state วงจรจะทำงาน render ซึ่ง state นี้จะทำสิ่งสำคัญ 2 อย่าง ได้แก่
- update game logic
- วาดฉากลงบน screen โดยอาศัย game logic ที่เกิดขึ้นนั้น
ขณะที่เกมถูก render หรือเล่นอยู่นั้น โอกาสที่จะเกิดการเปลี่ยนแปลงขนาดของหน้าจอก็มีอยู่ตลอดเวลา เหตุนี้อัลกอริทึม check platform type จะคอยตรวจสอบเสมอว่าขนาดของจอภาพยังเท่าเดิมอยู่หรือไม่ หากไม่ก็จะเข้าสู่ resize state เพื่อคำนวณขนาดใหม่อีกครั้ง
เกมโปรแกรมเมอร์เอ๋ย เจ้าจงทำความเข้าใจ Life Cycle นี้ให้ขึ้นใจ
ApplicationListener ทำงานผ่าน ApplicationAdapter
ภาษา Java และ Kotlin เขียนบนหลักความคิด OOP ย่อมาจาก Object Oriented Programming การเขียนโปรแกรมเชิงวัตถุ คือให้คิดถึงความจริงที่วัตถุนั้นๆเป็นแล้วแปลงเป็นโปรแกรม ประกอบด้วย 4 หลักสำคัญดังนี้
- Abstraction หลักนามธรรม
- Encapsulation ปกป้องปิดบัง
- Inheritance สืบถอดถ่ายทอด
- Polymorphism หลายรูปแบบ
อ่านเพิ่มเติม
ApplicationAdapter ใช้ความคิด abstraction ได้รับสมบัติของ ApplicationListener ประกอบด้วย
ภายในเตียนโล่งไม่พบโค้ดใดๆ มือใหม่อาจถามว่างั้น ApplicationAdapter มีประโยชน์อะไร? คำตอบคือมันเป็นคลาสอำนวยความสะดวกครับ ความเป็นนามธรรมของมันช่วยให้เราสามารถ override เมธอดหรือ state ที่ต้องการได้ ส่วนเมธอดหรือ state ไหนที่ต้องการก็ไม่ต้องไปสนใจไงล่ะ ตัวอย่างเห็นชัดเจนที่คลาส Main
คลาส Main ใช้ความคิด inheritance สืบทอดสมบัติจากคลาส ApplicationAdapter จึงเข้าใจ libGDX life cycle ได้เลย จากรูปข้างต้นมันสนใจแค่ 3 states ได้แก่ create, render และ dispose เห็นไหม สนใจแค่นี้ก็ override (ใช้งาน) แค่นี้ states อื่นๆก็ปล่อยไป
โอกาสนี้เราจึงควรเขียนคลาส Main กันอีกสักรอบ ให้เป็นภาษา Kotlin
Main.kt
ที่ core folder ให้ลบคลาส Main เดิมนี้ทิ้งไป (Main.java) แล้วคลิกขวา New > Kotlin/Class เลือกเป็นคลาส ตั้งชื่อว่า Main (Main.kt)
package com.pros
class Main {
}
กำหนดให้สืบทอด ApplicationAdapter
package com.pros
import com.badlogic.gdx.ApplicationAdapter
class Main: ApplicationAdapter() {
}
เราจะเขียนแค่นี้ก่อนครับ จากนั้นให้ไปจัดการ Launcher ที่ขึ้น error ดังรูป มองหาใน desktop folder
จัดการ import คลาส Main ให้เรียบร้อย (ที่มัน error ก็เพราะว่าเราได้ลบ Main.java เดิมทิ้งไป โค้ด import จึงหายไปด้วย)
ลองรัน main ของไฟล์ DesktopMain.kt (Desktop Launcher)
จะได้หน้าจอสีแดงกระพริบรั่วๆ แต่พอ capture แล้วกลับได้สีดำ เหตุเพราะเราต้องเขียนโค้ด clean screen ด้วย
ลองเพิ่มโค้ด clearn screen ใน core folder ไฟล์ Main.kt สักเล็กน้อยครับ
package com.pros
import com.badlogic.gdx.ApplicationAdapter
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.GL20
class Main: ApplicationAdapter() {
override fun render() {
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
}
}
แล้วรัน
จะได้หน้าจอสีดำและไม่กระพริบใดๆเลย
เพิ่มเปลี่ยนสีพื้นหลัง ขอเป็นสีเขียวนะ RGBA ก็ 0, 1, 0, 1 และเนื่องจากค่าสีเป็น Float จึงต่อท้ายตัวเลขด้วยอักษร F หมายถึงแปลงเป็นชนิด Float
package com.pros
import com.badlogic.gdx.ApplicationAdapter
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.GL20
class Main: ApplicationAdapter() {
override fun render() {
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
Gdx.gl.glClearColor(0F, 1F, 0F, 1F)
}
}
แล้วรัน
ต่อไปนี้เพื่อความไม่สับสนผมขอลบไฟล์ชื่อ DesktopLauncher (DesktopLauncher.java) ทิ้งไปนะครับ โครงสร้างโปรเจกต์เราจะเป็นแบบนี้
สรุป
part นี้เราได้เรียนรู้ libGDX Life Cycle ได้เขียนคลาส Main ใหม่ด้วยภาษา Kotlin จากนั้นเขียนโค้ด override render state มา clear screen กับเปลี่ยนสีพื้นหลัง
โอกาสหน้าเจอกันครับ อย่าลืมศึกษาภาษา Kotlin มาก่อนล่วงหน้าด้วยนะ