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

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

by 大熊猫侯佩

什么是TexturePacker

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

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

什么是角色的帧动画

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

再创建一个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个是对它们的“后台”支持,你基本可以不用关心。

当你拖入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(){}

最终的界面类似下图:

没必要再为每个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,当当当…

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

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

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

六.熟悉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,1.0)

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

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

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

七.让动画跑起来

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

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

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

是不是超简单,它背后的原理是使用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,感觉一下效果:

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

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

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

@IBAction func right(){
    if currentDirection != .right{
        currentDirection = .right
        sprite.playAnimation("w右%d",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,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",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",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:

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

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

十一.修复边界”回退”

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

可以参考上图,该图示意的是主角向上或向右移动的情况;当主角处在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,看一下效果吧:

这下主角遇到边界也不会回退了,我们的目的达到了。下面我们就来尝试将新的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,rate: 6)
}

@IBAction func left(){
    moveSprite(toward: .left,rate: 6)
}

@IBAction func right(){
    moveSprite(toward: .right,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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
用Android Studio的VideoView组件实现简单的本地视频播放器。本文将讲解如何使用Android视频播放器VideoView组件来播放本地视频和网络视频,实现起来还是比较简单的。VideoView组件的作用与ImageView类似,只是ImageView用于显示图片,VideoView用于播放视频。...
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
基于yolov5+deepsort+slowfast算法的视频实时行为检测。1. yolov5实现目标检测,确定目标坐标 2. deepsort实现目标跟踪,持续标注目标坐标 3. slowfast实现动作识别,并给出置信率 4. 用框持续框住目标,并将动作类别以及置信度显示在框上
数字电子钟设计本文主要完成数字电子钟的以下功能1、计时功能(24小时)2、秒表功能(一个按键实现开始暂停,另一个按键实现清零功能)3、闹钟功能(设置闹钟以及到时响10秒)4、校时功能5、其他功能(清零、加速、星期、八位数码管显示等)前排提示:前面几篇文章介绍过的内容就不详细介绍了,可以看我专栏的前几篇文章。PS.工程文件放在最后面总体设计本次设计主要是在前一篇文章 数字电子钟基本功能的实现 的基础上改编而成的,主要结构不变,分频器将50MHz分为较低的频率备用;dig_select
1.进入官网下载OBS stdioOpen Broadcaster Software | OBS (obsproject.com)2.下载一个插件,拓展OBS的虚拟摄像头功能链接:OBS 虚拟摄像头插件.zip_免费高速下载|百度网盘-分享无限制 (baidu.com)提取码:6656--来自百度网盘超级会员V1的分享**注意**该插件必须下载但OBS的根目录(应该是自动匹配了的)3.打开OBS,选中虚拟摄像头选择启用在底部添加一段视频录制选择下面,进行录制.
Meta公司在9月29日首次推出一款人工智能系统模型:Make-A-Video,可以从给定的文字提示生成短视频。基于**文本到图像生成技术的最新进展**,该技术旨在实现文本到视频的生成,可以仅用几个单词或几行文本生成异想天开、独一无二的视频,将无限的想象力带入生活
音频信号叠加噪声及滤波一、前言二、信号分析及加噪三、滤波去噪四、总结一、前言之前一直对硬件上的内容比较关注,但是可能是因为硬件方面的东西可能真的是比较杂,而且需要渗透的东西太多了,所以学习进展比较缓慢。因为也很少有单纯的硬件学习研究,总是会伴随着各种理论需要硬件做支撑,所以还是想要慢慢接触理论学习。但是之前总找不到切入点,不知道从哪里开始,就一直拖着。最近稍微接触了一点信号处理,就用这个当作切入点,开始接触理论学习。二、信号分析及加噪信号处理选用了matlab做工具,选了一个最简单的语音信号处理方
腾讯云 TRTC 实时音视频服务体验,从认识 TRTC 到 TRTC 的开发实践,Demo 演示& IM 服务搭建。
音乐音频分类技术能够基于音乐内容为音乐添加类别标签,在音乐资源的高效组织、检索和推荐等相关方面的研究和应用具有重要意义。传统的音乐分类方法大量使用了人工设计的声学特征,特征的设计需要音乐领域的知识,不同分类任务的特征往往并不通用。深度学习的出现给更好地解决音乐分类问题提供了新的思路,本文对基于深度学习的音乐音频分类方法进行了研究。首先将音乐的音频信号转换成声谱作为统一表示,避免了手工选取特征存在的问题,然后基于一维卷积构建了一种音乐分类模型。
C++知识精讲16 | 井字棋游戏(配资源+视频)【赋源码,双人对战】
本文主要讲解如何在Java中,使用FFmpeg进行视频的帧读取,并最终合并成Gif动态图。
在本篇博文中,我们谈及了 Swift 中 some、any 关键字以及主关联类型(primary associated types)的前世今生,并由浅及深用简明的示例向大家讲解了它们之间的奥秘玄机。