版本記錄
版本號 | 時間 |
---|---|
V1.0 | 2017.10.19 星期五 |
前言
SpriteKit框架使用優(yōu)化的動畫系統(tǒng)赢底,物理模擬和事件處理支持創(chuàng)建基于2D精靈的游戲。接下來這幾篇我們就詳細的解析一下這個框架。相關(guān)代碼已經(jīng)傳至GitHub - 刀客傳奇侣背,感興趣的可以閱讀另外幾篇文章。
1. SpriteKit框架詳細解析(一) —— 基本概覽(一)
2. SpriteKit框架詳細解析(二) —— 一個簡單的動畫實例(一)
3. SpriteKit框架詳細解析(三) —— 創(chuàng)建一個簡單的2D游戲(一)
4. SpriteKit框架詳細解析(四) —— 創(chuàng)建一個簡單的2D游戲(二)
5. SpriteKit框架詳細解析(五) —— 基于SpriteKit的游戲編程的三角函數(shù)(一)
開始
在本系列的第一部分 SpriteKit框架詳細解析(五) —— 基于SpriteKit的游戲編程的三角函數(shù)(一)中慨默,您學(xué)習(xí)了三角學(xué)的基礎(chǔ)知識贩耐,并親眼看到它對于制作游戲有多大用處。
在本系列的第二部分中厦取,您將通過添加導(dǎo)彈潮太,軌道小行星盾牌和動畫“game over”
屏幕來擴展您的簡單太空游戲。 在此過程中虾攻,您還將了解有關(guān)正弦和余弦函數(shù)的更多信息铡买,并了解一些其他有用的方法,以便在游戲中使用三角函數(shù)的強大功能台谢。
目前寻狂,你的游戲中有一個宇宙飛船和一個旋轉(zhuǎn)的大炮,每個都帶有生命條朋沮。 雖然它們可能是死敵蛇券,但是除非宇宙飛船直接飛向大炮(對于大炮來說效果更好),否則它們都無法破壞對方樊拓。
Firing a Missile by Swiping - 通過滑動射擊導(dǎo)彈
現(xiàn)在纠亚,您將通過滑動屏幕讓玩家能夠從太空船發(fā)射導(dǎo)彈。 宇宙飛船將向滑動方向發(fā)射導(dǎo)彈筋夏。
打開GameScene.swift
蒂胞。 將以下屬性添加到GameScene
:
let playerMissileSprite = SKSpriteNode(imageNamed:"PlayerMissile")
var touchLocation = CGPoint.zero
var touchTime: CFTimeInterval = 0
你將導(dǎo)彈精靈從玩家的船上向其朝向的方向移動。 您將使用觸摸位置和時間來跟蹤用戶在屏幕上點擊以觸發(fā)導(dǎo)彈的位置和時間条篷。
然后骗随,將以下代碼添加到didMove(to :)
的末尾:
playerMissileSprite.isHidden = true
addChild(playerMissileSprite)
請注意蛤织,導(dǎo)彈精靈最初是隱藏的;只有當(dāng)玩家開火時你才能看到它鸿染。 為了增加挑戰(zhàn)指蚜,玩家一次只能有一枚導(dǎo)彈在飛行中。
要檢測放置在觸摸屏上的第一根手指涨椒,請將以下方法添加到GameScene
:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
touchLocation = location
touchTime = CACurrentMediaTime()
}
這非常簡單 - 只要檢測到觸摸摊鸡,您就可以存儲觸摸位置和時間。 實際工作發(fā)生在touchesEnded(_:with :)
中蚕冬,您將在下一步添加:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let touchTimeThreshold: CFTimeInterval = 0.3
let touchDistanceThreshold: CGFloat = 4
guard CACurrentMediaTime() - touchTime < touchTimeThreshold,
playerMissileSprite.isHidden,
let touch = touches.first else { return }
let location = touch.location(in: self)
let swipe = CGVector(dx: location.x - touchLocation.x, dy: location.y - touchLocation.y)
let swipeLength = sqrt(swipe.dx * swipe.dx + swipe.dy * swipe.dy)
guard swipeLength > touchDistanceThreshold else { return }
// TODO
}
guard
語句檢查開始和結(jié)束滑動之間經(jīng)過的時間是否小于touchTimeThreshold
值0.3秒免猾。然后,檢查導(dǎo)彈是否隱藏囤热。如果沒有猎提,則玩家的一個允許導(dǎo)彈飛行并且忽略觸摸。
下一部分將確定用戶所做的那種姿勢赢乓;是真的輕掃忧侧,還是只是點擊?你應(yīng)該只在滑動而不是點擊上發(fā)射導(dǎo)彈牌芋。你已經(jīng)做過幾次這樣的計算 - 減去兩個坐標(biāo),然后用畢達哥拉斯定理找到它們之間的距離松逊。如果距離大于4
點的touchDistanceThreshold
值躺屁,則將其視為有意滑動。
注意:您可以使用UIKit內(nèi)置的手勢識別器经宏,但這里的目的是了解如何使用三角法來實現(xiàn)這種邏輯犀暑。
有兩種方法可以使導(dǎo)彈飛行。第一個選項是根據(jù)您瞄準(zhǔn)導(dǎo)彈的角度創(chuàng)建一個playerMissileVelocity
矢量耐亏。在update(_:)
內(nèi)部,然后你將這個速度乘以增量時間到導(dǎo)彈精靈每幀的位置沪斟,并檢查導(dǎo)彈是否已經(jīng)飛出可見屏幕區(qū)域以便重置广辰。這類似于您在本系列教程的第1部分中制作飛船的方法。
與宇宙飛船不同主之,導(dǎo)彈永遠不會改變航向择吊;它總是直線飛行。因此槽奕,您可以采取更簡單的方法几睛,并在發(fā)射時提前計算導(dǎo)彈的最終目的地。掌握了這些信息后粤攒,您可以讓SpriteKit
為導(dǎo)彈精靈設(shè)置動畫到最終位置所森。
這使您無需檢查導(dǎo)彈是否已離開可見屏幕囱持。而且,這也是一個做更多有趣數(shù)學(xué)的機會焕济!
首先洪唐,使用以下代碼替換touchesEnded(_:with :)
中的TODO
注釋:
let angle = atan2(swipe.dy, swipe.dx)
playerMissileSprite.zRotation = angle - 90 * degreesToRadians
playerMissileSprite.position = playerSprite.position
playerMissileSprite.isHidden = false
在這里,您使用atan2(_:_ :)
將滑動矢量轉(zhuǎn)換為角度吼蚁,設(shè)置精靈的旋轉(zhuǎn)和位置凭需,并使導(dǎo)彈精靈可見。
現(xiàn)在是有趣的部分肝匆。 你知道導(dǎo)彈的起始位置(宇宙飛船的當(dāng)前位置)粒蜈,你知道角度(來自玩家的滑動動作)。 因此旗国,您可以根據(jù)這些事實計算導(dǎo)彈的目的地點枯怖。
Calculating Missile Destination - 計算導(dǎo)彈目的地
您已經(jīng)擁有了方向向量,并且您在第1部分中學(xué)習(xí)了如何使用規(guī)范化normalization
將向量的長度設(shè)置為您需要的任何值能曾。 但你想要多大的長度度硝? 嗯,這是具有挑戰(zhàn)性的一點寿冕。 因為你希望導(dǎo)彈在移出屏幕邊界時停止蕊程,所以它的行進長度取決于起始位置和方向。
目標(biāo)點始終位于屏幕邊框外驼唱,而不是屏幕邊框上藻茂。 因此,導(dǎo)彈在完全飛出視線時會消失玫恳。 這是為了讓游戲更具視覺吸引力辨赐。 要實現(xiàn)這一點,請在GameScene.swift
的頂部添加另一個常量:
let playerMissileRadius: CGFloat = 20
找到目標(biāo)點有點復(fù)雜京办。例如掀序,如果你知道玩家向下射擊,你可以計算出導(dǎo)彈需要飛行的垂直距離惭婿。首先不恭,通過簡單地找到導(dǎo)彈開始Y位置和playerMissileRadius
的總和來計算Y分量。其次审孽,通過確定導(dǎo)彈與該邊界線相交的位置來計算X分量县袱。
對于從屏幕的底部或頂部邊緣飛出的導(dǎo)彈,可以使用以下公式計算目的地的X分量:
destination.x = playerPosition.x +((destination.y - playerPosition.y)/ swipe.dy * swipe.dx)
這類似于第1部分中的歸一化技術(shù)佑力,其中您通過首先將x和y分量除以當(dāng)前長度然后乘以所需長度來放大矢量式散。在這里,您可以計算滑動矢量的Y分量與最終距離的比率打颤。然后將X分量乘以相同的值并將其添加到船舶的當(dāng)前X位置以獲得目標(biāo)X坐標(biāo)暴拄。
對于離開左邊或右邊的導(dǎo)彈漓滔,你基本上使用相同的功能,但交換所有的X和Y值乖篷。
這種將矢量延伸到邊緣的技術(shù)稱為投影projection
响驴,它對各種游戲應(yīng)用非常有用,例如檢測敵人是否可以通過沿著他們的視線投射矢量并看其是否能夠看到玩家墻壁或玩家撕蔼。
有一個障礙豁鲤。如果交叉點靠近一個角落,導(dǎo)彈首先與哪個邊緣交叉并不明顯鲸沮。
沒關(guān)系琳骡。你只需計算兩個交叉點,然后看看與玩家距離較短的距離讼溺!
在touchesEnded(_:with :)
的末尾添加以下代碼:
//calculate vertical intersection point
var destination1 = CGPoint.zero
if swipe.dy > 0 {
destination1.y = size.height + playerMissileRadius // top of screen
} else {
destination1.y = -playerMissileRadius // bottom of screen
}
destination1.x = playerSprite.position.x +
((destination1.y - playerSprite.position.y) / swipe.dy * swipe.dx)
//calculate horizontal intersection point
var destination2 = CGPoint.zero
if swipe.dx > 0 {
destination2.x = size.width + playerMissileRadius // right of screen
} else {
destination2.x = -playerMissileRadius // left of screen
}
destination2.y = playerSprite.position.y +
((destination2.x - playerSprite.position.x) / swipe.dx * swipe.dy)
在這里楣号,你要計算導(dǎo)彈的兩個候選目標(biāo)點;現(xiàn)在你需要找出哪個更接近玩家怒坯。 接下來炫狱,在上面的代碼正下方添加以下代碼:
// find out which is nearer
var destination = destination2
if abs(destination1.x) < abs(destination2.x) || abs(destination1.y) < abs(destination2.y) {
destination = destination1
}
你可以在這里使用畢達哥拉斯定理來計算從玩家到每個交叉點的對角線距離并選擇最短的距離,但是有一個更快的方法剔猿。 由于兩個可能的交叉點位于相同的矢量上视译,如果X或Y分量較短,則整個距離必須較短艳馒。 因此憎亚,無需計算對角線長度。
在剛剛添加的代碼下方弄慰,將最后一段代碼添加到touchesEnded(_:with :)
:
// run the sequence of actions for the firing
let missileMoveAction = SKAction.move(to: destination, duration: 2)
playerMissileSprite.run(missileMoveAction) {
self.playerMissileSprite.isHidden = true
}
構(gòu)建并運行應(yīng)用程序。 您現(xiàn)在可以滑動以在炮塔上射擊等離子體的螺栓蝶锋。 請注意陆爽,您一次只能發(fā)射一枚導(dǎo)彈。 你必須等到前一枚導(dǎo)彈從屏幕上消失后再次發(fā)射扳缕。
Making a Missile Travel at a Constant Speed - 使導(dǎo)彈以恒定速度飛行
還有一個問題慌闭。 根據(jù)飛行距離,導(dǎo)彈似乎行進得更快或更慢躯舔。
那是因為動畫的持續(xù)時間被硬編碼為持續(xù)2秒驴剔。 如果導(dǎo)彈需要進一步行進,它將以更快的速度行進粥庄,以便在相同的時間內(nèi)覆蓋更多的距離丧失。 如果導(dǎo)彈始終以一致的速度行進將更加現(xiàn)實。
你的好朋友艾薩克·牛頓爵士可以在這里幫忙惜互! 牛頓發(fā)現(xiàn)布讹,time = distance / speed
琳拭。 您可以使用畢達哥拉斯來計算距離,因此只需指定速度即可描验。
在GameScene.swift
的頂部添加另一個常量:
let playerMissileSpeed: CGFloat = 300
這是你希望導(dǎo)彈每秒傳播的距離白嘁。 現(xiàn)在,替換你在touchesEnded(_:with :)
中添加的最后一個代碼塊:
// calculate distance
let distance = sqrt(pow(destination.x - playerSprite.position.x, 2) +
pow(destination.y - playerSprite.position.y, 2))
// run the sequence of actions for the firing
let duration = TimeInterval(distance / playerMissileSpeed)
let missileMoveAction = SKAction.move(to: destination, duration: duration)
playerMissileSprite.run(missileMoveAction) {
self.playerMissileSprite.isHidden = true
}
您可以使用牛頓公式從距離和速度中得出它膘流,而不是對持續(xù)時間進行硬編碼絮缅。 再次運行應(yīng)用程序,您將看到導(dǎo)彈現(xiàn)在總是以相同的速度飛行呼股,無論目標(biāo)點有多遠或多近耕魄。
這就是你如何使用三角函數(shù)來發(fā)射移動的導(dǎo)彈。 這有點牽扯卖怜。 與此同時屎开,SpriteKit會讓所有的精靈運動動畫為你工作。
Detecting Collision Between Cannon and Missile - 探測炮與導(dǎo)彈之間的碰撞
現(xiàn)在马靠,導(dǎo)彈完全忽略了大炮奄抽。 那即將改變。
您將像以前一樣使用基于半徑的簡單方法進行碰撞檢測甩鳄。 你已經(jīng)添加了playerMissileRadius
逞度,所以你已經(jīng)準(zhǔn)備好使用你用于炮/船碰撞的相同技術(shù)來檢測大炮/導(dǎo)彈碰撞。
添加新方法:
func checkMissileCannonCollision() {
guard !playerMissileSprite.isHidden else { return }
let deltaX = playerMissileSprite.position.x - turretSprite.position.x
let deltaY = playerMissileSprite.position.y - turretSprite.position.y
let distance = sqrt(deltaX * deltaX + deltaY * deltaY)
if distance <= cannonCollisionRadius + playerMissileRadius {
playerMissileSprite.isHidden = true
playerMissileSprite.removeAllActions()
cannonHP = max(0, cannonHP - 10)
updateHealthBar(cannonHealthBar, withHealthPoints: cannonHP)
}
}
這與checkShipCannonCollision()
非常相似妙啃。 您可以計算精靈之間的距離档泽,如果該距離小于半徑之和,則將其視為碰撞揖赴。
如果檢測到碰撞馆匿,首先隱藏導(dǎo)彈精靈并取消其動畫。 然后降低大炮的生命值燥滑,并重新繪制它的生命條渐北。
在其他更新之后立即在update(_ :)
方法中添加對checkMissileCannonCollision()
的調(diào)用:
checkMissileCannonCollision()
構(gòu)建并運行,然后嘗試一下铭拧。 最后你可以對敵人造成一些傷害赃蛛!
在繼續(xù)之前,如果導(dǎo)彈有一些聲音效果會很好搀菩。 與以前的炮塔碰撞一樣呕臂,您可以使用SpriteKit
動作播放聲音。 將以下兩個屬性添加到GameScene
:
let missileShootSound = SKAction.playSoundFileNamed("Shoot.wav", waitForCompletion: false)
let missileHitSound = SKAction.playSoundFileNamed("Hit.wav", waitForCompletion: false)
現(xiàn)在肪跋,用touchesEnded(_:with :)
替換playerMissileSprite.run(missileMoveAction)
:
playerMissileSprite.run(SKAction.sequence([missileShootSound, missileMoveAction]))
你可以設(shè)置一個序列來播放聲音然后移動導(dǎo)彈歧蒋,而不是單個動作來移動導(dǎo)彈。
還要在checkMissileCannonCollision()
中的updateHealthBar(cannonHealthBar,withHealthPoints:cannonHP)
之后添加以下行:
run(missileHitSound)
導(dǎo)彈現(xiàn)在用ZZAPP
聲音射出疏尿,如果你的目標(biāo)是真的瘟芝,用一個令人滿意的BOINK
撞擊炮塔!
Adding an Orbiting Asteroid Shield for the Cannon - 為大炮添加軌道小行星盾
為了讓游戲更具挑戰(zhàn)性褥琐,你將給敵人一個盾牌锌俱。 盾牌將是一個神奇的小行星,它繞著大炮運行并摧毀靠近它的任何導(dǎo)彈敌呈。
在GameScene.swift
的頂部添加一些常量:
let orbiterSpeed: CGFloat = 120
let orbiterRadius: CGFloat = 60
let orbiterCollisionRadius: CGFloat = 20
初始化精靈節(jié)點常量并在GameScene
中添加一個新屬性:
let orbiterSprite = SKSpriteNode(imageNamed:"Asteroid")
var orbiterAngle: CGFloat = 0
將以下代碼添加到didMove(to :)
的末尾:
addChild(orbiterSprite)
這會將orbiterSprite
添加到GameScene
中贸宏。
現(xiàn)在,將以下方法添加到GameScene
:
func updateOrbiter(_ dt: CFTimeInterval) {
// 1
orbiterAngle = (orbiterAngle + orbiterSpeed * CGFloat(dt)).truncatingRemainder(dividingBy: 360)
// 2
let x = cos(orbiterAngle * degreesToRadians) * orbiterRadius
let y = sin(orbiterAngle * degreesToRadians) * orbiterRadius
// 3
orbiterSprite.position = CGPoint(x: cannonSprite.position.x + x, y: cannonSprite.position.y + y)
}
小行星將圍繞大炮在圓形路徑上運行磕洪。要做到這一點吭练,你需要兩個部分:確定小行星距離大炮中心的距離的半徑,以及描述它圍繞該中心點旋轉(zhuǎn)多遠的角度析显。
這就是updateOrbiter(_ :)
所做的:
1) 它通過
orbiterSpeed
增加角度鲫咽,并根據(jù)增量時間進行調(diào)整。然后使用truncatingRemainder(divideBy :)
將角度包裝到0-360范圍谷异。這并不是絕對必要的分尸,因為sin()
和cos()
在該范圍之外的角度下正確工作,但是如果角度變得太大歹嘹,則浮點精度可能成為問題箩绍。此外谍肤,如果角度在此范圍內(nèi)以進行調(diào)試你稚,則更容易將角度可視化。2) 它使用
sin()
和cos()
計算軌道器的新X和Y位置走哺。它們?nèi)“霃剑ㄐ纬扇切蔚男边叄┖彤?dāng)前角度怎抛,然后分別返回相鄰和相對的邊卑吭。3) 它通過將X和Y位置添加到大炮的中心位置來設(shè)置軌道器精靈的新位置。
你曾經(jīng)簡單地看過sin()
和cos()
马绝,但它們可能并不完全清楚它們是如何工作的陨簇。您知道,一旦您有角度和斜邊迹淌,這兩個函數(shù)都可用于計算直角三角形的其他邊長。
畫一個圓圈:
上面的插圖準(zhǔn)確地描繪了圍繞大炮旋轉(zhuǎn)的小行星的情況己单。圓圈描述了小行星的路徑唉窃,圓圈的原點是大炮的中心。
角度從零度開始纹笼,但始終一直增加纹份,直到它在開始時結(jié)束。如您所見,圓的半徑?jīng)Q定了小行星放置中心的距離蔓涧。
因此件已,給定角度和半徑,您可以分別使用余弦和正弦導(dǎo)出X和Y位置:
現(xiàn)在元暴,看一下正弦波和余弦波的圖:
水平軸包含圓的度數(shù)篷扩,從0到360或0到2π弧度。垂直軸通常從-1到+1茉盏。但是如果你的圓的半徑大于1并且它傾向于鉴未,那么垂直軸實際上從-radius
變?yōu)?code>+ radius。
當(dāng)角度從0度增加到360度時鸠姨,在余弦和正弦波的圖中找到水平軸上的角度铜秆。然后縱軸告訴你x和y值:
- 1) 如果角度為0度,則cos(0)為
1 * radius
讶迁,但sin(0)為0 * radius
连茧。這完全對應(yīng)于圓中的(x,y)坐標(biāo):x等于半徑巍糯,但y為0啸驯。 - 2) 如果角度是45度,則cos(45)是
0.707 * radius
鳞贷,sin(45)也是一樣的坯汤。這意味著x和y在圓上的這一點上都是相同的。注意:如果您在計算器上嘗試此操作搀愧,請先將其切換為DEG
模式惰聂。如果它處于RAD
模式,你會得到截然不同的答案咱筛。 - 3) 如果角度是90度搓幌,則cos(90)是
0 * radius
而sin(90)是1 * radius
。您現(xiàn)在位于(x迅箩,y)坐標(biāo)為(0, radius)
的圓的頂部溉愁。 - 4) 等等。為了更直觀地了解圓中的坐標(biāo)如何與正弦饲趋,余弦甚至正切函數(shù)的值相關(guān)拐揭,請嘗試這個很酷的 interactive circle。
您是否也注意到正弦和余弦的曲線非常相似奕塑?事實上堂污,余弦波只是正弦波移動了90度。
在update(_:)
結(jié)束時調(diào)用updateOrbiter(_ :)
:
updateOrbiter(deltaTime)
構(gòu)建并運行應(yīng)用程序龄砰。 你現(xiàn)在應(yīng)該擁有一顆永遠圍繞敵人大炮的小行星盟猖。
Spinning the Asteroid Around Its Axis - 圍繞其軸旋轉(zhuǎn)小行星
您還可以使小行星圍繞其軸旋轉(zhuǎn)讨衣。 將以下行添加到updateOrbiter(_ :)
的末尾:
orbiterSprite.zRotation = orbiterAngle * degreesToRadians
通過將旋轉(zhuǎn)設(shè)置為orbiterAngle
,小行星始終保持與相對于大炮相同的位置式镐,就像月亮總是顯示地球的同一側(cè)反镇。
Detecting Collision Between Missile and Orbiter - 探測導(dǎo)彈與軌道器的碰撞
讓我們給軌道器一個目的。 如果導(dǎo)彈太靠近娘汞,小行星會在它有機會對大炮造成任何傷害之前將其摧毀歹茶。 添加以下方法:
func checkMissileOrbiterCollision() {
guard !playerMissileSprite.isHidden else { return }
let deltaX = playerMissileSprite.position.x - orbiterSprite.position.x
let deltaY = playerMissileSprite.position.y - orbiterSprite.position.y
let distance = sqrt(deltaX * deltaX + deltaY * deltaY)
guard distance < orbiterCollisionRadius + playerMissileRadius else { return }
playerMissileSprite.isHidden = true
playerMissileSprite.removeAllActions()
orbiterSprite.setScale(2)
orbiterSprite.run(SKAction.scale(to: 1, duration: 0.5))
}
并且不要忘記在update(_ :)
結(jié)束時調(diào)用checkMissileOrbiterCollision()
:
checkMissileOrbiterCollision()
這看起來應(yīng)該很熟悉。 它與checkMissileCannonCollision()
基本相同价说。 當(dāng)檢測到碰撞時辆亏,導(dǎo)彈精靈被移除。 這次鳖目,你不播放聲音扮叨。 但是作為一種額外的視覺效果,你將小行星精靈的大小增加了兩倍领迈。 然后彻磁,您立即動畫小行星縮放再次縮小。 這使它看起來像軌道小行星“吃掉”導(dǎo)彈狸捅!
建立并運行以查看新的軌道護盾衷蜓。
Game Over, With Trig! - 游戲結(jié)束
還有更多可以用正弦和余弦做的事情。 它們也可以派上用場做動畫尘喝。
演示這樣的動畫的好地方是屏幕上的game over
磁浇。 將以下常量添加到GameScene.swift
的頂部:
let darkenOpacity: CGFloat = 0.8
并向GameScene
添加一些屬性:
lazy var darkenLayer: SKSpriteNode = {
let color = UIColor(red: 0, green: 0, blue: 0, alpha: 1)
let node = SKSpriteNode(color: color, size: size)
node.alpha = 0
node.position = CGPoint(x: size.width/2, y: size.height/2)
return node
}()
lazy var gameOverLabel: SKLabelNode = {
let node = SKLabelNode(fontNamed: "Helvetica")
node.fontSize = 24
node.position = CGPoint(x: size.width/2 + 0.5, y: size.height/2 + 50)
return node
}()
var gameOver = false
var gameOverElapsed: CFTimeInterval = 0
您將使用這些屬性來跟蹤游戲狀態(tài)和節(jié)點以顯示“Game Over”
信息。
接下來朽褪,將此方法添加到GameScene
:
func checkGameOver(_ dt: CFTimeInterval) {
// 1
guard playerHP <= 0 || cannonHP <= 0 else { return }
if !gameOver {
// 2
gameOver = true
gameOverElapsed = 0
stopMonitoringAcceleration()
// 3
addChild(darkenLayer)
// 4
let text = (playerHP == 0) ? "GAME OVER" : "Victory!"
gameOverLabel.text = text
addChild(gameOverLabel)
return
}
// 5
darkenLayer.alpha = min(darkenOpacity, darkenLayer.alpha + CGFloat(dt))
}
此方法檢查游戲是否完成置吓,如果是,則通過動畫處理游戲:
- 1) 游戲繼續(xù)進行缔赠,直到玩家或大炮用完生命值衍锚。
- 2) 當(dāng)游戲結(jié)束時,您將
gameOver
設(shè)置為true
嗤堰,并禁用加速度計戴质。 - 3) 在其他所有內(nèi)容上添加新的黑色圖層。 稍后在該方法中踢匣,您將為該圖層的alpha值設(shè)置動畫告匠,使其顯示為淡入。
- 4) 添加新文本標(biāo)簽并將其放在屏幕上离唬。 如果玩家獲勝凫海,則文本為“Victory!”或者如果玩家輸了,則為“Game Over”男娄,根據(jù)玩家的生命值確定。
- 5) 上述步驟只發(fā)生一次,以便在屏幕上設(shè)置游戲 - 每次在此之后模闲,您將
darkenOpacity
的alpha動畫從0設(shè)置為0.8 - 幾乎完全不透明建瘫,但不完全。
在update(_ :)
的底部添加對checkGameOver(_ :)
的調(diào)用:
checkGameOver(deltaTime)
并在touchesEnded(_:with :)
的頂部添加一小段邏輯:
guard !gameOver else {
let scene = GameScene(size: size)
let reveal = SKTransition.flipHorizontal(withDuration: 1)
view?.presentScene(scene, transition: reveal)
return
}
當(dāng)用戶在屏幕上點擊游戲時尸折,這將重新開始游戲啰脚。
構(gòu)建并運行以試用它。 在大炮上射擊或與你的船相撞实夹,直到你們中的一個人沒有生命橄浓。 屏幕將淡入黑色,將顯示game over
文本亮航。 游戲不再響應(yīng)加速度計荸实,但動畫仍在繼續(xù):
這一切都很好,花花公子缴淋,但正弦和余弦在哪里准给? 您可能已經(jīng)注意到,黑色圖層的淡入淡出非常線性重抖。 它以一致的速度從透明變?yōu)椴煌该鳌?/p>
你可以比這更好 - 你可以使用sin()
來改變淡入淡出的時間露氮。 這被稱為easing
,你在這里應(yīng)用的效果被稱為ease out
钟沛。
注意:您可以使用
run()
來執(zhí)行alpha淡入淡出畔规,因為它支持各種easing
模式。 同樣恨统,本教程的目的不是學(xué)習(xí)SpriteKit叁扫;它是學(xué)習(xí)它背后的數(shù)學(xué),包括easing
延欠!
在GameScene.swift
頂部添加一個新常量:
let darkenDuration: CFTimeInterval = 2
接下來陌兑,使用以下內(nèi)容替換checkGameOver(_ :)
中的最后一行代碼:
gameOverElapsed += dt
if gameOverElapsed < darkenDuration {
var multiplier = CGFloat(gameOverElapsed / darkenDuration)
multiplier = sin(multiplier * CGFloat.pi / 2) // ease out
darkenLayer.alpha = darkenOpacity * multiplier
}
gameOverElapsed
記錄了自游戲結(jié)束以來已經(jīng)過了多少時間。 淡入黑色層需要兩秒鐘(darkenDuration)
由捎。 multiplier
確定經(jīng)過的持續(xù)時間兔综。 無論darkenDuration
到底有多長,它的值始終介于0.0和1.0之間狞玛。
然后你執(zhí)行:
multiplier = sin(multiplier * CGFloat.pi / 2) // ease out
這將multiplier
從線性插值轉(zhuǎn)換為為生命注入更多生命的插值:
構(gòu)建并運行以查看新的“ease out”
效果软驰。 如果您發(fā)現(xiàn)很難看到差異,請在注釋掉的“ease out”
行中嘗試心肪,或更改動畫的持續(xù)時間锭亏。 效果很微妙,但它就在那里硬鞍。
注意:如果您想要使用值并快速測試效果慧瘤,請嘗試將
cannonHP
設(shè)置為10
戴已,這樣您就可以一次性結(jié)束游戲。
Easing
是一種微妙的效果锅减,所以讓我們用更明顯的彈跳效果結(jié)束 - 因為反彈的東西總是更有趣糖儡!
將以下代碼添加到checkGameOver(_ :)
的末尾:
// label position
let y = abs(cos(CGFloat(gameOverElapsed) * 3)) * 50
gameOverLabel.position = CGPoint(x: gameOverLabel.position.x, y: size.height/2 + y)
好的,這里發(fā)生了什么怔匣? 回想一下余弦的樣子:
如果你取cos()
的絕對值 - 使用abs()
- 那么先前低于零的部分將被翻轉(zhuǎn)握联。 曲線看起來像是反彈的東西,你不覺得嗎每瞒?
因為這些函數(shù)的輸出介于0.0和1.0之間金闽,所以將它乘以50以將其拉伸到0-50
。 cos()
的參數(shù)通常是一個角度剿骨,但是你給它gameOverElapsed
時間讓余弦向前移動它的曲線代芜。
因子3
只是為了讓它更快一點。 您可以修改這些值懦砂,直到您認為某些東西看起來很酷蜒犯。
構(gòu)建并運行以查看彈跳文本:
您已經(jīng)使用余弦的形狀來描述文本標(biāo)簽的彈簧運動。 這些余弦對各種事物都很有用荞膘!
你可以做的最后一件事是讓彈跳運動隨著時間的推移而失去振幅罚随。 您可以通過添加阻尼系數(shù)來完成此操作。 在GameScene
中創(chuàng)建一個新屬性:
var gameOverDampen: CGFloat = 0
這里的想法是當(dāng)游戲結(jié)束時羽资,您需要將此值重置為1.0淘菩,以便阻尼生效。 隨著文本反彈屠升,隨著時間的推移潮改,阻尼將再次慢慢淡化為0。
在checkGameOver(_ :)
中腹暖,將gameOver
設(shè)置為true后添加以下內(nèi)容:
gameOverDampen = 1
用以下內(nèi)容替換// label position
下面的代碼:
let y = abs(cos(CGFloat(gameOverElapsed) * 3)) * 50 * gameOverDampen
gameOverDampen = max(0, gameOverDampen - 0.3 * CGFloat(dt))
gameOverLabel.position = CGPoint(x: gameOverLabel.position.x, y: size.height/2 + y)
它與以前大致相同汇在。 您將y值乘以阻尼系數(shù)。 然后脏答,將阻尼系數(shù)從1.0緩慢降低到0.0糕殉,但不要小于0。這就是max()
所實現(xiàn)的殖告。 構(gòu)建并運行阿蝶,然后嘗試一下!
源碼
1. Swift
看一下代碼文檔結(jié)構(gòu)
看一下sb中的內(nèi)容
下面看一下源碼
1. GameScene.swift
import SpriteKit
import CoreMotion
let darkenOpacity: CGFloat = 0.8
let darkenDuration: CFTimeInterval = 2
let playerMissileSpeed: CGFloat = 300
let degreesToRadians = CGFloat.pi / 180
let radiansToDegrees = 180 / CGFloat.pi
let maxPlayerAcceleration: CGFloat = 400
let maxPlayerSpeed: CGFloat = 200
let borderCollisionDamping: CGFloat = 0.4
let maxHealth = 100
let healthBarWidth: CGFloat = 40
let healthBarHeight: CGFloat = 4
let cannonCollisionRadius: CGFloat = 20
let playerCollisionRadius: CGFloat = 10
let collisionDamping: CGFloat = 0.8
let playerCollisionSpin: CGFloat = 180
let playerMissileRadius: CGFloat = 20
let orbiterSpeed: CGFloat = 120
let orbiterRadius: CGFloat = 60
let orbiterCollisionRadius: CGFloat = 20
class GameScene: SKScene {
lazy var darkenLayer: SKSpriteNode = {
let color = UIColor(red: 0, green: 0, blue: 0, alpha: 1)
let node = SKSpriteNode(color: color, size: size)
node.alpha = 0
node.position = CGPoint(x: size.width/2, y: size.height/2)
return node
}()
lazy var gameOverLabel: SKLabelNode = {
let node = SKLabelNode(fontNamed: "Helvetica")
node.fontSize = 24
node.position = CGPoint(x: size.width/2 + 0.5, y: size.height/2 + 50)
return node
}()
var gameOver = false
var gameOverElapsed: CFTimeInterval = 0
var gameOverDampen: CGFloat = 0
var accelerometerX: UIAccelerationValue = 0
var accelerometerY: UIAccelerationValue = 0
var playerAcceleration = CGVector(dx: 0, dy: 0)
var playerVelocity = CGVector(dx: 0, dy: 0)
var lastUpdateTime: CFTimeInterval = 0
var playerAngle: CGFloat = 0
var previousAngle: CGFloat = 0
let playerHealthBar = SKSpriteNode()
let cannonHealthBar = SKSpriteNode()
var playerHP = maxHealth
var cannonHP = maxHealth
var playerSpin: CGFloat = 0
let playerSprite = SKSpriteNode(imageNamed: "Player")
let cannonSprite = SKSpriteNode(imageNamed: "Cannon")
let turretSprite = SKSpriteNode(imageNamed: "Turret")
let playerMissileSprite = SKSpriteNode(imageNamed:"PlayerMissile")
let orbiterSprite = SKSpriteNode(imageNamed:"Asteroid")
let collisionSound = SKAction.playSoundFileNamed("Collision.wav", waitForCompletion: false)
let missileShootSound = SKAction.playSoundFileNamed("Shoot.wav", waitForCompletion: false)
let missileHitSound = SKAction.playSoundFileNamed("Hit.wav", waitForCompletion: false)
let motionManager = CMMotionManager()
var orbiterAngle: CGFloat = 0
var touchLocation = CGPoint.zero
var touchTime: CFTimeInterval = 0
override func didMove(to view: SKView) {
// set scene size to match view
size = view.bounds.size
backgroundColor = SKColor(red: 94.0/255, green: 63.0/255, blue: 107.0/255, alpha: 1)
cannonSprite.position = CGPoint(x: size.width/2, y: size.height/2)
addChild(cannonSprite)
turretSprite.position = CGPoint(x: size.width/2, y: size.height/2)
addChild(turretSprite)
playerSprite.position = CGPoint(x: size.width - 50, y: 60)
addChild(playerSprite)
addChild(playerHealthBar)
addChild(cannonHealthBar)
cannonHealthBar.position = CGPoint(
x: cannonSprite.position.x,
y: cannonSprite.position.y - cannonSprite.size.height/2 - 10
)
updateHealthBar(playerHealthBar, withHealthPoints: playerHP)
updateHealthBar(cannonHealthBar, withHealthPoints: cannonHP)
addChild(orbiterSprite)
startMonitoringAcceleration()
playerMissileSprite.isHidden = true
addChild(playerMissileSprite)
}
override func update(_ currentTime: TimeInterval) {
// to compute velocities we need delta time to multiply by points per second
// SpriteKit returns the currentTime, delta is computed as last called time - currentTime
let deltaTime = max(1.0/30, currentTime - lastUpdateTime)
lastUpdateTime = currentTime
updatePlayerAccelerationFromMotionManager()
updatePlayer(deltaTime)
updateTurret(deltaTime)
checkShipCannonCollision()
checkMissileCannonCollision()
updateOrbiter(deltaTime)
checkMissileOrbiterCollision()
checkGameOver(deltaTime)
}
func startMonitoringAcceleration() {
guard motionManager.isAccelerometerAvailable else { return }
motionManager.startAccelerometerUpdates()
print("accelerometer updates on...")
}
func stopMonitoringAcceleration() {
guard motionManager.isAccelerometerAvailable else { return }
motionManager.stopAccelerometerUpdates()
print("accelerometer updates off...")
}
func updatePlayerAccelerationFromMotionManager() {
guard let acceleration = motionManager.accelerometerData?.acceleration else { return }
let filterFactor = 0.75
accelerometerX = acceleration.x * filterFactor + accelerometerX * (1 - filterFactor)
accelerometerY = acceleration.y * filterFactor + accelerometerY * (1 - filterFactor)
playerAcceleration.dx = CGFloat(accelerometerY) * -maxPlayerAcceleration
playerAcceleration.dy = CGFloat(accelerometerX) * maxPlayerAcceleration
}
func updatePlayer(_ dt: CFTimeInterval) {
playerVelocity.dx = playerVelocity.dx + playerAcceleration.dx * CGFloat(dt)
playerVelocity.dy = playerVelocity.dy + playerAcceleration.dy * CGFloat(dt)
playerVelocity.dx = max(-maxPlayerSpeed, min(maxPlayerSpeed, playerVelocity.dx))
playerVelocity.dy = max(-maxPlayerSpeed, min(maxPlayerSpeed, playerVelocity.dy))
var newX = playerSprite.position.x + playerVelocity.dx * CGFloat(dt)
var newY = playerSprite.position.y + playerVelocity.dy * CGFloat(dt)
var collidedWithVerticalBorder = false
var collidedWithHorizontalBorder = false
if newX < 0 {
newX = 0
collidedWithVerticalBorder = true
} else if newX > size.width {
newX = size.width
collidedWithVerticalBorder = true
}
if newY < 0 {
newY = 0
collidedWithHorizontalBorder = true
} else if newY > size.height {
newY = size.height
collidedWithHorizontalBorder = true
}
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
}
playerSprite.position = CGPoint(x: newX, y: newY)
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)
if playerSpin > 0 {
playerAngle += playerSpin * degreesToRadians
previousAngle = playerAngle
playerSpin -= playerCollisionSpin * CGFloat(dt)
if playerSpin < 0 {
playerSpin = 0
}
}
playerSprite.zRotation = playerAngle - 90 * degreesToRadians
}
playerHealthBar.position = CGPoint(
x: playerSprite.position.x,
y: playerSprite.position.y - playerSprite.size.height/2 - 15
)
}
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
}
func updateHealthBar(_ node: SKSpriteNode, withHealthPoints hp: Int) {
let barSize = CGSize(width: healthBarWidth, height: healthBarHeight);
let fillColor = UIColor(red: 113.0/255, green: 202.0/255, blue: 53.0/255, alpha:1)
let borderColor = UIColor(red: 35.0/255, green: 28.0/255, blue: 40.0/255, alpha:1)
// create drawing context
UIGraphicsBeginImageContextWithOptions(barSize, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return }
// draw the outline for the health bar
borderColor.setStroke()
let borderRect = CGRect(origin: CGPoint.zero, size: barSize)
context.stroke(borderRect, width: 1)
// draw the health bar with a colored rectangle
fillColor.setFill()
let barWidth = (barSize.width - 1) * CGFloat(hp) / CGFloat(maxHealth)
let barRect = CGRect(x: 0.5, y: 0.5, width: barWidth, height: barSize.height - 1)
context.fill(barRect)
// extract image
guard let spriteImage = UIGraphicsGetImageFromCurrentImageContext() else { return }
UIGraphicsEndImageContext()
// set sprite texture and size
node.texture = SKTexture(image: spriteImage)
node.size = barSize
}
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 }
playerAcceleration.dx = -playerAcceleration.dx * collisionDamping
playerAcceleration.dy = -playerAcceleration.dy * collisionDamping
playerVelocity.dx = -playerVelocity.dx * collisionDamping
playerVelocity.dy = -playerVelocity.dy * collisionDamping
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
)
playerSpin = playerCollisionSpin
playerHP = max(0, playerHP - 20)
cannonHP = max(0, cannonHP - 5)
updateHealthBar(playerHealthBar, withHealthPoints: playerHP)
updateHealthBar(cannonHealthBar, withHealthPoints: cannonHP)
run(collisionSound)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
touchLocation = location
touchTime = CACurrentMediaTime()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard !gameOver else {
let scene = GameScene(size: size)
let reveal = SKTransition.flipHorizontal(withDuration: 1)
view?.presentScene(scene, transition: reveal)
return
}
let touchTimeThreshold: CFTimeInterval = 0.3
let touchDistanceThreshold: CGFloat = 4
guard CACurrentMediaTime() - touchTime < touchTimeThreshold,
playerMissileSprite.isHidden,
let touch = touches.first else { return }
let location = touch.location(in: self)
let swipe = CGVector(dx: location.x - touchLocation.x, dy: location.y - touchLocation.y)
let swipeLength = sqrt(swipe.dx * swipe.dx + swipe.dy * swipe.dy)
guard swipeLength > touchDistanceThreshold else { return }
let angle = atan2(swipe.dy, swipe.dx)
playerMissileSprite.zRotation = angle - 90 * degreesToRadians
playerMissileSprite.position = playerSprite.position
playerMissileSprite.isHidden = false
//calculate vertical intersection point
var destination1 = CGPoint.zero
if swipe.dy > 0 {
destination1.y = size.height + playerMissileRadius // top of screen
} else {
destination1.y = -playerMissileRadius // bottom of screen
}
destination1.x = playerSprite.position.x +
((destination1.y - playerSprite.position.y) / swipe.dy * swipe.dx)
//calculate horizontal intersection point
var destination2 = CGPoint.zero
if swipe.dx > 0 {
destination2.x = size.width + playerMissileRadius // right of screen
} else {
destination2.x = -playerMissileRadius // left of screen
}
destination2.y = playerSprite.position.y +
((destination2.x - playerSprite.position.x) / swipe.dx * swipe.dy)
// find out which is nearer
var destination = destination2
if abs(destination1.x) < abs(destination2.x) || abs(destination1.y) < abs(destination2.y) {
destination = destination1
}
// calculate distance
let distance = sqrt(pow(destination.x - playerSprite.position.x, 2) +
pow(destination.y - playerSprite.position.y, 2))
// run the sequence of actions for the firing
let duration = TimeInterval(distance / playerMissileSpeed)
let missileMoveAction = SKAction.move(to: destination, duration: duration)
playerMissileSprite.run(SKAction.sequence([missileShootSound, missileMoveAction])) {
self.playerMissileSprite.isHidden = true
}
}
func checkMissileCannonCollision() {
guard !playerMissileSprite.isHidden else { return }
let deltaX = playerMissileSprite.position.x - turretSprite.position.x
let deltaY = playerMissileSprite.position.y - turretSprite.position.y
let distance = sqrt(deltaX * deltaX + deltaY * deltaY)
guard distance <= cannonCollisionRadius + playerMissileRadius else { return }
playerMissileSprite.isHidden = true
playerMissileSprite.removeAllActions()
cannonHP = max(0, cannonHP - 10)
updateHealthBar(cannonHealthBar, withHealthPoints: cannonHP)
run(missileHitSound)
}
func updateOrbiter(_ dt: CFTimeInterval) {
// 1
orbiterAngle = (orbiterAngle + orbiterSpeed * CGFloat(dt)).truncatingRemainder(dividingBy: 360)
// 2
let x = cos(orbiterAngle * degreesToRadians) * orbiterRadius
let y = sin(orbiterAngle * degreesToRadians) * orbiterRadius
// 3
orbiterSprite.position = CGPoint(x: cannonSprite.position.x + x, y: cannonSprite.position.y + y)
orbiterSprite.zRotation = orbiterAngle * degreesToRadians
}
func checkMissileOrbiterCollision() {
guard !playerMissileSprite.isHidden else { return }
let deltaX = playerMissileSprite.position.x - orbiterSprite.position.x
let deltaY = playerMissileSprite.position.y - orbiterSprite.position.y
let distance = sqrt(deltaX * deltaX + deltaY * deltaY)
guard distance < orbiterCollisionRadius + playerMissileRadius else { return }
playerMissileSprite.isHidden = true
playerMissileSprite.removeAllActions()
orbiterSprite.setScale(2)
orbiterSprite.run(SKAction.scale(to: 1, duration: 0.5))
}
func checkGameOver(_ dt: CFTimeInterval) {
// 1
guard playerHP <= 0 || cannonHP <= 0 else { return }
guard gameOver else {
gameOver = true
gameOverDampen = 1
gameOverElapsed = 0
stopMonitoringAcceleration()
// 3
addChild(darkenLayer)
// 4
let text = playerHP == 0 ? "GAME OVER" : "Victory!"
gameOverLabel.text = text
addChild(gameOverLabel)
return
}
// 5
gameOverElapsed += dt
if gameOverElapsed < darkenDuration {
var multiplier = CGFloat(gameOverElapsed / darkenDuration)
multiplier = sin(multiplier * CGFloat.pi / 2) // ease out
darkenLayer.alpha = darkenOpacity * multiplier
}
// label position
let y = abs(cos(CGFloat(gameOverElapsed) * 3)) * 50 * gameOverDampen
gameOverDampen = max(0, gameOverDampen - 0.3 * CGFloat(dt))
gameOverLabel.position = CGPoint(x: gameOverLabel.position.x, y: size.height/2 + y)
}
deinit {
stopMonitoringAcceleration()
}
}
2. GameViewController.swift
import UIKit
import SpriteKit
import GameplayKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = SKScene(fileNamed: "GameScene") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
// Present the scene
view.presentScene(scene)
}
view.ignoresSiblingOrder = false
view.showsFPS = true
view.showsNodeCount = true
}
}
override var shouldAutorotate: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override var prefersStatusBarHidden: Bool {
return true
}
}
下面看一下實現(xiàn)效果
后記
本篇主要講述了基于SpriteKit的游戲編程的三角函數(shù)黄绩,感興趣的給個贊或者關(guān)注~~~