الخميس، 25 ديسمبر 2014

الدرس 2 أسس برمجة الألعاب (جافا) - تحريك الرُّقُوشْ [ Sprites Animation ]

مرحبا بك في ثاني الدُّروس من سلسلة  أسس برمجة الألعاب (جَافَا). لقد قمت في الدرس الأول بشرح حلقة اللعبة وهيكلها الرئيسي وكيفية برمجتها.
أدعوك للإطلاع عليه من هذا الرابط الدرس 1 أسس برمجة الألعاب (جافا) - حلقة اللعبة.

الأن وبعدما أصبحنا نعلم كيف نحظر هيكل اللعبة، سوف نباشر بتحريك الرُّقُوشْ (Sprites animation).

لكن أولا دعونا نستكشف معنى الرُّقوش.
الرُّقْشَة هي عبارة عن رسم لمكوّن متحرّك في اللعبة كالشخصيات والديكور والمؤثّرات والخلفيات.. بإختصارهي صورة لكل ما نود رسمه أوتحريكه.

سأستخدم في هذا الدرس  رُقوش لإحدى شخصيات لعبة الأندرويد Replica Island  الرائعة والمفتوحة المصدر(open source) يمكنك تجريبها مجانا إذا كنت تتوفر على جهاز أندرويد على هذا الرابط  Replica Island on play store


كيف نجعل من شخصيات اللعبة تتحول من رسوم جامدة إلى شخصيات حية؟

لتحريك الرّقوش في الألعاب يجب أولا القيام بتحضير ورسم كل الحركات التي يمكن للشخصيات القيام بها في اللعبة كالمشي أو القفز أو السقوط...  وذلك بإنشاء رسم لكل حالة للشخصية أثناء قيامها بالحركة التي نرغب بها. تماما كما في صناعة الرسوم المتحركة.
على سبيل المثال لجعل رجل الوحل أعلاه قادرا على المشي، سنحتاج لرسم لكل مراحل المشي. ولن يوضح هذا أكثر من مثال حي.
كما نرى، في الصورة أعلاه، لدينا رسم لكل حركة في عملية المشي تقريبا، ولجعلها تتحرك سنقوم برسمها وإظهارها لمدة معينة من الزمن واحدة تلوى الأخرى.

وهنا النتيجة  بالصورة 

إن إعداد الرُّقوش يتطلب خبرة كبيرة وسنين من الممارسة، وسأركز هنا على أسس برمجة الألعاب فقط. لذلك لن أتحدث عن كيفية إنشاء الرُّقوش.
 يمكنكم إيجاد العديد من الرُّقُوش الجاهزة والمفتوحة المصدر على الإنترنت. كما أنصحكم بزيارة هذا الموقع للإكتشاف المزيد opengameart.org

الأن كيف يمكننا برمجة كل هذا في لعبتنا؟

أولا سنقوم بإنشاء الفصيلة (Class) التي ستتكلف بإدارة تحريك الرُّقوش:
هذه الفصيلة يجب أن تكون قادرة على تحديد الرَّقشة الحالية  التي يجب أن نرسمها من بين مجموعة الرُّقوش التي تحدد حركة المشي ومدة ظهورها.
كما تلاحظ الفصيلة مصحوبة بشرح مفصل سيساعدك على الفهم، فلا تتردد بقراءته
public class Animation {
    private BufferedImage[] frames; //جدول الرقوش التي نود تحريكها
    private int numFrames;         //عدد الرقوش في الجدول
    private int currentFrame;     //الرقشة الحالية

    private int delay;     //المدة اللازمة قبل المرور للرقشة الموالية
    private int count;    //عداد لتتبع المدة اللازمة لتغيير الرقشة
    
    //تهيئة جدول الرقوش
    public void setFrames(BufferedImage[] frames) {
        this.frames = frames;     //تهيئة جدول الرقوش
        numFrames = frames.length; //تهيئة عدد الرقوش في الجدول
        currentFrame = 0;         // تهيئة الرقشة الحالية في أول رقشة
        count = 0;               //تهيئة العداد
        delay = 2;              //تهيئة مدة ظهور الرقشة الحالية
    }
    
    //وظيفة تمكننا من تغيير المدة اللازمة لتغيير الرقوش
    public void setDelay(int i) { delay = i; }
    //وظيفة تمكننا من الحصول على الرقشة الحالية
    public BufferedImage getImage() { return frames[currentFrame]; }
 
    //وظيفة التحديث، تتحكم في إدارة الرقوش
    //كما تحدد الرقشة الحالية
    public void update() {
       // -1  لايوجد تحديث في حالة مدة 
      if (delay == -1)   return;
       
       //تحديث العداد
       count++;
       if (count == delay) {
          currentFrame++; // تحديث الرقشة الحالية 
          count = 0; // إعادة العداد للصفر
       }
       //عند وصولنا إلى اخر رقشة، نعيد الرقشة الحالية إلى نقطة البداية
       if (currentFrame == numFrames) {
          currentFrame = 0;
       }
    }
}
الان لنقم بكتابة فصيلة  لتجسيد وحشنا الطيني، كل مكونات اللعبة تكون محددة بصورة مكونة من مجموعة الرّقوش التي تحدد الحركات التي يمكن لهذه المكونات القيام بها ومجموعة من الخاصيات الأخرى :
كالإحداثيات على الشاشة، سرعة التحرك، قوة الهجوم، الإتجاه... هذه الخاصيات متعلقة بطبيعة اللعبة ونوعها طبعا. 
الرقوش يجب أن تنتمي إلى موارد البرنامج وذلك بوضعها في ملف  وإضافته إلى مسار بناء المشروع (Build path). كما توضحه الصورة الموالية


public class Monster {
  
    //إحداثيات الوحش على الشاشة
    public double x;
    public double y;

    //أبعاد مساحة الرسم  طول وعرض
    public int cHeight;
    public int cWidth;

    public boolean facingRight;    // إتجاه الوحش : يمين يسار 
    public double velocite = 0.4; // السرعة التي تتغير بها إحداثيات الوحش

    // إسم الصورة التي تحتوي الرَّقوش في موارد المشروع
    private final String sprite_name = "/monstre.png";  
    private BufferedImage[] sprites;    //جدول الرقوش
    private final int NBR_SPRITE = 6;    //عدد الرقوش
    //أبعاد الرقشة
    private final int sWidth = 128;
    private final int sHeight = 128;

    // فصيلة التحريك
    private Animation animation;

    // منشئ الفصيلة
    public Monstre(int screenWidth, int screenHeight) {
        //تهيئة أبعاد مساحة الرسم
        this.cWidth = screenWidth;
        this.cHeight = screenHeight;
       
        //تهيئة الإحداثيات الأولية 
        this.x = 0;
        this.y = this.cHeight / 2 - sHeight;
        facingRight = true;    //تهيئة الإتجاه الأولي يمين

        try {
           loadSprites(); // ْتهيئة الرُّقُوش
           //تهيئة فصيلة التحريك
           animation = new Animation();
           animation.setFrames(sprites);//جدول الرقوش
           //سنجعل المدة اللازمة لتغيير الرقوش تساوي عددها
           animation.setDelay(NBR_SPRITE);
        } catch (IOException e) {}
    }

  // تحميل الرُّقُوش
  private void loadSprites() throws IOException {
    //تحميل الصورة المحتوية على الرقوش من موارد اللعبة
    // عن طريق إسم الصورة في موارد اللعبة
    InputStream inStream = getClass().getResourceAsStream(sprite_name);
    //قراءة الصورة
    BufferedImage spritesSheet = ImageIO.read(inStream);
    //تهيئة جدول الرقوش حسب عددها
    sprites = new BufferedImage[NBR_SPRITE];
    for (int i = 0; i < NBR_SPRITE; i++) {
     //قص الرقوش من الصورة الرئيسية وتحميلها في جدول الرقوش
     sprites[i]=spritesSheet.getSubimage(i*sWidth, 0, sWidth, sHeight);
    }
  }

  public void update() {
    //وظيفة التحديث، سنقوم فيها بتحديث الإحداثيات 
   // الإتجاه والرقش الموالي 
  }

  public void draw(Graphics2D g) {
    // وظيفة الرسم، سنقوم فيها برسم الرقش الحالي
   // حسب الإتجاه المحدد بخاصية التحديث 
  }
}

 الأن لنقم بكتابة وظيفة التحديث، ومن خلالها سنجعل شخصيتنا الطينية تتحرك من يسار الشاشة إلى يمينها، وعند وصولها إلى أقصى اليمين سنجعلها تغير إتجاهها إلي الجهة المعاكسة.

    public void update() {
        //حسب إتجاهنا سنقوم بتحديث السرعة 
        if (facingRight) {
            if (x >= cWidth) { // عند الوصول إلى أقصى يمين الشاشة نغير الإتجاه
               facingRight = false;
            }
            //نتجه نحو اليمين بسرعة موجبة
            velocite = Math.abs(velocite);
        } else {
            if (x < 0) {// عند الوصول إلى أقصى يسار الشاشة نغير الإتجاه
               facingRight = true;
            }
            //نتجه نحو اليسار بسرعة سالبة
            velocite = -1 * Math.abs(velocite);
        }
        //نقوم بتحديث الإحداثية الأفقية بالسرعة المحددة سابقا
        x = x + velocite;
        //نقوم بتحديث الرقش الحالي
        animation.update();
    }

الأن  بعدما قمنا بإنهاء وظيفة التحديث، فلننشئ وظيفة الرسم التي ستقوم برسم وحشنا الطيني على الشاشة حسب خاصياته:  الرقشة الحالية،  اللإتجاه والإحداثيات.
  public void draw(Graphics2D g) {
   if (facingRight) {//نحن نتجه نحو اليمين
     g.drawImage(animation.getImage(), (int) (x), (int) (y), null);
   } else {//نحن نتجه نحو اليسار
     //هنا سنقوم بقلب الرّقشة الحالية نحو لليسار
     g.drawImage(animation.getImage(), (int) (x), (int) (y), -sWidth, sHeight, null);
   }
}
هذا كل ما سنحتاجه لجعل هذه الشخصية تتحرك من اليمين إلى اليسار.
فلنقم الأن بدمج شخصيتنا في حلقة اللعبة.
 //خيط اللعبة الذي يحتوي حلقتها الامتناهية
 public class GameThread extends Thread {
  private boolean running;
  private int FPS = 60;
  private long targetTime = 1000 / FPS;

  private GamePanel gamePanel;//الصفيحة التي تحتوي مساحة الرسم
  private BufferedImage image;//الصورة النهائية للعبة
  private Graphics2D g;//مساحة الرسم للعبة

  private Monster monstre;//الشخصية التي سنقوم برسمها وتحريكها

  public GameThread(GamePanel gamePanel) {
     this.gamePanel = gamePanel;
     this.monstre = new Monster(this.gamePanel.WIDTH, this.gamePanel.HEIGHT);
  }
  @Override
  public void run() {
    load();
    long start;
    long elapsed;
    long wait;
    while (running) {
         start = System.currentTimeMillis();
         update();
         draw();
         elapsed = System.currentTimeMillis() - start;
         wait = targetTime - elapsed / 1000;
         try {
            if (wait > 0) {
                 Thread.sleep(wait);
     }
         } catch (Exception e) {}
     }
  }
  //وظيفة التحميل
  private void load() {
    running = true;
    //نقوم بتهييء صورة بحجم  شاشة اللعبة
    image = new BufferedImage(gamePanel.WIDTH,
                              gamePanel.HEIGHT,
                              BufferedImage.TYPE_INT_RGB);
    //نحصل على مساحة الرسم من الصورة
    g = (Graphics2D) image.getGraphics();
    //نختار اللون الأسود لرسم الخلفية لاحقا
    g.setColor(Color.BLACK);
  }
  //وظيفة التحديث
  private void update() {
    //نقوم بتحديث خاصيات الشخصية
    this.monstre.update();
  }
  //وظيفة الرسم
  private void draw() {
    //نقوم بملئ الخلفية باللون الأسود
    g.fillRect(0, 0, gamePanel.WIDTH, gamePanel.HEIGHT);
    //نقوم برسم شخصيتنا
    this.monstre.draw(g);
    //نقوم بالحصول على مساحة الرسم من الصفيحة الرئييسية للعبة
    Graphics2D g2 = (Graphics2D) gamePanel.getGraphics();
    //نقوم برسم صورة لعبتنا على صفيحة اللعبة
    g2.drawImage(image, 0, 0, gamePanel.WIDTH, gamePanel.HEIGHT, null);
    //نحرر مساحة الرسم
    g2.dispose();
 }
}
 

هكذا نكون قد وصلنا لنهاية الدرس الثاني، أتمنا أن يكون قد أفادكم ولو بقليل. ستجدون رابط تحميل المشروع والعبة أسفله.
إلى القاء في الدرس الموالي.


ليست هناك تعليقات:

إرسال تعليق

من نحن

مدونة صناع الألعاب عبارة عن مدونة برمجية سنحاول من خلالها مشاركتكم دروس و تطبيقات حول برمجة الألعاب الالكترونية بطريقة مبسطة و سهلة لمساعدة المبرمجين المبتدئين خصوصا على فهم أسس برمجة الألعاب الالكترونية.

راسلنا

الاسم

بريد إلكتروني *

رسالة *