人人都能寫遊戲系列(三)Unity 3D平衡球遊戲

引言

本系列中,我會在0美術的情況下,教大家開發幾款簡單的小遊戲。適合Unity的初學者。
本系列其他遊戲開發
今天要開發的就是本系列教程中的第一篇3D遊戲教程,過程比較繁瑣,我會分幾個部分一一講解。

開始的準備

老規矩,我們來新建一個項目,起名爲BalanceBall(平衡球)。
在這裏插入圖片描述
先來佈置一個簡單的場景,以方便我們用來測試接下來要做的各種道具。
場景很簡單,添加一個我們的核心小球(sphere),一個廣闊的場地(plane)。
在這裏插入圖片描述
接下來我們調整一下小球的位置,大小以及命名,並給小球添加上一個rigidbody組件。
在這裏插入圖片描述
因爲小球是我們的玩家(player),所以我們約定俗成的,給小球設置一個tag,在unity中已經有現成的player的tag了,我們直接選上就可以,這個tag在後面,會有很重要的使用。
在這裏插入圖片描述
我們的基礎準備,到這裏就可以了。

基本代碼

控制小球的移動

在平衡球遊戲中,小球的移動是重中之重,如果小球不能動,那麼我們做出花來也沒有用,所以,我們就創建一個名字叫Player的腳本,用來控制小球的移動。
在這裏插入圖片描述
讓小球動起來,我們有很多很多種方式,比較常用的是直接操作的小球的position,但是這種方法,沒有緩慢加速的功能,小球也不會旋轉,會顯得很滑,很假。所以我們這裏採用更加真實的方法,就好像真實世界中小球受到了重力作用移動了一樣。所以我們這裏,使用rigidbody來給小球添加外力。
我們可以用Input.GetKey(KeyCode.W);的方式來添加四個判斷(wasd),來控制小球的移向,我們也可以用更高級的Axis來控制,Axis可以模擬軸移動,類似搖桿的感覺。
我們還希望能在編輯器中隨意控制小球移動的速度,所以還需要一個public的變量。
我們可能會反覆使用小球的rigidbody,所以我們爲了效率,要把它存儲起來。
有了以上的內容,我們就可以寫出小球移動的腳本了。

	Rigidbody rigid;
    public float force=5;
    
    void Start () 
    {
        rigid = transform.GetComponent<Rigidbody>();
    }
    
    
    void Update () 
    {
        rigid.AddForce(new Vector3(Input.GetAxis("Horizontal"),0,Input.GetAxis("Vertical"))*force);
    }
   

將腳本掛載到小球上,運行遊戲,我們的小球已經可以自由運動了。

更進一步

我們的小球可以運動了,但是它很難停下來,這也是不太合理的。我們希望可以控制它停下來。
那麼接下來,我們就要寫腳本讓小球停下來了。
有同學說了,停住還不容易,只要讓小球的速度爲0就可以了。
但是我們更希望小球是緩慢停住,而不是突然停住,那我們應該怎麼辦呢?
答案當然是插值了。我在人人都能寫遊戲系列(一)Unity簡單跳一跳遊戲開發一文中使用了平滑插值,用來壓縮方塊。這裏我們當然也可以使用平滑插值,爲了學習到更多的內容,我們這回使用mathf中的lerp,線性插值來解決這個問題。
lerp是很簡單的,需要三個參數, (float a, float b, float t),a就是起點,b就是終點,t就是0-1的範圍,此函數返回float,是(b-a)*t+a的值(就是一次線性函數),有了這些知識,我們就可以動手升級我們的小球腳本了。

完成的小球腳本如下。

using UnityEngine;

public class Player : MonoBehaviour {

   
    Rigidbody rigid;
    public float force=5;
    //用來控制剎車的快慢
    public float reduce = 0.1f;
    void Start () 
    {
       
        rigid = transform.GetComponent<Rigidbody>();
       
    }
    
    
    void Update () 
    {
        rigid.AddForce(new Vector3(Input.GetAxis("Horizontal"),0,Input.GetAxis("Vertical"))*force);
        //按住空格,小球會緩慢減速。
        if(Input.GetKey(KeyCode.Space))
        {
            rigid.velocity = new Vector3(Mathf.Lerp(rigid.velocity.x, 0, reduce),rigid.velocity.y,Mathf.Lerp(rigid.velocity.z, 0, reduce)); 
        }
    }
}

至此,小球的腳本已經完成了。
tips:Input.GetKey和Input.GetKeyDown都可以獲取按鍵的值,區別在於GetKeyDown只觸發一次,GetKey會持續觸發。

相機跟隨

我們在運行遊戲的時候發現,我們的小球忽遠忽近,感覺很怪,所以我們需要相機始終跟隨着小球。
我們很容易就能發現,相機應該始終在小球的後方,相差一個固定的值。
我們創建一個名字叫Track的腳本。
在這裏插入圖片描述
我們希望能在編輯器中調整相機和小球的偏差,而不是每次運行遊戲才知道調整的合適不合適,這樣有助於我們提升效率。所以我們在腳本的最開始,加上[ExecuteInEditMode],表示,我們不運行的遊戲的時候,此腳本也可以工作。
在這裏插入圖片描述
爲了跟隨小球,我們很明顯的需要一個小球的位置,爲此,我們可以通過Find函數來尋找場景的中的物體,也可以通過public的方法來拖拽復職,我們這裏選用簡單的public方式。

public Transform player;

爲了性能,我們需要保存一下相機自己的位置。
爲了方便調整,我們需要一個public的變量,用來表示相機到小球的距離,我們通常在LateUpdate()中更新相機位置,我們還需要相機看向小球。有了這些想法,腳本很容易就寫完了。

using UnityEngine;
[ExecuteInEditMode]
public class Track : MonoBehaviour 
{
    public Transform player;
    Transform trans;
    public Vector3 dis;
	// Use this for initialization
	void Start () 
    {
        trans = this.transform;
      
	}
	void LateUpdate()
	{
        trans.position = player.position+dis;
        trans.LookAt(player);
	}
}

將腳本掛載到相機上,然後在編輯器面板中調整dis到一個自己覺得合適的值。
在這裏插入圖片描述
運行下游戲,是不是我們的小球已經有模有樣了?

製作道具

光有個小球,我們還是什麼都幹不了的,遊戲的魅力在於就是有各種各樣的關卡和道具,在平衡球遊戲中,主要就是道具了。下面我們就來製作各種各樣的道具。

大風車

大風車就是能把小球打飛的那種。因爲沒有美術,我們就只能女留房號男自強,咳咳,是自己製作了。
我們在場景中創建一個Cylinder(圓柱)
在這裏插入圖片描述
我們更改一下它的長寬高(縮放),讓它變成細長條。並沿着x軸旋轉90度(放倒)。

在這裏插入圖片描述
同樣的,我們再創建一個圓柱,然後調整一下它的旋轉,讓其爲直角,風車的模型就有了。
在這裏插入圖片描述
中間的小球很影響咱們的視野,我們隱藏掉它。
在這裏插入圖片描述
爲了方便我們寫腳本控制風車,我們創建一個空物體,讓這兩根圓柱都成爲其的子物體。
在這裏插入圖片描述
模型已經ok,我們來寫腳本讓它旋轉起來吧。
創建一個名叫Windmill的腳本。
在這裏插入圖片描述
風車的旋轉也很簡單,我們只要讓他不停的轉就可以了,爲了保證幀數穩定,我們使用FixedUpdate。
這裏的轉,我們使用Transform的Rotate方法。爲什麼不使用剛體旋轉?因爲我們希望風車在和小球發生碰撞時,也不會減速和跳起。
經過本系列前面的教程,這種腳本應該對你來說很簡單了。

using UnityEngine;

public class Windmill : MonoBehaviour {

    Transform trans;
    public float speed = 30;
	void Start () 
	{
        trans = this.transform;
	}
	
	
	void FixedUpdate()
	{
        trans.Rotate(Vector3.up * speed * Time.fixedDeltaTime);
	}
}

將腳本掛載到風車的父物體的GameObject上,運行遊戲,我們就可以看到風車在轉動了。
在這裏插入圖片描述
tips:這裏我將小球挪了個位置,防止小球在風車內部
運行測試發現,小球確實不能影響風車的轉動,風車確實可以影響小球的運動。(如果你覺得風車的長度太短,你也可以自行修改)
修改下風車的命名,並將其拖入下方,成爲prefab。
在這裏插入圖片描述
我們第一個道具就完成了!有沒有點成就感啊,嘿嘿。

跳板

跳板是小球踩上去以後,會瞬間的跳起來的一個道具。
藉由跳板,我們的小球就可以跳上更高的平臺。
還是先來製作跳板。跳板的模型選擇可以有多種多樣,爲了更廣的知識面,我們選用Quad(四邊形)。
在這裏插入圖片描述
爲了和地面區分開來,我們給跳板掛載一個紅色的材質。
在這裏插入圖片描述
接着,調整四邊形,讓它躺在地上的合適位置,方便我們的測試。
在這裏插入圖片描述
我們採用觸碰觸發的方式觸發跳躍,而不是碰撞的方式觸發跳躍,所以要勾選上如圖所示內容。
在這裏插入圖片描述
至此,跳板的模型,我們就做好了,接下來來寫跳板的腳本。
跳板的腳本也很簡單,主要是檢測觸發器被觸發,然後施加給小球一個瞬間的力,就可以讓小球跳起了。

using UnityEngine;

public class Jump : MonoBehaviour {

    public float force=10;
	
	private void OnTriggerEnter(Collider other)
	{
        if(other.CompareTag("Player"))
        {
            other.GetComponent<Rigidbody>().AddForce(Vector3.up * force,ForceMode.Impulse);
        }
	}
}

這裏就使用了tag標籤,只有當小球觸碰觸發器的時候纔會跳躍,別的物體觸碰它並不會被彈起。

更進一步

我們發現,小球在剛剛進入紅色區域的時候就已經被彈起了,我們更希望小球完全進入紅色區域在被彈起,那我們應該怎麼辦呢?
這個問題有兩種辦法,第一種是寫腳本,計算小球中心到平面的距離,第二種就是修改觸發器的面積。
這裏採用第二種方法,把觸發器的面積變小,自然小球就能更好的更自然的彈起來了。
刪除mesh collider,添加box collider,將其面積變小,即可完成上述需求。
在這裏插入圖片描述
將跳板更名爲Jump,保存爲prefab,至此,我們的第二個道具,跳板也完成了。

循環移動的木板

在很多遊戲中,就存在循環移動的木板,把玩家從一岸移動到另一岸。這裏,我們也創建一個可以循環移動的木板。
還是先來場景中創建我們的板子,當然了,這裏用cube最方便了。
在這裏插入圖片描述
我們的測試場景中已經有很多東西了,我們已經保存了prefab,就可以刪除一些不必要的東西了。我們把風車 跳板 大地都刪除掉,讓我們的player佔在方塊上。
在這裏插入圖片描述
然後在創建一個方塊,表示成河對面。
在這裏插入圖片描述
在創建一個方塊,將其拍扁,它就是我們的木板了。
在這裏插入圖片描述
場景有了,我們來寫腳本吧。
我們不希望直接在腳本中爲移動距離賦值,因爲我們更希望我們的板子可以通用,不受到河岸距離的限制。
那我們應該怎麼做呢?
很簡單,檢測碰撞!只要碰到河岸就換方向
這樣我們就可以實現了更廣泛的適配。
腳本如下:

using UnityEngine;

public class Board : MonoBehaviour {

    Transform trans;
    //前進方向
    int dir = 1;
    public float speed = 1;
	void Start () 
	{
        trans = this.transform;
	}
	
	void FixedUpdate () 
	{
        trans.position += Vector3.forward * speed*Time.fixedDeltaTime*dir;
	}

	private void OnCollisionEnter(Collision collision)
	{
        if(!collision.gameObject.CompareTag("Player"))
        dir = -dir;
	}
}

這樣,我們的循環移動的木板也做好了。

更進一步

我們運行發現,板子移動速度太快,我們的小球很難在板子上站穩,直接操作position更是無摩擦的抽板子,是在是地獄式難度。爲了降低難度,爲了和物理世界規律相同,我們將移動的代碼改爲使用rigidbody下的MovePosition。於是,腳本變成了如下所示:

using UnityEngine;

public class Board : MonoBehaviour {
    
    Rigidbody rigid;
    //前進方向
    int dir = 1;
    public float speed = 1;
	void Start () {
        rigid = GetComponent<Rigidbody>();
	}
	
	void FixedUpdate () {
        rigid.MovePosition(rigid.position + Vector3.forward * speed * Time.fixedDeltaTime * dir);
	}

	private void OnCollisionEnter(Collision collision)
	{
        if(!collision.gameObject.CompareTag("Player"))
        dir = -dir;
	}
}

經過測試發現,我們的小球站上去後,給了板子一個摩擦力,板子會運行的飛快,這是不合理的,所以我們這回不在移動位置,只控制移動的速度。

using UnityEngine;

public class Board : MonoBehaviour {
    
    Rigidbody rigid;
    //前進方向
    int dir = 1;
    public float speed = 1;
	void Start () {
        rigid = GetComponent<Rigidbody>();
	}
	
	void FixedUpdate () {
        rigid.velocity = Vector3.forward * speed * Time.fixedDeltaTime * dir;
	}

	private void OnCollisionEnter(Collision collision)
	{
        if(!collision.gameObject.CompareTag("Player"))
        dir = -dir;
	}
}

我們給木板添加上一個材質球,更改一下其顏色,與河岸分割開來,更名並保存爲prefab,我們的木板製作,到此結束。
在這裏插入圖片描述

一次性上升踏板

我們這回來製作一次性上升踏板,可以幫助我們的小球上升到更高的位置,這裏涉及了兩個腳本之間的通訊。
還是回到我們的場景,刪除我們的board,然後再添加一個cube。
在這裏插入圖片描述
調整縮放,使其變成一個扁木板,並創建一個小球,賦予小球紅色材質,並安放到板子的合適位置上,使小球有所嵌入到板子中,因爲這樣看起來比較像開關。
在這裏插入圖片描述
接下來,我們要做的就是,當小球觸碰開關的時候,浮板會上升,然後隱藏掉開關,製作成一次性上升浮板。
很顯然,我們需要這個開關和板子之間的通信,也需要上升的速度,上升的最大高度,這樣的參數。
這個上升,我們依然像循環的木板一樣,有很多種方案的選擇。這裏我們先採用剛體的MovePosition看看
先來寫開關的代碼

using UnityEngine;

public class Switch : MonoBehaviour {

	private void OnTriggerEnter(Collider other)
	{
        if(other.gameObject.CompareTag("Player"))
        {
            //通知板子上升

            //隱藏自己
            this.gameObject.SetActive(false);
        }
	}
}

再來寫板子上升的代碼

using UnityEngine;

public class UpBoard : MonoBehaviour 
{

    Rigidbody rigid;
    //指示是否要上升
    bool canup = false;
    //1向上走,-1向下走
    public int dir = 1;
    //上升速度
    public float speed = 20;
    //最大上升高度
    public float max = 10;
    //記錄最開始的y位置
    float startY;
	void Start () 
    {
        rigid = GetComponent<Rigidbody>();
        startY = rigid.position.y;
	}

	void FixedUpdate()
	{
        if (!canup)
            return;
        rigid.MovePosition(rigid.position +Vector3.up*dir*speed*Time.fixedDeltaTime);
        if (rigid.position.y - startY >= max)
            canup = false;
	}
    //用於和開關通信
	public void MoveUp()
    {
        
    }
}

現在我們來寫兩個腳本通信的方法,這件事情上我們也有很多種選擇,比如共用一個全局變量,或者直接Find到物體,再通過GetComponent找到腳本,再修改其中的值,直接用public賦值等等。
我們這裏遵循簡單原則,選擇public賦值的方法。那麼我們先補全開關的腳本。

using UnityEngine;

public class Switch : MonoBehaviour {
    //在面板賦值,需要注意,必須是掛載的物體。
    public UpBoard up;
	private void OnTriggerEnter(Collider other)
	{
        if(other.gameObject.CompareTag("Player"))
        {
            //通知板子上升
            up.MoveUp();
            //隱藏自己
            this.gameObject.SetActive(false);
        }
	}
}

這個要怎麼賦值?先把開關腳本和上升浮板腳本掛好,然後按圖所示。
在這裏插入圖片描述
注意,這裏不能從下面的面板中拖拽上去,那樣會在內存創建一個新的實例,是不會達到我們的效果的。
我們的moveup還是空方法,我們需要補全他。
觀看我們的腳本可以發現,我們只需要將canup賦值爲true,即可讓腳本上升了,所以,完整的浮板腳本如下。

using UnityEngine;

public class UpBoard : MonoBehaviour 
{

    Rigidbody rigid;
    //指示是否要上升
    bool canup = false;
    //1向上走,-1向下走
    public int dir = 1;
    //上升速度
    public float speed = 20;
    //最大上升高度
    public float max = 10;
    //記錄最開始的y位置
    float startY;
	void Start () 
    {
        rigid = GetComponent<Rigidbody>();
        startY = rigid.position.y;
	}

	void FixedUpdate()
	{
        if (!canup)
            return;
        rigid.MovePosition(rigid.position +Vector3.up*dir*speed*Time.fixedDeltaTime);
        if (rigid.position.y - startY >= max)
            canup = false;
	}
    //用於和開關通信
	public void MoveUp()
    {
        canup = true;
    }
}

保存腳本,我們運行遊戲,發現報錯了
在這裏插入圖片描述
這是因爲,我們的上升浮板並沒有添加一個rigidbody組件,所以我們將其填上,再運行遊戲,我們發現,我們的開關沒有消失,小球碰撞到了開關,沒能穿過它,這是因爲我們的開關的碰撞器沒有勾選Is Trigger。
勾選上,再次運行遊戲,我們發現我們的小球把浮板砸下去了,這是因爲物理組件的特性,所以我們需要鎖止浮板的軸。
在這裏插入圖片描述
我們運行發現,我們的板子因爲受到小球的重力作用,仍會下降,這是不符合我們的需要的,而且小球不一定可以一瞬間就碰到開關,這樣的體驗很不好,所以我們採用MovePosition的方案失敗了。所以這裏我們採用直接移動板子的方法。
完整的上升浮板代碼如下

using UnityEngine;

public class UpBoard : MonoBehaviour 
{

    Transform trans;
    //指示是否要上升
    bool canup = false;
    //1向上走,-1向下走
    public int dir = 1;
    //上升速度
    public float speed = 1;
    //最大上升高度
    public float max = 10;
    //記錄最開始的y位置
    float startY;
	void Start () 
    {
        trans = this.transform;
        startY = trans.position.y;
	}

	void FixedUpdate()
	{
        if (!canup)
            return;
        trans.position += Vector3.up * dir * speed * Time.fixedDeltaTime;
        if (trans.position.y - startY >= max)
            canup = false;
	}
    //用於和開關通信
	public void MoveUp()
    {
        canup = true;
    }
}

刪除掉上升浮板的rigidbody組件,調整速度大小,調整命名,最終效果如下
在這裏插入圖片描述
在這裏插入圖片描述
保存爲prefab,我們的上升浮板就製作完成了。

火炮

這是本篇教程中最難的部分,我們還是先來到場景中,製作一個大炮。
先將cube的x拉長,方便我們擺放大炮。
在這裏插入圖片描述
我們知道,大炮是由兩個輪子,一個炮筒組成的。
這三個部分都可以用圓柱體來代替,所以,我們就創建三個圓柱體。把兩個改成扁扁的輪子,一個改成長長的炮筒。
這就是我造的輪子了。
在這裏插入圖片描述
大家可以按這個參數造輪子,也可以按自己覺得合適的大小來。
炮管的參數
在這裏插入圖片描述
這裏我覺得剛纔的輪子太小了,又進行了一波修改,反正只要看起來差不多就行了,畢竟這種東西,也沒有什麼定數。
將兩個輪子擺在炮筒旁邊,我們就完成了大炮的基本構建。
附上新的輪子參數
在這裏插入圖片描述
我們知道,unity在操作物體的時候,是按物體的mesh中心來的,圓柱體的mesh中心在圓柱的中間。
而我們希望操作大炮是操作圓柱的底部,因爲操作中間,當炮筒變成90度的時候,很可能就離開輪子了,那看起來太假了。
我們知道,操作父物體的時候,子物體會跟隨父物體的變化而變化,所以這個問題就很容易解決了,我們直接在炮筒的底部,添加一個空物體,作爲炮管的父物體,這樣,操作這個空物體的transform,炮筒就會跟着動了。
先創建物體,將物體擺放在圓柱的底部,具體做法是,x,z座標和圓柱一致,只修改y值,就可以保證是在圓柱底部的中央了。
在這裏插入圖片描述
接着,把炮筒掛爲其的子物體。這裏直接拖拽炮筒就可以了。
在這裏插入圖片描述
我們還需要一個發射點,就是小球是從哪裏發射出去的,顯然,這應該是在炮筒的頂部,而且隨着炮筒的移動而移動,根據父物體移動子物體會跟隨的道理,我們再創建一個空物體,調整好位置後,掛載到炮筒上,變成炮筒的子物體。
在這裏插入圖片描述
這裏要注意,我們必須把發射點離炮筒遠一些,因爲小球有半徑,如果不夠遠,小球會卡在炮筒裏無法發射。
這個位置也是要經過實際測試纔會知道,現在我就先把它擺在這裏了。
爲了方便管理大炮,我們還需要創建一個空物體,作爲整個大炮的父物體。
在這裏插入圖片描述
然後,我們刪除掉大炮子物體的所有碰撞體,我們來自己給大炮做一個碰撞模型。
在這裏插入圖片描述
現在大炮的碰撞體已經全部刪除了,我們給大炮更改下命名,方便觀察和說明。
在這裏插入圖片描述
我們給Cannon添加上一個Box Collider。
在這裏插入圖片描述
然後點擊,儘可能的調整碰撞碰撞範圍,使碰撞體緊貼大炮。
在這裏插入圖片描述
我調整的效果如下
在這裏插入圖片描述
好了,大炮已經造好,創建一個名爲Cannon的腳本,我們可以開始寫代碼了。
在這裏插入圖片描述
這裏的思路是,當小球碰撞到了大炮,小球就進入大炮,然後我們可以調整炮管的角度,按下指定鍵,就可以發射了。
有幾個關鍵的地方需要注意,首先,小球未和大炮發生碰撞,我們不能操作大炮。其次,如何體現出小球進入了大炮,再然後,如何讓攝像機跟隨大炮。這幾個想明白了,腳本就很好寫了。
先來寫碰撞檢測和如何移動大炮的角度吧。

using UnityEngine;

public class Cannon : MonoBehaviour {
    //移動點的位置。
    public Transform trans;
    Transform player;
    //炮管的位置。
    public Transform gun;
    //移動炮管的速度
    public float speed=20;
	void Start () 
    {
       
	}
	
	void Update () 
    {
        //表示沒碰到大炮時,大炮不會被操作。
        if (player == null)
            return;
        trans.Rotate(Vector3.right * speed * Time.deltaTime * Input.GetAxis("Vertical"));
	}

	void OnCollisionEnter(Collision collision)
	{
        if(collision.gameObject.CompareTag("Player"))
        {
            player = collision.gameObject.transform;
            player.gameObject.SetActive(false);
            //這裏用了取巧的辦法,讓攝像機看向大炮。
            player.position = gun.position;
        }
	}
}

將腳本掛載到Cannon上,拖過拖拽賦值,我們運行遊戲,發現腳本很好的運行了。
在這裏插入圖片描述
接下來,我們來寫如何讓大炮發射小球。
爲了和前面的按鍵區分開來,我們這裏使用g鍵發射。

using UnityEngine;

public class Cannon : MonoBehaviour {
    //移動點的位置。
    public Transform trans;
    Transform player;
    //炮管的位置。
    public Transform gun;
    //發射點的位置
    public Transform shoot;
    //移動炮管的速度
    public float speed=20;
    //發射力
    public float force = 10;
	void Start () 
    {
       
	}
	
	void Update () 
    {
        //表示沒碰到大炮時,大炮不會被操作。
        if (player == null)
            return;
        trans.Rotate(Vector3.right * speed * Time.deltaTime * Input.GetAxis("Vertical"));
        if(Input.GetKeyDown(KeyCode.G))
        {
            //讓小球到發射點位置
            player.position = shoot.position;
            //讓小球可見
            player.gameObject.SetActive(true);
            //施加一個瞬時力,方向是移動點的y軸朝向。
            player.GetComponent<Rigidbody>().AddForce(force*trans.up,ForceMode.Impulse);
        }
	}

	void OnCollisionEnter(Collision collision)
	{
        if(collision.gameObject.CompareTag("Player"))
        {
            player = collision.gameObject.transform;
            player.gameObject.SetActive(false);
            //這裏用了取巧的辦法,讓攝像機看向大炮。
            player.position = gun.position;
        }
	}
}

保存代碼,在場景賦值,調整好角度,按下g,就可以發射了。
在這裏插入圖片描述

更進一步

我們運行遊戲會發現,大炮可以360度旋轉,會插到地面中去,所以,我們要調整大炮所能旋轉的角度。比如最基本的,我們讓大炮只能0-90度旋轉。
那我們應該怎麼做呢?
有同學說了,這還不簡單?只要判斷transform.rotation.x就可以了。
事實是這樣麼?
在transform的文檔中是這麼寫的
在這裏插入圖片描述
這個返回的是Quaternion,並不是所謂的角度。
那麼Quaternion,又是什麼呢?
文檔中說了

四元數用於表示旋轉。它們結構緊湊,不受萬向節鎖定的影響,可以輕鬆插補。 Unity內部使用四元數來表示所有旋轉。

四元數是一個很不容易理解的概念。它的出現是爲了解決歐拉旋轉中的萬向鎖問題。具體的更多的關於四元數的內容,大家可以自行百度,這裏因爲篇幅原因,只講如何使用。
四元數和歐拉角的互化,有着一套極爲繁瑣的公式,不過別擔心,unity引擎已經爲我們做好了這套轉化工作。
通過四元數的eulerAngles方法,我們就可以輕鬆獲取到xyz三軸的旋轉角了。
有了這些知識,我們控制旋轉角度看似就很簡單了。

using UnityEngine;

public class Cannon : MonoBehaviour {
    //移動點的位置。
    public Transform trans;
    Transform player;
    //炮管的位置。
    public Transform gun;
    //發射點的位置
    public Transform shoot;
    //移動炮管的速度
    public float speed=20;
    //發射力
    public float force = 10;
	void Start () 
    {
      
	}
	
	void Update () 
    {
        //表示沒碰到大炮時,大炮不會被操作。
        if (player == null)
            return;
        //控制旋轉
        Quaternion a = trans.rotation;
        if (a.eulerAngles.x >= 90 && Input.GetAxis("Vertical") >= 0)
            return;
        if(a.eulerAngles.x <= 0 && Input.GetAxis("Vertical") <= 0)
            return;
        trans.Rotate(Vector3.right * speed * Time.deltaTime * Input.GetAxis("Vertical"));
        if(Input.GetKeyDown(KeyCode.G))
        {
            //讓小球到發射點位置
            player.position = shoot.position;
            //讓小球可見
            player.gameObject.SetActive(true);
            //施加一個瞬時力,方向是移動點的朝向。
            player.GetComponent<Rigidbody>().AddForce(force*trans.up,ForceMode.Impulse);
        }
	}

	void OnCollisionEnter(Collision collision)
	{
        if(collision.gameObject.CompareTag("Player"))
        {
            player = collision.gameObject.transform;
            player.gameObject.SetActive(false);
            //這裏用了取巧的辦法,讓攝像機看向大炮。
            player.position = gun.position;
        }
	}
}

我們保存腳本,回到場景中運行代碼,我們發現,代碼並沒有按我們想象中的那樣生效,這是爲什麼呢?
答案很簡單,那是因爲四元數返回的角度是從0-360度的,不存在負角,而場景中,則是正負0-180的,所以我們需要一步轉化,將角度轉化成我們面板看到的那樣。

  float changeAngle(float angle)
    {
        if (angle > 180)
            return angle - 360;
        return angle;
    }

有了這個,我們在把角度放到裏面轉化一下,這樣子,我們的腳本就貌似完成了。

using UnityEngine;

public class Cannon : MonoBehaviour {
    //移動點的位置。
    public Transform trans;
    Transform player;
    //炮管的位置。
    public Transform gun;
    //發射點的位置
    public Transform shoot;
    //移動炮管的速度
    public float speed=20;
    //發射力
    public float force = 10;
	void Start () 
    {
      
	}
	
	void Update () 
    {
        //表示沒碰到大炮時,大炮不會被操作。
        if (player == null)
            return;
        Quaternion a = trans.rotation;
       //轉化角度
        if (changeAngle(a.eulerAngles.x) >= 90 && Input.GetAxis("Vertical") >= 0)
            return;
        if(changeAngle(a.eulerAngles.x) <= 0 && Input.GetAxis("Vertical") <= 0)
            return;
        trans.Rotate(Vector3.right * speed * Time.deltaTime * Input.GetAxis("Vertical"));
        if(Input.GetKeyDown(KeyCode.G))
        {
            //讓小球到發射點位置
            player.position = shoot.position;
            //讓小球可見
            player.gameObject.SetActive(true);
            //施加一個瞬時力,方向是移動點的朝向。
            player.GetComponent<Rigidbody>().AddForce(force*trans.up,ForceMode.Impulse);
        }
	}

	void OnCollisionEnter(Collision collision)
	{
        if(collision.gameObject.CompareTag("Player"))
        {
            player = collision.gameObject.transform;
            player.gameObject.SetActive(false);
            //這裏用了取巧的辦法,讓攝像機看向大炮。
            player.position = gun.position;
        }
	}

    float changeAngle(float angle)
    {
        if (angle > 180)
            return angle - 360;
        return angle;
    }
}

我們再次運行發現,它有時候會生效,讓炮筒無法旋轉,有時候不會卡住,直接轉到了底部。這是爲什麼呢?
通過Debug發現,a.eulerAngles.x返回的角在正方向上是0-90-0的過程。由於90爲極限值,用戶按住w時候,由於大炮速度過快,很容易就超過了90,並沒有觸發a.eulerAngles.x >= 90這個判斷,而到底部之後,a.eulerAngles.x開始返回大於180度的角,經過轉化,會觸發changeAngle(a.eulerAngles.x) <= 0 這個判斷,導致大炮卡住。如果在最開始,直接向後運動,會直接觸發changeAngle(a.eulerAngles.x) <= 0 這個判斷卡住,也就是說,我們的問題就在於90度這個位置。我們應該怎麼辦呢?
很簡單,還是利用四元數,這回我們不是直接返回a.eulerAngles.x的角度去判斷,而是通過a和世界正上方向的夾角來判斷,這樣,我們在正方向上也會返回大於90度的值,這個問題就解決了。

最後修改後的大炮代碼如下:

using UnityEngine;

public class Cannon : MonoBehaviour {
    //移動點的位置。
    public Transform trans;
    Transform player;
    //炮管的位置。
    public Transform gun;
    //發射點的位置
    public Transform shoot;
    //移動炮管的速度
    public float speed=20;
    //發射力
    public float force = 10;
	void Start () 
    {
      
	}
	
	void Update () 
    {
        //表示沒碰到大炮時,大炮不會被操作。
        if (player == null)
            return;
        Quaternion a = trans.rotation;
        Quaternion c = Quaternion.Euler(Vector3.up);
        //c是世界的正上方向,通過判斷ac夾角,我們來判斷是否超過了90度。
        if (changeAngle(Quaternion.Angle(a,c)) >= 90 && Input.GetAxis("Vertical") >= 0)
            return;
        if(changeAngle(a.eulerAngles.x) <= 0 && Input.GetAxis("Vertical") <= 0)
            return;
        //既然都用了四元數了,我們也把旋轉角改成四元數的模式吧!
        Quaternion b = Quaternion.Euler(Vector3.right * speed * Time.deltaTime * Input.GetAxis("Vertical"));
        trans.rotation = a * b;
        if(Input.GetKeyDown(KeyCode.G))
        {
            //讓小球到發射點位置
            player.position = shoot.position;
            //讓小球可見
            player.gameObject.SetActive(true);
            //施加一個瞬時力,方向是移動點的朝向。
            player.GetComponent<Rigidbody>().AddForce(force*trans.up,ForceMode.Impulse);
        }
	}

	void OnCollisionEnter(Collision collision)
	{
        if(collision.gameObject.CompareTag("Player"))
        {
            player = collision.gameObject.transform;
            player.gameObject.SetActive(false);
            //這裏用了取巧的辦法,讓攝像機看向大炮。
            player.position = gun.position;
        }
	}

    float changeAngle(float angle)
    {
        if (angle > 180)
            return angle - 360;
        return angle;
    }
}

小結

到此爲止,我們的道具就製作完成了,由於篇幅原因,進一步的關卡/死亡/通關等判斷和佈置,請期待下一篇章。

支持我

您的支持,就是我創作的最大動力

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章