【マインスイーパー開発 #7】ゲームのプレイ時間(経過時間)をChronometerで表示する|Android & Kotlinアプリ開発

Android & Kotlin

Android & Kotlinの環境でマインスイーパーを開発する方法を説明します。

今回は、Chronometerを使ってゲームのプレイ時間(経過時間)を表示します。

そそたた
そそたた

Chronometerは、経過時間を表示する時に使用するAndroid SDKのGUIコンポーネントです。

動作イメージ

タイルを開いたらプレイ時間をカウントアップしていき、爆弾を開いたらカウントアップを停止します。

ソースコード

開発環境は次の通りです。

PCMacBook Pro(2016年モデル)
IDEAndroid Studio 4.0.1
Android SDKminSdkVersion 16
targetSdkVersion 30
言語Kotlin 1.3.72
package com.sosotata.minesweeper.ui.widgets

import android.os.SystemClock
import android.widget.Chronometer

/**
 * プレイ時間カウンター
 */
class PlayTimeCounter (chronometer: Chronometer) {
    private val timer = chronometer
    private var offsetPlayTimer = 0L
    private var isStartPlayTimer = false
    private var stopTime = 0L

    fun start() {
        if (!isStartPlayTimer) {
            isStartPlayTimer = true
            offsetPlayTimer = 0
            stopTime = 0
            timer.base = SystemClock.elapsedRealtime()
            timer.start()
        }
    }

    fun stop() {
        if (isStartPlayTimer) {
            timer.stop()
            isStartPlayTimer = false
            stopTime = SystemClock.elapsedRealtime() - timer.base
        }
    }

    fun pause() {
        if (isStartPlayTimer && offsetPlayTimer == 0L) {
            timer.stop()
            offsetPlayTimer = SystemClock.elapsedRealtime() - timer.base
        }
    }

    fun resume() {
        if (isStartPlayTimer) {
            timer.base = SystemClock.elapsedRealtime() - offsetPlayTimer
            offsetPlayTimer = 0
            timer.start()
        }
    }

    fun reset() {
        timer.base = SystemClock.elapsedRealtime()
        offsetPlayTimer = 0
    }

    fun currentTime(): Long {
        return if (isStartPlayTimer) {
            if (offsetPlayTimer > 0) {
                offsetPlayTimer
            } else {
                SystemClock.elapsedRealtime() - timer.base
            }
        } else {
            stopTime
        }
    }
}
package com.sosotata.minesweeper.ui.main

import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.ScaleAnimation
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import com.sosotata.minesweeper.R
import com.sosotata.minesweeper.model.TileController
import com.sosotata.minesweeper.model.TileType
import com.sosotata.minesweeper.ui.widgets.PlayTimeCounter
import kotlinx.android.synthetic.main.game_fragment.*

class GameFragment : Fragment() {
    companion object {
        fun newInstance() = GameFragment()
    }

    private lateinit var viewModel: GameViewModel
    private var playTimeCounter: PlayTimeCounter? = null

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View {
        return inflater.inflate(R.layout.game_fragment, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)

        viewModel.tile = TileController.create(TileType.Square)
        viewModel.tile.startedGame = {
            playTimeCounter?.start()
        }
        viewModel.tile.gameOver = {
            playTimeCounter?.stop()
            resetButton.setImageBitmap(BitmapFactory.decodeResource(context?.resources, R.drawable.reset_button_gameover))
        }
        viewModel.tile.initialize(requireContext())

        playTimeCounter = PlayTimeCounter(playTimer)
        squareView.initialize(viewModel.tile)

        resetButton.setOnClickListener {
            val btnEffect = ScaleAnimation(
                1.0f, 0.9f, 1.0f, 0.9f,
                Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
            btnEffect.duration = 100
            btnEffect.setAnimationListener(object : Animation.AnimationListener {
                override fun onAnimationStart(animation: Animation) {}
                override fun onAnimationRepeat(animation: Animation) {}
                override fun onAnimationEnd(animation: Animation) {
                    // アニメーション完了後にやらせる処理を書く
                }
            })
            resetButton.startAnimation(btnEffect)
        }
    }

    override fun onResume() {
        playTimeCounter?.resume()
        super.onResume()
    }

    override fun onPause() {
        playTimeCounter?.pause()
        super.onPause()
    }
}

解説

スマホアプリは、電話がかかってきてバックグラウンドに遷移したり、画面ロックすることを想定すると、プレイ時間を一時停止→再開する機能が必要になりますがChronometerにそのような機能がありません。

そのため、Chronometerを使って一時停止や再開ができるプレイ時間カウンターとしてPlayTimeCounterクラスを自作します。

PlayTimeCounterクラスのインターフェースは次の通りです。

start()カウントを開始する
stop()カウントを停止する
pauseカウントを一時停止する
resumeカウントを再開する
resetカウントをリセットする
currentTime現在のプレイ時間を取得する

Chronometerの使い方

Chronometerのインターフェースは単純でやることは次の4つのみです。

  1. ベース時間としてSystemClock.elapsedRealtime()をbaseに設定
  2. start()で開始
  3. stop()で停止
  4. プレイ時間をSystemClock.elapsedRealtime() – timer.baseで取得

一時停止は、次のようにChronometerをいったん停止してプレイ時間を保存しておきます。

fun pause() {
    if (isStartPlayTimer && offsetPlayTimer == 0L) {
        timer.stop()
        offsetPlayTimer = SystemClock.elapsedRealtime() - timer.base
    }
}

再開時に、SystemClock.elapsedRealtime()から一時停止した時間を引くことでプレイ時間を再開できます。

fun resume() {
    if (isStartPlayTimer) {
        timer.base = SystemClock.elapsedRealtime() - offsetPlayTimer
        offsetPlayTimer = 0
        timer.start()
    }
}
そそたた
そそたた

ターゲットのAPIレベルによって使えるメソッドが変わってきますのでChronometerのドキュメントを確認してください。

PlayTimeCounterの使い方

ゲーム開始時にカウントを開始。

viewModel.tile.startedGame = {
    playTimeCounter?.start()
}

バックグラウンド遷移時にカウントを一時停止。

override fun onPause() {
    playTimeCounter?.pause()
    super.onPause()
}

バックグラウンドから復帰時にカウントを再開。

override fun onResume() {
    playTimeCounter?.resume()
    super.onResume()
}

ゲームオーバー時にカウントを停止。

viewModel.tile.gameOver = {
    playTimeCounter?.stop()
    resetButton.setImageBitmap(BitmapFactory.decodeResource(context?.resources, R.drawable.reset_button_gameover))
}

コメント

タイトルとURLをコピーしました