iOS卡通人物帧动画入门

Cocoa利用TexturePacker创建的纹理图集实现角色的帧动画

by 大熊猫侯佩


什么是TexturePacker

TexturePacker是一个非常棒的纹理集制作工具,广泛应用在2D游戏的制作中。它可以支持多种开发平台,比如Unity,Cocos2D-x,Cocos2D,SpriteKit等等。利用这些游戏开发平台使用它来制作帧动画那是小菜一碟。不过这里要说的是如何在Cocoa环境下利用TexturePacker来制作角色的帧动画。

iOS卡通人物帧动画入门_第1张图片
这里写图片描述
iOS卡通人物帧动画入门_第2张图片
这里写图片描述

TexturePacker有windows、linux和mac三种版本,可以到官网下载:
https://www.codeandweb.com/texturepacker

什么是角色的帧动画

常见于游戏开发中,比如主人公的行走,奔跑和跳跃等动画效果

iOS卡通人物帧动画入门_第3张图片
这里写图片描述

辅以各种方向和位置的位移,可以实现在地图中四处行走的效果。

正常来说我们需要为角色的每个方向创建一套纹理,上图只是制作了角色面朝左侧的纹理。如果加上其他特殊的动作,比如躺倒,跳跃,弯腰等等效果,要制作的纹理就更多了。不过别担心,我们这里讲解只涉及到角色4个方向,即上、下、左、右的纹理。

使用场景

神马!Cocoa里面还需要这种动画?

额...怎么说呢...正常来说确实不多见,不过在一些特别的场合利用角色动画可以给你的App带来意想不到的惊艳效果。比如想象一下开发一款任务管理类型的App,但这种App太多了,如何让它与众不同一点?我们尝试这样一个idea:用打怪升级的方式来记事,用游戏冒险的方式来完成任务,只不过这次游戏的主角不再是虚拟的人物,而是真真切切的大活人-----就是你自己!

我们需要在设计时就让App外观给人的感受定一个基调:卡通英雄迎接变态挑战!那么可以想象到的一个场景就是:用户完成了一个任务后,一个卡通骑士从屏幕里冲出来欢呼雀跃!

注意我们的App的基调是冒险升级,所以UI里到处应该可见各种角色的各种玩耍动画,这里如果简单的使用UIKit的视图或层动画来纯手工完成这些活,对于这么多的工作量,会出人命的!-_-b

利用前面提到的TexturePacker,结合新的或已有的纹理素材,我们可以在Cocoa中快速完成我们所需要的动画效果。

有人会说,可以用SpriteKit来完成这一效果哦,再不济也可以SpriteKit与Cocoa混搭。这是可以的,但需要有SpriteKit基础,而且涉及到两者的整合,尤其是UI整合的问题,有机会我们可以在以后的偏向游戏开发的内容中探讨。

概览:我需要准备神马?

简单的说你只需要TexturePacker、一些素材、Xcode、再加上一些特定的库就哦了!当然你需要有iOS开发的基础,我会用Swift语言(4.0)来介绍,虽然那些特定的库是用Objective-C开发的,但这对我们的使用不会有太大影响。

我的开发环境是OS X 10.12.6 + Xcode 9.2 + TexturePacker 4.6.1
大家可以参考一下。

零.准备纹理素材

如果没有现成的纹理集,我们需要依次为角色建立不同动作的纹理,然后加以整合。本猫从第三方的图片集中使用图片处理工具截取了16张图片纹理,对应角色行走的4个方向,每个方向4帧。

iOS卡通人物帧动画入门_第4张图片
这里写图片描述

注意这些图片的命名方式,我们写代码的时候会涉及到。

一.TexturePacker出场:制作纹理图集

打开TexturePacker,将上面16张图片拖拽到左侧工具栏:

iOS卡通人物帧动画入门_第5张图片
这里写图片描述

可以看到TexturePacker中间已经显示了纹理图集的预览图,右侧中间部分可以设置一些通用属性,如果觉得不满意,右下角还可以选择打开高级设置。不过这里我们啥也不用调整,使用默认设置即可。

现在我们注意一下右侧顶部:

iOS卡通人物帧动画入门_第6张图片
这里写图片描述

在这里有3个关键的配置点,依次为:

  1. 导出的Framework配置格式
  2. 导出的数据文件路径
  3. 导出的纹理文件路径

对于后两个配置,大家可以指定保存数据文件和纹理文件的位置。有些人可能好奇这两个文件分别表示什么?其中数据文件用来描述纹理图集中各个单个纹理,比如第1张纹理在图集的什么位置,是否旋转,叫什么名称等信息;而纹理文件就是实际导出的各个纹理的集合了,这里是之前16张纹理的整合。

现在我们面临一个关键的问题,我们要导出何种格式的配置文件?这就是第一个配置点的用途,点击打开可选择的格式列表:

iOS卡通人物帧动画入门_第7张图片
这里写图片描述

可以发现:哇!好多格式可以选择啊!这只是冰山一角,你可以向下拖动选择更多的格式。这里我们只关注两种格式:UIKit(Plist)和xml格式。

xml是一种通用格式,用过的人都知道它是神马。Cocoa是可以支持读取xml文件的,所以如果用它也是可以的。不过这里我们使用导出iOS或者OS X上非常常用的Plist格式,这是因为Cocoa对其支持更好。

选中第一个UIKit(Plist)格式,点击Convert按钮。这时并没有真正导出任何东西哦。回到TexturePacker主界面,点击上方工具栏中的Publish sprite sheet按钮,选择一个保存名称w(这不是误敲,我选择的导出名称就是w.如果你之前选择过了则会自动跳过),点击确定后会自动完成发布,也就是导出纹理。如果不出意外你会看到一列绿钩,然后点击Ok按钮就可以。

iOS卡通人物帧动画入门_第8张图片
这里写图片描述

回到导出纹理文件的目录中,你会发现多了2个文件:w.png和w.plist

TexturePacker的使命暂时告一段落了,接下来轮到Xcode隆重登场了!

二.创建一个新项目,导入纹理图集和动画库

打开Xcode,创建一个单视图工程,作为一个熟练的iOS开发者,你一定知道怎么做。在新工程左侧的资源导航视图中新建一个group,名字就叫:Support Files.将之前创建的w.png和w.plist文件拖入该group。

iOS卡通人物帧动画入门_第9张图片
这里写图片描述

再创建一个group,名称为API。将4对Objective-C文件(共8个,.h和.m各4个)拖入该组。这8个文件分别为:

  • CAWSpriteReader.h和CAWSpriteReader.m
  • CAWSpriteData.h和CAWSpriteData.m
  • CAWSpriteCoreLayer.h和CAWSpriteCoreLayer.m
  • CAWSpriteLayer.h和CAWSpriteLayer.m

它们可以在github中下载到:

https://github.com/CodeAndWeb/UIKit-TexturePacker/tree/master/demo/CAWTexturePackerSprites

不过后面使用中需要稍微做些修改和扩展。别看它们有8个感觉好多,不过别怕,我们实际只会用到2个,就是加粗显示的那2个,其中1个还是轻度使用。我们只会稍微多的使用CAWSpriteLayer这个类,另外4个是对它们的“后台”支持,你基本可以不用关心。

iOS卡通人物帧动画入门_第10张图片
这里写图片描述

当你拖入Objective-C文件到Swift项目中时,Xcode会为你自动创建一个桥接文件,打开它,将其修改成如下内容:

#import "CAWSpriteReader.h"
#import "CAWSpriteLayer.h"

三.调整UI界面

打开main.storyboard,大致按如下步骤调整界面:

  1. 拖入一个UIView,占据View的上边大部分空间,将其背景色设置为灰色;
  2. 拖入4个按钮,向PS4游戏手柄方向键那样布局,放在View的下半部分,分别设置好其title对应的名称: up,down,left和right;
  3. 在ViewController类中创建1个outlet和4个action,分别对应于灰色的View和4个按钮,然后从IB中绑定它们:
@IBOutlet weak var sandBoxView:UIView!

@IBAction func up(){}
@IBAction func down(){}
@IBAction func left(){}
@IBAction func right(){}

最终的界面类似下图:

iOS卡通人物帧动画入门_第11张图片
这里写图片描述

没必要再为每个UI元素设定自动布局了,因为我们决定只在iPhone6上运行。

四.正式开始前的一点小调整

首先这8个文件(4个类)是用Objective-C写的,比较早了。所以导入项目后会有若干语法错误和警告。总的来说都是比较容易修复的问题,大家可以自行尝试修复,可以只修复错误而忽略警告。

如果Objective-C语言不太熟的,可以使用我最终修改后的版本。

另外在正式写代码之前,我们有必要对那8个文件中的CAWSpriteReader.m文件代码做些小调整。打开CAWSpriteReader.m文件,定位到 + (NSDictionary *)spritesWithContentOfFile:(NSString *)filename方法,注释掉方法开头这段代码:

    // check if we need to load the @2x file
    if ([[UIScreen mainScreen] respondsToSelector:@selector(displayLinkWithTarget:selector:)] &&
        ([UIScreen mainScreen].scale == 2.0))
    {
        file = [NSString stringWithFormat:@"%@@2x", file];
    }
    

因为我们不准备做一套2x大小的纹理,因为你还可能要再做一套3x大小的,以讲解为目的意义不大,所以这段代码不要也罢。如果不懂啥意思的可以自行忽略。

五.写一个简单的测试

是时候写一些代码了 0

当你看到如上的界面,你大概已经了解要写一个怎样的测试:就是通过点击方向键控制游戏主角在沙盒(sandBox)中行走,同时显示动画,这是必须的!

打开ViewController类,在viewDidLoad中添加如下代码:

let spritesData = CAWSpriteReader.sprites(withContentOfFile: "w.plist")!
let texture = UIImage(named: "w.png")!
print("sprites count is \(spritesData.count)")

运行App,你应该在调试console中看到

sprites count is 16

这句话,否则一定之前的哪个步骤有问题,请回到前面检查。

因为你的纹理图集中有16个纹理,所以这里spritesData中也会有16个对应的项目。你猜的没错,CAWSpriteReader就是用来读取纹理图集的配置文件并将其内容保存为内存对象供后续使用的类。

随后再添加如下几句代码:

sprite = CAWSpriteLayer(spriteData: spritesData, andImage: texture)
        
sandBoxView.layer.addSublayer(sprite)
sprite.position = sandBoxView.center
    
sprite.showFrame("w正1")
    

你会发现编译不过去,提示sprite变量未定义,你一定知道怎么办。在ViewController中添加一个实例变量:

var sprite:CAWSpriteLayer!

现在运行App,当当当...

iOS卡通人物帧动画入门_第12张图片
这里写图片描述

哇!主角闪亮登场...很有成就感的样子。不过除了他还不会动之外,可能还有几个问题:

  • 他为什么那么小?
  • 上面这些代码都是啥意思?

别急,下面本猫会解释代码并给出解决办法

六.熟悉CAWSpriteLayer类

CAWSpriteLayer类派生自CALayer类。它其实只是一个包装类,真正在后面干活的是CAWSpriteCoreLayer这个类,CAWSpriteCoreLayer也派生自CALayer。

正常情况下对于第三方的库或类,我们只要简单看一下其接口声明,不用太过关心它的内部实现,基本把它们当做黑盒来用。不过在某些情况下我们需要稍微了解一下实现,比如你觉得类缺少某些功能,需要自己添加的情形。在后面我们会尝试对CAWSpriteLayer做一些扩展,到时候我们会再详细说明。

简单浏览一下CAWSpriteLayer的接口你大致可以知道怎么用这个类了,我再解释一下上面添加的代码:

//创建一个CAWSpriteLayer类,同时关联纹理配置信息和纹理图片
sprite = CAWSpriteLayer(spriteData: spritesData, andImage: texture)
//将sprite加入到沙盒场景中        
sandBoxView.layer.addSublayer(sprite)
//设置sprite的位置为居中显示
sprite.position = sandBoxView.center
//显示角色正面的第一个静态纹理    
sprite.showFrame("w正1")
    

最后一句很重要,如果没有它,屏幕上就会啥也没有。

为什么sprite在场景中显示会那么小?这时因为我是在iPhone 6p上运行的,这意味着如果要正常显示,得提供@3x大小的纹理素材,否则相对来说就会“缩小”3倍显示。对这个概念不太了解的童鞋可以自行搜索一下。

如果有@3x的素材那就会十分完美,不过咱不是没有嘛!还好我们只是以讲解为目的,所以丑就丑点,只要能让它放大3倍,哪怕分辨率变差,变模糊也是可以接受的。

前面说过CAWSpriteLayer派生自CALayer,所以我们直接将CALayer放大就可以了,添加如下代码:

sprite.transform = CATransform3DScale(sprite.transform, 3.0, 3.0, 1.0)

运行App,我们感觉变得稍微好了一点:

iOS卡通人物帧动画入门_第13张图片
这里写图片描述

不过,还有一个大问题:它呆呆的站在那里,丝毫不会动!

解决起来很容易,超乎你的想象!!!

七.让动画跑起来

让主角动起来很容易,只需一句!紧接上面的代码添加如下一行:

sprite.playAnimation("w正%d", withRate: 6, andRepeat: Int32.max)

运行App,这就是原地踏步的赶脚:

iOS卡通人物帧动画入门_第14张图片
这里写图片描述

是不是超简单,它背后的原理是使用CALayer上的动画,可以通过CAWSpriteCoreLayer类源代码来查看。

注意这里的rate表示的是每一帧显示的秒数。比如这里被设置为6,向下纹理共有4帧,所以每帧显示4/6 = 0.67秒,总共显示 4/6 * 4 = 2.67秒。如果总共有6帧则每帧显示1秒,共显示6秒。所以这里可以通过调整rate的大小来决定纹理集动画显示的时间,越大动画显示的越快,越小动画显示的越慢。

八.让主角走起来

显然你不想让主角原地踏步,你想让它走动起来。实现起来也不难,只要动画配合位移就可以了。我们先来实现向上方向的移动

首先在ViewController类中创建一个Direction枚举:

enum Direction {
    case none
    case down
    case up
    case left
    case right
}

然后创建一个currentDirection实例方法:

var currentDirection:Direction = .none

接着在up方法里添加如下代码:

@IBAction func up(){
    if currentDirection != .up{
        currentDirection = .up
        sprite.playAnimation("w背%d", withRate: 6, andRepeat: Int32.max)
    }
    sprite.position.y -= 10
}

每次按下up按钮,我们将主角向上移动10个点。

运行App,感觉一下效果:

iOS卡通人物帧动画入门_第15张图片
这里写图片描述

哇!我们之前的努力没有白费,值得拍手庆祝一下!既然向上的放心搞定了,其它放心也没什么难度了,依次补全其它3个方法:

@IBAction func down(){
    if currentDirection != .down{
        currentDirection = .down
        sprite.playAnimation("w正%d", withRate: 6, andRepeat: Int32.max)
    }
    sprite.position.y += 10
}

@IBAction func left(){
    if currentDirection != .left{
        currentDirection = .left
        sprite.playAnimation("w左%d", withRate: 6, andRepeat: Int32.max)
    }
    sprite.position.x -= 10
}

@IBAction func right(){
    if currentDirection != .right{
        currentDirection = .right
        sprite.playAnimation("w右%d", withRate: 6, andRepeat: Int32.max)
    }
    sprite.position.x += 10
}

现在我们的主角可以向四个方向随意行走了,并且还伴随动画,爱死它了!!!

九.设置边界

现在主角走着走着就看不见人影了,所以有必要给沙盒设置一个边界。理论上很容易,只要确定好每个边界上的x和y值就可以了,不过我们需要同时考虑到sprite本身的大小!但遗憾的是直接通过:

sprite.bounds.size

取出的值是(0,0),所以我们得尝试用其他办法来取得主角的大小。这就得像前面所说的那样深入第三方类去一窥究竟了。

我们发现在CAWSpriteCoreLayer里包含一个spriteData对象,其中包含了所有纹理的信息,当然包括尺寸了。我们采用同样的策略:

CAWSpriteCoreLayer干活,CAWSpriteLayer享受

首先在CAWSpriteCoreLayer类里添加如下方法:

- (CGSize)sizeForFrame:(NSString *)frameName{
    CAWSpriteData *data = [spriteData objectForKey:frameName];
    CGSize size = CGSizeMake(data.spriteWidth, data.spriteHeight);
    return size;
}

然后修改它的接口:

- (CGSize)sizeForFrame:(NSString *)frameName;

同样在CAWSpriteLayer类里添加同名方法:

- (CGSize)sizeForFrame:(NSString *)frameName{
    return [animationLayer sizeForFrame:frameName];
}

最后修改其接口:

- (CGSize)sizeForFrame:(NSString *)frameName;

OK,回到ViewController类中,创建一个spriteSize方法:

func spriteSize(for toward:Direction)->CGSize{
    let spriteSize:CGSize
    switch toward{
    case .down:
        spriteSize = sprite.size(forFrame: "w正0")
    case .up:
        spriteSize = sprite.size(forFrame: "w背0")
    case .left:
        spriteSize = sprite.size(forFrame: "w左0")
    case .right:
        spriteSize = sprite.size(forFrame: "w右0")
    default:
        fatalError()
    }
    return spriteSize
}

这里我们取每个方向第一个帧的纹理作为基准,返回它的大小。

现在我们可以写边界检查方法了,新建boundaryTest方法:

func boundaryTest(toward:Direction){
        
    let spriteSize = self.spriteSize(for: toward)
    
    if sprite.position.x <= spriteSize.width / 2{
        sprite.position.x = spriteSize.width / 2
    }
    
    if sprite.position.x >= sandBoxView.bounds.width - spriteSize.width * 1.5{
        sprite.position.x = sandBoxView.bounds.width - spriteSize.width * 1.5
    }
    
    if sprite.position.y <= spriteSize.height / 2{
        sprite.position.y = spriteSize.height / 2
    }
    
    if sprite.position.y >= sandBoxView.bounds.height - spriteSize.height * 1.5{
        sprite.position.y = sandBoxView.bounds.height - spriteSize.height * 1.5
    }
}

然后在up,down,left,right四个方法的最后添加一句:

boundaryTest(toward: currentDirection)

运行App,欧耶!终于不能突破边框啦!Perfect!!!

十.静若处子,动若脱兔

继续在沙盒里游走一番,享受一下我们的战斗成果.你会发现当主角保持静止状态时仍然会显示一个行走的动画.有时候这很好,但有时原地踏步也会显得很怪异.

我们希望当主角移动的时候显示行走动画,当他停下来的时候动画也停下来.

因为CAWSpriteLayer类实际上是一个CALayer,所以我们想办法使用层上的动画来达到这一目的.同样我们先尝试实现一个方向,然后拓展到所有方向,就先拿向上的方向up来说吧,基本逻辑是这样:

  1. 因为播放层动画不希望被打断,所以up方法不能重入.这是靠实例变量wasEntered来保证;
  2. 只有当转向到up方向时才需要重新播放动画,否则只需要恢复动画;
  3. 创建层动画指定向下的位移,计算动画播放需要经历的时间,将动画添加到sprite上去;
  4. 在层动画完成时暂停主角帧动画的播放;
  5. 最终进行边界检查.

OK,我们首先注释掉之前viewDidLoad中的动画播放代码:

//sprite.playAnimation("w正%d", withRate: 6, andRepeat: Int32.max)

同时新建一个实例方法:

var wasEntered = false

然后我们修改up方法为如下内容:

@IBAction func up(){
    guard wasEntered == false else {return}
    
    wasEntered = true
    
    if currentDirection != .up{
        currentDirection = .up
        sprite.playAnimation("w背%d", withRate: 6, andRepeat: Int32.max)
    }else{
        sprite.resume()
    }
    
    sprite.position.y -= 20
    let moveAnim = CABasicAnimation(keyPath: "position.y")
    moveAnim.fromValue = sprite.position.y + 20
    moveAnim.toValue = sprite.position.y
    moveAnim.duration = 4.0/6.0
    
    sprite.add(moveAnim, forKey: nil)
    
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 4.0/6.0){
        self.sprite.pause()
        self.wasEntered = false
    }
    boundaryTest(toward: currentDirection)
}

运行一下App:

iOS卡通人物帧动画入门_第16张图片
这里写图片描述

是不是满足我们的期望呢? 0

等一下,如果你看到最后,会发现主角在碰到上方边界时有一个回退现象,好像有些唐突,我们马上就来修复它.

十一.修复边界"回退"

这种情况出现的原因是我们先位移再判断边界,当检查到超出边界强制退回,此时已经晚了.解决的办法就是主动调整位移的长度,做到"先下手为强"!

iOS卡通人物帧动画入门_第17张图片
这里写图片描述

可以参考上图,该图示意的是主角向上或向右移动的情况;当主角处在y=5的位置时,此时向上移动10个点将会超出边界0,达到y=-5(向右侧移动同理).所以此时不可以移动10个点,只能移动 当前位置(5) - 边界(0) = 5个点.其他方向道理是一样的,我们很快可以写一个新的方法来计算实际的位移:

func adjustDistance(_ distance:CGFloat,for toward:Direction)->CGFloat{
    let maxPoint:CGFloat
    let spriteSize = self.spriteSize(for: toward)
    
    switch toward {
    case .up:
        maxPoint = spriteSize.height / 2
        return min(sprite.position.y - maxPoint, distance)
    case .down:
        maxPoint = sandBoxView.bounds.height - spriteSize.height * 1.5
        return min(maxPoint - sprite.position.y, distance)
    case .left:
        maxPoint = spriteSize.width / 2
        return min(sprite.position.x - maxPoint, distance)
    case .right:
        maxPoint = sandBoxView.bounds.width - spriteSize.width * 1.5
        return min(maxPoint - sprite.position.x, distance)
    default:
        fatalError()
    }
}

adjustDistance包含2个参数,第一个是尝试移动的距离,第二个是移动的方向.该方法返回调整后移动的距离.

回到我们新实现的up方法,将其中下面两句代码:

sprite.position.y -= 20
moveAnim.fromValue = sprite.position.y + 20

分别替换为:

sprite.position.y -= adjustedDistance
moveAnim.fromValue = sprite.position.y + adjustedDistance

别忘了在前面加上adjustedDistance变量的定义:

let adjustedDistance = adjustDistance(20, for: .up)

再次运行App,看一下效果吧:

iOS卡通人物帧动画入门_第18张图片
这里写图片描述

这下主角遇到边界也不会回退了,我们的目的达到了。下面我们就来尝试将新的up方法拓展到所有的方向吧。

十二.拓展还是重构?

但是先等一下!!!你确定要把up里的内容重复3遍,其中的内容到底有多少要改动呢?我们来看一下:实际要改动的地方只有和方向有关的位移,也就两、三句代码而已。并且如果你只是重复拷贝代码,还会带来一个非常严重的问题:你的位移距离以及动画时长会同时存在于4个地方,如果你将来觉得不妥要修改,那可麻烦了,你要同时修改所有这些地方,而且稍有不慎忘了或改错了哪个地方,那么调试起来可有你受的哦。

所以为了不以后遭罪,为了不违反DRY原则,我们当然选择重构代码!

为了避免同一方向反复重新播放动画,我们首先创建一个新的实例变量:

var lastDirection:Direction = .none

我们看一下新的方法需要哪些参数:

  • 角色需要移动的方向
  • 角色需要移动的位移距离
  • 同一方向帧的数量
  • 每一帧显示的时间

有了这些参数再结合我们上面新实现up方法的内容,我们就可以灵活可变的实现角色移动功能了,在ViewController类中新建如下moveSprite实例方法:

func moveSprite(toward:Direction,point:CGFloat,framesCount:Int,rate:Int){
    guard wasEntered == false else {return}
    wasEntered = true
    
    let moveAnim:CABasicAnimation
    let duration = TimeInterval(CGFloat(framesCount)/CGFloat(rate))
    let frameName:String
    let adjustedPoint = adjustDistance(point, for: toward)
    
    switch toward {
    case .down:
        frameName = "w正%d"
        sprite.position.y += adjustedPoint
        moveAnim = CABasicAnimation(keyPath: "position.y")
        moveAnim.fromValue = sprite.position.y - adjustedPoint
        moveAnim.toValue = sprite.position.y
    case .up:
        frameName = "w背%d"
        sprite.position.y -= adjustedPoint
        moveAnim = CABasicAnimation(keyPath: "position.y")
        moveAnim.fromValue = sprite.position.y + adjustedPoint
        moveAnim.toValue = sprite.position.y
    case .left:
        frameName = "w左%d"
        sprite.position.x -= adjustedPoint
        moveAnim = CABasicAnimation(keyPath: "position.x")
        moveAnim.fromValue = sprite.position.x + adjustedPoint
        moveAnim.toValue = sprite.position.x
    case .right:
        frameName = "w右%d"
        sprite.position.x += adjustedPoint
        moveAnim = CABasicAnimation(keyPath: "position.x")
        moveAnim.fromValue = sprite.position.x - adjustedPoint
        moveAnim.toValue = sprite.position.x
    default:
        fatalError()
    }
    
    if toward == lastDirection{
        sprite.resume()
    }else{
        lastDirection = toward
        sprite.playAnimation(frameName, withRate: Float(rate), andRepeat: Int32.max)
    }
    
    moveAnim.duration = duration
    sprite.add(moveAnim, forKey: nil)
    
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + duration){
        self.sprite.pause()
        self.wasEntered = false
    }
}

貌似有点长,不过带来的好处是显而易见的,我们消除了重复代码错误的万恶之源!并且我们新的四个方向处理方法变了异乎寻常的简单了,将up,down,left和right方法修改为如下内容:

@IBAction func up(){
    moveSprite(toward: .up, point: 60, framesCount: 4, rate: 6)
}
    
@IBAction func down(){
    moveSprite(toward: .down, point: 60, framesCount: 4, rate: 6)
}
    
@IBAction func left(){
    moveSprite(toward: .left, point: 60, framesCount: 4, rate: 6)
}
    
@IBAction func right(){
    moveSprite(toward: .right, point: 60, framesCount: 4, rate: 6)
}

我们可以删除原先的currentDirection变量,因为已经用不着了。

好了,运行一下App,欣赏一下我们的劳动果实吧 _

我们的文章到此即将告一段落了,不过如果你还意犹未尽,可以看一下如何按需求扩展第三方的类,以达到我们的特定的需求。如果你感觉有点累,想要去happy一下,跳过它直接看结尾也没有问题哦。

十三.番外篇:扩展第三方类

细心的朋友可能会发现,我们前面计算主角的大小用的总是同一方向第一帧纹理的大小,如果纹理大小有出入的话,会产生较大的偏差,最好的方法是取当前动画帧纹理的大小。不过这有些难度,所以我们退之求其次,计算所有帧的平均大小吧。

这次我们不修改原有的第三方类,因为我们上面已经熟悉了类的内部功能,所以我们直接用Swift写一个类的扩展吧(Objective-C的语法...)。

在项目API组中新建一个Swift文件,名为CAWSpriteLayer+ext.swift。

打开该文件,将其替换为如下内容:

import UIKit

extension CAWSpriteLayer{
    func avgSizeForFrameBase(_ frameNameBase:String)->CGSize{
        //待实现
    }
}

可以看到我们在CAWSpriteLayer类的扩展里新建了方法,该方法唯一的参数为同一方向的纹理名称前缀,即如果是向上,则会传入 "w背" 实参,它会将所有"w背"前缀的纹理大小都加入计算。

我们前面已经了解到,CAWSpriteLayer类中含有一个animationLayer.spriteData变量,其中有我们想要每一帧名称、大小等等所需要的所有信息。

我们现在来实现avgSizeForFrameBase方法,将其中的注释一行替换为如下内容:

let dict = animationLayer.spriteData as! [String:CAWSpriteData]
let baseNames = Array(dict.keys)
let frameNames = baseNames.filter {$0.hasPrefix(frameNameBase)}
    
var totalWidth:CGFloat = 0
var totalHeight:CGFloat = 0
let count = CGFloat(frameNames.count)
for frameName in frameNames{
    let spriteData = dict[frameName]!
    totalWidth += CGFloat(spriteData.spriteWidth)
    totalHeight += CGFloat(spriteData.spriteHeight)
}
    
return CGSize(width: totalWidth/count, height: totalHeight/count)

回到spriteSize方法,将其中的:

spriteSize = sprite.size(forFrame: "w正0")

之类的方法,换为新的平均值方法:

spriteSize = sprite.avgSizeForFrameBase("w正")

其他方向类似。

好啦!我们已经成功的按我们的需求扩展了第三方的类!!!

十四.结尾

经历了前面这么多的内容,大家看的一定很累,这是自然的。(虽然本猫写的也很累...),希望大家可以略微学到一丢丢新知识,希望大家可以把它应用到实际App开发中去 _

现在!抛开电脑,到了happy的时候了!冲个热水澡,来杯冰镇可乐+至尊大汉堡套餐?之类的美味吧!!!

感谢观赏,再会!

PS:全部代码可以到我的github中下载:

https://gitee.com/hopy/iOS-JingJin/tree/master/TPSupportsTest

你可能感兴趣的:(iOS卡通人物帧动画入门)