Android Studio v3.1.x. & kotlin_version = '1.2.30+' にアップグレードするとRealmが動かない問題
kotlin_version 1.2.30からビルドの仕方が変わったのかRealmのビルド時にエラーが出るようになった。
Original kapt is deprecated. Please add "apply plugin: 'kotlin-kapt'" to your build.gradle.
ので言われたようにプラグインを追加する
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'realm-android'
とやっぱりエラーが出る。
\build\generated\source\kaptKotlin\release
3rd-party Gradle plug-ins may be the cause
ので.gradleのキャッシュを消す。
C:\Users\ユーザ名\.gradle\caches
更にCleanする
とやっと動くようになった。
gradleになってもやっぱりビルドスクリプトは動かないときは動かないんだなと実感。
あとこの現象は別のプラグインでも結構起きてたらしい。
Java屋にもわかるScalaのImplicit
KotlinともFEHとも関係ありませんがScalaMatsuri 2018でImplicitの発表を見てようやく理解できたのでJava使いから見たImplicitの話を書きます。なお私はScalaは素人同然なので記事は修正・削除される可能性が大いにあります。
これもScalaMatsuriの感想ブログに入るんでしょうか…?
Interface実装しまくるとメソッドが増えすぎる問題
javaではそのオブジェクトをどう扱えるかをInterfaceで宣言し、それのメソッドを実装します。
例えばあるクラスXのインスタンスx1とx2が比較可能である場合は、XにComparableインタフェースを実装します。
そしてこのComparableなオブジェクトはソート可能になるのでCollection.sort()などに渡すことができます。
public class X implements Comparable<X> {
public int i;
public X(int i) {
this.i = i;
}
@Override
public int compareTo( X x) {
return this.i - x.i;
}
@Test
public void testSort(){
List<X> list = new ArrayList<X>();
list.add(new X(3));
list.add(new X(1));
list.add(new X(0));
list.add(new X(2));
Collections.sort(list);
}
}
Interfaceはどう扱えるかの宣言なので、Aのように扱えるとかBのようにも扱えるとかいくらでも実装することができます。
しかしむやみに実装すると、Interfaceのメソッドを大量に実装することにもなります。
public interface A {
void a1();
void a2();
}
public class X implements Comparable<X>, A, B, C {
public int i;
public X(int i) {
this.i = i;
}
@Override
public int compareTo( X x) {
return this.i - x.i;
}
@Override
public void a1() {
}
@Override
public void a2() {
}
@Override
public void b1() {
}
@Override
public void b2() {
}
@Override
public void c1() {
}
@Override
public void c2() {
}
}
これはよろしくありません。単にメソッドが多いだけで見通しが悪くなりますし、クラス当たりのメソッド数が規約で制限されていたらレビュアーに怒られるかもしれません。それに実のところこれらのメソッドはあまり関連していないので同じ場所に書くメリットはあんまりありません。
そもそもソートするためにComparableを実装したということはソートの必要が無ければ実装しなかった、つまりそのオブジェクトがなんであるかの本質とは割と関係ないということです。
クラスは「それは何であるか」であり、Interface(型)は「それを何として扱えるか」であり宣言は同じところに書くことで見やすくなりますが実装のほうは混ぜるメリットはあまりありません。
クラスを分割する
そこでクラスを分割することを考えます。Comparableなどのインタフェースを満足させるためのメソッドは元オブジェクトのラッパとなるクラスを作れば済むことが多いです。
このComparableをImplementしたXを仮にComparableXクラスとし、XからtoComparableX()できるようにします。
public class ComparableX implements Comparable<ComparableX> {
public X x;
public ComparableX(X x) {
this.x = x;
}
@Override
public int compareTo(ComparableX comparableX) {
return this.x.i - comparableX.x.i;
}
}
public class X {
public int i;
public X(int i) {
this.i = i;
}
public ComparableX toComparableX() {
return new ComparableX(this);
}
public A toA() {
return new XinA(this);// class XinA implements Aというラッパを作ったとする。
}
public B toB() {
return new XinB(this);
}
public C toC() {
return new XinC(this);
}
}
ソートしたいときはこのComparableXのリストを作ってソートし、中身のXを取り出せばいいわけですね。
同様にほかのインタフェースを持ったクラスも定義することができます。
これで見通しは良くなりましたね。
実際にはtoA()とかtoB()を書くよりはラッパ側にコンストラクタやファクトリを書くべきでしょう。
class A {
public A(X x){}
public static A getInstance(X x){}
}
変換コードと言うのは変換先が増えるものです。しかし変換先が増えるのはXの問題ではなく変換先を要求する他のクラス・コードの問題です。AだのBだのと変換先が増えるたびにXを変更するのは不合理と言えます。
分割するとそのまま扱えなくなる
ですが、メソッドやコンストラクタ、ファクトリを挟むことによりXをそのまま扱うことができないという問題が発生してしまいます。
A xA= x.toA()
aFnc(xA)
aFnc(new A(x))
aFnc(A.getInstance(x))
//aFnc(X)と直接書けたらいいのに…
また、もし同じ型のオブジェクトを返すメソッドが複数存在した場合にどちらを使うべきか判断できなくなるかもしれません。
class X{
//どちらを使うべきなんだ!?
public A toWonderfulA(){}
public A toGratestA(){}
}
逆に名前がぶつかる可能性もありますし正直言うともとになるインタフェースごとに分けて書きたいくらいです。
そこでJavaでは実現できない妄想として、同じクラスをImplementしているインタフェースごとに分割して書ける言語というものを考えてみましょう。
public class X {
public int i;
public X(int i) {
this.i = i;
}
}
public class X implements Comparable<X> {
public int compareTo(X x) {
return this.i - x.i;
}
}
public class X implements A {
public void a1() {
}
public void a2() {
}
}
とても見通しが良いかとおもいます。
もちろん、このコードはコンパイルできません。同じクラスであることを利用してコンパイラが気を利かせてマージしてくれたっていいと思うんですけどね。
Scalaならできます
ですがScalaは似たようなコードを書くことができます。そう、Implicitです。
case class X(i: Int) {
}
//Interfaceみたいなもの
trait A[T] {
def a1():Int
def a2():Int
}
object X {
// Implicitで「Xに別の型が要求されたとき」のコードを書くことができる。これは単にXから取り出せる値を返している。
implicit def xToInt(x:X): Int = x.i
// class X { toOrderingX(){return new Ordering()}}と同じ.Xが直接バインドされないのでcomparableというよりcomparator。sortedでこのOrdering.compare()にリストの要素Xを二つ渡して比較することでソートする
implicit val toOrderingX: Ordering[X] = new Ordering[X] {
override def compare(x: X, y: X): Int = x.i - y.i
}
// 普通のコードからはclass X implimentes Ordering、またはOrderingコンパレータがあるXのように見える
val xList = List(new X(3), new X(1), new X(2), new X(0))
val sortedList = xList.sorted
// class X { toA(){return new A(this)}}と同じ。直接引数xをバインドすることもできるけど型クラスっぽくないからかサンプルコードとして見たことが無い。
implicit def xToA(x:X): A[X] = new A[X] {
def a1()=x.i
def a2(){}
}
// class X implimentes A に見える…けど単にnew A(new X(1))として動いると考えるほうが簡単かな?
aFnc(new X(1))
}
Implicitは暗黙の型変換と呼ばれますがJava屋から見れば後付けインタフェースみたいなものです。
クラスとインタフェースは同じように型として扱えますが本来はちょっと違うものです。
このように、クラスがどういうものかと外から扱うときにどう扱えばいいかを分離することができるのでよりシンプルなモデルを書くには有効でしょう。
implicitはクラスと型の間に存在するものである
あるクラスのメソッドはそのクラスのインスタンスを操作対象にしたコードブロックです。
なら同じインスタンスを対象にするコードはそのクラスにまとめて書いたほうが管理しやすいのではないか?というのがオブジェクト指向の基本的な考えの一つです。
//関数はどこに置くこともできるので自由だが管理しにくい
def f(x : X) : Y = ...
def g(x : X) : Y = ...
//メソッドはクラス・インスタンスと結びついたコードブロック。Xがthisとして渡されてるように動く
class X {
def f() = ...
def g() = ...
}
ではImplicitはというと、ある「クラスが別の型を要求されている関係」に結びついたコードブロックです。
先ほどJavaで妄想として書いたコードが実際に動くようなもんですね。
//Implicitは「XがA型を要求されたとき」に結びつく(コンパイル通る
implicit def xToA(x:X): A[X] = new A[X] {
def a1()=...
def a2(){}
}
DDDとの関連
ドメインモデルはドメイン以外の何にも依存しないべきです。これは他のレイヤに依存しないようにするというのが基本的な実装ですが、逆に他のレイヤから依存されるコードをできるだけ避けるというのも必要かと思います。
例えばソートを想定しないドメインモデルが有ったとして、画面に表示するときにソートできると便利だからと言う理由でComparableを実装するべきでしょうか?便利と言うだけでモデルにコードを書き足していくとモデルはその純粋さを失います。Implicitを使うことでよりドメインに絞ったコードが書けるかもしれません。
また、ドメインモデルはできるだけ統一したいところですが別のコンテキストではまた別のモデルとして設計されることがあります。
例えば販売に関連するあるオブジェクトがあるとします。このオブジェクトは売るときは商品であるが店舗まで運搬するときは単なる貨物であり、買った側は売る側の都合とは関係なく使いたいように使うでしょう。つまりコンテキストによってそのオブジェクトは異なるという事です。
書籍などではコンバータを書いてドメインモデルを変換するように書いてありますが、コンバータというのはその立ち位置が不安定で同じようなものが複数作成されることなどは珍しくありませんし、「コンバータはモデルの一部か?」などの不毛な論争が起きることもあります。
Implicitを使うことでコンバータをコンパイラに認識させることができ、管理できる可能性があります。(現状ではあまり勧められる使い方ではなさそうですが。)
Implicitの問題点
どこに書いてあるのかどこに書くべきなのかわかりにくい
メソッドはクラスと深く結びついた関数で少なくともXと物理的に同じ場所にあります。インタフェースもほとんどの言語ではクラスの横で宣言するので宣言は同じ場所にあります。
Implicitはそれを破壊します。単にクラスの外側で宣言するのでクラスと同じ場所に書いてあるとは限らないというだけの話ですが。XがA型を要求されたときのコードはXと同じ場所にあるべきか?Aと同じ場所にあるべきか?XもAも既存クラスで同じ場所に書けないときはどこに書くべきか?これは難しく深刻な問題です。
というのもクラスは「それは何であるか」の記述ですが、「それは何であるか」について書く伝統・指針はありますが「それがアレとして認識されてるときに何であるか」を書く指針は無いからです。
関係は自明でなければいけないが自明であるか判断がつかない
例えばIntがFloatとして振舞うことを要求された場合、IntをFloatにできるのは自明でしょう。一方、FloatをIntにできるかは自明ではありません。
多くの言語は端数を切り捨てていますが四捨五入など他の丸め方もあります。
あるデータを別の型のデータに変換するときに普通は便利かどうかで考える一方で自明であるかを考えることはあまりないので想定外の変換をしてしまうことは珍しくありません。
暗黙の型変換という名前が悪い
Implicitを直訳すると暗黙になるのでしょうがないのですが
やりたいことは型変換をコンパイラに知らせること、つまり宣言なので宣言的型変換とかそういう名前にするべきでした。型変換というのもちょっと疑問です。「クラスと要求されてる型のギャップを埋める何か」は型変換というんでしょうかね?とはいえ私はScalaにも英語にも詳しくないのでこれは単なる言いがかりでしょう。
感想
Implicitはコンセプトは正しくコードに自由度を与えられますが、悪用もしやすく正しく書くための指針が足りない状態という印象です。
ゲーム(FEH)におけるダメージ管理で学ぶ増やせるメソッド設計
前置き
あるオブジェクトのメソッドはそのオブジェクトを操作対象にしたコードブロックである、という話をしました。↓の二つは同じオブジェクトを対象にするときは等価です。
class A (var x : Int){
fun incX() = x++
val incX:(A)->Int = {a->a.x++}
}
そして、fun incXはAのメソッドですから当然Aに依存する操作であり、このincXはclass Aの外側に出しても「たまたま物理的にAの外にある」Aに依存する操作です。今回はこれを利用して、
この計算式すべてを記述すると同時に、この計算式自体をまるっと交換できる構造を目指します。
各項目の詳細とそれが攻撃側・被害側のどちらに依存するかの説明をしますが読み飛ばしてもらって構いません。
Atk:攻撃側の攻撃能力値
Eff:攻撃側の武器と被害側の兵/武器種による特効
Adv:攻撃側と被害側の武器(色)相性による補正(お互いのスキルにより変動あり)
SpcStat:攻撃側の奥義発動時に参照する攻撃側能力値
SpcMod:攻撃側の奥義発動時の能力値倍率
Mit:攻撃側の武器と被害側の能力値によるダメージ軽減
MitMod:攻撃側の奥義によるダメージ増加と被害側地形によるダメージ軽減
OffMult:攻撃側の奥義によるダメージ増加と武器によるダメージ減少
OffFlat:攻撃側のスキル一般による追加ダメージ
DefMult:被害側奥義・スキルによるダメージ軽減
DefFlat:被害側スキル一般によるダメージ軽減
えらいことになってしまいましたがまずはこれを前回に倣って攻撃の対象にダメージを与えるメソッドとして書き、攻撃側と被害側を分離しましょう。
攻撃側と被害側を分離する
この計算式で青いところが攻撃側のパラメータによる部分、赤いところが被害側のパラメータによる部分、紫部分が両方にかかわるところです。
まずは被害側にダメージを与えるもの(メッセージ・もしくはアフォーダンス)があり、被害側はそれを受け取って自分のHPを減らすことにしましょう。
計算式中の赤いところは自分(被害側)だけに依存するのでこれはダメージではなく自分側に書けます。
data class Hero(var hp: Int, var atk: Int, var spd: Int, var def: Int, var res: Int, var weapon: Weapon, var special: Special) {
fun damage(damage: Damage) {
val dealed = damage.dealValue(this)
val preventedDamage = prevent(dealed)//prevent()内で計算して減算するがダメージに依存しないので省略
hp = if (hp > preventedDamage) hp - preventedDamage else 0
}
}
class Damage(/* これから書く攻撃側から渡すパラメータ*/) {
fun dealValue(target: Hero): Int {/* これから書くダメージを計算する部分*/
}
}
逆にDamageクラス側に攻撃側からパラメータを受け取って計算する処理を書きます。
この時に厄介なのが「図の紫の部分は両方に依存する処理」ということと「SpcStatは参照パラメータが奥義に依存する・MitMod/OffMultは数値が奥義に依存する」
という点です。
まずは両方に依存する処理を分離しましょう.まずは基本ダメージとなるこの部分です。
val atkEffAdv = atk*eff+atk*eff*adv
atkは攻撃側の能力値なのでシンプルですが、effとadvは攻撃側と被害側の色やスキルで変わってしまうのでこれはもう分離できません。
攻撃側にatkEffAdvメソッドまたはコードブロックを作って被害側を引数として渡すしかありません。
さいわい、このメソッドは攻撃側と被害側を参照して値を計算するだけなので、両者とも状態が変わりません。
いわば、両者を引数として受け取る関数のようなものです。
実装は後で考えるとしてダメージとの関係を関数として書きましょう。Mitは前回書いたように対象を参照する武器の関数として書けます。
data class Hero(var hp: Int, var atk: Int, var spd: Int, var def: Int, var res: Int, var weapon: Weapon, var special: Special) {
//メソッドの場合
fun atkEffAdv(target: Hero) = atk * eff(target) + atk * eff(target) * adv(target)
//コードブロックの場合
val atkEffAdvFnc: (Hero) -> Int = { target -> atk * eff(target) + atk * eff(target) * adv(target) }
}
class Damage(val source: Hero) {
fun dealValue(target: Hero): Int = source.atkEffAdv(target) - source.weapon.selectPreventParam(target)
}
メソッドではなくコードブロックにした場合はダメージ計算式から攻撃側への参照を除去することができます。
このダメージクラスは被害側に依存するものですが攻撃側に依存するものではないのでこれが正しい形ですが最初からこの形で書くのは難しいのであまり気にしないほうが良いでしょう。
なお、攻撃側の参照を取り除くことで攻撃側と被害側の取り違えを防ぐ事が出来ます。
この攻撃側と被害側の取り違えはRPGではメジャーなバグで、特に命中率・回避率のあるゲームでは体感しにくいためか回避率を上げる魔法を使ったら敵の回避率が上がるなんてことがしょっちゅうあります。
class Damage(val atkEffAdvFnc: (Hero) -> Int, val weapon: Weapon) {
fun dealValue(target: Hero): Int = atkEffAdvFnc(target) - weapon.selectPreventParam(target)
}
本題:奥義は色々あるので交換・追加を考慮する必要がある
残りは奥義によって計算される部分です。
特に奥義は大きく分けて「特定の能力値のn%追加ダメージ」「最終的なダメージ倍率増加」「被害側の能力値によるダメージ軽減をn%減らす」とパターンが豊富です。
これをwhenで書くと以下のようになります。
val spcStatSpcMod = when (special){
Special.緋炎->hero.def * 0.5f
Special.華炎->hero.def * 0.8f
Special.氷華->hero.res * 0.8f
else -> 0
}
val mitMod = when (special){
Special.月虹-> -0.3f
Special.月光-> -0.5f
else -> 0
}
val offMult = when (special){
Special.凶星-> 1.5f
Special.流星-> 2.5f
else -> 1
}
val damage = (atkEffAdv + spcStatSpcMod - (mit + mit * mitMod) ) * offMult + offFlat
問題はこのダメージを変える奥義が20以上あること、たまに増える事、どう増えるかが予測できない事です。
速さの40%の分だけ追加ダメージ、なんて奥義追加するのだったらwhenを1行追加するだけで済みますが
Special.剣姫の流星->hero.spd * 0.4f
奥義が増えるたびにソースを修正していては間違えて既存の奥義を上書きしてしまったりします。
val mitMod = when (hero.skill){
Special.月虹-> -0.3f
Special.天空-> -0.5f //コピペして天空を追加したつもりが月光に上書きして消してしまった!
else -> 0f
}
奥義にダメージ計算式をメソッドとして持たせる
奥義によってダメージが変わるのですから奥義にダメージ計算式を持たせてしまいましょう。定数に直接書いてしまいます。
interface BaseSpecial {
fun damage(source: Hero, atkEffAdv: Int, mit: Int, offFlat: Int) = atkEffAdv - mit + offFlat
}
enum class Special: BaseSpecial {
通常攻撃,
緋炎 {
override fun damage(source: Hero, atkEffAdv: Int, mit: Int, offFlat: Int) = atkEffAdv + source.def * 5 / 10 - mit + offFlat
},
華炎 {
override fun damage(source: Hero, atkEffAdv: Int, mit: Int, offFlat: Int) = atkEffAdv + source.def * 8 / 10 - mit + offFlat
},
流星 {
override fun damage(source: Hero, atkEffAdv: Int, mit: Int, offFlat: Int) = (atkEffAdv - mit) * 25 / 10 + offFlat
},
//奥義が増えたらここに追加していけばいい
}
class Damage(val source: Hero, val offFlat: Int) {
fun dealValue(target: Hero): Int = source.special.damage(source, source.atkEffAdv(target), source.weapon.selectPreventParam(target), offFlat)
}
奥義は発生しなければ通常攻撃であり、また通常攻撃とダメージが変わらない奥義もあるのでインタフェースにデフォルトのメソッドとして通常攻撃のダメージ計算式を書き、それを奥義側でオーバーライドします。
奥義が増えたらenumが増えますが既存のコードに手を入れる危険が無くenumとwhenを両方増やすより楽で、コピペをミスった場合はほぼコンパイルエラーになるので安心です。
共通項を括りだしていく
これでも十分役を果たしますが、MitModをコードブロックにしたようにSpcStat * SpcModもコードブロックになりますし、 ダメージ倍率など数値が違うだけの物はまとめることができます。Enumなので初期化パラメータとして渡してしまいましょう。
enum class Special(val spcStatSpcMod: (Hero) -> Int = { _ -> 0 }, val mitMod: Float = 0f, val offMult: Float = 1f) : BaseSpecial {
通常攻撃,
緋炎(spcStatSpcMod = { hero: Hero -> hero.def / 2 }),
月光(mitMod = -0.5f),
流星(offMult = 2.5f)
;
override fun damage(source: Hero, atkEffAdv: Int, mit: Int, offFlat: Int) = ((atkEffAdv + spcStatSpcMod(source) - mit + mit * mitMod) * offMult).toInt()
}
奥義の効果はゲーム内の説明ではこう書かれています。
- 緋炎:守備の50%をダメージに加算
- 月光:敵の守備、魔防-50%扱いで攻撃
- 流星:与えるダメージ2.5倍
上記のコードがゲーム内の説明をそのまま表現していると思いませんか?
正しく分割し正しく合成する
纏めたコードは以下のようになります。
data class Hero(var hp: Int, var atk: Int, var spd: Int, var def: Int, var res: Int, var weapon: Weapon, var special: Special) {
// targetを攻撃してダメージを与える
fun attack(target: Hero) {
val damage = Damage(this, offFlat())
target.damage(damage)
}
// ダメージを受け取って計算しHPを減らす
fun damage(damage: Damage) {
val dealed = damage.dealValue(this)
val preventedDamage = prevent(dealed)
//状態を変更するのはここだけ!
hp = if (hp > preventedDamage) hp - preventedDamage else 0
}
//メソッドの場合の攻撃力計算
fun atkEffAdv(target: Hero) = atk * eff(target) + atk * eff(target) * adv(target)
//コードブロックの場合の攻撃力計算
val atkEffAdvFnc: (Hero) -> Int = { target -> atk * eff(target) + atk * eff(target) * adv(target) }
//特効・色によるダメージ・固定値ダメージ・スキルのダメージ軽減は省略
fun eff(target: Hero) = 0
fun adv(target: Hero) = 0
fun offFlat() = 0
fun prevent(damage: Int) = 0
}
interface BaseSpecial {
fun damage(source: Hero, atkEffAdv: Int, mit: Int, offFlat: Int) = atkEffAdv - mit + offFlat
}
// 奥義
enum class Special(val spcStatSpcMod: (Hero) -> Int = { _ -> 0 }, val mitMod: Float = 0f, val offMult: Float = 0f) : BaseSpecial {
通常攻撃,
緋炎(spcStatSpcMod = { hero: Hero -> hero.def / 2 }),
月光(mitMod = -0.5f),
流星(offMult = 1.5f)
// 奥義が増えたときはここに追加する
;
//万一計算式を完全に取り換えるときはこれをオブジェクト側でさらにOverrideする
override fun damage(source: Hero, atkEffAdv: Int, mit: Int, offFlat: Int) = ((atkEffAdv + spcStatSpcMod(source) - mit - mit * mitMod) * offMult).toInt()
}
// 武器
enum class Weapon(val selectPreventParam: (target: Hero) -> Int = { 0 }, val isRanged: Boolean = false) {
FeliciasPlate({ target -> if (target.def < target.res) target.def else target.res }),
RefinedBreath({ target -> if (target.weapon.isRanged && target.def < target.res) target.def else target.res }),
MaterialWeapon({ target -> target.def }),
MagicWeapon({ target -> target.res }),
// 武器が増えたときはここに追加する
}
// ダメージを受け渡すためのクラス。前回も書いたようにコードブロックでも構わない。これを交換することもできる
// privateなので攻撃側の参照は漏れない=外からは攻撃側に依存していないように見えるし、参照を完全に取り除くこともできる
class Damage(private val source: Hero, private val offFlat: Int) {
fun dealValue(target: Hero): Int = source.special.damage(source, source.atkEffAdv(target), source.weapon.selectPreventParam(target), offFlat)
}
正しく分割・合成されたオブジェクトは奥義が増えたときは奥義が増え他のコードに影響を与えず、武器が増えたら武器が増え他のコードに影響を与えず、他にスキルなどいろいろあるものも同様です。
また、攻撃側と被害側は完全に分けられており、状態を変えるコードはHeroの被害側メソッドにしかありません。奥義やダメージオブジェクトなどは全てHeroに影響を与えるアフォーダンスであるか、Heroを参照して値を出力する値オブジェクトです。
なおDDDで言えば、これはHeroを集約ルートにしたドメインモデルとなります。
実のところ色の相性にしろスキルにしろお互いの能力値やスキルを参照するのでそう綺麗にはわかれませんし、書いたコードに思い違いがあればそれを変更・修正することになるでしょう。ですがこのようにメソッドやコードブロックを切り出して交換可能とすることにより、少なくとも追加に強い設計は実現可能です。
と言う記事をQiitaに書いたけど今回はあまり読んでもらえなかったなー
ゲーム(FEH)におけるダメージ管理で学ぶメソッド設計の前置き
RPGやSLGの多くでは敵味方キャラクター間の戦闘が発生し、お互いにダメージを与えHPを削りあいます。
この時に能力値をパラメータとしてダメージを計算することになるのですが、これがゲームバランスやギミックの要求により結構面倒な計算式になったりしますので管理しやすい設計を考えます。
1.FEHのダメージ計算
例としてFEH(ファイアーエムブレムヒーローズ)を挙げます。言語はKotlinですがKotlin特有の記述はそんなに使わないはずなので誰でも読めるかと思います。
data class Hero(var hp: Int, var atk: Int, var spd: Int, var def: Int, var res: Int, var weapon: Weapon)
FEHの能力値はHP,攻撃、速さ、守備、魔防の4種類です。武器には物理攻撃と非物理攻撃がありisMaterial()などのメソッドで判別できるものとします。今回速さは使いません。なおHPは現在値と最大値の2種類を持つのが現実的です。同じオブジェクトが持つべきかはまた別問題ですが。
基本式
ダメージ=攻撃側の攻撃 - (攻撃側が物理攻撃の時は被害側の守備 | そうでないときは被害側の魔防)
ダメージがマイナスの時は0とする
攻撃された側はダメージの分だけHPが減少する
HPがマイナスの時は0とする
攻撃側をAttacker, 被害側をTargetとします
これをシンプルにコード化して、例えばattacker.attack(target)メソッドと名付けると次のようになるでしょう
data class Hero(var hp: Int, var atk: Int, var spd: Int, var def: Int, var res: Int, var weapon: Weapon) {
fun attack(target: Hero) {
val damage = atk - if (weapon.isMaterial) target.def else target.res
val validDamage = if (damage >= 0) damage else 0
target.hp = if (damage >= target.hp) 0 else target.hp - damage
}
}
これだけならそのまま書いても問題ないでしょう。ですが最近次のような武器が追加されました。
フェリシアの氷皿:ダメージ=攻撃側の攻撃 - (被害側の守備 と魔防の低いほう)
錬成ブレス:ダメージ=攻撃側の攻撃 - (被害側が2射程武器の時は被害側の守備 と魔防の低いほう|そうでないときは被害側の魔防)
これをWhen式を使って追加するとこうなるでしょう(これよりHPの減少部分は省略します)
data class Hero(var hp: Int, var atk: Int, var spd: Int, var def: Int, var res: Int, var weapon: Weapon) {
fun attack(target: Hero) {
val damage = atk - when (weapon) {
Weapon.FeliciasPlate -> if (target.def < target.res) target.def else target.res
Weapon.RefinedBreath -> if (!target.weapon.isRanged || target.res < target.def) target.res else target.def
else -> if (weapon.isMaterial) target.def else target.res
}
}
}
これでもいいといえばいいのですが、将来また別の計算をする武器が追加されるたびにWhenを追加することになってしまいますし、実のところこれはダメージ計算のごく一部でしかありません。
実際のダメージ計算式は以下のようになります(英語のWikiから転載)
上記の守備・魔防の計算はこの式のMitの部分だけです。
この式のどの部分をとっても攻撃側と被害側の関係や能力値によって値が変わってくるので、同じように書くとIfとWhenの山ができてしまいます。この計算を整理することが必要です。
どうやって整理するか?
オブジェクト指向の設計原則と呼ばれるものはいくつもあります。
オブジェクト指向三原則
単一責任の原則
オープン・クローズドの原則
クリーンアーキテクチャのように「依存関係を一方向にする」なんてのもよく言われますね。
では、これらを実現するにはどうすればいいでしょうか?
よく疎結合にするとか内部のデータ構造を隠すとかオブジェクトではなくインタフェースに依存するとか言いますが、このときに当たり前すぎてめったに言及されないことがあります。
それは、外と内、つまり、攻撃側と被害側を正しく分けるべきである、ということです。
正しく分けるにはどうすればいいか?
一口に正しく分けるといってもこれではまだ具体的とは言えません。
具体的にどうすればいいか?ここはひとつ人文学の研究成果を借りてみましょう。
ギブソンのアフォーダンスです。
アフォーダンス
Wikipediaから引用します。
アフォーダンス(afforedance)とは、環境が動物に対して与える「意味のことである」(中略)
ギブソンの提唱した本来の意味でのアフォーダンスとは「動物と物の間に存在する行為についての関係性そのもの」である。例えば引き手のついたタンスについて語るのであれば、「"私"はそのタンスについて引いて開けるという行為が可能である」、この可能性が存在するという関係を「このタンスと私には引いて開けるというアフォーダンスが存在する」あるいは「このタンスが引いて開けるという行為をアフォードする」と表現するのである。
心理学的にどういう意味なのかは説明しかねますが、これを工学的に言うとこうなります。
私がタンスを開ける、という文を「タンスが"タンスを開けるという行為"というアフォーダンスを提供する」「私はその行為を実行することができる」の二つに分けて記述することができる。つまり、アフォーダンスを途中に置くことによって両者を分けることができる、ということです。
被害側が攻撃側に「攻撃する行為」をアフォードする
まずは今までシンプルに攻撃側として書いていたメソッドを被害側に移しましょう。
data class Hero(var hp: Int, var atk: Int, var spd: Int, var def: Int, var res: Int, var weapon: Weapon) {
fun attack(attacker: Hero) {
val damage = attacker.atk - when (attacker.weapon) {
Weapon.FeliciasPlate -> if (def < res) def else res
Weapon.RefinedBreath -> if (!weapon.isRanged || res < def) res else def
else -> if (attacker.weapon.isMaterial) def else res
}
}
}
タンスのアフォーダンスはタンスが提供しますが、これは私を対象にして能動的に送り付けるものではなく環境的に存在するものです。つまり、アフォーダンスは提供された相手(私)への参照を持ちません。これを使って攻撃側と被害側に分離します。
具体的にはアフォーダンスを提供される攻撃側への参照を持たないようにする、つまりメソッドの引数から攻撃側への参照を取り除きます。
data class Hero(var hp: Int, var atk: Int, var spd: Int, var def: Int, var res: Int, var weapon: Weapon) {
fun attack(damage: Int, weapon: Weaopn) {
val preventedDamage= damage - when (weapon) {
Weapon.FeliciasPlate -> if (def < res) def else res
Weapon.RefinedBreath -> if (!this.weapon.isRanged || res < def) res else def
else -> if (weapon.isMaterial) def else res
}
}
}
ダメージはただの数値なので攻撃側に計算して送ってもらいます。これで攻撃側と被害側を分離できました!
とはいえまだ武器への参照が残っています。しかも武器が具体的であるため、将来武器が追加されたときにこのWhenの中に武器が追加されることになります。これではオブジェクトが正しく分離できてるとは言えません。そこでWhenを取り除きます。そう、守備か魔防かを出力するアフォーダンスを武器が提供する、ということにするのです。
アフォーダンスは対象へ何かの行為を行うものです。これはつまり、対象を引数にしたコードブロック(関数)もやはりアフォーダンスだということです。そこで、各武器にアフォーダンスとなるコードブロックを保持させ、それを被害側に提供するというコードとして表現することもできます。
アフォーダンスは何かを対象とするアフォーダンスとして環境的に存在するので、こうやって対象以外から提供することもできます。
enum class Weapon(val selectPreventParam: (target: Hero) -> Int = { 0 }, val isRanged:Boolean = false) {
FeliciasPlate({ target -> if (target.def < target.res) target.def else target.res }),
RefinedBreath({ target -> if (!target.weapon.isRanged || target.res < target.def) target.res else target.def }),
MaterialWeapon({ target -> target.def}),
MagicWeapon({ target -> target.res}),
}
data class Hero(var hp: Int, var atk: Int, var spd: Int, var def: Int, var res: Int, var weapon: Weapon) {
fun attack(damage: Int, weapon: Weapon) {
val preventedDamage = damage - weapon.selectPreventParam(this)
}
}
でも武器への参照が残っているのが気持ち悪いという人もいるのではないでしょうか?
もちろんこれも除去することができます。
enum class Weapon(val selectPreventParam: (target: Hero) -> Int = { 0 }) {
FeliciasPlate({ target -> if (target.def < target.res) target.def else target.res }),
RefinedBreath({ target -> if (!target.weapon.isRanged || target.res < target.def) target.res else target.def }),
MaterialWeapon({ target -> target.def}),
MagicWeapon({ target -> target.res}),
}
data class Hero(var hp: Int, var atk: Int, var spd: Int, var def: Int, var res: Int, var weapon: Weapon) {
fun attack(damage: Int, selectPreventParam: (target: Hero) -> Int) {
val preventedDamage = damage - let(selectPreventParam)
}
}
被害側はコードブロックを受信し、letを使って自分を対象に実行します。もちろんselectPreventParam(this)と書いても同じです。
余談ですがこれを推し進めていくと、例えば数値のダメージとその軽減も一つのコードブロックにすることもできます。
Data Class Hero (val hp:Int, val atk:Int, val spd:Int, val def:Int, val res:Int, val weapon:Boolean) {
fun attack(damageDealer: (target: Hero) -> Int) {
val preventedDamage = let(damageDealer)
}
}
//攻撃側の呼び出し
val wpn = Weapon.FeliciasPlate
val damage = 10//攻撃側のダメージを計算し↓コードブロックのdamageにバインドする
val damageDealer:(Hero)->Int = {target->damage - target.let(wpn.selectPreventParam)}
target.attack(damageDealer)
このdamageDealerコードブロックはクロージャとして数値をバインドしているだけなので、やはり攻撃側への参照は持っていません。…がさすがにこれはやりすぎですね。コードブロックはだれが持っても同じですから普通に被害側に持たせましょう。ダメージと武器を引数としてを受け取りコードブロックを返すメソッドを作ってみましょう。
Data Class Hero (val hp:Int, val atk:Int, val spd:Int, val def:Int, val res:Int, val weapon:Boolean) {
fun damageDealer(damage:Int, weapon:Weapon):(Hero)->Int = {target-> damage - target.let(weapon.selectPreventParam)}
fun attack(damageDealer: (target: Hero) -> Int) {
val preventedDamage = let(damageDealer)
}
}
ところでdamageDealer,つまりダメージと武器によるダメージ減少を保持するコードは別の何かと交換する必要は有るでしょうか?ないならわざわざ引数として渡す必要は無いですよね?ということで単なるメソッドにすると一周回ってただのメソッドになってしまいました。
Data Class Hero (val hp:Int, val atk:Int, val spd:Int, val def:Int, val res:Int, val weapon:Boolean) {
fun damage(damage:Int, weapon:Weapon):Int = damage - let(weapon.selectPreventParam)
fun attack(damage:Int, weapon:Weapon) {
val preventedDamage = damage(damage,weapon)
}
}
これはメッセージを送って実行させているだけでは?
はい。
一周して戻ってきた感がありますが、この記事で言いたいことはあるオブジェクトを引数にとるコードブロックをそのオブジェクトに送信して実行させることとそのオブジェクトのメソッドを実行させることは等価ということです。
それらは対象となるオブジェクト以外の要素、つまりパラメータをどうやってバインドしているかとそのコードブロックがどこに格納されているかの違いでしかありません。
オブジェクト指向言語の元祖であるSmalltalkでは実際にコードブロックを送信して対象のオブジェクトに実行させます。その伝統を受けて、letやapplyでコードブロックを受け取るオブジェクトをレシーバと呼ぶわけですね。
そしてメソッドが最終的に対象となるもの・対象をとって操作するもの(アフォーダンス)・それを提供されて利用するものに分割できること、あるオブジェクトのメソッドが自分以外を操作対象にしない場合、それは自分自身を対象にとるアフォーダンスであることが説明できたかと思います。
そしてアフォーダンスで重要なことはあるオブジェクトを操作対象とするアフォーダンスはその操作対象に依存する存在であること、それは環境的に存在するので操作対象が提供するとは限らずコードとしてはどこにあってもいいこと、アフォーダンス自体は状態を持たないということです。つまり操作対象とコードとしてどこにあるかでメソッドは分類でき、用途によって向き不向きが有るのでそれを意識してメソッドを設計することができるわけですが前置きだけで長くなったので一度切ります。
Kotlin1.2でAndroid用のモデルをJavascriptから利用する
Javascriptのブラウザ版を作ったのをQiitaに書いたけどこっちに書いてないのすっかり忘れてた…。
Fire Emblem Heroes Battle Simulator
そろそろなんか機能追加とかプレイ画面作成とかしないとな。
要約
Javaに依存せずDDD的にKotlinだけでモデルを記述すればブラウザ上のJSから問題なく利用できる。実際の画面はこちら
KotlinはJavascriptへの変換がサポートされていますが今一つ有効活用されていないようです。これとか。
全てをKotlinで済ませるのは非常に大変ですし、Javaに依存するコードやJSに依存するコードが混在するとさらにややこしいことになります。これは1.2から導入されたMultiplatformでも変わりません。
しかし、JavaにもJSにも依存しないコードならば手間要らずで変換できますし、真のドメイン駆動設計ならばドメインモデルはドメイン自体以外の何物にも―Java.utilにすら依存しないはずです。そこでDDDのモデルをJSに変換して利用します。
対象のプロジェクト
しつこくこれを使います。Androidでは画面が狭いのでWeb版が欲しかったのですがJSで書き直すのは辛いのでちょうどいいのと、たまたまですがこれのドメインモデル部分はほとんどJavaに依存しておらずこれまた都合が良いからです。具体的にはLocaleとString.format()だけで、これらは容易に除去することができます。listやmapは普通に使えます。
Multiplatformでのビルド
MultiplatformはIntelliJ IDEAの機能なのでIntelliJからプロジェクトを作成します。
JVMとJSのモジュール名は実際にモジュール名になるので短くわかりやすい名前が良いです。たとえばuntitled2-jsはuntitled2-js.jsになるしJS上ではuntitled2-js.jp.co...とパッケージをたどってオブジェクトにアクセスすることになります。
ここにドメインモデルのコードを そおい!と放り込みます。
Kotlin/JSやMultiplatformの説明では色々詳しい説明がありますが、何にも依存していないモデルだけ変換する分には関係ありません。
あと今回は事情によりこの環境は捨てるので適当にやってますが、モジュールとして取り込んで真面目にgradleでビルドしたほうが良いと思います。
放り込んだままビルドすると普通にJSが生成されます。
このJSはそのまま普通に使えます。ランタイムがあればですが。JSにランタイムが入ってないしJVMはAndroidではなくJVMだしで、このMultiplatformはWeb/Androidマルチという感じがしません。node.jsとSpringでの利用が目的でしょうか?
とはいえこのままでは使用に耐えないので方針を変更してKotlin/JSを利用します。
Kotlin/JSでのビルド
シンプルな初期プロジェクトです。ていうかGradleプロジェクトじゃないんですね。AndroidやMultiplatformはGradleなのに…
ここにもやっぱりドメインモデルのコードを そおい!と放り込みます。
モジュールとして取り込むビルド環境を作るべきなのでしょうが分かりませんでした。
ビルドするとMultiplatformと同じようにJSが生成されますが、こちらにはランタイムも入っています。
生成されているJSはMultiplatformで生成されるJSと同じものです。なのでKotlin.jsだけMultiplatformに移植すれば普通に動くはずです。いずれMultiplatformでKotlin.jsとAndroid用のモジュールの取り込みをサポートしてもらえるとWeb/Androidでのマルチプラットフォームが楽になるのですが。
github.ioで公開する準備
これはサンプルではなくWeb版のアプリとして公開するものなのでそのままgithub.ioで公開します。
まずは公開用のdocsディレクトリを作ってJSをコピーし、index.htmlを書きます。JSは今回の主題ではないのでJQueryでシンプルに書きます。
GithubにPushした後、このプロジェクトのSettingsからGitHub Pagesとして公開します。
JS側のコード
index.htmlを右クリック>ブラウザで開くと普通に開けるのでF12のコンソールからモデルの構造を見てみます。
モデルのクラスやオブジェクトに普通にアクセスできますので、これを使うだけです。以下は画面上の入力を省略したコード例です。
//長いパッケージ名を省略する
var fehs = FEHSIM.jp.blogspot.turanukimaru.fehs
//全てのヒーローを取り出してソートする。allItems()はKotlinのListだがtoArray()でJSのArrayになる。
var allHeroes = fehs.StandardBaseHero.allItems().toArray().sort(function (a, b) {
if (a.name > b.name) {
return 1
} else if (a.name < b.name) {
return -1
} else {
return 0
}
})
//オーバーロードしているとメソッド名が変わるのでオーバーロードは避ける事
var heroA = fehs.StandardBaseHero.get_61zpoe$('エフラム')
var armedHeroA = new fehs.ArmedHero(heroA , "new heroA")
var battleUnitA = new fehs.BattleUnit(armedHeroA,armedHeroA.maxHp)
var heroB = fehs.StandardBaseHero.get_61zpoe$('エイリーク')
var armedHeroB = new fehs.ArmedHero(heroB , "new heroB")
var battleUnitB = new fehs.BattleUnit(armedHeroB,armedHeroB.maxHp)
var fightResults = battleUnitA.fightAndAfterEffect_trfvk0$(battleUnitB)
var last = fightResults[fightResults.length - 1]
//あとは戦闘結果を表示するだけ
このドメインモデルは二人のユニットを生成して能力値や状況を設定して戦わせ結果を得る、というものなので他のコードは画面の入力をJQueryで拾ってnewするときに初期値を変更しているJSだけです。
画面側の処理もKotlinで書きたい!というのはそれはそれでありでしょうが、画面は直接JSのほうがデバッグや管理がしやすいかと思います。欲しい結果やフィルタがあったらドメインモデルで作成するべきでしょう。
気を付けるべきは「ついJava.utilを使ってしまうが避ける」「List,MapはJava.utilではないので普通に使えるがJSのArrayでなかったりする」「コンパニオンオブジェクトはクラス名.Companionになる」「プロパティはあまり名前が変わらないがメソッド名は簡単に変わる」「JSやMapで使われそうなメソッド名は変わってしまうので避ける」「newはつい忘れるしコンストラクタでvoid 0ではなくnullを渡すと落ちたりするのでなるべく避ける」「Enumは安心して使える」くらいでしょうか。
おわりに
Kotlin/JSもMultiplatformもまだ発展途上中とはいえ、モデルを利用することだけならできました。
今回作ったアプリはゲームのツールなので、ゲームのバージョンアップに合わせてモデルに新キャラが追加されていきます。その更新をKotlin内で終わらせられるだけでも私には価値がありました。
ただしこれはDDDにより依存が少なかったためでDDDの力ともたまたまとも言えます。
ビルド環境も統合されているとは言えませんし、いくらDDDだってJava.utilにすら依存しないモデルとか普通は作りません。たまたま使えるなら使う、くらいでいいんじゃないでしょうか。
ORMラッパーライブラリRoomをRealmと取り換えてみる(Kotlin)
本当はKotlin multi-platformでAndroid用のソースをjsに変換していたのだが上手く動かないので断念してQiitaへ投稿した記事を転載する。変換してオブジェクトを作るまではうごくのだがinit()内で自分の関数が呼べない…明日Kotlin側のソースを修正して上手くいかなかったら諦めるか…イカ転載
良くドメイン駆動設計では「データベースの実装は気にするな」とか言いますが、実際にDB周りを気にせず開発することは少ないですよね。DBの実装を切り替えられるとは言え実際に切り替える人も滅多にいません。メリットは皆無ですが試しにやってみようと思います。具体的にはRealmで書いたDBアクセスをRoomに置き換えます。
#Kotlin用のRoomのサンプルをダウンロードする
正確にはRoom & Rx Java (Kotlin)ですがAndroidStudioでサンプルがダウンロードできます。
プロジェクトを開けていない画面かFile>new>Import Sampleで
![importroom.png](https://qiita-image-store.s3.amazonaws.com/0/151783/536b4574-ec9a-8d36-b8ec-c9e9579c668d.png)
ただしプロジェクトに問題があってビルドできませんし、RxJavaは使わないのでPersistanceディレクトリの中身だけもらいます。
entity
```
@Entity(tableName = "users")
data class User(@PrimaryKey
@ColumnInfo(name = "userid")
val id: String = UUID.randomUUID().toString(),
@ColumnInfo(name = "username")
val userName: String)
```
公式にdata classがサポートされているのは嬉しいですね。
dao
```
@Dao
interface UserDao {
/**
* Get a user by id.
* @return the user from the table with a specific id.
*/
@Query("SELECT * FROM Users WHERE userid = :id")
fun getUserById(id: String): Flowable<User>
/**
* Insert a user in the database. If the user already exists, replace it.
* @param user the user to be inserted.
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUser(user: User)
/**
* Delete all users.
*/
@Query("DELETE FROM Users")
fun deleteAllUsers()
}
```
FlowableはRxJavaのクラスなので後で取り除きます。これに限らずListなどのコンテナも使えます。
database
```
@Database(entities = arrayOf(User::class), version = 1)
abstract class UsersDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile private var INSTANCE: UsersDatabase? = null
fun getInstance(context: Context): UsersDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
}
private fun buildDatabase(context: Context) =
Room.databaseBuilder(context.applicationContext,
UsersDatabase::class.java, "Sample.db")
.build()
}
}
```
コンパニオンオブジェクトを使っていますがKotlinなのでSingletonのオブジェクトを別に作るほうがそれっぽい気がします。
#モジュールを作る
DDDなのでDatabaseはモジュールに隔離します。
基になったプロジェクトは以前紹介した[これ](https://qiita.com/turanukimaru/items/4176da313f370b0007f9)です。
![roommodule.png](https://qiita-image-store.s3.amazonaws.com/0/151783/2eeca59b-1a80-d879-27fe-1f993f2144e5.png)
build.gradleはこんな感じ。
```
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
// Room用に追加
implementation "android.arch.persistence.room:runtime:1.0.0"
kapt "android.arch.persistence.room:compiler:1.0.0"
// DDDなのでモデルのモジュールに依存する
compile project(path: ':fehsbattlemodel')
}
```
entityをRealm用に作ったクラスからコピペします。
```
@RealmClass
open class RealmArmedHero(
@PrimaryKey
var nickname: String = "",
var baseName: String = "",
var weapon: String = "NONE",
...
) : RealmObject() {
fun toModelObject(): ArmedHero {}
}
```
```
@Entity(tableName = "heroes")
data class RoomArmedHero(
@PrimaryKey
var nickname: String = "",
var baseName: String = "",
var weapon: String = "NONE",
...
) {
fun toModelObject(): ArmedHero {}
}
```
RealmはEntityとなるクラスを継承して機能を自動生成する都合上、@RealmClassとopen,RealmObjectの継承が必要です。
一方、Room側はEntityのクラスはそのまま使います。@Entity(tableName = "")アノテーションをつけるだけです。敢えてカラム名は指定せず極力手を抜いてみます。
daoはRealm側にも対応するものがありますが名前が良くわかりません。サンプルはContentって名前でしたが…。
```
object RealmArmedHeroContent : RealmContent<ArmedHero>() {
/** realmのkotlin用ハンドラ */
private var realm: Realm by Delegates.notNull()
/** 初期化ブロック。テーブル変更時などはここでマイグレーションすることになる */
init {
realm = Realm.getDefaultInstance()
realm.executeTransaction {
// realm.deleteAll()
}
}
override fun delete(item: ArmedHero): Int {
val results = realm.where(RealmArmedHero::class.java).equalTo("nickname", item.name).findAll()
realm.executeTransaction {
results.deleteAllFromRealm()
}
return results.size
}
override fun deleteById(id: String): Int {
val results = realm.where(RealmArmedHero::class.java).equalTo("nickname", id).findAll()
realm.executeTransaction {
results.deleteAllFromRealm()
}
return results.size
}
override fun createOrUpdate(item: ArmedHero): ArmedHero {
item.apply {
realm.executeTransaction {
realm.copyToRealmOrUpdate(RealmArmedHero(name, baseHero.name, weapon.value, refinedWeapon.value, assist.value, special.value, aSkill.value, bSkill.value, cSkill.value, seal.value, rarity, levelBoost, boon.name, bane.name
, defensiveTerrain, atkBuff, spdBuff, defBuff, resBuff, atkSpur, spdSpur, defSpur, resSpur))
}
}
return item
}
override fun allItems(): List<ArmedHero> {
return heroDao.allHeroes().map { e -> e.toModelObject() }
}
override fun getById(id: String): ArmedHero? = heroDao.getHeroById(id)?.toModelObject()
}
```
※クラスを直接指定するちょっと古いコードです。
```
@Dao
interface HeroDao {
@Query("SELECT * FROM heroes WHERE nickname = :id")
fun getHeroById(id: String): RoomArmedHero
@Query("SELECT * FROM heroes")
fun allHeroes(): List<RoomArmedHero>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertHero(hero: RoomArmedHero)
@Query("DELETE FROM heroes")
fun deleteAllHeroes()
@Query("DELETE FROM heroes WHERE nickname = :id")
fun deleteHero(id: String)
}
```
@Queryアノテーションの中にSQLを記述します。@Insert(onConflict = OnConflictStrategy.REPLACE)でcreate/updateになりそうです。(試し忘れ)
Room側のDaoは実際にはレポジトリを継承してDaoを呼び出すオブジェクトが必要になります。
```
object RoomArmedHeroContent : ModelObjectRepository<ArmedHero> {
var appContext: Context? = null
val heroDao get() = UsersDatabase.getInstance(appContext!!).heroDao()
override fun delete(item: ArmedHero): Int {
heroDao.deleteHero(item.name)
return 1
}
override fun deleteById(id: String): Int {
heroDao.deleteHero(id)
return 1
}
override fun createOrUpdate(item: ArmedHero): ArmedHero {
item.apply {
heroDao.insertHero(RoomArmedHero(name, baseHero.name, weapon.value, refinedWeapon.value, assist.value, special.value, aSkill.value, bSkill.value, cSkill.value, seal.value, rarity, levelBoost, boon.name, bane.name
, defensiveTerrain, atkBuff, spdBuff, defBuff, resBuff, atkSpur, spdSpur, defSpur, resSpur))
}
return item
}
override fun allItems(): List<ArmedHero> {
return heroDao.allHeroes().map { e -> e.toModelObject() }
}
override fun getById(id: String): ArmedHero? = heroDao.getHeroById(id)?.toModelObject()
}
```
やってることはほとんど同じですね。SQL相当のものをリポジトリに書くかDaoに書くかどうかくらいです。
Entityが増えたDBはこんな感じに。
```
@Database(entities = arrayOf(User::class, RoomArmedHero::class), version = 1)
abstract class UsersDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun heroDao(): HeroDao
companion object {
@Volatile private var INSTANCE: UsersDatabase? = null
fun getInstance(context: Context): UsersDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
}
private fun buildDatabase(context: Context) =
Room.databaseBuilder(context.applicationContext,
UsersDatabase::class.java, "Sample.db").allowMainThreadQueries()
.build()
}
}
```
@Database(entities = arrayOf(User::class, RoomArmedHero::class), version = 1)に対象のEntityを追加して、Daoも増やします。
allowMainThreadQueries()は別スレッドに分けずにアクセスするための記述です。面倒なので追加しましたが無くて済むならないほうが良いでしょう。
##テスト
```
@RunWith(AndroidJUnit4::class)
class RoomInstrumentedTest {
@Test
@Throws(Exception::class)
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getTargetContext()
appContext.deleteDatabase("Sample.db")
RoomArmedHeroContent.appContext = appContext
val modelHero = ArmedHero(StandardBaseHero.get("エフラム")!!,"new エフラム")
RoomArmedHeroContent.createOrUpdate(modelHero)
val insertedArmedHero = RoomArmedHeroContent.getById("new エフラム")
assertEquals("new エフラム",insertedArmedHero!!.name)
}
}
```
Modelに宣言したインタフェース経由で普通にアクセスできました。実機でも同じように取り換えて動かせます。同時に使うこともできましたがきっと意味はないでしょう。
##感想
・両方Androidに依存しているので今回は意味は無いが、サーバサイドに移植するときにアノテーションにSQLを書くライブラリ、例えばmyBatisと共用するとかなら意味があるかも?
・Realmのほうが色々楽ではあるがSQLの自動生成などが絡んでくるとRoomも悪くないか?
・Realmはカラムが増える分にはマイグレーション抜きでも動いたりするけどRoomは厳しい
AndroidStudioでjdkを指定してGradleが死んだ話
Error:Failed to complete Gradle execution.
Cause:
Write access is allowed from event dispatch thread only
とGradleがエラーをはくことがある。File > Project Structure > SDK LocationでUse embeded JDKを選ぶと直る。
今マルチプラットフォーム対応で相当モジュールをごちゃごちゃあれこれしてるのでプラグインとか設定がちょくちょく吹っ飛ぶので突然出るようになった原因は不明。
ここ数日こんなんばっかだなー。