สอนเขียนเกม Android ด้วย Box2D ตอนที่ 1

สอนเขียนเกม Android ด้วย Box2D ตอนที่ 1 Cover Image

วันนี้ขอพูดถึงเรื่อง Box2D บน LibGDX(Android) ซักหน่อยละกันครับ โดยในบทความนี้จะพาไปรู้จักกับ Box2D ว่ามันคืออะไร? มีประโยชน์อะไร? ใช้เมื่อไหร่ และวิธีการใช้งานมีอะไรบ้าง

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

Box2D คืออะไร

Box2D นั้นเป็น Physics Engine (2D) ที่ได้รับความนิยมมากที่สุด Engine หนึ่งเลยก็ว่าได้ ตัว Engine มีขนาดเล็ก มีประสิทธิภาพ ที่สำคัญเป็น Open Source หรือก็คือ ฟรีนั่นเอง

แต่เดิมนั้น Box2D เขียนด้วยภาษา C++ เนื่องจากมันเป็น Physics Engine ที่ยอดเยี่ยมทำให้มีคนดัดแปลงโค๊ดแล้ว port ไปเป็นภาษาอื่นๆ เช่น

จริงๆแล้ว Physics Engine มันไม่ใช่ Game Engine จะให้มัน render กราฟฟิค เรียกไฟล์รูป โหลด Game Assets ก็คงจะทำไม่ได้ มันทำได้เพียงแค่ จำลองแวดล้อมให้เป็น Physics เท่านั้น ทำให้ Game Engine ต่างๆ เหล่านี้มี Box2D รวมอยู่ใน features ของ Engine ด้วย เช่น

จะเห็นว่า Game Engine 2D ส่วนใหญ่ เลือกใช้ Box2D กันทั้งนั้นเลย ทำให้มันเหมาะมากที่จะนำมาเขียนเกมส์ โอเค มาเริ่มสร้างโปรเจ็คเกม Android ร่วมกับ Box2D เลยดีกว่า

Create Project

โปรเจ็คนี้จะเป็นการเขียนเกม Android ง่ายๆ ด้วย Box2D โดยใช้ LibGDX ฉะนั้นเริ่มแรก ก็ทำการสร้างโปรเจ็คด้วย LibGDX กันก่อนครับ เปิดตัว libGDX Project Setup ครับ หรือดาวน์โหลดได้จากลิงค์นี้ gdx-setup.jar

หากเป็นมือใหม่ LibGDX แนะนำให้อ่านนี้ก่อนครับ วิธีการสร้างโปรเจ็ค LibGDX อ่านที่นี่ครับ

จากนั้นทำการ Setup ตามรูปด้านล่างเลยครับ

Setup LibGDX

จะเห็นว่าในส่วน Sub Project ผมเลือกแค่ Android เนื่องจากจะรันเฉพาะ Android เท่านั้น (หากใครอยากให้รัน Cross-Platform ก็เลือกเอาเองนะครับ)

ในส่วน Extensions นั้นเลือกเฉพาะ Box2D ในส่วนอื่นๆ ยังไม่ต้องรู้ว่ามันคืออะไร สามารถเลือก เพิ่ม หรือ ลบ ที่หลังได้ครับ

เมื่อทำการ Setup เรียบร้อยแล้ว กด Generate ครับ

ถ้าหาก Generate ผ่าน ก็จะได้แบบนี้

Generate Completed

หากใครขึ้น error แบบข้างล่างนี้

LibGDX Generate Error

ให้ทำการลบ gradle-wrapper ออก ตาม path ที่มันแจ้งไว้เลยครับ อยากของผม *nix มันก็จะแจ้ง path ~/.gradle/ แล้วทำการ Generate ใหม่นะครับ อีกวิธีนึง ให้มัน error อย่างงี้แหละ เดี่ยวค่อยไปเปลี่ยนเอาเวลา import ก้ได้เหมือนกัน

จากนั้นก็ Import โปรเจ็คครับ เปิด IDE ขึ้นมาเลย จะ Eclipse หรือ IntelijIDEA ก็แล้วแต่ สำหรับบทความนี้ใช้ IntelijIDEA นะครับ ผมข้ามขั้นตอนวิธีการ import โปรเจ็ค LibGDX ไปเลยละกัน สมมติว่าผู้อ่านทุกท่านทำเป็นอยู่แล้ว ไม่เป็นกลับไปอ่านลิงค์จากด้านบนครับ :D ส่วนการ error gradle-wrapper ให้ทำการเปิดโฟลเดอร์ gradle/wrapper/ มองหาไฟล์ gradle-wrapper.properties จากนั้นแก้โค๊ดบรรทัดสุดท้ายจาก

distributionUrl=http\://services.gradle.org/distributions/gradle-1.11-all.zip

เป็น

distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip

หรือไม่อย่างงั้นก็เซต GRADLE_HOME เป็น path ที่ได้ติดตั้ง gradle ไว้ครับ

gradle-wrapper มันเหมือนเป็นเครื่องมือ ที่ให้เรารัน gradle ได้โดยที่เครื่องที่รัน ไม่จำเป็นต้องลง gradle เพราะมันจะใช้ gradle-wrapper แทน วิธีแก้ปัญหา gradle-wrapper บางทีก็อาจเจอใน Android Studio เหมือนกันครับ ให้ใช้วิธีเดียวกัน

เมื่อ import เสร็จแล้ว เราจะมี 2 โฟลเดอร์ นั่นก็คือ AhoyBox2D-core และ AhoyBox2D-android นะครับ สนใจแค่ MyGame.java ใน Module core อย่างเดียวนะครับ ลบโค๊ดออกให้หมดเหลือไว้แค่นี้

ไฟล์ MyGame.java

package com.devahoy.sample.box2d;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;

public class MyGame extends ApplicationAdapter {

    @Override
    public void create () {

    }

    @Override
    public void render () {
        Gdx.gl.glClearColor(37 / 255.0f, 168 / 255.0f, 239 / 255.0f, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
    }
}

Box2D Concept

ก่อนเริ่มเขียนโค๊ด เรามาสนใจทฤษฎีและคอนเซปของ Box2D กันก่อนดีกว่า ก่อนเราจะสร้างเกมด้วย Box2D เราจำเป็นจะต้องรู้จัก Class พวกนี้ครับ

Body

Body เป็นออปเจ็คหนึ่ง ใน Box2D ที่เอาไว้จัดการเกี่ยวกับฟิสิกส์ เช่น น้ำหนัก, ความเร็ว, ตำแหน่งของวัตถุ เป็นต้น (จะเห็นว่ามีแต่ค่าที่เรามองไม่เห็น จำต้องไม่ได้)

Fixture

Fixture ก็คล้ายๆกับ Body แต่ต่างกันที่มันคือวัตถุที่สามารถจับต้องได้ มองเห็นได้ เช่น รูปทรงของวัตถุ สี่เหลี่ยม, สามเหลี่ยม หรือวงกลม, แรง (ฺBody กับ Fixture มักจะมาคู่กันครับ)

World

World ผมเรียกทับศัพท์ไปเลย ตามชื่อเลยครับ มันก็คือโลกนั้นเอง โลกของ Box2D ตัวเกม เราจำเป็นจะต้องมี World เพื่อเก็บ Body, Fixture หรือค่าต่างๆที่เกี่ยวกับฟิสิกส์เอาไว้ ถือเป็นหัวใจของ Box2D เลยก็ว่าได้

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

ขั้นตอนการสร้าง Object ใน Box2D

  1. สร้าง World โดยการกำหนดแรงดึงดูด ทั้งแกน x และ y โดยใช้คลาส Vector2
  2. สร้าง Body 2.1 สร้าง BodyDef เพื่อกำหนดตำแหน่งให้กับ Body ก่อน (Definition Body) 2.2 จากนั้นก็สร้าง Body จาก BodyDef ที่เรากำหนดค่าไว้
  3. สร้าง Fixture 3.1 สร้าง FixtureDef ขึ้นมาก่อน สำหรับกำหนดคุณสมบัติให้กับวัตถุ 3.2 สร้าง Shape (รูปทรงต่างๆของวัตถุ) 3.3 จับ Shape ยัดใส่ FixtureDef เพื่อทำให้ FixtureDef มีรูปทรงตาม Shape 3.4 สร้าง Fixture จาก FixtureDef ที่กำหนดไว้ โดยใช้ Body จากข้อ2

คู่มื่อศึกษา Box2D เพิ่มเติม Box2D Manual

Create a Game

ทีนี้เมื่อเข้าใจคอนเซป ขั้นตอนและหลักการแล้ว ก็เริ่มลงมือโค๊ดเลย โดยจะไล่ไปทีละสเตปๆนะครับ เริ่มจากเปิดไฟล์ MyGame.java ที่อยู่ใน AhoyBox2D-core ขึ้นมา

เริ่มแรกทำการประกาศ OrthographicCamera, World และ Box2DDebugRenderer ดังนี้

public class MyGame extends ApplicationAdapter {

    private OrthographicCamera camera;
    private World world;
    private Box2DDebugRenderer renderer;

    @Override
    public void create () {

        camera = new OrthographicCamera();
        camera.setToOrtho(false, 800, 480);
        renderer = new Box2DDebugRenderer();
        ...

    }
}

1. สร้าง World

ต่อมา ทำการสร้าง World ขึ้นมาก่อน เนื่องจาก World นั้นคืออ็อปเจ็ค ที่เป็นเหมือนศูนย์รวมคอยจัดการ memory, objects รวมถึงการ simulation ต่างๆ การสร้าง World ทำได้ด้วยคำสั่งนี้

World world = new World(new Vector2(0, -9.81f), true);

โดยเราทำการสร้างออปเจ็ค world โดยมีค่าแรงโน้มถ่วง(แรง g) อยู่ที่ 9.81 m/s^2 แต่ว่าทำไมเป็นค่าลบ (-) เนื่องจาก LibGDX นับแกน x,y โดยเริ่มจากมุมซ้ายล่างคือจุด 0, 0

2. สร้างพื้น

ต่อมาเราจะทำการสร้างพื้น แต่ว่าการสร้าง Body เราต้องทำตามขั้นตอนครับ ฉะนั้นก็ต้องเริ่มจากสร้าง พื้น Body จาก Body ที่เราได้นิยามไว้ จนได้โค๊ดแบบนี้

// 2.1 สร้าง BodyDef
BodyDef bodyDef = new BodyDef();
bodyDef.type = BodyDef.BodyType.StaticBody; // ชนิดของ Body เป็น Static คือเคลื่อนไหวไม่ได้
bodyDef.position.set(300, 50); // กำหนดตำแหน่งให้กับ Body

// 2.2 สร้าง Body
Body bodyGround = world.createBody(bodyDef);

// 3.1 สร้าง FixtureDef
FixtureDef fixtureDef = new FixtureDef();

// 3.2 สร้าง Shape
PolygonShape shape = new PolygonShape();
shape.setAsBox(200, 10); // กำหนดขนาด 10x200


// 3.3 จับ Shape ยัดใส่ FixtureDef
fixtureDef.shape = shape;

// 3.4 สร้าง Fixture ด้วย Body
bodyGround.createFixture(fixtureDef);

จากโค๊ดด้านบน ผมอธิบายไปในคอมเม้นแล้ว หากไม่เข้าใจ ให้กลับขึ้้นไปอ่านคอนเซป และขั้นตอนการสร้าง Object ใน Box2D ใหม่ครับ จะเห็นภาพมากขึ้น

ต่อมาที่เมธอด render() ทำการเพิ่มโค๊ดนี้ลงไป

@Override
public void render () {
    Gdx.gl.glClearColor(37 / 255.0f, 168 / 255.0f, 239 / 255.0f, 1);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

    renderer.render(world, camera.combined);
    camera.update();

    float dt = Gdx.graphics.getDeltaTime();
    world.step(dt, 8, 3);
}

จากโค๊ดด้านบน ทำการเคลียร์หน้าจอ จากนั้นใช้ Box2DDebugRenderer สำหรับ render โดยรับพารามิเตอร์ 2 ตัวคือ World และ Matrix ครับ ส่วนตรง world.step(dt, 8, 3); ส่วนนี้คือ step สำหรับเช็คพวก collision (การชนกันของวัตถุ) ส่วนค่า 8, 3 คือ velocityIterations(ความเร็ว) และ positionIterations(ตำแหน่ง) ส่วนนี้ อาจจะเปลี่ยนให้อยู่ระหว่าง 1-10 ดูครับ

ทดสอบ รันโปรแกรม จะได้ไอ้กล่องสี่เหลี่ยมยาวๆ ครับ

Show ground

ต่อมา ผมจะสร้างลูกบอล 1 ลูก ก็จะได้โค๊ดแบบนี้

// สร้างลูกบอล
bodyDef.type = BodyDef.BodyType.DynamicBody;
bodyDef.position.set(400, 300);
Body bodyBall = world.createBody(bodyDef);

CircleShape circleShape = new CircleShape();
circleShape.setRadius(50);

fixtureDef.shape = circleShape;
fixtureDef.restitution = 0.5f;
bodyBall.createFixture(fixtureDef);

ทำตามสเตปเลยครับ สร้าง BodyDef -> Body -> Shape -> FixtureDef -> Fixture โดย Shape คราวนี้เป็น CircleShape ซึ่งเป็นคลาสที่ไว้แสดงวัตถุรูปวงกลมครับ มีขนาดรัศมี 50 หน่วย

ส่วน BodyDef ของลูกบอล ก็จะเป็น DynamicBody คือเป็นวัตถุที่สามารถเคลื่อนที่ได้

สุดท้าย จะเห็น restitution คือค่าการเด้งของวัตถุ ค่า 0.5f คือ เมื่อวัตถุตกกระทบ จะเด้งขึ้นมา ครึ่งหนึ่งของความสูงเดิมเรื่อยๆ ครับ

Note! เรื่องที่คุณยังไม่รู้เกี่ยวกับ หน่วย unit ใน Box2D จะมีค่าเท่ากับ 1 เมตรนะครับ ฉะนั้น ที่สร้างลูกบอลไป มีขนาดรัศมี 50 เมตร โอ้ว ใหญ่ทับสนามฟุตบอลได้เลย :D แต่ว่าหน่วยใน LibGDX เป็น pixel ฉะนั้น เวลาเรากำหนดหน่วยทุกๆอย่าง ใน Box2D เราก็ต้อง scale ให้เป็นหน่วยเมตร ซะก่อนครับ

สร้างตัวแปรขึ้นมาใหม่ ชื่อ PPM โดยกำหนดไว้ที่ 100 คือ หน่วยใน Box2D จะถูกหารด้วย 100 ฉะนั้นลูกบอล ลูกใหม่ จะมีขนาด 50 เซนติเมตร อ่า กำลังดีละครับ ไม่ใหญ่มาก

private static final float PPM = 100;

จากนั้นก็แปลงหน่วย Unit ใหม่ เป็นดังนี้

@Override
public void create () {

    camera = new OrthographicCamera();
    camera.setToOrtho(false, 800 / PPM, 480 / PPM);
    renderer = new Box2DDebugRenderer();

    // 1. สร้าง World
    world = new World(new Vector2(0, -9.81f), true);

    // 2.1 สร้าง BodyDef
    BodyDef bodyDef = new BodyDef();
    bodyDef.type = BodyDef.BodyType.StaticBody; // ชนิดของ Body เป็น Static คือเคลื่อนไหวไม่ได้
    bodyDef.position.set(300 / PPM, 50 / PPM); // กำหนดตำแหน่งให้กับ Body x = 300 y = 50

    // 2.2 สร้าง Body
    Body bodyGround = world.createBody(bodyDef);

    // 3.1 สร้าง FixtureDef
    FixtureDef fixtureDef = new FixtureDef();

    // 3.2 สร้าง Shape
    PolygonShape shape = new PolygonShape();
    shape.setAsBox(200 / PPM, 10 / PPM); // กำหนดขนาด 10x200

    // 3.3 จับ Shape ยัดใส่ FixtureDef
    fixtureDef.shape = shape;

    // 3.4 สร้าง Fixture ด้วย Body
    bodyGround.createFixture(fixtureDef);

    //////////////////////////////////////////////////////

    // สร้างลูกบอล
    bodyDef.type = BodyDef.BodyType.DynamicBody;
    bodyDef.position.set(400 / PPM, 600 / PPM);
    Body bodyBall = world.createBody(bodyDef);

    CircleShape circleShape = new CircleShape();
    circleShape.setRadius(50 / PPM);

    fixtureDef.shape = circleShape;
    fixtureDef.restitution = 0.5f;
    bodyBall.createFixture(fixtureDef);
}

ลองสั่งรัน แล้วสังเกต การตกกระทบ การกระเด้งของวัตถุ จากนั้นลองเปลี่ยนค่า restitution ดูครับ

Result Box2D gif

สำหรับ Box2D บทความแรก ขอจบเพียงเท่านี้ก่อนครับ ฝากไว้เป็นการบ้านสำหรับผู้ที่เข้ามาอ่านครับ ลองสร้าง Body ชนิดกล่องสี่เหลี่ยม ให้เป็น DynamicBody แล้วให้มันร่วงลงมา เหมือนอย่างลูกบอลครับ ลองทำดูนะครับ แล้วเดี่ยวบทความถัดไป จะมาเฉลย รวมถึงต่อเรื่องการจับการชนกันของวัตถุ (collision), Filter(MaskBits, CategoryBits) และ ContactListener ในบทความต่อๆไปครับ

โค๊ดทั้งหมด

MyGame.java

package com.devahoy.sample.box2d;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.*;

public class MyGame extends ApplicationAdapter {

    private OrthographicCamera camera;
    private World world;
    private Box2DDebugRenderer renderer;

    // 100 pixel = 1 meter 
    private static final float PPM = 100;

    @Override
    public void create () {

        camera = new OrthographicCamera();
        camera.setToOrtho(false, 800 / PPM, 480 / PPM);
        renderer = new Box2DDebugRenderer();

        // 1. สร้าง World
        world = new World(new Vector2(0, -9.81f), true);

        // 2.1 สร้าง BodyDef
        BodyDef bodyDef = new BodyDef();
        bodyDef.type = BodyDef.BodyType.StaticBody; // ชนิดของ Body เป็น Static คือเคลื่อนไหวไม่ได้
        bodyDef.position.set(300 / PPM, 50 / PPM); // กำหนดตำแหน่งให้กับ Body x = 300 y = 50

        // 2.2 สร้าง Body
        Body bodyGround = world.createBody(bodyDef);

        // 3.1 สร้าง FixtureDef
        FixtureDef fixtureDef = new FixtureDef();

        // 3.2 สร้าง Shape
        PolygonShape shape = new PolygonShape();
        shape.setAsBox(200 / PPM, 10 / PPM); // กำหนดขนาด 10x200

        // 3.3 จับ Shape ยัดใส่ FixtureDef
        fixtureDef.shape = shape;

        // 3.4 สร้าง Fixture ด้วย Body
        bodyGround.createFixture(fixtureDef);

        //////////////////////////////////////////////////////

        // สร้างลูกบอล
        bodyDef.type = BodyDef.BodyType.DynamicBody;
        bodyDef.position.set(400 / PPM, 600 / PPM);
        Body bodyBall = world.createBody(bodyDef);

        CircleShape circleShape = new CircleShape();
        circleShape.setRadius(50 / PPM);

        fixtureDef.shape = circleShape;
        fixtureDef.restitution = 0.5f;
        bodyBall.createFixture(fixtureDef);

        ///////////////////////////////////////////////////////
        // สร้างกล่องสี่เหลี่ยม????
        // ????  


    }

    @Override
    public void render () {
        Gdx.gl.glClearColor(37 / 255.0f, 168 / 255.0f, 239 / 255.0f, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        renderer.render(world, camera.combined);
        camera.update();

        float dt = Gdx.graphics.getDeltaTime();
        world.step(dt, 8, 3);
    }
}

Chai Chai Phonbopit : Web Developer @Nimbl3 • ผู้ชายธรรมดาๆ ที่ชื่นชอบ Node.js, JavaScript และ Open Source มีงานอดิเรกเป็น Acoustic Guitar และ Football

บทความล่าสุด