لعبة قمري المملكة: رحلة تطوير لعبتي ثنائية الأبعاد في محرك يونتي
- Yasser M.
- 13 فبراير
- 13 دقائق قراءة
جدول المحتويات:
المقدمة
الفكرة والإلهام
عملية التطوير
التحديات التي واجهتها
الخطط المستقبلية
المواقع الداعمة
تفاصيل إضافية عن المشروع
الخاتمة

المقدمة:
تطوير لعبة هو تجربة مثيرة ولكنها مليئة بالتحديات، ورحلتي في إنشاء قمري المملكة لم تكن استثناءً.
في هذا المنشور، سأستعرض عملية تطوير لعبتي للجوال، بدءًا من الفكرة الأولية وصولاً إلى التنفيذ الفني والخطط المستقبلية.
العنوان: قمري المملكة / Gomorry Al-Mamlakah
النوع: بدأت كلعبة عداء لا نهائي ثنائية الأبعاد، ثم تطورت إلى لعبة من 6 مراحل مع نهاية.
المنصة: أجهزة أندرويد (مع خطط مستقبلية لدعم macOS).
محرك التطوير: Unity
لغة البرمجة: C#
الفكرة والإلهام:
جاءت فكرة قمري المملكة من مزيج من الحنين إلى الماضي، والمراجع الثقافية، ورغبتي في إنشاء لعبة جوال بسيطة ولكن ممتعة.
استلهمت اللعبة من ألعاب كلاسيكية مثل Flappy Bird، وأردت دمج عناصر من التراث السعودي، خاصة من الثمانينات والتسعينات، من خلال العناصر التالية:
شخصية طائر يتنقل بين عقبات تشبه التكوينات الصخرية.
موسيقى سعودية قديمة تتضمن أمثالًا ورسائل تقليدية شبيهة برسائل نوكيا القديمة.
ستة مستويات فريدة تعكس مناطق سعودية مختلفة:

زيادة تدريجية في الصعوبة مع ازدياد سرعة الطائر مع تقدم اللاعب.
اللعبة قائمة على المهارة فقط دون أي مكافآت أو قوى إضافية.
الإيرادات المتوقعة:
الإعلانات وعمليات الشراء داخل التطبيق:
خيار إزالة الإعلانات.
تخصيصات بصرية للطائر.
عملية التطوير:
1- اختيار محرك التطوير:
وقع الاختيار على Unity بسبب قدرته القوية على تطوير الألعاب ثنائية الأبعاد ودعمه للأنظمة المختلفة.

2- بناء النموذج الأولي:
تطوير الآليات الأساسية:
يتحرك الطائر تلقائيًا للأمام.
النقر على الشاشة يجعله يرتفع مع تأثير الجاذبية.
ظهور العقبات في أماكن عشوائية، مما يتطلب توقيتًا دقيقًا لتجاوزها.
تسجيل نقطة مع كل اجتياز ناجح للعقبات.
المشاهد: 13 مشهدًا لـ 6 مراحل و6 قوائم خرائط بالإضافة إلى القائمة الرئيسية.

3- تصميم عالم اللعبة:
تصميم المستويات لتعكس الثقافة السعودية.
حائل - بيئة صحراوية ذات تشكيلات صخرية.
المجالس التسعينية - أجواء تجمعات التسعينات.
بريدة - معمار الأسواق التقليدية.
الدرعية القديمة - طابع تاريخي مبني بالطوب الطيني.
ليالي التسعينات - أجواء مليئة بالذكريات.
الدرعية الحديثة - رؤية مستقبلية للتراث السعودي.
4- تصميم واجهة المستخدم (UI):
مستوحاة من هواتف نوكيا القديمة، مع خيارات:
بدء اللعبة
ضبط الصوت
تغيير اللغة
الاعتمادات


إجمالي المشتريات: 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.
إضافة محتوى حنين جديد، مثل موسيقى ورسائل نصية قديمة.
نظام الإنجازات ولوحات الصدارة.
توسعة ثقافية عبر إضافة مناطق جديدة تعكس تاريخ السعودية.
المواقع الداعمة:
Kenney Shape - system FOR 3D model
AI Retouch: Remove Unwanted Objects from Photos with AI | Photoroom
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