السلام عليكم ومرحبا بكم في الدرس الثالث من سلسلة أسس برمجة الألعب بالجافا. هدفنا من هذه السلسلة مشاركتكم أسس برمجة الألعاب وذلك بطريقة سهلة ومبسطة جدا.
لقد قمت في الدرسين السابقين بشرح حلقة اللعبة ثم أسس تحريك الرّقوش، أمران أساسيان لبرمجة أي لعبة. أنصحكم بالإطلاع عليهما قبل الشروع في قرائة هذا الدرس لأنه تتمة لما سبق.
ماذا نعني بإصدار الأوامر في اللعبة؟
بكل بساطة جعل الشّخصيات تقوم بحركات وذلك بالضغط على بعض الأزرار.مثلا أزرار لوحة المفاتيح أو الفأرة أو ذراع التحكم (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).
لنقم الأن بتحميل رقوشنا.
الأن وبعدما قمنا بتحميل صفحة رُقوشنا. فلنقم بإستخدامها لإظهار الرّقشة المناسبة منها حسب حالة شخصيتنا والتي سجَّلناها في المتغيرات الثلاثة left, right, atck كما تتذكرون وذلك حسب الأزرار المفعلة.
الحركات التي نود برمجتها هي المشي والهجوم وذلك حسب الإتجاه .
إذن ما سنحتاجه كمعلومات لإنجاز هذا هي سرعة المشي، إحداثيات الشخصية، سرعة التوقف، إتجاه المشي، السرعة القصوى لشخصيتنا.
سنجعل الوحش الطيني غير قادر على تجاوز أقصى يمين ويسار الشاشة، ليبقى في مجال رؤيتنا.
كل هذا سنقوم به في وظيفة التحديث للفصيلة المجسدة لوحشنا.
الوظيفة calculateNextPosition تقوم فقط بحساب القيمة التي ستتغير بها الإحداثية الأفقية X وذلك حسب سرعتها التي يجب ان لا تتجاوز السرعة القصوى.
لدينا أيضا سرعة للتوقف في حالة توقفنا عن الضغط على الأزرار.
نقوم بأخذ بعين الإعتبار أبعاد الشاشة أيضا لكي لا نتجاوزها كما سبق وذكرت.
فل نرى كيف يمكننا برمجة هذا
هكذا نكون قد حددنا مكان رسم الوحش لكننا لم نحدد بعد الرّقشة التي نود رسمها ومدة ظهورها.
سنجعل الوظيفة calculateNextAnimation تقوم بذلك
بهذا نكون قد أنهينا درسنا الثالث من سلسلة أسس برمجة الألعاب بالجافا. ستجدون أسفله رابطين لتحميل اللعبة والمشروع كاملا.
وإلى القاء في الدرس القادم.
ما يتبقا القيام به هو تحميل الرّقوش المخصّصة لهذه الحركات والقيام برسمها حسب ما قمن بتهيئته من معلومات.
سأستمر في إستخدام الرّقوش من لعبة 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);
}
}
//...
}
الحركات التي نود برمجتها هي المشي والهجوم وذلك حسب الإتجاه .
إذن ما سنحتاجه كمعلومات لإنجاز هذا هي سرعة المشي، إحداثيات الشخصية، سرعة التوقف، إتجاه المشي، السرعة القصوى لشخصيتنا.
سنجعل الوحش الطيني غير قادر على تجاوز أقصى يمين ويسار الشاشة، ليبقى في مجال رؤيتنا.
كل هذا سنقوم به في وظيفة التحديث للفصيلة المجسدة لوحشنا.
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();
}
}
لدينا أيضا سرعة للتوقف في حالة توقفنا عن الضغط على الأزرار.
نقوم بأخذ بعين الإعتبار أبعاد الشاشة أيضا لكي لا نتجاوزها كما سبق وذكرت.
فل نرى كيف يمكننا برمجة هذا
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]);
}
بهذا نكون قد أنهينا درسنا الثالث من سلسلة أسس برمجة الألعاب بالجافا. ستجدون أسفله رابطين لتحميل اللعبة والمشروع كاملا.
وإلى القاء في الدرس القادم.


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