从零开始的8-bit迷宫探险【Level 13】主角总是孤独的

「呀!呀!」一只乌鸦飞了过去,因为视线不明,让移动的黑影更加引人遐想。
在这诺大的森林里,就只有山姆一个人,登山杖插进土里的声音清楚回荡在空气中。
山姆调整了一下背包,深深吸了一口气,再次提起脚步前进。

今日目标

  • 新增角色 (父类别) 及主角 (子类别)
  • 新增一个主角在游戏画面中
  • 播放主角的动画

PS. 这里是开发 iOS 手机游戏的系列文,如果还没看过之前 剧情 文章的朋友,欢迎先点这边回顾唷!


新增角色类别

我们新增一个共用的角色类别,可以让主角继承,未来也能让怪物继承。
首先新增一个 swift 档案,将程序码区开,比较方便管理。

  • 首先点选新增档案
    https://imgur.com/DyRgPfA.png

  • 选择 Swift File -> Next
    https://imgur.com/IJukGWI.png

  • 将档案命名为 GameCharacter,点击 Create 按钮,完成 .swift 档的新增

引入 SpriteKit

记得万事先 import

  • GameCharacter.swift
import SpriteKit

角色种类

先建立一个列举,加入 NONE 及主角山姆 SAM

  • GameCharacter.swift
enum Role: String {
    case NONE = "none"
    case SAM = "sam"
}

类别的内容

我们希望能记录角色的一些属性:

  • role:角色的种类。
  • node:角色的 SKSpriteNode,可以对其设定位置 (position)、定位点 (anchorPoint)、尺寸 (size)、层级 (zPosition) 等等。
  • gridWH:纪录一格的宽度
  • gridX:记录当前格子的位置 x
  • gridY:记录当前格子的位置 y
  • startGridX:纪录起始格子的位置 x
  • startGridY:纪录起始格子的位置 y
  • imageName:纪录角色起始图片

我们在建构子 (init) 写上初始化时需带入的参数:

  • gridWH:一格的宽度
  • startGridX:角色初始格子的位置 x
  • startGridY:角色初始格子的位置 y
  • imageName:角色图片
  • zPosition:角色图片的层级
  • role:角色种类

在建构子中,储存带入的参数值,并且新增一个 SKSpriteNode,将定位点 (anchorPoint)、尺寸 (size)、位置 (position)、层级 (zPosition) 都设定好。

  • GameCharacter.swift
class GameCharacter {
    var role: Role = .NONE
    var node: SKSpriteNode
    var gridWH: Int
    var gridX: Int
    var gridY: Int
    var startGridX: Int
    var startGridY: Int
    var imageName: String
    
    init(gridWH: Int, startGridX: Int, startGridY: Int, imageName: String, zPosition: CGFloat, role: Role) {
        self.role = role
        self.gridWH = gridWH
        self.gridX = startGridX
        self.gridY = startGridY
        self.startGridX = startGridX
        self.startGridY = startGridY
        self.imageName = imageName

        self.node = SKSpriteNode(imageNamed: imageName)
        self.node.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        self.node.size.width = CGFloat(gridWH)
        self.node.size.height = CGFloat(gridWH)
        self.node.position = CGPoint(x: gridWH * startGridX + (gridWH/2), y: -gridWH * startGridY - (gridWH/2))
        self.node.zPosition = zPosition
    }
}

新增 ZPosition 列举

我们的游戏画面就像是个画布层层堆叠,放在上面的物体都是有层级的,数字越高的在越上方
而 SpriteKit 会帮我们预设定为:0.0
先试想游戏会出现几种交叠在一起的物体:收集品、主角、坏天气怪物、能隐身的树,分别帮他们先定义好正确的层级。其中一种是隐藏 (-1),当设定为此种类型,则会有消失在画面中的效果。
请先定义好 ZPosition

  • GameScene.swift
enum ZPosition {
    static let HIDE = -1
    static let COLLECTION = 1
    static let SAM = 2
    static let WEATHER = 3
    static let PURPLE_TREE = 4
}

新增主角类别

新增一个主角类别档案:Sam.swift,继承 GameCharacter 类别。

  • imageName 带入主角的图档名称 sam
    https://imgur.com/HzKZSJh.png

  • zPosition 带入 ZPosition.SAM

  • role 带入 .SAM (因为父类别已经有定义 role 的型态为 Role,所以可将 Role.SAM 省略为 .SAM)

  • Sam.swift
import SpriteKit

class Sam: GameCharacter {
    init(gridWH: Int, startGridX: Int, startGridY: Int) {
        super.init(gridWH: gridWH, startGridX: startGridX, startGridY: startGridY, imageName: "sam", zPosition: CGFloat(ZPosition.SAM), role: .SAM)
    }
}

将主角加到游戏画面中

考虑到未来会有不同种类的角色,分别会有不同的起始点 (这边提的起始点是指位於迷宫格子中的位置),可以先新增一个结构 (struct),定义不同种类位置的 (x, y) 常数值,方便未来管理。请先新增一个 samStart 的 x 及 y 值。

  • GameScene.swift
struct gridMapping {
    struct samStart {
        static let x = 1
        static let y = 1
    }
}

在 didMove 中,创建一个 Sam 类别的实体,并将起始位置带入。
接着将实体的 node 添加到 mapNode 中。

  • GameScene.swift
class GameScene: SKScene {
    ...
    var sam: Sam?
    
    override func didMove(to view: SKView) {
        ...
        self.sam = Sam(gridWH: self.gridWH, startGridX: gridMapping.samStart.x, startGridY: gridMapping.samStart.y)
        self.mapNode!.addChild(self.sam!.node)
    }
}

执行成果

主角出现在画面中了

https://imgur.com/pl4ngnz.png


主角的动画

如果只有一张贴图,游戏会比较不生动,加入动画可以提升精致度。
我们试着制作两张主角手部举起登山杖的连续图,面朝下的方向,分别取名为:

  • sam_down_1
  • sam_down_2

https://imgur.com/HzKZSJh.pnghttps://imgur.com/CUkapCL.png

回到角色类别,找到设定图片的地方,将原本建立 SKSpriteNode 的地方做调整。

  • 使用 SKTexture 新增两张序列图
  • 透过 SKAction.animate 建立图片序列动画,timePerFrame 代表每张图片的持续时间(秒)
  • 由於这个动画只会执行一次,因此透过 SKAction.repeatForever,带入原本的图片序列动画,让它重复播放
  • 接着建立 SKSpriteNode,并且透过 .run,让它播放动画
  • GameCharacter.swift
// self.node = SKSpriteNode(imageNamed: imageName)

let sequence1 = SKTexture(imageNamed: imageName + "_1")
let sequence2 = SKTexture(imageNamed: imageName + "_2")
let ani = SKAction.animate(with: [sequence1, sequence2], timePerFrame: 0.4)
let aniRepeat = SKAction.repeatForever(ani)
self.node = SKSpriteNode(texture: sequence1)
self.node.run(aniRepeat, withKey: "sequence")

将图档名称改为 sam_down

  • Sam.swift
super.init(gridWH: gridWH, startGridX: startGridX, startGridY: startGridY, imageName: "sam_down", zPosition: CGFloat(ZPosition.SAM), role: .SAM)

执行後就可以看到主角有动画了!

微调图片大小

我们发现主角在地图中看起来似乎有点小,想要将角色图片调整大一点。
因为角色的 anchorPoint 是定在 (0.5, 0.5),因此我们可以直接将 node 的 size 调大,不会影响到定位。
将宽高各加上 13 微调

  • GameCharacter.swift
self.node.size.width = CGFloat(gridWH + 13)
self.node.size.height = CGFloat(gridWH + 13)

执行结果

主角在画面上能显示序列动画罗!

https://imgur.com/CyaguQ6.gif

明日来控制主角移动吧!


参考来源:
SpriteKit zPosition
SpriteKit SKAction animate


<<:  IT 铁人赛 k8s 入门30天 -- day7 K8s YAML 设定档

>>:  DAY06 - API串接常见问题 - CORS - 解决CORS问题篇

DAY 22-凭证颁发机构CA

「子非鱼,安知鱼之乐。」 在介绍协定之前, 我们要来介绍一个非常重要的概念,叫做凭证颁发机构(Cer...

CSS overflow

前言 当子元素溢出母元素时该如何处理 可单指设定X轴或Y轴 overflow-x overflow-...

【从零开始的Swift开发心路历程-Day4】Xcode介面基础介绍

昨天我们提到红色框框里面的东西,今天就来根据他们的作用进行简单介绍吧! AppDelegate.sw...

Day 18. 阿咧?我记得我安装过XCode? Can't find Xcode install for Metal compiler | Unreal Engine

当我在Unreal Engine 4.27.0下载好试图启动软件时,跳出了下面这个视窗,     U...