دروس عداء لا نهاية لها من أجل الوحدة

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

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

لعبة مترو الانفاق سيرفرز

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

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

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

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

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

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

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

بالنسبة للعقبات، سيكون لدي 3 أشكال مختلفة من العوائق، ولكن يمكنك إنشاء أكبر عدد منها حسب الحاجة:

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

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

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

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

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

  • قم بإنشاء سكريبت جديد، وأطلق عليه اسم "SC_PlatformTile" والصق الكود أدناه داخله:

SC_PlatformTile.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" والصق الكود أدناه داخله:

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

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

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

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

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

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

  • قم بإنشاء Sphere جديد (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.

مصدر
📁EndlessRunner.unitypackage26.68 KB
المقالات المقترحة
كيفية إنشاء لعبة مستوحاة من لعبة Flappy Bird في Unity
لعبة صغيرة في الوحدة | المكعب القفاز
كيفية صنع لعبة الثعبان في الوحدة
إنشاء لعبة 2D Brick Breaker في Unity
لعبة صغيرة في الوحدة | تجنب المكعب
زومبي المزرعة | صنع لعبة منصات ثنائية الأبعاد في Unity
إنشاء لعبة ألغاز منزلقة في Unity