الأحد، 4 يناير 2015

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

فكيف يمكننا إدارة هذا الأمر في اللعبة؟
هناك العديد من الطرق للقيام بذلك. فيمكننا إدماج هذا مباشرة في حلقة اللعبة مثلا
//حلقة اللعبة
public void run() {
  while(running) {
    if(this->currentState == STATE_MENU) {
      // تحديث ورسم قائمة الإختيارات
    } else if(this->currentState == STATE_LEVEL1) {
      // تحديث ورسم المستوى الأول
    } else if(this->currentState == STATE_CREDITS) {
      // تحديث ورسم لوحة المراتب والنقاط
    }
  }
}
لكن بهذه الطريقة سريعا ما سيصبح الأمر معقدا كما سيصعب فهم أسطر العبة وصيانتها وكذا متعة برمجتها. وستصبح مهمة إضافة حالة جديدة للعبة كلوحة إختيار شخصية اللعبة معقدة.
لكن ماذا لو كان لدينا مكون يتحكم في كل حالات اللعبة ويحدد أياََ منها يجب تحديثه ورسمه؟ ألن يصبح الأمر أسهل وأسرع.
وهذا هدفنا من هذا الدرس. شرح كيفية إنشاء مكون يتحكم في إدارة حالات اللعبة.
يوجد نمط تصميم سهل وفعال لإدارة كل هذا يسما ب Game state manager أو مدير حالات اللعبة.
وهذا ما سنقوم بشرحه وبرمجته ثم إدماجه في مثال السلسلة. لعبة المحطم هكذا قررت تسميت مثال السلسلة هههه.

لكن أولا دعونا نحدد حالات لعبتنا. ولنجعل الأمر سهلا وبسيطا سنجعل مثالنا يتكون من حالتين أو مرحلتين فقط :

  • لوحة الإختيارات : ستمكننا من بدء لعبتنا أو إنهاء اللعبة  والخروج.
  • المستوى الأول للعبة : سيمكننا من لعب المستوى الأول. 
بهذا نكون قد حددنا حالتين للعبتنا. ويجب على المكون المتحكم في هذه الحالات إظهار المناسبة منها حسب الأزرار المفعلة والسياق الذي نتواجد فيه.

أترككم مع فيديو قصير يوضح نتيجة هذا الدرس


تذكير سريع : العبة هي عبارة عن حلقة لامتناهية نقوم فيها بتحديث المعلومات ثم رسمها لإظهارها على الشاشة.

ما سنقوم به هو تفويض عمليات التحديث و الرسم وكذلك التحميل لحالات اللعبة. حيث ستقوم كل حالة بتحديث معلوماتها الخاصة ثم رسمها. وسنجعل من مدير الحالات يحدد أيا منها يجب إستدعائها وتنفيذ وظائفها داخل حلقة اللعبة.

لنبدئ بإنشاء فصيلة تحدد خاصيات حالات لعبتنا عموما. كل حالة من حالات العبة يجب أن تكون قادرة على تحميل الموارد التي ستحتاجها و على تحديث المعلومات الخاصة بها والرسم على الشاشة وكذلك التجاوب مع الأوامر المُصْدَرة.
//حالات اللعبة، فصيلة مجردة
//تحدد خاصيات حالاتها
public abstract class GameState {
 //مرجع نحو مدير الحالات
 // سنقوم بإنشائه لاحقا
 protected GameStateManager gsm;
 //منشئ الحالة
 public GameState(GameStateManager gsm) {
  this.gsm = gsm;
 }
 //وظيفة مجردة:تهييء و تحميل
 public abstract void init();
 //وظيفة مجردة:التحديث
 public abstract void update();
 //وظيفة مجردة:الرسم
 public abstract void draw(Graphics2D g);
 //وظيفة مجردة:التجاوب مع المداخيل والأوامر المصدرة
 public abstract void handleInput();
 
}
بعد هذا يجب على كل حالات اللعبة ان ترث من GameState وتحدد وظائفها المجردة.
وقد قمنا سابقا بتحديد حالتين في مثالنا لوحة الإختيارات و المستوى الأول للعبة.
لنقم ببرمجتهما. ولنبدأ بالفصيلة التي تجسد حالة لوحة الإختيارات.
لوحة الإختيارات ستحتوي إختيارين : بدء اللعبة و الخروج. يجب ان نستطيع تغيير الإختيارات عن طرق أسهم التوجيه فوق تحت من لوحة المفاتيح. تأكيد الإختيار نقوم به عن طريق زر الدخول.

public class MenuState extends GameState {
    //الإختيارات
    private final static int PLAY = 0;
    private final static int QUIT = 1;
    private final static int NBR_CHOICE = 2; //عدد الإختيارات
    private int currentChoice = 0; //الإختيار الحالي

    // الصور المستخدمة في لوحة الإختيارات
    private BufferedImage game_title;//عنوان اللعبة
    private BufferedImage ui_btn_start_normal;//إختيار البدءعادي
    private BufferedImage ui_btn_start_active;//إختيار البدء مفعل
    private BufferedImage ui_btn_quit_normal;//إختيار الخروج عادي
    private BufferedImage ui_btn_quit_active;//إختيار الخروج مفعل

   //تهيءة الفصيلة
    public MenuState(GameStateManager gsm) {
       super(gsm);
       init();
    }
    //تحميل الموارد
    public void init() {
      game_title = load("/game_title.png");
      ui_btn_start_normal = load("/ui_btn_start_normal.png");
      ui_btn_start_active = load("/ui_btn_start_active.png");
      ui_btn_quit_normal = load("/ui_btn_quit_normal.png");
      ui_btn_quit_active = load("/ui_btn_quit_active.png");
    }
    //وظيفة التحديث
    public void update() {
        //إدارة الأوامر
        handleInput();
    }
    
    //وظيفة الرسم
    public void draw(Graphics2D g) {

      // لون خلفية اللوحة
      g.setColor(new Color(236, 236, 236));
      g.fillRect(0, 0, GamePanel.WIDTH, GamePanel.HEIGHT);

      // رسم عنوان اللعبة
      g.drawImage(game_title, 0, 50, null);

      //  رسم الإختيارات حسب حالتها
      if (currentChoice == PLAY) {
         g.drawImage(ui_btn_start_active, 214, 200, null);
      } else {
          g.drawImage(ui_btn_start_normal, 214, 200, null);
      }

      if (currentChoice == QUIT) {
          g.drawImage(ui_btn_quit_active, 214, 300, null);
      } else {
           g.drawImage(ui_btn_quit_normal, 214, 300, null);
      }
   }

   //إدارة الأوامر
   public void handleInput() {
        //تفعيل الإختيار
        if (Keys.isPressed(Keys.ENTER)){
          select();
        }
        //تغيير الإختيار الحالي
        if (Keys.isPressed(Keys.UP)) {
            if (currentChoice > 0) {
               currentChoice--;
            }
        }
        //تغيير الإختيار الحالي
        if (Keys.isPressed(Keys.DOWN)) {
           if (currentChoice < NBR_CHOICE - 1) {
               currentChoice++;
           }
        }
    }
    //تفعيل الإختيار
    private void select() {
    if (currentChoice == PLAY) {
         //نقوم بتغيير الحالة الحالية داخل مدير الحالات
         gsm.setState(GameStateManager.LEVEL1_STATE);
    } else if (currentChoice == QUIT) {
        System.exit(0);
    }
  }
  //تحميل الموارد
  public BufferedImage load(String s) {
     try {
         return ImageIO.read(MenuState.class.getResourceAsStream(s));
     } catch (Exception e) {}
     return null;
  }
}
هكذا نكون قد انهينا أول حالة من اللعبة (لوحة الإختيارات).
فل ننتقل إلى إنشاء المستوى الأول من اللعبة. في هذه المرحلة من سلسلة الدروس المستوى الأول لا يحتوي على الكثير. فالهدف من هذا الدرس هو توضيح إدارة حالات اللعبة فقط. سنقوم بتطوير خصائص المستوى الأول في دروس لاحقة من هذه السلسلة.
المستوى الأول يرث أيظا من GameState ويحدد وظائفها المجردة
//حالة المستوى الأول 
public class Level1State extends GameState {
    //الوحش الذي نقوم بتحريكه في المثال
    private Monster monstre;
    //منشء الفصيلة
    public Level1State(GameStateManager gsm) {
       super(gsm);
       init();
    }
 
    //تهيءة موارد الفصيلة
    public void init() {
       this.monstre = new Monster(GamePanel.WIDTH, GamePanel.HEIGHT);
    }

    //تحديث معلومات مكونات المستوى
    public void update() {
        handleInput();
        this.monstre.update();
    }

    //رسم وإظهار المستوى
    public void draw(Graphics2D g) {
         // رسم الخلفية
         g.setColor(new Color(130, 218, 242));
         g.fillRect(0, 0, GamePanel.WIDTH, GamePanel.HEIGHT);
         g.setColor(new Color(177, 137, 73));
         g.fillRect(0, GamePanel.HEIGHT - 128, GamePanel.WIDTH, GamePanel.HEIGHT);
         // رسم الوحش
         this.monstre.draw(g);
    }

    //إدارة الأوامر في المستوى
    public void handleInput() {
        this.monstre.left = Keys.isPressed(Keys.LEFT);
        this.monstre.right = Keys.isPressed(Keys.RIGHT);
        this.monstre.atack = Keys.isPressed(Keys.BUTTON_ATACK);
        // نعود للوحة الإختيارات عند الضغط على
        // Escape
        if (Keys.isPressed(Keys.ESCAPE)) {
           // تغيير الحالة الحالية
           // في مدير الحالات
           gsm.setState(GameStateManager.MENU_STATE);
        }
    }
}
هكذا نكون قد أنهينا برمجة الحالتين اللتان سبق وحددناهما. الأن لنقم بإنشاء الفصيلة التي ستقوم بإدارة الحالات داخل اللعبة.
هذه الفصيلة يجب أن تكون قادرة على تحديد الحالة التي يجب تنفيذ وظائفها ( التحديث والرسم ).
لنرى كيف يمكننا برمجة هذا
//فصيلة إدارة الحالات
public class GameStateManager {
    //جدول الحالات التي نود إدارتها
    private GameState[] gameStates;
    private int currentState; //الحالةالحالية
    //أكواد الحالات داخل اللعبة
    public static final int MENU_STATE = 0;
    public static final int LEVEL1_STATE = 1;
    //عدد الحالات التي نود إدارتها
    public static final int NUM_GAME_STATES = 2;

    //منشئ الفصيلة
    public GameStateManager() {
      gameStates = new GameState[NUM_GAME_STATES];//تهيئة جدول الحالات
      currentState = MENU_STATE;// تهيئة الحالة الحالية
      loadState(currentState);//تحميل الحالة الحالية
    }
    //وظيفة تحميل الحالات
    private void loadState(int state) {
      if (state == MENU_STATE) {
         gameStates[state] = new MenuState(this);
      } else if (state == LEVEL1_STATE) {
         gameStates[state] = new Level1State(this);
      }
   }
   //وظيفة تفريغ الحالات
   private void unloadState(int state) {
        gameStates[state] = null;
   }
   //تحديد الحالة الحالية
   public void setState(int state) {
          unloadState(currentState);
          currentState = state;
          loadState(currentState);
   }
   //تحديث الحالة الحالية
   public void update() {
       if (gameStates[currentState] != null) {
           gameStates[currentState].update();
       }
   }
   //رسم الحالة الحالية
   public void draw(java.awt.Graphics2D g) {
     if (gameStates[currentState] != null) {
        gameStates[currentState].draw(g);
     }
   }
}
هكذا نكون قد أنهينا برمجة فصيلة الإدارة لحالات اللعبة.
لنقم بدمجها مع حلقة اللعبة.
//خيط اللعبة
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 GameStateManager gsm;

    //...

    @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 update() {
        //نقوم بإستدعاء وظيفة التحديث من مدير الحالات
        //الذي يحدد وظيفة التحديث للمرحلة الحالية
        gsm.update();
    }

    private void draw() {
        //نقوم بإستدعاء وظيفة الرسم من مدير الحالات
        //الذي يحدد وظيفة الرسم للحالة الحالية
        gsm.draw(g);
        //إظهار الحالة على الشاشة
        Graphics2D g2 = (Graphics2D) gamePanel.getGraphics();
        g2.drawImage(image, 0, 0, GamePanel.WIDTH * GamePanel.SCALE, GamePanel.HEIGHT * GamePanel.SCALE, null);
        g2.dispose();
    }
}

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

الأربعاء، 31 ديسمبر 2014

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

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


أترككم مع فيديو قصير يوضح النتيجة النهائية لهذا الدرس

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

//هذه الفصيلة تحتوي على جدول من المتغيرات المنطقية
//تحدد حالة كل زر من الأزرار المستخدمة في اللعبة
//يكون الزر مفعل إذا كان
//keyState[codeKey] = true
public class Keys {
    //عدد الأزرار
    public static final int NUM_KEYS = 3;
    //جدول من المتغيرات المنطقية
    //يحدد حالة كل زر من الأزرار المستخدمة في اللعبة
    public static boolean keyState[] = new boolean[NUM_KEYS];
    
    //أكواد الأزرار المستخدمة داخل اللعبة
    public static int LEFT = 0;
    public static int RIGHT = 1;
    public static int BUTTON_ATACK = 2;
    
    // وظيفة تقوم بتغيير حالة الأزرار داخل اللعبة
    public static void keySet(int i, boolean b) {
      if (i == KeyEvent.VK_LEFT) keyState[LEFT] = b;
      else if (i == KeyEvent.VK_RIGHT) keyState[RIGHT] = b;
      else if (i == KeyEvent.VK_A) keyState[BUTTON_ATACK] = b;
    }
    // وظيفة تمكننا من تحديد حالة زر ماَ
    public static boolean isPressed(int i) {
      return keyState[i];
    }
}

الأن لنقم بتسجيل حالة الأزرار داخل اللعبة من خلال تتبع حالتها على لوحة المفاتيح. وذللك في صفيحة اللعبة التي قمت بالتطرق لها في الدرسين السابقين.
من أجل ذلك سنقوم بتزويد صفيحة لعبتنا بمتتبع لحالة الأزرار (KeyListener) في الجافا.

//نزود صفيحة اللعبة بمتتبع الأزرار
public class GamePanel extends JPanel implements KeyListener {
    //...
    @Override
    public void addNotify() {
         super.addNotify();
        //نحدد متتبع حالة الأزرار
        addKeyListener(this);
        gameThread = new GameThread(this);
        gameThread.start();
    }
    //هذه الوظيفة غير مستعملة في مثالنا
    @Override
    public void keyTyped(KeyEvent e) {}
    // في حالة تفعيل زر ما
    @Override
    public void keyPressed(KeyEvent key) {
        //تغيير حالة الزّر في جدول الأزرار إلى مفعل
        Keys.keySet(key.getKeyCode(), true);
    }
    //إنهاء تفعيل الزّر
    @Override
    public void keyReleased(KeyEvent key) {
        //تغيير حالة الزّر في جدول الأزرار إلى معطل
        Keys.keySet(key.getKeyCode(), false);
    }
}

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

public class Monster {

    //...
    public boolean facingLeft;
    public boolean right; //حالة المشي نحو اليمين
    public boolean left; //حالة المشي نحو اليسار
    public boolean atack; //حالة الهجوم
    //...
}

الأن من خلال حلقة اللعبة سنقوم بتحديث هذه المعلومات حسب الأزرار المفعلة في اللعبة.

public class GameThread extends Thread {

    //...
    private Monster monstre;

    public GameThread(GamePanel gamePanel) {
        this.gamePanel = gamePanel;
        this.monstre = new Monster(GamePanel.WIDTH, 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 update() {
        //في هذه الوظيفة نقوم بتحديث حالة شخصية اللعبة
        // حسب الأزرار المفعلة
        handelInput(); 
        this.monstre.update();
    }
    // هذه الوظيفة تغير المعلومات حول حالة الشخصية
    // التي نود التحكم فيها حسب حالة الأزرار
    private void handelInput() {
       this.monstre.left  = Keys.isPressed(Keys.LEFT);
       this.monstre.right = Keys.isPressed(Keys.RIGHT);
       this.monstre.atack = Keys.isPressed(Keys.BUTTON_ATACK);
    }

    //...
}

الأن الحركات التي نود لشخصيتنا القيام بها محفوظة في المتغيرات الثلاثة  left , right, atack التي أضفناها إلى الفصيلة Monster وقمنا بتهيئها في حلقة اللعبة.
ما يتبقا القيام به هو تحميل الرّقوش المخصّصة لهذه الحركات والقيام برسمها حسب ما قمن بتهيئته من معلومات.
سأستمر في إستخدام الرّقوش من لعبة replica island المفتوحة المصدر.

السطر الأول من صفحة الرّقوش يحتوي على رقشة واحدة تمثل شخصيتنا في حالة سُكُونْ.
السطر الثاني من صفحة الرّقوش يحتوي على حالات المشي للشخصية.
أما السطر الثالث فيحتوي على الرّقوش التي تجسد حالة الهجوم للوحش الطيني.
سنقوم بتحميلها في لعبتنا وإظهارها حسب الزر المفعل.
كيف يمكننا تحميل صفحة الرقوش؟
لقد سبق وتطرقة للموضوع في درس تحريك الرقوش لكن المثال كان يحتوي على سطر واحد فقط. لذلك سأوضح كيفية تحميل صفحة متعددة الأسطر.
تذكر أن صفحة الرقوش يجب أن تنتمي إلى موارد البرنامج وذلك بوضعها في ملف  وإضافته إلى مسار بناء المشروع (Build path).
لنقم الأن بتحميل رقوشنا.
public class Monster {
    //...
    //مسار الولوج لصفحة الرقوش
    private final String sprite_name = "/monster.png";
    //جدول الرقوش
    private List sprites;
    //أبعاد الرقوش طول و عرض
    private final int sWidth = 128;
    private final int sHeight = 128;
    //جدول يحدد عدد الرقوش في كل سطر
    private int[] NUM_FRAMES = { 1, 6, 6 };
    //عدد الأسطر في جدول الرقوش
    private final int NBR_SPRITE_ROWS = 3;
    //....
    public Monster(int screenWidth, int screenHeight) {
         this.cWidth = screenWidth;
         this.cHeight = screenHeight;
        //...
        try {
            //وظيفة تحميل صفحة الرقوش
            loadSprites();
            //...
        } catch (IOException e) {}
    }
    //وظيفة تحميل صفحة الرقوش
    private void loadSprites() throws IOException {
        //تحميل صفحةالرقوش من موارد اللعبة 
        //عن طريق مسارها
        InputStream inStream = getClass().getResourceAsStream(sprite_name);
        //قراءة صفحة الرقوش
        BufferedImage spritesSheet = ImageIO.read(inStream);
        //تهيئة جدول الرقوش
        sprites = new ArrayList();
        //تحميل كافة الرقوش إلى جدول الرقوش
        //وذلك بقرائتها سطرا بسطر
        for (int i = 0; i < NBR_SPRITE_ROWS; i++) {
            //جدول الرقوش في كل سطر
            BufferedImage[] actionSprites = new BufferedImage[NUM_FRAMES[i]];
            for (int j = 0; j < NUM_FRAMES[i]; j++) { 
                // تحميل الرّقوش من كل خانة في صفحة الرّقوش
                // حسب السطر والعمود
                actionSprites[j] = spritesSheet.getSubimage(j * sWidth, 
                                                            i * sHeight,
                                                            sWidth,
                                                            sHeight);
            }
            // إضافة جدول الرقوش من كل سطر إلى جدول الرّقوش العام
            sprites.add(actionSprites);
        }
    }
  //...
}
الأن وبعدما قمنا بتحميل صفحة رُقوشنا. فلنقم بإستخدامها لإظهار الرّقشة المناسبة منها حسب حالة شخصيتنا والتي سجَّلناها في المتغيرات الثلاثة left, right, atck كما تتذكرون وذلك حسب الأزرار المفعلة.
الحركات التي نود برمجتها هي المشي والهجوم وذلك حسب الإتجاه .
إذن ما سنحتاجه كمعلومات لإنجاز هذا هي سرعة المشي، إحداثيات الشخصية، سرعة التوقف، إتجاه المشي، السرعة القصوى لشخصيتنا.
سنجعل الوحش الطيني غير قادر على تجاوز أقصى يمين ويسار الشاشة، ليبقى في مجال رؤيتنا.
كل هذا سنقوم به في وظيفة التحديث للفصيلة المجسدة لوحشنا.

public class Monster {
    //إحداثيات الوحش على الشاشة
    public double x;
    public double y;
    //أبعاد الشاشة طول وعرض
    public int cWidth;
    public int cHeight;
    //حالة الوحش
    public boolean facingLeft; //الإتجاه الحالي
    //الأوامر المطلوب تنفيذها
    public boolean right;
    public boolean left;
    public boolean atack;
    //سرعة الوحش
    public double velocite = 0.2;
    //قيمت تغير الإحداثية الأفقية للوحش
    public double dx = 0.0;
    public double maxSpeed = 0.4;//السرعة القصوى
    public double stopSpeed = 0.4;//سرعة التوقف
    //وظيفة التحديث تقوم بتحديث معلومات الوحش قبل الرسم
    public void update() {
        //نقوم بحساب الموقع الموالي للوحش على الشاشة
        calculateNextPosition();
        //نقوم بحساب الحركة الموالية للوحش سكون، مشي أو هجوم
       calculateNextAnimation();
       //نغير الإحداثية الأفقية حسب ما قمنا بحسابه سابقا
       x += dx;
       //نقوم بتحديث الحركة حسب ما قمنا بتحديده سابقا
       animation.update();
   }
}
الوظيفة calculateNextPosition تقوم فقط بحساب القيمة التي ستتغير بها الإحداثية الأفقية X وذلك حسب سرعتها التي يجب ان لا تتجاوز السرعة القصوى.
لدينا أيضا سرعة للتوقف في حالة توقفنا عن الضغط  على الأزرار.
نقوم بأخذ بعين الإعتبار أبعاد الشاشة أيضا لكي لا نتجاوزها كما سبق وذكرت.
فل نرى كيف يمكننا برمجة هذا
private void calculateNextPosition() {
 if (left) { //نريد التوجه نحو اليسار
     facingLeft = true;//نغير الإتجاه الحالي
     dx -= velocite; //نحسب قيمة تغير الإحداثية الأفقية
     //إذا تجاوزت قيمة التغير السرعة القصوى
     //نقوم بإعادة تحديدها
     if (Math.abs(dx) > maxSpeed) {
              dx = -maxSpeed;
     }
 } else if (right) { //نريد التوجه نحو اليمين
     facingLeft = false;//نغير الإتجاه الحالي
     dx += velocite;//نحسب قيمة تغير الإحداثية الأفقية
     //إذا تجاوزت قيمة التغير السرعة القصوى
     //نقوم بإعادة تحديدها
     if (dx > maxSpeed) {
          dx = maxSpeed;
     }
 } else {//نريد التوقف لايوجد أمر بالتحرك
    // نقوم بحسابة قيمة التغير ونقصها حتا تصبح منعدمة 
    if (dx > 0) {
        dx -= stopSpeed;
    if (dx < 0) {
      dx = 0;
    }
    } else if (dx < 0) {
       dx += stopSpeed;
       if (dx > 0) {
           dx = 0;
       }
    }
 }
 //إذا كنا على وشك تجاوز أطراف الشاشة
 //نقوم بإيقاف الشخصية وذلك بجعل قيمة تغير الإحداثية الأفقية منعدمة
 if (x < -(sWidth / 2)) {
     dx = 0;
     x = -(sWidth / 2);
 } else if (x > (cWidth - sWidth / 2)) {
     dx = 0;
     x = cWidth - sWidth / 2;
 }
}
هكذا نكون قد حددنا مكان رسم الوحش لكننا لم نحدد بعد الرّقشة التي نود رسمها ومدة ظهورها.
سنجعل الوظيفة calculateNextAnimation تقوم بذلك
public class Monster {
    //إحداثيات الوحش على الشاشة
    public double x;
    public double y;
    //أبعاد الشاشة طول وعرض
    public int cWidth;
    public int cHeight;
    //حالة الوحش
    public boolean facingLeft; //الإتجاه الحالي
    //الأوامر المطلوب تنفيذها
    public boolean right;
    public boolean left;
    public boolean atack;
    
    private int currentAction = 0; // الحركة الحالية
    public final int IDL = 0;//حالة السكون
    public final int WALK = 1;//حلة المشي
    public final int ATACK = 2;//حالة الهجوم
    
    // المدة اللازمة قبل المرور للرقشة الموالية لكل حركة
    private int[] SPRITE_DELAY = { -1, 6, 5 };
    private Animation animation; //فصيلة التحريك
    //...
   //هذه الوظيفة تقوم بحسابة الرقشة الموالية حسب نوع الحركة التي نرغب بها 
   private void calculateNextAnimation() {
   // لا نستطيع التحرك في حالة الهجوم
   if (currentAction == ATACK) {
     dx = 0;
   }

   // في حالة الهجوم نهيء رقوش الهجوم
   if (atack && currentAction != ATACK) {
     setAnimation(ATACK);
   }
   //في حالة الهجوم يجب أن نظهر الحركة مرة واحدة فقط
   //ولذلك نتحقق من ذلك من خلال الوظيفة 
   //animation.hasPlayedOnce()
   if (currentAction == ATACK && animation.hasPlayedOnce()) {
     atack = false;
     setAnimation(IDL);
   }
   //في حالة إنعدام الأوامر نجعل الوحش في حالة سكون
   if (!left && !right && !atack && animation.hasPlayedOnce()) {
     setAnimation(IDL);
   }
   //في حالة المشي نحدد الحركة المناسبة
 if ((left || right) && (currentAction != WALK && currentAction != ATACK))   {
     setAnimation(WALK);
 }
}
//هذه الوظيفة تحدد الحركة الحالية للوحش
private void setAnimation(int i) {
 currentAction = i;
 animation.setFrames(sprites.get(currentAction));
 animation.setDelay(SPRITE_DELAY[currentAction]);
}

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


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

مرحبا بك في ثاني الدُّروس من سلسلة  أسس برمجة الألعاب (جَافَا). لقد قمت في الدرس الأول بشرح حلقة اللعبة وهيكلها الرئيسي وكيفية برمجتها.
أدعوك للإطلاع عليه من هذا الرابط الدرس 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();
 }
}
 

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


السبت، 20 ديسمبر 2014

مرحبا بك في مدونة صُنَاع الألعاب، سأحاول في أول درس من سلسلة الدروس التي سنقدمها لكم عبر هذه المدونة شرح أساسيات برمجة الألعاب الإلكترونية.
المكتسبات الازمة لمتابعة الدرس :
  • أساسيات البرمجة بلغة الجافا
  • برنامج إيكليبس (Eclipse) يمكن تحميله من هذا الرابط
  • 30 دقيقة من وقتك :)

ماهي الألعاب الإلكترونية؟
ببساطة هي عبارة عن برنامج إلكتروني عادي كباقي البرامج الأخرى مع فرق بسيط جدا، المتعة والترفيه الذي تقدمه لمستخدميها.

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

ثانيا تصميم المستويات، وهو دور مصمم العبة (Game designer) وهي خطوة مهمة جدا لنجاح اللعبة.

ثالثا الغرافكس أو رسوميات اللعبة  وهو دور فنان الغرافيست (Graphic artist)، كلما زاد جمالها كلما زادت اللعبة جمالا.

رابعا الصوتيات المستخدمة في اللعبة، يجب أن تلائم القصة ،الرسوم وكذلك الموضوع الأساسي للعبة.

خامسا وأخيرا مدى مشاركة اللاعب في العبة، فكلما زادت مشاركته في القرارات المتخدة في اللعبة كلما زاد حبه لها.

ومحبوا برمجة الألعاب المستقلون يبدؤون بتطوير كل هذه المهارات لإنشاء ألعابهم، ففقط الشركات قادرة على جمع فريق عمل إحترافي حيث يقوم كل فرد بإتمام جزء من اللعبة. من التصميم مرورا بالرسم والبرمجة إلى تأليف الصوتيات.
لقد لقِيَتْ العديد من الألعاب المصممة من قبل مستقلين نجاحا باهرا لتميزها. أنصحكم بالبحث عنها. وهذا مقطع من فلم وثائقي جميل يتحدث عن بعظ من هؤلاء الأشخاص (Inde game the movie trailer)  لاكننا لانجد للأسف الكثير منها في العالم العربي رغم كثرة المبرمجين.

هل تريد صناعة لعبة جيدة؟ فلننطلق من الصفر.


إن اللعبة هي عبارة عن حلقة لامتناهية  (Game Loop) تتكرر طول مدة إشتغال اللعبة وتقوم بتحديث معلومات الكائنات (Objects) المكونة لها كالموقع على الشاشة، الوقت، الطاقة... ثم تقوم برسمها على الشاشة وذللك في مجال زمني محدد وهنا نتكلم عن عدد القطات في الثانية (FPS : Frames per second) وهو عدد اللقطات التي تقوم حلقة اللعبة بتحديثها و رسمها في الثانية.
صورة توضيحية لحلقة اللعبة

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

بعد ذلك نجد مرحلة التحديث و الرسم اللتان تتكرران حسب عدد القطات التي نريد في الثانية. مما يحدد سلاسة اللعبة.

في بداية  عالم السينما كان عدد اللقطات يتراوح بين 16 إلى 18 لقطة في الثانية، اليوم ومع تطور التكنلوجيا معيار PAL في أوروبا هو 25 لقطة في الثانية  وفي أمريكا واسيا معيارNTSC هو 30 لقطة في الثانية. 
في مجال الألعاب هذا المعيار يتعلق بالأجهزة المستعملة لإظهار الصورة ويتراوح بين 30 إلى 60  لقطة في الثانية. وذللك لضمان سلاسة اللعبة.

الان أصبحت تعلم أساس برمجة أي لعبة. تحميل الموارد، ثم تكرار :  تحديث المعلومات و الرسم.
كيف يمكننا برمجة كل هذا؟ الأمر سهل جدا.
  // قيمة منطقية تتحكم في إستمرار حلقة اللعبة
  boolean running= true;
  load(); // وظيفة تقوم بتحميل موارد اللعبة
  while(running){
    update(); // وظيفة تقوم بتحديث خاصيات ومكونات اللعبة
    draw(); // وظيفة تقوم برسم مكونات اللعبة بعد التحديث
  }

الاَن وبعد ما أصبح لدينا الهيكل الأساسي للعبة، كيف يممكننا جعلها تشتغل بنظام القطات في الثانية FPS.
لنجعل العبة تشتغل ب 60 لقطة في الثانية. إذن FPS = 60
كم يلزمنا من الوقت لإظهار لقطة واحدة : targetTime = 1000 / FPS  (ألف هو عدد أجزاء الثانية)
إذن سنجعل البرنامج يشغل وظيفتي التحديث و الرسم ثم نقوم بتوقيفه حتى تكتمل المدة targetTime ثم نعيد تشغيله هكذا نضمن عدد القطات المطلوبة في الثانية.
    private int FPS = 60;
    private long targetTime = 1000 / FPS;
    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) {}
    }

لنقم برسم شيء ما على الشاشة. لكن قبل ذللك يجب علينا أولا تحضير مساحة الرسم.
سنقوم بتحظير نافذة جيفريم JFrame وهي عبارة عن النافذة التي ستحمل مساحة الرسم في لغة الجافا.

ولأجل ذلك فلنبدئ بكتابت الكلاس الرئيسي الذي سيشغل البرنامج.
public class Game {

    public static void main(String[] args) {
       // عنوان نافذة الرسم
       JFrame window = new JFrame("أول لعبة"); 
       window.add(new GamePanel());  // الكلاس الرئيسي المحدد لحلقة العبة

       // لإغلاق النافذة عند الضغط على علامة إكس
       window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
       window.setResizable(false); // تعطيل إمكانية تغيير حجم النافذة
       window.pack(); // تحديد حجم النافذة ليتوافق مع حجم مكوناته
       
       // تحديد مكان إظهار النافذة وسط الشاشة
       window.setLocationRelativeTo(null);
       window.setVisible(true); // إظهار النافذة
    }

}

الأن لننشئ مساحة الرسم والتي ستحتوي كذلك حلقة اللعبة ووظائف التحميل، التحديث و الرسم كوظائف أساسية.
مساحة الرسم ستكون عبارة عن خيط (Thread) مستقل  لتشغيل الحلقة.
public class GamePanel extends JPanel implements Runnable {
     
     //حجم مساحة الرسم
     public static final int WIDTH = 320;
     public static final int HEIGHT = 240;

     // لون للرسم 
     private Color color;

     //منشئ الكلاس
     public GamePanel() {
        super();
        //حجم مساحة الرسم
        setPreferredSize(new Dimension(WIDTH, HEIGHT)); 
        setFocusable(true); // تفعيل التركيز على المساحة
        requestFocus();// طلب التركيز
     }

    private void load(){} // وظيفة التحميل
    private void update(){} // وظيفة التحديث
    private void draw(){} // وظيفة الرسم
   
    // (Thread)  هذه الوظيفة تقوم بالإشتغال عند بداية الخيط
    private 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) {}
        }
    }
}

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

private void load() {
 this.color = Color.blue;
 running = true;
}
  • وظيفة التحديث
  هنا سنقوم بتغير اللون عشوائيا عبر إنشاء ثلاثة أرقام عشوائية التي ستكون الون 
R : الأحمر
G : الأخضر
B : الأزرق

وظيفة ()rndInt تقوم فقط بإنشاء رقمنا العشوائي المحصور بين 0 و 256 عدد الألوان الممكنة. 

private void update() {
 int r = rndInt(0, 256);
 int b = rndInt(0, 256);
 int g = rndInt(0, 256);
 this.color = new Color(r, g, b);
}

private int rndInt(int i, int maxDimension) {
 return i + (int) (Math.random() * (maxDimension - i));
}

  • وظيفة الرسم
 هنا سنقوم برسم مستطيل بحجم الشاشة وملون بالون الذي أنشأناه في وظيفة التحديث. الأمر في غاية البساطة
private void draw() {
 Graphics g2 = getGraphics();
 g2.setColor(color);
 g2.fillRect(0, 0, WIDTH, HEIGHT);
 g2.dispose();
}
سنضيف اخر وظيفة والتي ستمكننا من تشغيل البرنامج
    // خيط العبة
    private Thread thread;

    public void addNotify() {
      super.addNotify();
      if (thread == null) {
            thread = new Thread(this); // نقوم بتهيء الخيط
            // ثم نقوم بتشغيله مما سيشغل الوظيفة
            // run()
            // التي تحتوي بدورها على حلقة اللعبة
            thread.start();   
       }
    }

الكود الكامل للمثال :

 
public class GamePanel extends JPanel implements Runnable {

    //حجم مساحة الرسم
    public static final int WIDTH = 320;
    public static final int HEIGHT = 240;


    private Thread thread;
    private boolean running;
    private int FPS = 60;
    private long targetTime = 1000 / FPS;

    // لون للرسم
    private Color color;

     //منشئ الكلاس
     public GamePanel() {
        super();
        //حجم مساحة الرسم
        setPreferredSize(new Dimension(WIDTH, HEIGHT)); 
        setFocusable(true); // تفعيل التركيز على المساحة
        requestFocus();// طلب التركيز
     }

    public void addNotify() {
        super.addNotify();
        if (thread == null) {
            thread = new Thread(this); // نقوم بتهيء الخيط     
            // ثم نقوم بتشغيله مما سيشغل الوظيفة            
            // run()
            // التي تحتوي بدورها على حلقة اللعبة
            thread.start();
         }
    }

    private void load() {
      this.color = Color.blue;
      running = true;
    }

    private void update() {
      int r = rndInt(0, 256);
      int b = rndInt(0, 256);
      int g = rndInt(0, 256);
      this.color = new Color(r, g, b);
    }

    private void draw() {
      Graphics g2 = getGraphics();
      g2.setColor(color);
      g2.fillRect(0, 0, WIDTH, HEIGHT);
      g2.dispose();
    }

    // (Thread)  هذه الوظيفة تقوم بالإشتغال عند بداية الخيط
    private 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 int rndInt(int i, int maxDimension) {
      return i + (int) (Math.random() * (maxDimension - i));
    }
}


هنا نتيجة إشتغال البرنامج الألوان تتغير عشوائيا في كل لقطة.


يمكنك تحميل المشروع وكذلك النتيجة الكاملة من هنا :

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

  


الأربعاء، 17 ديسمبر 2014


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


من نحن

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

راسلنا

الاسم

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

رسالة *