A Day In The Life

とあるプログラマの備忘録

UserDefaultsのキー名をenumで管理する

NSUserDefaults あらため UserDefaults のキーを enum で管理する方法(Swift3対応)です。UserDefaults のキーを enum で管理する方法はいくつか見つけたのですが Swift3 に対応しているものが見つからなかったので書きました。

キーに文字列リテラルを直接指定したくない

UserDefaults のキーを文字列リテラルでやり取りするのはタイプミスなどのリスクが高く enum で管理したほうがコンパイルエラーでミスを検知できるのでオススメです。

import Foundation

enum UserSettings: String {
  case autoPlay = "autoPlay"
  case purchasedItems = "purchasedItems"
  :
  : キーをこちらに宣言する
  :
  
  func set(value: Int) {
    UserDefaults.standard.set(value, forKey: self.rawValue)
    UserDefaults.standard.synchronize()
  }
 
  func integer() -> Int {
    return UserDefaults.standard.integer(forKey: self.rawValue)
  }
    
  func set(value: Float) {
    UserDefaults.standard.set(value, forKey: self.rawValue)
    UserDefaults.standard.synchronize()
  }
    
  func float() -> Float {
    return UserDefaults.standard.float(forKey: self.rawValue)
  }
    
  func set(value: Double) {
    UserDefaults.standard.set(value, forKey: self.rawValue)
    UserDefaults.standard.synchronize()
  }
    
  func double() -> Double {
    return UserDefaults.standard.double(forKey: self.rawValue)
  }
    
  func set(value: Bool) {
    UserDefaults.standard.set(value, forKey: self.rawValue)
    UserDefaults.standard.synchronize()
  }
    
  func bool() -> Bool {
    return UserDefaults.standard.bool(forKey: self.rawValue)
  }

  func set(value: Any) {
    UserDefaults.standard.set(value, forKey: self.rawValue)
    UserDefaults.standard.synchronize()
  }
    
  func object() -> Any? {
    return UserDefaults.standard.object(forKey: self.rawValue)
  }
}

Int, Float, Double, Bool はオプショナル型を使いたくなかったのでそれぞれメソッドを定義しました。 使い方はこんな感じになります。

// Bool
UserSettings.autoPlay.set(value: true)
let autoPlay: Bool = UserSettings.autoPlay.bool()

// Array
UserSettings.purchasedItems.set(value: ["item1", "item2", "item3"])
if let items = UserSettings.purchasedItems.object() as? [String] {
  print(items.count)
}      

もう一つのメリット

UserDefaults を使う場所を一元管理できるメリットもあります。アプリのいろんな場所で UserDefaults を使いまくって一体どんなデータが保存されてるかわかりませんってなったことありませんか?(自分はよくあります)

参考記事

uGUIのテキストを点滅させてみる

Unity の uGUI でテキストをこんな感じに点滅させる方法です。

f:id:glass-_-onion:20161027174139g:plain

Update メソッドに処理を書いても良かったのですが個人的な好みで UniRX を使って実装してみました。Mathf.Sin メソッド(三角関数)を使ってアルファ値を0から1に緩やかに変化させています。

using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;
using UniRx;

public class FlashingText : MonoBehaviour
{

  [SerializeField]
  private float angularFrequency = 5f;
  // 30FPS
  static readonly float DeltaTime = 0.0333f;
  private Text text;

  void Start()
  {
    float time = 0.0f;
    text = GetComponent<Text>();
    Observable.Interval(TimeSpan.FromSeconds(deltaTime)).Subscribe(_ =>
      {
        time += angularFrequency * deltaTime;
        var color = text.color;
        color.a = Mathf.Sin(time) * 0.5f + 0.5f;
        text.color = color;
      }).AddTo(this);
  }
}

angularFrequency の値をいじることで点滅のタイミングを調整することができます。

なんで Mathf.Sin(time) * 0.5f + 0.5f なのか

Mathf.Sin メソッドの結果に 0.5 をかけたり足したりしているのはアルファの値を0から1の間で緩やかに変化させるためです。

Mathf.Sin(x)

サイン関数を使うと以下のグラフのように、値は-1から1の間で変化します。 f:id:glass-_-onion:20161027171240p:plain

Mathf.Sin(x) * 0.5

それに0.5をかけて値を-0.5から0.5の間で変化するようにしてから f:id:glass-_-onion:20161027171245p:plain

Mathf.Sin(x) * 0.5 + 0.5

0.5を足すことによって0から1の間に値が収まるようになります。 f:id:glass-_-onion:20161027171250p:plain

Mathf.Abs(Mathf.Sin(time)) じゃダメなの?

値を0から1の間で変化させるだけであれば Mathf.Abs(Mathf.Sin(time)) でも問題なさそうですが、値の変化が滑らかになりません。

Mathf.Abs(Mathf.Sin(time))

Mathf.Abs メソッドを使った場合の値の変化は以下のようになります。0付近の値の変化が急な感じになります。 f:id:glass-_-onion:20161027171256p:plain Mathf.Abs メソッドを使って点滅させると以下のような動きになります。

f:id:glass-_-onion:20161027174140g:plain

微妙な違いですが少し違和感がありますね。

AVAudioSequencerでMIDIファイルを再生して曲の終わりにコールバックを設定する方法

AVAudioSequencerでMIDIファイルを再生して曲の終わりにコールバックを設定する方法です。AVAudioSequencer 自体にそのようなコールバックがないので CoreMIDI のコールバックを使います。CoreMIDIはSwiftのブロックではなくてC言語の関数ポインタでコールバックを取らないといけないケースがあって大変です。

public class Sequencer {

  let callBack: @convention(c) (UnsafeMutablePointer<Void>, MusicSequence, MusicTrack, MusicTimeStamp, UnsafePointer<MusicEventUserData>, MusicTimeStamp, MusicTimeStamp) -> Void = {
    (obj, seq, mt, timestamp, userData, timestamp2, timestamp3) in
    // Cタイプ関数なのでselfを使えません
    let mySelf: Sequencer = unsafeBitCast(obj, Sequencer.self)
    :
    : 曲の終了後に行う処理
    :
  }
    
  var sequencer: AVAudioSequencer

  public func playWithMidiURL(midiFileUrl: NSURL, audioFiles: [NSURL]) {
    // サンプラーとオーディオエンジンの初期化
    let audioEngine = AVAudioEngine()
    let samplerNode = AVAudioUnitSampler()

    // サンプラーとオーディオエンジンをつなげる
    audioEngine.attachNode(samplerNode)
    audioEngine.connect(samplerNode,
      to: audioEngine.mainMixerNode,
      format: samplerNode.outputFormatForBus(0))

    // オーディオエンジンをスタートする
    do {
      try samplerNode.loadAudioFilesAtURLs(audioFiles)
      try audioEngine.start()
    } catch {
            
    }

    // シーケンサーの初期化
    self.sequencer = AVAudioSequencer(audioEngine: audioEngine)
    let musicSequence = audioEngine.musicSequence
        
    // MIDIファイル読み込み
    do {
      try sequencer.loadFromURL(midiFileUrl, options: .SMF_ChannelsToTracks)
    } catch {
      print("Error load MIDI file")
    }
    // シーケンサスタート
    do {
      sequencer.prepareToPlay()
      try sequencer.start()
    } catch {
      print("Error play MIDI file")
    }
        
    // 曲の長さを取得
    var musicLengthInBeats: NSTimeInterval = 0.0
    for track in sequencer.tracks {
      let lengthInBeats = track.lengthInBeats
      if musicLengthInBeats < lengthInBeats {
        musicLengthInBeats = lengthInBeats
      }
    }
    // 曲の最後にコールバックを仕込む
    MusicSequenceSetUserCallback(musicSequence, callBack, unsafeBitCast(self, UnsafeMutablePointer<Void>.self))
    var musicTrack: MusicTrack = nil
    MusicSequenceGetIndTrack(musicSequence, 0, &musicTrack)
    let userData: UnsafeMutablePointer<MusicEventUserData> = UnsafeMutablePointer.alloc(1)
    MusicTrackNewUserEvent(musicTrack, ceil(musicLengthInBeats), userData)
  }
}

曲の長さを調べてタイマーでやれば良いと思われるかもしれませんが、それだと曲のポーズに対応できないのでこのような方法が必要になります。

参考記事

ソースコードはこちら

動作確認済みのソースコードはこちらに置いてあります。 - glassonion1/R9MIDISequencer

UnityのuGUI関連のクラスを図にしてみた

Unity で開発をはじめた頃、一番わけがわからなかったのがゲームを構成するオブジェクト同士の関係でした。ゲームオブジェクト?コンポーネント?トランスフォーム?なんじゃこれってなりました。Cocos2d-x や SpriteKit のように単純なノードツリーで構成されてないことにかなり戸惑いました。Unity を本格的に使い始めて3ヶ月ほど経ちそこそこ Unity 特有の世界観に慣れてきたのでこの辺りでクラスの関係をクラス図にまとめてみました。自分は2Dゲーム開発と uGUI を使った UI 開発で使うことがほとんどなのでその辺で使うクラスに絞って図にしてあります。

クラス図

f:id:glass-_-onion:20160614095059p:plain

補足

  • UnityEngine ネームスペース(namespace)は省略しています
  • ネームスペースごとに色をわけています
  • GameObject、Component クラスには代表的なメソッドとして GetComponent メソッドを挙げています。GetComponentInChildren や GetComponentsInChildren メソッドなんかはわざと省略しています

個人的な感想

  • GameObject と Component クラスが似ててややこしいがここをちゃんと理解すれば混乱の大部分はなくなる
  • Canvas クラスは UI と関係ないところにいたのが意外だった
  • オブジェクトの親子関係を管理しているのは Transform クラス、なんで GameObject じゃないのか疑問
  • UI 系クラスの親が MonoBehaviour だった
  • UI 系クラスは Selectable(ユーザ入力) と Graphic(表示)クラスに大別できる
  • イベント系クラスは別のネームスペース(EventSystems)で管理されている
    • UIBehaviour クラスのネームスペースが EventSystems だった(UI じゃないんだ的な)

参考

おすすめUnity本

今から始める Swift 3 対策

Xcode 7.3 から Swiftのバージョンが 2.2 になりました。このバージョンから Swift 3 に向けた deprecated(非推奨)警告が出るようになったのでその対策です。

'++' is deprecated: it will be removed in Swift 3

結構話題になったインクリメント(デクリメントも)演算子の廃止の対策です。+=使えってことらしいです。Python もインクリメント演算子ないので、案外無くても困らないのかもしれませんね。

修正前
var i = 0
i++
修正後
var i = 0
i += 1

「--」も同じで「-=」を使いましょう。

C-style for statement is deprecated and will be removed in a future version of Swift

C言語風の for 文が廃止になりました。下記の例だとはじめに「'++' is deprecated: it will be removed in Swift 3」の警告が出ます。インクリメント演算子を修正したあとにこの警告が出るので一気に修正してちゃいましょう。

修正前
for var i = 0; i < 10; ++i {
  //
}
修正後
for i in 0 ..< 10 {
  //
}

「i」は宣言しなくても良いみたいです。少し気持ち悪いけど慣れれば大丈夫です。

Use of string literal for Objective-C selectors is deprecated; use '#selector' instead

セレクタの渡し方が変わりました。Objective-c に近くなった印象です。個人的にも文字列でいいんかいなと思ってたのでまぁ妥当な変更だと思います。

修正前
class HogeViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    let button = UIButton()
    button.addTarget(self,
      action: "RespondToButton:",  // 警告
      forControlEvents: .TouchUpInside)
  }
  func RespondToButton(sender: UIButton) {
  }
}
修正後
class HogeViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    let button = UIButton()
    button.addTarget(self,
      action: #selector(HogeViewController.RespondToButton(_:))
      forControlEvents: .TouchUpInside)
  }
  func RespondToButton(sender: UIButton) {
  }
}

Use '#selector' instead of explicitly constructing a 'Selector'

文字列の代わりにセレクタ関数を使ってた場合も同じです。

修正前
class HogeViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    let button = UIButton()
    button.addTarget(self,
      action: Selector("RespondToButton:"),  // 警告
      forControlEvents: .TouchUpInside)
  }
  func RespondToButton(sender: UIButton) {
  }
}
修正後
class HogeViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    let button = UIButton()
    button.addTarget(self,
      action: #selector(HogeViewController.RespondToButton(_:))
      forControlEvents: .TouchUpInside)
  }
  func RespondToButton(sender: UIButton) {
  }
}

'init()' is deprecated: init() will be removed in Swift 3. Use nil

init()が非推奨になりました。C言語で開発されたライブラリを使ってると遭遇確率が高いと思います。

修正前
var musicTrack = MusicTrack()
MusicSequenceGetIndTrack(musicSequence, 0, &musicTrack)
修正後
var musicTrack: MusicTrack = nil
MusicSequenceGetIndTrack(musicSequence, 0, &musicTrack)

アドレス渡しする前にコンストラクタ呼び出しするのは確かにおかしいので納得です。

UnsafeMutablePointerを使う場合

UnsafeMutablePointer を使ってる場合も同じ警告がでます。

修正前
let packetListPtr: UnsafeMutablePointer<MIDIPacketList> = UnsafeMutablePointer.alloc(1)
var packet = UnsafeMutablePointer<MIDIPacket>() //警告
packet = MIDIPacketListInit(packetListPtr)
修正後
let packetListPtr: UnsafeMutablePointer<MIDIPacketList> = UnsafeMutablePointer.alloc(1)
var packet: UnsafeMutablePointer<MIDIPacket> = nil
packet = MIDIPacketListInit(packetListPtr)

別の関数で Init するのに宣言時にコンストラクタ呼び出しするのはおかしいですね、これも納得です。

参考

UnityでRedux.NETを使ってみた

フロントエンド開発と iOS アプリ開発でステート管理に Redux(iOSはReSwift)を使っています。Unity でも Redux 使えないかなと探していたところ Redux.NET というC♯の実装を見つけたので使ってみることにしました。

手順

Redux.NET を使うためには Reactive Extensions というやつをインストールしないといけないのですが今のところ(2016年6月現在) Unity には対応していません。なので代わりに Reactive Extensions の Unity 実装である UniRx をインストールします。

UniRxのインストール

Asset Store からUniRxをインストールする

Redux.NETのインストール

Redux.NET は100行にも満たないシンプルなコードなのでインストールというほど大げさなものではないです。

GuillaumeSalles/redux.NET から Store.cs と IAction.cs をコピーして Unity プロジェクトの Scripts フォルダに追加してください。 github.com

Redux.NETをUnityで動くように修正する

Redux.NET はそのままではコンパイルエラーになるので以下のように Store.cs を修正します。

using System;
//using System.Reactive.Subjects;
using UniRx;

namespace Redux
{
    ...その他コード
}

以上で準備は完了です。

使い方

カウンターを使った簡単なサンプルを例に使い方をみてみましょう。

アクションの作成

初めにアクションを定義します。

public class IncrementAction : IAction { }

public class DecrementAction : IAction { }

ストアの作成

次にストアを定義します。

public class State
{
  public int Counter { get; set; }
}

Reducerの作成

最後に Reducer を作成します。

public static class CounterReducer
{
  public static State Execute(State state, IAction action)
  {
    if (action is IncrementAction)
    {
      return new State {
        Counter = state.Counter + 1
      }
    }
    else if (action is DecrementAction)
    {
      return new State {
        Counter = state.Counter - 1
      }
    }
    return state;
  }
}

ステートの監視

アクションの発行とステートを監視してみます。

using UnityEngine;
using UniRx;
using Redux;

public class Hoge : MonoBehaviour
{
  public IStore<State> Store { get; private set; }
  void Start()
  {
    // ストアの生成
    store = new Store<State>(Reducers.Hoge, initialState);
    store.Subscribe(state => {
      // stateが変わったら呼ばれる
      print(state.Counter);
    });
    // ボタンのイベント(ボタンが2つ配置されていると仮定)
    var buttons = GetComponentsInChildren<Button>();
    foreach (var button in buttons)
    {
      var name = button.name;
      button.onClick.AddListener(() => OnButtonClick(name));
    }
  }
  void OnButtonClick(string name)
  {
    if (name == "Increment")
    {
      // アクションの発行
      this.Store.Dispatch(new IncrementAction());
    }
    else if (name == "Decrement")
    {
      // アクションの発行
      this.Store.Dispatch(new DecrementAction());
    }
  }
}

注意点

一つだけ注意点があります。UniRx が iOS の Mono2x に対応していないため iOS アプリのビルドをする場合は IL2CPP に設定をしてから実行する必要があります。

おすすめUnity本

自作ライブラリをBitcodeに対応させる

Swiftで開発した自作ライブラリを Bitcode に対応させるためには Xcode の Build Settings 項目に以下を設定する必要があります。

  • Enable BitcodeをYesにセット
  • Other C Flagsに-fembed-bitcodeをセット(Other C++ Flagsには自動でセットされます)
  • User-DefinedにBITCODE_GENERATION_MODEを追加してbit codeをセット

f:id:glass-_-onion:20160602215450p:plain

上記を設定しないとアーカイブアップロード時に

ITMS-90668
Invalid Bundle Executable.
The executable file 'ライブラリ名' contains incomplete bitcode.
To compile binaries with complete bitcode, open Xcode and choose Archive in the Product menu.

というエラーが出て App Store にアップできなくなります。

参考記事