⌚watchOS 2で
ゲーム作ってみた話
iOS 9 週連続 Bootcamp! vol5
@giginet
@giginet
趣味:ゲーム開発👾
cocos2d-xではじめるスマートフォンゲーム開発
このセッションでは
• watch OS2アプリの基本的な作り方を紹介
• ゲームを作る際に発生した問題をいろいろと
紹介
作ってみた
watch OS2
watch OS2 overview
• WatchKit -> watch OS2
• CoreFoundation
• HealthKit, HomeKit, PassKit, EventKit, Contacts
• WCSession
• Complications(ClockKit)
Rendering
watch アプリのUIデザイン
• UIViewのようなViewの任意配置ができない
• 動的にViewを増やすことができない
• 制約の中でどのようにゲーム的な画面を作る
か?
let imageView = UIImageView()
imageView.frame = CGRectMake(10, 10, 100, 100)
watch アプリのUIデザイン
• WKInterfaceController
• UIViewControllerに相当
• WKInterfaceObject
• UIViewに相当。Image/Button/Pickerなどを
サブクラスとして持つ
• alpha, width/height, inset, color, alignment
watch アプリのUIデザイン
• WKInterfaceGroup
• 子要素を縦、または横に整列できる
気合で頑張る
animationWithDuration
// 1秒かけて幅を0から100にする
self.imageView.setWidth(0)
self.animateWithDuration(1.0) { () -> Void in
self.imageView.setWidth(100)
}
animationWithDuration
• 制御できるものが限られている
• レイヤーを重ねられない
• 動的に要素を追加できない
func draw() {
UIGraphicsBeginImageContext(self.screenSize())
let context = UIGraphicsGetCurrentContext()
UIColor.greenColor().setStroke()
UIColor.whiteColor().setFill()
// Set ball position
let rect = CGRectMake(self.ballPosition.x, self.ballPosition.y, 10, 10)
let path = UIBezierPath(ovalInRect: rect)
path.lineWidth = 1.0
path.fill()
path.stroke()
let cgimage = CGBitmapContextCreateImage(context);
let uiimage = UIImage(CGImage: cgimage!)
UIGraphicsEndImageContext()
// Render UIImage
self.canvasView.setImage(uiimage)
}
UIImage + CGContext
UIImage + CGContext
• 何でも描ける
• 大変重い
Input
Detect Digital Crown
• watchOS2でDigital Crownを取得する方法は
ない
• タッチ位置も取得できない
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
// Focus to picker forcedly
self.controlPicker.focus()
// Create picker items
let numbers = (Array<Int>)(0...maxPaddleIndex)
let pickerItems = numbers.map({ (number : Int) -> WKPickerItem in
let pickerItem = WKPickerItem()
pickerItem.title = "(number)"
return pickerItem
})
// Initialise picker
self.controlPicker.setItems(pickerItems)
self.controlPicker.setSelectedItemIndex(0)
}
@IBAction func pickerDidChanged(index : Int) {
// Calc picker ratio
self.paddle.controlPickerRatio = CGFloat(index) /
CGFloat(maxPaddleIndex - 1)
}
In Doom
Run Loop
// Create queue
let queue: dispatch_queue_t = dispatch_queue_create("mainLoop",
DISPATCH_QUEUE_SERIAL)
// Create timer
let timer: dispatch_source_t =
dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue)
// Define main loop
dispatch_source_set_event_handler(timer, { () in
// Main loop
self.update()
self.draw()
})
let secondPerFrame: Double = 1.0 / 30.0
let startTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW,
Int64(NSEC_PER_SEC))
let interval: UInt64 = UInt64(Double(NSEC_PER_SEC) *
secondPerFrame)
// Start timer
dispatch_source_set_timer(timer, startTime, interval, 0)
dispatch_resume(timer)
Audio
func playSound(mediaURL: NSURL) -> Void {
let asset: WKAudioFileAsset = WKAudioFileAsset(URL: mediaURL)
let playerItem = WKAudioFilePlayerItem(asset: asset)
let player = WKAudioFilePlayer(playerItem: playerItem)
guard player.status == WKAudioFilePlayerStatus.ReadyToPlay else {
print("Audio is not available")
return
}
player.play()
}
WKAudioFilePlayer
func playSound(mediaURL: NSURL) -> Void {
// Show media player
self.presentMediaPlayerControllerWithURL(mediaURL,
options: nil,
completion: { (didPlayToEnd: Bool, endTime: NSTimeInterval,
error :NSError?) in
// callback
self.dismissMediaPlayerController()
})
}
presentMediaPlayer
WKInterfaceDevice.currentDevice().playHaptic(WKHapticType.Failure)
playHaptic
まとめ
• 任意の音をWatchのスピーカーから非同期に
鳴らす方法はない
• WCSessionなどを使ってiPhoneから音を鳴ら
すのはできるかも
• Hapticも効果音代わりに使える
HealthKit
HealthKit
• 脈拍に応じて球の速さを変えたい
❌
⭕
Query
Samples
func initialise()
{
// Check whether HealthKit is available
guard HKHealthStore.isHealthDataAvailable() else {
print("not available")
return
}
// Request Permissions
let dataTypes = Set([heartRateType])
healthStore.requestAuthorizationToShareTypes(nil, readTypes:
dataTypes) {
(success, error) -> Void in
if success {
print("authorized")
}
}
}
func executeAnchordObjectQuery() -> Void {
let healthStore = HKHealthStore()
let heartRateType =
HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierHe
artRate)!
// Define predicate
let predicate =
HKQuery.predicateForSamplesWithStartDate(NSDate(), endDate: nil,
options: .None)
// Create query
let query = HKAnchoredObjectQuery(type: heartRateType,
predicate: predicate,
anchor: nil,
limit: Int(HKObjectQueryNoLimit)) {
(query, samples, deletedObjects, anchor, error) -> Void
in
// Callback
dispatch_async(dispatch_get_main_queue(), {() in
let rate = self.heartRateFromSamples(samples)
if rate != nil {
self.currentHeartRate = rate
}
})
}
query.updateHandler = {
(query, samples, deletedObjects, anchor, error) -> Void in
// on Update
dispatch_async(dispatch_get_main_queue(), {() in
let rate = self.heartRateFromSamples(samples)
if rate != nil {
self.currentHeartRate = rate
}
})
}
healthStore.executeQuery(query)
private func heartRateFromSamples(samples: [HKSample]?) -> Double?
{
guard let samples = samples as? [HKQuantitySample] else {
return nil
}
guard let sample = samples.last?.quantity else {
return nil
}
let unit = HKUnit(fromString: "count/min")
let rate = sample.doubleValueForUnit(unit)
return rate
}
60bpm 80bpm
まとめ
• watchOS 2になっても制約が多い
• がんばれば結構できる
Special Thanks
• shu223/watchOS-2-Sampler
• https://github.com/shu223/watchOS-2-Sampler
• Designing for Apple Watch - WWDC 2015 -
Videos - Apple Developer
• https://developer.apple.com/videos/play/
wwdc2015-802/
cocos2d-xではじめるスマートフォンゲーム開発
Questions?

watchOS 2でゲーム作ってみた話