ビットコインが暴落すると沈没するヴィーナスアーク

なんかできました

最近仮想通貨をはじめたというフォロワーさんが見たいというので作りました。

ヴィーナスアークとはアイカツスターズ!に出てくる,豪華客船型のアイドル学園です。このアイドル学園,資料があまりないので公式の画像を雑に切り抜いてWebからの値を流し込んで30分くらいでこしらえました。

ちょっとした解説

コインチェックのWebSocketAPIを利用して,次々と流れてくる取引額をみて,あらかじめ決めておいたレンジのどの辺りに居るかを正規化して,船の縦のオフセットをいじってるだけです。サムネ映えを狙って,レンジは今の取引額周辺の1430000から1450000円を設定しています。つまり今日の昼間には完全沈没していたことになります。

現実的には1400000-1800000あたりに設定しておいて,気がついたら沈没,あるいは浮上していたといった楽しみ方が良いかと思われます。

コインチェックのAPIはサイトのトップページから見える情報概ね公開API経由で取得できるようです。

完全にアウトな画像を使って居るので,Webサービスとしての公開予定はありません。

ちなみに

僕の資産も沈没中です。

その後の様子

画像を抜いたリポジトリ

https://github.com/gomachan7/btc-venusark

RxSwiftでObservable<Void>をonNext()する方法

もしかしたらRx本来の使い方から逸れているかもしれませんが,引数のいらない連続したイベントを通知するのにも, Rxは便利ですよね。

そこで Observable<Void> ,あるいは PublishSubject<Void> というようなObservableを定義したことがある方も多くいるのではないでしょうか。

ただ,このVoidが流れるObservableは,onNext() あるいは onNext(Void)という形でVoidを送り込んでやろうとすると,引数を与えてやれという旨のエラーが発生します。

そんなときは onNext(()) としてやると良いです。これは値の入ってない空のTupleを与えてあげていることになりますが,これで通るようになります。

いっそのこと,Void型を取るObservableには,引数を必要としないようなExtensionを定義するとより直感的になりますね。

extension ObserverType where E == Void {
    public func onNext() {
        onNext(())
    }
}

RxSwiftでBindTo可能な独自プロパティを生やす方法

MVVM的なアーキテクチャを採用しようとしたとき,KRProgressHUD のような簡単にLoadingViewを出せるライブラリを使っていると,そのViewのOn/Offの制御をどうしようかと少し悩みます。ViewModelのOutputを普通にSubscribeして制御しても良いですが,できればRxCocoaで拡張されたUIView同様,BindToで見た目を制御したいです。

そこで,そもそもBindToをどうやって実現しているのか本家ソースコードを除くと一目瞭然で,Binder というコンポーネントを利用すれば良いことがわかりました。ObservableのbindにBinderを渡してやることで他のUIViewと同様,自動的にsubscribeしてくれます。

具体的な使い方として,たとえばUIViewControllerに以下のようなextensionを定義してあげると良さげです。 以下の例におけるComputedPropertyのloadingViewはBindTo可能なプロパティを定義している本家コードほぼそのまま持ってきています。 第一引数で渡したインスタンスが,クロージャの第一引数へ,クロージャの第二引数に渡ってくる値が入っています。

extension UIViewController {
    
    func showLoadingView() {
        KRProgressHUD.show()
    }
    
    func hideLoadingView() {
        KRProgressHUD.dismiss()
    }
    
    var loadingView: Binder<Bool> {
        return Binder(self) {(vc, value: Bool) in
            value ? vc.showLoadingView() : vc.hideLoadingView()
        }
    }
}

これで,なんの違和感もなくBindできました。

class TestViewController: UIViewController {
    
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // ローディングViewを一定間隔でOnOffさせる。
        Observable<Int>
            .interval(1.0, scheduler: MainScheduler.instance)
            .scan(true) { current, _ in !current }
            .take(3)
            .bind(to: self.loadingView)
            .disposed(by: self.disposeBag)
    }
}

ちなみにUILabelのその他のプロパティも本家の定義と同様にextensionを定義してやることで,Bindableにすることができます。

2018年について

あけましておめでとうございます。

年が明けてから実家でやることがなくてひたすらコードを描くという修行のようなことをしていました。確か,去年も同じような事していたと思います。

今年の目標は月に1本,新しいアウトプットを出せるように頑張ります。

アウトプットとは,軽量リポジトリ1本とか,趣味で内輪にお披露目してるクソゲーとか,ちまちま作ってるゲームの完成とかそんな感じです。
もうちょっと外向きに,出していけたらとおもます。

あと地味な意気込みが一つ。ブログの更新頻度を上げたいです。
Web,アプリ制作その他のお仕事欲しいので宣伝も兼ねて,人の目につくようなことをたくさんやる。そんな1年にしたいです。

PhaserでTintアニメーションをするボタンを作る

最近プライベートではPhaserとTypescriptでWebブラウザゲームを開発しています。

Phaserの知見が溜まって来ているのでいつか放出したいのですが,今回は取り急ぎ表題の通り,クリックすると色がグラデーションで変わっていくボタンの作り方をメモします。 コピペで使えます。

実装

Prototype拡張でPhaser.Buttonにメソッドとプロパティを生やします。

// Button.ts
// 拡張メソッド

declare module Phaser {
    interface Button {
        _tintStep: number;
        _clickTintStart: number;
        _clickTintEnd: number;
        _clickTintDuration: number;
        _isClickTintEnabled: boolean;
        _isAddedTintCallback: boolean;
        _clickTintTweenToStart: Phaser.Tween;
        _clickTintTweenToEnd: Phaser.Tween;
        setClickTint(startColor: number, endColor: number, duration: number): void;
        setClickTintEnabled(enabled: boolean): void;
    }
}

// private

function removeTween(context: Phaser.Button) {
    if (context._clickTintTweenToStart != null) {
        context._clickTintTweenToStart.stop();
        context.game.tweens.remove(context._clickTintTweenToStart);
        context._clickTintTweenToStart = null;
    }
    if (context._clickTintTweenToEnd != null) {
        context._clickTintTweenToEnd.stop();
        context.game.tweens.remove(context._clickTintTweenToEnd);
        context._clickTintTweenToEnd = null;
    }
}

function addToEndTween(context: Phaser.Button): Phaser.Tween {
    return context.game.add.tween(context)
        .to({ _tintStep: 100 }, context._clickTintDuration * ((100 - context._tintStep) / 100), Phaser.Easing.Default, false)
        .onUpdateCallback(() => {
            context.tint = Phaser.Color.interpolateColor(context._clickTintStart, context._clickTintEnd, 100, context._tintStep, 1);
        }).start();
}

function addToStartTween(context: Phaser.Button): Phaser.Tween {
    return context.game.add.tween(context)
        .to({ _tintStep: 0 }, context._clickTintDuration * (context._tintStep / 100), Phaser.Easing.Default, false)
        .onUpdateCallback(() => {
            context.tint = Phaser.Color.interpolateColor(context._clickTintStart, context._clickTintEnd, 100, context._tintStep, 1);
        }).start();
}

Phaser.Button.prototype._tintStep = 0;
Phaser.Button.prototype._isAddedTintCallback = false;
Phaser.Button.prototype._isClickTintEnabled = false;
Phaser.Button.prototype._clickTintTweenToStart = null;
Phaser.Button.prototype._clickTintTweenToEnd = null;

Phaser.Button.prototype.setClickTint = function (startColor: number, endColor: number, duration: number) {
    this._clickTintStart = startColor; // 起点の色。InputUpした時点の色からこの色へ向かう
    this._clickTintEnd = endColor; // InputDownした時点の色からこの色へ向かう
    this._clickTintDuration = duration;

    if (this._isAddedTintCallback) {
        return;
    }
    this._isAddedTintCallback = true;

    this.onInputDown.add(() => {
        removeTween(this);

        if (this._isClickTintEnabled) {
            this._clickTintTweenToEnd = addToEndTween(this);
        }
    });

    this.onInputUp.add(() => {
        removeTween(this);

        if (this._isClickTintEnabled) {
            this._clickTintTweenToStart = addToStartTween(this);
        }
    });
};

Phaser.Button.prototype.setClickTintEnabled = function (enabled: boolean) {
    this._isClickTintEnabled = enabled;
    removeTween(this);
};

この拡張定義はエントリーポイントのファイルにインポートするなりよしなに読み込んでいただけると使えるようになります。 使い方は以下のような感じ。

// exsample.ts
const button = this.game.add.button(0, 0, "ResourceName");
button.tint = 0xffffff;
button.setClickTintEnabled(true);
button.setClickTint(0xffffff, 0x838383, 200);

実際の動作イメージ

f:id:azaraseal_7:20171210175515g:plain

補足

色の変化のStep数を適当に100としていますが,もっと細かく刻んでも問題ないと思います。Tintアニメーションが走っている最中にボタンから手を離しても,その状態の色から元の色に違和感なく戻っていきます。これは現在どのステップまでアニメーションが進んでいるかを自身のインスタンスに記録しておくことで,ちゃんと元に戻るまでの適切なdurationがTweenを作成されるたびに計算されるためです。

Tweenのインスタンスを使いまわせたら一番良いのですが,うまくいきませんでした。

当然今回定義した関数やプロパティにユーザは自由にアクセスできてしまうので,扱いには少し注意が必要です。

Phaserについて思うこと

Phaserはかなり多くの機能を提供してくれるゲームエンジンなのですが,こういう凝ったUIを作ろうとすると,かなり苦労する印象です。スクロールビューとリストビューあたりも,デフォルトでは提供されておらず,ライブラリもあることはあるのですが,かなり微妙なのでこれからもPhaserを使っていくとしたら自作の機運があります。。。

Cocos2d-xやSpriteKitといったSceneを提供するゲームフレームワークにMVCの仕組みを取り込むための設計

開発中の音ゲーがだいぶプレイアブルになってきたのでプレイ画面を構成するモジュールがどのようになっているのかを整理するのも兼ねて, Scene を提供するありがちなゲームフレームワークをリーダブルでメンテナブルにするための設計についてすこしまとめてみます。

Sceneにできること

Cocos2d-xやSpriteKitが提供するSceneと呼ばれるクラスは,通常60FPSで呼ばれゲーム内容をコマ単位で表現するために提供される update(elapsed) 関数や onClick(event)onTouch(event) 関数のような画面とのインタラクション結果を通知するベースの機能を備えていることが大半です。 これらの仕組みを組み合わせると簡単にインタラクティブなゲームが作れるので,Sceneは言わばゲームの入出力処理のハブとなるようなクラスといえます。何も考えずにゲームを実装していくとSceneにゲームを表現する大半のプログラムが書かれることになり,ゲームへの入力,描画処理,ゲーム内容のロジックといった処理が集約された大規模なクラスへと成長しがちです。 規模の小さいゲームではあまり問題にはなりませんが,それなりの規模であったり,似たようなシーンを多用するようなゲームだと処理の使い回しもしにくくソースコードの管理が煩雑になり,開発が大変になります。

それを避けるためにMVCやMVPといったアーキテクチャが考案され,ひとつの開発手法として定着しています。個人的にはゲームにMVCの考え方を導入するのはアリだと思っていて,今回の開発でもMVCをシーンに適用しやすくするための簡単な仕組みを実装しました。

Scene上におけるMVCの主な役割

Model

ゲームのロジック部分は全てここへ置く。たとえばスコア計算だったり,ゲーム設定の反映,通信処理,主人公の状態や敵の状態などが該当します。シーンの真上に乗るMVCの一部としてのModelは,シーンに深く依存しても良いと考えます。他のシーンで使いまわすことは考えなくて良いです。その代わり,シーンに依存しない細かい単位で実装されたModelをいくつか格納し,それらをうまく組み合わせてシーン独自のより具体的なModelを表現することを考えると良いです。ただし,この場合においてもViewやControllerに対して依存はしないように注意したいです。

どうしてもControllerから操作される前提でControllerにとって都合の良いインタフェースを決めがちですが,それは自然なことなので大丈夫です。

View

Cocos2d-xやSpriteKitにおける汎用 Node クラスを継承して,そのノードにシーンを構成するスプライトを配置していく形で利用します。 本来Sceneに直に置いていたものを,Viewというレイヤーを一枚かませて配置します。Sceneが直接Childとして保持するビューはただひとつ,MVCにおけるViewのみです。あまり意味のない行為にみえるかもしれませんが,onClickonTouch などの入力系のインタフェースが消え去ったクラスを用意することでビューに関する処理だけに関心を向けることができ,見通しがよくなります。

また一つの大きなUIとして使い回せるので,デバッグシーンにおいてこのViewに対するテストも簡単にできるようになります。

ViewはModelを保持することができ,都度Modelからデータを引っ張ってきたり,Modelからデータの更新通知をもらって,ビューに反映させたりすることができます。ViewにModelのインスタンスを渡すのはSceneかControllerの役目で良いと思います。

スプライトと紐づくタッチイベントなどはViewの中でハンドリングしてModelを叩いたり,他のビューに影響を及ぼしても良いと考えます。

最後に,Sceneにおける update 関数と同じインタフェースを用意し,Sceneで行なっていたupdate処理をViewに移譲させます。

Controller

Sceneにおける onClickonTouch といった入力系処理の移譲先という考え方ができます。

Sceneではこれらの処理をスルーし,Controllerに投げてハンドリングしてもらうようにします。Viewとupdate関数の関係と同じです。入力に対してViewやModelを操作します。基本的にやることはそれだけに留めます。 よってViewとModelのインタフェースを叩くだけなので,必然的にControllerのやることはシンプルで,行数は短くなりがちです。ロジックもControllerでやってしまいがちですが,Modelに押し込むことを徹底するべきです。

必要に応じて入力をViewやModel用の都合の良いデータ型に変換したり,Modelを叩いた結果をそのままView渡すといったこともやります。

ではSceneがやることは

MVCの保持

上記MVCに求められる基本的なインタフェースを備えたベースとなるModel,View,Controllerクラスを自前で作り,必ずこれを継承したMVCの組みを保持するような初期化方法を要求するSceneを,Cocos2d-xやSpriteKitが用意するSceneクラスを継承して作ります。 以下のような感じでどうでしょうか。

Model gist.github.com

View gist.github.com

Controller gist.github.com

ゲームにおいては必ず自分が用意したMVC付きSceneから継承し,Sceneを構成するようにルール化すると良いと思います。 Sceneは自動でViewをAddChildするなり,UpdateをView移譲するなりやりやすい形にカスタマイズします。自分は以下のようなベースシーンをつくりました。

BasicScene gist.github.com

具体的な利用例

上記MVCとSceneを継承した,Harpで実際に遊ぶことのできるPlayScene,PlayModel,PlayView,PlayControllerを紹介します。 やっていることの割にはだいぶ短くまとまっていると思います。Modelを細かく分割して,それぞれのModelに処理を隠蔽すると,それらを組み合わせても処理の流れだけを参照でき,見通しの良いModelを表現できます。 具体的にどんなことをMVCにやらせればいいか,参考程度に眺めてみてください。

github.com

まとめの図

MVCとSceneについての関係

f:id:azaraseal_7:20170530012034p:plain

まとめ

実はこれは実際の業務で得た知見を参考にしている部分も多く,自分自身まだまだ試行錯誤の段階で未完成な部分も多くあるのですが,だいぶMVCの仕組みを導入しやすい環境が整ったのではないかと思います。

作るゲームやプロジェクトによってはこの設計が正解とは真逆をいってしまうこともあるかと思いますが,一つの選択肢として参考程度にみてもらえればと思いまとめてみました。

ちなみに業務ではCocos2d-xにMVPアーキテクチャを適用させていましたが,View<-Presenter->ModelとViewからのイベントを必ずPresenterを通しハンドリング方法がとてもしんどく,Modelから直接データをPullするやり方の方がシンプルで良いなと感じたのであえて採用していません。Harpのようなゲームパッドやキーボードからの一方的な入力オンリーのゲームでは,ViewがModelのデータを参照するだけのMVCの方が絶対に良いです。ここはお好みでというのと,Viewからのイベントが頻繁に発生するような場合にはMVPの方が良いかもなぁという印象です。