A Day In The Life

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

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 にアップできなくなります。

参考記事

SpriteKitでUIScrollView的なスクロール処理を実装する

SpriteKit でゲームやアプリを開発していると UI 系の部品がなくて困ることがあります。SpriteKit にはラベルしかなくボタンすらないです。ましてやスクロールビューなんて贅沢なものは当然ありません。現在開発しているアプリでどうしても必要になったので自作してみました(プログラムが複雑になるので横スクロールのみ実装しています)。

慣性処理のないスクロールノー

まずはじめに一番単純な実装からいきます。指で動かした分だけラベルが横にスクロールします。慣性の処理がないので touchesBegan: withEvent: と touchesMoved: withEvent: の処理だけで実現できます。

import SpriteKit

class ScrollNode: SKSpriteNode {
    
    // スクロールさせるコンテンツを配置するためのノード
    private var contentNode = SKNode()
    private var startX: CGFloat = 0.0
    private var lastX: CGFloat = 0.0
    
    init(size: CGSize) {
        super.init(texture: nil, color: SKColor.clearColor(), size: size)

        self.userInteractionEnabled = true

        self.contentNode.position = CGPoint(x: 0, y: 0)
        self.addChild(self.contentNode)
        
        // スクロールさせるコンテンツ
        let myLabel = SKLabelNode(fontNamed: "Helvetica")
        myLabel.text = "scroll"
        myLabel.fontSize = 20
        self.contentNode.addChild(myLabel)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        // store the starting position of the touch
        let touch = touches.first
        let location = touch!.locationInNode(self)
        startX = location.x
        lastX = location.x
    }
    
    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
        let touch = touches.first
        let location = touch!.locationInNode(self)
        
        // set the new location of touch
        let currentX = location.x
        
        // スクロールのスピード。好みで変えてください
        let scrollSpeed: CGFloat = 1.0        
        let newX = self.contentNode.position.x + ((currentX - lastX) * scrollSpeed)

        // 左と右端の設定
        let limitFactor: CGFloat = 0.3 // 好みで変えてください
        let leftLimitX = self.size.width * (-limitFactor)
        let rightLimitX = self.size.width * limitFactor

        // 移動処理
        if newX < leftLimitX {
            self.contentNode.position = CGPointMake(leftLimitX, self.contentNode.position.y)
        } else if newX > rightLimitX {
            self.contentNode.position = CGPointMake(rightLimitX, self.contentNode.position.y)
        } else {
            self.contentNode.position = CGPointMake(newX, self.contentNode.position.y)
        }
        
        // Set new last location for next time
        lastX = currentX
    }   
}

使い方

使い方は簡単で適当なサイズを設定して SKScnene に配置してやるだけです。

class GameScene: SKScene {
    override func didMoveToView(view: SKView) {
        let scrollNode = ScrollNode(size: CGSize(width: 200, height: 44))
        self.addChild(scrollNode)
    }
}

参考記事

慣性処理に対応したスクロールノー

慣性処理のないスクロールノードでもとりあえずスクロールの動きはできていますがやっぱり UIScrollView のように慣性スクロールしたほうが自然だし使いごごちも良いです。というわけで慣性処理を追加します。 慣性処理は update メソッドの中で行います。 SKSpriteNode オブジェクトは SKScene オブジェクトのように自動で update メソッドが呼び出されないので自前でタイマーを仕込むか SKScene の update メソッドと連動して動かす必要があります。

import SpriteKit

class ScrollNode: SKSpriteNode {
    
    private var contentNode = SKNode()
    private var startX: CGFloat = 0.0
    private var lastX: CGFloat = 0.0
    // タッチされているかどうか
    private var touching = false
    // 少しずつ移動させる
    private var lastScrollDistX: CGFloat = 0.0
    
    init(size: CGSize) {
        super.init(texture: nil, color: SKColor.clearColor(), size: size)

        self.userInteractionEnabled = true

        self.contentNode.position = CGPoint(x: 0, y: 0)
        self.addChild(self.contentNode)
        
        // スクロールさせるコンテンツ
        let myLabel = SKLabelNode(fontNamed: "Helvetica")
        myLabel.text = "scroll"
        myLabel.fontSize = 20
        self.contentNode.addChild(myLabel)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        self.touching = true
        // store the starting position of the touch
        let touch = touches.first
        let location = touch!.locationInNode(self)
        startX = location.x
        lastX = location.x
    }
    
    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
        let touch = touches.first
        let location = touch!.locationInNode(self)
        
        // set the new location of touch
        let currentX = location.x
        
        lastScrollDistX =  lastX - currentX
        
        self.contentNode.position.x -= lastScrollDistX
        
        // Set new last location for next time
        lastX = currentX
    }
    
    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        self.touching = false
    }
    
    override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
        self.touching = false
    }
    
    func update(currentTime: CFTimeInterval) {
        // タッチされてたら
        guard !touching else {
            return
        }
       
        // 左と右端の設定
        let limitFactor: CGFloat = 0.3
        let leftLimitX: CGFloat = self.size.width * (-limitFactor)
        let rightLimitX: CGFloat = self.size.width * limitFactor
        if self.contentNode.position.x < leftLimitX {
            // 行き過ぎたから戻す
            self.contentNode.position.x = leftLimitX
            lastScrollDistX = 0.0
            return
        }
        if self.contentNode.position.x > rightLimitX {
            // 行き過ぎたから戻す
            self.contentNode.position.x = rightLimitX
            lastScrollDistX = 0.0
            return
        }
            
        // 慣性処理
        var slowDown: CGFloat = 0.98
        if fabs(lastScrollDistX) < 0.5 {
            slowDown = 0.0
        }
        lastScrollDistX *= slowDown
        self.contentNode.position.x -= lastScrollDistX
    }
}

使い方

はじめに説明しましたが SKScnene に配置してやるだけでは updata メソッドが実行されないので SKScene の update メソッドと連動させてやる必要があります。

class GameScene: SKScene {

    var scrollNode: ScrollNode!
    
    override func didMoveToView(view: SKView) {
        self.scrollNode = ScrollNode(size: CGSize(width: 200, height: 44))
        self.addChild(scrollNode)
    }

    override func update(currentTime: CFTimeInterval) {
        // scrollNodeのupdateメソッドを呼ぶ
        self.scrollNode.update(currentTime)
    }
}

参考記事

慣性処理をなめらかにしたい

慣性スクロールには対応できましたが少し動きが硬いです。そこでアニメーションを入れてなめらかに動くように修正してみます。

class ScrollNode: SKSpriteNode {
    :
    : 省略
    :
    func update(currentTime: CFTimeInterval) {
        // タッチされてたら
        guard !touching else {
            return
        }
        
        let limitFactor: CGFloat = 0.3
        let leftLimitX: CGFloat = self.size.width * (-limitFactor)
        let rightLimitX: CGFloat = self.size.width * limitFactor
        if self.contentNode.position.x < leftLimitX {
            // アニメーションさせる
            if !self.contentNode.hasActions() {
                let move = SKAction.moveToX(leftLimitX, duration: 0.2)
                move.timingMode = .EaseInEaseOut
                self.contentNode.runAction(move)
            }
            lastScrollDistX = 0.0
            return
        }
        if self.contentNode.position.x > rightLimitX {
            // アニメーションさせる
            if !self.contentNode.hasActions() {
                let move = SKAction.moveToX(rightLimitX, duration: 0.2)
                move.timingMode = .EaseInEaseOut
                self.contentNode.runAction(move)
            }
            lastScrollDistX = 0.0
            return
        }
            
        var slowDown: CGFloat = 0.98
        if fabs(lastScrollDistX) < 0.5 {
            slowDown = 0.0
        }
        lastScrollDistX *= slowDown
        self.contentNode.position.x -= lastScrollDistX
    }
}