Apple Watch -- 作爲遊戲開發者的你準備好了麼? (上)

作者: Bowie    時間: 2015-1-16 16:28
標題: Apple Watch -- 作爲遊戲開發者的你準備好了麼? (上)
本帖最後由 Bowie 於 2015-2-16 11:57 編輯 

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中

  1. <font size="3">//
  2. //  InterfaceController.h
  3. //  WatchKitTutorial WatchKit Extension
  4. //
  5. //  Created by Bowie Xu on 15/1/14.
  6. //  Copyright (c) 2015年 CoconutIsland. All rights reserved.
  7. //

  8. #import <WatchKit/WatchKit.h>
  9. #import <Foundation/Foundation.h>

  10. @interface InterfaceController : WKInterfaceController
  11. @property (weak, nonatomic) IBOutlet WKInterfaceImage *watchImage;
  12. @property (weak, nonatomic) IBOutlet WKInterfaceButton *watchButton;
  13. @property (strong, nonatomic) NSArray*    buttonTitles;
  14. @property (assign, nonatomic) int       titleIndex;
  15. - (IBAction)WatchButtonClicked;


  16. @end
  17. </font>
複製代碼



大家注意到了沒有, 這的Image和Button並不是UIImage和UIButton,而是以WKInterface開頭的類。 沒錯,這個就是WatchKit類庫了,其中開頭的WK就是WatchKit的縮寫。

搭好空的框架後我們就可以編譯啓動看看結果了,點選Watchkit App,目標選iphone5還是iphone6都行,然後運行!!


如果只看到了iPhone模擬器而沒看到Watch的,請切換到模擬器程序,在其菜單中檢查Hardware是否選中了擴展顯示設備(話說我初次玩WatchKit在這裏卡了很久,總以爲自己項目不對,蘋果你就不能自動檢測項目然後自動彈出Apple Watch模擬器麼)


最後結果如下:

如果你也能看到這個結果,恭喜你,已經有一個可以運行的WatchKit App程序了。後面我們添加代碼,讓其響應按鈕,顯示圖片。InterfaceController.m中添加代碼如下:
  1. <font size="3">//
  2. //  InterfaceController.m
  3. //  WatchKitTutorial WatchKit Extension
  4. //
  5. //  Created by Bowie Xu on 15/1/14.
  6. //  Copyright (c) 2015年 CoconutIsland. All rights reserved.
  7. //

  8. #import "InterfaceController.h"


  9. @interface InterfaceController()

  10. @end


  11. @implementation InterfaceController

  12. - (void)awakeWithContext:(id)context {
  13.     [super awakeWithContext:context];
  14.     _buttonTitles = [NSArray arrayWithObjects:@"From Watch App", @"From Extension", @"From iOS App",nil];
  15.     // Configure interface objects here.
  16.     _titleIndex = 0;
  17.     [self SetButtonTitle:_titleIndex];
  18. }

  19. - (void)willActivate {
  20.     // This method is called when watch view controller is about to be visible to user
  21.     [super willActivate];
  22. }

  23. - (void)didDeactivate {
  24.     // This method is called when watch view controller is no longer visible
  25.     [super didDeactivate];
  26. }

  27. - (void) ShowPicFromWatchKitApp
  28. {
  29.    
  30. }

  31. - (void) ShowPicFromExtension
  32. {
  33.    
  34. }

  35. - (void) ShowPicFromiOSApp
  36. {
  37.    
  38. }

  39. - (void) SetButtonTitle:(int)titleIndex
  40. {
  41.     [_watchButton setTitle:[_buttonTitles objectAtIndex:titleIndex]];
  42. }

  43. - (void) IncreaseIndex
  44. {
  45.     _titleIndex++;
  46.     _titleIndex %= [_buttonTitles count];
  47. }


  48. - (IBAction)WatchButtonClicked {
  49.     switch (_titleIndex) {
  50.         case 0:
  51.             [self ShowPicFromWatchKitApp];
  52.             break;
  53.         case 1:
  54.             [self ShowPicFromExtension];
  55.             break;
  56.         case 2:
  57.             [self ShowPicFromiOSApp];
  58.             break;
  59.         default:
  60.             break;
  61.     }
  62.    
  63.     [self IncreaseIndex];
  64.     [self SetButtonTitle:_titleIndex];
  65. }

  66. @end
  67. </font>
複製代碼

然後運行程序,在模擬器中點擊按鈕,可以看到它的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收到這個名字後會在包中尋找同名文件並自動加載顯示。 因此相應的代碼如下:
  1. <font size="3">- (void) ShowPicFromWatchKitApp
  2. {
  3.     [_watchImage setImageNamed:@"AppleImage.png"];
  4. }
  5. </font>
複製代碼

其中的setImageNamed就是直接顯示圖片了。



如果圖片在Extension中,那流程會稍微複雜些,一行代碼變成了兩行:
  1. <font size="3">- (void) ShowPicFromExtension
  2. {
  3.     UIImage* image = [UIImage imageNamed:@"AppleRainbow.png"];
  4.     [_watchImage setImage:image];
  5. }
  6. </font>
複製代碼

代碼裏出現了UIImage,說明一開始圖片的加載和WatchKit是無關的,也就是說是Extension在iPhone運行的結果。然後再設置到WatchKit App中。

相比前一種方式,這裏傳輸的不是文件名而是Extension中的整個圖片資源了。
是不是覺得也還好,代碼不復雜吧。 最後我們來個大躍進,看看如何從iOS App中拿到一系列的圖片資源並在WatchKit App中播放動畫。

要和iOS App通訊,必須調用
  1. <font size="3">+ (BOOL)openParentApplication:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo, NSError *error)) reply;    // launches containing iOS application on the phone. userInfo must be non-nil
  2. </font>
複製代碼

函數, 由Extension發起請求。 注意:這裏還是Extension而不是WatchKit App,前面說過WatchKit App是沒有任何運算能力的。 在iOS App的AppDelegate中添加
  1. <font size="3">- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply NS_AVAILABLE_IOS(8_2);
  2. </font>
複製代碼

進行響應。


牽扯到三方,中間的溝通自然多了不少,因此這個也是最耗時的方法。

Extension中的請求代碼
  1. <font size="3">-(void) _RequestData:(NSNumber*) nindex {
  2.     //NSData* indexdata = [NSData dataWithBytes:&_AnimationIndex length:sizeof(_AnimationIndex)];
  3.     //NSDictionary *request = [NSDictionary dictionaryWithObject:indexdata forKey:@"request"];
  4.     int index = [nindex intValue];
  5.     if (index>=MAXKEYS || index<0) {
  6.         return;
  7.     }
  8.    
  9.     NSDictionary *request = @{@"request":@"PIC"};
  10.     NSString* key = [NSString stringWithFormat:@"%d", index];
  11.    
  12.     [_AnimationPics removeObjectForKey:key];
  13.    
  14.     [InterfaceController openParentApplication:request reply:^(NSDictionary *replyInfo, NSError *error) {
  15.         
  16.         if (error) {
  17.             NSLog(@"%@", error);
  18.             [_AnimationPics removeObjectForKey:key];
  19.         } else {
  20.             NSData* data = [replyInfo objectForKey:@"PIC"];
  21.             NSArray* picarray = [NSKeyedUnarchiver unarchiveObjectWithData:data];
  22.             [_AnimationPics setObject:picarray forKey:key];
  23.             
  24.             //start animation
  25.             if (_AnimationTimer == nil) {
  26.                 _AnimationTimer = [NSTimer scheduledTimerWithTimeInterval:0.2f target:self selector:@selector(SetAnimationIndex) userInfo:nil repeats:YES];
  27.             }
  28.             
  29.         }
  30.     }];
  31.    
  32. }

  33. iOS App中的響應代碼
  34. #define PICCOUNT 30
  35. - (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply {
  36.    
  37.     if ([[userInfo objectForKey:@"request"] isEqualToString:@"PIC"]) {
  38.         
  39.         NSLog(@"containing app received message from watch");
  40.         
  41.         
  42.         NSMutableArray* marray = [NSMutableArray arrayWithCapacity:PICCOUNT];
  43.         for (int i=0; i<PICCOUNT; i++) {
  44.             UIImage* image = [UIImage imageNamed:[NSString stringWithFormat:@"glance-%[email protected]", i+_picindex]];
  45.             NSData* imagedata = UIImagePNGRepresentation(image);
  46.             [marray addObject:imagedata];
  47.         }
  48.         
  49.         _picindex += PICCOUNT;
  50.         _picindex %= 360;
  51.         
  52.         NSData* imageData = [NSKeyedArchiver archivedDataWithRootObject:marray];
  53.         
  54.         //NSData* imageData = UIImageJPEGRepresentation(image, 1.0f);
  55.         NSDictionary *response = [NSDictionary dictionaryWithObject:imageData forKey:@"PIC"];//@{@"response" : @"Watchkit"};
  56.         
  57.         reply(response);
  58.     }
  59. }
  60. </font>
複製代碼

不出所料,讓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/

文中的例子程序源碼:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章