top of page

لعبة قمري المملكة: رحلة تطوير لعبتي ثنائية الأبعاد في محرك يونتي

  • صورة الكاتب: Yasser M.
    Yasser M.
  • 13 فبراير
  • 13 دقائق قراءة

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

المقدمة

الفكرة والإلهام

عملية التطوير

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

الخطط المستقبلية

المواقع الداعمة

تفاصيل إضافية عن المشروع

الخاتمة




المقدمة:


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

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


  • العنوان: قمري المملكة / Gomorry Al-Mamlakah

  • النوع: بدأت كلعبة عداء لا نهائي ثنائية الأبعاد، ثم تطورت إلى لعبة من 6 مراحل مع نهاية.

  • المنصة: أجهزة أندرويد (مع خطط مستقبلية لدعم macOS).

  • محرك التطوير: Unity

  • لغة البرمجة: C#


الفكرة والإلهام:


جاءت فكرة قمري المملكة من مزيج من الحنين إلى الماضي، والمراجع الثقافية، ورغبتي في إنشاء لعبة جوال بسيطة ولكن ممتعة.

استلهمت اللعبة من ألعاب كلاسيكية مثل Flappy Bird، وأردت دمج عناصر من التراث السعودي، خاصة من الثمانينات والتسعينات، من خلال العناصر التالية:


  • شخصية طائر يتنقل بين عقبات تشبه التكوينات الصخرية.

  • موسيقى سعودية قديمة تتضمن أمثالًا ورسائل تقليدية شبيهة برسائل نوكيا القديمة.

  • ستة مستويات فريدة تعكس مناطق سعودية مختلفة:


Levels: The game features five levels inspired by regions of Saudi Arabia
Levels: The game features five levels inspired by regions of Saudi Arabia

  • زيادة تدريجية في الصعوبة مع ازدياد سرعة الطائر مع تقدم اللاعب.


  • اللعبة قائمة على المهارة فقط دون أي مكافآت أو قوى إضافية.


Game Trailer using ChatGPT - Sore

الإيرادات المتوقعة:


  • الإعلانات وعمليات الشراء داخل التطبيق:


    • خيار إزالة الإعلانات.

    • تخصيصات بصرية للطائر.



عملية التطوير:

1- اختيار محرك التطوير:


وقع الاختيار على Unity بسبب قدرته القوية على تطوير الألعاب ثنائية الأبعاد ودعمه للأنظمة المختلفة.



2- بناء النموذج الأولي:

تطوير الآليات الأساسية:


  • يتحرك الطائر تلقائيًا للأمام.

  • النقر على الشاشة يجعله يرتفع مع تأثير الجاذبية.

  • ظهور العقبات في أماكن عشوائية، مما يتطلب توقيتًا دقيقًا لتجاوزها.

  • تسجيل نقطة مع كل اجتياز ناجح للعقبات.


المشاهد: 13 مشهدًا لـ 6 مراحل و6 قوائم خرائط بالإضافة إلى القائمة الرئيسية.


3- تصميم عالم اللعبة:

تصميم المستويات لتعكس الثقافة السعودية.


  1. حائل - بيئة صحراوية ذات تشكيلات صخرية.

  2. المجالس التسعينية - أجواء تجمعات التسعينات.

  3. بريدة - معمار الأسواق التقليدية.

  4. الدرعية القديمة - طابع تاريخي مبني بالطوب الطيني.

  5. ليالي التسعينات - أجواء مليئة بالذكريات.

  6. الدرعية الحديثة - رؤية مستقبلية للتراث السعودي.





4- تصميم واجهة المستخدم (UI):

مستوحاة من هواتف نوكيا القديمة، مع خيارات:


  • بدء اللعبة

  • ضبط الصوت

  • تغيير اللغة

  • الاعتمادات

كلّفني 5 دولارات من متجر Unity.
كلّفني 5 دولارات من متجر Unity.
كلّفني 110 ريال سعودي من متجر NAIF-PIXEL.
كلّفني 110 ريال سعودي من متجر NAIF-PIXEL.

إجمالي المشتريات: 230 ريال سعودي إذا أضفنا ترخيص Google كمطور لنشر الألعاب.


5- إضافة المؤثرات الصوتية والموسيقى:


  • موسيقى قديمة من الثمانينات والتسعينات.

  • أمثال سعودية تظهر كنصوص لتحدي اللاعب.

  • مؤثرات صوتية للحركة، والفشل، والضغط على الأزرار.


6- تحسين أداء اللعبة:


  • ضمان توافق الخلفيات مع جميع أحجام الشاشات.

  • تحسين الأداء على الأجهزة ذات المواصفات المنخفضة.

  • اختبار الإصدار WebGL رغم مشكلة الشاشة السوداء التي تطلبت تصحيحها.



خط اللعبة: Handjet لملف (اللغة العربية) مع نظام يسمى Arabic Writer.



7- استراتيجية تحقيق الأرباح:


  • نسخة مجانية تحتوي على إعلانات.

  • نسخة مدفوعة بدون إعلانات.

  • مشتريات داخل اللعبة (Skins) بدون تأثير على التوازن العام.



8- إطلاق اللعبة:


  • إطلاق نسخة تجريبية (APK) لجمع ردود الفعل.

  • رفع اللعبة على Google Play Console مع الامتثال لسياسات المتجر.

  • تحديثات ما بعد الإطلاق لإصلاح الأخطاء وإضافة ميزات جديدة.



9- التحديات التي واجهتها:


  • تكرار مشهد الخسارة: اضطررت لإيجاد طريقة لعرض الفيديو مرة واحدة قبل التلاشي.

  • توافق عناصر الواجهة (UI): ضمان تكيّفها مع أحجام الشاشات المختلفة.

  • مشاكل WebGL: اللعبة تعمل في Unity Editor لكنها تظهر شاشة سوداء عند البناء.

  • إعداد Keystore: واجهت بعض الصعوبات في إعداد مفتاح التوقيع للنشر على Google Play.



10. الأكواد:


  • MainMenu

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using UnityEngine.Video;
using System.Collections;

public class MainMenu : MonoBehaviour
{
    public VideoPlayer videoPlayer;    // Reference to the Video Player
    public Button startButton;         // Reference to the Start Button
    public GameObject screen;          // Reference to the Screen GameObject
    public AudioSource backgroundMusic; // Reference to the AudioSource for music
    public RawImage videoDisplay;      // Reference to the RawImage showing the video
    public float fadeDuration = 2f;    // Duration for the fade effect
    public Button muteButton;          // Button to mute all audio
    public Button unmuteButton;        // Button to unmute all audio
    public string sceneToLoad;

    void Start()
    {
        // Video Player setup
        if (videoPlayer == null)
        {
            Debug.LogError("VideoPlayer is not assigned in the Inspector.");
        }
        else
        {
            videoPlayer.loopPointReached += OnVideoEnd;
        }

        // Start Button setup
        if (startButton == null)
        {
            Debug.LogError("StartButton is not assigned in the Inspector.");
        }
        else
        {
            startButton.gameObject.SetActive(false); // Hide the button initially
            startButton.onClick.AddListener(() => StartGame(sceneToLoad)); // Add listener to button
        }

        // Screen setup
        if (screen == null)
        {
            Debug.LogError("Screen GameObject is not assigned in the Inspector.");
        }

        // Background Music setup
        if (backgroundMusic == null)
        {
            Debug.LogError("AudioSource (backgroundMusic) is not assigned in the Inspector.");
        }
        else
        {
            backgroundMusic.Stop(); // Ensure music is off initially
        }

        // Video Display setup
        if (videoDisplay == null)
        {
            Debug.LogError("VideoDisplay (RawImage) is not assigned in the Inspector.");
        }

        // Mute Button setup
        if (muteButton == null)
        {
            Debug.LogError("MuteButton is not assigned in the Inspector.");
        }
        else
        {
            muteButton.onClick.AddListener(MuteAllAudio);
            muteButton.gameObject.SetActive(true); // Show the button initially
        }

        // Unmute Button setup
        if (unmuteButton == null)
        {
            Debug.LogError("UnmuteButton is not assigned in the Inspector.");
        }
        else
        {
            unmuteButton.onClick.AddListener(UnmuteAllAudio);
            unmuteButton.gameObject.SetActive(false); // Hide initially
        }
    }

    void OnVideoEnd(VideoPlayer vp)
    {
        StartCoroutine(FadeOutVideo());

        if (backgroundMusic != null)
        {
            backgroundMusic.Play();
        }
    }

    IEnumerator FadeOutVideo()
    {
        float elapsedTime = 0f;
        Color startColor = videoDisplay.color;
        startColor.a = 1f; // Fully opaque
        Color endColor = videoDisplay.color;
        endColor.a = 0f; // Fully transparent

        while (elapsedTime < fadeDuration)
        {
            elapsedTime += Time.deltaTime;
            videoDisplay.color = Color.Lerp(startColor, endColor, elapsedTime / fadeDuration);
            yield return null;
        }

        // After fade-out, deactivate the video display and show the start button
        videoDisplay.gameObject.SetActive(false);
        startButton.gameObject.SetActive(true);
    }

    public void StartGame(string sceneName)
    {
        if (string.IsNullOrEmpty(sceneName))
        {
            Debug.LogError("Scene name is empty or null!");
            return;
        }

        if (SceneManager.GetSceneByName(sceneName) != null)
        {
            SceneManager.LoadScene(sceneName); // Load the specified scene
        }
        else
        {
            Debug.LogError("Scene '" + sceneName + "' not found! Ensure the scene is added to Build Settings.");
        }
    }
    public void MuteAllAudio()
    {
        AudioListener.volume = 0; // Mute all audio
        muteButton.gameObject.SetActive(false);
        unmuteButton.gameObject.SetActive(true);
        Debug.Log("All audio muted.");
    }

    public void UnmuteAllAudio()
    {
        AudioListener.volume = 1; // Restore audio
        muteButton.gameObject.SetActive(true);
        unmuteButton.gameObject.SetActive(false);
        Debug.Log("All audio unmuted.");
    }
}
  • OpenMultipleURLs

using UnityEngine;

public class OpenMultipleURLs : MonoBehaviour
{
    // URLs for the two buttons
    public string url1 = "https://www.instagram.com/naifpxl/";
    public string url2 = "https://www.threads.net/@yms.1995/";

    // Method for Button 1
    public void OpenURL1()
    {
        if (!string.IsNullOrEmpty(url1))
        {
            Application.OpenURL(url1);
            Debug.Log($"Opening URL 1: {url1}");
        }
        else
        {
            Debug.LogWarning("URL 1 is empty or not set.");
        }
    }

    // Method for Button 2
    public void OpenURL2()
    {
        if (!string.IsNullOrEmpty(url2))
        {
            Application.OpenURL(url2);
            Debug.Log($"Opening URL 2: {url2}");
        }
        else
        {
            Debug.LogWarning("URL 2 is empty or not set.");
        }
    }
}
  • Playermove

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class Playermove : MonoBehaviour
{
    public float jumpForce = 10f;
    public Rigidbody2D rb;
    public int currentScore = 0;
    public int lives = 2; 
    public Text CurrentScoreText;
    public Text gameOverText;

    // >>> Add a public reference to a game over Image <<<
    public Image gameOverImage;

    public List<GameObject> lifeUIElements;
    public AudioSource jumpSound;
    public AudioSource collisionSound;
    public AudioSource outOfBoundsSound;
    public AudioSource scoreSound;
    public AudioSource gameOverSound; 
    public Animator playerAnimator; 

    // Flag to track if Game Over sound has played
    private bool hasGameOverSoundPlayed = false; 

    // -----------------------------------------------------------
    //  Random MUSIC SETUP
    // -----------------------------------------------------------
    [Header("Music Randomization")]
    [Tooltip("Drop multiple AudioSource objects here (each with different music clips)")]
    public List<AudioSource> musicTracks;
    // Keep track of which AudioSource is currently playing (optional)
    private AudioSource currentMusicTrack;

    // -----------------------------------------------------------
    //  HIGH SCORE
    // -----------------------------------------------------------
    public int highScore = 0;
    [Tooltip("Optional: Assign a UI Text to display the high score on screen")]
    public Text highScoreText;

    // Target score and next scene
    public int targetScore;  // Score threshold for the next scene
    public string nextSceneName; // Name of the scene to load

    private void Start()
    {
        // Safety checks
        if (jumpSound == null) Debug.LogWarning("Jump sound is not assigned in the Inspector!");
        if (collisionSound == null) Debug.LogWarning("Collision sound is not assigned in the Inspector!");
        if (outOfBoundsSound == null) Debug.LogWarning("Out of bounds sound is not assigned in the Inspector!");
        if (scoreSound == null) Debug.LogWarning("Score sound is not assigned in the Inspector!");
        if (gameOverSound == null) Debug.LogWarning("Game Over sound is not assigned in the Inspector!");
        if (playerAnimator == null) Debug.LogWarning("Player Animator is not assigned in the Inspector!");
        if (lifeUIElements.Count == 0) Debug.LogWarning("Life UI elements are not assigned in the Inspector!");

        // If we have a Game Over text, hide it at the start
        if (gameOverText != null) gameOverText.gameObject.SetActive(false);

        // Hide Game Over Image at the start
        if (gameOverImage != null) gameOverImage.gameObject.SetActive(false);

        // -------------------------------------
        //  Load High Score from PlayerPrefs
        // -------------------------------------
        if (PlayerPrefs.HasKey("HighScore"))
        {
            highScore = PlayerPrefs.GetInt("HighScore");
        }
        else
        {
            PlayerPrefs.SetInt("HighScore", 0);
            highScore = 0;
        }

        // Update High Score UI text if assigned
        if (highScoreText != null)
        {
            highScoreText.text = "High Score: " + highScore;
        }

        // Initialize current score text
        if (CurrentScoreText != null)
        {
            CurrentScoreText.text = currentScore.ToString();
        }

        // Play random music
        PlayRandomMusic();
    }

    private void Update()
    {
        // Check if the player goes out of bounds vertically
        if (transform.position.y >= 6.5f || transform.position.y <= -5f)
        {
            LoseLife();
        }

        // Handle jumping with space key
        if (Input.GetKey(KeyCode.Space))
        {
            Jump();
        }

        // Handle jumping with touch input
        if (Input.touchCount > 0)
        {
            Touch touch = Input.GetTouch(0);
            if (touch.phase == TouchPhase.Began)
            {
                Jump();
            }
        }

        // Update score text in real-time (optional)
        if (CurrentScoreText != null)
        {
            CurrentScoreText.text = currentScore.ToString();
        }
    }

    private void Jump()
    {
        Vector2 velocity = rb.velocity;
        velocity.y = jumpForce;
        rb.velocity = velocity;

        if (jumpSound != null)
        {
            jumpSound.Play();
        }
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        // Increment score if the player triggers a "Score" object
        if (other.gameObject.tag == "Score")
        {
            currentScore += 1;

            // Play score sound
            if (scoreSound != null)
            {
                scoreSound.Play();
            }

            // -------------------------------------
            //  Check for new high score
            // -------------------------------------
            if (currentScore > highScore)
            {
                highScore = currentScore;
                PlayerPrefs.SetInt("HighScore", highScore);
                PlayerPrefs.Save();  // Good practice to save immediately

                // Update High Score UI
                if (highScoreText != null)
                {
                    highScoreText.text = "High Score:" + highScore;
                }
            }

            // Load the next scene if the target score is reached
            if (currentScore >= targetScore)
            {
                SceneManager.LoadScene(nextSceneName); // Replace with the actual scene name
            }
        }
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        // Lose a life if the player collides with "Ground" or "Pipe"
        if (collision.collider.tag == "Ground" || collision.collider.tag == "Pipe")
        {
            LoseLife();
        }
    }

    private void LoseLife()
    {
        // Decide which sound to play (out of bounds vs. collision)
        if (outOfBoundsSound != null && (transform.position.y >= 6.5f || transform.position.y <= -5f))
        {
            outOfBoundsSound.Play();
        }
        else if (collisionSound != null)
        {
            collisionSound.Play();
        }

        if (lives > 0)
        {
            lives--;

            // Hide a life UI element
            if (lifeUIElements.Count > lives)
            {
                lifeUIElements[lives].SetActive(false);
            }

            // Reset player position
            transform.position = new Vector3(-0.94f, 0.27f, 0f);
        }

        // If lives are 0 or below, trigger Game Over
        if (lives <= 0)
        {
            if (playerAnimator != null)
            {
                playerAnimator.SetTrigger("GameOverTrigger");
            }

            // Start the Game Over routine which will reload the scene
            StartCoroutine(GameOverRoutine());
        }
    }

    private IEnumerator GameOverRoutine()
    {
        // Play Game Over sound once
        if (!hasGameOverSoundPlayed && gameOverSound != null)
        {
            gameOverSound.Play();
            hasGameOverSoundPlayed = true;
        }

        // Show Game Over text and image
        if (gameOverText != null)
        {
            gameOverText.text = $"GAME OVER\nScore: {currentScore}\nHigh Score: {highScore}";
            gameOverText.gameObject.SetActive(true);
        }
        if (gameOverImage != null)
        {
            gameOverImage.gameObject.SetActive(true);
        }

        // Wait for the sound/animation to finish
        yield return new WaitForSeconds(1f);

        // Deactivate the Player
        gameObject.SetActive(false);

        // Notify the GameManager to reload the scene
        GameManager.Instance.ReloadSceneAfterDelay(0); // Set delay as needed
    }

    private void PlayRandomMusic()
    {
        if (musicTracks == null || musicTracks.Count == 0)
        {
            Debug.LogWarning("No music tracks are assigned in the musicTracks list!");
            return;
        }

        // Stop any previously playing track (optional)
        if (currentMusicTrack != null)
        {
            currentMusicTrack.Stop();
        }

        int randomIndex = Random.Range(0, musicTracks.Count);

        // Pick a random track
        currentMusicTrack = musicTracks[randomIndex];
        currentMusicTrack.Play();
    }
}
  • GameManager

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour
{
    private static GameManager instance;

    public static GameManager Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<GameManager>();
                if (instance == null)
                {
                    GameObject gm = new GameObject("GameManager");
                    instance = gm.AddComponent<GameManager>();
                }
            }
            return instance;
        }
    }

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }

    public void ReloadSceneAfterDelay(float delay)
    {
        StartCoroutine(ReloadSceneCoroutine(delay));
    }

    private IEnumerator ReloadSceneCoroutine(float delay)
    {
        yield return new WaitForSeconds(delay);
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
    }
}
  • MoveRightUI

using UnityEngine;
using UnityEngine.UI;
using System.Linq; // Add this line to include LINQ

public class MoveRightUI : MonoBehaviour
{
    // Array of Text objects
    public Text[] textObjects;

    // Speed of movement
    public float speed = 5f;

    // Spawn (appear) point
    public Vector2 spawnPosition = new Vector2(-500f, 0f);

    // Disappear point
    public Vector2 disappearPosition = new Vector2(600f, 0f);

    // Array to keep track of active Text objects
    private bool[] isActive;

    private void Start()
    {
        // Ensure we have at least one Text object
        if (textObjects.Length == 0)
        {
            Debug.LogError("No Text objects assigned!");
            return;
        }

        // Initialize the active tracking array
        isActive = new bool[textObjects.Length];
        for (int i = 0; i < textObjects.Length; i++)
        {
            // Initially, set all Text objects to inactive
            isActive[i] = false;
            // Optionally, deactivate all Text objects at the start
            textObjects[i].gameObject.SetActive(false);
        }

        // Start by activating a random Text
        ActivateRandomText();
    }

    private void Update()
    {
        for (int i = 0; i < textObjects.Length; i++)
        {
            if (isActive[i])
            {
                RectTransform rectTransform = textObjects[i].GetComponent<RectTransform>();
                if (rectTransform != null)
                {
                    // Move the Text object to the right
                    rectTransform.anchoredPosition += Vector2.right * speed * Time.deltaTime;

                    // Debug log for movement
                    Debug.Log($"Text {i} Current Position: {rectTransform.anchoredPosition}");

                    // Check if the Text has passed the disappear position
                    if (rectTransform.anchoredPosition.x > disappearPosition.x)
                    {
                        // Deactivate the Text
                        DeactivateText(i);

                        // Optionally, activate another random Text
                        ActivateRandomText();
                    }
                }
                else
                {
                    Debug.LogError($"Text object at index {i} does not have a RectTransform!");
                }
            }
        }
    }

    // Activates a random inactive Text object and positions it at the spawn point
    private void ActivateRandomText()
    {
        // Find all inactive Text objects
        int[] inactiveIndices = System.Linq.Enumerable.Range(0, isActive.Length)
                                                   .Where(index => !isActive[index])
                                                   .ToArray();

        if (inactiveIndices.Length == 0)
        {
            Debug.LogWarning("All Text objects are currently active!");
            return;
        }

        // Randomly select one inactive Text object
        int randomIndex = inactiveIndices[Random.Range(0, inactiveIndices.Length)];
        Text randomText = textObjects[randomIndex];
        RectTransform rectTransform = randomText.GetComponent<RectTransform>();

        if (rectTransform == null)
        {
            Debug.LogError($"Selected Text object at index {randomIndex} does not have a RectTransform!");
            return;
        }

        // Move the Text to the spawn position
        rectTransform.anchoredPosition = spawnPosition;

        // Activate the Text (ensure it's enabled)
        randomText.gameObject.SetActive(true);

        // Mark it as active
        isActive[randomIndex] = true;
    }

    // Deactivates the Text object at the specified index
    private void DeactivateText(int index)
    {
        Text textToDeactivate = textObjects[index];
        if (textToDeactivate != null)
        {
            // Optionally, disable the GameObject
            textToDeactivate.gameObject.SetActive(false);

            // Mark it as inactive
            isActive[index] = false;

            Debug.Log($"Text {index} has been deactivated.");
        }
        else
        {
            Debug.LogError($"Text object at index {index} is null!");
        }
    }
}
  • Ground

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Ground : MonoBehaviour
{
    public float speed = 4f;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        transform.Translate(new Vector3(-1f, 0, 0) * Time.deltaTime * speed);

        if (transform.position.x <= -10.5f)
        {
            transform.position = new Vector3(1.5f, transform.position.y, transform.position.z);
        }
    }
}
  • Spawner

using System.Numerics;
using System;
using System.Threading;
using System.Diagnostics;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Spawner : MonoBehaviour
{
    public GameObject pipePrefab;
    public Transform[] spawnPositions;
    public float startTime = 2.5f;
    private float timeBetweenSpawn;

    // Start is called before the first frame update
    void Start()
    {
        timeBetweenSpawn = startTime;
    }

    // Update is called once per frame
    void Update()
    {
        if (timeBetweenSpawn <= 0f)
        {
            SpawnPipe();
            timeBetweenSpawn = startTime;
        }
        else 
        {
            timeBetweenSpawn -= Time.deltaTime;
        }
    }

    void SpawnPipe()
    {
        int randomPoint = UnityEngine.Random.Range(0, spawnPositions.Length);
        Instantiate(pipePrefab, spawnPositions[randomPoint].position, UnityEngine.Quaternion.identity);
    }
}
  • Pipe

using System.Threading; // Remove if not needed
using UnityEngine;

public class Pipe : MonoBehaviour
{
    public float speed = 3f;

    // Update is called once per frame
    void Update()
    {
        // Move the pipe to the left
        transform.Translate(Vector2.left * speed * Time.deltaTime);

        // Destroy the pipe if it moves out of bounds
        if (transform.position.x <= -20f)
        {
            Destroy(this.gameObject);
        }
    }
}
  • VideoBackgroundController

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;
using System.Collections.Generic;

public class VideoBackgroundController : MonoBehaviour
{
    [Header("UI References")]
    public RawImage rawImage;
    public Text legacyText; // UI Text element for displaying the legacy text.

    [Header("Video Setup")]
    public RenderTexture videoRenderTexture;

    [Header("Multiple Video Players")]
    public List<VideoPlayer> videoPlayers; // Assign multiple VideoPlayers in the Inspector.

    [Header("Multiple Video Clips")]
    public List<VideoClip> videoClips; // Assign your video clips in the Inspector.

    [Header("Legacy Text")]
    public List<string> videoTexts; // Add corresponding text for each video.

    private int activePlayerIndex = -1;

    void Start()
    {
        if (rawImage == null || videoRenderTexture == null || videoPlayers == null || videoPlayers.Count == 0 || videoClips == null || videoClips.Count == 0 || videoTexts == null || videoTexts.Count == 0)
        {
            Debug.LogWarning("VideoBackgroundController: Missing references in the Inspector!");
            return;
        }

        if (videoClips.Count != videoTexts.Count)
        {
            Debug.LogWarning("VideoBackgroundController: Mismatch between video clips and texts!");
            return;
        }

        PlayRandomVideo();
    }

    void PlayRandomVideo()
    {
        // Pick a random VideoClip from the list
        int randomIndex = Random.Range(0, videoClips.Count);
        VideoClip chosenClip = videoClips[randomIndex];

        // Disable all VideoPlayers first
        foreach (var player in videoPlayers)
        {
            player.enabled = false;
        }

        // Enable the selected VideoPlayer
        activePlayerIndex = randomIndex % videoPlayers.Count; // Ensures we don't exceed the available players
        var activePlayer = videoPlayers[activePlayerIndex];
        activePlayer.enabled = true;

        // Assign the chosen clip to the active VideoPlayer
        activePlayer.clip = chosenClip;

        // Set the RenderTexture for the active VideoPlayer
        activePlayer.targetTexture = videoRenderTexture;

        // Set the same RenderTexture on the RawImage
        rawImage.texture = videoRenderTexture;

        // Display the corresponding legacy text
        legacyText.text = videoTexts[randomIndex];

        // Start playback
        activePlayer.Play();
    }

    public void SwitchToNextVideo()
    {
        if (videoPlayers == null || videoPlayers.Count == 0)
            return;

        // Stop the current video
        if (activePlayerIndex >= 0 && activePlayerIndex < videoPlayers.Count)
        {
            videoPlayers[activePlayerIndex].Stop();
        }

        // Play the next video
        PlayRandomVideo();
    }
}
  • LoadSceneOnScore

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using System.Collections; // Required for coroutines

public class LoadSceneOnScore : MonoBehaviour
{
    public int targetScore = 50;  // Target score to trigger the scene load
    public string sceneToLoad = "NextScene";  // Name of the scene to load
    public Text scoreText;  // Reference to the UI Text component displaying the score
    public Image transitionImage; // Reference to the UI Image for transition
    public AudioClip transitionSound; // Sound effect before loading scene
    private AudioSource audioSource;
    private int currentScore = 0;  // Current score value

    void Start()
    {
        if (transitionImage != null)
        {
            transitionImage.gameObject.SetActive(false); // Ensure image is hidden initially
        }
        audioSource = gameObject.AddComponent<AudioSource>();
    }

    void Update()
    {
        // Update the score display
        if (scoreText != null)
        {
            scoreText.text = "Score: " + currentScore.ToString();
        }

        // Check if the player's score has reached the target score
        if (currentScore >= targetScore)
        {
            StartCoroutine(ShowTransitionEffect());
        }
    }

    // Method to add score
    public void AddScore(int points)
    {
        currentScore += points;
    }

    // Coroutine to show image and play sound before scene transition
    IEnumerator ShowTransitionEffect()
    {
        if (transitionImage != null)
        {
            transitionImage.gameObject.SetActive(true);
            transitionImage.canvasRenderer.SetAlpha(0f);
            transitionImage.CrossFadeAlpha(1f, 1f, false); // Fade in effect
        }
        
        if (audioSource != null && transitionSound != null)
        {
            audioSource.PlayOneShot(transitionSound);
        }
        
        yield return new WaitForSeconds(2f); // Wait before scene loads
        LoadNextScene();
    }

    // Method to load the next scene
    void LoadNextScene()
    {
        SceneManager.LoadScene(sceneToLoad);
    }
}
  • ImageDisplay

using UnityEngine;
using UnityEngine.UI;

public class ImageDisplay : MonoBehaviour
{
    public Image image; // Assign your UI Image in the inspector
    private float timer = 8f;
    private bool isVisible = true;

    void Start()
    {
        if (image != null)
        {
            image.gameObject.SetActive(true); // Show the image at the start
        }
    }

    void Update()
    {
        if (isVisible)
        {
            timer -= Time.deltaTime;
            if (timer <= 0)
            {
                HideImage();
            }
        }
    }

    public void HideImage()
    {
        if (image != null)
        {
            image.gameObject.SetActive(false);
            isVisible = false;
        }
    }

    public void OnImageClick()
    {
        HideImage();
    }
}


الخطط المستقبلية:


  • إصدار على iOS وmacOS.

  • إضافة محتوى حنين جديد، مثل موسيقى ورسائل نصية قديمة.

  • نظام الإنجازات ولوحات الصدارة.

  • توسعة ثقافية عبر إضافة مناطق جديدة تعكس تاريخ السعودية.



المواقع الداعمة:


  1. Remove Background from Image for Free – remove.bg

  2. Sora

  3. Voice Maker - Create a Voice - Online & Free

  4. Modify image - ResizePixel

  5. Uncrop Image - Pixelcut

  6. Mobile UI Pixel Icons Pack | 2D | Unity Asset Store

  7. Kenney Shape - system FOR 3D model

  8. AI Retouch: Remove Unwanted Objects from Photos with AI | Photoroom

  9. Untitled - FlexClip

  10. Pixel It - Create pixel art from an image

  11. Fotor GoArt: Turn Your Photos into Stunning Artworks with Hundreds of Photo Effects



    :تفاصيل إضافية عن المشروع


    • المطور: ياسر محمد الشريف

    • الفنان البيكسل: نايف اللحيدان

    • تاريخ اختبار اللعبة: 1/1/2025

    • رقم الإصدار: 2.0

    • مدة التطوير: أسبوعان للنسخة الأولى (1.0)

    • إصدار Unity المستخدم: 2022.3.28f1


    • لم يتم النشر بعد، متوقع الانتهاء في فبراير 2025.




الخاتمة:


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



 
 
 

Commentaires


bottom of page