これはFirebase #2 Advent Calendar 2019の20日目の記事です。
はじめに
Firebase Realtime Databaseでスマホを連結して、スマホの中で流しそうめんします。
流れてくるそうめんは、箸デバイス(おそらく世界初)でとれます。
箸デバイスとモードの背景は、@krohigewagmaさんに作ってもらいました。
流しそうめんを作ろうと思ったのは、ダライアスが好きで、今ならスマホで作れるだろうと試作を始めたら、いつの間にか流しそうめんを作ってました。
概要
- 1台のそうめんを流す端末と、複数台のそうめんが流れる端末から構成されています。
- 流す端末は、定期的にそうめん情報をFirebase Realtime Databaseに追加します。
- 流れる端末は、そうめん情報をonChildChanged()で監視し、そうめんの位置が自身の端末番号と同じであれば、そうめんを描画して流します。
- 流れる端末の端末番号は、事前に登録しておきます。
- 制限時間内に、そうめん取り放題のゲームモードがあります。
- 展示会で実演した流しそうめん
流しそうめんの紹介動画
展示会では子供が夢中でやってくれます。
データ定義
- モード情報
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種類です。
- モード毎に流すもの・縦の表示位置・スピード・スコアを設定しています。
流しそうめん
魚群(海)
寿司
そうめんが流れる仕組み
そうめんを流す端末と、そうめんが流れる端末の仕組みを説明します。
そうめん追加(流す端末)
- 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台目の端末の右端にかかったら、posを2に変更します。
-
これを繰り返します。
モード切り替え(流す端末)
- 流す端末の起動時に、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の無料枠の最大接続数)の端末で流しそうめん!