2018-02-12 Beginning Tile Maps

因為工作上的事情,將SK學習擱置了一周左右,心頭實在過意不去,所以晚上繼續(xù)抽空開始學習新的篇章:Tile Maps


image.png

地圖編輯

地圖編輯大概原理就是將背景拆分成一個個的單元格,首先預置一些單元格的填充元素,比如花花草草啊、河流啊,植物啊之類的,然后通過地圖編輯器將之前預置的填充元素編輯到bg中即可,效果如下:


image.png

單元格的形狀也可以是多樣,除了標準的正方形以外,還可以是以下形狀:


image.png

本教程運用的是標準正方形的單元格,步驟如下:

1. 創(chuàng)建預置單元格模板TileSet

快捷鍵 cmd+N 選擇 SpriteKit Tile Set,自己命名,一路enter到底就完成了TileSet的創(chuàng)建。


image.png

2. 創(chuàng)建grass tile模板

方法很簡單,首先將父節(jié)點命名為Background,然后將默認的子節(jié)點命名為grass tile(一開始就種點花花草草嘛),在media library中選擇花花草草的素材并拖動到屏幕中間就完成了grass tile模板的創(chuàng)建。


image.png

有時候我們可能會在地圖中隨機生成不同樣式的花花草草,實現(xiàn)也非常簡單,我們只需要再拖點其他樣式的花花草草到中間的tile中,選擇create new variant即可


image.png

拖了以后該Tile下就會有多個素材了(因為我將grass2拖了兩遍,所以出現(xiàn)了兩個grass2)
image.png

3. 回到GameScene.sks,拖一個Tile Map Node到場景中,雙擊編輯地圖

image.png

雙擊地圖后如下圖所示,這樣就可以選擇不同的素材進行地圖編輯了。


image.png

編輯完成點擊“Done”退出編輯。
還有一種比較使用的tile,就是8-Way Adjacency Group,大概原理就是將素材拆分成九宮格進行處理,這樣我就可以編輯一條帶邊框、不規(guī)則的河了。


image.png

以上就是純界面操作的地圖基本編輯功能。接下來我們就要在這個地圖上來添加player和bugs了

添加Player

1. 創(chuàng)建Player類,初始化相關參數(shù)

//創(chuàng)建Player類
class Player: SKSpriteNode {
    
    //預置player的動畫數(shù)組
    var animations: [SKAction] = []
    
    
    //大概的意思就是要求初始化吧,否則報錯,不能理解就暫時當成模板記住吧?
    required init?(coder aDecoder: NSCoder) {
        fatalError("Please use init()")
    }
    
    /*初始化player的參數(shù)如下:
     1. 圖片紋理
     2. 名稱
     3. zPosition
     4. 物理碰撞相關參數(shù)
     5. 添加動畫效果
    */
    init() {
        
        let texture = SKTexture(imageNamed: "player_bk1")
        
        super.init(texture:texture,
                   color:.white,
                   size:texture.size())
        
        name = "Player"
        
        zPosition = 50
        
        physicsBody = SKPhysicsBody(circleOfRadius: size.width/2)
        
        physicsBody?.restitution = 1.0
        
        physicsBody?.linearDamping = 0.5
        
        physicsBody?.friction = 0
        
        physicsBody?.allowsRotation = false
        
        createAnimations(character: "player")
        
    }

2. 設置player運動速率

enum PlayersSetting {
    static let playerSpeed: CGFloat = 5.0
}

3. 創(chuàng)建player運動方法

教程中是如下寫的,直接讓兩個CGPoint值相減,我按照這個來寫,直接報錯。

func move(target: CGPoint) {
  guard let physicsBody = physicsBody else { return }
  let newVelocity = (target - position).normalized()
                             * PlayerSettings.playerSpeed
  physicsBody.velocity = CGVector(point: newVelocity)
}

所以我還是老老實實x-x y-y咯

func move(target: CGPoint) {
        
        guard let physicsBody = physicsBody else {
            return
        }
        
        let newVelocity = CGVector(dx: (target.x - position.x) * PlayersSetting.playerSpeed, dy: (target.y - position.y)  * PlayersSetting.playerSpeed)
        
        physicsBody.velocity = newVelocity
        
        
        print("* \(animationDirection(for: physicsBody.velocity))")
        
        checkDirection()
        
        
    }

然后在GameScene.swift中的touchesBegan中添加player的move事件即可

 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch  = touches.first else {
            return
        }
        
        player.move(target: touch.location(in: self))
    }

到現(xiàn)在player就指哪打哪了,但是player沒有動畫,也無法轉(zhuǎn)向

設置Camera

設置camera的需求是想讓相機(也就是視角)跟隨player。

我們先拖一個camera到場景中并命名為“camera”。然后點擊Scene,將Camera設置為camera如下圖所示。


image.png

在學習這一節(jié)時,又學到了一個新的東東SKConstraint。顧名思義就是跟約束相關的吧。這節(jié)用到這個類的作用就是要設置相機的constraint,使得相機跟隨player動,而且不能漏出邊框。

首先讓相機跟隨player動,實現(xiàn)方法如下(Player類中):

func setupCamera() {
  // 保證有camera的存在
  guard let camera = camera else { return }
// 保證與player的距離為0
  let zeroDistance = SKRange(constantValue: 0)
  let playerConstraint = SKConstraint.distance(zeroDistance,
// 設置該約束于camera
  camera.constraints = [playerConstraint]
}

然后在GameScene中didMove中添加setCamera()

現(xiàn)在player到哪,camera就到哪了(2D第一人稱視角,哈哈哈)

但是問題就來了,當player到場景邊框時,邊框以外的空白區(qū)域就顯現(xiàn)出來了,很難看,而且player會跑到場景邊框以外,如下所示


image.png

我們想要的是如果player接近場景邊框了,相機照到的區(qū)域就是對齊場景邊框的區(qū)域。而且player觸碰到場景邊框了就要被彈回來。
首先我們需要將場景也設置為物理體,那么我們就新建以下方法:

func setupWorldPhysics() {
  background.physicsBody =
      SKPhysicsBody(edgeLoopFrom: background.frame)
}

現(xiàn)在我們需要設置到邊框的constraint了,大概的原理就是長寬都取view和background的最小值,用這個范圍設置為相機到邊框的約束條件。最后將相機的constraints數(shù)組添加edgeConstraint。關于SKConstraint的用法,以后用到了再詳細參閱

func setupCamera() {
        
        guard let camera = camera else {
            return
        }
        
        let zeroDistance = SKRange(constantValue: 0)
        
        let playerConstraint = SKConstraint.distance(zeroDistance, to: player)
        
        let xInset = minValue(a: (view?.bounds.width)!/2 * camera.xScale, b: background.frame.width/2)
        
        let yInset = minValue(a: (view?.bounds.width)!/2 * camera.yScale, b: background.frame.height/2)
        
        let constraintRect = background.frame.insetBy(dx: xInset, dy: yInset)
        
        let xRange = SKRange(lowerLimit: constraintRect.minX, upperLimit: constraintRect.maxX)
        
        let yRange = SKRange(lowerLimit: constraintRect.minY, upperLimit: constraintRect.maxY)
        
        let edgeConstraint = SKConstraint.positionX(xRange, y: yRange)
        
        edgeConstraint.referenceNode = background
        
        camera.constraints = [playerConstraint,edgeConstraint]
        
        
    }
    

這樣相機就能隨著player移動,如果player接近邊框了,相機就會停留在依照view的尺寸對齊background邊框的中心區(qū)域(我擦,好拗口,還真不好描述,大概能意會到那個位置就行),直到player移除了那個區(qū)域再跟隨player一起動(感覺我的上述描述,我都想到了可以用另外一種實現(xiàn)方式來實現(xiàn)了~~~而不用SKConstraint)

讓player動起來

1. 設定player運動的四個方向及player的動畫素材

新建Type.swift文件,加入方向的枚舉,問了搞iOS的朋友,如果首個forward的值設為0,后面的backward,left,right在沒有特別指定值的情況下默認依次遞增(也就是1,2,3)


enum Direction: Int {
  case forward = 0, backward, left, right
}

新建Animatable.swift文件,設置Animatable協(xié)議

protocol Animatable  {
}

然后設置extension延展規(guī)則,讓遵從Animatable協(xié)議的對象都可使用以下方法(解釋請查看注釋):

extension Animatable {
    
    func animationDirection(for directionVector: CGVector) -> Direction {
        
        let direction: Direction
        
        if abs(directionVector.dy) > abs(directionVector.dx) {
            
            //當Y位移大于X位移時,如果Y位移為負,則player朝向為forward。否則player朝向為backward。
            
            direction = directionVector.dy < 0 ? .forward : .backward
            
        } else {
            
            //當Y位移小于X位移時,如果X位移為負,則player朝向為left。否則player朝向為right。

            direction = directionVector.dx < 0 ? .left : .right
            
        }
        
        return direction
        
    }
    
    //加載player行走動畫紋理并添加到animations動畫數(shù)組中。
    func createAnimations(character:String) {
        
        let actionForward: SKAction = SKAction.animate(with: [
            SKTexture(imageNamed: "\(character)_ft1"),
            SKTexture(imageNamed: "\(character)_ft2")
            ], timePerFrame: 0.2)
        
        animations.append(SKAction.repeatForever(actionForward))
        
        
        let actionBackward: SKAction = SKAction.animate(with: [
            SKTexture(imageNamed: "\(character)_bk1"),
            SKTexture(imageNamed: "\(character)_bk2")
            ], timePerFrame: 0.2)
        
        animations.append(SKAction.repeatForever(actionBackward))

        
        
        let actionLeft: SKAction = SKAction.animate(with: [
            SKTexture(imageNamed: "\(character)_lt1"),
            SKTexture(imageNamed: "\(character)_lt2")
            ], timePerFrame: 0.2)
        
        animations.append(SKAction.repeatForever(actionLeft))
        
        
        //這里加兩次的原因是left和right朝向的素材是一個,當朝向為right時,只需要將動畫素材的xScale值設為-1即可
        animations.append(SKAction.repeatForever(actionLeft))
        
        
    }
    
    
}

然后在Player.swift中添加以下代碼,讓Player類的對象遵從Animatable協(xié)議

extension Player : Animatable {}

然后在Animatable.swift的協(xié)議中添加以下成員變量,使得每一個遵從該協(xié)議的實例化對象都必須要定義承載動作的animations數(shù)組(必須實例化Player類的對象就必須要定義該數(shù)組)

var animations: [SKAction] {get set}

因為player對象需遵從Animatable協(xié)議,所以我們需要在Player類中定義該動畫數(shù)組

var animations: [SKAction] = []

這里有個細節(jié)需要說明或者說是Mark一下,之前我們定義Animatable的時候是如下定義的。

protocol Animatable  {
}

然后我們在寫Animatable的extension方法以后發(fā)現(xiàn)變異的時候會報錯,大概報錯的意思就是那兩個方法返回的數(shù)據(jù)類型是mutable的,但實際是不允許mutable的,查了一下教程,原版的解釋如下:

Unfortunately, the protocol no longer compiles. Protocols assume that conforming types may have value semantics (i.e. structures and enumerations) which would make animations immutable. In this situation, the Characters (Player and Bug) are the only classes which will conform to Animatable and, being classes, they have reference semantics. So you can safely inform Animatable that only class types will conform.

翻譯一下

不幸地是,有了這個協(xié)議就編譯不過了。協(xié)議通常會認為遵循協(xié)議的這些類型可能會包含值語義(比如結構體和枚舉),從而使得animations不可改變。在這種情況下只有 Characters 中的Player、 Bug類將遵循Animatable協(xié)議,正是因為他們屬于class所以有引用語義,所以你可以告訴Animatable只有class類的對象才會遵循該協(xié)議。(新手表示到這里的時候有點一臉懵逼,大概的意思就是因為player是class類的實例化對象,而Animatable又是為player量身定制的,所以讓Animatable只作用class類的對象,其extension的方法就是mutable的了?先暫時這樣理解,等日后再來笑話現(xiàn)在的自己,哈哈哈)。

 protocol Animatable: class {
}

2. 讓player動起來

回到Player.swift,在其初始化函數(shù)中添加以下代碼:

  createAnimations(character: "player")

返回createAnimations方法一看就知道,這樣就把相關的素材全部合成了動畫并添加到了animations[]數(shù)組中以備后用。

然后就是根據(jù)當前player的velocity判斷player的朝向并讓他動起來。

func checkDirection() {
        
        guard let physicsBody = physicsBody else { return }
        
        let direction = animationDirection(for: physicsBody.velocity)
        
        if direction == .left {
            
            xScale = abs(xScale)
            
        }
        if direction == .right {
            
            xScale = -abs(xScale)
            
        }
        
        run(animations[direction.rawValue], withKey: "animation")
        
        
        
    }

查了一下rawValue的意思是指返回枚舉對象的成員值,那就想通了這里的運行原理。
因為之前定義的時候就將forward、backward、left及right的值分別定義為0、1、2、3

enum Direction: Int {
    case forward = 0, backward, left, right
}

然后對應的動畫動作也是按照forward、backward、left及right的順序添加到animations中,所以數(shù)組animation[i]中的動畫對象就和枚舉中的對象一一對應,自然通過animations[direction.rawValue]就可以找到對應的動畫了。

因為方向校驗是通過move(target:)中去觸發(fā)的,所以需要將checkDirection()添加到move(target:)中。

現(xiàn)在player不僅僅可以指哪打哪了,而且可以根據(jù)對應的方向調(diào)整朝向,而且也動起來了


image.png

創(chuàng)建Bug

在challenge中讓添加bug類并添加到場景中,這個就沒什么說的了,代碼如下(留作下一章使用):

class Bug: SKSpriteNode {
    
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("Please use init()")
    }
    
    init() {
        
        let texture = SKTexture(imageNamed: "bug_bk1")
        
        super.init(texture:texture,
                   color:.white,
                   size:texture.size()
                   )
        
        name = "Bug"
        
        zPosition = 49
        
        physicsBody = SKPhysicsBody(circleOfRadius: size.width/2)
        
        physicsBody?.restitution = 0.3
        
        physicsBody?.linearDamping = 0.5
        
        physicsBody?.friction = 0
        
        physicsBody?.allowsRotation = false
        
        
        
    }
    
    
    
    
}

這一章大概就這樣了,話說實際操作一遍,然后邊梳理邏輯邊寫筆記好累啊,不過這樣確實也很有用,免得無腦跟著教程敲一遍代碼就over了,接下來的兩章就是中級地圖編輯及保存和加載數(shù)據(jù)。

image.png

年前就先這樣了,好好過年放松一下,年后再戰(zhàn)。大家新年快樂。在此我也給自己樹立個小目標吧。爭取今年能有兩個成熟的小游戲成功上架APP STORE吧~新年還要加油

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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

  • 發(fā)現(xiàn) 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,180評論 4 61
  • 用到的組件 1、通過CocoaPods安裝 2、第三方類庫安裝 3、第三方服務 友盟社會化分享組件 友盟用戶反饋 ...
    SunnyLeong閱讀 14,651評論 1 180
  • 全目錄|【愛在失憶的日子】 上一章|愛在失憶的日子(63) 三天過去了,婷婷那個二流子老公都沒有再來找過她。婷婷依...
    小豆利子閱讀 621評論 14 29
  • "這個地球離開了誰依舊照轉(zhuǎn)。" 大林走了,老何也不在了,如今,你仿佛也畫下了某一個句點。 我今天依然在好好生活,找...
    Enking閱讀 285評論 0 0
  • 獨自消磨病未除,功名淡遠更交疏。 斜陽有意催孤雁,落葉無心宿蔽廬。 偶得新知秋色晚,共敲殘句月明初。 停杯更向黔山...
    塵埃落定1閱讀 207評論 4 12