הדרכת רץ אינסופית לאחדות

במשחקי וידאו, לא משנה כמה גדול העולם, תמיד יש לו סוף. אבל חלק מהמשחקים מנסים לחקות את העולם האינסופי, משחקים כאלה נכנסים לקטגוריה שנקראת Endless Runner.

Endless Runner הוא סוג של משחק שבו השחקן נע כל הזמן קדימה תוך איסוף נקודות והימנעות ממכשולים. המטרה העיקרית היא להגיע לסוף הרמה מבלי ליפול לתוך המכשולים או להתנגש בהם, אך לעיתים קרובות, הרמה חוזרת על עצמה בלי סוף, ומגדילה בהדרגה את הקושי, עד שהשחקן מתנגש במכשול.

משחק גולשי רכבת תחתית

בהתחשב בכך שגם למחשבים/מכשירי משחקים מודרניים יש כוח עיבוד מוגבל, אי אפשר ליצור עולם אינסופי באמת.

אז איך משחקים מסוימים יוצרים אשליה של עולם אינסופי? התשובה היא על ידי שימוש חוזר באבני הבניין (המכונה איגום אובייקטים), במילים אחרות, ברגע שהגוש עובר מאחורי או מחוץ לתצוגת המצלמה, הוא מועבר לחזית.

כדי ליצור משחק רצים אינסופי ב-Unity, נצטרך ליצור פלטפורמה עם מכשולים ובקר שחקן.

שלב 1: צור את הפלטפורמה

אנו מתחילים ביצירת פלטפורמת רעפים שתאוחסן מאוחר יותר ב- Prefab:

  • צור GameObject חדש וקרא לו "TilePrefab"
  • צור קובייה חדשה (GameObject -> אובייקט תלת מימד -> קובייה)
  • הזז את הקוביה בתוך האובייקט "TilePrefab", שנה את מיקומו ל- (0, 0, 0), וקנה קנה מידה ל- (8, 0.4, 20)

  • לחלופין, ניתן להוסיף מסילות לצדדים על ידי יצירת קוביות נוספות, כך:

עבור המכשולים, יהיו לי 3 וריאציות מכשולים, אבל אתה יכול לעשות כמה שצריך:

  • צור 3 GameObjects בתוך האובייקט "TilePrefab" ושם להם "Obstacle1", "Obstacle2" ו "Obstacle3"
  • למכשול הראשון, צור קובייה חדשה והזז אותה בתוך האובייקט "Obstacle1"
  • קנה קנה מידה של הקובייה החדשה לאותו רוחב של הפלטפורמה והגדל את הגובה שלה (השחקן יצטרך לקפוץ כדי להימנע ממכשול זה)
  • צור חומר חדש, שם לו "RedMaterial" ושנו את צבעו לאדום, ואז הקצה אותו לקובייה (זה רק כדי להבדיל את המכשול מהפלטפורמה הראשית)

  • עבור "Obstacle2" צור כמה קוביות והניח אותן בצורה משולשת, תוך השארת חלל אחד פתוח בתחתית (השחקן יצטרך להתכופף כדי להימנע מהמכשול הזה)

  • ולבסוף, "Obstacle3" הולך להיות כפיל של "Obstacle1" ו-"Obstacle2", בשילוב יחד

  • כעת בחר את כל האובייקטים בתוך מכשולים ושנו את התג שלהם ל-"Finish", זה יהיה נחוץ מאוחר יותר כדי לזהות את ההתנגשות בין שחקן למכשול.

כדי ליצור פלטפורמה אינסופית נזדקק לכמה סקריפטים שיטפלו ב-Object Pool והפעלת מכשולים:

  • צור סקריפט חדש, קרא לו "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" למערך מכשולים

עבור נקודת ההתחלה ונקודת הסיום, עלינו ליצור 2 GameObjects שיש למקם בהתחלה ובסוף הפלטפורמה בהתאמה:

  • הקצה משתני נקודת התחלה ונקודת סיום ב-SC_PlatformTile

  • שמור את האובייקט "TilePrefab" ב-Prefab והסר אותו מהסצנה
  • צור GameObject חדש וקרא לו "_GroundGenerator"
  • צרף את הסקריפט SC_GroundGenerator לאובייקט "_GroundGenerator"
  • שנה את מיקום המצלמה הראשית ל- (10, 1, -9) ושנה את הסיבוב שלה ל- (0, -55, 0)
  • צור GameObject חדש, קרא לו "StartPoint" ושנה את מיקומו ל- (0, -2, -15)
  • בחר את האובייקט "_GroundGenerator" וב-SC_GroundGenerator הקצה את המשתנים המוקדמים של המצלמה הראשית, נקודת ההתחלה והאריחים

כעת לחץ על Play וצפה כיצד הפלטפורמה זזה. ברגע שאריח הפלטפורמה יוצא מתצוגת המצלמה, הוא מוזז אחורה לסוף כאשר מכשול אקראי מופעל, ויוצר אשליה של רמה אינסופית (דלג ל-0:11).

המצלמה חייבת להיות ממוקמת בדומה לסרטון, כך שהפלטפורמות ילכו לכיוון המצלמה ומאחוריה, אחרת הפלטפורמות לא יחזרו.

Sharp Coder נגן וידאו

שלב 2: צור את הנגן

המופע של השחקן יהיה Sphere פשוט באמצעות בקר עם יכולת לקפוץ ולהתכופף.

  • צור 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