القائمة الرئيسية

الصفحات

كيفية برمجة لعبة مطابقة العناصر Match-3 على LibGDX Java Android Studio

How to Code، Match-3 Game on LibGDX Java Android Studio،How to Code a Match-3 Game on LibGDX Java Android Studio، Match-4، Match-5، تطابق L-shape، تطابق T-shape، منطق المطابقة، LibGDX Screens، شاشة بداية اللعبة، شاشة Game Over، Game State Management، LibGDX Audio، تشغيل الصوت في الألعاب، موسيقى الخلفية، LibGDX Actions، Tweening، رسوم متحركة في الألعاب، سقوط العناصر، اختفاء العناصر، تشغيل لعبة LibGDX، اختبار لعبة أندرويد، بنية لعبة Match-3، فئة العنصر، فئة لوحة اللعب، LibGDX Game Board، إعداد LibGDX، مشروع أندرويد ستوديو، LibGDX Setup، LibGDX Match-3 Game، تطوير ألعاب أندرويد Java، إنشاء لعبة مطابقة العناصر، تصميم واجهة المستخدم في LibGDX، حفظ النقاط في الألعاب، Android Studio Game Development، برمجة ألعاب الجوال، طريقة عمل لعبة Match-3، LibGDX UI، Game Development Tutorial، LibGDX Scene2D، رسوم متحركة ألعاب، مؤثرات صوتية ألعاب، إدارة شاشات اللعبة، انشاء مطابقة العناصر (Match-3) باستخدام LibGDX (Java)، اندرويد ستوديو، كيفية برمجة لعبة مطابقة العناصر Match-3 على LibGDX Java Android Studio، تصميم واجهة المستخدم في LibGDX، إعداد LibGDX، LibGDX Game Board،




كيفية برمجة لعبة مطابقة العناصر Match-3 على LibGDX Java Android Studio



تُعد ألعاب مطابقة العناصر (Match-3) من أكثر أنواع الألعاب شعبية عل
ى الأجهزة المحمولة، بفضل بساطة آلياتها وإمكانياتها اللانهائية للتحدي والمتعة.
 في هذا المقال، سنقوم بإنشاء لعبة Match-3 احترافية من البداية باستخدام إطار 
عمل LibGDX القوي والمرن، والذي يتيح لنا تطوير الألعاب مرة واحدة ونشرها 
على منصات متعددة، بما في ذلك Android. سنركز على الجوانب الأساسية من
 تصميم واجهة المستخدم (UI) التفاعلية، منطق اللعبة الرئيسي
(مطابقة العناصر، الحركة، توليد العناصر الجديدة)، إضافة الرسوم المتحركة الجذابة،
 المؤثرات الصوتية، إدارة شاشات اللعبة المتعددة، والعناصر الخاصة، وصولاً إلى حفظ النقاط العالية.

* المتطلبات الأساسية:
Android Studio مثبت، معرفة أساسية بلغة Java،
فهم لمفاهيم البرمجة الكائنية التوجه (OOP).


خطوات بناء لعبة مطابقة العناصر (Match-3) باستخدام LibGDX (Java)



الخطوة 1: إعداد مشروع LibGDX في Android Studio وتهيئة بيئة العمل
أولاً، نحتاج إلى إعداد بيئة المشروع وتجهيزها للاعتمادات الإضافية.
1- تحميل LibGDX Project Setup Tool:
انتقل إلى الموقع الرسمي لـ LibGDX وقم بتحميل أداة الإعداد:
 https://libgdx.com/download.html
 (عادة ما تكون gdx-setup.jar).

2- تشغيل أداة الإعداد:
افتح موجه الأوامر (CMD/Terminal) وانتقل إلى المجلد الذي حفظت فيه gdx-setup.jar. ثم قم بتشغيله:
java -jar gdx-setup.jar
--

3- تكوين المشروع:

- Name: Match3Game
- Package: com.yourcompany.match3game 
(استبدل yourcompany باسم شركتك أو اسمك).
- Game Class: Match3Game
- Destination: اختر مجلدًا فارغًا حيث سيتم إنشاء المشروع.
- Android SDK: تأكد من أن المسار إلى Android SDK صحيح.
- Sub Projects: تأكد من تحديد Desktop و Android.
- Extensions: هنا نقطة مهمة: ستحتاج إلى تحديد freetype (لإدارة الخطوط بشكل أفضل) و
 box2d (حتى لو لم تستخدمها للفيزياء، فإنها غالبًا ما تُضمّن مع مشاريع LibGDX الأكبر وقد تحتاجها لاحقًا).
- انقر على "Generate".

4- استيراد المشروع إلى Android Studio:
افتح Android Studio، اختر Open an existing Android Studio project،
اذهب إلى المجلد الذي أنشأه gdx-setup.jar وحدده، 
انتظر حتى يقوم Android Studio بمزامنة مشروع Gradle.

5- تحديث build.gradle (اختياري لكن موصى به):
تحقق من ملف core/build.gradle وتأكد من إضافة الاعتمادات التالية ضمن قسم dependencies
 (يجب أن تكون قد أضيفت تلقائيًا إذا اخترتها في أداة الإعداد):
Gradle

// في ملف core/build.gradle
dependencies {
    // ... (الاعتمادات الموجودة)

    api "com.badlogicgames.gdx:gdx-freetype:$gdxVersion" // لإدارة الخطوط بشكل أفضل
    api "com.badlogicgames.gdx:gdx-box2d:$gdxVersion" // (إذا اخترتها، ليست ضرورية لـ Match-3 بحد ذاتها)
}
--

بعد أي تعديلات، قم بمزامنة مشروع Gradle بالنقر على "Sync Now" إذا ظهرت لك.

الخطوة 2: إدارة الأصول (Assets) والأصوات والـ Skin
تُعد الأصول المرئية والصوتية أساسية لجعل اللعبة جذابة.

1- صور العناصر (Gems/Tiles):
- صمم 5 صور مختلفة (أو أكثر، حسب NUM_ELEMENT_TYPES).
- اجعل حجم كل صورة 64x64 بكسل (أو الحجم الذي حددته لـ GameElement.SIZE).
- احفظها بأسماء مثل gem0.png, gem1.png, gem2.png, gem3.png, gem4.png.

2- صور العناصر الخاصة:
- إذا أردت عناصر خاصة مثل القنابل، قم بتصميم صور لها.
مثال: bomb_gem.png, color_bomb_gem.png, line_clearer_gem.png.

3- ملفات الصوت:
- احصل على ملفات صوتية بصيغ مثل .mp3 أو .ogg أو .wav.
مثال: match_sound.ogg (عند المطابقة)، swap_sound.ogg (عند التبديل)،
 background_music.ogg (موسيقى خلفية).

4- Skin لـ Scene2D.ui:
لتشغيل واجهة المستخدم المتقدمة، تحتاج إلى "Skin" يحتوي على الأنسجة 
والخطوط وأنماط الأزرار. يمكنك استخدام uiskin.json الافتراضي لـ LibGDX أو إنشاء واحد خاص بك.
الخيار الأسهل: قم بتحميل uiskin.json, uiskin.atlas, uiskin.png من أمثلة
 LibGDX (ابحث عن "LibGDX uiskin") أو من مستودع LibGDX GitHub.

5- وضع جميع الأصول:
ضع جميع هذه الملفات (الصور، الأصوات، ملفات الـ Skin) في مجلد
 android/assets داخل مشروعك. هذا هو المجلد الذي يبحث فيه LibGDX عن الأصول.

الخطوة 3: تصميم فئات اللعبة الأساسية (العناصر ولوحة اللعب)
سنقوم بإنشاء الفئات الأساسية للعبة، مع الأخذ في الاعتبار الرسوم المتحركة والتفاعل المستقبلي.

أ. فئة GameElement (العنصر)
هذه الفئة ستمثل كل عنصر في لوحة اللعبة، وستدعم الرسوم المتحركة
 بفضل وراثتها من Actor من مكتبة Scene2D.
Java



// في مجلد core/src/com/yourcompany/match3game/
package com.yourcompany.match3game;

import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.scenes.scene2d.Actor; // هام: للرسوم المتحركة والأكشنز
import com.badlogic.gdx.scenes.scene2d.actions.Actions;

public class GameElement extends Actor {
    public static final float SIZE = 64; // حجم العنصر بالبكسل

    private int type; // نوع العنصر (مثال: 0 لأحمر، 1 لأزرق، إلخ.)
    private int logicalRow, logicalCol; // موضع العنصر المنطقي في اللوحة (غير متحرك)
    private Texture texture; // نسيج (صورة) العنصر
    private boolean isSelected; // هل العنصر محدد حالياً؟

    public enum ElementSpecialType {
        NONE, BOMB, COLOR_BOMB, LINE_CLEARER // أنواع خاصة للعناصر
    }
    private ElementSpecialType specialType;

    // Constructor للعناصر العادية
    public GameElement(int type, int row, int col, Texture texture) {
        this(type, row, col, texture, ElementSpecialType.NONE);
    }

    // Constructor للعناصر الخاصة
    public GameElement(int type, int row, int col, Texture texture, ElementSpecialType specialType) {
        this.type = type;
        this.logicalRow = row;
        this.logicalCol = col;
        this.texture = texture;
        this.isSelected = false;
        this.specialType = specialType;

        // تعيين حجم وموضع Actor الأولي (موضع الرسم)
        setSize(SIZE, SIZE);
        setPosition(col * SIZE, row * SIZE);
    }

    // لتحديث الموضع المنطقي للعنصر بعد الحركة
    public void setLogicalPosition(int row, int col) {
        this.logicalRow = row;
        this.logicalCol = col;
    }

    // بدء رسوم متحركة لتحريك العنصر إلى موضع جديد
    public void startMoveTo(int newRow, int newCol, float duration) {
        float targetX = newCol * SIZE;
        float targetY = newRow * SIZE;

        // إزالة أي Actions سابقة لمنع التداخل
        clearActions();
        // إضافة Action للحركة
        addAction(Actions.sequence(
            Actions.moveTo(targetX, targetY, duration, Interpolation.sineOut), // حركة سلسة
            Actions.run(() -> { // عندما تنتهي الحركة
                isMoving = false;
                // بعد انتهاء الحركة، تحديث الموضع المنطقي
                this.setLogicalPosition(newRow, newCol);
            })
        ));
        isMoving = true;
    }

    public void startFadeOut(float duration) {
        clearActions();
        addAction(Actions.sequence(
            Actions.fadeOut(duration)
        ));
    }

    // رسم العنصر
    @Override
    public void draw(SpriteBatch batch, float parentAlpha) {
        // نستخدم getX() و getY() من Actor لأنها تعكس الموضع المتحرك
        // نستخدم getColor().a للشفافية إذا كان العنصر يتلاشى
        batch.setColor(getColor().r, getColor().g, getColor().b, getColor().a * parentAlpha);
        batch.draw(texture, getX(), getY(), SIZE, SIZE);
        batch.setColor(1, 1, 1, 1); // إعادة تعيين اللون الافتراضي

        // رسم إطار التحديد إذا كان العنصر محدداً
 batch.draw(texture, drawX, drawY, SIZE, SIZE);
        if (isSelected) {
                 // رسم إطار التحديد، ستحتاج إلى Texture للإطار
            // مثال بسيط: رسم مربع أبيض شبه شفاف فوق العنصر
            // (يجب أن يكون لديك texture للاطار أو ترسم مستطيل بـ ShapeRenderer)
            // batch.setColor(1, 1, 1, 0.5f); // نصف شفاف أبيض
            // batch.draw(selectionFrameTexture, drawX, drawY, SIZE, SIZE);
            // batch.setColor(1, 1, 1, 1); // إعادة اللون الأصلي
        }
    }

    // يجب استدعاء هذا في حلقة update

    // يتم استدعاء act(delta) تلقائياً بواسطة الـ Stage
    // وهو المسؤول عن تحديث الـ Actions
    @Override
    public void act(float delta) {
        super.act(delta);
    }

    // Getters
    public int getType() { return type; }
    public int getLogicalRow() { return logicalRow; }
    public int getLogicalCol() { return logicalCol; }
    public ElementSpecialType getSpecialType() { return specialType; }
    public boolean isSelected() { return isSelected; }

    // Setters
    public void setSelected(boolean selected) { this.isSelected = selected; }
    public void setType(int type, Texture texture) {
        this.type = type;
        this.texture = texture;
    }
    public void setSpecialType(ElementSpecialType specialType, Texture texture) {
        this.specialType = specialType;
        this.texture = texture;
    }
}


--

ب. فئة ScoreManager (إدارة النقاط)
هذه الفئة ستقوم بإدارة النقاط الحالية وحفظ أعلى نقاط اللاعب باستخدام Preferences.
Java




// في مجلد core/src/com/yourcompany/match3game/
package com.yourcompany.match3game;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Preferences;

public class ScoreManager {
    private static final String PREFS_NAME = "Match3GamePrefs";
    private static final String HIGH_SCORE_KEY = "highScore";

    private Preferences prefs;
    private int currentScore;
    private int highScore;

    public ScoreManager() {
        prefs = Gdx.app.getPreferences(PREFS_NAME);
        highScore = prefs.getInteger(HIGH_SCORE_KEY, 0);
        currentScore = 0;
    }

    public void addScore(int points) {
        currentScore += points;
        if (currentScore > highScore) {
            highScore = currentScore;
            prefs.putInteger(HIGH_SCORE_KEY, highScore);
            prefs.flush(); // حفظ التغييرات على الفور
        }
    }

    public void resetScore() {
        currentScore = 0;
    }

    public int getCurrentScore() {
        return currentScore;
    }

    public int getHighScore() {
        return highScore;
    }

    // يمكنك إضافة دالة لحفظ النقاط يدويا (ليست ضرورية هنا لأنها تحفظ عند تجاوز الرقم القياسي)
    // public void saveScore() {
    //     prefs.putInteger(HIGH_SCORE_KEY, highScore);
    //     prefs.flush();
    // }
}



--

ج. فئة GameBoard (لوحة اللعب)




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




// في مجلد core/src/com/yourcompany/match3game/
package com.yourcompany.match3game;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.Timer;
import com.badlogic.gdx.audio.Sound;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

public class GameBoard implements Disposable {
    public static final int COLS = 8;
    public static final int ROWS = 8;
    public static final int NUM_ELEMENT_TYPES = 5; // عدد أنواع العناصر العادية

    private GameElement[][] board;
    private Texture[] elementTextures; // أنسجة العناصر العادية
    private Texture bombTexture, colorBombTexture, lineClearerTexture; // أنسجة العناصر الخاصة
    private Random random;
    private GameElement selectedElement1; // العنصر الأول المحدد باللمس
    private GameElement selectedElement2; // العنصر الثاني الذي تم التبديل معه

    private ScoreManager scoreManager;
    private Stage stage; // Stage لإدارة GameElements كـ Actors

    private Sound matchSound, swapSound; // المؤثرات الصوتية

    // حالات اللعبة لمنع التفاعل أثناء الرسوم المتحركة ومعالجة اللوحة
    public enum GameState {
        WAITING_FOR_INPUT, // انتظار تحديد العنصر الأول
        ELEMENT_SELECTED,  // تم تحديد العنصر الأول، انتظار الثاني أو السحب
        SWAPPING,          // عناصر تتبادل الأماكن
        PROCESSING_BOARD,  // معالجة التطابقات، السقوط، التوليد
        GAME_OVER          // اللعبة انتهت
    }
    public GameState currentGameState; // جعلها عامة للوصول من GameScreen

    public GameBoard(ScoreManager scoreManager, Stage stage, Sound matchSound, Sound swapSound) {
        this.scoreManager = scoreManager;
        this.stage = stage;
        this.matchSound = matchSound;
        this.swapSound = swapSound;
        board = new GameElement[ROWS][COLS];
        elementTextures = new Texture[NUM_ELEMENT_TYPES];
        random = new Random();

        // تحميل أنسجة العناصر العادية (gem0.png, gem1.png, ...)
        for (int i = 0; i < NUM_ELEMENT_TYPES; i++) {
            elementTextures[i] = new Texture(Gdx.files.internal("gem" + i + ".png"));
        }
        // تحميل أنسجة العناصر الخاصة
        bombTexture = new Texture(Gdx.files.internal("bomb_gem.png"));
        colorBombTexture = new Texture(Gdx.files.internal("color_bomb_gem.png"));
        lineClearerTexture = new Texture(Gdx.files.internal("line_clearer_gem.png"));

        initializeBoard();
        currentGameState = GameState.WAITING_FOR_INPUT;
    }

    private void initializeBoard() {
        // توليد عناصر اللوحة الابتدائية، مع التأكد من عدم وجود تطابقات في البداية
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                int type;
                do {
                    type = random.nextInt(NUM_ELEMENT_TYPES);
                } while (isStartingMatch(r, c, type));
                GameElement newElement = new GameElement(type, r, c, elementTextures[type]);
                board[r][c] = newElement;
                stage.addActor(newElement); // إضافة العنصر كـ Actor إلى الـ Stage
            }
        }
    }

    // يتحقق مما إذا كان هناك تطابق 3 عناصر في البداية لتجنب ذلك
    private boolean isStartingMatch(int row, int col, int type) {
        // تحقق أفقي
        if (col >= 2 && board[row][col - 1] != null && board[row][col - 2] != null &&
            board[row][col - 1].getType() == type && board[row][col - 2].getType() == type) {
            return true;
        }
        // تحقق عمودي
        if (row >= 2 && board[row - 1][col] != null && board[row - 2][col] != null &&
            board[row - 1][col].getType() == type && board[row - 2][col].getType() == type) {
            return true;
        }
        return false;
    }

    public void update(float delta) {
        // منطق اللعبة يعتمد على الحالة الحالية
        switch (currentGameState) {
            case SWAPPING:
                // انتظر انتهاء الرسوم المتحركة للتبديل
                if (!isAnyElementMoving()) {
                    currentGameState = GameState.PROCESSING_BOARD;
                    // بعد انتهاء التبديل، تحقق من وجود تطابقات جديدة
                    if (findMatches().isEmpty()) {
                        // إذا لم يكن هناك تطابق، قم بالتبديل مرة أخرى لإرجاع العناصر
                        swapElementsAnimation(selectedElement1, selectedElement2, 0.2f, true);
                        // يتم إعادة تعيين الحالة إلى WAITING_FOR_INPUT بعد انتهاء الحركة العكسية
                    } else {
                        // هناك تطابقات، ابدأ المعالجة
                        processMatches();
                    }
                    // إعادة تعيين التحديد بغض النظر عن التبديل
                    selectedElement1.setSelected(false);
                    if (selectedElement2 != null) selectedElement2.setSelected(false);
                    selectedElement1 = null;
                    selectedElement2 = null;
                }
                break;
            case PROCESSING_BOARD:
                // منطق معالجة اللوحة (إزالة، سقوط، توليد)
                if (!isAnyElementMoving()) { // تأكد من عدم وجود عناصر متحركة
                    List<GameElement> matchedElements = findMatches();
                    if (!matchedElements.isEmpty()) {
                        removeMatchedElements(matchedElements);
                        // تأخير بسيط قبل السقوط بعد اختفاء العناصر
                        Timer.schedule(new Timer.Task() {
                            @Override
                            public void run() {
                                fillEmptySpaces(); // سحب العناصر لأسفل وملء الفراغات
                                // لا نغير الحالة هنا، لأن fillEmptySpaces ستبدأ حركات السقوط
                                // وسنظل في PROCESSING_BOARD حتى تتوقف كل الحركات
                            }
                        }, 0.3f);
                    } else {
                        // لا توجد المزيد من التطابقات، العودة لحالة الانتظار
                        currentGameState = GameState.WAITING_FOR_INPUT;
                    }
                }
                break;
            case WAITING_FOR_INPUT:
            case ELEMENT_SELECTED:
            case GAME_OVER:
                // لا شيء يحدث في هذه الحالات في دالة update
                break;
        }
    }

    // يتعامل مع النقر الأولي لتحديد العنصر
    public void handleTouchDown(float screenX, float screenY) {
        if (currentGameState != GameState.WAITING_FOR_INPUT && currentGameState != GameState.ELEMENT_SELECTED) {
            return; // لا تعالج المدخلات إذا كانت اللعبة في حالة معالجة أو تبديل
        }

        float touchX = screenX;
        float touchY = Gdx.graphics.getHeight() - screenY; // تحويل إحداثيات الشاشة إلى إحداثيات عالم اللعبة

        int touchedCol = (int) (touchX / GameElement.SIZE);
        int touchedRow = (int) (touchY / GameElement.SIZE);

        if (touchedRow >= 0 && touchedRow < ROWS && touchedCol >= 0 && touchedCol < COLS) {
            GameElement touchedElement = board[touchedRow][touchedCol];

            if (currentGameState == GameState.WAITING_FOR_INPUT) {
                selectedElement1 = touchedElement;
                selectedElement1.setSelected(true);
                currentGameState = GameState.ELEMENT_SELECTED;
            } else if (currentGameState == GameState.ELEMENT_SELECTED) {
                // إذا تم النقر على عنصر آخر غير المحدد (دون سحب)، قم بتحديده بدلاً من السابق
                selectedElement1.setSelected(false);
                selectedElement1 = touchedElement;
                selectedElement1.setSelected(true);
            }
        }
    }

    // يتعامل مع إيماءة السحب (Fling) لتبديل العناصر
    public void handleFling(float velocityX, float velocityY) {
        if (currentGameState != GameState.ELEMENT_SELECTED || selectedElement1 == null) {
            return; // يجب أن يكون هناك عنصر محدد بالفعل
        }

        int targetRow = selectedElement1.getLogicalRow();
        int targetCol = selectedElement1.getLogicalCol();

        // تحديد اتجاه السحب (الأكبر في القيمة المطلقة يحدد الاتجاه)
        if (Math.abs(velocityX) > Math.abs(velocityY)) { // سحب أفقي
            if (velocityX > 0) { // لليمين
                targetCol++;
            } else { // لليسار
                targetCol--;
            }
        } else { // سحب عمودي
            if (velocityY > 0) { // للأسفل (في نظام الإحداثيات الشاشي، الأسفل يعني Y أكبر)
                targetRow--; // في نظام إحداثيات اللعبة، Y أكبر تعني صف أقل
            } else { // للأعلى
                targetRow++;
            }
        }

        // التأكد من أن الموضع المستهدف ضمن حدود اللوحة
        if (targetRow >= 0 && targetRow < ROWS && targetCol >= 0 && targetCol < COLS) {
            selectedElement2 = board[targetRow][targetCol];
            if (selectedElement2 != null && areAdjacent(selectedElement1, selectedElement2)) {
                swapElementsAnimation(selectedElement1, selectedElement2, 0.2f, false);
                currentGameState = GameState.SWAPPING; // الدخول في حالة التبديل
            } else {
                // إذا لم يكن متجاوراً أو كان فارغاً، قم بإلغاء التحديد
                selectedElement1.setSelected(false);
                selectedElement1 = null;
                selectedElement2 = null;
                currentGameState = GameState.WAITING_FOR_INPUT;
            }
        } else {
            // سحب خارج حدود اللوحة، إلغاء التحديد
            selectedElement1.setSelected(false);
            selectedElement1 = null;
            selectedElement2 = null;
            currentGameState = GameState.WAITting_FOR_INPUT;
        }
    }

    // تحقق مما إذا كان العنصران متجاورين أفقياً أو عمودياً فقط
    private boolean areAdjacent(GameElement e1, GameElement e2) {
        return (e1.getLogicalRow() == e2.getLogicalRow() && Math.abs(e1.getLogicalCol() - e2.getLogicalCol()) == 1) ||
               (e1.getLogicalCol() == e2.getLogicalCol() && Math.abs(e1.getLogicalRow() - e2.getLogicalRow()) == 1);
    }

    // تبديل العناصر مع رسوم متحركة
    private void swapElementsAnimation(GameElement e1, GameElement e2, float duration, boolean isReverse) {
        swapSound.play(0.7f); // تشغيل صوت التبديل

        // تبديل المراجع في المصفوفة المنطقية
        board[e1.getLogicalRow()][e1.getLogicalCol()] = e2;
        board[e2.getLogicalRow()][e2.getLogicalCol()] = e1;

        // بدء الرسوم المتحركة للحركة
        e1.startMoveTo(e2.getLogicalRow(), e2.getLogicalCol(), duration);
        e2.startMoveTo(e1.getLogicalRow(), e1.getLogicalCol(), duration);

        // تحديث المواقع المنطقية فوراً بعد بدء الحركة
        int tempRow1 = e1.getLogicalRow();
        int tempCol1 = e1.getLogicalCol();
        e1.setLogicalPosition(e2.getLogicalRow(), e2.getLogicalCol());
        e2.setLogicalPosition(tempRow1, tempCol1);

        if (isReverse) {
            // إذا كان التبديل عكسيا (لم يكن هناك تطابق)، أعد الحالة إلى WAITING_FOR_INPUT بعد الانتهاء
            Timer.schedule(new Timer.Task() {
                @Override
                public void run() {
                    currentGameState = GameState.WAITING_FOR_INPUT;
                    selectedElement1.setSelected(false);
                    selectedElement2.setSelected(false);
                    selectedElement1 = null;
                    selectedElement2 = null;
                }
            }, duration);
        }
    }

    // معالجة التطابقات المتتالية (cascading matches)
    private void processMatches() {
        List<GameElement> matchedElements;
        do {
            matchedElements = findMatches();
            if (!matchedElements.isEmpty()) {
                removeMatchedElements(matchedElements);
                // تأخير بسيط قبل السقوط للسماح للرسوم المتحركة بالظهور
                Timer.schedule(new Timer.Task() {
                    @Override
                    public void run() {
                        fillEmptySpaces(); // سحب العناصر وملء الفراغات
                    }
                }, 0.3f);
            }
        } while (!matchedElements.isEmpty() || isAnyElementMoving()); // استمر طالما توجد تطابقات أو حركات
    }

    // البحث عن جميع التطابقات (3+، L-shape, T-shape)
    private List<GameElement> findMatches() {
        Set<GameElement> allMatches = new HashSet<>(); // استخدام Set لتجنب التكرار

        // 1. البحث عن تطابقات أفقية (3+, 4+, 5+)
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS - 2; c++) {
                if (board[r][c] != null &&
                    board[r][c+1] != null &&
                    board[r][c+2] != null &&
                    board[r][c].getType() == board[r][c+1].getType() &&
                    board[r][c].getType() == board[r][c+2].getType()) {

                    List<GameElement> currentHorizontalMatch = new ArrayList<>();
                    currentHorizontalMatch.add(board[r][c]);
                    currentHorizontalMatch.add(board[r][c+1]);
                    currentHorizontalMatch.add(board[r][c+2]);

                    for (int k = c + 3; k < COLS; k++) {
                        if (board[r][k] != null && board[r][k].getType() == board[r][c].getType()) {
                            currentHorizontalMatch.add(board[r][k]);
                        } else {
                            break;
                        }
                    }
                    allMatches.addAll(currentHorizontalMatch);
                }
            }
        }

        // 2. البحث عن تطابقات عمودية (3+, 4+, 5+)
        for (int c = 0; c < COLS; c++) {
            for (int r = 0; r < ROWS - 2; r++) {
                if (board[r][c] != null &&
                    board[r+1][c] != null &&
                    board[r+2][c] != null &&
                    board[r][c].getType() == board[r+1][c].getType() &&
                    board[r][c].getType() == board[r+2][c].getType()) {

                    List<GameElement> currentVerticalMatch = new ArrayList<>();
                    currentVerticalMatch.add(board[r][c]);
                    currentVerticalMatch.add(board[r+1][c]);
                    currentVerticalMatch.add(board[r+2][c]);

                    for (int k = r + 3; k < ROWS; k++) {
                        if (board[k][c] != null && board[k][c].getType() == board[r][c].getType()) {
                            currentVerticalMatch.add(board[k][c]);
                        } else {
                            break;
                        }
                    }
                    allMatches.addAll(currentVerticalMatch);
                }
            }
        }

        // 3. البحث عن تطابقات على شكل حرف L أو T (أكثر تعقيداً، هذا مثال مبسط)
        // يتم ذلك عن طريق التحقق من تقاطع التطابقات الأفقية والعمودية.
        // يمكن أن تزيد من تعقيد المنطق بشكل كبير. لأغراض التوضيح:
        // تحقق من كل عنصر إذا كان جزءًا من تطابق أفقي وتطابق عمودي في نفس الوقت
        // (وهو ما يتم تضمينه بالفعل عن طريق إضافة كل من التطابقات الأفقية والعمودية إلى نفس الـ Set)

        return new ArrayList<>(allMatches);
    }

    private void removeMatchedElements(List<GameElement> matches) {
        int scoreToAdd = 0;
        if (!matches.isEmpty()) {
            matchSound.play(0.7f); // تشغيل صوت المطابقة
        }

        // منطق توليد العناصر الخاصة قبل إزالة العناصر المطابقة
        // يمكن تحسين هذا المنطق ليكون أكثر تعقيداً بناءً على حجم أو شكل التطابق
        GameElement specialElementToCreate = null;
        int specialElementRow = -1, specialElementCol = -1;

        if (matches.size() >= 4) {
            // اختر موقع العنصر الخاص ليكون في وسط التطابق أو في مكان محدد
            // لأغراض التبسيط، سنختار أول عنصر في القائمة
            specialElementRow = matches.get(0).getLogicalRow();
            specialElementCol = matches.get(0).getLogicalCol();

            if (matches.size() == 4) {
                // Match-4: توليد قنبلة (تزيل 3x3)
                specialElementToCreate = new GameElement(matches.get(0).getType(), specialElementRow, specialElementCol, bombTexture, GameElement.ElementSpecialType.BOMB);
            } else if (matches.size() >= 5) {
                // Match-5 أو أكثر: توليد قنبلة ألوان (تزيل جميع العناصر من نفس النوع)
                specialElementToCreate = new GameElement(matches.get(0).getType(), specialElementRow, specialElementCol, colorBombTexture, GameElement.ElementSpecialType.COLOR_BOMB);
            }
            // يمكن إضافة منطق لـ LINE_CLEARER من تطابق 4 في صف/عمود مستقيم، أو L/T
        }

        // إزالة العناصر المطابقة من اللوحة وبدء رسوم متحركة للتلاشي
        for (GameElement element : matches) {
            scoreToAdd += 10;
            board[element.getLogicalRow()][element.getLogicalCol()] = null; // تصفير العنصر في المصفوفة
            element.startFadeOut(0.2f); // بدء رسوم متحركة للاختفاء
            Timer.schedule(new Timer.Task() {
                @Override
                public void run() {
                    element.remove(); // إزالة الـ Actor من الـ Stage بعد انتهاء التلاشي
                }
            }, 0.2f);
        }
        scoreManager.addScore(scoreToAdd);

        // إضافة العنصر الخاص الجديد بعد إزالة العناصر المطابقة
        if (specialElementToCreate != null) {
            board[specialElementRow][specialElementCol] = specialElementToCreate;
            stage.addActor(specialElementToCreate);
        }
    }

    private void fillEmptySpaces() {
        // سحب العناصر لأسفل لملء الفراغات
        boolean elementsMoved = false;
        for (int c = 0; c < COLS; c++) {
            for (int r = 0; r < ROWS; r++) {
                if (board[r][c] == null) {
                    // ابحث عن عنصر فوق هذا الفراغ
                    for (int k = r + 1; k < ROWS; k++) {
                        if (board[k][c] != null) {
                            GameElement fallingElement = board[k][c];
                            board[r][c] = fallingElement;
                            board[k][c] = null;
                            fallingElement.startMoveTo(r, c, 0.3f); // رسوم متحركة للسقوط
                            fallingElement.setLogicalPosition(r, c); // تحديث الموضع المنطقي
                            elementsMoved = true;
                            break;
                        }
                    }
                }
            }
        }

        // توليد عناصر جديدة لملء الفراغات في الأعلى (مع الرسوم المتحركة)
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                if (board[r][c] == null) {
                    int type = random.nextInt(NUM_ELEMENT_TYPES);
                    GameElement newElement = new GameElement(type, r, c, elementTextures[type]);
                    // لجعلها تسقط من خارج الشاشة
                    newElement.setY((ROWS + 1) * GameElement.SIZE); // ابدأ من فوق اللوحة
                    newElement.setLogicalPosition(r, c);
                    stage.addActor(newElement);
                    newElement.startMoveTo(r, c, 0.5f); // رسوم متحركة للسقوط للجديد
                    elementsMoved = true;
                }
            }
        }

        // إذا كانت هناك عناصر تحركت، سننتظر حتى تتوقف جميع الحركات ثم نعود لحالة WAITING_FOR_INPUT
        if (elementsMoved) {
            // سيتم التعامل مع الانتقال إلى WAITING_FOR_INPUT في update() بعد توقف كل الحركات
        } else if (!isAnyElementMoving()) {
            // لا توجد حركات جديدة ولا حركات قيد التنفيذ
            currentGameState = GameState.WAITING_FOR_INPUT;
        }
    }

    // تتحقق مما إذا كان أي عنصر في اللوحة يتحرك حالياً
    private boolean isAnyElementMoving() {
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                if (board[r][c] != null && board[r][c].hasActions()) {
                    return true;
                }
            }
        }
        return false;
    }

    // تفعيل العناصر الخاصة (يتم استدعاؤها بعد التبديل إذا كان أحد العناصر المبدلة خاصًا)
    private void activateSpecialElement(GameElement specialElement) {
        List<GameElement> affectedElements = new ArrayList<>();
        switch (specialElement.getSpecialType()) {
            case BOMB:
                // إزالة 3x3 حول القنبلة
                for (int r = specialElement.getLogicalRow() - 1; r <= specialElement.getLogicalRow() + 1; r++) {
                    for (int c = specialElement.getLogicalCol() - 1; c <= specialElement.getLogicalCol() + 1; c++) {
                        if (r >= 0 && r < ROWS && c >= 0 && c < COLS && board[r][c] != null) {
                            affectedElements.add(board[r][c]);
                        }
                    }
                }
                break;
            case COLOR_BOMB:
                // إزالة جميع العناصر من نفس نوع العنصر الذي تم تبديل قنبلة الألوان معه
                int typeToClear = -1;
                // يجب تحديد نوع العنصر الذي تم تبديله مع قنبلة الألوان
                if (selectedElement2 != null && selectedElement2 != specialElement) { // العنصر الذي تم تبديله مع القنبلة
                    typeToClear = selectedElement2.getType();
                } else if (selectedElement1 != null && selectedElement1 != specialElement) {
                    typeToClear = selectedElement1.getType();
                } else {
                    // إذا لم يتم تبديلها مع عنصر آخر، يمكن اختيار نوع عشوائي أو الأكثر شيوعًا
                    typeToClear = random.nextInt(NUM_ELEMENT_TYPES);
                }

                for (int r = 0; r < ROWS; r++) {
                    for (int c = 0; c < COLS; c++) {
                        if (board[r][c] != null && board[r][c].getType() == typeToClear) {
                            affectedElements.add(board[r][c]);
                        }
                    }
                }
                break;
            case LINE_CLEARER:
                // إزالة الصف والعمود بالكامل
                for (int c = 0; c < COLS; c++) { // العمود
                    if (board[specialElement.getLogicalRow()][c] != null) {
                        affectedElements.add(board[specialElement.getLogicalRow()][c]);
                    }
                }
                for (int r = 0; r < ROWS; r++) { // الصف
                    if (board[r][specialElement.getLogicalCol()] != null) {
                        affectedElements.add(board[r][specialElement.getLogicalCol()]);
                    }
                }
                break;
            case NONE:
                break;
        }

        // إزالة العنصر الخاص نفسه من اللوحة (بعد إضافة تأثيراته)
        board[specialElement.getLogicalRow()][specialElement.getLogicalCol()] = null;
        specialElement.remove(); // إزالة الـ Actor من الـ Stage

        // إزالة العناصر المتأثرة
        removeMatchedElements(affectedElements);
        currentGameState = GameState.PROCESSING_BOARD; // للبدء في سحب العناصر وملء الفراغات
    }

    @Override
    public void dispose() {
        for (Texture texture : elementTextures) {
            texture.dispose();
        }
        bombTexture.dispose();
        colorBombTexture.dispose();
        lineClearerTexture.dispose();
        // الـ Stage والـ Actors الموجودة فيها ستتم التخلص منها بواسطة Match3Game
    }
}



--

الخطوة 4: تصميم الشاشات الرئيسية للعبة




سنقوم بإنشاء فئات منفصلة لكل شاشة من شاشات اللعبة
 (القائمة الرئيسية، شاشة اللعب، شاشة نهاية اللعبة) باستخدام واجهة Screen من LibGDX.

أ. فئة Match3Game (فئة اللعبة الرئيسية - Game Class)
هذه الفئة ستكون نقطة الدخول الرئيسية للعبة وستدير الانتقال بين الشاشات المختلفة.
Java




// في مجلد core/src/com/yourcompany/match3game/
package com.yourcompany.match3game;

import com.badlogic.gdx.Game; // هام: يرث من Game الآن
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound;

public class Match3Game extends Game {
    // الموارد المشتركة التي تحتاجها جميع الشاشات
    public SpriteBatch batch;
    public BitmapFont font;
    public ScoreManager scoreManager;
    public Music backgroundMusic;
    public Sound matchSound;
    public Sound swapSound;

    @Override
    public void create() {
        batch = new SpriteBatch();
        font = new BitmapFont();
        font.setColor(Color.WHITE); // لون الخط
        font.getData().setScale(1.5f); // حجم الخط

        scoreManager = new ScoreManager();

        // تحميل الصوتيات
        backgroundMusic = Gdx.audio.newMusic(Gdx.files.internal("background_music.ogg"));
        matchSound = Gdx.audio.newSound(Gdx.files.internal("match_sound.ogg"));
        swapSound = Gdx.audio.newSound(Gdx.files.internal("swap_sound.ogg"));

        backgroundMusic.setLooping(true); // تكرار الموسيقى
        backgroundMusic.setVolume(0.5f); // ضبط مستوى الصوت
        backgroundMusic.play(); // تشغيل الموسيقى

        // تعيين الشاشة الأولى عند بدء اللعبة (شاشة القائمة الرئيسية)
        setScreen(new MainMenuScreen(this));
    }

    @Override
    public void dispose() {
        super.dispose(); // مهم جداً لاستدعاء dispose للشاشة الحالية
        batch.dispose();
        font.dispose();
        backgroundMusic.dispose();
        matchSound.dispose();
        swapSound.dispose();
    }
}



--

ب. فئة MainMenuScreen.java (شاشة القائمة الرئيسية)
Java




// في مجلد core/src/com/yourcompany/match3game/
package com.yourcompany.match3game;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.utils.ScreenUtils;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;

public class MainMenuScreen implements Screen {
    final Match3Game game;
    OrthographicCamera camera;
    Stage stage; // Stage للـ UI
    Skin skin; // Skin لـ Scene2D.ui

    public MainMenuScreen(final Match3Game game) {
        this.game = game;
        camera = new OrthographicCamera();
        // حجم الكاميرا لشاشة القائمة (يمكن أن يكون مختلفًا عن شاشة اللعب)
        camera.setToOrtho(false, 800, 480);

        stage = new Stage(new com.badlogic.gdx.utils.viewport.FitViewport(camera.viewportWidth, camera.viewportHeight, camera), game.batch);
        Gdx.input.setInputProcessor(stage); // هام: لكي تستقبل الـ Stage المدخلات

        skin = new Skin(Gdx.files.internal("uiskin.json")); // تحميل الـ Skin

        // إنشاء زر "Start Game"
        TextButton startGameButton = new TextButton("Start Game", skin);
        startGameButton.setSize(200, 60);
        // وضعه في منتصف الشاشة تقريباً
        startGameButton.setPosition(camera.viewportWidth / 2 - startGameButton.getWidth() / 2, camera.viewportHeight / 2 - 30);

        // إضافة مستمع للأحداث للزر
        startGameButton.addListener(new ChangeListener() {
            @Override
            public void changed(ChangeEvent event, com.badlogic.gdx.scenes.scene2d.Actor actor) {
                game.setScreen(new GameScreen(game)); // الانتقال إلى شاشة اللعب
                dispose(); // التخلص من موارد هذه الشاشة
            }
        });

        stage.addActor(startGameButton);
    }

    @Override
    public void show() {
        // يتم استدعاؤه عندما تصبح هذه الشاشة هي الشاشة النشطة
    }

    @Override
    public void render(float delta) {
        ScreenUtils.clear(0.0f, 0.0f, 0.2f, 1); // خلفية زرقاء داكنة

        camera.update();
        game.batch.setProjectionMatrix(camera.combined);

        game.batch.begin();
        game.font.draw(game.batch, "Match-3 Game!", 100, 400);
        game.batch.end();

        stage.act(delta); // تحديث الـ Stage (مهم لتشغيل الـ UI)
        stage.draw(); // رسم عناصر الـ Stage (بما في ذلك الزر)
    }

    @Override
    public void resize(int width, int height) {
        stage.getViewport().update(width, height, true);
    }

    @Override
    public void pause() {}

    @Override
    public void resume() {}

    @Override
    public void hide() {}

    @Override
    public void dispose() {
        stage.dispose();
        skin.dispose();
    }
}



--

ج. فئة GameScreen.java (شاشة اللعب الرئيسية)
Java




// في مجلد core/src/com/yourcompany/match3game/
package com.yourcompany.match3game;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.utils.ScreenUtils;
import com.badlogic.gdx.input.GestureDetector; // لاستخدام GestureDetector
import com.badlogic.gdx.math.Vector2; // لإحداثيات اللمس في GestureDetector
import com.badlogic.gdx.InputMultiplexer; // للتعامل مع مدخلات متعددة (Stage و GestureDetector)

public class GameScreen implements Screen {
    final Match3Game game;
    OrthographicCamera camera;
    GameBoard gameBoard;
    Stage stage; // Stage للعناصر والرسوم المتحركة
    private float gameTime; // الوقت المتبقي بالثواني
    private final float MAX_GAME_TIME = 60; // 60 ثانية للعبة

    public GameScreen(final Match3Game game) {
        this.game = game;
        camera = new OrthographicCamera();
        // حجم الكاميرا لعرض اللوحة، مع مساحة إضافية للنقاط والمؤقت
        camera.setToOrtho(false, GameBoard.COLS * GameElement.SIZE, GameBoard.ROWS * GameElement.SIZE + 100);

        stage = new Stage(new com.badlogic.gdx.utils.viewport.FitViewport(camera.viewportWidth, camera.viewportHeight, camera), game.batch);

        // تمرير الأصوات إلى GameBoard
        gameBoard = new GameBoard(game.scoreManager, stage, game.matchSound, game.swapSound);

        gameTime = MAX_GAME_TIME; // تهيئة المؤقت

        // إعداد GestureDetector لمعالجة السحب
        GestureDetector gestureDetector = new GestureDetector(new MyGestureListener(gameBoard));

        // استخدام InputMultiplexer للتعامل مع مدخلات من Stage و GestureDetector
        InputMultiplexer inputMultiplexer = new InputMultiplexer();
        inputMultiplexer.addProcessor(stage); // Stage يجب أن تعالج المدخلات لأي عناصر UI فيها (إن وجدت)
        inputMultiplexer.addProcessor(gestureDetector); // GestureDetector لمعالجة السحب
        Gdx.input.setInputProcessor(inputMultiplexer);
    }

    @Override
    public void show() {
        // يتم استدعاؤه عندما تصبح هذه الشاشة هي الشاشة النشطة
    }

    @Override
    public void render(float delta) {
        ScreenUtils.clear(0.2f, 0.2f, 0.2f, 1); // خلفية رمادية داكنة

        camera.update();
        game.batch.setProjectionMatrix(camera.combined);

        gameBoard.update(delta); // تحديث منطق اللعبة (مطابقة، سقوط، توليد)
        stage.act(delta); // تحديث الـ Stage لتشغيل الرسوم المتحركة

        // تحديث المؤقت
        if (gameBoard.currentGameState != GameBoard.GameState.GAME_OVER) {
            gameTime -= delta;
            if (gameTime <= 0) {
                gameTime = 0;
                gameBoard.currentGameState = GameBoard.GameState.GAME_OVER; // تعيين حالة اللعبة كـ Game Over
                game.setScreen(new GameOverScreen(game)); // الانتقال إلى شاشة Game Over
                dispose();
            }
        }

        // الرسم
        game.batch.begin();
        // هنا يمكنك رسم أي خلفيات أو عناصر أخرى لا تنتمي للـ Stage
        game.batch.end();

        stage.draw(); // رسم عناصر الـ Stage (الجواهر المتحركة)

        // رسم النقاط والمؤقت (يمكن أن تكون جزءًا من UI في الـ Stage أيضاً)
        game.batch.begin();
        game.font.draw(game.batch, "Score: " + game.scoreManager.getCurrentScore(), 10, camera.viewportHeight - 20);
        game.font.draw(game.batch, "High Score: " + game.scoreManager.getHighScore(), 10, camera.viewportHeight - 50);
        game.font.draw(game.batch, String.format("Time: %.1f", gameTime), 10, camera.viewportHeight - 80);
        game.batch.end();
    }

    @Override
    public void resize(int width, int height) {
        stage.getViewport().update(width, height, true); // تحديث الـ Viewport للـ Stage
        camera.update(); // تأكد من تحديث الكاميرا
    }

    @Override
    public void pause() {}

    @Override
    public void resume() {}

    @Override
    public void hide() {}

    @Override
    public void dispose() {
        gameBoard.dispose();
        stage.dispose();
    }

    // فئة داخلية لمعالجة إيماءات اللمس باستخدام GestureDetector
    class MyGestureListener implements GestureDetector.GestureListener {
        private GameBoard board;

        public MyGestureListener(GameBoard board) {
            this.board = board;
        }

        @Override
        public boolean touchDown(float x, float y, int pointer, int button) {
            // هذا يعالج النقر الأولي لتحديد العنصر
            board.handleTouchDown(x, y);
            return false; // نرجع false إذا أردنا أن تستمر معالجة الحدث لأداة أخرى
        }

        @Override
        public boolean tap(float x, float y, int count, int button) { return false; }
        @Override
        public boolean longPress(float x, float y) { return false; }

        @Override
        public boolean fling(float velocityX, float velocityY, int button) {
            // هذا هو الحدث الذي يتم استخدامه للسحب!
            board.handleFling(velocityX, velocityY);
            return false;
        }

        @Override
        public boolean pan(float x, float y, float deltaX, float deltaY) { return false; }
        @Override
        public boolean panStop(float x, float y, int pointer, int button) { return false; }
        @Override
        public boolean zoom(float initialDistance, float distance) { return false; }
        @Override
        public boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2) { return false; }
        @Override
        public void pinchStop() {}
    }
}



--

د. فئة GameOverScreen.java (شاشة نهاية اللعبة)
Java




// في مجلد core/src/com/yourcompany/match3game/
package com.yourcompany.match3game;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.utils.ScreenUtils;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;

public class GameOverScreen implements Screen {
    final Match3Game game;
    OrthographicCamera camera;
    Stage stage;
    Skin skin;

    public GameOverScreen(final Match3Game game) {
        this.game = game;
        camera = new OrthographicCamera();
        camera.setToOrtho(false, 800, 480);

        stage = new Stage(new com.badlogic.gdx.utils.viewport.FitViewport(camera.viewportWidth, camera.viewportHeight, camera), game.batch);
        Gdx.input.setInputProcessor(stage);

        skin = new Skin(Gdx.files.internal("uiskin.json"));

        // زر "Play Again"
        TextButton playAgainButton = new TextButton("Play Again", skin);
        playAgainButton.setSize(200, 60);
        playAgainButton.setPosition(camera.viewportWidth / 2 - playAgainButton.getWidth() / 2, camera.viewportHeight / 2 - 30);

        playAgainButton.addListener(new ChangeListener() {
            @Override
            public void changed(ChangeEvent event, com.badlogic.gdx.scenes.scene2d.Actor actor) {
                game.scoreManager.resetScore(); // إعادة تعيين النقاط
                game.setScreen(new GameScreen(game)); // بدء لعبة جديدة
                dispose();
            }
        });

        stage.addActor(playAgainButton);
    }

    @Override
    public void show() {}

    @Override
    public void render(float delta) {
        ScreenUtils.clear(0.5f, 0.0f, 0.0f, 1); // خلفية حمراء داكنة

        camera.update();
        game.batch.setProjectionMatrix(camera.combined);

        game.batch.begin();
        game.font.draw(game.batch, "GAME OVER!", 100, 400);
        game.font.draw(game.batch, "Your Score: " + game.scoreManager.getCurrentScore(), 100, 350);
        game.font.draw(game.batch, "High Score: " + game.scoreManager.getHighScore(), 100, 300);
        game.batch.end();

        stage.act(delta);
        stage.draw();
    }

    @Override
    public void resize(int width, int height) {
        stage.getViewport().update(width, height, true);
    }

    @Override
    public void pause() {}

    @Override
    public void resume() {}

    @Override
    public void hide() {}

    @Override
    public void dispose() {
        stage.dispose();
        skin.dispose();
    }
}


--

الخطوة 5: التشغيل والاختبار
بعد الانتهاء من جميع الأكواد، حان الوقت لتشغيل اللعبة واختبارها.

1- تأكد من مجلد assets:
تأكد مرة أخرى أن جميع صور العناصر (العادية والخاصة)، ملفات الصوت، و
ملفات uiskin.json، uiskin.atlas، uiskin.png موجودة في مجلد android/assets.

2- تشغيل نسخة سطح المكتب:
في Android Studio، اختر التكوين desktop.
انقر على زر Run الأخضر. ستفتح نافذة على سطح المكتب تعرض اللعبة.

3- تشغيل نسخة أندرويد:
تأكد من توصيل جهاز أندرويد (مع تمكين وضع تصحيح الأخطاء USB) أو تشغيل محاكي أندرويد.
اختر التكوين android.
انقر على زر Run الأخضر. سيتم تثبيت اللعبة وتشغيلها على جهازك/المحاكي.

4- نصائح للاختبار:
- اختبر التبديل بين العناصر ، تأكد من أن المطابقات تعمل وتتم إزالتها.
- لاحظ رسوم متحركة السقوط والاختفاء.
- اختبر توليد العناصر الخاصة عند مطابقة 4 أو 5 عناصر.
- حاول تفعيل العناصر الخاصة وشاهد تأثيرها ، راقب المؤقت والنقاط.
- تأكد من أن الانتقال بين الشاشات (قائمة، لعب، نهاية لعبة) يعمل بشكل صحيح.

* خاتمة :
لقد قمنا ببناء لعبة مطابقة عناصر (Match-3) متكاملة ومتقدمة باستخدام LibGDX. 
من خلال دمج الرسوم المتحركة السلسة، المؤثرات الصوتية الجذابة،
 نظام إدارة الشاشات، منطق العناصر الخاصة القوية، ومؤقت اللعبة، أصبحت لديك
 الآن أساس متين للعبة يمكن توسيعها وتحسينها بشكل أكبر. 
هذا المقال يقدم لك الهيكل الأساسي الذي يمكنك البعد منه لإنشاء تجربة لعب غنية وجذابة!


جدول المحتويات