A Day In The Life

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

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
    }
}

React と Redux を使った Electron 環境を作ってみました

最近、デスクトップアプリ開発界隈で流行ってる Electron の環境を ReactRedux を組み合わせて作成してみました。Redux は Swift にも移植されたたりしているので前から気になってました。せっかくなんでオリジナル Redux 触ってみようと思った次第です。

最終成果物的なソースコードはこちら

Electron + React + Redux の最小構成 Boilerplate を作成しました。Boilerplate ってのは「再利用を意図した標準的な文例集」という意味で、必要最低限これだけあれば Electron + React + Redux 開発ができるよってことを意識してあります。ソースコードはこちらに置きました。 github.com

インストールと Electron の実行

以下の手順だけで Electron アプリが起動します。babel-core/register を使っているので ES6 のコンパイルは不要です。

$ git clone https://github.com/glassonion1/electron-react-redux-boilerplate.git
$ cd electron-react-redux-boilerplate
$ npm install
$ electron app

ディレクトリ構成

ディレクトリ構成は以下のような感じにしました。

$ cd ~/electron-react-redux-boilerplate
$ tree -I node_modules
.
├── app
│   ├── actions
│   │   └── counter.js
│   ├── components
│   │   ├── Root.js
│   │   └── main.js
│   ├── css
│   │   └── style.css
│   ├── index.html
│   ├── index.js
│   └── reducers
│       ├── counter.js
│       └── index.js
└── package.json

使用した Node.js のライブラリ

Electron 0.3系、React 0.14系、Redux 3.3系で構築しています。ReactDOM は React 0.14系から必須になったみたいなので追加してます。 ES6 から ES5 へのトランスパイラは Babel 6系を使うことにしました。Babel 5と6で設定が違ったのですこし手こずりました。

{
    "name": "electron-boilerplate",
    "version": "1.0.0",
    "description": "electron app",
    "main": "index.js",
    "author": "glassonion1",
    "license": "MIT",
    "repository": {
        "type": "git",
        "url": "https://github.com/glassonion1/electron-react-redux-boilerplate.git"
    },
    "keywords": [
        "electron",
        "react",
        "redux",
        "babel",
        "boilerplate"
    ],
    "dependencies": {
        "babel-core": "^6.6.5",
        "babel-preset-es2015": "^6.3.13",
        "babel-preset-react": "^6.3.13",
        "electron-prebuilt": "^0.31.0",
        "react": "^0.14.2",
        "react-dom": "^0.14.2",
        "react-redux": "^4.0.0",
        "redux": "^3.3.1"
    },
    "devDependencies": {
        "react": "^0.14.2",
        "react-redux": "^4.0.0",
        "redux": "^3.3.1"
    }
}

ES6 の自動トランスパイル

babel-core/register を使うと ES6 をgulp なんかでわざわざトランスパイルしなくても自動でトランスパイルしてくれます。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Hello Electron!</title>
        <link href="css/style.css" rel="stylesheet" type="text/css">
    </head>
    <body>
        <div id="root"></div>
        <script>
            window.onload = function() {
                require("babel-core/register")({
                    "presets": ["es2015", "react"]
                });
                require("./components/main.js");
            }
        </script>
    </body>
</html>

詳しくはこちらの記事を参照してください。 - ES2015でコマンドラインツールを書くときは、require hookで提供するべきか、事前にバベるべきか? - Koa入門 - Part2: babel/registerでES6を自動コンパイル

参考にしたサイト

Unity5.3のインストールからプロジェクト作成までの流れ

一昨年(2014年)の10月にスマホのネイティブゲームの開発をはじめたのですが、そこからずっと Cocos2d-x を使っています。同じゲームエンジンを使い続けると知識が偏るし ゲームエンジンのシェア的には Unity の方が大きそうなんで Unity にもチャレンジすることにしました。手始めとして昨年末にリリースされた Unity 5.3のインストールからプロジェクト作成までの手順をまとめてみました。

Unity 5.3のインストーラーをダウンロードする

こちらから Unity5.3のインストーラーをダウンロードします。

Unity 5.3のインストール

インストーラーの起動

ダウンロードしたファイルをクリックして Unity インストーラーを起動します。 Unityのインストーラーの起動 起動画面にある「Unity Download Assistant」ボタンをクリックすると以下の「Download And Install Unity」というタイトルのウインドウが立ち上がります。 Download And Install Unityウインドウ

コンポーネントのインストール

ライセンスに同意して「Unity component selection」まで進めます。この画面でインストールするコンポーネントを聞かれます。スマホゲーム(iOS/Android)の開発を行う場合は以下のように「Android Build Support」と「iOS Build Supoort」のチェックを追加して進めます。 Unityのインストール画面 以下のようにインストール先を選択する画面になるのでインストール先を選んで次に進めるとコンポーネントのダウンロードが始まります。 インストール先を選ぶ コンポーネントのダウンロード(通信環境によってはかなり時間がかかります)が終わったら自動でインストールが始まります。以下の画面が表示されればインストール成功です。 インストール完了 Unity はアプリケーションフォルダの Unity 以下にインストールされます。

Unity アカウントの作成

インストールが終わったので Unity を立ち上げます。起動すると Unity アカウントを作成しろと言われるので作成します。以下のような Unity アカウントの作成ページに飛ぶと思うので名前、ユーザ名、メールアドレス、国、パスワード、セキュリティー質問を入力します。 Unityアカウントの作成 Create account ボタンを押すと次のページに進んで幾つか質問(開発者の人数とかとのデバイス向けに開発するのとか)があるので適当に答えます。Webサイト上での作業はこれで終わりです。次に登録したメールアドレスにアカウントの確認メールがくるので、メール本文のリンクを押してアカウントを有効にします。

Pro 版か Personal 版を選択する

作成したアカウントの有効化が終わったら、Unity の作業に戻ります。Unity を立ち上げるとログイン画面になるので先ほど作成したアカウントでログインします。次に Professional 版か Personal 版を選べと言われるので Personal 版を選びます(Pro版のライセンスキーをお持ちの方は方はPro版でもOKです)。ここまでの手続きが終わると以下の画面が表示され新規プロジェクトを作成できるようになります。 新規プロジェクト作成画面

プロジェクトの作成

Unity のインストールが終わったのでプロジェクトを作成します。先ほどの画面の「New project」ボタンを押すと以下の画面が表示されるのでプロジェクト名と保存場所を指定してプロジェクトを作成します(3D、2Dどちらかを選択してください。例では2Dを選びました)。 プロジェクト作成画面 今回プロジェクト名は「MyFirstUnityGame」としました(適宜変更してください)。「Create project」ボタンを押すとプロジェクトが作成されて以下のように Unity IDE が立ち上がります。 UnityIDE 以上で Unity のインストールとプロジェクトの作成は終わりです。

おすすめUnity本