【マインスイーパー開発】9x9のタイルを描画する(その1)

Android & Kotlin

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

今回は、単純に9x9のタイルを画面に描画してみます。

動作イメージ

画面とタイルのサイズを気にせず9x9のタイルを描画します。

そそたた
そそたた

タイルが画面からはみ出していますが、拡大縮小と縦横斜めスクロールが可能なカスタムイメージビューを継承したビューに描画しているため、画面をスワイプ してスクロールすることで見えるようになります。

ソースコード

各クラスの役割をざっくり説明します。

GameFragmentのGameViewModelにタイル画像生成、タイル状態の管理を行うSquareを生成&保持し、SquareTileGameViewにてタイルを描画します。

Squareは将来的に三角形、五角形、六角形などを増やしていく想定のため、デザインパターンのFactory Methodを使用した設計にしています。

次に、9x9のタイルを描画するのに必要な箇所をざっくり説明します。

Sqareのinitializeメソッドでタイル画像の生成、タイル状態の初期化、爆弾のセットを行い、SquareTileGameViewのsetTileControllerメソッドでタイルを描画しています。

そそたた
そそたた

複雑なことをやっていないため、あとはソースコードを見てください。

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

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

enum class TileType {
    Square,
    Triangle,
    Pentagon,
    Hexagon
}
package com.sosotata.minesweeper.model

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Matrix

/**
 * タイル制御インターフェース
 */
interface TileController {
    companion object {
        fun create(type: TileType): TileController = when (type) {
            TileType.Square -> Square()
            TileType.Triangle -> TODO("Not Implemented")
            TileType.Pentagon -> TODO("Not Implemented")
            TileType.Hexagon -> TODO("Not Implemented")
        }
    }

    /** 横方向のタイル要素数 */
    var numX: Int

    /** 縦方向のタイル要素数 */
    var numY: Int

    /** タイル幅 */
    var tileWidth: Int

    /** タイル高さ */
    var tileHeight: Int

    /** 初期化処理 */
    fun initialize(context: Context)

    /** 指定したタイル画像の取得 */
    fun getTileStateImage(x: Int, y: Int): Bitmap
}
package com.sosotata.minesweeper.model

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import com.sosotata.minesweeper.LOG
import com.sosotata.minesweeper.R

/**
 * 四角タイル制御
 */
class Square() : TileController {

    /** タイル状態定義 */
    enum class TileState(var value: Int) {

        NUM0(0),
        NUM1(1),
        NUM2(2),
        NUM3(3),
        NUM4(4),
        NUM5(5),
        NUM6(6),
        NUM7(7),
        NUM8(8),
        TILE(9),
        TILE_FLAG(10),
        TILE_QUESTION(11),
        BOMB(12),
        BOMB_HIT(13),
        BOMB_NG(14),
        BOMB_OK(15);

        companion object {
            fun valueFrom(value: Int): TileState {
                return values().firstOrNull { it.value == value } ?: TILE
            }
        }
    }

    /** タイル要素  */
    data class TileElement(var state: TileState, var isBomb: Boolean)

    /** タイル画像リスト */
    private val mTileImages: MutableList<Bitmap> = mutableListOf()

    /** タイル状態管理配列 */
    private lateinit var mTiles: Array<Array<TileElement>>

    /** 爆弾数*/
    private var mNumBomb: Int = 0

    override var numX: Int = 0
    override var numY: Int = 0
    override var tileWidth: Int = 0
    override var tileHeight: Int = 0

    override fun initialize(context: Context) {
        // タイル画像を生成
        mTileImages.add(BitmapFactory.decodeResource(context.resources, R.drawable.base_square))

        // タイル数、爆弾数を設定
        numX = 9
        numY = 9
        mNumBomb = 10
        tileWidth = mTileImages[0].width
        tileHeight = mTileImages[0].height

        // タイル状態初期化
        mTiles = Array(numY) {Array(numX) {
            TileElement(TileState.TILE, false)
        } }

        // 爆弾をランダムにセット
        var count: Int = 0
        while (count < mNumBomb) {
            val x: Int = (0 until numX).random()
            val y: Int = (0 until numY).random()
            if (!mTiles[y][x].isBomb) {
                mTiles[y][x].isBomb = true
                count++
            }
        }
    }

    override fun getTileStateImage(x: Int, y: Int): Bitmap {
        return when {
            x < numX && y < numY -> mTileImages[mTiles[y][x].state.value]
            else -> {
                LOG.e("invalid param x[${x}] y[${y}]")
                mTileImages[mTiles[0][0].state.value]
            }
        }
    }
}
package com.sosotata.minesweeper.ui.main

import androidx.lifecycle.ViewModel
import com.sosotata.minesweeper.model.TileController

class GameViewModel: ViewModel() {
    lateinit var tile: TileController
}
package com.sosotata.minesweeper.ui.main

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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 kotlinx.android.synthetic.main.game_fragment.*

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

    private lateinit var viewModel: GameViewModel

    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.initialize(requireContext())
        squareView.setTileController(viewModel.tile)
    }
}
package com.sosotata.minesweeper.ui.widgets

import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import com.sosotata.minesweeper.model.TileController

/**
 * 四角タイルゲーム画面
 */
class SquareTileGameView : SosotataImageView {

    /** タイル描画用ビットマップ */
    private lateinit var mSourceBitmap: Bitmap

    /** タイル描画用キャンバス */
    private lateinit var mRenderCanvas: Canvas

    /** タイル制御 */
    private lateinit var mTile: TileController

    /**
     * コンストラクタ
     */
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {}

    /**
     * タイルを描画する
     */
    fun setTileController(tile: TileController) {
        mTile = tile
        mSourceBitmap = Bitmap.createBitmap(
            mTile.tileWidth * mTile.numX, mTile.tileHeight * mTile.numY, Bitmap.Config.ARGB_8888
        )
        mRenderCanvas = Canvas(this.mSourceBitmap)
        for (y in 0 until mTile.numY) {
            for (x in 0 until mTile.numX) {
                mRenderCanvas.drawBitmap(
                    mTile.getTileStateImage(x, y),
                    mTile.tileWidth.toFloat() * x,
                    mTile.tileHeight.toFloat() * y,
                    null
                )
            }
        }
        setImage(mSourceBitmap)
    }
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <com.sosotata.minesweeper.ui.widgets.SquareTileGameView
        android:id="@+id/squareView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
package com.sosotata.minesweeper

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.sosotata.minesweeper.ui.main.GameFragment

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)
        if (savedInstanceState == null) {
            supportFragmentManager.beginTransaction()
                    .replace(R.id.container, GameFragment.newInstance())
                    .commitNow()
        }
    }
}

コメント

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