2016年12月22日 星期四

Day 23: 遊戲背景

昨天做完結束畫面後,這個遊戲大致完成了。

但遊戲進行時,背景一片藍藍的,實在很醜。所以今天就來把背景變得動態、好看一點。


先說一下我想做的背景長什麼樣子。我希望背景可以分為早上和晚上,早上背景呈淺藍色,並有一顆太陽從螢幕右側緩緩移向左側,晚上背景則呈深藍色,太陽則變成月亮。此外,早上會不斷有雲從上往下飄,讓玩家有主角在向上長高的感覺。晚上則把飄動的雲改成星星。



在Scene中建立一個新的Sprite,命名為「Background」,並把它的Sprite指定為一個全白的矩形,這就是隨著早上和晚上改變顏色的背景了。把它的Transfrom Z變成正數(如5),使它在所有Game Object的最後方。

建立一個名為「BackgroundAnimation.cs」的C# Script。我一步一步來完成要呈現的動畫。先來做緩緩由淺藍變成深藍,再緩緩變回淺藍的背景。

public class BackgroundAnimation : MonoBehaviour

{
    //白天或晚上的持續時間

    public float dayNightDuration;

    //白天時背景的顏色(淺藍色)

    public Color dayColor;

    //晚上時背景的顏色(深藍色)

    public Color nightColor;


    void Start()
    {

        StartCoroutine (MainCoroutine ());

    }


    //控制白天<->晚上的主要Coroutine

    IEnumerator MainCoroutine()

    {

        while (true) {
            //晚上,從nightColor變到dayColor

            StartCoroutine (ColorLerpCoroutine (nightColor, dayColor));
 
  
            //過了dayNightDuration時間,變成早上

            yield return new WaitForSeconds (dayNightDuration);
   
            //白天,從dayColor變到nightColor

            StartCoroutine (ColorLerpCoroutine (dayColor, nightColor));


            //過了dayNightDuration時間,變成晚上

            yield return new WaitForSeconds (dayNightDuration);
        }

    }
 

    //控制背景顏色的Coroutine

    IEnumerator ColorLerpCoroutine(Color fromColor, Color toColor)

    {

        float i = 0f;


        while (i <= 1f) {
            //計算上影格到這影格的時間佔dayNightDuration多少時間,並加至i上

            i += Time.deltaTime / dayNightDuration;

            //設定SpriteRenderer的color

            GetComponent< SpriteRenderer > ().color = Color.Lerp (fromColor, toColor, i);

            yield return null;

        }

    }
}

這段程式碼有許多複雜的地方。首先來講public變數,dayNightDuration代表白天和晚上的持續時間,也就是由白天轉換到晚上、晚上轉換到白天所需的時間。同為Color型態的dayColornightColor則可讓我在Inspector中選則白天和晚上背景的顏色。

Start()中,簡單地使MainCoroutine()開始。

MainCoroutine()是這段程式碼中最主要的Coroutine,用以控制白天和晚上的變化。我讓遊戲開始於晚上,並使ColorLerpCoroutine(nightColor, dayColor)開始,讓背景的顏色由nightColor緩緩轉變為dayColor。呼叫ColorLerpCoroutine()緩緩更變顏色時,MainCoroutine()yield return new WaitForSeconds(dayNightDuration)等待顏色更變完成,邁入早上。

最後,來看ColorLerpCoroutine()。這其實是一個Unity Scripting中非常典型的Coroutine。它會在一段時間內緩緩改變Game Object的某一屬性。我先設float i=0f,進入while後,i會不斷隨時間增加,增加的量為「過去的時間」和「更變屬性動畫的總時間」的比例,也就是Time.deltaTime / dayNightDuration。如此,在一次次的while中,i會不斷隨時間由0增加到1。但這個i究竟是要幹嘛的呢?其實是為了底下呼叫線性插值法(Linear Interpolation,也就是Lerp)用的。Color.Lerp(fromColor, toColor, i)是使用插值法(fromColor + i x (toColor - fromColor))回傳一個fromColortoColor之間的數。更改完顏色,以yield return null把控制交還給Unity,等待下一個影格再次運行while()

回到Unity Editor,為背景Sprite加上Background Animation (Script) Component,設定好早上晚上的持續時間與顏色,進入Play Mode,背景顏色就會慢慢變換了。

接著來做移動的月亮與太陽。為BackgroundAnimation加上幾行public變數:

public Transform sunMoonStartTransform; //太陽/月亮起始位置

public Transform sunMoonEndTransform;  //太陽/月亮最終位置

public GameObject sun;  //太陽

public GameObject moon;  //月亮

再多寫一個Coroutine

IEnumerator SunMoonMovingCoroutine(GameObject obj)

{


    float i = 0f;

    while (i <= 1f) {

        i += Time.deltaTime / dayNightDuration;

        obj.transform.position = Vector3.Lerp (sunMoonStartTransform.transform.position, sunMoonEndTransform.transform.position, i);

        yield return null;

    }


}

這個Coroutine和改變背景顏色的Coroutine很像,只是把Color換成Vector3。這會讓月亮或太陽隨時間從sunMoonStartTransform的位置移動到sunMoonEndTransform的位置。

寫完新的Coroutine就可以來為MainCoroutine加料了。在MainCoroutine//晚上//白天 處各加上

StartCoroutine (SunMoonMovingCoroutine (moon));




StartCoroutine (SunMoonMovingCoroutine (sun));


回到Unity Editor。新增兩個Game Object,分別代表太陽/月亮的起點和終點,並增加太陽和月亮的Game Object到Scene上,再把它們拉至Background Animation (Script) Component對應的欄位。進入Play Mode,現在不只背景顏色變化,日月也會交替了。

最後,來做下降的星星和雲吧!把不同形狀的星星和雲的Sprite都拉到Scene上,為它們增加Random Initial Speed (Script)、Destroy After Seconds (Script),並設定好數值(Destroy After Seconds應設為從螢幕最上方到螢幕最下方所需的時間)。把它們都拉到Project欄成為Prefab,就可以再度開啟BackgroundAnimation.cs,為背景動畫加工了。

在BackgroundAnimation.cs再新增幾個public變數。

public Transform cloudStarMinSpawnTransform; //星星或雲生成的最小(左)位置

public Transform cloudStarMaxSpawnTransform; //星星或雲生成的最大(右)位置

public GameObject[] cloudPrefabs; //所有雲的Prefab


public GameObject[] starPrefabs; //所有星星的Prefab
public float cloudStarSpawnMinInterval;  //一波星星或雲的最小間隔時間

public float cloudStarSpawnMaxInterval;  //一波星星或雲的最大間隔時間

public int cloudStarSpawnMinAmount; //一波星星或雲的最小個數

public int cloudStarSpawnMaxAmount; //一波星星或雲的最大個數

接著,再新增一個Coroutine:

IEnumerator SpawnCloudStarCoroutine(bool cloud)

{

    while (true) {
        //要產生星星或雲的數量

        int spawnAmount = Random.Range (cloudStarSpawnMinAmount, cloudStarSpawnMaxAmount + 1);

        while (spawnAmount > 0) {
            //產生

            Instantiate(
cloud ? cloudPrefabs[Random.Range(0, cloudPrefabs.Length)] : starPrefabs[Random.Range(0, starPrefabs.Length)],

                new Vector3(

                    Random.Range(cloudStarMinSpawnTransform.transform.position.x, cloudStarMaxSpawnTransform.transform.position.x),

                    cloudStarMinSpawnTransform.transform.position.y,

                    cloudStarMinSpawnTransform.transform.position.z

                ),

                Quaternion.identity

            );


            spawnAmount--;

        }
  

        //等待下一波的時間

        yield return new WaitForSeconds (Random.Range (cloudStarSpawnMinInterval, cloudStarSpawnMaxInterval));

    }

}

這個Coroutine相當簡單,每隔一段時間產生一波星星或雲,而每波星星或雲的數量都不一樣(在[cloudStarSpawnMinAmount, cloudStarSpawnMaxAmount]之間)。每個產生的星星或雲都在cloudStarMaxSpawnTransformcloudStarMinSpawnTransform之間。因為這兩個座標只有X軸相異,所以也只有X軸需呼叫Random.Range()產生隨機數值。


完成新的Coroutine後,馬上來修變MainCoroutine()

IEnumerator MainCoroutine()

{
    //儲存上一個生成星星或雲的Coroutine

    Coroutine spawnCoroutine = null;



    while (true) {
        //若上一個生成雲的Coroutine存在,則停止它

        if (spawnCoroutine != null)

            StopCoroutine (spawnCoroutine);


        //呼叫生成星星Coroutine

        spawnCoroutine = StartCoroutine (SpawnCloudStarCoroutine (false));

        //—-↓舊的程式碼↓—-//

        StartCoroutine (ColorLerpCoroutine (nightColor, dayColor));

        StartCoroutine (SunMoonMovingCoroutine (moon));


        yield return new WaitForSeconds (dayNightDuration);

        //—-↑舊的程式碼↑—-//
 
        //停止生成星星的Coroutine

        StopCoroutine (spawnCoroutine);

        //呼叫生成雲的Coroutine

        spawnCoroutine = StartCoroutine (SpawnCloudStarCoroutine (true));

        //—-↓舊的程式碼↓—-//

        StartCoroutine (ColorLerpCoroutine (dayColor, nightColor));

        StartCoroutine (SunMoonMovingCoroutine (sun));


        yield return new WaitForSeconds (dayNightDuration);
        //—-↑舊的程式碼↑—-//

    }

}

沒錯,Coroutine可以像變數一樣被傳來傳數。因為在呼叫下一個SpawnCloudStarCoroutine之前,必須先把前一個停止(生成雲之前需把生成星星的Coroutine停掉,反之亦然)。所以必須另宣告一個Coroutine形態的變數來儲存上一個Coroutine,之後才能以StopCoroutine()停止它。此外,一開始因為spawnCoroutinenull,所以必須多一個if式來檢查,不然會停止到不存在的Coroutine。

回到Unity Editor,再新增兩個代表星星和雲生成範圍的Game Object,拖到Cloud Star Min/Max Spawn欄,並設定好它們生成的數值。進入Play Mode,星星和雲出現啦!

此外,要讓背景的雲和星星更多樣化,可以為它們的Prefab再加上下面兩段Script來隨機它們的旋轉角度和大小比例。

//隨機大小比例
public class RandomInitialLocalScale : MonoBehaviour 

{

    public float minVal;

    public float maxVal;



    void Start()

    {

        transform.localScale = transform.localScale * Random.Range (minVal, maxVal);

    }

}




//隨機旋轉角度
public class RandomInitialRotation : MonoBehaviour 

{
    
void Start()

    {

        transform.rotation = Quaternion.Euler (0f, 0f, Random.Range (0f, 359f));

    }

}



待續。

沒有留言 :

張貼留言