
بناء لعبة الذاكرة Memory Game على محرك jMonkeyEngine
يُعَدّ محرك jMonkeyEngine خيارًا ممتازًا للمطورين المبتدئين
الذين يرغبون في دخول عالم برمجة الألعاب ثلاثية الأبعاد باستخدام لغة Java.
بفضل واجهته البرمجية البسيطة والفعالة، يتيح لك المحرك التركيز على منطق اللعبة
دون الحاجة للتعمق في تعقيدات الرسوميات الأساسية. في هذا المقال،
سنتعلم خطوة بخطوة كيفية إنشاء لعبة الذاكرة الكلاسيكية، والتي تعد مشروعًا مثاليًا للتعرف
على مفاهيم المحرك الأساسية مثل إدارة الرسوميات، والمدخلات، ومنطق اللعبة.
خطوات برمجة لعبة الذاكرة على محرك jMonkeyEngine
يُعَدّ محرك jMonkeyEngine خيارًا ممتازًا للمطورين المبتدئين الذين
يرغبون في دخول عالم برمجة الألعاب ثلاثية الأبعاد.
- لتمثيل البطاقات، سنستخدم مجسمات ثنائية الأبعاد (Quad)، يمكنك تطبيق أنسجة (Textures) مختلفة عليها.
- النتيجة الأهم هي عدد الحركات التي استغرقها اللاعب لحل اللغز.
- هذه المهارات هي حجر الأساس لأي مشروع أكبر وأكثر تعقيدًا.
1. إعداد المشروع وبناء اللعبة
لإنشاء لعبة الذاكرة، سنقوم بإنشاء مشروع جديد على jMonkeyEngine.
الفكرة الرئيسية هي ترتيب مجموعة من البطاقات المقلوبة على شبكة.
كل بطاقتين تحملان صورة متطابقة. عندما ينقر اللاعب على بطاقتين، تنقلبان.
إذا كانتا متطابقتين، تبقىان مكشوفتين؛ وإلا، فإنهما تعودان إلى وضعها الأصلي بعد فترة وجيزة.
الهدف هو العثور على جميع الأزواج في أقل عدد من الحركات.
أولًا، تأكد من إنشاء مشروع jMonkeyEngine جديد باستخدام SDK.
الكود التالي هو الهيكل الأساسي لملف Main.java الخاص باللعبة.
Java
import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Quad;
import com.jme3.texture.Texture;
import com.jme3.asset.TextureKey;
public class MemoryGame extends SimpleApplication {
public static void main(String[] args) {
MemoryGame app = new MemoryGame();
app.start();
}
@Override
public void simpleInitApp() {
// يتم وضع كود إعداد اللعبة هنا
}
@Override
public void simpleUpdate(float tpf) {
// يتم وضع كود تحديث اللعبة هنا
}
}
--
2- الرسوميات والمدخلات
لتمثيل البطاقات، سنستخدم مجسمات ثنائية الأبعاد (Quad).
هذه المجسمات بسيطة ومثالية للرسوميات المسطحة. يمكنك تطبيق أنسجة
(Textures) مختلفة على هذه المجسمات لتمثيل صور البطاقات.
كل بطاقة سيكون لها نسيج لوجهها ونوع آخر من الأنسجة لظهرها (الجانب المقلوب).
للكشف عن نقرات اللاعب، سنستخدم نظام المدخلات الخاص بالمحرك.
يمكننا إعداد "listener" للاستماع إلى أحداث الماوس.
عندما ينقر اللاعب على مكان ما في الشاشة، يمكننا استخدام وظيفة raycasting
لتحديد أي بطاقة تم النقر عليها بالضبط.
لإنشاء البطاقات، سنقوم بإنشاء مجسمات ثنائية الأبعاد (Quads) وتطبيق الأنسجة عليها.
يجب أن يكون لديك ملفات صور البطاقات (card1.png, card2.png, etc.)
وملف صورة للجانب الخلفي للبطاقة (back.png) في مجلد assets/Textures الخاص بالمشروع.
Java
// في دالة simpleInitApp()
Quad quad = new Quad(1, 1); // إنشاء مجسم مربع
Geometry cardGeom = new Geometry("Card", quad);
Material cardMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
// تحميل نسيج الوجه الأمامي للبطاقة
Texture cardTexture = assetManager.loadTexture(new TextureKey("Textures/card1.png"));
cardMat.setTexture("ColorMap", cardTexture);
cardGeom.setMaterial(cardMat);
rootNode.attachChild(cardGeom);
--
3- منطق اللعبة وإدارة الحالة
- تخزين حالة البطاقات : سنستخدم مصفوفة (Array) أو قائمة لتخزين حالة كل بطاقة:
مقلوبة، مكشوفة، أو مطابقة. هذه المصفوفة ستمثل "ذاكرة" اللعبة.
- إدارة الحركات : سنحتاج إلى عداد للحركات. في كل مرة يقلب فيها اللاعب بطاقتين، سنزيد هذا العداد.
- المنطق الأساسي : عندما يقلب اللاعب أول بطاقة، سنقوم بتخزينها. عند قلب البطاقة الثانية، سنقارنها بالأولى.
- إذا كانتا متطابقتين : نغير حالتهما في المصفوفة إلى مطابقتان ونتركهما مكشوفتين.
- إذا كانتا غير متطابقتين : سنستخدم مؤقتًا (timer) لإعادتهما إلى وضع مقلوبة بعد ثانية واحدة.
** التعامل مع المدخلات (نقرات الماوس)
لاستقبال نقرات الماوس، سنقوم بإنشاء Input Listener
(مستمع للمدخلات) باستخدام ActionListener وAnalogListener.
Java
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.MouseButtonTrigger;
// في دالة simpleInitApp()
inputManager.addMapping("Click", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
inputManager.addListener(new ActionListener() {
@Override
public void onAction(String name, boolean isPressed, float tpf) {
if (name.equals("Click") && isPressed) {
// كود معالجة النقر
}
}
}, "Click");
--
4- حفظ النقاط والنتائج
لجعل اللعبة تنافسية، يجب أن نحفظ النقاط والنتائج. في هذه اللعبة، النتيجة الأهم
هي عدد الحركات التي استغرقها اللاعب لحل اللغز. كلما قل عدد الحركات، كانت النتيجة أفضل.
- آلية حفظ النقاط : يمكنك ببساطة استخدام متغير ثابت داخل اللعبة لتخزين عدد الحركات.
عند كل حركة جديدة، يتم زيادة هذا المتغير.
- عرض النتيجة : يمكن استخدام مكونات الواجهة الرسومية (GUI) الخاصة
بـ jMonkeyEngine لعرض عدد الحركات الحالي على الشاشة.
- حفظ أفضل نتيجة : يمكنك تخزين أفضل نتيجة في ملف نصي أو باستخدام
Shared Preferences (للتطبيقات على الأجهزة المحمولة) حتى تبقى محفوظة
بين جولات اللعب. عند بدء لعبة جديدة، يتم تحميل أفضل نتيجة سابقة ومقارنتها بالنتيجة الحالية.
لإضافة عداد للنقاط، سنستخدم متغيرًا بسيطًا ونظام عرض النصوص (GUI).
Java
import com.jme3.font.BitmapText;
import com.jme3.font.BitmapFont;
import com.jme3.ui.Picture;
// تعريف المتغيرات في بداية الكلاس
private int moves = 0;
private BitmapText movesText;
// في دالة simpleInitApp()
guiNode.detachAllChildren();
guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
movesText = new BitmapText(guiFont, false);
movesText.setSize(guiFont.getCharSet().getRenderedSize());
movesText.setText("الحركات: 0");
movesText.setLocalTranslation(10, settings.getHeight() - 10, 0);
guiNode.attachChild(movesText);
// عند كل حركة في اللعبة، قم بتحديث النص
public void updateMoves() {
moves++;
movesText.setText("الحركات: " + moves);
}
--
5- تهيئة البطاقات وتوزيعها
أول خطوة هي تهيئة البطاقات عشوائيًا وتوزيعها على الشاشة.
سنستخدم مصفوفة لتخزين المعلومات الخاصة بكل بطاقة (موقعها، حالتها، ومعرف زوجها).
Java
// تعريف المتغيرات في بداية الكلاس
private int[] cardValues; // مصفوفة قيم البطاقات
private Geometry[] cardGeometries; // مصفوفة المجسمات
private Material backMaterial; // نسيج الوجه الخلفي للبطاقة
private Geometry firstCard = null; // أول بطاقة يتم النقر عليها
private Geometry secondCard = null; // ثاني بطاقة يتم النقر عليها
private boolean isBusy = false; // لمنع النقر أثناء قلب البطاقات
// في دالة simpleInitApp()
// 1. إعداد قائمة قيم البطاقات (مثلاً 1-8 مرتين)
int[] values = {1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8};
// 2. خلط القيم عشوائياً
Random rand = new Random();
for (int i = 0; i < values.length; i++) {
int randomIndexToSwap = rand.nextInt(values.length);
int temp = values[randomIndexToSwap];
values[randomIndexToSwap] = values[i];
values[i] = temp;
}
this.cardValues = values;
this.cardGeometries = new Geometry[16]; // 16 بطاقة
// 3. تحميل نسيج الوجه الخلفي
backMaterial = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
backMaterial.setTexture("ColorMap", assetManager.loadTexture(new TextureKey("Textures/back.png")));
// 4. بناء البطاقات وتوزيعها في شبكة 4x4
float cardSize = 1.2f;
float spacing = 0.2f;
int index = 0;
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 4; col++) {
Quad quad = new Quad(cardSize, cardSize);
Geometry card = new Geometry("Card" + index, quad);
card.setMaterial(backMaterial); // عرض الوجه الخلفي
// تحديد موقع البطاقة على الشاشة
card.setLocalTranslation(col * (cardSize + spacing), row * (cardSize + spacing), 0);
card.setUserData("value", cardValues[index]);
rootNode.attachChild(card);
cardGeometries[index] = card;
index++;
}
}
--
6- منطق قلب البطاقات (Flip Logic)
هنا يأتي دور معالجة نقرات الماوس. سنقوم بقلب البطاقات وعرض نسيجها الصحيح، ثم مقارنتهما.
Java
// في ActionListener الخاص بالماوس
// داخل دالة onAction
if (name.equals("Click") && isPressed && !isBusy) {
// 1. تحديد البطاقة التي تم النقر عليها
CollisionResults results = new CollisionResults();
Vector2f click2d = inputManager.getCursorPosition();
Vector3f click3d = cam.getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 0f).clone();
Vector3f dir = cam.getWorldCoordinates(new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d).normalizeLocal();
Ray ray = new Ray(click3d, dir);
rootNode.collideWith(ray, results);
if (results.size() > 0) {
Geometry clickedCard = results.getClosestCollision().getGeometry();
// 2. منع النقر على نفس البطاقة أو بطاقة مكشوفة
if (clickedCard.getMaterial().equals(backMaterial) && clickedCard != firstCard) {
// 3. قلب البطاقة
int cardValue = clickedCard.getUserData("value");
Material newMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
newMat.setTexture("ColorMap", assetManager.loadTexture(new TextureKey("Textures/card" + cardValue + ".png")));
clickedCard.setMaterial(newMat);
// 4. إدارة حالة النقر
if (firstCard == null) {
firstCard = clickedCard;
} else {
secondCard = clickedCard;
updateMoves(); // تحديث عداد الحركات
isBusy = true; // منع النقر أثناء المقارنة
// 5. مقارنة البطاقتين
if (firstCard.getUserData("value").equals(secondCard.getUserData("value"))) {
// البطاقتان متطابقتان، نتركهما مكشوفتين
System.out.println("Match!");
isBusy = false;
firstCard = null;
secondCard = null;
} else {
// البطاقتان غير متطابقتين، إعادتهما إلى وضعها الأصلي بعد 1.5 ثانية
// استخدم مؤقتاً لتأخير التنفيذ
}
}
}
}
}
--
7. إكمال حلقة اللعبة
لجعل اللعبة تعمل بشكل كامل، يجب عليك إضافة كود "إعادة البطاقات"
في حالة عدم التطابق، والتحقق من انتهاء اللعبة.
** إعادة البطاقات :
في دالة simpleUpdate()، قم بإنشاء مؤقت بسيط.
إذا كانت البطاقتان غير متطابقتين، انتظر 1.5 ثانية ثم قم بإعادة تعيين مادة كل بطاقة
إلى backMaterial، ثم أعد تعيين firstCard وsecondCard إلى null وأعد isBusy إلى false.
** التحقق من الفوز :
- قم بإنشاء دالة تفحص ما إذا كانت جميع البطاقات قد تمت مطابقتها.
- إذا كان عدد البطاقات المطابقة يساوي 16، اعرض رسالة "تهانينا! لقد فزت في X حركة".
بهذا المقال التفصيلي تكون قد أكملت بناء لعبة ذاكرة وظيفية بالكامل.
الخاتمة
تُظهر لعبة الذاكرة مدى سهولة وقوة jMonkeyEngine في التعامل مع
أساسيات برمجة الألعاب. من خلال هذا المشروع البسيط، تمكنت من التعرف
على كيفية إدارة الرسوميات، والمدخلات، ومنطق اللعبة، بالإضافة إلى كيفية حفظ
ومقارنة النتائج. هذه المهارات هي حجر الأساس لأي مشروع أكبر وأكثر تعقيدًا.
هذه التجربة تمهد لك الطريق لبناء ألعاب أكثر إثارة، مثل ألعاب الأركيد أو الألغاز،
مما يجعلك مستعدًا بشكل أفضل للمشاريع المستقبلية.