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

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
แถว 301: แถว 301:
  
 
ในส่วนนี้เราจะทดลองใช้ [http://en.wikipedia.org/wiki/Factory_method_pattern Factory method pattern] ในการแยกส่วนการสร้าง DirectionalBullet ออกมาจาก constructor ของ StarBullet
 
ในส่วนนี้เราจะทดลองใช้ [http://en.wikipedia.org/wiki/Factory_method_pattern Factory method pattern] ในการแยกส่วนการสร้าง DirectionalBullet ออกมาจาก constructor ของ StarBullet
 +
 +
เราจะสร้างคลาสที่วัตถุมีเมท็อดเดียวคือ build และคืน object ที่เป็น DirectionalBullet (หรือ subclass ของ DirectionalBullet)  โดยเราจะเรียกว่าคลาส DirectionalBulletFactory
 +
 +
<syntaxhighlight lang="java">
 +
public class DirectionalBulletFactory {
 +
  public DirectionalBullet build(float x, float y, float dir, float velocity) {
 +
    return new DirectionalBullet(x, y, dir, velocity);
 +
  }
 +
}
 +
</syntaxhighlight>
 +
 +
เราจะแก้โค้ด constructor ของคลาส StarBullet ให้รับ object ที่เป็น DirectionalBulletFactory เพื่อนำมาสร้าง bullets ดังด้านล่าง
 +
 +
<syntaxhighlight lang="java">
 +
  public StarBullet(float x, float y, float velocity,
 +
      DirectionalBulletFactory factory) {
 +
    super(x, y);
 +
   
 +
    bullets = new ArrayList<DirectionalBullet>();
 +
    for (int i=0; i < 36; i++) {
 +
      bullets.add(factory.build(x, y, i*10, velocity));
 +
    }
 +
  }
 +
</syntaxhighlight>
 +
 +
และโค้ดในเมท็อด init ของ BulletGame จะเปลี่ยนไปดังนี้
 +
 +
<syntaxhighlight lang="java">
 +
  @Override
 +
  public void init(GameContainer container) throws SlickException {
 +
    entities.add(new StarBullet(320,240,1, new DirectionalBulletFactory()));
 +
  }
 +
</syntaxhighlight>
 +
 +
สังเกตว่าเรา new DirectionalBulletFactory ส่งไปให้เพื่อให้ constructor สามารถเรียกเพื่อสร้าง bullet ได้

รุ่นแก้ไขเมื่อ 08:02, 8 กันยายน 2557

หน้านี้เป็นส่วนหนึ่งของ oop lab

ในปฏิบัติการนี้ เราจะทดลองใช้ interface Entity และทดลองสร้าง subclass นอกจากนี้เรายังจะได้ใช้ collection LinkedList เพื่อเก็บข้อมูล entity ด้วย

หมายเหตุ: การใช้ inheritance ในการเพิ่มประสิทธิภาพของคลาสนั้น ในบางมุมพิจารณาว่าเป็นเทคนิคที่อาจจะไม่ได้ดีที่สุดในการออกแบบคลาส อย่างไรก็ตาม ในส่วนนี้เพื่อฝึกเขียน เราจะใช้วิธีดังกล่าวไปก่อน

เริ่มต้น

สร้างโปรเจ็ค bulletgame จากนั้นสร้างคลาส BulletGame ที่ extends มาจาก BasicGame ตามที่เราเคยสร้างตามปกติ เพิ่มเมท็อดที่ต้อง implement ทั้งหมด (init, update, render) จากนั้นทดลองรันให้โปรแกรมแสดงหน้าจอว่าง ๆ

Gitmark.png เก็บเวอร์ชั่นหน้าจอว่างโดยการ commit

interface Entity

ในลักษณะเดียวกับที่เราทำในคลิป YouTube เรื่อง interface เราจะสร้าง interface Entity เพื่อใช้ระบุเมท็อดพื้นฐานทั้งหมดที่ "ของ" ที่จะอยู่บนหน้าจอเกมของเราจะต้องเขียน

public interface Entity {
  void render(Graphics g);
  void update(int delta);
}

สังเกตว่าเมท็อด render จะส่ง Graphics g มาด้วย และเมท็อด update ก็จะส่ง delta มาให้ด้วยเช่นกัน

เราจะประกาศ interface ทิ้งไว้ก่อน ส่วนโค้ดที่เรียกใช้งานนั้น เราจะเขียนหลักเขียนคลาส Bullet แล้ว

คลาส Bullet

คลาส Bullet นี้จะเป็นคลาสพื้นฐานของกระสุนทั้งหมด โดยทำหน้าที่หลักเพียงแค่วาดรูปกระสุนด้วยวงกลม และสามารถขยับกระสุนไปในทิศทางแกน y เท่านั้น

สังเกตว่าในคลาสนี้ เราให้ field x และ y เป็น private แต่เราสร้าง getters/setters ดังนี้

  • getX, getY เป็น public
  • setXY เป็น protected เพราะว่าเราต้องการให้ subclass คำนวณการเคลื่อนที่ของ Bullet ได้ แต่ต้องเรียกผ่านทางเมท็อดนี้
public class Bullet implements Entity {

  private static final float BULLET_SIZE = 5;
  private float x;
  private float y;

  public Bullet(float x, float y) {
    this.setXY(x,y);    
  }
  
  @Override
  public void render(Graphics g) {
    g.fillOval(getX(), getY(), BULLET_SIZE, BULLET_SIZE);
  }

  @Override
  public void update(int delta) {
    y += 10;
  }

  public float getX() {
    return x;
  }

  public float getY() {
    return y;
  }

  protected void setXY(float x, float y) {
    this.x = x;
    this.y = y;
  }
}

เพิ่ม Bullet ใน BulletGame

เราจะสร้าง bullet เพื่อทดลองในโปรแกรม ในคลาส BulletGame โดยในโปรแกรมจะพิจารณาวัตถุทั้งหมดเป็น Entity

เพิ่มฟิลด์ entities ในคลาส BulletGame

  private LinkedList<Entity> entities;

สังเกตว่าเราใช้ LinkedList ในการเก็บ entity เพราะว่าสุดท้าย เราจะต้องจัดการลบ entity ที่ไม่ได้ใช้งานออกจากระบบด้วย entity พวกนี้ เช่น กระสุนที่วิ่งออกไปนอกจอแล้ว เป็นต้น ถ้าเราใช้ ArrayList การลบข้อมูลดังกล่าวจะไม่ค่อยมีประสิทธิภาพเท่า LinkedList

ปรับ render และ update ให้เรียก render และ update ทุก ๆ entity ใน entities

  @Override
  public void render(GameContainer container, Graphics g) throws SlickException {
    for (Entity entity : entities) {
      entity.render(g);
    }
  }

  @Override
  public void update(GameContainer container, int delta) throws SlickException {
    for (Entity entity : entities) {
      entity.update(delta);
    }
  }

สุดท้าย สร้างกระสุนใน init

  @Override
  public void init(GameContainer container) throws SlickException {
    entities.add(new Bullet(200,0));
  }

ทดลองเรียกโปรแกรมให้ทำงาน ดูว่ากระสุนวิ่งหรือไม่

Gitmark.png ถ้าโปรแกรมทำงานได้ ให้ commit งานของคุณด้วย

กระสุนแบบมีทิศทาง

เราจะสร้างกระสุนแบบมีทิศทาง โดยกระสุนดังกล่าว เมื่อสร้าง จะมีการกำหนดทิศทางและความเร็วได้ กระสุน DirectionalBullet นี้เป็น subclass ของ Bullet

public class DirectionalBullet extends Bullet {
  private float dir;
  private float velocity;

  public DirectionalBullet(float x, float y, float dir, float velocity) {
    super(x, y);
    this.dir = dir;
    this.velocity = velocity;
  }

  public float getVelocity() {
    return velocity;
  }

  public float getDir() {
    return dir;
  }
}

สังเกตว่า:

  • เรามี field dir และ velocity แต่ทั้งสอง field มีแต่ getters ไม่มี setters เนื่องจากเราไม่ต้องการให้มีการเปลี่ยนแปลงทิศทางของกระสุน เราจึงไม่สร้างเมท็อดเอาไว้

แบบฝึกหัด: ปรับตำแหน่ง

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

แก้ส่วน init ใน BulletGame ให้สร้าง directional bullet ดังนี้

  @Override
  public void init(GameContainer container) throws SlickException {
    entities.add(new DirectionalBullet(320,240,70,10));
  }

อย่าลืมทดสอบโดยทดลองปรับมุมเป็นค่าต่าง ๆ และความเร็วเป็นค่าต่าง ๆ ด้วย

Gitmark.png ทดลองจนใช้ได้ แล้ว commit

วิ่งแบบเป็นคลื่น SineBullet

เราจะสร้างคลาส SinceBullet ที่ทำให้กระสุนวิ่งเป็นคลื่น วิธีการที่เราจะทำให้กระสุนวิ่งเป็นคลื่นนั้น คือเราจะมี track position ที่วิ่งในลักษณะเดียวกับ directional bullet แต่เมื่อเวลาผ่านไป เราจะปรับทิศทางที่เราย้ายออกไปทางซ้ายและขวาเป็นฟังก์ชันแบบ sine แสดงดังตัวอย่างด้านล่าง

Sinebullet.png

แบบฝึกหัด: ให้คุณเขียนคลาส SineBullet ให้เป็น subclass ของ DirectionalBullet และมี constructor แบบเดียวกับ DirectionalBullet ในการเคลื่อนที่ให้กระสุนวิ่งไปในทิศที่กำหนด แต่มีการส่ายไปมาด้วย

คำใบ้: สังเกตว่าการส่ายเป็นคลื่น sine นั้น เป็นการเปลี่ยนตำแหน่งในทิศทางตั้งฉากกับทิศของกระสุน ภายใต้ทิศทางดังกล่าวระดับการเปลี่ยนนั้นเปลี่ยนแปลงตามคลื่น sine

Gitmark.png ทดลองดูหลาย ๆ มุมและหลายความเร็วต้น ถ้าทำงานได้ให้ commit

ย้ายโค้ดส่วน track position ไปที่ DirectionalBullet

สังเกตว่าความสามารถในการ track ตำแหน่งนั้นเป็นประโยชน์มาก ในการสร้างกระสุนที่วิ่งแบบมีทิศทาง เราจะย้ายความสามารถดังกล่าวมาไว้ในคลาส DirectionalBullet ดังนี้

เพิ่ม field trackX และ trackY

  private float trackX;
  private float trackY;

และเพิ่ม getters

  protected float getTrackX() {
    return trackX;
  }

  protected float getTrackY() {
    return trackY;
  }

จากนั้นใน constructor เราจะเก็บค่าเริ่มต้นของ trackX และ trackY

  public DirectionalBullet(float x, float y, float dir, float velocity) {
    // ...
    trackX = x;
    trackY = y;
  }

เราจะต้อง update ค่า field trackX และ trackY ในเมท็อด update ในลักษณะด้านล่าง

  public void update(int delta) {
    trackX += dx;
    trackY += dy;
    setXY(trackX, trackY);
  }

อย่างไรก็ตาม โค้ดดังกล่าวมีปัญหาเมื่อ subclass ต้องการใช้ค่า trackX และ trackY ในเมท็อด update ของ subclass เพราะว่าเมื่อ subclass เขียนเมท็อด update ใหม่ จะไปแทน update เดิม ดังนั้นถ้าเราต้องการปรับค่า track เราต้องเรียก update ของ DirectionalBullet อย่างไรก็ตาม ในการเรียกดังกล่าวเรามีการปรับค่า x, y ด้วย ซึ่งมักจะเป็นสิ่งที่เราต้องการให้ subclass เป็นผู้ดำเนินการ

เนื่องจากเมท็อดเป็นหน่วยย่อยสุดในการ override เราจะแยกเมท็อด update ออกเป็นสองเมท็อดย่อย โดยแยกกิจกรรมของ update เป็นสองส่วนคือ updateTrack และ updatePosition เราตั้งเป้าหมายไว้ว่า subclass จะเขียนเฉพาะเมท็อด updatePosition ใหม่เท่านั้น โค้ดของเราเปลี่ยนไปดังนี้:

  public void update(int delta) {
    updateTrack();
    updatePosition(delta);
  }

  protected void updatePosition(int delta) {
    setXY(trackX, trackY);
  }

  private void updateTrack() {
    trackX += dx;
    trackY += dy;
  }

สังเกตว่า updatePosition เป็น protected ส่วน updateTrack เราตั้งไว้ให้เป็น private

แบบฝึกหัด: แก้โค้ด SineBullet

หลังจากที่เราแยกส่วน track ตำแหน่งมาแล้ว ให้แก้คลาส SineBullet ให้เรียกใช้ความสามารถดังกล่าวจาก DirectionalBullet แทน

กระสุนสั่น RandomDirectionalBullet

เมื่อเรามีโค้ดส่วน track ตำแหน่งตามทิศทางใน DirectionalBullet แล้ว เราจะเพิ่มกระสุนประเภทนี้อีกได้โดยง่าย ด้านล่างแสดงตัวอย่างคลาส RandomDirectionalBullet

public class RandomDirectionalBullet extends DirectionalBullet {

  private Random random;

  public RandomDirectionalBullet(float x, float y, float dir, float velocity) {
    super(x, y, dir, velocity);
    random = new Random();
  }

  protected void updatePosition(int delta) {
    setXY((float)(getTrackX() + (random.nextFloat()-0.5)*5),
        (float)(getTrackY() + (random.nextFloat()-0.5)*5));
  }
}

กระสุนดาวกระจาย

ในส่วนนี้เราจะสร้างกระสุนอีกประเภท คือ StarBullet ซึ่งจริง ๆ แล้วประกอบไปด้วยกระสุนแบบมีทิศทางเป็นจำนวนมาก

กระสุนดาวกระจายด้วย DirectionalBullet

สร้างคลาส StarBullet ให้เป็น subclass ของ Bullet ใน constructor ให้รับพารามิเตอร์มากกว่า Bullet ธรรม คือให้รับความเร็วด้วย (ซึ่งเราจะส่งไปให้กับ directional bullet ต่อไป)

  public StarBullet(float x, float y, float velocity) {
    super(x, y);
    
    bullets = new ArrayList<DirectionalBullet>();
    for (int i=0; i < 36; i++) {
      bullets.add(new DirectionalBullet(x, y, i*10, velocity));
    }
  }

อย่าลืมประกาศ field bullets:

  private ArrayList<DirectionalBullet> bullets;

เมท็อด render และ update ทำงานโดยเรียก render และ update ของ DirectionalBullet

  public void render(Graphics g) {
    for (DirectionalBullet bullet : bullets) {
      bullet.render(g);
    }
  }
  
  public void update(int delta) {
    for (DirectionalBullet bullet : bullets) {
      bullet.update(delta);
    }    
  }

ทดลองเรียกให้โปรแกรมทำงาน ตรวจสอบว่าแสดงกระสุนเป็นวงกลม ขนาดขยายใหญ่ขึ้นเรื่อย ๆ

Factory Method Design Pattern

สังเกตว่าโค้ดใน StarBullet นั้นสามารถทำงานกับ DirectionalBullet อะไรก็ได้ อย่างไรก็ตาม โปรแกรมบรรทัดนี้:

     bullets.add(new DirectionalBullet(x, y, i*10, velocity));

ทำให้ StarBullet สร้างได้แต่ DirectionalBullet เท่านั้น

ในส่วนนี้เราจะทดลองใช้ Factory method pattern ในการแยกส่วนการสร้าง DirectionalBullet ออกมาจาก constructor ของ StarBullet

เราจะสร้างคลาสที่วัตถุมีเมท็อดเดียวคือ build และคืน object ที่เป็น DirectionalBullet (หรือ subclass ของ DirectionalBullet) โดยเราจะเรียกว่าคลาส DirectionalBulletFactory

public class DirectionalBulletFactory {
  public DirectionalBullet build(float x, float y, float dir, float velocity) {
    return new DirectionalBullet(x, y, dir, velocity);
  }
}

เราจะแก้โค้ด constructor ของคลาส StarBullet ให้รับ object ที่เป็น DirectionalBulletFactory เพื่อนำมาสร้าง bullets ดังด้านล่าง

  public StarBullet(float x, float y, float velocity,
      DirectionalBulletFactory factory) {
    super(x, y);
    
    bullets = new ArrayList<DirectionalBullet>();
    for (int i=0; i < 36; i++) {
      bullets.add(factory.build(x, y, i*10, velocity));
    }
  }

และโค้ดในเมท็อด init ของ BulletGame จะเปลี่ยนไปดังนี้

  @Override
  public void init(GameContainer container) throws SlickException {
    entities.add(new StarBullet(320,240,1, new DirectionalBulletFactory()));
  }

สังเกตว่าเรา new DirectionalBulletFactory ส่งไปให้เพื่อให้ constructor สามารถเรียกเพื่อสร้าง bullet ได้