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的參照對比表格:
WatchKit | UIKit |
WKInterfaceController | UIViewController |
WKUserNotificationInterfaceController | UIApplicationDelegate + UIAlertController |
WKInterfaceDevice | UIDevice |
WKInterfaceObject | UIView |
WKInterfaceButton | UIButton |
WKInterfaceDate | UILabel + NSDateFormatter |
WKInterfaceGroup | UIScrollView |
WKInterfaceImage | UIImageView |
WKInterfaceLabel | UILabel |
WKInterfaceMap | MKMapView |
WKInterfaceSeparator | UITableView.separatorColor / .separatorStyle |
WKInterfaceSlider | UIStepper + UISlider |
WKInterfaceSwitch | UISwitch |
WKInterfaceTable | UITableView |
WKInterfaceTimer | UILabel + NSDateFormatter + NSTimer |
本文的參考連接配接:
1:蘋果開發者網站: https://developer.apple.com/watchkit/ 2:WatchKit-NSHipster: http://nshipster.com/watchkit/
文中的例子程式源碼: https://github.com/CoconutIslandStudio/WatchKitTutorial.git