13
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Firebaseで流しそうめん

Posted at

これはFirebase #2 Advent Calendar 2019の20日目の記事です。

はじめに

Firebase Realtime Databaseでスマホを連結して、スマホの中で流しそうめんします。
流れてくるそうめんは、箸デバイス(おそらく世界初)でとれます。
箸デバイスとモードの背景は、@krohigewagmaさんに作ってもらいました。

流しそうめん展示.png

流しそうめんを作ろうと思ったのは、ダライアスが好きで、今ならスマホで作れるだろうと試作を始めたら、いつの間にか流しそうめんを作ってました。

概要

  • 1台のそうめんを流す端末と、複数台のそうめんが流れる端末から構成されています。
  • 流す端末は、定期的にそうめん情報をFirebase Realtime Databaseに追加します。
  • 流れる端末は、そうめん情報をonChildChanged()で監視し、そうめんの位置が自身の端末番号と同じであれば、そうめんを描画して流します。
  • 流れる端末の端末番号は、事前に登録しておきます。
  • 制限時間内に、そうめん取り放題のゲームモードがあります。

流しそうめん概要.png

データ定義

  • モード情報
    modeが背景と流す物の設定です。
data class ModeInfo(
    var key:String = "",
    var mode:Int = 0,      // ステージモード種別
    var timePlay:Int = 20  // ゲームプレイ時間(秒)
)
  • そうめん情報
    typeとrowは、流す端末がモード情報のmodeを元に決定します。
data class FlowInfo(
    var key:String = "",
    var name:String = "",
    var type:Int = 0,             // アイテム種別(モード毎に設定)
    var pos:Int = 0,              // 現在の端末位置
    var row:Int = 0,              // 縦の表示位置(モード毎に設定)
    var complete:Boolean = false
)

モード種類

  • モードは流しそうめん、魚群(海)、寿司の3種類です。
  • モード毎に流すもの・縦の表示位置・スピード・スコアを設定しています。

流しそうめん

モードそうめん.png

魚群(海)

モード魚群.png

寿司

モード寿司.png

そうめんが流れる仕組み

そうめんを流す端末と、そうめんが流れる端末の仕組みを説明します。

そうめん追加(流す端末)

  • timerで定期的にそうめんを流します。
  • 最初にそうめん情報のposを0で追加します。
  • 次にそうめん情報のposを1(最初の端末)に変更します。
    この手順を行うのは、そうめんが流れる端末がonChildChanged()のみ監視しているためです。
// そうめん情報参照を生成
var refFlow = database?.getReference("flow")

// そうめんをDatabaseへ追加
fun addEntity() {
    // ユニークKey取得
    val key = refFlow?.child("flow")?.push()?.getKey()!!

    // そうめん情報を設定
    val data = FlowInfo()
    data.key = key
    data.name = "そうめん"
    data.type = stageGame?.randomEntityType()!!  // 流す物をランダムに設定
    data.pos = 0
    data.row = 1  // モード毎に縦の位置を設定

    // Databaseへ追加
    refFlow?.child(data.key)?.setValue(data)

    // Databaseを更新
    val map = mutableMapOf<String, Any>("pos" to 1)
    refFlow?.child(data.key)?.updateChildren(map)
}

そうめん取得(流れる端末)

  • ChlidEventListenerのonChildChanged()でそうめん情報のposを監視します。posが端末番号と同じならば、そうめんを追加して再描画します。
  • onChildAdded()でそうめん情報のposを監視しないのは、アプリ再起動後のDatabase同期でそうめんを追加すると、posが変更されずに留まってしまったそうめんも追加することになり、大量のそうめんで負荷かかるのを避けるためです。つまり、そうめんが落ちたか取られたか分からないけど、失くなったと判断します。(都合良すぎ)
val listenerChildEventFlow = object : ChildEventListener {
    override fun onCancelled(dataSnapshot: DatabaseError) { }

    override fun onChildMoved(dataSnapshot: DataSnapshot, previousChildName: String?) { }

    override fun onChildChanged(dataSnapshot: DataSnapshot, previousChildName: String?) {
        val value = dataSnapshot.getValue(FlowInfo::class.java)
        if (value != null) {
            // posが端末番号と同じならそうめんを追加&再描画
            if (AppGlobal.deviceNumber == value.pos) {
                stageGame?.addEntity(value)
                invalidate()
            }
        }
    }

    override fun onChildAdded(dataSnapshot: DataSnapshot, previousChildName: String?) { }

    override fun onChildRemoved(p0: DataSnapshot) { }
}

全体イメージ

  • 流す端末からそうめんを追加します。
    詳細フロー1.png

  • 1台目の端末がposの変化を検知して、そうめんを描画します。
    詳細フロー2.png

  • 1台目の端末の右端にかかったら、posを2に変更します。

  • 2台目の端末がposの変化を検知して、そうめんを描画します。
    詳細フロー3.png

  • これを繰り返します。

モード切り替え(流す端末)

  • 流す端末の起動時に、configにモードが存在するか確認し、存在すればモードのkeyを取得します。
// configのモード情報を生成
var refMode = database?.getReference("config")

// モード変更Listener登録(1回のみ取得)
refMode?.child("mode")?.addListenerForSingleValueEvent(listenerValueEventMode)

// 現在のモードを取得
val listenerValueEventMode = object : ValueEventListener {
    override fun onDataChange(dataSnapshot: DataSnapshot) {
        if (dataSnapshot.exists()) {
            val value = dataSnapshot.getValue(ModeInfo::class.java)
            if (value != null) {
                key_mode = value.key
                val mode = ModeStage.values().filter { it.mode == value.mode }.first()
                AppGlobal.modeStage = mode
            }
        }
    }

    override fun onCancelled(databaseError: DatabaseError) { }
}
  • 流す端末へモード切り替えボタンを用意し、ボタンクリック時にモードを新規登録 or 変更します。
// モード設定
fun setMode(mode:Int) {
    if (key_mode.isEmpty()) {
        // 新規
        val data = ModeInfo()
        data.mode = mode
        // ユニークKey取得
        key_mode = refMode?.child("mode")?.push()?.getKey()!!
        data.key = key_mode
        refMode?.child("mode")?.setValue(data)
    } else {
        // 更新
        val map = mutableMapOf<String, Any>("mode" to mode)
        refMode?.child("mode")?.updateChildren(map)
    }
}

モード切り替え(流れる端末)

  • configの生成/変更を監視し、モードを再設定して画面を切り替えます。
// configのモード情報を生成
var refMode = database?.getReference("config")
refMode?.addChildEventListener(listenerChildEventMode)

// configのモード情報を監視
val listenerChildEventMode = object : ChildEventListener {
    override fun onCancelled(dataSnapshot: DatabaseError) { }

    override fun onChildMoved(dataSnapshot: DataSnapshot, previousChildName: String?) { }

    override fun onChildChanged(dataSnapshot: DataSnapshot, previousChildName: String?) {
        val value = dataSnapshot.getValue(ModeInfo::class.java)
        if (value != null) {
            var mode = ModeStage.values().filter { value.mode == it.mode }.first()
            // ステージ変更の初回のみ切り替え
            if (AppGlobal.modeStage != mode) {
                // ステージ生成
                createStage(mode)
                // ステージ初期化
                stageGame?.initialize(screen_width, screen_height)
                AppGlobal.modeStage = mode
            }
            // プレイ時間設定
            AppGlobal.timePlay = value.timePlay
        }
    }

    override fun onChildAdded(dataSnapshot: DataSnapshot, previousChildName: String?) {
        val value = dataSnapshot.getValue(ModeInfo::class.java)
        if (value != null) {
            var mode = ModeStage.values().filter { value.mode == it.mode }.first()
            // ステージ変更の初回のみ切り替え
            if (AppGlobal.modeStage != mode) {
                // ステージ生成
                createStage(mode)
                // ステージ初期化
                stageGame?.initialize(screen_width, screen_height)
                AppGlobal.modeStage = mode
            }
            // プレイ時間設定
            AppGlobal.timePlay = value.timePlay
        }
    }

    override fun onChildRemoved(dataSnapshot: DataSnapshot) { }
}

当たり判定

  • 本当は2点タッチで掴む動作(タッチしたまま箸を動かして範囲が狭くなった)まで判定したかったのですが、現在はそうめん内で2点タッチしてれいばOKとしています。
    今後は掴む動作に改良したいと思います。

おわりに

  • 展示会では、何で連携しているか(Bluetooth?、Wifi Direct?)問い合わせが多く、Firebase Realtime Databaseを使って、そうめんの位置をバケツリレーしてると答えると、驚きと納得があることが嬉しかったります。
  • 流しそうめんは竹の節やつなぎ目でもたついたりするので、リアルタイム同期の速度やネットワークの遅延でもたつきが表現できたと思います。(前向きすぎるかも)
  • 現在は流れる端末に事前に端末番号の登録が必要なので、新しく端末を追加したり、途中の端末を抜いたりの融通がききません。参加者の端末で、飛び入り参加などができると楽しいので改善したいと思います。
  • 夢は100台(Firebaseの無料枠の最大接続数)の端末で流しそうめん!
13
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?