如何用高效的方式實現遊戲世界中的擊中效果

前言

模擬一個導彈或者炮彈擊中一個標目,是大部分遊戲經常要做得的一件事,通常我們會使用遊戲引擎中自帶的碰撞檢測事件來實現這個效果,或者是判斷距離,但這兩者都不是最優的方法,下面我會介紹一種我目前使用的方法。這裏着重以Unity舉例來說,但是不限制於Unity引擎,也就是適用於任何一款遊戲引擎。

爲什麼不使用碰撞檢測

1.理由

  1. 碰撞檢測是一個很耗費性能的操作,讀者可以想象一下,爲什麼引擎自帶的碰撞系統會在物體和目標發生碰撞時會有現實世界中的物理特性,比如物體上的某一個點與目標發生碰撞就會導致物體的自身旋轉屬性發生一系列變化,這都是非常複雜的數學計算產生的結果,而且最重要的一點,CPU還要去計算到底是物體的哪一個點和目標發生碰撞,所以這一系列的數學計算絕對都是很複雜的,所以如果你要做的是一個或者幾個單位還可以,但是一旦單位太多的話,CPU機會計算太多的物理數據,所以這是個很耗費性能的操作,如果讀者想做一個多單位的項目,那麼這種方法基本可以放棄。
  2. 當然還有一點在Unity中,遊戲是按幀執行的,讀者可以理解爲自己看到的遊戲就是又一系列連環畫組成的,只是以極小時間間隔來這些畫面,一般幀率在24幀以上,人眼就認爲這是一個連貫不卡頓的動畫,也就是一秒鐘人眼至少要處理24張連環畫,才能認爲這是副連貫的畫面,在Unity中有Update函數,在虛幻中也有Tick函數,來實現這個效果;
    爲什麼我要說這個,遊戲是按真執行的,也就是遊戲物體的位置是按幀更新的,如果一個遊戲物體的速度太快,也就是說,每幀位置之間的距離很大,就會出現碰撞失效的方法,當然前提是目標位置剛好在物體的兩幀位置之間。
    2.結論
    碰撞檢測是一個很不靠譜的方法,能不用就不用!

用計算距離來判斷

1.理由

  1. 除開碰撞檢測之外還可以通過距離去判斷物體是否到達目標點,一般這應該是一個廢棄碰撞檢測方法後首選的方案,因爲相對於碰撞檢測,他不需要CPU去計算那麼複雜的數據,大大降低了CPU的運算量,這個方法接收的計算量肯定是遠遠少於碰撞檢測的,也就是說它能計算更多的單位,至少比碰撞檢測多,但是他仍然不是最優的方法,要理解這一點就要去理解在遊戲引擎中的兩個位置之間的距離是如何計算的:
    (這裏略過向量的講解)
    我們知道,如果我們想去計算一個物體的和另一個物體的距離,那麼我們首先要知道這兩個物體的位置,在遊戲中也就是Position屬性,然後做差,得到一個新的向量,再取這個向量的模,下面我用C#代碼演示:
	    void Distance()
		{
    		Vector3 tempPos = new Vector3();//建立一個臨時的變量

   			tempPos = target - self;//計算出從自身指向目標的一個向量

    		NormOfVector(tempPos);//求出距離
		}

		float NormOfVector(Vector3 vector)//求一個向量的模
		{
    		return Mathf.Sqrt(Mathf.Pow(vector.x, 2) 
    		+ Mathf.Pow(vector.y, 2) + Mathf.Pow(vector.z, 2));
		}

		void Start()
		{
   			Distance();//最後調用計算距離
		}
注:“^”運算符在C#中適用與Int類型變量,Mathf.Pow()爲求一個值的平方的函數

我們可以看到在這個方法中CPU要進行一次開根號的操作,作爲程序員,我們要知道的是CPU在計算 “+,*”的速度是要比計算“-,/, Sqrt”的速度快的,所以頻繁的開根號也是一個很耗時的操作,能少用就少 用,最好不用,那麼以這個例子來說開個爲什麼耗時呢?這裏不再多說,讀者可以自己去找找相關的實現 代碼;
2. 結論
對於這個方法來說,雖然相對碰撞比較實用,但是他需要每幀都計算一個距離,這也是很耗費時間的操作,另外你還需要控制導彈的運動軌跡,如果軌跡是直線,也仍需要判斷距離。

第三種方法——Lerp()插值

在我之前的項目中,我嘗試了一種新的方法,用插值去實現導彈的飛行和擊中判定。
思路:
如何用插值實現?
Unity系統內置的Vector3中提供了一種對Vector3變量進行線性插值的方法,Lerp()函數,它的函數原型是這樣:

public static Vector3 Lerp(Vector3 a, Vector3 b, float t);

有了這個函數,我們可以先獲取開始位置和目標位置,然後用時間去判斷是否擊中目標,其中t的值是限制在0 - 1之間的數,當 t == 0時,這個函數範圍的位置是開始位置,當 t == 0.5f 時這個函數返回的位置時位於開始位置和結束位置之間的位置,當 t == 1 時函數返回的位置就是結束位置,這也是我們判斷的依據,
其中他是線性插值也是我們的重要判斷依據:
在這裏插入圖片描述
這個測試證明了它是一個線性插值,下面是測試代碼:

		public float tempValue;
		
		public float interval;
		
		public Vector3 self;
		
		public Vector3 target;
		
		public Transform testCube;
		
		void Update()
		{
			SetLerpVector3();
		}
		
		void SetLerpVector3()
		{
			tempValue = 0;
			for(int i = 0; i < testCube.childCount; ++i)
			{
		    	tempValue += interval;
		    	testCube.GetChild(i).position = Vector3.Lerp(self, target, tempValue);
			}
		}
我這裏也提供一個Lerp函數內部實現的版本(當然,這是以我自己的理解寫的):
    public float Lerp(float a, float b, float t)
	{
	    return a + (b - a) * Mathf.Clamp01(t);
	}
很好理解的函數對吧,Vedctor3.Lerp()內部就是把每個分量都調用這個函數進行插值;

這樣我們把判斷距離的問題轉化爲了判段一個值是否大於1的問題,大大的減少了計算量,當然你會說這麼做只適用於直線飛行的導彈,實際上我們可以對這個位置上的值做任何改變,只要保持頭和尾都在對應的起點和終點就行了,那麼怎麼做呢?

我會在下一次更新時說明解決方法,當前前提是你得有3D數學的基礎,否則你可能會看的一頭霧水,同時我也會解決導彈的朝向問題。

結尾

我所介紹的東西都是偏於實戰,因爲這些知識都是我從實戰中摸索所來的,所以理論東西可能欠缺,只是我的理解,由於本人不喜歡吧東西講的太深奧,而往往講出一些有邏輯推理而得到的結論,往往會更有說服力,更容易讓讀者理解,所以文檔基於實踐,不基於理論。

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