برنامج تعليمي للعبة Endless Runner على Unity
في ألعاب الفيديو، مهما كان حجم العالم، فإنه دائمًا له نهاية. لكن بعض الألعاب تحاول محاكاة العالم اللانهائي، وتندرج مثل هذه الألعاب تحت فئة تسمى Endless Runner.
Endless Runner هو نوع من الألعاب حيث يتحرك اللاعب باستمرار للأمام أثناء جمع النقاط وتجنب العوائق. الهدف الرئيسي هو الوصول إلى نهاية المستوى دون الوقوع في العوائق أو الاصطدام بها، ولكن في كثير من الأحيان، يتكرر المستوى إلى ما لا نهاية، مما يزيد من الصعوبة تدريجيًا، حتى يصطدم اللاعب بالعائق.
إذا أخذنا في الاعتبار أن حتى أجهزة الكمبيوتر/أجهزة الألعاب الحديثة لديها قوة معالجة محدودة، فمن المستحيل إنشاء عالم لانهائي حقًا.
إذن كيف تخلق بعض الألعاب وهمًا بعالم لا نهائي؟ الإجابة هي إعادة استخدام كتل البناء (أو تجميع الكائنات)، بمعنى آخر، بمجرد أن تنتقل الكتلة إلى الخلف أو خارج عرض الكاميرا، يتم نقلها إلى الأمام.
لإنشاء لعبة عداء لا نهاية لها في 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_PlatformTile" وقم بلصق الكود أدناه بداخله:
منصة 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" ثم قم بلصق الكود أدناه بداخله:
برنامج 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).
يجب وضع الكاميرا بشكل مشابه للفيديو، بحيث تتجه المنصات نحو الكاميرا وخلفها، وإلا فلن تتكرر المنصات.
الخطوة 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 للانحناء. الهدف هو تجنب العوائق الحمراء:
تحقق من هذا Horizon Bending Shader.