http://inpla.net/thread-8145-1-1.html
Apple Watch -- 作为游戏开发者的你准备好了么? (上)
对于去年(2014年)苹果的产品线来说,最具创新的或许应该算得上Apple Watch了,在此之前各个网站和分析师的猜测就已经漫天飞舞。 关注点无不外乎是在手表和iTV这两个产品上。 2014年9月9日上午10点(北京时间9月10日凌晨1点),苹果2014年秋季新品发布会在总部所在地——加州库比蒂诺当地的Flint表演艺术中心举行,会上苹果CEO宣布发布全新的产品:Apple Watch。 上市时间是2015年预计第一或者第二季度。 紧接着在14年的11月份,苹果在开发者网站上给出了Apple Watch的SDK: WatchKit。 转眼到了2015年, 为了配合Apple Watch相应的iOS测试版已经更新到了8.2, XCode更新到了 6.2 Beta3。 Apple Watch离我们已经越来越近了, 目前的Apple Watch是如何运作的? 上面的程序要如何开发? 对于游戏开发者而言,又能在上面实现哪些和游戏相关的功能? 这里就从软件开发的角度而言,说说Apple Watch的那些事儿。
硬件
切入正题之前,我们先来看看参数。
Apple Watch有两种尺寸:38mm和42mm,对应的分辨率是 272*340, 312*390。 宽高比 4比5。 作为智能手表的初代产品,跑上来就玩两个分辨率会不会让某些完美主义的程序员头痛不已呢...
处理器官方的叫法是 Apple S1,对于这款处理器目前信息还是相当的匮乏,只知道苹果将其描述成SiP(System in Package), 里面集成了运算处理模块,内存模块,存储模块,无线模块,传感模块,IO模块——几乎就是把所有的东西集成到了一起。
软件开发
作为软件开发者,我们最关心的自然还是开发环境和例子程序以便快速了解Apple Watch的特性。 下面我们就通过一个例子一步一步的来说明。
目前可以在Apple Watch上运行的内容可以分成三个类型: Glances, Actionable Notifications和Watch App
先来说说简单的Glance和Actionable Notifications
Glance简单的说就是只读信息,说它是只读,是因为你不能和Glance有任何的交互,各位在学校里都有梦想的女神不? 可远观而不可亵玩焉,这个就是Glance,给了你信息就好了,没有其他。
Actionable Notifications 相比Glance来说多了一些很简单的互动, 比如HomeKit通知你说家里的灯忘记关了,附带一个选项:关灯。 好比女神有天突然跑过来叫你, 某某某,帮我把这堆厚重的教材拿到教室发给同学们好吗?心里那个美啊.....,然后屁颠屁颠的就去了。
最后是WatchKit App
WatchKit Apps就不一样了,既然叫App,那自然可以做很多事情:显示文字,展现图片,可以有列表选择,有按钮输入反馈等等。终于追到了心目中的女神能不开心嘛,想尽办法一起互动啊, 逛街,买东西,吃饭,看通宵电影(你懂的....)
Glances和Actionable Notifications比较简单,这里就不说了。
下面的例子着重介绍如何建立WatchKit Apps。
如果Xcode还没有升级,请到开发者网站上下载最新的Beta版本,我这里使用的是Xcode 6.2 Beta3,里面有iOS 8.2 Beta SDK和相应的模拟器。
创建一个新的项目,选择iOS Application, 至于 template无所谓, 这里选择的是Game
项目工程出现,然后在菜单中选择File→New→Target
就可以看到WatchKit App选项了
添加完成之后我们可以看到工程里多了两个项目:WatchKit Extension和WatchKit App。 等等...iOS App和WatchKit App: 你和你的女神一对甜蜜的恋人间为啥会有一个第三者? Extension是何许人也?
为了弄清里面的关系,我们需要着重说明下WatchKit的结构体系。
在增加了Watch Apps的Target之后,这个Extension就会包含在iOS App里面,用户下载安装iOS App的时候,会一并把Extension安装到iPhone里面。 而Extension中又包含了WatchKit App, 这部分内容会被自动安装到Apple Watch中。 也就是说一个我们发布的程序最后兵分两路,部分驻留在iPhone上,而只负责显示和输入的部分放在了Apple Watch上。
当程序运行起来以后,WatchKit App大部分时间是和Extension进行交互。他们之间的交互,是要通过WatchKit进行的,我们的工作,其中很重要的一部分就是如何使用WatchKit让他们之间好好沟通,达到互动的目的。
注意上面两张图片,在iOS App和WatchKit Extension中,可以看到除了Resources之外,还有Code,但是在WatchKit App中却没有!这说明什么?这说明Watch App没有运算能力, 也就是说它本身并不能处理任何事物,做任何决定,这些事情都是Extension代之完成的。 他是不过是Extension的傀儡而已。 好不容易追到女神了,却发现她及其没有主见,啥都要听她爸的, 逛街她爸要跟着去, 买东西吃饭她爸要陪着, 连看场电影她爸也要坐中间把你们隔开。 想死的心都有了吧。
回到例子, 为了让iOS App, Extension, Watch App能够相互正常通信,工程里还需要进行一些额外的设置。
首先:在Target中,先选择iOS App,在General中的Team选项中选择现有的组(非None选项)。
对Extension和WatchKit App也重复上面的步骤
其次:在Capabilities中,打开App Groups选项,增加一个Group并选中
对于Extension也做同样的操作并选中刚才创建的那个Group。如果在此过程中出现红色感叹号就点击“Fix issue”,直到全部正确为止。
完成了设置之后我们就可以设计WatchKit App的界面了, 找到Interface.storyborad,加入需要的元素
我们在这里放上一个Label,一个Image和一个Button。 给这些控件添加对应的IBOutlet变量和IBAction函数
下面的代码出现在Extension项目的InterfaceController.h中
- // InterfaceController.h
- // WatchKitTutorial WatchKit Extension
- //
- // Created by Bowie Xu on 15/1/14.
- // Copyright (c) 2015年 CoconutIsland. All rights reserved.
- //
- #import <WatchKit/WatchKit.h>
- #import <Foundation/Foundation.h>
- @interface InterfaceController : WKInterfaceController
- @property (weak, nonatomic) IBOutlet WKInterfaceImage *watchImage;
- @property (weak, nonatomic) IBOutlet WKInterfaceButton *watchButton;
- @property (strong, nonatomic) NSArray* buttonTitles;
- @property (assign, nonatomic) int titleIndex;
- - (IBAction)WatchButtonClicked;
- @end
大家注意到了没有, 这的Image和Button并不是UIImage和UIButton,而是以WKInterface开头的类。 没错,这个就是WatchKit类库了,其中开头的WK就是WatchKit的缩写。
搭好空的框架后我们就可以编译启动看看结果了,点选Watchkit App,目标选iphone5还是iphone6都行,然后运行!!
如果只看到了iPhone模拟器而没看到Watch的,请切换到模拟器程序,在其菜单中检查Hardware是否选中了扩展显示设备(话说我初次玩WatchKit在这里卡了很久,总以为自己项目不对,苹果你就不能自动检测项目然后自动弹出Apple Watch模拟器么)
最后结果如下:
如果你也能看到这个结果,恭喜你,已经有一个可以运行的WatchKit App程序了。后面我们添加代码,让其响应按钮,显示图片。InterfaceController.m中添加代码如下:
- // InterfaceController.m
- // WatchKitTutorial WatchKit Extension
- //
- // Created by Bowie Xu on 15/1/14.
- // Copyright (c) 2015年 CoconutIsland. All rights reserved.
- //
- #import "InterfaceController.h"
- @interface InterfaceController()
- @end
- @implementation InterfaceController
- - (void)awakeWithContext:(id)context {
- [super awakeWithContext:context];
- _buttonTitles = [NSArray arrayWithObjects:@"From Watch App", @"From Extension", @"From iOS App",nil];
- // Configure interface objects here.
- _titleIndex = 0;
- [self SetButtonTitle:_titleIndex];
- }
- - (void)willActivate {
- // This method is called when watch view controller is about to be visible to user
- [super willActivate];
- }
- - (void)didDeactivate {
- // This method is called when watch view controller is no longer visible
- [super didDeactivate];
- }
- - (void) ShowPicFromWatchKitApp
- {
-
- }
- - (void) ShowPicFromExtension
- {
-
- }
- - (void) ShowPicFromiOSApp
- {
-
- }
- - (void) SetButtonTitle:(int)titleIndex
- {
- [_watchButton setTitle:[_buttonTitles objectAtIndex:titleIndex]];
- }
- - (void) IncreaseIndex
- {
- _titleIndex++;
- _titleIndex %= [_buttonTitles count];
- }
- - (IBAction)WatchButtonClicked {
- switch (_titleIndex) {
- case 0:
- [self ShowPicFromWatchKitApp];
- break;
- case 1:
- [self ShowPicFromExtension];
- break;
- case 2:
- [self ShowPicFromiOSApp];
- break;
- default:
- break;
- }
-
- [self IncreaseIndex];
- [self SetButtonTitle:_titleIndex];
- }
- @end
复制代码
然后运行程序,在模拟器中点击按钮,可以看到它的title在From WatchApp, From Extension, From iOS App之间切换。到这里你也许会问,我在手表上点击按钮,但是代码却是在Extension中,也就是在iPhone中执行。这之间是怎么联系的呢?答案是:你无需知道! 这中间的一切,全部由WatchKit后台处理的,是不是觉得很爽。
按键搞定,最后我们来看看图片的显示,WatchKit中的图片类可以显示单张静态图片,也可以显示序列帧动画。 根据图片资源所在位置不同,我们有三种加载图片的方法。 分别对应资源在Watch App中,在Extension和在iOS App中。
首先在工程的三个项目中添加图片资源,图片大家随意,在这个例子中,Watch App和Extension我放一张图片,而iOS App中我借用苹果官方的例子,放入360张图片做动画。
先来看看最容易处理的情况:图片资源在WatchKit App中。 对于这种情况, Extension和WatchApp之间只需要传输很少量的数据:传输的是图片资源的名字而已。 在WatchApp中的WatchKit收到这个名字后会在包中寻找同名文件并自动加载显示。 因此相应的代码如下:
- - (void) ShowPicFromWatchKitApp
- {
- [_watchImage setImageNamed:@"AppleImage.png"];
- }
其中的setImageNamed就是直接显示图片了。
如果图片在Extension中,那流程会稍微复杂些,一行代码变成了两行:
- - (void) ShowPicFromExtension
- {
- UIImage* image = [UIImage imageNamed:@"AppleRainbow.png"];
- [_watchImage setImage:image];
- }
复制代码
代码里出现了UIImage,说明一开始图片的加载和WatchKit是无关的,也就是说是Extension在iPhone运行的结果。然后再设置到WatchKit App中。
相比前一种方式,这里传输的不是文件名而是Extension中的整个图片资源了。
是不是觉得也还好,代码不复杂吧。 最后我们来个大跃进,看看如何从iOS App中拿到一系列的图片资源并在WatchKit App中播放动画。
要和iOS App通讯,必须调用
- + (BOOL)openParentApplication:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo, NSError *error)) reply; // launches containing iOS application on the phone. userInfo must be non-nil
复制代码
函数, 由Extension发起请求。 注意:这里还是Extension而不是WatchKit App,前面说过WatchKit App是没有任何运算能力的。 在iOS App的AppDelegate中添加
- - (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply NS_AVAILABLE_IOS(8_2);
进行响应。
牵扯到三方,中间的沟通自然多了不少,因此这个也是最耗时的方法。
Extension中的请求代码
- -(void) _RequestData:(NSNumber*) nindex {
- //NSData* indexdata = [NSData dataWithBytes:&_AnimationIndex length:sizeof(_AnimationIndex)];
- //NSDictionary *request = [NSDictionary dictionaryWithObject:indexdata forKey:@"request"];
- int index = [nindex intValue];
- if (index>=MAXKEYS || index<0) {
- return;
- }
-
- NSDictionary *request = @{@"request":@"PIC"};
- NSString* key = [NSString stringWithFormat:@"%d", index];
-
- [_AnimationPics removeObjectForKey:key];
-
- [InterfaceController openParentApplication:request reply:^(NSDictionary *replyInfo, NSError *error) {
-
- if (error) {
- NSLog(@"%@", error);
- [_AnimationPics removeObjectForKey:key];
- } else {
- NSData* data = [replyInfo objectForKey:@"PIC"];
- NSArray* picarray = [NSKeyedUnarchiver unarchiveObjectWithData:data];
- [_AnimationPics setObject:picarray forKey:key];
-
- //start animation
- if (_AnimationTimer == nil) {
- _AnimationTimer = [NSTimer scheduledTimerWithTimeInterval:0.2f target:self selector:@selector(SetAnimationIndex) userInfo:nil repeats:YES];
- }
-
- }
- }];
-
- }
- iOS App中的响应代码
- #define PICCOUNT 30
- - (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply {
-
- if ([[userInfo objectForKey:@"request"] isEqualToString:@"PIC"]) {
-
- NSLog(@"containing app received message from watch");
-
-
- NSMutableArray* marray = [NSMutableArray arrayWithCapacity:PICCOUNT];
- for (int i=0; i<PICCOUNT; i++) {
- UIImage* image = [UIImage imageNamed:[NSString stringWithFormat:@"glance-%[email protected]", i+_picindex]];
- NSData* imagedata = UIImagePNGRepresentation(image);
- [marray addObject:imagedata];
- }
-
- _picindex += PICCOUNT;
- _picindex %= 360;
-
- NSData* imageData = [NSKeyedArchiver archivedDataWithRootObject:marray];
-
- //NSData* imageData = UIImageJPEGRepresentation(image, 1.0f);
- NSDictionary *response = [NSDictionary dictionaryWithObject:imageData forKey:@"PIC"];//@{@"response" : @"Watchkit"};
-
- reply(response);
- }
- }
不出所料,让iOS App传递祯序列是很耗时间的操作,在例子程序中我尝试使用了Double Buffer,但是效果也不明显。
最后程序运行起来是这个样子滴....
前面说了这么多,又是代码又是图片的, 有人要问了,为啥苹果设计一个这么蛋疼的Extenion存在? 为啥不让Watch App 和iOS App直接通讯? 我的理解是Extension是为了处理轻事物而存在的,这样iOS可以在后台执行Extension而不会消耗太多的资源。 也正是因为如此,Extension被设计成只能在后台运行很短的一段时间,如果应用程序需要诸如定位之类的长时间的运算,苹果的官方建议是交给iOS App来完成。
到这里Apple Watch和WatchKit的介绍就告一段落了。 下篇我们着重探讨WatchKit在游戏开发中的运用。
最后给出一张 WatchKit和UIKit的参照对比表格:
|
|
|
|
WKUserNotificationInterfaceController
|
UIApplicationDelegate + UIAlertController
|
|
|
|
|
|
|
|
UILabel + NSDateFormatter
|
|
|
|
|
|
|
|
|
|
UITableView.separatorColor / .separatorStyle
|
|
|
|
|
|
|
|
UILabel + NSDateFormatter + NSTimer
|
本文的参考连接:
1:苹果开发者网站: https://developer.apple.com/watchkit/
2:WatchKit-NSHipster: http://nshipster.com/watchkit/
文中的例子程序源码:
https://github.com/CoconutIslandStudio/WatchKitTutorial.git