ผลต่างระหว่างรุ่นของ "Oop lab/flappy dot"

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
แถว 313: แถว 313:
  
 
== สถานะเกม ==
 
== สถานะเกม ==
 +
เกมของเราเมื่อเปิดมาก็เริ่มทันที่ เราอาจจะเริ่มกดปุ่มไม่ทันก็ได้  ในส่วนนี้เราจะเพิ่ม field
 +
 +
  private boolean isStarted;
 +
 +
ในคลาส FlappyDotGame  และเราจะให้ isStarted มีค่าเป็น false ในตอนเริ่มต้น  เราจะเพิ่มบรรทัด
 +
 +
    isStarted = false;
 +
 +
ในเมท็อด init
 +
 +
'''แบบฝึกหัด:''' ให้แก้เมท็อด update และ keyPressed ให้จัดการกับ isStarted โดยที่เราจะให้ isStarted เป็น true เมื่อมีการเริ่มกดปุ่ม  นอกจากนี้ เมท็อด update จะไม่เรียกให้ dot นั้น update จนกว่า isStarted จะเป็นจริง
  
 
== ท่อและการเคลื่อนที่ของท่อ ==
 
== ท่อและการเคลื่อนที่ของท่อ ==

รุ่นแก้ไขเมื่อ 01:17, 1 กันยายน 2557

หน้านี้เป็นส่วนหนึ่งของ oop lab
อ่านเวอร์ชันที่เป็น JavaScript บน Cocos2d-html5

การแยกงานเป็นงานย่อย

Ooplab-flappydot.png

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

แสดง Dot

เริ่มโปรเจ็คและหน้าจอว่าง

สร้าง project ชื่อ flappydot จากนั้นสร้าง Git repository สำหรับ project นี้ด้วย

สร้าง class FlappyDotGame ซึ่งจะเป็นคลาสหลักของเกมเรา จากนั้นให้เพิ่มเมท็อด render, init, update และสร้างเมท็อด main ที่เริ่มเกม (ดูจากเอกสารก่อน)

public class FlappyDotGame extends BasicGame {

  public FlappyDotGame(String title) {
    super(title);
  }

  // ... เพิ่มเอง
}

เมื่อทดลองรันและเห็นหน้าจอว่าง ให้ commit งานลงใน Git ต่อไปเพื่อเป็นการเตือนให้คุณ commit งาน เราจะใส่ย่อหน้าดังด้านล่างไว้เตือน

Gitmark.png Commit งานที่คุณเขียน

แสดงสี background

เกมนี้เราจะไม่ใช้ background เป็นสีดำแล้ว เราจะแก้สี background ได้ โดยกำหนด background ลงใน graphics ของเกม เพิ่มคำสั่งเหล่านี้ในเมท็อด init

  @Override
  public void init(GameContainer container) throws SlickException {
    Color background = new Color(128, 128, 128);
    container.getGraphics().setBackground(background);        
  }

ทดลองรันเพื่อดูว่า background แสดงสีเทาหรือไม่? ปรับค่าสีตามใจชอบ

แสดง Dot

สร้างรูป dot.png สำหรับแสดงตัวผู้เล่น ให้สร้างให้มีขนาด 40 x 40 จุด จากนั้นให้ copy ไฟล์ไปไว้ที่ res/dot.png

เราจะสร้างคลาส Dot สำหรับจัดการแสดงผลตัวผู้เล่น และคำนวณการเคลื่อนที่ รวมถึงจัดการอื่น ๆ คลาสดังกล่าวจะถูกเรียกใช้โดย FlappyDotGame ดังนี้:

  @Override
  public void init(GameContainer container) throws SlickException {
    dot = new Dot(320, 240);
  }

  @Override
  public void render(GameContainer container, Graphics g) throws SlickException {
    dot.render();
  }

อย่าลืมเพิ่มบรรทัดที่ประกาส field ของ Dot ด้วย

  private Dot dot;

ด้านล่างแสดงคลาส Dot

public class Dot {

  private float x;
  private float y;
  private Image image;

  public Dot(float x, float y) throws SlickException {
    this.x = x;
    this.y = y;
    image = new Image("res/dot.png");
  }
  
  public void render() {
    image.draw(x - 20, 480 - (y + 20));
  }
}

ข้อสังเกต:

  • พิกัดต่าง ๆ เป็น float (เพราะว่าเราต้องการปรับความเร็ว/ความเร่งให้ละเอียดขึ้น และ Slick2D รับพิกัดเป็น float อยู่แล้ว)
  • ในเมท็อด render เรามีการแปลงตำแหน่งที่จะวาดรูป โดยเราเลื่อนพิกัดแกน x ไป -20 หน่วย, ส่วนพิกัดในแกน y นั้น เรากลับข้างมัน เพราะว่าในการคิด physics เรามักนิยมคิดค่าในแกน y เป็นความสูง แต่ทิศทางของแกน y ในการวาด graphics นั้น นับจุด (0,0) เป็นจุดมุมบนซ้าย
Gitmark.png ทดลองสั่งให้โปรแกรมทำงาน ถ้าโปรแกรมแสดงจุดที่ตรงกลางหน้าจออย่างถูกต้อง ให้ commit งานของคุณ

จัดการกับค่าคงที่ (magic number)

สังเกตว่าในโปรแกรมมีการใช้ค่าคงที่ต่าง ๆ เป็นจำนวนมาก เช่น ค่าคงที่ที่เกี่ยวกับกับขนาดหน้าจอของเกม (640, 480, 320, และ 240) การใช้ค่าดังกล่าวทำให้ถ้ามีการแก้ไข เราจะต้องไปแก้หลายที่ และทำให้เกิดความยุ่งยากเวลาเราอ่านโค้ด เพราะว่าเราอาจจะไม่แน่ใจว่าค่าดังกล่าวหมายถึงอะไร

เราจะประกาศค่าคงที่ดังกล่าวในคลาส FlappyDotGame และใช้ค่าคงที่นั้น (แบบที่มีชื่อแล้ว) ในโปรแกรมแทน เราสามารถประกาศค่าคงที่ด้วย keyword final โดยเราจะประกาศแบบ public static ให้เป็นค่าคงที่ของคลาส และเปิดให้คลาสอื่น ๆ ใช้ได้ด้วย

เพิ่มบรรทัดเหล่านี้ในตอนต้นคลาส FlappyDotGame

public class FlappyDotGame extends BasicGame {

  public static final int GAME_WIDTH = 640;
  public static final int GAME_HEIGHT = 480;

  // ...
}

จากนั้นให้แก้โค้ดบรรทัดต่าง ๆ ที่มีการใช้ค่าเหล่านี้ให้เรียกผ่านทางค่าคงที่แบบมีชื่อที่เราสร้างนี้ เช่น

  // ...
  @Override
  public void init(GameContainer container) throws SlickException {
    dot = new Dot(GAME_WIDTH/2, GAME_HEIGHT/2);
  }

  public static void main(String[] args) {
      // ...
      container.setDisplayMode(GAME_WIDTH, GAME_HEIGHT, false);
  }
  // ...

นอกจากนี้ โค้ดของคลาส Dot ยังมีการใช้ค่าคงที่ 20 ซึ่งครึ่งหนึ่งของความสูง และความกว้างของรูป เราจะแก้ไขโค้ดโดยเพิ่มค่าคงที่

  public static final int WIDTH = 40;
  public static final int HEIGHT = 40;

และแก้เมท็อด render เป็น

  public void render() {
    image.draw(x - WIDTH/2, 
        FlappyDotGame.GAME_HEIGHT - y - (HEIGHT/2));
  }
Gitmark.png ทดลองว่าโปรแกรมทำงานได้ไม่ต่างจากเดิม แล้ว commit

การเคลื่อนที่และการควบคุม Dot

การเคลื่อนที่ของ Dot

เราจะให้ Dot เปลี่ยนตำแหน่งเองได้ (เพื่อที่เราจะได้ไม่ต้องไปดูแลมัน) การเปลี่ยนตำแหน่งนี้จะทำให้เมท็อด update ของ FlappyDotGame โดยเราจะเรียกดังนี้:

  @Override
  public void update(GameContainer container, int delta) throws SlickException {
    dot.update();
  }

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

โค้ดเริ่มต้นของเราเป็นดังนี้

public class Dot {
  // ...
  private float vy;

  public Dot(float x, float y, float vy) throws SlickException {
    // ...
    this.vy = vy;
  }

  // ...
  public void update() {
    y += vy;
    vy -= 0.1;
  }
}

สังเกตว่าเราจะรับค่าความเร็วในแกน y เริ่มต้นมาด้วย ดังนั้นโค้ดที่สร้าง dot ในเมท็อด FlappyDotGame.init จะต้องส่งค่าดังกล่าวให้กับ constructor

    // ...
    dot = new Dot(GAME_WIDTH/2, GAME_HEIGHT/2, 10);

ให้ทดลองดูว่าจุดเคลื่อนได้ที่ลักษณคล้ายกับการหล่นหรือไม่ ให้ปรับค่าความเร็วเริ่มต้นและความเร่งตามพอเหมาะ (จากการทดลอง 10 กับ -0.5 จะกำลังดี)

ถ้าเครื่องคุณเร็วไป (เช่น ทำงานแล้วได้ FPS = 1000) ให้เพิ่มบรรทัดด้านล่างลงใน main ก่อน container.start(); เพื่อเพิ่มระยะเวลาระหว่างการเรียก update

      container.setMinimumLogicUpdateInterval(1000 / 60);

ค่าดังกล่าวคือระยะเวลาเป็นมิลลิวินาทีระหว่างการสั่ง update สังเกตว่าเราใส่ 1000 / 60 เพื่อระบุว่าต้องการประมาณ 60 เฟรมต่อวินาที

เก็บกวาดค่าคงที่

เราใช้ค่าคงที่อีกแล้ว... เก็บกวาดค่าต่าง ๆ โดยประกาศค่าคงที่ด้านล่างที่ตอนต้น FlappyDotGame

   //...
   public static final float DOT_INITIAL_VY = 10;
   public static final float G = (float) -0.5;

หมายเหตุ: ที่เราต้องคาส (float) ที่ค่าคงที่ G เพราะว่า Java ไม่ยอมแปลง -0.5 ที่เป็น double ให้เป็น float ให้เรา

จากนั้นไปแก้จุดที่เรียกใช้ค่าดังกล่าว เช่น

  // ...
  public void update() {
    y += vy;
    vy -= FlappyDotGame.G;
  }

และ

  public void init(GameContainer container) throws SlickException {
    dot = new Dot(GAME_WIDTH/2, GAME_HEIGHT/2, DOT_INITIAL_VY);
  }
Gitmark.png ถ้าทดลองแล้วโปรแกรมทำงานได้โอเค อย่าลืม commit

การอ่าน input แบบ event-based

อ่านเพิ่มเกี่ยวกับวิธีการอ่าน input แบบต่าง ๆ ใน Slick2D ได้ที่: Event based input

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

สำหรับในเกม FlappyDot นี้ เราจะอ่าน input แบบ event-based นั่นคือ เราจะเขียนเมท็อดพิเศษขึ้นมา ซึ่งระบบจะเรียกเมท็อดนี้ เมื่อมีเหตุการณ์ที่เราสนใจขึ้นเองโดยอัตโนมัติ สิ่งที่เราต้องทำคือจัดการกับเหตุการณ์ตามที่เราต้องการ สำหรับเหตุการณ์การกดปุ่มนั้น ถ้าเราเขียนเกมโดยสร้างมาจาก BasicGame (โดยการ extends เช่นที่เราทำ) เราจะสามารถรับการกดปุ่มโดยเขียนเมท็อด keyPressed ดังด้านล่าง

โค้ดดังกล่าวตรวจสอบว่ามีการกดปุ่ม space bar หรือไม่ (อย่าลืม import org.newdawn.slick.Input)

  @Override
  public void keyPressed(int key, char c) {
    if (key == Input.KEY_SPACE) {
      // do something
    }
  }

สิ่งที่เราจะทำคือ เราจะไปกำหนดความเร็วเริ่มต้นในแกน y ให้กับ Dot ใหม่ เราอาจจะอยากสั่งเป็น

   dot.vy = DOT_INITIAL_VY;

แต่เราไม่สามารถทำได้ เพราะว่า vy เป็น field แบบ private เราสามารถแก้ปัญหานี้ตรง ๆ ได้สองแบบ (จริง ๆ มี 3 แบบ แบบที่ 3 อยู่ส่วนถัดไป) คือ

  • เปลี่ยน vy เป็น public
  • เขียนเมท็อดเพื่อกำหนดค่า vy

เราจะใช้วิธีที่ 2 เมท็อดดังกล่าวนิยมเรียกว่า setter (เพราะว่าเอาไว้กำหนดค่า) การเขียน setter นี้ ทำให้เรามีอิสระที่จะเปลี่ยนการ implement ภายในของคลาสเกี่ยวกับ field vy ได้ในภายหลัง (เพราะว่าคลาสต่าง ๆ เมื่อจะอ้างถึงและแก้ไขจะต้องทำผ่านเมท็อดนี้

เพิ่มเมท็อด setVy ในคลาส Dot

  public void setVy(float vy) {
    this.vy = vy;
  }

จากนั้นในเมท็อด keyPressed เราสามารถสั่งดังต่อไปนี้ได้

    if (key == Input.KEY_SPACE) {
      dot.setVy(DOT_INITIAL_VY);
    }
Gitmark.png ทดลองว่า dot กระโดดและร่วงได้ ถ้าโปรแกรมทำงานได้ตามต้องการ อย่าลืม commit

เมท็อด jump

โค้ดบรรรทัดด้านล่าง

     dot.setVy(DOT_INITIAL_VY);

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

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

    dot.jump();

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

สังเกตว่า dot จะ jump ได้ ตัว object จะต้องทราบความเร็วต้นในการกระโดด ดังนั้นเราจะต้องส่งค่า DOT_INITITAL_VY ให้วัตถุจำไว้ด้วย (ไม่ใช่แค่กำหนดเป็นค่าเริ่มต้นของ vy อย่างเดียว) นอกจากนี้ การตั้งชื่อว่า DOT_INITIAL_VY นั้นไม่ค่อยจะสื่อความหมายของความเร็วดังกล่าวที่เราจะใช้ ดังนั้นเราจะ rename DOT_INITIAL_VY ให้เป็น DOT_JUMP_VY แน่นอนว่าการแก้ชื่อดังกล่าว ไม่ได้แก้ที่เดียว แต่ IDE เช่น Eclipse หรือ NetBeans มีความสามารถที่เรียกว่า refactor ที่ทำให้เราสามารถแก้ชื่อค่าคงที่ในที่เดียวและ IDE จะไปแก้ไขชื่อดังกล่าวให้ทุกที่ในโปรแกรมของเราได้

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

Ooplab-rename.png

หมายเหตุ: ความสามารถที่ refactor ได้โดยอัตโนมัตินี่เป็นสิ่งที่ทำให้ภาษาพวก Java เข้มแข็งมาก

จากนั้นเราจะแก้โค้ด Dot โดยจะแก้ constructor และเพิ่มเมท็อด jump (และจะลบเมท็อด setVy ทิ้งเลยด้วยก็ได้)

  public Dot(float x, float y, float vjump) throws SlickException {
    // ...
    this.vy = vjump;
    this.vjump = vjump;
    // ...
  }

  public void jump() { 
    vy = vjump;
  }

สังเกตว่าเราไม่ต้องแก้ส่วนที่สร้าง dot ใน FlappyDotGame.init แต่อย่างใด ซึ่งโค้ดดังกล่าวตอนนี้กลายเป็น:

    dot = new Dot(GAME_WIDTH/2, GAME_HEIGHT/2, DOT_JUMP_VY);

ไปแล้ว ตั้งแต่ตอนที่เราสั่ง refactor

ขั้นสุดท้าย อย่าลืมไปแก้บรรทัด dot.setVy ให้เป็น dot.jump()

Gitmark.png เมื่อทดลองโปรแกรมว่าทำงานเรียบร้อยดี อย่าลืม commit

สถานะเกม

เกมของเราเมื่อเปิดมาก็เริ่มทันที่ เราอาจจะเริ่มกดปุ่มไม่ทันก็ได้ ในส่วนนี้เราจะเพิ่ม field

  private boolean isStarted;

ในคลาส FlappyDotGame และเราจะให้ isStarted มีค่าเป็น false ในตอนเริ่มต้น เราจะเพิ่มบรรทัด

    isStarted = false;

ในเมท็อด init

แบบฝึกหัด: ให้แก้เมท็อด update และ keyPressed ให้จัดการกับ isStarted โดยที่เราจะให้ isStarted เป็น true เมื่อมีการเริ่มกดปุ่ม นอกจากนี้ เมท็อด update จะไม่เรียกให้ dot นั้น update จนกว่า isStarted จะเป็นจริง

ท่อและการเคลื่อนที่ของท่อ

ในส่วนนี้เราจะเพิ่มคลาส PillarPair ที่จัดการเกี่ยวกับท่อคู่ในเกม

แสดงท่อ

ท่อเลื่อน

สุ่มความสูงท่อ

ท่อหลายท่อ

การตรวจสอบการชน

เกม