برنامج تعليمي للعبة Endless Runner على Unity

في ألعاب الفيديو، مهما كان حجم العالم، فإنه دائمًا له نهاية. لكن بعض الألعاب تحاول محاكاة العالم اللانهائي، وتندرج مثل هذه الألعاب تحت فئة تسمى Endless Runner.

Endless Runner هو نوع من الألعاب حيث يتحرك اللاعب باستمرار للأمام أثناء جمع النقاط وتجنب العوائق. الهدف الرئيسي هو الوصول إلى نهاية المستوى دون الوقوع في العوائق أو الاصطدام بها، ولكن في كثير من الأحيان، يتكرر المستوى إلى ما لا نهاية، مما يزيد من الصعوبة تدريجيًا، حتى يصطدم اللاعب بالعائق.

طريقة اللعب في Subway Surfers

إذا أخذنا في الاعتبار أن حتى أجهزة الكمبيوتر/أجهزة الألعاب الحديثة لديها قوة معالجة محدودة، فمن المستحيل إنشاء عالم لانهائي حقًا.

إذن كيف تخلق بعض الألعاب وهمًا بعالم لا نهائي؟ الإجابة هي إعادة استخدام كتل البناء (أو تجميع الكائنات)، بمعنى آخر، بمجرد أن تنتقل الكتلة إلى الخلف أو خارج عرض الكاميرا، يتم نقلها إلى الأمام.

لإنشاء لعبة عداء لا نهاية لها في Unity، سنحتاج إلى إنشاء منصة بها عقبات ووحدة تحكم للاعب.

الخطوة 1: إنشاء المنصة

نبدأ بإنشاء منصة مبلطة سيتم تخزينها لاحقًا في Prefab:

  • إنشاء GameObject جديد واستدعائه "TilePrefab"
  • إنشاء مكعب جديد (GameObject -> 3D Object -> Cube)
  • حرك المكعب داخل الكائن "TilePrefab"، وقم بتغيير موضعه إلى (0، 0، 0)، وقم بتغيير المقياس إلى (8، 0.4، 20)

  • اختياريًا، يمكنك إضافة قضبان إلى الجوانب عن طريق إنشاء مكعبات إضافية، مثل هذا:

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

  • قم بإنشاء 3 كائنات لعبة داخل الكائن "TilePrefab" وأطلق عليها اسم "Obstacle1" و"Obstacle2" و "Obstacle3"
  • بالنسبة للعائق الأول، قم بإنشاء مكعب جديد وحركه داخل الكائن "Obstacle1"
  • قم بتوسيع المكعب الجديد إلى نفس عرض المنصة تقريبًا وقم بتقليص ارتفاعه (سيحتاج اللاعب إلى القفز لتجنب هذه العقبة)
  • قم بإنشاء مادة جديدة، وأطلق عليها اسم "RedMaterial" وقم بتغيير لونها إلى الأحمر، ثم قم بتعيينها للمكعب (هذا فقط حتى يتم تمييز العائق عن المنصة الرئيسية)

  • بالنسبة لـ "Obstacle2"، قم بإنشاء مكعبين ووضعهما في شكل مثلث، مع ترك مساحة مفتوحة في الأسفل (سيحتاج اللاعب إلى الانحناء لتجنب هذه العقبة)

  • وأخيرًا، سيكون "Obstacle3" نسخة مكررة من "Obstacle1" و"Obstacle2"، مجتمعين معًا

  • الآن قم بتحديد جميع الكائنات الموجودة داخل العوائق وقم بتغيير علامتها إلى "Finish"، وسوف تكون هناك حاجة إلى هذا لاحقًا لاكتشاف الاصطدام بين اللاعب والعائق.

لتوليد منصة لا نهائية، سنحتاج إلى مجموعة من البرامج النصية التي ستتعامل مع تجميع الكائنات وتنشيط العوائق:

منصة SC_Tile.cs

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

public class SC_PlatformTile : MonoBehaviour
{
    public Transform startPoint;
    public Transform endPoint;
    public GameObject[] obstacles; //Objects that contains different obstacle types which will be randomly activated

    public void ActivateRandomObstacle()
    {
        DeactivateAllObstacles();

        System.Random random = new System.Random();
        int randomNumber = random.Next(0, obstacles.Length);
        obstacles[randomNumber].SetActive(true);
    }

    public void DeactivateAllObstacles()
    {
        for (int i = 0; i < obstacles.Length; i++)
        {
            obstacles[i].SetActive(false);
        }
    }
}

برنامج SC_GroundGenerator.cs

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

public class SC_GroundGenerator : MonoBehaviour
{
    public Camera mainCamera;
    public Transform startPoint; //Point from where ground tiles will start
    public SC_PlatformTile tilePrefab;
    public float movingSpeed = 12;
    public int tilesToPreSpawn = 15; //How many tiles should be pre-spawned
    public int tilesWithoutObstacles = 3; //How many tiles at the beginning should not have obstacles, good for warm-up

    List<SC_PlatformTile> spawnedTiles = new List<SC_PlatformTile>();
    int nextTileToActivate = -1;
    [HideInInspector]
    public bool gameOver = false;
    static bool gameStarted = false;
    float score = 0;

    public static SC_GroundGenerator instance;

    // Start is called before the first frame update
    void Start()
    {
        instance = this;

        Vector3 spawnPosition = startPoint.position;
        int tilesWithNoObstaclesTmp = tilesWithoutObstacles;
        for (int i = 0; i < tilesToPreSpawn; i++)
        {
            spawnPosition -= tilePrefab.startPoint.localPosition;
            SC_PlatformTile spawnedTile = Instantiate(tilePrefab, spawnPosition, Quaternion.identity) as SC_PlatformTile;
            if(tilesWithNoObstaclesTmp > 0)
            {
                spawnedTile.DeactivateAllObstacles();
                tilesWithNoObstaclesTmp--;
            }
            else
            {
                spawnedTile.ActivateRandomObstacle();
            }
            
            spawnPosition = spawnedTile.endPoint.position;
            spawnedTile.transform.SetParent(transform);
            spawnedTiles.Add(spawnedTile);
        }
    }

    // Update is called once per frame
    void Update()
    {
        // Move the object upward in world space x unit/second.
        //Increase speed the higher score we get
        if (!gameOver && gameStarted)
        {
            transform.Translate(-spawnedTiles[0].transform.forward * Time.deltaTime * (movingSpeed + (score/500)), Space.World);
            score += Time.deltaTime * movingSpeed;
        }

        if (mainCamera.WorldToViewportPoint(spawnedTiles[0].endPoint.position).z < 0)
        {
            //Move the tile to the front if it's behind the Camera
            SC_PlatformTile tileTmp = spawnedTiles[0];
            spawnedTiles.RemoveAt(0);
            tileTmp.transform.position = spawnedTiles[spawnedTiles.Count - 1].endPoint.position - tileTmp.startPoint.localPosition;
            tileTmp.ActivateRandomObstacle();
            spawnedTiles.Add(tileTmp);
        }

        if (gameOver || !gameStarted)
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                if (gameOver)
                {
                    //Restart current scene
                    Scene scene = SceneManager.GetActiveScene();
                    SceneManager.LoadScene(scene.name);
                }
                else
                {
                    //Start the game
                    gameStarted = true;
                }
            }
        }
    }

    void OnGUI()
    {
        if (gameOver)
        {
            GUI.color = Color.red;
            GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 100, 200, 200), "Game Over\nYour score is: " + ((int)score) + "\nPress 'Space' to restart");
        }
        else
        {
            if (!gameStarted)
            {
                GUI.color = Color.red;
                GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 100, 200, 200), "Press 'Space' to start");
            }
        }


        GUI.color = Color.green;
        GUI.Label(new Rect(5, 5, 200, 25), "Score: " + ((int)score));
    }
}
  • قم بإرفاق البرنامج النصي SC_PlatformTile إلى الكائن "TilePrefab"
  • تعيين الكائنات "Obstacle1" و"Obstacle2" و"Obstacle3" إلى مجموعة العوائق

بالنسبة لنقطة البداية ونقطة النهاية، نحتاج إلى إنشاء كائنين للعبة يجب وضعهما في بداية ونهاية المنصة على التوالي:

  • تعيين متغيرات نقطة البداية ونقطة النهاية في SC_PlatformTile

  • احفظ الكائن "TilePrefab" في Prefab ثم قم بإزالته من Scene
  • إنشاء GameObject جديد واستدعائه "_GroundGenerator"
  • قم بإرفاق البرنامج النصي SC_GroundGenerator بالكائن "_GroundGenerator"
  • قم بتغيير موضع الكاميرا الرئيسية إلى (10، 1، -9) وقم بتغيير دورانها إلى (0، -55، 0)
  • قم بإنشاء GameObject جديد، وأطلق عليه اسم "StartPoint" وقم بتغيير موضعه إلى (0, -2, -15)
  • حدد الكائن "_GroundGenerator" وفي SC_GroundGenerator قم بتعيين متغيرات الكاميرا الرئيسية ونقطة البداية والبلاط المسبق الصنع

الآن اضغط على زر التشغيل ولاحظ كيف تتحرك المنصة. بمجرد خروج بلاطة المنصة من عرض الكاميرا، يتم نقلها إلى النهاية مع تنشيط عقبة عشوائية، مما يخلق وهمًا بمستوى لا نهائي (انتقل إلى 0:11).

يجب وضع الكاميرا بشكل مشابه للفيديو، بحيث تتجه المنصات نحو الكاميرا وخلفها، وإلا فلن تتكرر المنصات.

Sharp Coder مشغل فديوهات

الخطوة 2: إنشاء المشغل

ستكون حالة اللاعب عبارة عن كرة بسيطة تستخدم وحدة تحكم ذات قدرة على القفز والانحناء.

  • قم بإنشاء كرة جديدة (GameObject -> 3D Object -> Sphere) ثم قم بإزالة مكون Sphere Collider الخاص بها
  • تعيين "RedMaterial" الذي تم إنشاؤه مسبقًا إليه
  • إنشاء GameObject جديد واستدعائه "Player"
  • حرك الكرة داخل الكائن "Player" وقم بتغيير موضعها إلى (0, 0, 0)
  • قم بإنشاء نص برمجي جديد، وأطلق عليه اسم "SC_IRPlayer" وألصق الكود أدناه بداخله:

ملف SC_IRPlayer.cs

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

[RequireComponent(typeof(Rigidbody))]

public class SC_IRPlayer : MonoBehaviour
{
    public float gravity = 20.0f;
    public float jumpHeight = 2.5f;

    Rigidbody r;
    bool grounded = false;
    Vector3 defaultScale;
    bool crouch = false;

    // Start is called before the first frame update
    void Start()
    {
        r = GetComponent<Rigidbody>();
        r.constraints = RigidbodyConstraints.FreezePositionX | RigidbodyConstraints.FreezePositionZ;
        r.freezeRotation = true;
        r.useGravity = false;
        defaultScale = transform.localScale;
    }

    void Update()
    {
        // Jump
        if (Input.GetKeyDown(KeyCode.W) && grounded)
        {
            r.velocity = new Vector3(r.velocity.x, CalculateJumpVerticalSpeed(), r.velocity.z);
        }

        //Crouch
        crouch = Input.GetKey(KeyCode.S);
        if (crouch)
        {
            transform.localScale = Vector3.Lerp(transform.localScale, new Vector3(defaultScale.x, defaultScale.y * 0.4f, defaultScale.z), Time.deltaTime * 7);
        }
        else
        {
            transform.localScale = Vector3.Lerp(transform.localScale, defaultScale, Time.deltaTime * 7);
        }
    }

    // Update is called once per frame
    void FixedUpdate()
    {
        // We apply gravity manually for more tuning control
        r.AddForce(new Vector3(0, -gravity * r.mass, 0));

        grounded = false;
    }

    void OnCollisionStay()
    {
        grounded = true;
    }

    float CalculateJumpVerticalSpeed()
    {
        // From the jump height and gravity we deduce the upwards speed 
        // for the character to reach at the apex.
        return Mathf.Sqrt(2 * jumpHeight * gravity);
    }

    void OnCollisionEnter(Collision collision)
    {
        if(collision.gameObject.tag == "Finish")
        {
            //print("GameOver!");
            SC_GroundGenerator.instance.gameOver = true;
        }
    }
}
  • قم بإرفاق البرنامج النصي SC_IRPlayer إلى الكائن "Player" (ستلاحظ أنه أضاف مكونًا آخر يسمى Rigidbody)
  • أضف مكون BoxCollider إلى الكائن "Player"

  • ضع الكائن "Player" فوق الكائن "StartPoint" مباشرةً أمام الكاميرا

اضغط على Play واستخدم مفتاح W للقفز ومفتاح S للانحناء. الهدف هو تجنب العوائق الحمراء:

Sharp Coder مشغل فديوهات

تحقق من هذا Horizon Bending Shader.