teriyaki note

好きなものはラーメンと将棋

Kotlin でAndroid 向け将棋アプリを作る Part 1 - 将棋盤の描画など

次回

はじめに

私は将棋が好きなので、将棋に関連したAndroidアプリ開発に挑戦してみようと思います。

まずは第一歩としてタップで動かせる将棋盤を作ってみましょう。

環境としてはAndroid Studio + Kotlin を用います。

Kotlin を使うのは今回が初めてなのですが、おすすめする記事が多く見られたので挑戦してみることにしました。

どうやらJava よりも簡潔に書けるのがメリットのようです。

今回は GridView というレイアウトを用いて将棋盤を描画してみます。オセロやチェスなども同様の手順で実装できそうですね。

Android Studio のインストール方法や基本等は省きます。

環境

成果物

将棋盤を描画できた。

f:id:teriyaki398:20190309094109p:plain:w400

タップすることで駒が動かせる。

が、本当に動かせるだけ(好きなところにワープできる)。

ゲームとして成り立たせる作業はこれから詰めていきます。

事前準備

まずは将棋の駒などの素材を準備します。今回は、オリジナル感を出そうとして自作することにしました。

全く納得のいくクオリティでは無いのですが、テスト開発には十分だろうとして妥協してます。

作成した駒の画像などをres/drawable にドラッグ&ドロップで置きます。

f:id:teriyaki398:20190308200450p:plain:w350

SVG形式の画像はdrawableに、PNG形式の画像はdrawable-24というディレクトリに置かれました。

Git などでバージョン管理している人は.gitignore/app/src/main/res/drawable-v24を追記した方が良いでしょう。

今回は自作しましたが、フリーで使える素材もあるようです。

XML レイアウトを作成する

将棋盤のレイアウト

activity_main.xmlを編集し、将棋盤を作成していきます。

今回は360dp x 360dpImageViewGridView を真ん中に配置しているだけです。

ImageView には将棋盤のベースとなる画像(将来的には木目っぽいやつ)を配置し、GridView に40dp x 40dp のマスを9x9 に敷き詰めていくことにします。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">


    <ImageView
            android:layout_width="360dp"
            android:layout_height="360dp"
            app:srcCompat="@drawable/board_image"
            android:id="@+id/imageView2"
            app:layout_constraintStart_toStartOf="parent" app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent" android:contentDescription="@string/shogi_board"/>
    <GridView
            android:id="@+id/shogi_board"
            android:layout_width="360dp"
            android:layout_height="360dp"
            android:gravity="center"
            android:columnWidth="40dp"
            android:numColumns="auto_fit"
            android:verticalSpacing="0dp"
            android:horizontalSpacing="0dp"
            android:stretchMode="spacingWidthUniform"
            app:layout_constraintStart_toStartOf="parent" app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"/>

</android.support.constraint.ConstraintLayout>

f:id:teriyaki398:20190308201148p:plain:w450

マス(駒)のレイアウト

GridView に敷き詰めるもののレイアウトを定めます。

とりあえずただのマスで良いので、40dp x 40dp のサイズのImageView だけをもつレイアウトにしました。

後に記述コードで駒がある場所には駒の画像を、駒がない場所には黒枠だけついた透明な画像を表示させます。

以下のxml ファイルをres/layout/board_cell.xml として保存しました。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                             xmlns:app="http://schemas.android.com/apk/res-auto"
                                             xmlns:tools="http://schemas.android.com/tools"
                                             android:layout_width="40dp"
                                             android:layout_height="40dp">

    <ImageView
            android:layout_width="40dp"
            android:layout_height="40dp"
            app:srcCompat="@drawable/empty_cell"
            android:id="@+id/imageView"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:contentDescription="@string/none"/>
</android.support.constraint.ConstraintLayout>

GridView で画像を描画させる

作成するファイルと役割は以下の通りです。

ファイル名 役割
MainActivity.kt メインの処理を司る
BoardCell.kt GridView に敷き詰めるマスの定義
BoardCellAdapter.kt GridView にセットするAdapter
ShogiBoardController.kt 将棋盤上の挙動を定義

app/java/com.XXX.YYY/ 以下に配置しました。

BoardCell.kt

上で作成したレイアウトはImageView のみなので、こちらも画像だけを保持するもので十分でしょう。

必要になったら後から変更します。

package com.t3rry.kifuapp

class BoardCell{

    var image: Int? = null

    constructor(image: Int){
        this.image = image
    }

}

今回は画像1つだけなのでわざわざクラスを作る必要がないかもしれませんが、今後の仕様変更に備えてクラスを作成しています。

また、画像リソースはリソースID をint 型で持たせておけば良いそうです。

BoardCellAdapter.kt

初見の人はこの部分が一番難解だと思います。

私もよく分かりませんでした。

Adapter とは、デザインパターンにもあるかと思いますが、受け取ったデータを他の形式に合うように変換するようなイメージだと思います。まさにアダプターです。

今回の例でいうとGridView にBoardCell を表示できるように上手いことしてあげています。

同時にViewHolder というクラスを定義し、getView 内で使用しています。

BaseAdapter は抽象クラスなので、4つのメソッドをオーバーライドする必要があります。

package com.t3rry.kifuapp
import ...

class BoardCellAdapter : BaseAdapter {

    var context : Context? = null
    var boardCellList : ArrayList<BoardCell>

    constructor(context: Context, boardCellList: ArrayList<BoardCell>) : super() {
        this.context = context
        this.boardCellList = boardCellList
    }

    class ViewHolder(view: View){
        var imageView: ImageView = view.findViewById(R.id.cell_view)
    }

    override fun getCount(): Int {
        return boardCellList.size
    }

    override fun getItem(position: Int): Any {
        return boardCellList[position]
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }


    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val viewHolder : ViewHolder
        var convertView = convertView

        if(convertView == null){
            /* if convertView is null, inflate custom view and use it as convert view */
            convertView = LayoutInflater.from(parent?.context).inflate(R.layout.board_cell, null)

            /* create a new viewHolder instance using convert view */
            viewHolder = ViewHolder(convertView)

            convertView.tag = viewHolder
        } else {
            viewHolder = convertView.tag as ViewHolder
        }

        viewHolder.imageView.setImageResource(boardCellList[position].image!!)

        return convertView!!
    }

}

MainActivity.kt

主となる画面の挙動を記述します。

今回はあまり複雑なことはしないのでかなりシンプルに構成されています。

  • 後述するShogiBoardControllerクラスのインスタンスを生成
  • GridView にAdapter をセット
  • GridView内のアイテムがクリックされてもいいようにListener をセット
  • クリックされると専用のメソッドがコールされる

Kotlin ではimport kotlinx.android.synthetic... のように書くことで直接レイアウトにアクセスできるようです。便利ですね。

package com.t3rry.kifuapp
import ...

class MainActivity : AppCompatActivity() {

    val shogiBoardController : ShogiBoardController = ShogiBoardController(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        /* set adapter on GridView */
        shogi_board.adapter = shogiBoardController.adapter

        /* set listeners for items */
        shogi_board.setOnItemClickListener{ parent, view, position, id ->
            shogiBoardController.onClickCell(position)
        }
    }
}

ShogiBoardController.kt

このクラスは将棋の駒の挙動などに関する記述を行います。

基本的に盤面上のどのマスにどの駒が配置されているかを二次元配列で管理させています。

マスがクリックには以下の二種類の意味があります

  • 動かす駒を決める
  • 駒の移動先を決める

よって、最初にクリックされた場所を変数保存しておき、2回目のクリックのときに保存してある場所から新たにクリックされた場所に駒を移動させています。

駒の移動が発生したときにはupdateBoardメソッドを呼び出して、Adapter の状態を更新しています。

adapter.notifyDataSetChanged() をコールしないとView が更新されないことに注意しましょう。

package com.t3rry.kifuapp

import android.content.Context


class ShogiBoardController {
    /*
    将棋盤の挙動に関する動作を記述
    */


    /* メンバ変数
        boardMatrix : 盤面の状態を文字列で管理する
        currentSelectedPos : クリックされたImageView のindex を保存する
        boardCellList : GridView に表示するBoardCell のリスト
        adapter : GridView に紐づいているAdapter
    */
    var boardMatrix = Array(9){arrayOfNulls<String>(9)}
    var currentSelectedPos : Int = -1
    var boardCellList : ArrayList<BoardCell> = arrayListOf()
    var adapter : BoardCellAdapter

    /*
        pieceDict : 駒を表す文字列 -> BoardCell(R.drawable."駒") の連想配列
    */
    val pieceDict = mapOf(
        "s_fu"          to BoardCell(R.drawable.s_fu),
        "s_to"          to BoardCell(R.drawable.s_to),
        "s_kyou"        to BoardCell(R.drawable.s_kyou),
        ...
        "g_uma"         to BoardCell(R.drawable.g_uma),
        "g_gyoku"       to BoardCell(R.drawable.g_gyoku),
        "empty"         to BoardCell(R.drawable.empty_cell)
    )


    /* コンストラクタ */
    constructor(context: Context){

        /* 駒の配置を初期化 */
        this.initPosition()

        /* boardCellList を初期化 */
        for(i in 0..8){
            for(j in 0..8){
                boardCellList.add(pieceDict[boardMatrix[i][j]]!!)
            }
        }

        /* adapter の初期化 */
        adapter = BoardCellAdapter(context, boardCellList)
    }


    /* 盤面のマスがクリックされたときの処理
        clickedPos : クリックされたGridView のindex: Int
    */
    fun onClickCell(clickedPos : Int){
        when(currentSelectedPos){
            -1 -> {
                currentSelectedPos = clickedPos
            }
            else -> {
                var targetPieceName : String? = boardMatrix[currentSelectedPos/9][currentSelectedPos%9]
                boardMatrix[clickedPos/9][clickedPos%9] = targetPieceName
                boardMatrix[currentSelectedPos/9][currentSelectedPos%9] = "empty"
                updateBoard()

                currentSelectedPos = -1
            }
        }
    }


    /* 駒の配置を初期化する */
    fun initPosition(){
        for(i in 0..8){
            for(j in 0..8){
                boardMatrix[i][j] = "empty"
            }
        }
        boardMatrix[0][0] = "g_kyou"
        boardMatrix[0][1] = "g_kei"
        ...
        boardMatrix[8][7] = "s_kei"
        boardMatrix[8][8] = "s_kyou"
    }


    /* 盤面をBoardMatrix の状態に更新する */
    fun updateBoard(){

        for(i in 0..8){
            for(j in 0..8){
                boardCellList[9*i + j] = pieceDict[boardMatrix[i][j]]!!
            }
        }
        adapter.notifyDataSetChanged()
    }


}

所感

将棋盤を描画するなんて簡単だろうと思っていたのですが、いざやろうとしてみると知らないことが多くて苦労しました。

そもそも、こういったものはどのように描画するのが一般的なのでしょう。

よく分かりませんが、とりあえず動いているので良しとします。

Kotlin については初めて触ったのですが、かなり使いやすい言語だと感じました。

特にwhen文などは直感的で分かりやすいと思います。

また、今回はただタップした場所に駒が動く。というだけのものなので、様々な制約などを加える必要があります。

参考にしたもの

Kotlin GridView example: Show List of Items on Grid | Android - grokonez

Android ListViewとAdapterとViewHolder - うちなー えんじにあ ぶろぐ