加入人物
人物共有11张图片,连起来就是动态走路,由于图片略大,这里把xy方向同时缩小到0.4倍,人物的物理体设置为纹理(非透明部分),且不允许旋转,采用精确碰撞检测,即纹理边缘,人物受各种力作用。最后设置11张图片循环播放
1 | func knightInit(){ |
虚拟摇杆
类似王者荣耀,在游戏左下角设置虚拟摇杆,但是位置不固定,可以随着手指改变位置
先构造摇杆UI,一个是外部圆圈circleBar,一个是内部手指按动的点位centerPoint
1 | func jointBar(){ |
然后分别重写下面三个函数,获取手指在屏幕上滑动动作,进行响应,手指触碰的第一个点作为虚拟摇杆当前的初始点位,内圈centerPoint始终限制在外圈里,滑动结束后,摇杆归位
1 | override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { |
对于2D游戏,虚拟摇杆只能控制左右的移动,因此内圈在左半边就是向左,在右半边就是向右,可以重写update函数加入对精灵的左右移动的控制
1 | override func update(_ currentTime: TimeInterval) { |
跳跃
首先在右下角放置一个跳跃的按键
1 | func jumpBarInit(){ |
改写touchesBegan函数如下,如果点击区域在跳跃键位置,则给人物一个向上的脉冲力,人物受重力影响,会按照正常物理规则上升再降落,此时也可操纵摇杆,一边跳跃一边移动
1 | override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { |
冲刺
水平冲刺就是给人物一个水平方向的脉冲力
先在右下角加入一个冲刺的按钮
1 | func sprintBarInit(){ |
然后修改touchesBegan函数,响应按下冲刺按钮的动作
1 | override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { |
其中direction为类成员变量,为false方向是左,true向右,在update函数中加入判断,实时更新方向
1 | override func update(_ currentTime: TimeInterval) { |
1.14更新
以上都是探索阶段,下面的是实际用的代码
玩家状态机
上面写的代码已经很复杂了,对于状态的转换,采用SpriteKit自带的类“状态机”很方便。需要先拟定玩家的几种状态,和它们之间的转换关系,第i行第j列表示是否能从状态i转换到状态j,是为1,否为0。这部分好难搞,花了好几天时间才弄明白。
\ | 站立 | 跳跃 | 下落 | 冲刺 | 走路 | 着陆 |
---|---|---|---|---|---|---|
站立 | 0 | 1 | 0 | 1 | 1 | 1 |
跳跃 | 0 | 0 | 1 | 0 | 0 | 0 |
下落 | 0 | 0 | 0 | 1 | 0 | 1 |
冲刺 | 1 | 0 | 1 | 0 | 1 | 0 |
走路 | 1 | 1 | 1 | 1 | 0 | 0 |
着陆 | 1 | 0 | 0 | 0 | 0 | 0 |
代码如下:
1 | class PlayerState: GKState { |
在GameScene中,添加GKStateMachine,与玩家绑定作为状态机,初始进入下落状态
1 | playerStateMachine = GKStateMachine(states: [JumpingState(playerNode: knight), SprintState(playerNode: knight), StandState(playerNode: knight), WalkingState(playerNode: knight), FallingState(playerNode: knight), LandingState(playerNode: knight)]) |
多点触控
最开始编码的时候,发现了个问题:同时移动摇杆,按跳跃键时不好使,原来是没有开启多点触控,导致touch的各个函数参数只有一个触摸位置,需要在GameScene的构造函数中加入
1 | view.isMultipleTouchEnabled = true |
美化-按钮点击动效
用户按下按钮之后需要加个动效,表示按钮已经按下,这里是用了简单的透明度渐变来做的,代码如下:
1 | extension GameScene { |
视角跟随
玩家左右移动时,需要进行视角跟随,避免丢失视野,建立SKNode作为世界坐标系,将场景中的物体全部添加到该节点的子节点,在物理渲染全部结束后,改变此节点的位置。下面的代码可以使得玩家永远处于屏幕正中央。
1 | override func didSimulatePhysics() { |
深度值设置
完成视角跟随之后,发现了另一个问题,如果地板与按钮重叠时,地板在上层,这样很不合理,因此改变节点的zPosition属性,将背景的节点设置为-1,其余默认为0,就可以使按钮正常显示在顶层了。
改进
当我希望向场景中添加多个同样的物体时,需要写很多重复的代码,我想到了一个方法,为一些不同类别的物体,设计SKSpriteNode的子类,他们的物体性质固定,但是可以放置在不同位置,改变大小。
比如下面的代码将游戏中经常出现的跳跃平台、玩家角色分别封装。需要注意的是对父类的super.init()必须采用非convenience的构造函数,否则会报错
1 | class PlatformProperty: SKSpriteNode { |
目前,基本上把逻辑调通了
update函数中也删去了一些不必要的代码,简化为只判断LandingState条件
1 | override func update(_ currentTime: TimeInterval) { |
SpriteKit Rendering Loop
The rendering loop is tied to the SKScene object. Rendering runs only when the scene is presented. SpriteKit only renders the scene when something changed so its efficient. Here’s what goes on in the rendering loop each frame:
- update - make changes to nodes, evaluate nodes
- SKScene -> evaluates actions
- didEvaluateActions -
- SKScene -> simulates physics
- didSimulatePhysics -
- SKScene -> applies constraints
- didApplyConstraints
- didFinishUpdate
- SKView renders the scene
官网上给的流程图如下:
使用纹理集
今天犯了一个及其愚蠢的错误,纹理集的文件夹后缀应该是.atlas,我给写成了.altas,调了半天,就是不好使,今晚浪费了…
1 | let dbAtlas = SKTextureAtlas(named: "enemy.atlas") |
把玩家纹理集也弄成atlas了,这样比单个图片加载效率更高
加入敌人
在素材网站下了几张图,凑合用了下,能左右行走
构造了一个敌人SKSpriteNode子类,加入了敌人的固定移动动作(keepWalking函数),保持重复的左右巡逻。
1 | class EnemyNode : SKSpriteNode { |
之前玩家状态机存在一个bug,就是下落状态时无法跳跃,可是忽略了从平台上下落的情况,因此在PlayerState类中添加成员变量hasJumped,初始化为false,如果进入JumpingState,则将其设为true,进入LandingState再设为false
1月24日更新:修复了若干bug,优化了代码结构
Protocol的使用
今天把代码全部重构,重头戏就是Protocol的妙用,现学现卖
最直观的理解就是类A在某些情况下需要类B做一些事情,但是他们的成员变量并不共享,无法触发一些连续的事件,这个时候类A作为委托方,写一个协议,类B中实现协议的接口,类A就可以调用类B的接口,实现一系列响应。实际应用的时候分为以下步骤:
先声明Protocol,作为class来定义,例如
1
2
3protocol OperatorBarPlayerDelegate : class{
func updateDirection(direction: Direction)
}作为被委托方的类继承该Protocol;
class PlayerNode: SKSpriteNode, OperatorBarPlayerDelegate {}
- 在委托方类中加入代理成员
weak var delegatePlayer: OperatorBarPlayerDelegate?
- 在被委托方中实现Protocol中的接口
- 创建委托方和被委托方的实例,设置代理
A.delegatePlayer = B
代码实际架构
包含三个主要类:按钮和用户交互类、人物类、玩家状态机类;
按钮和用户交互类作为控制源头,设置为委托方,在另外两个类中分别设计协议,更新玩家状态,赋予人物相应动作。