読者です 読者をやめる 読者になる 読者になる

Swiftでassociate/associateBy

配列をディクショナリにする便利関数です。 Swiftになさそうなので実装してみました。

同じことをするにはreduceを駆使してやや冗長に書く必要があります。 keyかvalueになる要素がnilだった場合は要素の追加をスキップします。お好みで変えてください。

extension Array {
    func associateBy<T, U>(_ forKey: (Array.Generator.Element) -> T?,
                     _ forValue: (Array.Generator.Element) -> U?) -> [T:U] {
        return self.reduce([T:U](), { result, element in
            let key = forKey(element)
            let value = forValue(element)

            if let key = key, let value = value {
                var newResult = result
                newResult[key] = value
                return newResult
            } else {
                return result
            }
        })
    }
    
    func associate<T, U>(_ bundler: (Array.Generator.Element) -> (T?, U?)) -> [T:U] {
        return self.reduce([T:U](), { result, element in
            let bundle = bundler(element) 
            
            if let key = bundle.0, let value = bundle.1 {
                var newResult = result
                newResult[key] = value
                return newResult
            } else {
                return result
            }
        })
    }
}

使い方

let array = [("a", 100), ("b", 20), ("c", 10)]
array.associateBy({ "Key: " + $0.0 }, { 100 + $0.1 }) // => [String:Int]

array.associate { ($0.0, $0.1) } // => [String:Int]

NSResponderのkeyDown()とI/OKitのIOHIDManagerのInputValueCallbackの反応速度の違い

ナノ秒の精度で時間を計測できる mach_absolute_time() で検証しました。 NSResponderはSpriteKitのシーンが保持するとあるSKNodeで,keyDown()時の速さを測定しました。 ちなみにSKNodeはfirstResonderになっています。

結論

ものすごくざっくり言うと,I/OKitのHIDManagerでハンドリングした方が常に早く,NSResponderのkeyDown()よりも大体1.5ms〜3ms早く反応します。どちらもメインスレッドでコールバックが呼ばれます。

I/OKitでkeyboardの入力を取るのはものすごーく大げさな実装になって面倒なのですが,タイミングがシビアなアプリケーションを作る際にはI/OKitを採用する価値がありそうです。

macOSで音ゲーを作る上での障壁と懸念事項

macOSで動く音ゲーを開発したい

最近SpriteKitの記事をよく書いていますが,macOSで動く本格的な音ゲーを作りたいと思いチマチマと下準備を進めていました。 前の記事で紹介したリポジトリがまさにそれです。

以前からそれらしいものを組んで検証していたのですが,音ゲーの核の部分であるタイミングが(体感的に)全くといいほど正確ではなく,どうしたものかと悩んでいて手が止まってしまい放置…というありがちな挫折の仕方を経験しました。

そこで,一旦解決していかなければならない懸念事項を挙げ,同じ失敗を繰り返さないように慎重に検証を重ねて組みなおしていこうと思いたちました。macでゲーム作る上での一番の障壁は以外と未踏の領域が多くて勝ちパターンが定まっていない(あるいはノウハウの需要がなく情報がシェアされにくい)ことだと思います。勝ちパターンを作れていけたらいいなぁという期待も込めてできるだけ音ゲー開発に関する記事は書いていきたいです。

コントローラ or キーボードからの入力

今の実装だとキーボードからの入力はmacで標準のNSResponderを継承したSKNodeのコールバックで知ることができます。コントローラに関しては,I/OKitのHIDDeviceManaerに登録した入力要素の値が変わった時に呼び出されるコールバックで処理できます。話をキーボードに固定すると,以下の3パターンの実装が考えられ,タイミングの違いを検証してみる必要がありそうです。

  • NSResponderで処理
  • I/OKitのコールバックで処理
  • I/OKitのコールバック呼び出しを待たず,高頻度でデバイスの値をチェックする独自のスレッドでチェック

気になるのは,I/OKitのコールバックの呼び出しタイミングがNSResponderのタイミングと一致してるのか,またI/OKitで調べられるデバイスの入力状態の更新タイミングとコールバック発生タイミングにどれくらいの差異があるのかという点です。

WindowsではDirectInputを利用していれば間違いがないそうですが,macOSに関してはそういった(ゲーム開発等に使えるかもしれない)情報があまりでてこないので謎が多いです。 経過時間の測定に関しては調べが付いていて,macOSでは mach_absolute_time() を使うことで,ゲーム開始時からの経過時間をナノ秒単位で計測できるため,入力を受け取ったあとの処理については問題はなさそうです。繰り返しになりますが,問題は,いつその入力を知ることができるのか,最速のパターンを知りたいということです。意外とNSResponderが最速だったりして。

現在はSpriteKitの仕組みに乗っているので描画周りのタイミングは任せるとして,その他のタイミング制御は全部で自前で処理していくのが良さそうですね。 タイミングが厳密なゲームにSpriteKitでは厳しいとかそういうのはないですよね???(無知)

mach_absolute_time()の精度と動作条件

問題がなさそうと言いつつも,不安になるもので,mach_absolute_time()関数がmacOS上において一番正確な値を取得できる関数であるかの裏を取る必要があります。また,一部の環境でしか動きませんということでは困ります。Windowsに比べてだいぶ環境の統一が為されているmacでは心配なさそうなきもしますが…。

やはりWindowsにおいてはQueryPerformanceCounter()を使うべしとの知見が簡単に得られましたが,macこれ使っとけという声を拾うことが難しい印象でした。StackOverflow曰く,

stackoverflow.com

stackoverflow.com

精度上々かつクロックの動作効率の変動に影響されないというmach_absolute_time()が攻守ともに優れいている印象です。クロックの変動をうけるのは中々に面倒臭いらしいので,話が簡単になるmach_absolute_timeを採用したいところです。ちなみにWindowsにおけるQueryPerformanceCounterも,CPUの周波数の変動を受ける模様。

所感

コントローラとキーボードからの入力は,一旦上に挙げたパターンの検証で一番早くレスポンスできるものを採用する。経過時間の取得はとりえあえず mach_absolute_time() で。後々もっといい方法があることがわかったらそちらに鞍替えする。

こういう心配ごとばかりに囚われてゲームをそれらしく形にしていくという作業を疎かにする癖をやめたいです…。でも知りたい。知見のある方はアドバイスください。

チラ裏ですが,タイミングの正確さはそこそこにして,プログラムでユーザの入力の癖をみて自動で判定タイミングをミリ秒ずらして挙げて補正という逃げ方もありかなぁと考えています。LR2とかそれでめっちゃ光りますし。

開発中のソースコードを晒すことにした

なんとなく,今までしてこなかったことをしてみたくなったので都度githubに上げることにしました。開発中のコードは荒削りなところがあってあまり晒したくない派ですが,まぁ誰もみないしいいかなと。

去年ちょっと開発していたSwift製macOS向けゲームを,一度白紙に戻して作り直しています。

最初はほぼSpriteKitだけで作っていたのですが,最近iOSのアプリ開発を業務でやっていて知った良さそうなライブラリを色々組み込みたくなったのと,作りかけのコードの規模も大きくなかったし,しばらくコードを触れていなかったという理由があってガラっと作り変えてみました。活躍する場面があるのかちょっと謎ですが,調子に乗ってRxも入れてみました。

ひとまず3月末までに完成させる目標で黙々と仕上げていきます。(仕事の新規開発サービスも3月着地目標だったなぁ…また修羅場りそう)

今年はアプトプットをちゃんと見える形で残していく年にしたい。

github.com

SpriteKitでシーンにクリック可能なオブジェクトを配置するための仕組み

qiita.com qiita.com

上記の記事はiOS向けの記事です。 OSX用のゲームを作っている場合,touchesBegan はないはずなのでどうするのだろうと少し考えてみました。

前述の記事はオブジェクトに名前をつけてdelegateで処理させる戦略のようです。 名前を付けてSwitchで分岐させたり,delegateを用意するのはちょっと大げさだなぁという気もしなくもない…。 onClickedという名前のクロージャを一度渡せばおしまい,というような仕組みがあればいいなと思ったので簡単に実装してみました。

import SpriteKit

protocol Clickable {
  var onClicked: ((SKNode) -> Void)? { get set }
}

class TextButton: SKLabelNode, Clickable {
  var onClicked: ((SKNode) -> Void)?
}

class BasicSene: SKScene {
  override func mouseDown(with event: NSEvent) {
    let location = event.location(in: self)
    
    let clicked = nodes(at: location)
    clicked.forEach {
      if let node = $0 as? Clickable {
        node.onClicked?($0)
      }
    }
  }
}

class DebugScene: BasicSene {

  override func didMove(to view: SKView) {
    scaleMode = .aspectFill

    let label = TextButton(text: "test")
    label.onClicked = { _ in
      print("label is clicked")
    }
    label.position = CGPoint(x: 200.0, y: 200.0)
    addChild(label)
  }

  override func mouseDown(with event: NSEvent) {
    // 独自実装があればここに記述,その際superは必ず呼ぶ
    super.mouseDown(with: event)
  }
}

Clickableプロトコルを用意するのでSpriteKitが最初から用意しているクラスをクリック可能にすることはできませんが,それなりの規模のゲームを作る場合はどうせ独自クラスを作りたくなるし,実用性はそこそこありそうです。 というかこのくらいのことはどこも当たり前にやってるはずなので今更感あるのですが,もしかしたらクロージャを渡しておしまいという仕組みがSpriteKitに最初からあるかもしれないという期待を込めてググってみても,Qiitaのような情報しかでてこなかったので一応。

同じ階層に重なってるオブジェクトもClickableであれば,上のオブジェクトから順に発火されます。また階層構造になっている場合は子要素から順に発火されるようです。これは node(at:) の仕様であり,バージョンや環境によって異なるかもしれません。 クロージャの返り値をBoolにしてpropagationの制御もできればなお良かったのですが,簡易実装ということでご勘弁を。

2017年やりたいことリスト

  • 開発中のゲームを完成させる
  • 次に作りたい作品があるので今年の冬コミ目指して制作する
  • お仕事的にそろそろWeb界隈に行きたい(戻りたい?)
  • 枚数こなして絵を上達させたい

とりあえずやりたいことというか目標です。 去年は割と仕事中心という生活だったのでまずは自分のための時間を作る土俵作りから…。

MacでHID準拠USBゲームコントローラの値を取得したい

コントローラがどんな種類の入力をもっているかスキャンできるコードを紹介します。 このプログラムの管理下にあるときは任意のタイミングで入力の状態がどうなっているかもチェックできます。 startメソッド の無限ループはチェックのためにその場におきました。よきに取り除いてください。

Criteriaには検出したいデバイスの条件を設定して下さい。

// HIDDeviceManager
#import <Foundation/Foundation.h>
#import <IOKit/hid/IOHIDLib.h>
#import "HIDDeviceManager.h"

static const int numOfDetection = 2;
static const NSDictionary* criteria = @{@kIOHIDDeviceUsagePageKey: @(kHIDPage_GenericDesktop),
                                         @kIOHIDDeviceUsageKey: @(kHIDUsage_GD_Joystick)};

static void CFSetApplier(const void *value,void *context)
{
  CFArrayAppendValue((CFMutableArrayRef)context,value);
}

@interface HIDDeviceManager (){}
  @property(nonatomic) IOHIDManagerRef manager;
  @property(nonatomic) IOHIDDeviceRef* devices;
  @property(nonatomic) int deviceNum;
@end

@implementation HIDDeviceManager

- (void) start {
  self.manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);

  IOHIDManagerSetDeviceMatching(self.manager, (__bridge CFDictionaryRef)criteria);
  IOHIDManagerOpen(self.manager, kIOHIDOptionsTypeNone);
  
  CFSetRef copyOfDevices = IOHIDManagerCopyDevices(self.manager);
  CFMutableArrayRef hidDevices = CFArrayCreateMutable(kCFAllocatorDefault, 0 ,&kCFTypeArrayCallBacks);
  CFSetApplyFunction(copyOfDevices, CFSetApplier, (void*)hidDevices);
  
  CFRelease(copyOfDevices);

  IOHIDDeviceRef shoudlManage[numOfDetection];
  int deviceCount = (int)CFArrayGetCount(hidDevices);

  for (int i=0; i<deviceCount && i < numOfDetection; i++) {
    IOHIDDeviceRef hidDevice = (IOHIDDeviceRef)CFArrayGetValueAtIndex(hidDevices, i);
    if (hidDevice == NULL) {
      continue;
    }

    shoudlManage[self.deviceNum++] = hidDevice;
  }
  
  self.devices = shoudlManage;
  CFRelease(hidDevices);
  
  while(1) {
    time_t t0;
    t0=time(NULL);
    while(t0==time(NULL))
    {
    }

    for (int i=0; i<self.deviceNum; i++) {
      CFArrayRef elements = IOHIDDeviceCopyMatchingElements(self.devices[i], NULL, 0);
      int elementCount = (int)CFArrayGetCount(elements);
      
      for (int j=0; j<elementCount; j++) {
        IOHIDElementRef element = (IOHIDElementRef)CFArrayGetValueAtIndex(elements, j);
        IOHIDElementType elementType = IOHIDElementGetType(element);
        unsigned int usage = IOHIDElementGetUsage(element);
        unsigned int usagePage = IOHIDElementGetUsagePage(element);
        
        printf("element %d: type: %d usage: %d value: ", j, elementType, usagePage);
    
        switch(elementType) {
          case kIOHIDElementTypeInput_ScanCodes:
          case kIOHIDElementTypeInput_Misc:
          case kIOHIDElementTypeInput_Button:
          case kIOHIDElementTypeInput_Axis:
          {
            IOHIDValueRef valueRef;
            IOHIDDeviceGetValue(self.devices[i], element, &valueRef);
            
            int value = (int)IOHIDValueGetIntegerValue(valueRef);
            printf("%d", value);
          }
          default: {
            printf("\n");
            break;
          }
            
        }
      }

      CFRelease(elements);
    }
  }
}

@end

今回はObjective-Cのコードを紹介しますが,Swift3上のコードでも実現できます。やることは各型のRefという接尾辞やCFReleaseを取り除いたり,引数のコールバックをクロージャあるいは関数のUnsafePointerで渡すといった変更を加えていけばObjective-Cレスでスッキリします。別の実装ではSwiftを使っていますが,今回はUnsafePointer <-> Tの相互変換が面倒くさかったので無理にSwiftを使う必要はないなという判断です。C++の関数を呼び出してるので,こっちの方が相性良さそう。

それにしてもI/OKit面倒臭いですね。

値が変更されたタイミングにその値をコールバックで受け取って処理したいならもっとスッキリとした実装があります。 qiita.com