#70 走進物理的世界!iOS SpriteKit練習-打磚塊遊戲(Breakout Game)-1-座標 、阻力、彈性、向量、推力、碰撞。

練習 SpriteKit 是一個極具挑戰性又充滿樂趣的過程。實作打磚塊遊戲來學習如何運用 SpriteKit 框架來創建具有物理特性的遊戲場景,包括球的彈跳、磚塊的碎裂等。同時也摸索物理引擎的運作原理,並學習如何處理碰撞、接觸和反彈等物理效應。

目前進度GIF

. 設定磚塊,球,球拍的類別。
. 設定手勢左右移動球拍。
. 給予球推力向量讓球移動。
. 設置阻力、彈性讓球碰撞後速度不變。
. 磚塊被碰撞後消失。

螢幕截圖

建立 SpriteKit 專案

1. 打開Xcode,選擇「建立新的Xcode專案」。

2. 選擇遊戲「Game」 。

3. 命名專案,預設語言Swift。遊戲技術是 SpriteKit

4. 點擊「建立」,查看預設的檔案列表。

建好後,它預設有一個簡單的sample code,執行起來的效果。

我們先刪除預設的程式,找到「GameScene.swift」檔案。在didMove(to:)方法中,刪除原本的程式碼。

建立 SKSpriteNode

創建一個名為 PhysicsCategory 的Swift檔案,我們的物體會有三個,球,磚塊,球拍,用一個 strcut 來判別

import Foundation

struct PhysicsCategory {
static let None: UInt32 = 0
static let Ball: UInt32 = 0b1
static let Brick: UInt32 = 0b10 //磚塊
static let Paddle: UInt32 = 0b100
}

定義 SKSpriteNode 類別,代表磚塊:

import SpriteKit

class Brick: SKSpriteNode {
init(color: UIColor, size: CGSize) {
super.init(texture: nil, color: color, size: size)
self.physicsBody = SKPhysicsBody(rectangleOf: size)
self.physicsBody?.isDynamic = false
self.physicsBody?.categoryBitMask = PhysicsCategory.Brick
self.physicsBody?.contactTestBitMask = PhysicsCategory.Ball
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

定義 SKSpriteNode 類別,代表球拍:

import SpriteKit

class Paddle: SKSpriteNode {
init(color: UIColor, size: CGSize) {
super.init(texture: nil, color: color, size: size)
self.physicsBody = SKPhysicsBody(rectangleOf: size)
self.physicsBody?.isDynamic = false
self.physicsBody?.categoryBitMask = PhysicsCategory.Paddle
self.physicsBody?.contactTestBitMask = PhysicsCategory.Ball
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

設置變數

我們會有一個球拍,一個球,還有很多個磚塊

class GameScene: SKScene {
var paddle: Paddle!
var ball: SKShapeNode!
var bricks = [[Brick]]()

座標系

了解一下座標系,先印出 self.anchorPoint 來看看,

        print("Scene anchorPoint: \(self.anchorPoint)")
//Scene anchorPoint: (0.5, 0.5)

根據輸出,這表示場景的 anchorPoint 被設置為 (0.5, 0.5),這意味著原點位於場景的中心。這樣設置的場景將以其中心點為中心,而不是左下角。在 GameScene.sks 檔也看的出來目前螢幕的中心點是 (0, 0) 的位置

    override func didMove(to view: SKView) {
print(frame)
print("frame.minX= \(frame.minX), maxX= \(frame.maxX)")
print("frame.minY= \(frame.minY), maxY= \(frame.maxY)")
print("frame.midX= \(frame.midX), midY= \(frame.midY)")
/*
(-375.0, -667.0, 750.0, 1334.0)
frame.minX= -375.0, maxX= 375.0
frame.minY= -667.0, maxY= 667.0
frame.midX= 0.0, midY= 0.0
*/
}

把球拍加到 GameScene 去,在 didMove 裡建立一個 paddle, 用 addChild 加入,處理 touchesMoved 事件,當我們手指左右移動時,球拍跟著移動

    override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self

// Create paddle
paddle = Paddle(color: .white, size: CGSize(width: 100, height: 20))
paddle.position = CGPoint(x: frame.midX, y: frame.minY + 100)
addChild(paddle)
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let touchLocation = touch.location(in: self)
paddle.position.x = touchLocation.x
}

再來我們建立球, 位置放先放在球拍上方處,然後設定一些物理屬性。

阻尼效應 linearDamping

當一個物體在運動時,可能會受到外部阻力的影響,使得它的運動速度逐漸減慢。 linearDamping 屬性就是用來模擬這種運動中的阻尼效應的,它會使物體的線性速度每秒減少一定的百分比。如果將 linearDamping 屬性設置為非零值,表示物體在運動過程中受到了阻尼效應,其運動速度會逐漸減慢。linearDamping 的值越大,阻尼效應就越明顯,物體的速度下降得越快;相反,如果 linearDamping 的值為0,則表示物體不受到任何阻尼效應,其速度會保持不變。

彈性 restitution

當球與磚塊碰撞時,可能會受到彈性(restitution)或其他物理屬性的影響,導致速度變化。為了確保球與磚塊碰撞後速度保持不變,我們在創建球的物理體時設置 restitution 屬性為1,這表示球與其他物體碰撞後速度不會減慢。這樣球就會保持相同的速度彈回,而不會減慢。

  override func didMove(to view: SKView) {
...
// Create ball
ball = SKShapeNode(circleOfRadius: 10)
ball.fillColor = .white
ball.position = CGPoint(x: frame.midX, y: paddle.frame.minY + 50)
ball.physicsBody = SKPhysicsBody(circleOfRadius: 10)
ball.physicsBody?.isDynamic = true
ball.physicsBody?.affectedByGravity = false
ball.physicsBody?.categoryBitMask = PhysicsCategory.Ball
ball.physicsBody?.contactTestBitMask = PhysicsCategory.Brick | PhysicsCategory.Paddle
ball.physicsBody?.linearDamping = 0 // 線性阻尼設置為0,球就不會受到外部阻力的影響,速度將保持不變
ball.physicsBody?.restitution = 1.0 // 彈性設置為1,碰撞後速度不減慢
addChild(ball)
}

球一開始還沒彈出去時,讓他跟球拍一樣,待在球拍上方,跟著手勢可以左右移動

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
...
ball.position.x = touchLocation.x
}

在上方加一些磚塊,先加個 5 列磚頭吧。

override func didMove(to view: SKView) {
...
// Create bricks
let brickSize = CGSize(width: 100, height: 40)
let numRows = 5
let rowsSpace = 1.5
let numCols = Int(frame.width) / Int(brickSize.width)
let colsSpace = 1.1

for row in 0..<numRows {
var rowBricks = [Brick]()
for col in 0..<numCols {
let brick = Brick(color: UIColor.random(), size: brickSize)
brick.physicsBody?.categoryBitMask = PhysicsCategory.Brick
brick.position = CGPoint(x: (CGFloat(col) * brickSize.width * colsSpace) + brickSize.width / 2 - 300.0,
y: frame.maxY - (CGFloat(row) * brickSize.height*rowsSpace) - (brickSize.height / 2) - 150.0)
addChild(brick)
rowBricks.append(brick)
}
bricks.append(rowBricks)
}
}

讓球彈出去

再來建立一個往上的向量來推球,CGVector(dx: 0, dy: 5.0),用 applyImpulse 方法可以將把這個推力加到球上,球會根據推力的大小和方向,改變物體的線速度和角速度。這個推力是瞬間性的,意味著它會立即改變物體的運動狀態,但不會持續影響物體的運動。

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if !gameStarted {
gameStarted = true
// 當遊戲還沒開始時,創造一個向量來推動球
let vector = CGVector(dx: 0, dy: 5.0)
ball.physicsBody?.applyImpulse(vector)
}
}

因為剛在建立球有指定
阻尼效應 linearDamping = 0
彈性 restitution = 1
所以球會在滾動及碰撞後的速度會維持不變。

再來要處理碰撞到磚頭的事件,先加入 SKPhysicsContactDelegate 並且把代理指向自己。

class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMove(to view: SKView) {
physicsWorld.contactDelegate = self // 將場景設置為碰撞代理
}

func didBegin(_ contact: SKPhysicsContact) {
// 當兩個物體開始碰撞時調用這個方法
// 在這裡處理碰撞事件的相應邏輯
}

func didEnd(_ contact: SKPhysicsContact) {
// 當兩個物體碰撞結束時調用這個方法
// 在這裡處理碰撞結束事件的相應邏輯
}
}

SKPhysicsContactDelegate 是 SpriteKit 框架提供的一個協議(protocol),用於處理物理碰撞事件的代理(delegate)。當兩個物體在 SpriteKit 中發生碰撞時,系統會通過這個協議來通知。通常遊戲場景(SKScene)會採用 SKPhysicsContactDelegate 協議,並實現協議中的方法來處理碰撞事件。

SKPhysicsContactDelegate 協議中最常見的方法是 didBegin(_:),這個方法在兩個物體開始碰撞時被調用,還有 didEnd(_:) 方法,當兩個物體碰撞結束時會被調用。

後續研究

  • 處理碰到球拍左右邊改變角度
  • 要設置邊界,處理球碰到上,左右邊界時要反彈,不然就飛出去了。

參考:

參考: SpriteKit

參考: SKPhysicsContactDelegate

參考: anchorpoint

參考: SKPhysicsBody

SKPhysicsBody 是 SpriteKit 框架中的一個類,用於定義 2D 物理特性。它代表著 SpriteKit 中的一個物理主體,可以應用於 Sprite 或 Node 上,以模擬物體之間的物理交互行為。以下是 SKPhysicsBody 的一些重要特性和功能:

形狀設置:您可以使用不同的方法來設置物理主體的形狀,包括矩形、圓形、多邊形等。

動態性:您可以設置物理主體是否動態,即是否受到力的影響,以及是否可以移動。

重力影響:您可以指定物理主體是否受到重力的影響,從而模擬物體下落的行為。

碰撞檢測:您可以設置物理主體的碰撞檢測位元遮罩,以指定與哪些物理類別的物體發生碰撞時,要觸發碰撞事件。

碰撞反應:您可以設置物理主體的碰撞位元遮罩和反彈系數,從而控制物體之間碰撞時的行為,如彈跳、吸附等。

摩擦力和密度:您可以設置物理主體的摩擦力和密度,從而控制物體之間的摩擦力和碰撞的影響程度。

旋轉設置:您可以指定物理主體是否允許旋轉,以及設置物理主體的重心位置。

身體連結:您可以創建連接多個物理主體的物理聯接點,以模擬複雜的物體結構。

--

--