เขียนเกมด้วย LibGDX : 4 – Simple Game ภาคแรก
สวัสดีครับ บทความนี้เป็นบทความที่ 4 ครับ จากซีรีส์ สอน LibGDX ครับ
บทความตอนอื่นๆ ในซีรีย์ เขียนเกมด้วย LibGDX ติดตามอ่านได้ตามลิงค์ข้างล่างเลยครับ
- เขียนเกมด้วย libGDX #1 – สร้างโปรเจ็ค
- เขียนเกมด้วย LibGDX #2 – Hello World
- เขียนเกมด้วย LibGDX #3 – Render และการรับ input
- เขียนเกมด้วย LibGDX #4 – Simple Game ภาคแรก
- เขียนเกมด้วย LibGDX #5 – Simple Game ภาคจบ
- เขียนเกมด้วย LibGDX #6 – Simple Game ภาคพิเศษ
- เขียนเกมด้วย LibGDX #7 - Simple Game - scene2d.ui
- เขียนเกมด้วย LibGDX #8 - Simple Game - Actor
หลังจากได้เขียนเรื่อง LibGDX ไป 3 ตอนแล้ว ซึ่งเป็นเบสิคพื้นฐาน ที่ควรรู้ก่อนจะได้เขียนเกมโดยใช้ LibGDX วันนี้เลยนำตัวอย่างการทำเกมส์จริงๆมานำเสนอเลย เพราะคิดว่า เมื่อรู้พื้นฐานบางอย่างแล้ว มาลองดูตัวอย่างจริง แล้วลองทำไปด้วย ทำความเข้าใจไปพร้อมๆกัน และมองเห็นภาพจริงเลย มันน่าจะทำให้เราเข้าใจอะไรๆมากขึ้น
ในตัวอย่างนี้ ผมได้ทำการแปลมาจากต้นฉบับที่อยู่ใน LibGDX Wiki Page รวมถึงเพิ่มเนื้อหาและดัดแปลงบางส่วนให้เข้ากับเนื้อหาของบทความครับจริงๆ ก็ไม่ได้แปลเก่งอะไรมากมาย อาศัยเดามั่วๆครับ อันไหนแปลไม่ออกผมก็ขอข้ามนะครับ ^^ สำหรับใครอยากอ่านต้นฉบับภาษาอังกฤษก็ตามลิงค์ด้านบนได้เลยครับ (LibGDX Wiki นั้นมีการอัพเดทสม่ำเสมอ รวมถึงตัว Library ด้วย มีอัพเดทแทบทุกวัน)
Simple Game
ตัวอย่างนี้จะเป็นการเขียนเกมส์ที่ชื่อว่า Simple Game เป็นเกมแบบง่ายๆ โดยบทความนี้จะครอบคลุมเนื้อหาดังต่อไปนี้ครับ
- Basic File Access
- การเคลียร์ Screen หน้าจอ
- การวาด image
- การใช้ camera
- พื้นฐาน input processing
- เล่น เสีียงและเอฟเฟคต่างๆ
ซึ่งเนื้อหาด้านบน บางอย่างผมก็มีพูดถึงไปแล้ว ในซีรีย์สอน LibGDX แต่ว่าวันนี้ เอาตัว Simple Game มานำเสนอ เพราะดูแล้วเค้าอธิบาย รวมถึงให้ตัวอย่างมาดูแล้ว เข้าใจง่ายครับ
Project Setup
เริ่มแรกก็สร้างโปรเจ็คเลยครับ หากมือใหม่ ก็สร้างตามนี้ครับ สร้างโปรเจ็ค LibGDX
ส่วนรายละเอียดโปรเจ็ค ผมใช้โครงสร้างตามนี้ครับ (ใช้ชื่อตามต้นฉบับ)
- Application name: drop
- Package name: com.badlogic.drop
- Game class: Drop
หลังจากสร้างโปรเจ็คเสร็จ เราก็จะมีโปรเจ็ค 5 โปรเจ็คครับ คือ drop, drop-android, drop-desktop, drop-gwt, drop-ios (หากไม่ได้เลือกอันใดอันหนึ่งออกนะครับ )
The Game
คอนเซปของเกมงั้น ง่ายๆครับ มีเพียงแค่
- ใช้ถังน้ำเก็บเม็ดฝนที่ตกลงมา
- ถังน้ำจะอยู่ที่ตำแหน่งล่างสุดของหน้าจอ
- เม็ดฝน จะสุ่มตกลงมาจากขอบบนของหน้าจอ ทุกๆวินาที
- ผู้เล่น สามารถเคลื่อนย้ายถังน้ำได้แค่ไปซ้ายหรือไปขวา โดยใช้เมาท์หรือแตะหน้าจอ (บนมือถือ) หรือ ใช้คีย์บอร์ด ปุ่มลูกศร ซ้าย/ขวา
- เกมไม่มีวันจบ จนกว่าจะกดปิดเกมเอง ^^
The Assets
Assets คือ รูปภาพ เสียง เอฟเฟคต่างๆ ที่ทำให้ตัวเกมออกมาดูดี สำหรับ Graphic ที่จะใช้กำหนด Resolution ให้กับหน้าจอ จะใช้ขนาด 800x480 pixels (สำหรับ Android ให้ตังโหมด landspace ที่ไฟล์ AndroidManifest.xml ด้วย) หากว่าเครื่องที่ใช้เล่นเกม มีขนาดหน้าจอไม่ตรงกับขนาดที่เรากำหนดไว้ เราจะทำการ scale ทุกอย่างให้พอดีกับหน้าจอ
สำหรับเม็ดฝนและถังน้ำ จะมีขนาด 64x64 pixels. ไฟล์ assets ต่างๆ สามารถหาดาวน์โหลดได้ตามนี้ครับ
หลักจากโหลด assets ไฟล์ด้านบนครบหมดแล้ว ให้ก็อปปี้ทั้งหมดไปไว้ที่ assets ของโปรเจ็ค android ครับ drop-android/assets/
โดยจะมี 4 ไฟล์ด้วยกันคือ drop.wav, rain.mp3, droplet.png และ bucket.png ทำไมเราก็อปไฟล์ไปไว้แค่โฟลเดอร์ drop-android
อันเดียวเอง หากเปิดบน Desktop หรือ HTML5 จะทำไง? จริงๆตัวโปรเจ็คเซ็ตไว้แล้ว ว่าให้ลิงค์ไปที่ assets ของ Android ครับ ฉะนั้นก็วาง assets ไว้ที่โฟลเดอร์เดียว แต่สามารถเรียกใช้ได้ทุก platform
Configuring the Starter Classes
มาถึงการตั้งค่าของคลาส Starter โดยจะใช้โปรเจ็ค Desktop นะครับ เปิด Main.java
ใน drop-desktop/
ต้องการให้มีหน้าจอขนาด 800x480 และมีชื่อว่า Drop ฉะนั้นโค๊ดจะเป้นดังนี้
package com.badlogic.drop;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
public class Main {
public static void main(String[] args) {
LwjglApplicationConfiguration cfg = new LwjglApplicationConfiguration();
cfg.title = "Drop";
cfg.width = 800;
cfg.height = 480;
new LwjglApplication(new Drop(), cfg);
}
}
มาที่โปรเจ็คแอนดรอยส์ ต้องการเซตแอพพลิเคชันให้เปิดเฉพาะแนวนอน เราจะต้องแก้ไขไฟล์ AndroidManifest.xml
ดังนี้ (จริงๆ ตัว libgdx-setup-tool.jar ตัวใหม่ ได้ setup ไว้ให้แล้ว คิดว่าไม่ต้องเปลี่ยนแปลงอะไร)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.badlogic.drop"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="19" />
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:screenOrientation="landscape"
android:configChanges="keyboard|keyboardHidden|orientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
สำหรับโค๊ดด้านบน android:screenOrientation
เซตค่าเป็น "landscape" เพื่อให้เปิดเกมแบบแนวนอน. หากต้องการให้เปิดเกมแบบแนวตั้ง ก็เซตค่าเป็น portrait
เพื่อประหยัดแบต ตัวเกมจะปิด accelerometer และ เข็มทิศ โดยแก้ไขไฟล์ MainActivity.java
จะได้โค๊ดดังนี้
package com.badlogic.drop;
import android.os.Bundle;
import com.badlogic.gdx.backends.android.AndroidApplication;
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
public class MainActivity extends AndroidApplication {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AndroidApplicationConfiguration cfg = new AndroidApplicationConfiguration();
cfg.useAccelerometer = false;
cfg.useCompass = false;
initialize(new Drop(), cfg);
}
}
จะเห็นว่า เราไม่สามารถกำหนด resolution ให้กับ Activity ได้เหมือนกับการกำหนดบน Desktop ตำ resolution ของแอนดรอยส์ มันถูกกำหนดโดย Operating System มันจะ scale ขนาดหน้าจอให้เอง ตามขนาดหน้าจอมือถือครับ
สุดท้าย แก้ไขที่โปรเจ็ค GWT ให้มีขนาด 800x480, เปิดไฟล์ GwtLauncher.java
ใน drop-gwt/
จะได้โค๊ดประมาณนี้ (เวอร์ชั่น HTML5 จำเป็นต้องใช้ Plugin GWT และรันบน Browser หรือก็อปไฟล์ war ไปเปิด หากใครเคยเขียน java ก็คงคุ้นกับไฟล์ .war นะครับ )
package com.badlogic.drop.client;
import com.badlogic.drop.Drop;
public class GwtLauncher extends GwtApplication {
@Override
public GwtApplicationConfiguration getConfig () {
GwtApplicationConfiguration cfg = new GwtApplicationConfiguration(800, 480);
return cfg;
}
@Override
public ApplicationListener getApplicationListener () {
return new Drop();
}
}
โอเค เราได้ทำการตั้งค่าคลาส Starter ทั้งหมดแล้ว ต่อไปก็มาดูที่ตัว main game กันครับ
The Code
โค๊ดสำหรับเกม จะใช้ไฟล์ Drop.java
ที่อยู่ในโปรเจ็ค core drop-core/
Loading the Assets
เมื่อเปิดไฟล์ Drop.java
มาแล้ว สิ่งแรกที่ต้องทำเลยก็คือ โหลดไฟล์ assets ขึ้นมา ไฟล์ assets ควรจะโหลดในเมธอด ApplicationListener.create()
แบบนี้
public class Drop implements ApplicationListener {
Texture dropImage;
Texture bucketImage;
Sound dropSound;
Music rainMusic;
@Override
public void create() {
// โหลดไฟล์รูปถังน้ำและเม็ดฝน ขนาด 64x64
dropImage = new Texture(Gdx.files.internal("droplet.png"));
bucketImage = new Texture(Gdx.files.internal("bucket.png"));
// โหลดเสียงเม็ดฝน และ effect
dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav"));
rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"));
// ตั้งค่าให้เปิดเสียง effect เรื่อยๆ (เ่สียง background)
rainMusic.setLooping(true);
rainMusic.play();
... more to come ...
}
// rest of class omitted for clarity
สำหรับตัวแปรที่ไว้โหลด assets ไฟล์ ได้ประกาศไว้ในคลาส Drop
เพื่อจะไว้เรียกใช้ในภายหลัง. สองบรรทัดแรกในเมธอด create()
คือการโหลดรูปฝนกับรูปตระกร้า. Texture
ไว้สำหรับโหลดรูปภาพ โดยโหลดที่อยู่ของรูปภาพ ด้วย โมดูลของ LibGDX คือ Gdx.files
มีหลายเมธอดให้เรียกใช้งาน แต่ในที่นี้เลือก internal เพื่อโหลดรูปภาพที่เก็บไว้ใน assets
ของโปรเจ็ค Android นั่นเอง
ต่อไป ก็ทำการโหลดเอฟเฟคและเสียงเพลง background. Sound
และ Music
มีความเหมือนกันคือ โหลดเสียงเพลงและเล่นเสียงเพลง แต่มีความแตกต่างกัน คือ Sound
จะโหลดมาเก็บไว้ในเมโมรี่ ส่วน Music
จะถูกเล่นเลย ฉะนั้น หากเพลงมีความยาวไม่เกิน 10 วินาที ควรใช้ Sound
และหากมีความยาวเกิน 10 วินาที ก็ควรใช้ Music
โหลด Sound
หรือ Music
โดยใช้ Gdx.audio.newSound()
และ Gdx.audio.newMusic()
. เมธอดทั้งคู่ใช้ FileHandle
เหมือนกับ Texture
สุดท้ายที่ create()
สั่งให้เล่น Music
วนลูปเรื่อยๆ เมื่อเปิดแอพ ก็จะได้ยินเสียงฝนตกเรื่อยๆ จนกว่าจะปิดแอพ นั่นเอง
A Camera and a SpriteBatch
ต่อไปเราจะทำการสร้าง camera และ SpriteBatch
กันครับ SpriteBatch เคยพูดถึงไปแล้วในบทความก่อนๆ คือมันเป็นคลาสที่ใช้วาดรูป 2D จาก Texture ที่ได้ทำการประกาศไว้
ก่อนอื่น เราแน่ใจว่าเราจะ render หน้าจอขนาด 800x480 ได้อย่างไม่มีปัญหา แม้ว่าจะเปิดมือถือหรือว่าคอมหลายๆหน้าจอก็ตาม โดยใช้ SpriteBatch
และ camera
ผมได้สร้าง field ขึ้นมาใหม่ ชื่อว่า camera และ batch ดังนี้
OrthographicCamera camera;
SpriteBatch batch;
ในเมธอด create()
ได้สร้าง camera ดังนี้:
camera = new OrthographicCamera();
camera.setToOrtho(false, 800, 480);
การกำหนดแบบนี้ คือแน่ใจว่า camera จะโชว์ Game World ที่ขนาด 800x480 เสมอ ลองจินตนาการว่า camera นั้นเป็น Virtual Window ใน Game World ของเรา camera จะทำการ match หน้าจอต่างๆให้มี scale เป็น 800x480 ให้เราในหน่วย unit เอง ส่วนรายละเอียดเรื่อง OrthographicCamera เดี่ยวจะนำเสนอในบทความต่อๆไปครับ
ต่อไปสร้าง Spritebatch
ยังคงอยู่ในเมธอด create()
batch = new SpriteBatch();
ตอนนี้ก็สร้างทุกๆอย่างเกือบเสร็จแล้วสำหรับเทส Simple Game
เพิ่มรูปถังน้ำ
สำหรับรูปถังน้ำ จะมีรายละเอียดที่ยังไม่ได้พูดถึง ดังนี้
- ถังน้ำและฝน จะมีตำแหน่ง x, y ในหน่วย 800x480 ของ Game World
- ถังน้ำและฝน จะมีขนาดความกว้าง ความยาว
- ถังน้ำและฝน นั้นเป็น Graphic เราจึงโหลดรูปด้วย
Texture
ตามที่อธิบายไว้ด้านบน ถังน้ำหรือฝน มันจะมีตำแหน่งและขนาดของมันอยู่. LibGDX นั้นมีคลาส Rectangle
เราจะใช้คลาสนี้กันครับ เพื่อแทนตำแหน่งของถังน้ำ, ทำการประกาศคลาส Rectangle
ชื่อ bucket ตามนี้
Rectangle bucket;
ในเมธอด create()
สร้างออปเจ็ค Rectangle
ขึ้นมา และกำหนดค่าที่ต้องการ โดยให้มันมีขนาดความกว้างและความยาว ขนาด 64 หน่วย โดยมีตำแหน่งแกน x ที่กึ่งกลางหน้าจอ และตำแหน่ง y ที่ 20 จากขอบล่างของจอ
bucket = new Rectangle();
bucket.x = 800 / 2 - 64 / 2;
bucket.y = 20;
bucket.width = 64;
bucket.height = 64;
อย่างที่เคยบอกไปแล้ว ว่า LibGDX(OpenGL) นั้น ตำแหน่ง x, y จะนับตำแหน่ง 0, 0 เริ่มจากขอบล่างซ้ายของหน้าจอ
Rendering ถังน้ำ
ถึงเวลาที่จะ render ถังน้ำของเรา เพื่อแสดงบนหน้าจอกันแล้ว แต่ก่อนอื่น ต้องทำการ clear หน้าจอก่อน โดยใช้สี dark blue, เมธอด 'render()` จะเป็นดังนี้
@Override
public void render() {
Gdx.gl.glClearColor(0, 0, 0.2f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
... more to come here ...
}
จากโค๊ดด้านบน มันคือการเคลียร์หน้าจอ โดยเซตสีตามฟอแมตดังนี้ (สีแดง, น้ำเงิน, เขียว, alpha) โดยมีหน่วยตั้งแต่ [0, 1]
ต่อไปเราจะเซ้ตให้ camera นั้นอัพเดท โดย camera นั้นใช้การคำนวณทางคณิตศาสตร์ เราเรียกว่า matrix ซึ่งไว้สำหรับตั้งค่าพิกัดแกน x, y สำหรับ render , camera ควรจะมีการอัพเดท ทุกๆครั้งเมื่อมีการเปลี่ยน frame หรือก็คือ ทุกๆครั้งที่เมธอด render()
ถูกเรียก
camera.update();
ต่อไป เรา render ถังน้ำ ด้วยคำสั่งด้านล่างนี้
batch.setProjectionMatrix(camera.combined);
batch.begin();
batch.draw(bucketImage, bucket.x, bucket.y);
batch.end();
จากด้านบน อย่าสับสนระหว่าง bucketImage และ bucket นะครับ bucket นั้นเป็นเพียงแค่คลาส Rectangle
ที่เราประกาศไว้ เพื่อให้ถังน้ำนั้นมีตำแหน่ง x, y ส่วน bucketImage นั้นคือรูปภาพที่โหลดโดยใช้ Texture
เริ่มแรก เราทำการเซ้ทเพื่อให้ระบบรู้ว่า จะให้ SpriteBatch
ใช้พิกัดของ camera โดย camera.combined
คือสั่งให้ SpriteBatch
ทำการ render ทุกๆอย่าง
หากต้องการวาดหรือ render ต่างๆ ต้องใช้คำสั่งให้อยู่ระหว่าง SpriteBatch.begin()
และ SpriteBatch.end()
เมื่อ SpriteBatch.end()
ถูกเรียก มันถึงก็จะส่งค่าการวาดต่างๆ ไปให้ OpenGL ทำการวาด
หลังจาก render แล้ว เมื่อลองเปิดแอพดูจะได้ลักษณะนี้ ตอนนี้แอพเราก็สามารถแสดงถังน้ำได้แล้วนะครับ แต่ยังไม่สามารถบังคับมันได้ สำหรับวิธีการบังคับนั้น ผมจะมาพูดอธิบายต่อในอีกบทความนะครับ สำหรับบทความนี้ขอจบเพียงเท่านี้ก่อนครับ ส่วนใครอยากลองให้มันขยับได้ ก็ลองๆเล่นๆพวก Gdx.input.*
ดูครับ แล้วจะรู้ว่า LibGDX นั้นมันไม่ยากเลย ^^
อันนี้เป็นหน้าจอเมื่อรันบน Desktop (มีเสียงฝนตกด้วยนะ^^)
ส่วนอันนี้เป็นหน้าจอเมื่อรันบน Android
โค๊ดทั้งหมด
package com.badlogic.drop;
import java.util.Iterator;
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.TimeUtils;
public class Drop implements ApplicationListener {
Texture dropImage;
Texture bucketImage;
Sound dropSound;
Music rainMusic;
SpriteBatch batch;
OrthographicCamera camera;
Rectangle bucket;
@Override
public void create() {
// โหลดไฟล์รูปถังน้ำและเม็ดฝน ขนาด 64x64
dropImage = new Texture(Gdx.files.internal("droplet.png"));
bucketImage = new Texture(Gdx.files.internal("bucket.png"));
// โหลดเสียงเม็ดฝน และ effect
dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav"));
rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"));
// ตั้งค่าให้เปิดเสียง effect เรื่อยๆ (เ่สียง background)
rainMusic.setLooping(true);
rainMusic.play();
// สร้าง camera และ SpriteBatch
camera = new OrthographicCamera();
camera.setToOrtho(false, 800, 480);
batch = new SpriteBatch();
// สร้างออปเจ็ค bucket จากคลาส Rectangle
bucket = new Rectangle();
bucket.x = 800 / 2 - 64 / 2; // ตั้งค่ากึ่งกลาง
bucket.y = 20;
bucket.width = 64;
bucket.height = 64;
}
@Override
public void render() {
// เคลียร์หน้าจอ ด้วยสี Dark Blue
// โดย argument ของ glClearColor คือ red, green, blue, alpha
// ค่าอยู่ระหว่าง [0, 1] (float)
Gdx.gl.glClearColor(0, 0, 0.2f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
// camera อัพเดท matrix
camera.update();
// ให้ SpriteBatch ทำการเรนเดอร์พิกัดทั้งหมดของระบบ
batch.setProjectionMatrix(camera.combined);
// เริ่มวาด เรนเดอร์ ถังน้ำ
batch.begin();
batch.draw(bucketImage, bucket.x, bucket.y);
batch.end();
// เช็คว่า มีการคลิกเมาท์หรือแตะหน้าจอหรือไม่
if(Gdx.input.isTouched()) {
Vector3 touchPos = new Vector3();
touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
camera.unproject(touchPos);
bucket.x = touchPos.x - 64 / 2;
}
}
@Override
public void dispose() {
}
@Override
public void resize(int width, int height) {
}
@Override
public void pause() {
}
@Override
public void resume() {
}
}
- Authors
- Name
- Chai Phonbopit
- Website
- @Phonbopit