因?yàn)楣ぷ魃系氖虑橐毓耄瑢K學(xué)習(xí)擱置了一周左右陡舅,心頭實(shí)在過(guò)意不去靶衍,所以晚上繼續(xù)抽空開(kāi)始學(xué)習(xí)新的篇章:Tile Maps
地圖編輯
地圖編輯大概原理就是將背景拆分成一個(gè)個(gè)的單元格颅眶,首先預(yù)置一些單元格的填充元素涛酗,比如花花草草啊商叹、河流啊,植物啊之類的卵洗,然后通過(guò)地圖編輯器將之前預(yù)置的填充元素編輯到bg中即可忌怎,效果如下:
單元格的形狀也可以是多樣榴啸,除了標(biāo)準(zhǔn)的正方形以外鸥印,還可以是以下形狀:
本教程運(yùn)用的是標(biāo)準(zhǔn)正方形的單元格库说,步驟如下:
1. 創(chuàng)建預(yù)置單元格模板TileSet
快捷鍵 cmd+N 選擇 SpriteKit Tile Set潜的,自己命名啰挪,一路enter到底就完成了TileSet的創(chuàng)建嘲叔。
2. 創(chuàng)建grass tile模板
方法很簡(jiǎn)單锰什,首先將父節(jié)點(diǎn)命名為Background丁逝,然后將默認(rèn)的子節(jié)點(diǎn)命名為grass tile(一開(kāi)始就種點(diǎn)花花草草嘛)霜幼,在media library中選擇花花草草的素材并拖動(dòng)到屏幕中間就完成了grass tile模板的創(chuàng)建辛掠。
有時(shí)候我們可能會(huì)在地圖中隨機(jī)生成不同樣式的花花草草回挽,實(shí)現(xiàn)也非常簡(jiǎn)單猩谊,我們只需要再拖點(diǎn)其他樣式的花花草草到中間的tile中牌捷,選擇create new variant即可
拖了以后該Tile下就會(huì)有多個(gè)素材了(因?yàn)槲覍rass2拖了兩遍喜滨,所以出現(xiàn)了兩個(gè)grass2)
3. 回到GameScene.sks虽风,拖一個(gè)Tile Map Node到場(chǎng)景中寄月,雙擊編輯地圖
雙擊地圖后如下圖所示漾肮,這樣就可以選擇不同的素材進(jìn)行地圖編輯了克懊。
編輯完成點(diǎn)擊“Done”退出編輯保檐。
還有一種比較使用的tile夜只,就是8-Way Adjacency Group,大概原理就是將素材拆分成九宮格進(jìn)行處理场躯,這樣我就可以編輯一條帶邊框踢关、不規(guī)則的河了粘茄。
以上就是純界面操作的地圖基本編輯功能。接下來(lái)我們就要在這個(gè)地圖上來(lái)添加player和bugs了
添加Player
1. 創(chuàng)建Player類吠架,初始化相關(guān)參數(shù)
//創(chuàng)建Player類
class Player: SKSpriteNode {
//預(yù)置player的動(dòng)畫(huà)數(shù)組
var animations: [SKAction] = []
//大概的意思就是要求初始化吧傍药,否則報(bào)錯(cuò)魂仍,不能理解就暫時(shí)當(dāng)成模板記住吧擦酌?
required init?(coder aDecoder: NSCoder) {
fatalError("Please use init()")
}
/*初始化player的參數(shù)如下:
1. 圖片紋理
2. 名稱
3. zPosition
4. 物理碰撞相關(guān)參數(shù)
5. 添加動(dòng)畫(huà)效果
*/
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. 設(shè)置player運(yùn)動(dòng)速率
enum PlayersSetting {
static let playerSpeed: CGFloat = 5.0
}
3. 創(chuàng)建player運(yùn)動(dòng)方法
教程中是如下寫(xiě)的仑氛,直接讓兩個(gè)CGPoint值相減锯岖,我按照這個(gè)來(lái)寫(xiě),直接報(bào)錯(cuò)遇伞。
func move(target: CGPoint) {
guard let physicsBody = physicsBody else { return }
let newVelocity = (target - position).normalized()
* PlayerSettings.playerSpeed
physicsBody.velocity = CGVector(point: newVelocity)
}
所以我還是老老實(shí)實(shí)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沒(méi)有動(dòng)畫(huà)渐排,也無(wú)法轉(zhuǎn)向
設(shè)置Camera
設(shè)置camera的需求是想讓相機(jī)(也就是視角)跟隨player驯耻。
我們先拖一個(gè)camera到場(chǎng)景中并命名為“camera”炒考。然后點(diǎn)擊Scene斋枢,將Camera設(shè)置為camera如下圖所示。
在學(xué)習(xí)這一節(jié)時(shí)描姚,又學(xué)到了一個(gè)新的東東SKConstraint轰胁。顧名思義就是跟約束相關(guān)的吧。這節(jié)用到這個(gè)類的作用就是要設(shè)置相機(jī)的constraint,使得相機(jī)跟隨player動(dòng)榛斯,而且不能漏出邊框搂捧。
首先讓相機(jī)跟隨player動(dòng)允跑,實(shí)現(xiàn)方法如下(Player類中):
func setupCamera() {
// 保證有camera的存在
guard let camera = camera else { return }
// 保證與player的距離為0
let zeroDistance = SKRange(constantValue: 0)
let playerConstraint = SKConstraint.distance(zeroDistance,
// 設(shè)置該約束于camera
camera.constraints = [playerConstraint]
}
然后在GameScene中didMove中添加setCamera()
現(xiàn)在player到哪聋丝,camera就到哪了(2D第一人稱視角,哈哈哈)
但是問(wèn)題就來(lái)了百姓,當(dāng)player到場(chǎng)景邊框時(shí)垒拢,邊框以外的空白區(qū)域就顯現(xiàn)出來(lái)了火惊,很難看屹耐,而且player會(huì)跑到場(chǎng)景邊框以外,如下所示
我們想要的是如果player接近場(chǎng)景邊框了仓技,相機(jī)照到的區(qū)域就是對(duì)齊場(chǎng)景邊框的區(qū)域脖捻。而且player觸碰到場(chǎng)景邊框了就要被彈回來(lái)地沮。
首先我們需要將場(chǎng)景也設(shè)置為物理體,那么我們就新建以下方法:
func setupWorldPhysics() {
background.physicsBody =
SKPhysicsBody(edgeLoopFrom: background.frame)
}
現(xiàn)在我們需要設(shè)置到邊框的constraint了危融,大概的原理就是長(zhǎng)寬都取view和background的最小值雷袋,用這個(gè)范圍設(shè)置為相機(jī)到邊框的約束條件楷怒。最后將相機(jī)的constraints數(shù)組添加edgeConstraint。關(guān)于SKConstraint的用法抱完,以后用到了再詳細(xì)參閱
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]
}
這樣相機(jī)就能隨著player移動(dòng)巧娱,如果player接近邊框了禁添,相機(jī)就會(huì)停留在依照view的尺寸對(duì)齊background邊框的中心區(qū)域(我擦上荡,好拗口馒闷,還真不好描述纳账,大概能意會(huì)到那個(gè)位置就行),直到player移除了那個(gè)區(qū)域再跟隨player一起動(dòng)(感覺(jué)我的上述描述永罚,我都想到了可以用另外一種實(shí)現(xiàn)方式來(lái)實(shí)現(xiàn)了~~~而不用SKConstraint)
讓player動(dòng)起來(lái)
1. 設(shè)定player運(yùn)動(dòng)的四個(gè)方向及player的動(dòng)畫(huà)素材
新建Type.swift文件呢袱,加入方向的枚舉翅敌,問(wèn)了搞iOS的朋友蚯涮,如果首個(gè)forward的值設(shè)為0卖陵,后面的backward泪蔫,left撩荣,right在沒(méi)有特別指定值的情況下默認(rèn)依次遞增(也就是1饶深,2粥喜,3)
enum Direction: Int {
case forward = 0, backward, left, right
}
新建Animatable.swift文件额湘,設(shè)置Animatable協(xié)議
protocol Animatable {
}
然后設(shè)置extension延展規(guī)則锋华,讓遵從Animatable協(xié)議的對(duì)象都可使用以下方法(解釋請(qǐng)查看注釋):
extension Animatable {
func animationDirection(for directionVector: CGVector) -> Direction {
let direction: Direction
if abs(directionVector.dy) > abs(directionVector.dx) {
//當(dāng)Y位移大于X位移時(shí)箭窜,如果Y位移為負(fù)磺樱,則player朝向?yàn)閒orward。否則player朝向?yàn)閎ackward芜辕。
direction = directionVector.dy < 0 ? .forward : .backward
} else {
//當(dāng)Y位移小于X位移時(shí)侵续,如果X位移為負(fù)憨闰,則player朝向?yàn)閘eft。否則player朝向?yàn)閞ight轧坎。
direction = directionVector.dx < 0 ? .left : .right
}
return direction
}
//加載player行走動(dòng)畫(huà)紋理并添加到animations動(dòng)畫(huà)數(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朝向的素材是一個(gè)属百,當(dāng)朝向?yàn)閞ight時(shí),只需要將動(dòng)畫(huà)素材的xScale值設(shè)為-1即可
animations.append(SKAction.repeatForever(actionLeft))
}
}
然后在Player.swift中添加以下代碼厌丑,讓Player類的對(duì)象遵從Animatable協(xié)議
extension Player : Animatable {}
然后在Animatable.swift的協(xié)議中添加以下成員變量渔呵,使得每一個(gè)遵從該協(xié)議的實(shí)例化對(duì)象都必須要定義承載動(dòng)作的animations數(shù)組(必須實(shí)例化Player類的對(duì)象就必須要定義該數(shù)組)
var animations: [SKAction] {get set}
因?yàn)閜layer對(duì)象需遵從Animatable協(xié)議,所以我們需要在Player類中定義該動(dòng)畫(huà)數(shù)組
var animations: [SKAction] = []
這里有個(gè)細(xì)節(jié)需要說(shuō)明或者說(shuō)是Mark一下耕驰,之前我們定義Animatable的時(shí)候是如下定義的朦肘。
protocol Animatable {
}
然后我們?cè)趯?xiě)Animatable的extension方法以后發(fā)現(xiàn)變異的時(shí)候會(huì)報(bào)錯(cuò)媒抠,大概報(bào)錯(cuò)的意思就是那兩個(gè)方法返回的數(shù)據(jù)類型是mutable的咏花,但實(shí)際是不允許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.
翻譯一下
不幸地是锉桑,有了這個(gè)協(xié)議就編譯不過(guò)了窍株。協(xié)議通常會(huì)認(rèn)為遵循協(xié)議的這些類型可能會(huì)包含值語(yǔ)義(比如結(jié)構(gòu)體和枚舉),從而使得animations不可改變后裸。在這種情況下只有 Characters 中的Player微驶、 Bug類將遵循Animatable協(xié)議因苹,正是因?yàn)樗麄儗儆赾lass所以有引用語(yǔ)義,所以你可以告訴Animatable只有class類的對(duì)象才會(huì)遵循該協(xié)議扶檐。(新手表示到這里的時(shí)候有點(diǎn)一臉懵逼,大概的意思就是因?yàn)閜layer是class類的實(shí)例化對(duì)象款筑,而Animatable又是為player量身定制的奈梳,所以讓Animatable只作用class類的對(duì)象,其extension的方法就是mutable的了漆撞?先暫時(shí)這樣理解叫挟,等日后再來(lái)笑話現(xiàn)在的自己限煞,哈哈哈)署驻。
protocol Animatable: class {
}
2. 讓player動(dòng)起來(lái)
回到Player.swift健霹,在其初始化函數(shù)中添加以下代碼:
createAnimations(character: "player")
返回createAnimations方法一看就知道糖埋,這樣就把相關(guān)的素材全部合成了動(dòng)畫(huà)并添加到了animations[]數(shù)組中以備后用。
然后就是根據(jù)當(dāng)前player的velocity判斷player的朝向并讓他動(dòng)起來(lái)征候。
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的意思是指返回枚舉對(duì)象的成員值疤坝,那就想通了這里的運(yùn)行原理馆铁。
因?yàn)橹岸x的時(shí)候就將forward、backward历谍、left及right的值分別定義為0、1印蔬、2扛点、3
enum Direction: Int {
case forward = 0, backward, left, right
}
然后對(duì)應(yīng)的動(dòng)畫(huà)動(dòng)作也是按照f(shuō)orward岂丘、backward奥帘、left及right的順序添加到animations中,所以數(shù)組animation[i]中的動(dòng)畫(huà)對(duì)象就和枚舉中的對(duì)象一一對(duì)應(yīng)松蒜,自然通過(guò)animations[direction.rawValue]就可以找到對(duì)應(yīng)的動(dòng)畫(huà)了秸苗。
因?yàn)榉较蛐r?yàn)是通過(guò)move(target:)中去觸發(fā)的运褪,所以需要將checkDirection()添加到move(target:)中。
現(xiàn)在player不僅僅可以指哪打哪了檀咙,而且可以根據(jù)對(duì)應(yīng)的方向調(diào)整朝向弧可,而且也動(dòng)起來(lái)了
創(chuàng)建Bug
在challenge中讓添加bug類并添加到場(chǎng)景中棕诵,這個(gè)就沒(méi)什么說(shuō)的了年鸳,代碼如下(留作下一章使用):
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
}
}
這一章大概就這樣了搔确,話說(shuō)實(shí)際操作一遍膳算,然后邊梳理邏輯邊寫(xiě)筆記好累啊,不過(guò)這樣確實(shí)也很有用华匾,免得無(wú)腦跟著教程敲一遍代碼就over了机隙,接下來(lái)的兩章就是中級(jí)地圖編輯及保存和加載數(shù)據(jù)有鹿。
年前就先這樣了葱跋,好好過(guò)年放松一下,年后再戰(zhàn)稍味。大家新年快樂(lè)模庐。在此我也給自己樹(shù)立個(gè)小目標(biāo)吧油宜。爭(zhēng)取今年能有兩個(gè)成熟的小游戲成功上架APP STORE吧~新年還要加油