引言
本系列中,我會在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;
}
}
小結
到此爲止,我們的道具就製作完成了,由於篇幅原因,進一步的關卡/死亡/通關等判斷和佈置,請期待下一篇章。
支持我
您的支持,就是我創作的最大動力