Android & Kotlinの環境でマインスイーパーを開発する方法を説明します。
今回は、単純に9x9のタイルを画面に描画してみます。
動作イメージ
画面とタイルのサイズを気にせず9x9のタイルを描画します。
そそたた
タイルが画面からはみ出していますが、拡大縮小と縦横斜めスクロールが可能なカスタムイメージビューを継承したビューに描画しているため、画面をスワイプ してスクロールすることで見えるようになります。
ソースコード
各クラスの役割をざっくり説明します。
GameFragmentのGameViewModelにタイル画像生成、タイル状態の管理を行うSquareを生成&保持し、SquareTileGameViewにてタイルを描画します。
Squareは将来的に三角形、五角形、六角形などを増やしていく想定のため、デザインパターンのFactory Methodを使用した設計にしています。
次に、9x9のタイルを描画するのに必要な箇所をざっくり説明します。
Sqareのinitializeメソッドでタイル画像の生成、タイル状態の初期化、爆弾のセットを行い、SquareTileGameViewのsetTileControllerメソッドでタイルを描画しています。
そそたた
複雑なことをやっていないため、あとはソースコードを見てください。
開発環境は次の通りです。
PC | MacBook Pro(2016年モデル) |
IDE | Android Studio 4.0.1 |
Android SDK | minSdkVersion 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()
}
}
}
コメント