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

リモートワークの話

要するにリモートワークがしたいです,ということです。

僕が勤めているのはそこそこの規模のイケイケ系IT会社で,見込み残業が給料に含まれていて残業代は一切でない,よくある裁量労働制を採用している会社なのですが,制度としてリモートワークやフレックスタイム制といったものがありません。所謂出勤時間に厳しく( 最近は弊チームではすごく多めに見てもらっていて個人的に助かっておりますが ),退勤時間が無限解放な,そんな感じの労働環境と言っていいでしょう。裁量労働ならば,こういうリモートやフレックスといった働き方とは相性がいいはずなので,是非ともやってもらいたいと思っています。ですが,現実的には難しいようです。

部署によっては週1でリモートワークを許可しているところもあるらしく,本日人事の方と話していてリモートって難しいんですかねぇ?と投げかけたところ,え?週1でもないの? という反応をされてしまいました。 僕も個人的にリモートしたいだの,せめてフレックスは可能なのかということをちょいちょい訴えてはいるのですが,やはり会社としての制度を変えるのは大変なようでなかなか勤務体制を変える動きまでは至っていません。

うちの会社は人材の動きが激しく,2,3年もすると新卒で入ったエンジニアの同期が結構な人数出て言ってしまうのが恒例となっていますが,こういった働き方をちょっと許容するだけでエンジニアの幸福度が段違いに上るし,人の動き,流れも変わると思うのですが,如何なものなのでしょうかね?個人的な価値観になってしまいますが,留まるに値する十分な理由って,何もお金や組織感だけじゃないと思うんですよ。これから何十年も続けていくのですから,ライフスタイルにまで影響を与えるような働き方が許容されるのは素敵なことだと思っています。

やっぱりMTGは重要だしフルリモートしたいだなんていう贅沢は言いません。対面での会話の重要性は十分わかっているつもりですが,さぁ開発フェーズだ といったときにフロー状態に入れるような環境づくりの選択肢としてリモートはやっぱりありなんじゃないでしょうか?

ですが…,今日エンジニア向けの集中開発専用ルーム的な部屋(弊社では精神とテクの部屋と呼んでいます)で作業していて,壁際の人に気づかれないような席を陣取っている人があからさまに寝ていて,リモートワークを採用したくない側の不安な気持ちもちょっとわかりました。あと僕の出勤時間がよくずれるのも原因か。。説得力無いもんな。。。でも裁量労働だから,やることやっていれば悪いことではないと思っています。

定時退社の話

定時退社するのは自分のためだし,長い目でみると会社のためにもなるので僕は定時退社を続けます。職業柄,どうしても残らなければいけないことがあることは理解した上で,でもあまり無理をしないで長く続けて行きたいです。

ViewControllerの戻るボタンのタップをハンドリングする。(戻る動作をキャンセル可能)

iOSにおいて前の画面に戻る際, 「本当に前の画面に戻りますか?」 というアラートダイアログを出して,選択次第で戻る動作をキャンセルしたい需要が生まれました。あまり大げさな実装はしたくないので,iOSが用意するUINavigationControllerによる画面遷移の仕組みに乗りたいとします。 どう対処すれば良いでしょうか。

qiita.com

こちらで紹介されている方法では,戻る動作を検知する以上のことはできませんが,これから提案する方法は戻る動作自体を取りやめることが可能になります。

なお,デメリットとして UINavigationController を拡張し,それを利用すると言うお約束が必要です。今回紹介する実装はpopする場合にとどまっていますが,modalとして呼び出された際のdismissも同じ理屈でハンドリング可能です。

UINavigationControllerの独自実装

まずは,前提となる独自実装です。UINavigationControllerへViewControllerをpushする際に,pop時のコールバックを明示的に指定するところがキモで,ここで本当にpopしていいかを一度delegateに問い合わせるということをしています。delegateが実装されていない場合は通常どおりpopされるだけです。

@objc protocol NavigationViewControllerDelegate: class {
    @objc optional func navigationController(_ navigationController: MyNavigationController,
                                             willPopViewController: UIViewController) -> Bool
}

final class MyNavigationController: UINavigationController {

    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        let action = #selector(pop)
        viewController.navigationItem.leftBarButtonItem?.action = action

        super.pushViewController(viewController, animated: animated)
    }

    @objc private func pop() {
        guard let willBePoppedVC = viewControllers.last else {
            return
        }

        if let delegate = willBePoppedVC as? NavigationViewControllerDelegate {
            let willBePoppedVC = delegate.navigationController?(self, willPopViewController: willBePoppedVC) ?? true
            if !shouldPop {
                return
            }
        }

        popViewController(animated: true)
    }
}

ハンドリングしたいViewControllerの実装

navigationController(_:willDismissViewController) の返り値のtrue/falseでpopしていいかを MyNavigationController に伝えます。 たとえば shouldShowAttention というbool値を用意しておき,これがfalseのときはダイアログを出さずにそのままpop,trueのときは,一旦popを取りやめて,ダイアログの選択次第であとから明示的にpopするということが可能になります。

final class SampleViewController: UIViewController, NavigationViewControllerDelegate {
    private var shouldShowAttention = false

    // NavigationViewControllerDelegate
    func navigationController(_ navigationController: MyNavigationController,
                              willPopViewController: UIViewController) -> Bool {
        if shouldShowAttention {
            showAttentionDialogBeforeLeaving(okCallback: {[weak self] _ in
                _ = self?.navigationController?.popViewController(animated: true)
            })
        }
        
        return !shouldShowAttention
    }

    private func showAttentionDialogBeforeLeaving(okCallback: @escaping () -> Void) {
        let alertDialog = UIAlertController(title: "警告", message: "本当に戻る?", preferredStyle: .alert)
        let okAction = UIAlertAction(title: "はい", style: .default, handler: { _ in okCallback() })
        let cancelAction = UIAlertAction(title: "いいえ", style: .cancel, handler: nil)
        
        alertDialog.addAction(cancelAction)
        alertDialog.addAction(okAction)
        
        present(alertDialog, animated: true, completion: nil)
    }
}

使い方

普通のUINavigationControllerと一緒です

let navigationVC = MyNavigationController(rootViewController: SampleViewController())
navigationVC.pushViewController(SampleViewController(), animated: false)
show(navigationVC, sender: nil)

以上,ViewControllerの戻る動作を取りやめたい場合があったので,とりあえず思いついた実装を紹介しました。意外と需要があるはずなので,何かのお役に立てれば幸いです。

追記:
バックジェスチャ?っう頭が…

Swiftのジェネリクスで型引数を別の型引数の制約に利用するとCommand failed due to signal: Segmentation fault: 11(未解決)

原因の詳細までは特定できていませんが,該当箇所を修正すると直るのでそういうことだと思っています。Swift3.0です。

製作中のゲームのSKSceneにロジックをずらずら書いていくのはしんどそうなので,自前でMVCの雛形を用意し,SKSceneクラスでやることはイベントをそのままControllerに流すのとMVC保持程度にしたいなと思っています。 そこで,任意のシーンにMVCを持たせる際,MVCの組み合わせにゆるい制約をもたせようと軽いDI的な仕組みを実装してみたいのですが, class MVCContainer<M: BasicModel, V: BasicView, C: BasicController<M, V>> と書いた部分で Segmentation fault が発生してしまうようでした。

XCodeはコンパイル時までエラーも警告も出しません。なぜだろう。同じことをM,V,Cを指定して実現できる方法はあるのでしょうか。そこまでこだわる部分じゃないのですが,釈然としないので原因と解決案を探り中です。

import Foundation
import SpriteKit

class BasicModel {
  required init() {}
}

class BasicView: SKNode {
  required override init() {
    super.init()
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

class BasicController<M: BasicModel, V: BasicView> {
  let model: M
  let view: V

  required init(model: M, view: V) {
    self.model = model
    self.view = view
  }
}

class TestModel: BasicModel {
}

class TestView: BasicView {
}

class TestController: BasicController<TestModel, TestView> {
}

class MVCContainer<M: BasicModel, V: BasicView, C: BasicController<M, V>> {
  let model: M
  let view: V
  let controller: C
  
  init() {
    model = M()
    view = V()
    controller = C(model: model, view: view)
  }
}

class TestScene: BasicScene {
  let mvc = MVCContainer<TestModel, TestView, TestController>()
}

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とかそれでめっちゃ光りますし。