用Unity去釋放自己的惡意——我們來(lái)實(shí)現(xiàn)一下《掘地求升》

作者:沈琰
前言
好吧,這個(gè)標(biāo)題起的有些驚悚,但是對(duì)于這期要做的游戲來(lái)說(shuō)也并不算無(wú)的放矢。
相信大家對(duì)前段時(shí)間大火的《Getting Over It With Bennett Foddy》這款游戲并不陌生,如果不熟悉這個(gè)名字,那么《掘地求升》這個(gè)接地氣的名字想必有所耳聞。
這款游戲一反“游戲帶給人快樂(lè)”的這個(gè)主旨,而是以傷害別人為目的的。

甚至游戲制作人本人都坦誠(chéng)的把這句話寫(xiě)在了Steam商店的游戲簡(jiǎn)介上。

雖然是這么一款玩起來(lái)很痛苦的游戲,但是最后還是火了。
這款游戲雖然自己玩起來(lái)經(jīng)常有砸鼠標(biāo)的沖動(dòng),但是當(dāng)在直播平臺(tái)上看著別的主播玩卻意外的節(jié)目效果爆炸,因此該游戲迅速的占領(lǐng)各大直播平臺(tái)。
最有名的當(dāng)屬一位名叫kimdoe的韓國(guó)主播,當(dāng)他在直播中歷經(jīng)12小時(shí)的奮戰(zhàn)卻一個(gè)不慎從半山腰跌入起始點(diǎn),真的是隔著屏幕都能感覺(jué)到那散發(fā)出來(lái)的滿滿的絕望。

我們這期就來(lái)嘗試著用Unity去復(fù)制一下這個(gè)充滿惡意游戲。
1.關(guān)于游戲?qū)崿F(xiàn)方式的猜想
老實(shí)說(shuō)當(dāng)一開(kāi)始思考這個(gè)游戲如何實(shí)現(xiàn)時(shí),我其實(shí)是懵逼的。因?yàn)榘凑宅F(xiàn)實(shí)中的物理學(xué)來(lái)看我們的爬山猛男擺出這么一個(gè)奇怪的姿勢(shì)明顯的不合理:

根據(jù)杠桿原理,猛男要斜著支撐起自己所要施加的力矩怕是這世界上最強(qiáng)壯的人也做不到。所幸這只是在游戲中模擬,既然真實(shí)物理學(xué)不太好用,我們就用代碼來(lái)實(shí)現(xiàn)。
要達(dá)到這種效果我們可以用代碼自己模擬一個(gè)絕世猛男,調(diào)整猛男手臂的力矩讓其拿著的錘子的錘頭始終指向鼠標(biāo)方向。
不過(guò)這么一來(lái)猛男手臂和手腕關(guān)節(jié)的扭矩的計(jì)算必然很復(fù)雜,即便能做到也是很繁瑣的做法。
首先我們過(guò)濾掉一切表象的東西,從根本的方向來(lái)思考這個(gè)問(wèn)題,為此我特意找到了這個(gè)游戲的原型SexyHiking:

根據(jù)原型的表現(xiàn)形式來(lái)看,我們可以把這個(gè)問(wèn)題再簡(jiǎn)化一下。

如上圖,如果我們把錘子運(yùn)動(dòng)的路徑看做一個(gè)圓,猛男身體的重心看做圓心,我們可以發(fā)現(xiàn)錘子相對(duì)于地面運(yùn)動(dòng)方向的向量剛好是這個(gè)圓的切線,而重心運(yùn)動(dòng)的方向則剛好相反,因此我們可以在錘子觸碰到任何碰撞體的時(shí)候給身體一個(gè)與錘子相反向量的力來(lái)模擬這個(gè)效果。
2.實(shí)現(xiàn)方法
1.場(chǎng)景搭建
說(shuō)干就干,我們來(lái)實(shí)際操作一下。
新建一個(gè)場(chǎng)景,用簡(jiǎn)易的3D物體來(lái)組裝成猛男和錘子,掛上不同的材質(zhì)用顏色區(qū)分一下。

然后分別掛上Rigibody,因?yàn)槲覀兿氲氖亲?D平面運(yùn)行,因此在Rigibody上分別鎖了Z軸的移動(dòng)和X,Y軸的旋轉(zhuǎn),順便把錘子上的重力去掉,因?yàn)榇龝?huì)要用代碼去控制錘子的位置。
2.控制錘頭
新建一個(gè)腳本掛載在父節(jié)點(diǎn)上,分別獲取身體和錘子。
我們想要的效果是錘子能一直跟隨鼠標(biāo)移動(dòng)且錘頭一直指向鼠標(biāo),思路是在錘頭添加一個(gè)空的子節(jié)點(diǎn)作為錘頭的錨點(diǎn),然后讓整個(gè)錘子圍繞著這個(gè)錨點(diǎn)的Z軸旋轉(zhuǎn)讓錘柄指向身體的方向。
public GameObject body;
public GameObject hammer;
Rigidbody body_rig;
Rigidbody hammer_rig;
Transform hammer_anchor;
void Start()
{
body_rig = body.GetComponent<Rigidbody>();
hammer_rig = hammer.GetComponent<Rigidbody>();
hammer_anchor = hammer.transform.GetChild(2);
}
//物理相關(guān)的操作一般最好放在FixedUpdate里進(jìn)行,與系統(tǒng)的物理計(jì)算保持同步
private void FixedUpdate()
{
HammerControl();
}
void HammerControl()
{
//獲取鼠標(biāo)在屏幕上的坐標(biāo)并轉(zhuǎn)換為世界坐標(biāo)
Vector2 MousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
//通過(guò)修改錘子身上剛體的速度讓錘子移動(dòng),向量的起點(diǎn)用錘頭的坐標(biāo)
//注意這里要讓錘頭的錨點(diǎn)與錘子本身的坐標(biāo)的Z值相等,為了讓旋轉(zhuǎn)軸與世界坐標(biāo)的Z軸平行,同理鼠標(biāo)坐標(biāo)的Z值也直接使用錘子的坐標(biāo)的Z值
hammer_rig.velocity = (new Vector3(MousePosition.x, MousePosition.y, hammer.transform.position.z) - hammer_anchor.position) * 10;
//獲取身體到鼠標(biāo)的方向
Vector3 direction = (new Vector3(MousePosition.x, MousePosition.y, hammer_anchor.position.z) - new Vector3(body.transform.position.x, body.transform.position.y, hammer_anchor.position.z)).normalized;
//讓錘子沿著錘頭錨點(diǎn)轉(zhuǎn)向身體的方向
hammer.transform.RotateAround(hammer_anchor.position, Vector3.Cross(hammer_anchor.up, direction), Vector3.Angle(hammer_anchor.up, direction));
}
效果如圖:

但是現(xiàn)在問(wèn)題來(lái)了,因?yàn)橹皇呛?jiǎn)單模擬功能的效果,所以猛男并沒(méi)有手,但是最終我們是希望錘子活動(dòng)的范圍離猛男有一個(gè)最大距離的限制,這樣看起來(lái)好像是有一只無(wú)形的手來(lái)操作錘子一樣,該如何計(jì)算這個(gè)范圍呢?

由圖可見(jiàn)這個(gè)問(wèn)題的思路是,當(dāng)錘頭錨點(diǎn)在最大活動(dòng)范圍里面時(shí)是跟著鼠標(biāo)的坐標(biāo)走,當(dāng)處于最大活動(dòng)范圍外的時(shí)候是跟著錘頭錨點(diǎn)與身體坐標(biāo)的向量和最大移動(dòng)范圍形成的圓的交點(diǎn)走。這個(gè)思路用于代碼上就是當(dāng)鼠標(biāo)移動(dòng)到最大距離范圍之外時(shí)得到當(dāng)前身體到鼠標(biāo)位置的方向向量,然后讓這個(gè)向量的長(zhǎng)度等于最大距離的長(zhǎng)度。
那么,我們?cè)诖嘶A(chǔ)上修改一下代碼。
public float MaxDistance;
float RelativeDistance;
//單獨(dú)抽出一個(gè)函數(shù)獲取最大距離以外的轉(zhuǎn)換后的鼠標(biāo)坐標(biāo)
Vector2 GetConfinedPosition(Vector2 mouseposition)
{
Vector2 Confined_MousePosition;
//去掉body坐標(biāo)的Z值,避免計(jì)算距離時(shí)的影響
Vector2 body_position = new Vector2(body.transform.position.x, body.transform.position.y);
//計(jì)算當(dāng)前鼠標(biāo)位置和身體位置的相對(duì)距離
RelativeDistance = Vector2.Distance(mouseposition, body_position);
if (RelativeDistance > MaxDistance)
{
//當(dāng)相對(duì)距離大于自己設(shè)置的最大距離時(shí),獲取轉(zhuǎn)換以后的目標(biāo)坐標(biāo)
//這里的思路需要稍稍轉(zhuǎn)一個(gè)彎,一開(kāi)始是獲取的是長(zhǎng)度為最大距離,方向?yàn)樯眢w到鼠標(biāo)方向的向量。
//在這個(gè)基礎(chǔ)上加上身體的當(dāng)前坐標(biāo),其等于是將這個(gè)向量的起始點(diǎn)設(shè)置為身體的坐標(biāo)。
//最后向量與坐標(biāo)點(diǎn)可以直接相互轉(zhuǎn)換,此時(shí)轉(zhuǎn)換后的目標(biāo)坐標(biāo)就等于這個(gè)向量。
Confined_MousePosition = (mouseposition - body_position).normalized * MaxDistance + body_position;
}
else
{
//若相對(duì)距離小于最大距離那么鼠標(biāo)當(dāng)前坐標(biāo)就是目標(biāo)坐標(biāo)
Confined_MousePosition = mouseposition;
}
return Confined_MousePosition;
}
然后把HammerControl()函數(shù)內(nèi)的MousePosition的賦值修改一下:
//獲取鼠標(biāo)在屏幕上的坐標(biāo)并轉(zhuǎn)換為世界坐標(biāo),若相對(duì)距離大于最大距離則獲得轉(zhuǎn)換后的坐標(biāo)
Vector2 MousePosition = GetConfinedPosition(Camera.main.ScreenToWorldPoint(Input.mousePosition));
最后調(diào)整MaxDistance到一個(gè)合適的值,效果如下:

3.控制身體的逆向運(yùn)動(dòng)
現(xiàn)在離實(shí)現(xiàn)就差最后一步了,前面思考實(shí)現(xiàn)方式的時(shí)候說(shuō)過(guò),錘頭的移動(dòng)方向與理論上身體移動(dòng)的方向剛好是相反的,并且同為以身體重心為原點(diǎn),錘子的最大移動(dòng)距離為半徑的圓的切線。那么我們順著這個(gè)思路去做。
首先還有一個(gè)先決條件,錘子不能憑空受力,必須是杵在地上或者掛在障礙物上才行。
我們先在錘頭上加一個(gè)碰撞盒子并在錘子上添加一個(gè)新建腳本,用幾句簡(jiǎn)單的代碼用來(lái)檢測(cè)錘頭是否碰撞到東西。
public class CollisionDetection : MonoBehaviour
{
public bool IsCollision;
private void OnCollisionEnter(Collision collision)
{
IsCollision = true;
}
private void OnCollisionExit(Collision collision)
{
IsCollision = false;
}
}
這里要注意的是錘子是錘頭的父物體,所以即便碰撞盒子不在錘子上,作為父物體的錘子依然能檢測(cè)到子物體上的碰撞信息,反過(guò)來(lái)由于錘頭上并沒(méi)有剛體組件,所以腳本掛在錘頭上是檢測(cè)不到碰撞信息的。
然后當(dāng)檢測(cè)到碰撞時(shí)給予身體一個(gè)相反的速度值。
void HammerControl()
{
//在給hammer_rig.velocity賦值后添加下面的代碼
if(hammer.GetComponent<CollisionDetection>().IsCollision)
{
BodyControl(hammer_rig.velocity);
}
}
void BodyControl(Vector3 velociy)
{
body_rig.velocity = -velociy;
}
最后效果如下:

4.后續(xù)優(yōu)化思路
到這里功能已經(jīng)基本實(shí)現(xiàn)了,但是并沒(méi)有結(jié)束,在手感上依然有很多可以調(diào)整的地方。比如原作中快速的杵地可以把自己“甩”飛起來(lái),通過(guò)調(diào)整障礙物上碰撞盒子的物理材質(zhì)達(dá)到原作中那詭異的摩擦力,添加物理關(guān)節(jié)模擬手的效果等等。
不過(guò)這些調(diào)整相當(dāng)?shù)穆闊┡c耗時(shí),想必原作者為了能給大家?guī)?lái)痛苦在最后調(diào)整參數(shù)與設(shè)計(jì)關(guān)卡的時(shí)候也是花費(fèi)了巨大的心力,這究竟是一種怎么樣的精神...

限于篇幅在這里就不展開(kāi)了,大家可以盡情發(fā)揮自己的想象力去坑自己的朋友。

結(jié)束
在本期文章里我們用相對(duì)簡(jiǎn)單的方法實(shí)現(xiàn)了《Getting Over It With Bennett Foddy》里的功能,并沒(méi)有用到什么復(fù)雜的組件,代碼量也很少,初學(xué)者也能較為容易實(shí)現(xiàn)。觀看到這里的同學(xué)大可自己動(dòng)手試試。原作也不見(jiàn)得就是用的同樣的方法,大家有什么其他的實(shí)現(xiàn)方法也歡迎在評(píng)論區(qū)留言。
工程連接:https://github.com/tank1018702/unity_001/tree/master/BennettFoodyMustDie
最后想系統(tǒng)學(xué)習(xí)游戲開(kāi)發(fā)的童鞋,歡迎訪問(wèn) http://levelpp.com/
游戲開(kāi)發(fā)攪基QQ群:869551769
微信公眾號(hào):皮皮關(guān)