如何将你的Sprite Kit游戏从ios移植到Mac OS X平台


原文地址:http://www.raywenderlich.com/70837/how-to-port-your-sprite-kit-game-from-ios-to-os-x
泰然翻译组:阳光新鲜。校对:glory。

Mou icon

你可能想过将自己的Sprite Kit游戏移植到Mac OS X平台?事实上,这比你想像的还简单。

苹果开发Sprite Kit的目的是让ios和Mac OS X开发尽可能的一样,甚至你可以使用相同的工程环境同时完成两个平台的开发。

这篇引导将告诉你怎样获取一个ios Sprite Kit项目-Sprite Kit Tutorial for Beginners-然后将他修改成Mac OS X版本。

你将学习到如何在同一个XCode工程中维护两个版本并且学习如何在不拷贝一大堆代码的情况下保证两个版本同步。

如果你选择完成这个教程,那么你最好有一定的Mac OS X基础。尽管如此,Sprite Kit的一个优美的特性就是你不需要有比较丰富的经验就可以做出一个漂亮的App。

如果想学习更多的关于Mac开发的知识,请查看这里three part tutorial on making a simple Mac app

让我们开始吧

这里下载我们教程需要的原始工程,并在你的iPhone上调试运行。

这个实例工程和原始的有一些不同,这是为了将ios和Mac OS X版的游戏区分开来。

水平仪支持

Space Game Starter Kit项目使用了水平仪,这样可以让玩家可以通过倾斜设备来上下移动精灵,像这样:

Mou icon

更具毁灭性的力量

忍者星正在遭受魔法忍者力量史诗般的毁灭性攻击!

Mou icon

好吧,也许不是史诗般的,但是这个版本使用了非常不错的粒子效果以给忍者一些视觉冲击。

一些技术上的改进

Vicki Wnderlich 发挥自己的才能给游戏添加了新的背景以使游戏看起来更具美感。当然,状态栏是隐藏的,并且游戏支持iPad的分辨率。

使用项目和目标开发

一个项目文件里包含你的工作用到的所有文件,其中 target 决定了你的工程该如何编译。

一个项目可以包含多个目标。这种方式可以让你用多种方式编译你的项目,选择目标和哪些特定的文件关联,和一些特殊的目标设置。这些也许能给你一些关于将这个项目用Mac OS X编译的启示。

让我们看一下项目的目标。打开 SpriteKitSimpleGame 项目,在界面的左上角,选择你的项目打开项目设置。然后在项目窗口中选择 SpriteKitSimpleGame

好,现在这里有两个目标,一个是ios另一个是ios单元测试,如下:

Mou icon

二选一,在项目和目标的列表都展开后你可以选择 SpriteKitSimpleGame

Mou icon

当你构建应用时,你的target必须支持当前设备。查看当前设备是否被目标支持,点击左上角停止按钮右边的 SpriteKitSimpleGame 下拉按钮,显示如下:

Mou icon

这里也是你要创建Mac OS X目标的地方。

创建一个MacOSX目标

确保你的Xcode窗口保持激活状态,按下图的方式选择 File/New/Target

Mou icon

Xcode提示你为目标选择一个模板。

选择 OS X\Application\SpriteKit Game 模板然后点下一步,如下所示:

Mou icon

最后在 product name 中输入 SpriteKitSimpleGameMac 然后点击完成,像这样:

Mou icon

注意:如果你打算将应用上传到苹果商店,你的 bundleidentifier 必须在 developer.apple.com 分别注册。Profiles和provisioning文件分别用于ios和Mac OS X开发中。

尝试在Mac OS X上运行你的app,在列表中选择 SpriteKitSimpleGameMac,然后选择 My Mac 64-bit,如下:

Mou icon

你认为当你使用新的目标运行你的项目时会发生什么?

  • A - 游戏完美运行 - 教程到此结束!
  • B - 编译中发生数个错误。
  • C - 游戏可以编译,但运行时跳出。
  • D - 游戏可以运行,但运行结果不是你预期的。

如果你猜D那么你猜对了,就像一个新工程运行的很好但是使用的是标准模板。

Mou icon

不用着急,这个工程马上就回变为令人惊讶的忍者游戏,但首先你需要清空工程已让我们能更方便的开始工作。

将你的文件整理成多个目标

现在,你有了一个Mac OS X目标,你将对他进行修改已使你的app能运行在osx下。随着时间的推移,跟踪这些文件将变得越来越困难,所以最好先设置系统以更好的组织所有文件。

最小化你所有的组然后新建一个叫 SharedResources 的组,如下所示:

Mou icon

这个组将包含可以让Mac OS X和ios目标公用的资源。

好了,现在创建一个叫 Testing 的组,然后将单元测试项目移动到里面,像这样:

Mou icon

将用于单元测试的项目移入特定的文件夹可以避免看起来杂乱。

现在你在项目里为文件组织了一个新的结构,你现在要做的是移动文件到指定的位置。

展开 SharedResources 和 SpriteKitSimpleGame 文件夹。从 SpriteKitSimpleGame 拖动 Particles 和 Sounds组到 SharedResources.

下一步,将 sprites.atlas folder, MyScene.h, MyScene.m, GameOverScene.h and GameOverScene.m 文件也拖动过来。你的文件夹结构看上去应该如下所示:

Mou icon

删除 Spaceship.png 文件 - 你将不再需要他了。这只是你在创建 sprite Kit 模板时自动生成的样板文件。

所有共享的资源都已经放到 SharedResources 组了。所有留在 SpriteKitSimpleGame 的文件都与 ios 游戏的启动与管理有关。

展开 SpriteKitSimpleGameMac 组。在开始下一步之前,你需要删除这个文件夹的实例文件。

从 SpriteKitSimpleGameMac 中删除 MyScene.h, MyScene.m and Spaceship.png 文件,选择移动到垃圾箱。然后你的文件列表应该像这样:

Mou icon

注意:你的Mac目标的组中不能含有ViewController类;取代它的应该是AppDelegate文件。 UIViewController 类是 UIKit 的一部分,他在Mac os x上是不起作用的。取代他的是AppDelegate,他将创建一个NSWindow实例以呈现Sprite Kit场景。

最后检查一下,你的完整的展开列表应该和下面的一样:

Mou icon

你已经删除了所有不必要的项目,并且重新组织了一下。现在,是时候修改目标编译时所指向的文件了。

添加目标成员

展开 FrameWorks 组选择 UIKit.framework,像这样:

Mou icon

展开右边的 Utilities Panel 选择 File Inspector,像这样:

Mou icon

在File Inspector的下半部分你会看到Target Membership区域。这里显示的是你的目标将要显示的文件。

UIKit 只是用在ios平台上,所以将Mac OS X的目标去掉,如下所示:

Mou icon

Cocoa Framework 只能在mac os x上使用,所以确保他在mac目标上被选中,在ios目标上被取消:

Mou icon

Sprite Kit Framework 在ios和mac上都可以使用,所以像这样设置:

Mou icon

每个文件都有他自己的Membership除了atlases纹理文件以外。Atlas文件是你唯一需要自己设置Membership的文件,其他的包括纹理都是自动设置的。

尽管如此,类在这方面有一些特别。你不能给.h文件设置Membership,但你必须给.m文件设置Membership。

带着你的新发现加深对Target Membership的理解,完成你的SharedResources组,确保SpriteKitSimpleGame 和 SpriteKitSimpleGameMac所有的文件都设置完成。总体上来讲,你需要8次设置。

下一步,完成SpriteKitSimpleGame组的文件,并确保SpriteKitSimpleGame的文件和两个目标都有关联 - 他们应该已经被设置好了,但是这里最好再设置一下。

最后,完成SpriteKitSimpleGameMac组,确保SpriteKitSimpleGameMac组的文件全都标记过。同样,你什么改变都不需要左,但是检查一下也无妨。

现在你的工程已经正确设置了ios和mac的目标,你接下来终于可以做你最擅长的事情 - 写程序!

构建与运行游戏

基于之前的工作,你的项目应该构建并且在ios上正确运行。之前做的修改应该不对实际的游戏效果产生影响。尽管去次,如果你编译OS的目标时,你将看到一连串的错误。这是因为你为对ios与osx代码之间差异给予说明。

构建和运行你的项目使用SpriteKitSimpleGameMac目标;你将会看到什么?

你将会遇到Module ‘CoreMotion’ not found的错误。Mac OS X没有CoreMotion和与他等价的类;你将围绕这个错误并且使用键盘控制主角移动。尽管如此,你的初级目标就是使项目能够构建,具体的实现细节放在后面。

但是我们如何修复它呢?你不能直视移除CoreMotion,否则ios版本就不能用了。你不能使用if语句解决,因为编译器会检查每一行代码并对他不认识的东西抛出错误。

打开MyScene.m然后替换:

@import CoreMotion;

替换成下面的代码:

#if TARGET_OS_IPHONE
    @import CoreMotion;
#endif

不同于if语句的规则,#if是在预处理中执行。TARGET_OS_IPHONE如果是ios平台则返回true。

注意:如果你计划使用#if检查当前的平台是否为Mac,如果是就执行一系列方法,那么你应该使用TARGET_OS_IPHONE。
TARGET_OS_MAC看起来也可以 - 但问题是他在ios下也返回true。
这看起来很糟糕,但苹果在他们的实例中使用!TARGET_OS_IPHONE来表示包含多平台的目标,所以这是一个小故障,看起来他们并不想修改。

现在你需要找到和CoreMotion有关的代码,并给他加上#if。

在MyScene.m中的实例变量中找下列代码:

CMMotionManager *_motionManager;

然后将他替换成:

#if TARGET_OS_IPHONE
    CMMotionManager *_motionManager;
#endif

滚动到int方法,寻找下列代码:

_motionManager = [[CMMotionManager alloc] init];
_motionManager.accelerometerUpdateInterval = 0.05;
[_motionManager startAccelerometerUpdates];

替换成如下代码:

#if TARGET_OS_IPHONE
    _motionManager = [[CMMotionManager alloc] init];
    _motionManager.accelerometerUpdateInterval = 0.05;
    [_motionManager startAccelerometerUpdates];
#endif

现在,找到如下代码

[self updatePlayerWithTimeSinceLastUpdate:timeSinceLast];

替换成如下代码:

#if TARGET_OS_IPHONE
    [self updatePlayerWithTimeSinceLastUpdate:timeSinceLast];
#endif

最后,当然这并不是最不重要的。找到updatePlayerWithTimeSinceLastUpdate方法:用下面的代码包裹整个方法:

#if TARGET_OS_IPHONE
    - (void)updatePlayerWithTimeSinceLastUpdate:> (CFTimeInterval)timeSinceLast 
    .
    .
    .
    }
#endif

如果你使用ios目标编译,所有的#if将返回TRUE,所以app会像以前一样编译。相反的,如果使用mac os x目标进行编译,所有的#if将返回FALSE,所有的程序块将不会被编译。

仔细查看touchesEnded:withEvents方法在MyScene.m。Mac版本不支持触屏,所以这个方法已经失效了。Mac版将使用鼠标作为屏幕触控的完美替代品。

为了避免给代码增加一个新分支,你将创建一个继承自SKScene类以帮助用来控制屏幕触控和鼠标点击!

添加事件操作

选择你的SharedResources组。

在菜单栏上选择File \ New \ File…,如下所示:

Mou icon

选择Objective-C Class不管你是ios还是os策略,点下一步。

Mou icon

命名为SKMScene继承于SKScene。

Mou icon

将文件直接置于工程文件夹下,确保ios和mac目标都被选中。

Mou icon

打开SKMScene.h替换代码:

@import SpriteKit;

@interface SKMScene : SKScene

//Screen Interactions
-(void)screenInteractionStartedAtLocation:(CGPoint)location;
-(void)screenInteractionEndedAtLocation:(CGPoint)location;

@end

你将用SKMScene重写上面的两个方法。

将下面的代码直接加在SKMScene.m文件的@implementation SKMScene:行下:

#if TARGET_OS_IPHONE
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  UITouch *touch = [touches anyObject];
  CGPoint positionInScene = [touch locationInNode:self];
  [self screenInteractionStartedAtLocation:positionInScene];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
  UITouch *touch = [touches anyObject];
  CGPoint positionInScene = [touch locationInNode:self];
  [self screenInteractionEndedAtLocation:positionInScene];
}

- (void)touchesCancelled:(NSSet *)touches
   withEvent:(UIEvent *)event
{
  UITouch *touch = [touches anyObject];
  CGPoint positionInScene = [touch locationInNode:self];
  [self screenInteractionEndedAtLocation:positionInScene];
}
#else
-(void)mouseDown:(NSEvent *)theEvent {
  CGPoint positionInScene = [theEvent locationInNode:self];
  [self screenInteractionStartedAtLocation:positionInScene];
}

- (void)mouseUp:(NSEvent *)theEvent
{
  CGPoint positionInScene = [theEvent locationInNode:self];
  [self screenInteractionEndedAtLocation:positionInScene];
}

- (void)mouseExited:(NSEvent *)theEvent
{
  CGPoint positionInScene = [theEvent locationInNode:self];
  [self screenInteractionEndedAtLocation:positionInScene];
}
#endif

-(void)screenInteractionStartedAtLocation:(CGPoint)location {
  /* Overridden by Subclass */
}

-(void)screenInteractionEndedAtLocation:(CGPoint)location {
  /* Overridden by Subclass */
}

这确实是一串很长的代码,但如果你从头到尾读下来,还是能找到其中的道理的。触摸屏幕和返回的方法在TARGET_OS_IPHONE程序块里。然后你创建一个包含位置信息的CGPoint触摸点并且调用screenInteraction相关的方法。

点击或释放鼠标将会调用#else处的方法。和上面一样,你创建一个包含点击信息的CGPoint并且调用和screenInteraction有关的方法。

使用这个子类的好处是触摸和点击都调用screenInteraction方法。screenInteraction方法没有代码,所以你需要在你的子类中重写。

打开MyScene.h并且在#import下添加如下类的声明:

#import "SKMScene.h"

然后修改父类为如下内容:

@interface MyScene : SKMScene

这确保你的游戏场景继承于SKMScene子类。你可以替代你子类中的触摸事件。

在MyScene.m寻找如下行:

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {

用下面的代码替换他:

-(void)screenInteractionEndedAtLocation:(CGPoint)location {

接下了,删除以下几行你已经不需要的方法:

UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];

使用Mac目标构建和运行你的项目;他应该可以编译通过,并且没有错误:

Mou icon

恭喜你 - 你已经成功的在Mac上运行了你的Sprite Kit game!你得注意这里有一些bug:

  1. 在一些苹果设备上,首次点击游戏会有短暂的卡顿。
  2. 在一些苹果上粒子效果可能显示不正常。
  3. 有些屏幕的尺寸可能有问题。
  4. 背景音乐没声音。

接下来我将带您改正所有的这些bug - 你在这个过程中将学到一些在做跨平台应用时经常遇到的问题。

纠正预加载错误

这个bug不会在所有的系统上出现,但一旦出现就会有性能问题。“First-time-through”通常源于资源加载。

Texture atlases是经常被使用的资源,但是我们的应用不包换动画和大型混合图片,所以问题出在别的地方。

音效更有可能出问题,当用户点击屏幕时,音效还没有加载。

修复这个问题,添加如下变量在 MyScene.m:

SKAction *_playPewPew;

下一步,在initWithSize的if语句中添加如下代码:

_playPewPew = [SKAction playSoundFileNamed:@"pew-pew-lei.caf" waitForCompletion:NO];

这个修改后你的应用在场景初始化时预加载声音。

在screenInteractionEndedAtLocation寻找下列代码:

[self runAction:[SKAction playSoundFileNamed:@"pew-pew-lei.caf" waitForCompletion:NO]];

替换成:

[self runAction:_playPewPew];

运行应用;点击鼠标确保延迟消失了。

如果你的系统没有表现出这个问题,那么你也应该修改一下防止在其他系统上运行时发生。

纠正SKS错误。

Mou icon

在写这篇文档时我发现在xcode5上运行,粒子效果会出现问题。你需要在Sks文件中重写引用的纹理文件。

技术上,你的Sks文件没有发生任何错误 - 你不会在所有的系统中体验到这种错误 - 但你需要尽量避免他出现。

在MyScene.m的projectile:dideCollideWithMonster中寻找下面的代码:

SKEmitterNode *emitter = [NSKeyedUnarchiver unarchiveObjectWithFile:[[NSBundle mainBundle] pathForResource:@"SmallExplosion" ofType:@"sks"]];

将下面的代码直接添加到你找的的代码下面:

emitter.particleTexture = [SKTexture textureWithImageNamed:@"spark"];

你之前所做的事情是为了告诉XCode哪里寻找粒子纹理。

创建你的应用;现在你可以欣赏你史诗般无障碍的粒子效果。

修复图片大小错误

打开SpriteKitSimpleGameMac组然后选择AppDelegate.m。查看你在applicationDidFinishLaunching中设置的尺寸。

1024 * 768 - 这是非视网膜屏的ipad分辨率。

现在查看一下sprites.atlas的内容。不出所料,所有的ipad版本图片都有~ipad后缀,这样你的应用在ipad上运行时就知道该使用哪种图片。

不幸的事,这里没有~mac后缀;取代他的是,你需要创建一个为mac使用的texture atlas纹理。

为了保证你的工程尽量的小,你应该使你的应用的分辨率尽可能的小。

右键sprites.atla选择Show in Finder。

复制sprites.atlas然后删除所有不带~ipad后缀的文件。

Mou icon

下一步,删除~ipad后缀文件,但是留下@2x后缀文件。

注意:必须留下@2x文件以支持配备视网膜屏的Macbook Pro。

重命名文件夹为spritesMac.atlas然后拖动重命名的文件夹到你的工程。

在Choose options for adding these files对话框中,确保只有SpriteKitSimpleGameMac目标被选中,如下所示:

Mou icon

点击完成。现在文件夹已经被导入了,选择sprites.atlas,在Membership关闭mac的目标。这可以保证每一个texture atlas都和其他的区分开。

让纹理文件夹的组织和原则一致,移动ios纹理到ios组mac纹理到mac组,如下:

Mou icon

下一步,选择Project\Clean。这将删除所有的旧文件从你的构建目录(如果你忘了做这一步,sprites.atlas会仍然存在)。

运行你的应用;你应该看到所有的纹理都按适当的尺寸载入,如下:

Mou icon

现在你的应用已经支持iPhone, iPad 和 Mac OS X的系统分辨率 - 也兼容视网膜屏。

修复音轨问题

最后,你需要处理音轨的问题。

查看SpriteKitSimpleGame组的ViewController.m文件。viewWillLayoutSubviews有一小部分代码是AVAudioPlayer的实例,设置他永远循环。

NSError *error;
NSURL *backgroundMusicURL = [[NSBundle mainBundle] URLForResource:@"background-music-aac" withExtension:@"caf"];
self.backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error];
self.backgroundMusicPlayer.numberOfLoops = -1;
[self.backgroundMusicPlayer prepareToPlay];
[self.backgroundMusicPlayer play];

啊哈-你的mac工程里没有ViewController。因此,你需要在AppDelegate调用这些代码来替代。

在SpriteKitSimpleGameMac组的AppDelegate.m文件中寻找如下代码:

@implementation AppDelegate

替换:

@import AVFoundation;

@implementation AppDelegate {
    AVAudioPlayer *backgroundMusicPlayer;
}

下一步,在applicationDidFinishLaunching顶部添加如下代码:

NSError *error;
NSURL * backgroundMusicURL = [[NSBundle mainBundle] URLForResource:@"background-music-aac" withExtension:@"caf"];
backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error];
backgroundMusicPlayer.numberOfLoops = -1;
[backgroundMusicPlayer prepareToPlay];
[backgroundMusicPlayer play];

构建应用;音乐开始播放了!

你已经解决了mac版本上所有的bug,但你还有一个游戏控制的问题没有解决。

使用键盘

ios版的忍者移动依靠的是倾斜设备。这种工作是依靠CoreMotion类处理的,游戏的主循环调用updatePlayerWithTimeSinceLastUpdate:以计算主角当前帧的位置。

我们现在需要一个不同的方法来监听键盘事件。

将下面代码添加到MyScene.m文件的updatePlayerWithTimeSinceLastUpdate:方法的#endif语句之前:

#else
-(void)keyDown:(NSEvent *)theEvent {


}

这个方法用于响应键盘操作。注意有个keyUp方法响应键盘的抬起事件,事件是键盘从按下到抬起的时间。

你不想处理响应所有的键盘事件;你可以在NSEvent中找到你需要处理的按键。

在keyDown的花括号中添加如下代码:

-(void)keyDown:(NSEvent *)theEvent {
  NSString *keyPressed = [theEvent charactersIgnoringModifiers];
if ([keyPressed length] == 1) {
      NSLog(@"Key: %c",[keyPressed characterAtIndex:0]);
  }
}

这里你可以提取你按下的字母,没有任何修饰键。这意味着组合键如Command + S将被忽略.同样的,你只检查按下的一个字母,这样可以过滤掉你不想要的字符事件。你将吧按压事件输出到控制台。

运行工程;你输入的按键将输出到debug区域,如下:

Mou icon

你可以使用上下键来移动你的精灵,试着按一些键看控制台输出什么:

Mou icon

恩,这看起来有点糟糕。方向键是功能键的一部分,所以他没有合适的字符可以代表。但这没关系:我这里有一个简单的方法可以检测功能键的按压。

NSEvent可以帮助你很好的管理mac上鼠标和键盘的操作。这个引导教程只介绍了NSEvent的一部分;强烈推荐你仔细查看NSEvent类的参考手册

现在,快速浏览一下NSEvent文档中关于按键时间枚举的部分。按键和NSUpArrowFunctionKey与NSDownArrowFunctionKey有关。

返回MyScene.m找到keyDown:添加代码。

注释掉NSLog语句并且立即粘贴如下代码:

unichar charPressed = [keyPressed characterAtIndex:0];
switch (charPressed) {
    case NSUpArrowFunctionKey:
        [_player runAction:[SKAction moveByX:0.0f y:50.0f duration:0.3]];
        break;
        case NSDownArrowFunctionKey:
        [_player runAction:[SKAction moveByX:0.0f y:-50.0f duration:0.3]];
        break;
    default:
        break;
}

在这里你将按Unicode的方式比较上下按键。然后使用SKAction上下移动角色。

运行工程;按上下键你将看到角色上下移动如下图:

Mou icon

你需要花更多的时间修改mac版游戏,但你需要确保你没有影响到ios版本部分的运行效果!

运行你的ios目标并且充分的玩每一个部分确保你之前的更改没有影响到ios部分的功能。

未来要做的事?

你可以从在这里得到完整的工程。

现在,你应该已经还好的理解了将ios工程转换成mac/ios工程所需的工作,你一定希望让创建的Sprite Kit游戏一开始就具有跨平台功能。我在github上放了一个Sprite Kit跨平台的模板,这对你一定有用的。

想学习更多的ios和osx游戏的知识(尤其是和场景尺寸,图片大小,横纵比,和UIKit对比Cocoa Touch的坐标系统相关的),看看我们即将出版的
iOS Games by Tutorials,这里你可以找到更多详细的内容。

如果你有一些注释或问题,在下面踊跃发言哦!

标签: sprite kit

?>