カタンとは
『カタンの開拓者たち』(カタンのかいたくしゃたち、Die Siedler von Catan)、または『カタン』は、ドイツで生まれたボードゲームである。1995年にコスモス社から販売された。
大航海時代に発見された無人島を複数の入植者たちが開拓していき、もっとも繁栄したプレイヤーが勝利するという内容のボードゲーム。
なぜアプリを作ろうと思ったか
###複雑な得点計算ルール
カタンはいろいろなルールがある、けっこう複雑なボードゲームです。
複雑な点の1つに、その得点計算ルールがあげられます。
- 「都市」を作ると2点
- 「開拓地」を作ると1点
- 連続した5本以上の「街道」を作成したプレイヤーのうち、最も長い距離の連続した街道を作成しているプレイヤーに2点(最長交易路)
- 「騎士カード」の使用枚数が3枚以上のプレイヤーのうち、最も使用枚数が多いプレイヤーに2点(最大騎士力)
- 「最長交易路」「最大騎士力」は、より長い街道を持つ/より多く騎士カードを使用したプレイヤーが現れた時点で、該当するプレイヤーが入れ替わる
- 「発展カード」の一種「勝利ポイントカード」を獲得すると1点
###どこかで起きているかもしれない会話
A「今、誰が点数1番高いっけ?」
B「Cが1番高いよ!」
C「いや、最長交易路忘れてるじゃん。最長交易路の2点足してDが1番だよ」
D「あれ?いつの間にか俺よりBの方が交易路長くなってね?」
A「ほんと?1,2,3,4・・・ほんとだ!じゃあDじゃなくてBに+2点で、Bは6点だね!」
B「違うよ、私は7点だよ」
A「あれ?ああ、都市なのに開拓地だと思って1点で計算しちゃってた」
Androidアプリを作ろう
- 各プレイヤーの情報を記録し、得点を計算して表示するアプリがあれば、上記のような会話をすることがなく、プレイに集中できると考えました。
- ちょうどスマホアプリを勉強しようと思っていた時期だったので、まず最初の練習として、このカタンの得点計算アプリの開発を試みました。
- 自分はAndroidユーザーなので、Androidアプリを選択しました。
完成したアプリ
UI
機能
- プレイヤーの情報を記録する
- プレイヤー名
- 都市、開拓地の数
- 連続する最長の街道(交易路)の長さ
- 騎士カードの使用枚数
- 発展カードのポイント数
- 得点を計算する
- 都市:2点
- 開拓地:1点
- 最長交易路:5つ以上の最も長い交易路を持つプレイヤーに2点
- 最大騎士力:3回以上の最も多く騎士カードを使用したプレイヤーに2点
- 発展カード:1点
- ゲームの状況を表示する
- プレイヤーの情報
- プレイヤーの得点
- 最長交易路、最大騎士力
開発過程
パッケージ構成
- こちらのサイトを参考にさせていただき、パッケージ構成を決めました。
- 当初、パッケージ名をcom.exampleにしてしまっていたら、Google Playにリリース申請をする際にエラーになってしまったので、こちらのサイトを参考にパッケージ名を変更しました。
苦労したこと①:同じ部品を複数個入れ子にするレイアウトについて
このアプリのレイアウトは
・メイン画面に、最大4人の「プレイヤー情報」を表示する
・1プレイヤーにつき、都市・開拓地・最長交易路・騎士力・発展の5つの「数情報」を表示する
という入れ子構造になっています。
入れ子にしたい部品毎にxmlを分けて、includeタグを使い、親となるxmlに子のxmlを書き加えていきました。
メイン画面:4人分のプレイヤー情報を子レイアウトに持つ
<include
android:id="@+id/player1"
layout="@layout/player_info"
(省略)
app:layout_constraintTop_toTopOf="parent" />
<include
android:id="@+id/player2"
layout="@layout/player_info"
(省略)
app:layout_constraintTop_toBottomOf="@id/player1" />
<include
android:id="@+id/player3"
layout="@layout/player_info"
(省略)
app:layout_constraintTop_toBottomOf="@id/player2" />
<include
android:id="@+id/player4"
layout="@layout/player_info"
(省略)
app:layout_constraintTop_toBottomOf="@id/player3" />
プレイヤー情報:5つの数情報の領域を子レイアウトに持つ
<include
android:id="@+id/cities"
layout="@layout/single_count"
(省略)
app:layout_constraintStart_toEndOf="@+id/player_name"
app:layout_constraintEnd_toStartOf="@+id/settlements"/>
<include
android:id="@+id/settlements"
layout="@layout/single_count"
(省略)
app:layout_constraintStart_toEndOf="@+id/cities"
app:layout_constraintEnd_toStartOf="@+id/roads"/>
<include
android:id="@+id/roads"
layout="@layout/single_count"
(省略)
app:layout_constraintStart_toEndOf="@+id/settlements"
app:layout_constraintEnd_toStartOf="@+id/knights"/>
<include
android:id="@+id/knights"
layout="@layout/single_count"
(省略)
app:layout_constraintStart_toEndOf="@+id/roads"
app:layout_constraintEnd_toStartOf="@+id/develops"/>
<include
android:id="@+id/develops"
layout="@layout/single_count"
(省略)
app:layout_constraintStart_toEndOf="@+id/knights"
app:layout_constraintEnd_toEndOf="parent"/>
数情報:タイトル、数、+ボタン、-ボタン
<TextView
android:id="@+id/count_title"
(省略)
app:layout_constraintWidth_min="45dp"/>
<TextView
android:id="@+id/count"
(省略)
app:layout_constraintVertical_weight="3"/>
<Button
android:id="@+id/up"
(省略)
app:layout_constraintDimensionRatio="W,1:1"/>
<Button
android:id="@+id/down"
(省略)
app:layout_constraintDimensionRatio="W,1:1"/>
ここで困った問題が発生しました。
どのプレイヤーのどのボタンを押しても、プレイヤー1の都市の数ばかり更新されてしまいます。
同じ画面に同じ要素を複数個includeしているため、ボタンのIDが一意にならず特定できないことが原因でした。
最終的に、押されたボタンを特定するときに、ボタンのIDだけではなく、ボタンの親要素のIDも参照するように記述することで解決しました。
//実際の実装ではリストを使ってfor文にしたりもしていますが、イメージはこんな感じ
//例1
if (view.parent.parent = findViewById(R.id.player1)) {
//処理
}
//例2
if (view = findViewById<View>(R.id.player1).findViewById<View>(R.id.cities).findViewById<Button>(R.id.up)){
//処理
}
参考
一つのlayout xmlを複数includeした時にViewへの参照を取得する方法
よく解説サイトには、includeタグを使う際はmergeタグも同時に使うようにと記載があります。
しかし、mergeタグを使ってしまうと、階層構造ができず、一番親のxmlに全てフラットに書かれているようになる(親要素という概念が無くなる)ため、親要素のIDによって押されたボタンを区別することができなくなってしまうようです。
苦労した点②:ボタンの色を、xmlではなくkotlin側で指定
カタンには、赤・オレンジ・青・クリーム色の4色のコマがあります。それぞれのプレイヤーに別々の色のコマが渡されるので、赤のコマは誰々、オレンジのコマは誰々というように、コマの色でプレイヤーの区別が付きます。
そこで、それぞれのプレイヤーの領域の色をコマの色に対応させるUIを考えました。
要素の色などは、xml側で指定するのが一般的なようですが、
上で述べたように、同じ部品を別々のプレイヤーで使い回しているので、xml側で色を指定することはできず、kotlin側でプレイヤーに応じた異なる色を、同じ部品に対して指定しなくてはいけません。
//kotlin側でテキストカラーを指定する
view.setTextColor(red)
//kotlin側で背景色を指定する
view.setBackgroundColor(red)
このあたりは簡単に見つかったのですが、
ボタンのデザインをプレイヤー毎に変える方法がなかなか見つからず苦労しました
結論として
①ボタンを定義するxmlファイルをres/drawable/button.xmlとして作成する
②GradientDrawableクラスとしてそのxmlを読み込む
③読み込んだクラスに対して色などを変更する
④view.backgroundに、読み込んだボタンのクラスを指定する
という方法に辿り着きました。
//ボタンを定義
val btn: GradientDrawable =
ResourcesCompat.getDrawable(
resources,
R.drawable.button,
null
) as GradientDrawable
btn.setColor(pink)
btn.setStroke(2, red)
//+ボタン
val upView =
findViewById<View>(R.id.player1).findViewById<View>(R.id.cities).findViewById<TextView>(R.id.up)
upView.setTextColor(red)
upView.background = btn
//-ボタン
val downView =
findViewById<View>(R.id.player1).findViewById<View>(R.id.cities).findViewById<TextView>(R.id.down)
downView.setTextColor(red)
downView.background = btn
苦労したこと③:いろいろな画面サイズにレイアウトを対応させる
当初、それぞれの部品の大きさをハードコーディングしてしまっていたせいで、レイアウト崩れが発生してしまいました。
Android Studioの公式を見ると、いろいろな画面サイズでレイアウトが崩れないための方法がいろいろ書かれていたのですが・・・
レイアウト サイズのハード コーディングを避ける
さまざまな画面サイズに合わせて調整できる柔軟なレイアウトにするには、ほとんどのビュー コンポーネントの幅と高さに、ハードコードされたサイズではなく "wrap_content" と "match_parent" を使用します。
これがよく分かりませんでした。wrap_contentは、コンテンツの大きさが大きければ、それに合わせて無限にビューの大きさが広がってしまい、画面からはみ出してしまいます。
「画面に占めるビューの大きさ(比率)を固定にして、コンテンツの大きさを、ビューの大きさに合わせて変化させる」ことが重要に思いますし、その点でwrap_contentはその逆・・・「コンテンツの大きさに合わせてビューの大きさを変える」という仕様になっており、画面レイアウトが崩れてしまうと感じてしまいます。(間違っていたら教えてください)
結論として、いろいろな画面サイズでレイアウトが崩れないようにするために以下の方法を用いました。
公式では紹介されていませんでしたが、比率の指定にとても助けられました。
- 部品サイズのハードコーディングは極力避ける
- 今回の開発では、一部(RESET,QUITボタンの大きさやマージン指定)のみハードコーディングを残しました。
-
wrap_contentの使用も極力避ける
- 一部を除きwrap_contentをやめました
- できるだけwidth=0dp,height=0dpとし、他の要素との制約や比率を使ってどんな画面サイズでも応用できる表現で、位置と大きさを表現する
- 他の要素との制約
- app:layout_constraintStart_toStartOf
- app:layout_constraintStart_toEndOf
- app:layout_constraintEnd_toEndOf
- app:layout_constraintEnd_toStartOf
- app:layout_constraintTop_toTopOf
- app:layout_constraintTop_toBottomOf
- app:layout_constraintBottom_toTopOf
- app:layout_constraintBottom_toBottomOf
- 比率
- app:layout_constraintHorizontal_weight
- app:layout_constraintVeritical_weight
- app:layout_constraintWidth_percent
- app:layout_constraintVertical_bias
- app:layout_constraintDimensionRatio
- 他の要素との制約
- テキストサイズもハードコーディングを極力避け、app:autoSizeTextType="uniform"を使用する
どこまで動作確認を行うか?
調べたところ、360dp*640dpの画面サイズが多数を占めていることが分かりました。
2016年発売Android端末のdp解像度まとめ
Android端末の画面サイズと解像度
画面幅のシェアの統計情報
また、幅320-410dpに対応できていれば良いのではないかという記載が多数見つかりました。
幅は320-410dpの範囲で動作確認をすれば良いとして、高さはどの範囲が動作確認の目安なのか?と疑問に思いましたが、それについての記載は見つけられませんでした。
多くのアプリは、高さに関しては縦スクロールを使って表示できれば良いので、高さについては特に制約として考えていないのかもしれません。
ですが、今回開発したアプリは、スクロールさせることなく、全てのプレイヤーの得点を同時に画面に映して、ユーザーが確認できるようにしたかったので、縦スクロールは使えません。結局、320-410dpにほぼ対応するような、533-845dpの高さ範囲で動作確認を行いました。
まとめ
今回学んだこと
- 基本的な画面の作り方(Constraint Layout)
- ボタン押下による処理の実装
- 画面遷移の実装
- 画面回転の実装
- 戻るボタンの実装
- ダイアログの実装
- 端末サイズに依存しない画面レイアウト作成
- Google Play へのリリース
今回やらなかったこと
- サーバーサイドの処理
- DB(Room・Realmとか)
- 非同期通信
- カメラやBluetoothや位置情報など「スマホらしい」機能の利用
- Twitterのような、縦スクロールのあるデザイン(RecyclerViewを使うようなデザイン)
最後に
ニッチなアプリではありますが、「自分の作ったものをリリースする」という経験を初めてできた点は良かったと考えています。
とても簡単なアプリではありますが、Androidアプリの第一歩を踏み出せたと感じています。