SpriteKit框架詳細(xì)解析(五) —— 基于SpriteKit的游戲編程的三角函數(shù)(一)

版本記錄

版本號 時間
V1.0 2017.10.19 星期五

前言

SpriteKit框架使用優(yōu)化的動畫系統(tǒng)衷模,物理模擬和事件處理支持創(chuàng)建基于2D精靈的游戲。接下來這幾篇我們就詳細(xì)的解析一下這個框架。相關(guān)代碼已經(jīng)傳至GitHub - 刀客傳奇改览,感興趣的可以閱讀另外幾篇文章。
1. SpriteKit框架詳細(xì)解析(一) —— 基本概覽(一)
2. SpriteKit框架詳細(xì)解析(二) —— 一個簡單的動畫實例(一)
3. SpriteKit框架詳細(xì)解析(三) —— 創(chuàng)建一個簡單的2D游戲(一)
4. SpriteKit框架詳細(xì)解析(四) —— 創(chuàng)建一個簡單的2D游戲(二)

開始

首先看一下寫作環(huán)境

Swift 4, iOS 11, Xcode 9

一個常見的誤解是游戲程序員需要了解很多關(guān)于數(shù)學(xué)的知識缤言。 雖然計算距離和角度確實需要數(shù)學(xué)宝当,但在理解了一些基本概念后,實際上很容易胆萧。

在本教程中庆揩,您將了解一些重要的三角函數(shù)以及如何在游戲中使用它們。 然后跌穗,您將通過使用SpriteKit框架開發(fā)一個簡單的太空射擊iOS游戲來應(yīng)用這些理論订晌。

如果您之前從未使用過SpriteKit或計劃為游戲使用不同的框架,請不要擔(dān)心 - 本教程中涵蓋的數(shù)學(xué)適用于您可能選擇使用的任何游戲引擎蚌吸。

注意:您將在本教程中構(gòu)建的游戲使用加速度計锈拨,因此您需要一個iOS設(shè)備和一個開發(fā)者帳戶。

三角函數(shù)(或簡稱為trig)僅僅意味著用三角形計算(這就是三角函數(shù)的由來)羹唠。

你可能沒有意識到這一點奕枢,但游戲中充滿了三角形。 例如肉迫,假設(shè)您有一個太空飛船游戲验辞,并且您想要計算這兩艘船之間的距離:

你有每艘船的X和Y位置,但你怎么能找到對角白線的長度喊衫?

那么跌造,你可以簡單地在每個船的中心點之間畫一條線,形成一個這樣的三角形:

請注意,此三角形的一個角具有90度的角度壳贪。 這被稱為直角三角形陵珍,本教程中將要處理的三角形類型。

任何時候你可以將游戲中的某些東西表達(dá)為具有90度直角的三角形 - 例如圖片中兩個精靈之間的空間關(guān)系 - 你可以使用三角函數(shù)對它們進行計算违施。

例如互纯,在這個太空飛船游戲中,您可能希望:

  • 讓一艘船朝另一艘船的方向射擊激光
  • 讓一艘船開始向另一艘船的方向移動以追逐
  • 如果敵艦距離過近磕蒲,則播放警告音效

所有這一切留潦,以及更多,你可以用三角函數(shù)來完成辣往!


Your Arsenal of Functions

首先兔院,讓我們看一下理論。 別擔(dān)心站削,我會保持簡短坊萝,這樣你就可以盡快進行有趣的編碼。

這些是構(gòu)成直角三角形的部分:

在上圖中许起,對角線側(cè)稱為hypotenuse十偶。 它總是出現(xiàn)在直角形上,是三邊中最長的一個园细。

當(dāng)從三角形的左下角看時惦积,剩下的兩個邊被稱為相鄰邊adjacent和相反邊opposite

如果從右上角看三角形珊肃,則相鄰和相對的兩側(cè)會切換位置:

Alpha(α)beta(β)是另外兩個角度的名稱荣刑。 您可以將這些角度稱為任何您想要的角度(只要它聽起來像希臘語!)伦乔,但通常alpha是感興趣角的角度,β是對角的角度董习。 換句話說烈和,您可以相對于alpha標(biāo)記相對和相鄰的邊。

很酷的是皿淋,如果你只知道兩個元素(邊和非直角的組合)招刹,三角函數(shù)允許你使用三角函數(shù)正弦,余弦和正切找出所有剩余的邊和角度窝趣。 例如疯暑,如果你知道其中一個邊的任何角度和長度,那么你可以很容易地得出其他邊和角的長度和角度:

你可以看到正弦哑舒,余弦和正切函數(shù)(通掣菊縮寫為sin,cos和tan)只是比率。 同樣越锈,如果你知道其中一個邊的alpha和長度仗嗦,那么sin,cos和tan是將兩邊和角度聯(lián)系在一起的比率甘凭。

把sin稀拐,cos和tan函數(shù)想象成“黑盒子” - 你插入數(shù)字并取回結(jié)果。 它們是標(biāo)準(zhǔn)庫函數(shù)丹弱,幾乎可用于包括Swift在內(nèi)的所有編程語言德撬。

注意:三角函數(shù)的行為可以通過將圓形投影到直線上來解釋,但是您不需要知道如何導(dǎo)出這些函數(shù)以便使用它們躲胳。 如果你很好奇蜓洪,有很多網(wǎng)站和視頻可以解釋細(xì)節(jié);查看Math is Fun網(wǎng)站的一個例子泛鸟。

1. Know Angle and Length, Need Sides

我們來看一個例子吧蝠咆。 假設(shè)船之間的α角是45度,斜邊是10點長北滥。

然后刚操,您可以將這些值插入公式:

sin(45) = opposite / 10

要為斜邊解決這個問題,你只需簡單地改變這個公式:

opposite = sin(45) * 10

45度的正弦值為0.707(四舍五入到小數(shù)點后三位)再芋,填充在公式中的結(jié)果為:

opposite = 0.707 * 10 = 7.07

2. Know 2 Sides, Need Angle

當(dāng)你已經(jīng)知道一個角度時菊霜,上面的公式很有用,但情況并非總是如此 - 有時你知道兩邊的長度并且正在尋找它們之間的角度济赎。 要導(dǎo)出角度鉴逞,可以使用反三角函數(shù),也稱為弧函數(shù):

angle = arcsin(opposite/hypotenuse)
angle = arccos(adjacent/hypotenuse)
angle = arctan(opposite/adjacent)

如果sin(a)= b司训,那么arcsin(b)= a也是如此构捡。 在這些反向三角函數(shù)中,您可能會在實踐中使用arc tangent (arctan)壳猜,因為它可以幫助您找到斜邊勾徽。 有時這些函數(shù)被寫成sin-1cos-1tan-1统扳,所以不要讓它混淆你喘帚。

3. Know 2 Sides, Need Remaining Side

有時你可能知道兩個邊長,你需要知道第三邊長咒钟。

這就是幾何學(xué)的Pythagorean Theorem可以實現(xiàn)的地方:

a2 + b2 = c2

或者吹由,就三角形方面而言:

opposite2 + adjacent2 = hypotenuse2

如果你知道任何兩個邊,計算第三個只是填寫公式并取平方根朱嘴。 這是游戲中非常常見的事情倾鲫,您將在本教程中多次執(zhí)行此操作。

4. Have Angle, Need Other Angle

最后,考慮角度级乍。 如果你知道三角形中的一個非直角舌劳,那么找出另一個角是非常簡單的事情。 在三角形中玫荣,三個角度的總和始終為180度甚淡。 因為這是一個直角三角形,它有一個90度角捅厂。

alpha + beta + 90 = 180

或者干脆:

alpha + beta = 90

剩下的兩個角度必須加起來90度贯卦。 所以如果你知道alpha,你可以計算beta焙贷,反之亦然撵割。

這些都是你需要知道的公式! 在實踐中使用哪一個取決于您已經(jīng)擁有的部分辙芍。 通常你要么有角度啡彬,要么至少有一個邊長,或者你沒有角度故硅,但你有兩個邊長庶灿。

理論已經(jīng)足夠了吧,讓我們把這些東西付諸實踐吃衅。


Begin the Trigonometry! - 開始三角函數(shù)

看一下開始starter項目是一個SpriteKit項目往踢。 在iOS設(shè)備上構(gòu)建并運行它。 你會看到有一艘宇宙飛船可以用加速度計和屏幕中心的大炮一起移動徘层。 兩個精靈都有一個完整的生命條峻呕。

此刻,宇宙飛船在移動時不會旋轉(zhuǎn)趣效。 看到宇宙飛船在移動時所朝向的位置而不是始終指向上方將是有幫助的瘦癌。 要旋轉(zhuǎn)宇宙飛船,您需要知道旋轉(zhuǎn)它的角度跷敬。 但你不知道那是什么佩憾;你有速度矢量。 那你如何從矢量中獲得一個角度呢干花?

考慮一下你所知道的。 玩家具有X方向速度長度和Y方向速度長度:

如果你重新排列這些楞黄,你可以看到它們形成一個三角形:

在這里你知道相鄰的(playerVelocity.dx)和相反的(playerVelocity.dy)邊長池凄。

所以基本上,你知道一個直角三角形的兩邊鬼廓,你想找到一個角度(Know 2 Sides, Need Angle的情況)肿仑,所以你需要使用反向三角函數(shù)之一:arcsin,arccos或arctan。

您知道的兩側(cè)是您需要的角度的相對側(cè)和相鄰側(cè)尤慰。因此馏锡,您將需要使用arctan函數(shù)來查找船的旋轉(zhuǎn)角度。請記住伟端,如下所示:

angle = arctan(opposite / adjacent)

Swift標(biāo)準(zhǔn)庫包含一個計算反正切的atan()函數(shù)杯道,但它有一些限制。首先责蝠,x / y產(chǎn)生與-x / -y完全相同的值党巾,這意味著您將獲得相反速度的相同角度輸出。其次霜医,三角形內(nèi)部的角度并不完全是您想要的角度 - 您想要相對于一個特定軸的角度齿拂,該角度可以是與atan()返回的角度偏移90度,180度或270度肴敛。

您可以編寫一個四向if語句署海,通過考慮速度符號來確定角度所在的象限,然后應(yīng)用正確的偏移量來計算出正確的角度医男。但是砸狞,有一個更簡單的方法:

對于這個特定問題,不使用atan()昨登,使用函數(shù)atan2(_:_ :)更簡單趾代,它將x和y分量作為單獨的參數(shù),并正確地確定整體旋轉(zhuǎn)角度丰辣。

angle = atan2(opposite, adjacent)

將以下代碼添加到GameScene.swiftupdatePlayer(_ :)的末尾:

let angle = atan2(playerVelocity.dy, playerVelocity.dx)
playerSprite.zRotation = angle

請注意撒强,Y坐標(biāo)先行。 記住第一個參數(shù)是opposite邊笙什。 在這種情況下飘哨,Y坐標(biāo)與您嘗試測量的角度相反。

構(gòu)建并運行應(yīng)用程序以進行嘗試:

嗯琐凭,這似乎沒有正常工作芽隆。 宇宙飛船肯定會旋轉(zhuǎn),但它指向的方向與它前進的方向不同统屈!

這里是原因:宇宙飛船精靈圖像直指向上胚吁,這對應(yīng)于0度的默認(rèn)旋轉(zhuǎn)值。 但是按照數(shù)學(xué)慣例愁憔,0度的角度不會指向上方腕扶,而是指向右側(cè),沿X軸:

要解決此問題吨掌,請從旋轉(zhuǎn)角度減去90度:

playerSprite.zRotation = angle - 90

Radians, Degrees and Points of Reference - 弧度半抱,度數(shù)和參考點

一般人傾向于將角度視為0到360(度)之間的值脓恕。 數(shù)學(xué)家通常用弧度來測量角度,用π表示(希臘字母Pi窿侈,聽起來像“餡餅”但味道不好)炼幔。

一個弧度是沿著圓弧行進半徑長度時獲得的角度。 你可以做到2π次(大約6.28次)史简,然后再次回到圓圈的開頭乃秀。

注意半徑(直黃線)與弧(紅色曲線)的長度相同乘瓤。 兩個長度相等的角是一個弧度环形!

因此,雖然您可以看到從0到360的角度值衙傀,但您也可以從0到2π看到它們抬吟。 大多數(shù)計算機數(shù)學(xué)函數(shù)都以弧度為單位。 SpriteKit也使用弧度進行所有角度測量统抬。 atan2(_:_ :)函數(shù)返回弧度值火本,但您已嘗試將該角度偏移90度。

由于您將使用弧度和度數(shù)聪建,因此有一種方法可以輕松地在兩者之間進行轉(zhuǎn)換钙畔。 轉(zhuǎn)換非常簡單。 由于圓中有2π弧度或360度金麸,因此π等于180度擎析。 要將弧度轉(zhuǎn)換為度數(shù),可以除以π并乘以180挥下;要將度數(shù)轉(zhuǎn)換為弧度揍魂,可以除以180并乘以π。

GameScene上面添加以下兩個常量:

let degreesToRadians = CGFloat.pi / 180
let radiansToDegrees = 180 / CGFloat.pi

最后棚瘟,編輯updatePlayer(_ :)中的旋轉(zhuǎn)代碼以使用degreesToRadians相乘:

playerSprite.zRotation = angle - 90 * degreesToRadians

Build并再次運行现斋。 你會看到宇宙飛船終于旋轉(zhuǎn)并面向它前進的方向。


Bouncing Off the Walls

你有一個可以使用加速度計移動的宇宙飛船偎蘸。 你正在使三角函數(shù)使它指向它前進的方向庄蹋。

讓太空飛船卡在屏幕的邊緣并不是很令人滿意,而你要通過讓它從屏幕邊框反彈來解決這個問題迷雪!

首先限书,從updatePlayer(_ :)刪除這些行:

newX = min(size.width, max(0, newX))
newY = min(size.height, max(0, newY))

替換成下面

if collidedWithVerticalBorder {
  playerAcceleration.dx = -playerAcceleration.dx
  playerVelocity.dx = -playerVelocity.dx
  playerAcceleration.dy = playerAcceleration.dy
  playerVelocity.dy = playerVelocity.dy
}

if collidedWithHorizontalBorder {
  playerAcceleration.dx = playerAcceleration.dx
  playerVelocity.dx = playerVelocity.dx
  playerAcceleration.dy = -playerAcceleration.dy
  playerVelocity.dy = -playerVelocity.dy
}

如果記錄了碰撞持偏,則反轉(zhuǎn)加速度和速度值拍鲤,導(dǎo)致船再次反彈亚皂。

構(gòu)建并運行以試用它新锈。

彈跳起作用,但似乎有點精力充沛所刀。 問題在于你不會期望太空船會像橡皮球那樣彈跳 - 它會在碰撞時失去大部分能量凿宾,并以比預(yù)先更低的速度反彈坠非。

let maxPlayerSpeed: CGFloat = 200下面添加另一個常量:

let bordercollisionDamping: CGFloat = 0.4

現(xiàn)在误澳,使用以下內(nèi)容替換剛添加到updatePlayer(_ :)的代碼:

if collidedWithVerticalBorder {
  playerAcceleration.dx = -playerAcceleration.dx * bordercollisionDamping
  playerVelocity.dx = -playerVelocity.dx * bordercollisionDamping
  playerAcceleration.dy = playerAcceleration.dy * bordercollisionDamping
  playerVelocity.dy = playerVelocity.dy * bordercollisionDamping
}

if collidedWithHorizontalBorder {
  playerAcceleration.dx = playerAcceleration.dx * bordercollisionDamping
  playerVelocity.dx = playerVelocity.dx * bordercollisionDamping
  playerAcceleration.dy = -playerAcceleration.dy * bordercollisionDamping
  playerVelocity.dy = -playerVelocity.dy * bordercollisionDamping
}

您現(xiàn)在通過阻尼值bordercollisionDamping來加速耻矮。這允許您控制碰撞中損失的能量。在這種情況下忆谓,您可以在撞到屏幕邊緣后使宇宙飛船僅保留其速度的40%裆装。

為了好玩,使用bordercollisionDamping的值來查看此常量的不同值的效果倡缠。如果你使它大于1.0哨免,宇宙飛船實際上從碰撞中獲得能量!

您可能已經(jīng)注意到一個小問題:將太空飛船瞄準(zhǔn)屏幕底部昙沦,使其繼續(xù)一遍又一遍地撞入邊框琢唾,您會看到它在向上和向下之間開始斷斷續(xù)續(xù)。

使用反正切來找到一對X和Y分量之間的角度只有在X和Y值相當(dāng)大的情況下才能正常工作盾饮。在這種情況下采桃,阻尼系數(shù)將速度降低到幾乎為零。當(dāng)您將atan2(_:_ :)應(yīng)用于非常小的值時丘损,即使這些值的微小變化也會導(dǎo)致最終角度發(fā)生很大變化普办。

解決此問題的一種方法是在速度非常慢時不改變角度。這聽起來是打電話給你的老朋友畢達(dá)哥拉斯的一個很好的理由徘钥。

現(xiàn)在你實際上并沒有存儲船的speed衔蹲。 相反,您存儲velocity呈础,它是矢量等效物(參見here有關(guān)speedvelocity之間差異的解釋)舆驶,其中一個分量在X方向,一個分量在Y方向猪落。 但是為了得出關(guān)于船的speed的任何結(jié)論(例如它是否太慢而不值得旋轉(zhuǎn)船)贞远,你需要將這些X和Y速度分量組合成一個標(biāo)量值。

在這里笨忌,你在前面討論過的Know 2 Sides蓝仲,Need Remaining Side case中。

正如您所看到的官疲,宇宙飛船的真實速度 - 它每秒在屏幕上移動的點數(shù) - 是由X方向的速度和Y方向的速度形成的三角形的斜邊袱结。

根據(jù)畢達(dá)哥拉斯公式:

true speed = √(playerVelocity.dx2 + playerVelocity.dy2)

updatePlayer(_ :)中刪除此代碼塊:

let angle = atan2(playerVelocity.dy, playerVelocity.dx)
playerSprite.zRotation = angle - 90 * degreesToRadians

替換成下面

let rotationThreshold: CGFloat = 40

let speed = sqrt(playerVelocity.dx * playerVelocity.dx + playerVelocity.dy * playerVelocity.dy)
if speed > rotationThreshold {
  let angle = atan2(playerVelocity.dy, playerVelocity.dx)
  playerSprite.zRotation = angle - 90 * degreesToRadians
}

建立并運行。 您會看到飛船旋轉(zhuǎn)在屏幕邊緣看起來更加穩(wěn)定途凫。 如果你想知道40的價值來自哪里垢夹,答案是:實驗。 將print()語句放入代碼中以查看飛船通常會到達(dá)邊界的速度有助于調(diào)整此值维费,直到感覺正確為止果元。


Blending Angles for Smooth Rotation - 混合角度以實現(xiàn)平滑旋轉(zhuǎn)

當(dāng)然促王,修復(fù)一件東西會破壞別的東西。 嘗試減慢飛船直到它停止而晒,然后翻轉(zhuǎn)設(shè)備蝇狼,以便飛船必須轉(zhuǎn)身并以另一種方式飛行。

以前倡怎,這會發(fā)生一個很好的動畫迅耘,你實際看到船轉(zhuǎn)向。 但是因為你剛剛添加了一些防止船只在低速時改變角度的代碼监署,現(xiàn)在轉(zhuǎn)彎非常突然颤专。 這是一個小細(xì)節(jié),但正是這些細(xì)節(jié)才能制作出色的應(yīng)用和游戲钠乏。

修復(fù)方法是不立即切換到新角度栖秕,而是在一系列連續(xù)幀中逐漸將其與先前角度混合。 這會重新引入轉(zhuǎn)動動畫缓熟,同時防止船舶在沒有足夠快速移動時旋轉(zhuǎn)累魔。

這種“混合”聽起來很奇特,但實際上很容易實現(xiàn)够滑。 它需要您跟蹤太空船在更新之間的角度垦写。 在GameScene類中添加以下屬性:

var playerAngle: CGFloat = 0

將從rotationThreshold聲明開始的代碼行替換為updatePlayer(_ :)中最后一個if語句的結(jié)尾,代碼如下:

let rotationThreshold: CGFloat = 40
let rotationBlendFactor: CGFloat = 0.2

let speed = sqrt(playerVelocity.dx * playerVelocity.dx + playerVelocity.dy * playerVelocity.dy)
if speed > rotationThreshold {
  let angle = atan2(playerVelocity.dy, playerVelocity.dx)
  playerAngle = angle * rotationBlendFactor + playerAngle * (1 - rotationBlendFactor)
  playerSprite.zRotation = playerAngle - 90 * degreesToRadians
}

playerAngle通過將它們與混合因子相乘來組合新角度和之前的角度彰触。 換句話說梯投,新角度僅對您在太空船上設(shè)置的實際旋轉(zhuǎn)貢獻(xiàn)20%。 隨著時間的推移况毅,更多的新角度被添加分蓖,宇宙飛船最終指向它前進的方向。

構(gòu)建并運行以驗證從一個旋轉(zhuǎn)角度到另一個旋轉(zhuǎn)角度不再發(fā)生突然變化尔许。

現(xiàn)在嘗試順時針和逆時針旋轉(zhuǎn)一圈么鹤。 你會注意到,在轉(zhuǎn)彎的某個時刻味廊,宇宙飛船突然向相反方向旋轉(zhuǎn)了360度蒸甜。 它總是發(fā)生在圓圈的同一點。 這是怎么回事余佛?

atan2(_:_ :)返回并且+π和-π之間的角度(在+180和-180度之間)柠新。 這意味著如果當(dāng)前角度非常接近+π,然后再轉(zhuǎn)動一點辉巡,它將繞回到-π(反之亦然)恨憎。

這實際上相當(dāng)于圓圈上的相同位置(就像-180和+180度是相同的點),但你的混合算法不夠聰明郊楣,沒有考慮和意識到這一點 - 它認(rèn)為角度已經(jīng)跳了整整360度(又名 2π弧度)憔恳,再近一步瓤荔,它需要在相反的方向旋轉(zhuǎn)360度以回升。

要修復(fù)它喇嘱,您需要識別角度何時超過該閾值茉贡,并相應(yīng)地調(diào)整playerAngle。 向GameScene類添加一個新屬性:

var previousAngle: CGFloat = 0

再次者铜,將從rotationThreshold聲明開始的代碼行替換為update Player(_ :)中最后一個if語句的末尾,替換為:

let rotationThreshold: CGFloat = 40
let rotationBlendFactor: CGFloat = 0.2

let speed = sqrt(playerVelocity.dx * playerVelocity.dx + playerVelocity.dy * playerVelocity.dy)
if speed > rotationThreshold {
  let angle = atan2(playerVelocity.dy, playerVelocity.dx)
  
  // did angle flip from +π to -π, or -π to +π?
  if angle - previousAngle > CGFloat.pi {
    playerAngle += 2 * CGFloat.pi
  } else if previousAngle - angle > CGFloat.pi {
    playerAngle -= 2 * CGFloat.pi
  }
  
  previousAngle = angle
  playerAngle = angle * rotationBlendFactor + playerAngle * (1 - rotationBlendFactor)
  playerSprite.zRotation = playerAngle - 90 * degreesToRadians
}

現(xiàn)在放椰,您要檢查當(dāng)前角度和前一個角度之間的差異作烟,以觀察閾值0和π(180度)之間的變化。 這應(yīng)該可以解決問題砾医。

構(gòu)建并運行拿撩。 你應(yīng)該沒有更多的問題來轉(zhuǎn)動你的宇宙飛船!


Using Trig to Find Your Target - 使用三角函數(shù)查找目標(biāo)

這是一個很好的開始 - 你有一艘飛船很順利地移動如蚜! 但到目前為止压恒,小宇宙飛船還是無憂無慮,因為大炮沒有做任何事情错邦。 讓我們改變這一點探赫。

大炮由兩個精靈組成:固定底座和可以旋轉(zhuǎn)以瞄準(zhǔn)玩家的炮塔。 你希望大炮的炮塔始終指向玩家撬呢。 為了實現(xiàn)這一點伦吠,你需要弄清楚炮塔和玩家之間的角度。

為了弄清楚這一點魂拦,它將與飛船旋轉(zhuǎn)計算非常相似毛仪,以面向其前進方向。 這一次芯勘,三角形來自兩個精靈的中心:

同樣箱靴,您可以使用atan2(_:_ :)來計算此角度。 在GameScene中添加以下方法:

func updateTurret(_ dt: CFTimeInterval) {
  let deltaX = playerSprite.position.x - turretSprite.position.x
  let deltaY = playerSprite.position.y - turretSprite.position.y
  let angle = atan2(deltaY, deltaX)
  
  turretSprite.zRotation = angle - 90 * degreesToRadians
}

deltaXdeltaY有助于測量玩家精靈和炮塔精靈之間的距離荷愕。 您將這些值插入atan2(_:_ :)以獲取它們之間的相對角度衡怀。

和以前一樣,您需要轉(zhuǎn)換此角度以包括與X軸(90度)的偏移路翻,以便精靈正確定向狈癞。 請記住atan2(_:_ :)總是給你斜邊和0度線之間的角度;它不是三角形內(nèi)的角度茂契。

將以下代碼添加到update(_:)結(jié)尾:

updateTurret(deltaTime)

Build并運行蝶桶。 炮塔現(xiàn)在總是指向宇宙飛船。

挑戰(zhàn):真正的大炮不太可能瞬間移動掉冶。 相反真竖,它總是在追趕脐雪,略微落后于船的位置。

您可以通過將舊角度與新角度“blending”來完成此操作恢共,就像您使用太空船的旋轉(zhuǎn)角度一樣战秋。 混合因子越小,炮塔需要越多時間趕上宇宙飛船讨韭。 看看你是否可以自己實現(xiàn)這個脂信。


Using Trig for Collision Detection - 使用三角函數(shù)進行碰撞檢測

宇宙飛船可以直接飛過大炮而不會產(chǎn)生任何影響。 如果它在與大炮相撞時失去生命透硝,那將更具挑戰(zhàn)性(也更現(xiàn)實)狰闪。 這是你碰撞檢測領(lǐng)域要做的地方。

你可以使用SpriteKit的物理引擎濒生,但自己進行碰撞檢測并不困難埋泵,特別是如果你用簡單的圓圈建模精靈。 檢測兩個圓是否相交就需要多思考了罪治。 你所要做的就是計算它們之間的距離(畢達(dá)哥拉斯)丽声,看看它是否小于兩個圓的半徑之和。

GameScene上方添加兩個新常量:

let cannonCollisionRadius: CGFloat = 20
let playerCollisionRadius: CGFloat = 10

這些是大炮和玩家周圍的碰撞圈的大小觉义。 看一下精靈雁社,你會看到加農(nóng)炮圖像的實際半徑(以像素為單位)略大于你指定的常數(shù)(大約25點),但是有一點擺動空間是很好的谁撼。 你不希望你的游戲過于無情歧胁,或者玩家可能覺得它不是很有趣。

宇宙飛船不是圓形的事實不應(yīng)該阻止你的實現(xiàn)厉碟。 對于任意形狀的精靈喊巍,圓圈通常是足夠近似的。 由于其形狀箍鼓,它具有更簡單的三角計算的巨大優(yōu)勢崭参。 在這種情況下,船體的直徑大約為20個點(請記住款咖,直徑是半徑的兩倍)何暮。

首先,將此屬性添加到GameScene以獲取碰撞聲效果:

let collisionSound = SKAction.playSoundFileNamed("Collision.wav", waitForCompletion: false)

將以下方法添加到GameScene以檢測碰撞:

func checkShipCannonCollision() {
  let deltaX = playerSprite.position.x - turretSprite.position.x
  let deltaY = playerSprite.position.y - turretSprite.position.y
  
  let distance = sqrt(deltaX * deltaX + deltaY * deltaY)
  guard distance <= cannonCollisionRadius + playerCollisionRadius  else { return }
  run(collisionSound)
}

你已經(jīng)看過它以前是如何實現(xiàn)的铐殃。 首先海洼,計算兩個精靈的X位置之間的距離。 其次富腊,計算兩個精靈的Y位置之間的距離坏逢。 將這兩個值視為直角三角形的邊,然后可以計算斜邊。 斜邊是兩個精靈之間的距離是整。 如果該距離小于碰撞半徑的總和肖揣,則播放聲音效果。

update(_:)結(jié)束時添加對此新方法的調(diào)用:

checkShipCannonCollision()

是時候構(gòu)建并再次運行浮入。 通過將宇宙飛船飛入大炮龙优,使碰撞邏輯成為一種旋轉(zhuǎn)。

請注意事秀,一旦碰撞開始彤断,聲音效果就會無休止地發(fā)揮作用。 那是因為易迹,當(dāng)宇宙飛船飛過大炮時瓦糟,游戲會一個接一個地記錄重復(fù)的碰撞。 不只有一次碰撞赴蝇,每秒有60次,它會為每一次發(fā)出聲音效果巢掺!

碰撞檢測只是問題的前半部分句伶。 下半部分是碰撞反應(yīng)response。 您不僅需要來自碰撞的音頻反饋陆淀,而且還需要物理響應(yīng)(physical response) - 宇宙飛船應(yīng)該從大炮反彈考余。

將此常量添加到GameScene.swift的頂部:

let collisionDamping: CGFloat = 0.8

然后在checkShipCannonCollision()中的guard語句正下方添加這些代碼行:

playerAcceleration.dx = -playerAcceleration.dx * collisionDamping
playerAcceleration.dy = -playerAcceleration.dy * collisionDamping
playerVelocity.dx = -playerVelocity.dx * collisionDamping
playerVelocity.dy = -playerVelocity.dy * collisionDamping

這與您使太空飛船從屏幕邊框反彈所做的非常相似。構(gòu)建并運行以查看其工作原理轧苫。

如果太空船在擊中大炮時速度很快楚堤,那看起來相當(dāng)不錯。但是如果它移動得太慢含懊,那么即使在反轉(zhuǎn)速度之后身冬,船也有時會停留在碰撞半徑內(nèi)并且永遠(yuǎn)不會離開它。顯然岔乔,這種解決方案存在一些問題酥筝。

不是通過反轉(zhuǎn)其速度將船從大炮上彈開,而是需要通過調(diào)整其位置以使半徑不再重疊來物理地將船推離大炮雏门。

要做到這一點嘿歌,你需要計算大炮和宇宙飛船之間的向量。幸運的是茁影,您之前已經(jīng)計算過它來測量它們之間的距離宙帝。那你如何使用那個距離向量來移動船?

deltaXdeltaY形成的向量已經(jīng)指向正確的方向募闲,但它的長度是錯誤的步脓。您需要的長度是船舶半徑與其當(dāng)前長度之間的差異。這樣,當(dāng)你將它添加到船的當(dāng)前位置時沪编,船將不再與大炮重疊呼盆。

向量的當(dāng)前長度是distance,但您需要的長度是:

cannonCollisionRadius + playerCollisionRadius - distance

那么如何改變矢量的長度呢蚁廓?

解決方案是使用稱為normalization的技術(shù)访圃。 您可以通過將X和Y分量除以當(dāng)前標(biāo)量長度(使用畢達(dá)哥拉斯計算)來標(biāo)準(zhǔn)化矢量。 得到的“標(biāo)準(zhǔn)化”向量的總長度為1相嵌。

然后腿时,您只需將X和Y乘以所需的長度即可得到宇宙飛船的偏移量。 在您添加到checkShipCannonCollision()的前面代碼行下面添加以下代碼:

let offsetDistance = cannonCollisionRadius + playerCollisionRadius - distance
let offsetX = deltaX / distance * offsetDistance
let offsetY = deltaY / distance * offsetDistance
playerSprite.position = CGPoint(
  x: playerSprite.position.x + offsetX,
  y: playerSprite.position.y + offsetY
)

Build并運行饭宾。 你會看到宇宙飛船現(xiàn)在可以從大炮上正常彈跳批糟。

為了完善碰撞邏輯,你將從宇宙飛船和大炮中減去一些生命值看铆。 然后徽鼎,更新生命欄。 在run(collisionSound)之前添加以下代碼:

playerHP = max(0, playerHP - 20)
cannonHP = max(0, cannonHP - 5)

updateHealthBar(playerHealthBar, withHealthPoints: playerHP)
updateHealthBar(cannonHealthBar, withHealthPoints: cannonHP)

Build并再次運行弹惦。 這艘船和大炮每次碰撞時都會失去一些生命值否淤。


Adding Some Spin - 添加一些旋轉(zhuǎn)

為了獲得良好的效果,您可以在碰撞后為太空船添加一些旋轉(zhuǎn)棠隐。 這種額外的旋轉(zhuǎn)不會影響飛行方向石抡;它只會使碰撞的效果更加深刻(飛行員更加頭暈)。 在GameScene.swift的頂部添加一個新常量:

let playerCollisionSpin: CGFloat = 180

這將旋轉(zhuǎn)量設(shè)置為每秒半圈助泽,我認(rèn)為看起來非常好啰扛。 現(xiàn)在向GameScene類添加一個新屬性:

var playerSpin: CGFloat = 0

checkShipCannonCollision()中,在更新生命欄方法之前添加以下代碼:

playerSpin = playerCollisionSpin

最后嗡贺,在playerSprite.zRotation = playerAngle - 90 * degreesToRadians之前隐解,將以下代碼添加到updatePlayer(_ :)

if playerSpin > 0 {
  playerAngle += playerSpin * degreesToRadians
  previousAngle = playerAngle
  playerSpin -= playerCollisionSpin * CGFloat(dt)
  if playerSpin < 0 {
    playerSpin = 0
  }
}

playerSpin在不影響速度的情況下有效地覆蓋了旋轉(zhuǎn)持續(xù)時間的船舶顯示角度。 旋轉(zhuǎn)量隨著時間的推移迅速減少暑刃,因此船在一秒鐘之后就會旋出厢漩。 旋轉(zhuǎn)時,更新previousAngle以匹配旋轉(zhuǎn)角度岩臣,以便在旋出后船不會突然捕捉到新的角度溜嗜。

Build并運行并設(shè)置該船旋轉(zhuǎn)!

后記

本篇主要講述了基于SpriteKit的游戲編程的三角函數(shù)架谎,感興趣的給個贊或者關(guān)注~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末炸宵,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子谷扣,更是在濱河造成了極大的恐慌土全,老刑警劉巖捎琐,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異裹匙,居然都是意外死亡瑞凑,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門概页,熙熙樓的掌柜王于貴愁眉苦臉地迎上來籽御,“玉大人,你說我怎么就攤上這事惰匙〖继停” “怎么了?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵项鬼,是天一觀的道長哑梳。 經(jīng)常有香客問我,道長绘盟,這世上最難降的妖魔是什么鸠真? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮龄毡,結(jié)果婚禮上弧哎,老公的妹妹穿的比我還像新娘。我一直安慰自己稚虎,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布偎捎。 她就那樣靜靜地躺著蠢终,像睡著了一般。 火紅的嫁衣襯著肌膚如雪茴她。 梳的紋絲不亂的頭發(fā)上寻拂,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天,我揣著相機與錄音丈牢,去河邊找鬼祭钉。 笑死,一個胖子當(dāng)著我的面吹牛己沛,可吹牛的內(nèi)容都是我干的慌核。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼申尼,長吁一口氣:“原來是場噩夢啊……” “哼垮卓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起师幕,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤粟按,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灭将,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡疼鸟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了庙曙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片空镜。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖矾利,靈堂內(nèi)的尸體忽然破棺而出姑裂,到底是詐尸還是另有隱情,我是刑警寧澤男旗,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布舶斧,位于F島的核電站,受9級特大地震影響察皇,放射性物質(zhì)發(fā)生泄漏茴厉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一什荣、第九天 我趴在偏房一處隱蔽的房頂上張望矾缓。 院中可真熱鬧,春花似錦稻爬、人聲如沸嗜闻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽琉雳。三九已至,卻和暖如春友瘤,著一層夾襖步出監(jiān)牢的瞬間翠肘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工辫秧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留束倍,地道東北人。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓盟戏,卻偏偏與公主長得像绪妹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子柿究,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容