Androidの方眼紙マップエディタ制作 – ほぼ完成
前回の投稿から12日も経っていることに驚き
時間(全て)を忘れてマップエディタ制作をしてました
最近は昼夜逆転気味。
なんでも人間の体内時計は25時間周期になっていて、意識的に朝に起きるようにしないと1時間ずつズレてくるらしいです。
深夜の1時に無理やり寝ようとしたら、頭がやたら冴えて5時まで考え事をして終わったので、寝る時間よりも起きる時間で調整しないといけないですね。
プログラムの方はかなり制作が進みました。
フィールドイベントの構造化
フィールドのイベントの種類を増やすたびにUndo処理を追加してくと1ファイルの記述量が膨大になってしまうので、フィールドイベントを構造化しました。
基底になるFieldEventクラスと、Undo,Redoを一元管理するUndoControllerクラスを作成し、イベントはそこから派生して個別にUndo・Redo処理を実装します。
個別に実装すると言ってもデータがリスト管理されていれば殆どコピペで済みます。
FieldEventクラスは、作成、削除、範囲削除などの基本的なコマンドを持ちます。
UndoControllerクラスは、インターフェイスを提供し、登録したイベントのundo・redoを管理します。
パックして一括Undoするための関数も備えています。
package com.example.mapwizard import android.graphics.Rect class UndoController { private val listUndoType = mutableListOf<MutableList<Int>>() private val listRedoType = mutableListOf<MutableList<Int>>() private var flagPackUndo = false private var flagPackRedo = false private var packUndoCount = 0 private var packRedoCount = 0 private val listInterface = mutableListOf<UndoInterface>() fun addInterface(i: UndoInterface){ listInterface.add(i) i.undoIndex = listInterface.size - 1 } interface UndoInterface{ var undoIndex: Int var undoCount: Int var redoCount: Int fun undo() fun redo() fun addUndo(rc: Rect?=null) fun addRedo(rc: Rect?=null) fun resetUndo() fun resetRedo() } fun undo(i: UndoInterface){ if(i.undoIndex in 0 until listInterface.size){ val obj = listInterface[i.undoIndex] obj.addRedo() addRedoType(i.undoIndex) obj.undo() } } fun redo(i: UndoInterface){ if(i.undoIndex in 0 until listInterface.size){ val obj = listInterface[i.undoIndex] obj.addUndo() addUndoType(i.undoIndex) obj.redo() } } fun addUndo(i: UndoInterface, rc: Rect?=null){ i.addUndo(rc) packUndoList() addUndoType(i.undoIndex) unpackUndoList() } fun addRedo(i: UndoInterface, rc: Rect?=null){ i.addRedo(rc) packRedoList() while(i.redoCount > 0) { addRedoType(i.undoIndex) i.redoCount-- } unpackRedoList() } fun resetUndo(){ for(obj in listInterface) obj.resetUndo() listUndoType.clear() } fun resetRedo(){ for(obj in listInterface) obj.resetRedo() listRedoType.clear() } // 元に戻す、総合処理 fun undo(){ val cnt = listUndoType.size if(cnt < 1) return val list = listUndoType.last() list.reverse() packRedoList() for(i in list){ val obj = listInterface[i] obj.addRedo() addRedoType(obj.undoIndex) obj.undo() } unpackRedoList() listUndoType.removeLast() } // 以後の、元に戻す処理を1つにまとめる fun packUndoList(){ flagPackUndo = true if(packUndoCount == 0) listUndoType.add(mutableListOf()) packUndoCount++ } // 元に戻す処理の1まとめを解除 fun unpackUndoList(){ packUndoCount-- if(packUndoCount==0) { for(i in listInterface) { if (i.undoCount > 0) { addUndoType(i.undoIndex) } } flagPackUndo = false } } // 元に戻す処理の種類を総合処理に登録 fun addUndoType(type: Int){ val it = listInterface[type] for(i in 0 until it.undoCount) { if (flagPackUndo) { val list = listUndoType.last() list.add(type) } else { val list = mutableListOf<Int>() list.add(type) listUndoType.add(list) } } it.undoCount=0 } fun redo(){ val cnt = listRedoType.size if(cnt < 1) return val list = listRedoType.last() list.reverse() packUndoList() for(i in list){ val obj = listInterface[i] obj.addUndo() addUndoType(obj.undoIndex) obj.undoCount=0 obj.redo() } unpackUndoList() listRedoType.removeLast() } fun packRedoList(){ flagPackRedo = true if(packRedoCount == 0) listRedoType.add(mutableListOf()) packRedoCount++ } fun unpackRedoList(){ packRedoCount-- if(packRedoCount == 0) flagPackRedo = false } fun addRedoType(type: Int){ if(flagPackRedo){ val list = listRedoType.last() list.add(type) }else{ val list = mutableListOf<Int>() list.add(type) listRedoType.add(list) } } }
FieldEventクラス
package com.example.mapwizard import android.graphics.Point import android.graphics.Rect import java.io.BufferedReader import java.io.BufferedWriter abstract class FieldEvent (): UndoController.UndoInterface{ override var undoIndex = 0 override var undoCount = 0 override var redoCount = 0 abstract fun delete(pt: Point) abstract fun deleteArea(rc: Rect) abstract fun copy(rc: Rect) abstract fun paste(ptOfs: Point) abstract fun clear() abstract fun shift(rc:Rect, x:Int, y:Int) abstract fun rotate(rc:Rect) abstract fun castArea(rc:Rect) abstract fun writeData(w: BufferedWriter) abstract fun readData(r: BufferedReader): Boolean fun ptInRect(pt:Point, rc:Rect): Boolean{ return pt.x in rc.left..rc.right && pt.y in rc.top..rc.bottom } fun readLineInt(r: BufferedReader): Int{ val i = r.readLine().toIntOrNull() if(i == null) return 0 return i } }
イベントクラスの1例
package com.example.mapwizard import android.graphics.Point import android.graphics.Rect import android.util.Log import java.io.BufferedReader import java.io.BufferedWriter data class FieldSwitch(var pt:Point=Point(0,0), var name:String="", var direction:Int=0, var text:String="", var flag:Boolean=false){ fun set(src: FieldSwitch){ pt = Point(src.pt) name = src.name text = src.text direction = src.direction flag = src.flag } } class SwitchEvent : FieldEvent(){ var list = mutableListOf<FieldSwitch>() private var listUndo = mutableListOf<MutableList<FieldSwitch>>() private var listRedo = mutableListOf<MutableList<FieldSwitch>>() private var listCopy = mutableListOf<FieldSwitch>() fun byName(name:String):FieldSwitch?{ for(sw in list){ if(sw.name == name) return sw } return null } fun add(pt: Point, direction:Int, name:String, text:String){ for(sw in list){ // 同じ位置にスイッチがあった場合上書き if(sw.pt == pt && direction == sw.direction){ sw.name = name sw.text = text return } } val swNew = FieldSwitch() swNew.pt = Point(pt) swNew.name = name swNew.text = text swNew.direction = direction list.add(swNew) } override fun delete(pt: Point){ val cnt = list.size for(i in cnt-1 downTo 0){ if(list[i].pt == pt){ list.removeAt(i) } } } override fun shift(rc:Rect, x:Int, y:Int){ for(sw in list){ if(ptInRect(Point(sw.pt.x, sw.pt.y), rc)) { sw.pt.x += x sw.pt.y += y } } } override fun castArea(rc: Rect) { val cnt = list.size for(i in cnt-1 downTo 0){ val sw = list[i] if(!ptInRect(Point(sw.pt.x, sw.pt.y), rc)){ list.removeAt(i) } } } override fun rotate(rc: Rect){ var x = 0 var y = 0 val h = rc.bottom - rc.top + 1 for(sw in list){ if(ptInRect(sw.pt, rc)){ y = sw.pt.x - rc.left y += rc.top x = sw.pt.y - rc.top x = h - 1 - x x += rc.left sw.pt.x = x sw.pt.y = y when(sw.direction){ 0->sw.direction=1 1->sw.direction=2 2->sw.direction=3 3->sw.direction=0 } } } } override fun clear(){ list.clear() } override fun deleteArea(rc: Rect) { val cnt = list.size for(i in cnt-1 downTo 0){ if(ptInRect(list[i].pt, rc)){ list.removeAt(i) } } } override fun copy(rc: Rect) { listCopy.clear() for(sw in list){ if(ptInRect(sw.pt, rc)) { val sw2 = FieldSwitch() sw2.set(sw) listCopy.add(sw2) } } } override fun paste(ptOfs: Point) { for(sw in listCopy){ val sw2 = FieldSwitch() sw2.set(sw) sw2.pt.x += ptOfs.x sw2.pt.y += ptOfs.y list.add(sw2) } } private fun dupe(l: MutableList<FieldSwitch>): MutableList<FieldSwitch>{ val ret = mutableListOf<FieldSwitch>() for(sw in l){ val sw2 = FieldSwitch() sw2.set(sw) ret.add(sw2) } return ret } override fun addUndo(rc:Rect?) { if(rc != null) { for (sw in list) { if (ptInRect(sw.pt, rc)) { listUndo.add(dupe(list)) undoCount++ break } } }else { listUndo.add(dupe(list)) undoCount++ } } override fun addRedo(rc:Rect?) { redoCount++ listRedo.add(dupe(list)) } override fun resetUndo() { undoCount=0 listUndo.clear() } override fun resetRedo() { redoCount=0 listRedo.clear() } override fun undo() { if(listUndo.size > 0){ list = listUndo.last() listUndo.removeLast() } } override fun redo(){ if(listRedo.size > 0){ list = listRedo.last() listRedo.removeLast() } } override fun writeData(w: BufferedWriter) { // タグの書き込み val tag = "SWITCH_DATA" w.write(tag); w.newLine() // データ個数の書き込み val cnt = list.size w.write(cnt.toString()); w.newLine() // 各データの書き込み for(sw in list){ w.write(sw.pt.x.toString()); w.newLine() w.write(sw.pt.y.toString()); w.newLine() w.write(sw.name); w.newLine() w.write(sw.text); w.newLine() w.write(sw.direction.toString()); w.newLine() w.newLine() } } override fun readData(r: BufferedReader):Boolean { list.clear() // タグの読み込みとチェック val tag = r.readLine() if(tag != "SWITCH_DATA"){ Log.d("FileReadError Switch", "tag check failed") return false } // マップサイズの読み込みとチェック val cnt = readLineInt(r) // 各マップデータの書き込み for(i in 0 until cnt){ val sw = FieldSwitch() sw.pt.x = readLineInt(r) sw.pt.y = readLineInt(r) sw.name = r.readLine() sw.text = r.readLine() sw.direction = readLineInt(r) val empty = r.readLine() if(empty != ""){ Log.d("FileReadError Switch", "check line error") return false } list.add(sw) } return true } }
こんな感じで、ワープイベント、エレベーターイベント、フロアイベント、テキストイベントと実装します。
そうすることで、マップ上に存在するオブジェクトのUndo・Redo処理を一元管理することが出来ます。
スイッチイベントの追加
プレイモードがあると実装したくなるのがスイッチイベント
ドアを開くためのスイッチと、スイッチを押したら開くスイッチドアの2種類を実装しました。
プレイモードにするとドアがロックされ、スイッチを押すとロックが解除される。
もう立派なゲームですね
スイッチドアを床に指定すると、ワープやスイッチONをロックするようにもしました。
Aというアイテムを拾わないと、Bというスイッチを押せないみたいな使い方が出来ます。
一通り完成して、データを入力していると、複数のスイッチを押さないと開かないドアの実装が必要だということがわかりました。
これはスイッチ名をカンマ区切りで入力して、対応することにしました。
しかし、これらを実装すると、経路探索機能がとても難しくなります。
ダイクストラ法は折り返して探索できるようなアルゴリズムではありません。
実装しようか悩みましたが、これが実装できればスゴイぞ!とテンションアップして実装に取り掛かりました。
スイッチを含む経路探索機能
完成に一週間は掛かりました。
考え方としては、目的地を複数に分解して、それぞれをダイクストラ法で結ぶという方法です。
まず最初に、スタート地点からゴール地点までをスイッチドア無視で経路探索します。
スイッチドアを潜ったときに、その記録を残します(開ける必要があるスイッチ)
目的地をスイッチに切り替え、スイッチを押すためのスイッチを列挙します。
これで必要なスイッチの列挙は完了(3路ショートカットがあるとうまくいきません)
次にドアの開閉状態を考慮したダイクストラ法で現在地から近い順番にスイッチを押していきます。
もし、押せるスイッチがなければ、検索失敗。
ゴールにたどり着ければ検索成功です。
ワープとか、エレベーターとか、スイッチがONなら押せるスイッチとか、かなり頭を使いました。
落とし穴に落ちて、昇降機で上がってくるというルート検索が失敗すると思ったら、昇降機がルートに含まれていない(原因はエレベーターによるショートカットが近道として選ばれるから)
これは戻り道も検索しなおすという手法で解決。
ソースコードも迷路になってしまいました(オイ
苦労の甲斐もあって、威力は抜群。
10階層の複雑なフロアをゲームクリアまで道筋を表示することに成功しました。
頑張った甲斐があった(TдT)
ただ、オフスイッチや迷路構造が変わるスイッチなどには対応できません。
オフスイッチが入ってしまうと、スイッチを押す順番が重要になってくるので、計算量が膨大になってしまいます。
あくまで一番近い押せるスイッチを順番に押していくだけですね。
テキストイベントの拡張
ここまで来たらメッセージダイアログを表示する機能も欲しくなりました。
フロアに踏み込んだらメッセージを表示
スイッチを押したらメッセージを表示
スイッチを押す前にメッセージを表示し、キャンセルしたらスイッチを押さない
壁に向かって進んだらメッセージ表示
などなど
現在は、床に文字を描画するか、コメント的な扱いをするかのテキスト情報だけしかありません。
そのセルごとに設定されたテキスト情報を、テキストイベントというクラスで管理するようにしました。
良いですね、ますますゲームらしくなりました。
しかし、これではマップエディタというよりゲームエディタになってしまうので、やり過ぎは禁物。
右図は、少し面白い事を閃いたので、それを実験をしたマップです。
ゲームのスイッチを押しながらゴールまでたどり着けるのなら、ゲームだけではなく、現実のあらゆる問題も解くことが出来るのではと考えました。
料理をするために、どこに何を買いに行くかとか、無駄のないルートはどちらかとかです。
結果は、あまりうまく行きませんでした。
現状は押すことができる一番近いスイッチを順番押すというアルゴリズムのため、最短ルートにはならなかったからです。
スイッチからスイッチまでのルートは最短なのですが、ルート選びの順番となると計算が複雑になってしまいます。
順番組み換えとなると、更に上位の考えが必要になってきますね。
ちなみに、この実験マップでは、サムとトニーと仲良くなってパーティを開くエンドと、サムとトニーを○して自首して刑務所にいる父と母に会って、財宝を手に入れるエンドがあります。
これも、ルート検索は成功するのですが、最短にはならないですね。
でも一番近いスイッチを順番に押すのが平均的に効率が良いと思います。
アプリケーションアイコンの制作
リリースも視野に入れて、アプリケーションアイコンの制作も行いました。
ここで壁になるのが画力
デフォルトアンドロイドアイコンの背景が方眼紙っぽいし、頭を魔法使いのハットにすれば良いんじゃneとか考えましたが、やめておきました。
アプリケーションのアイコンは、パッと見なんのアプリか分かったほうが良いですよね。
(グーグルマップのアイコンがピンマークに変わってから見つけるのが大変になった)
方眼紙っぽさを出して、ウィザードっぽく杖でも書いたらどうかと考えましたが、杖が書けません。
いっそ、このマップエディタで文字を書いてそれをスクショしてアイコンにしてしまえば良いのではと思いやってみました。
なかなか雰囲気があって良くないですか?
Wの歪み具合とか
2件のフィードバック
[…] 次回 […]
[…] 前回から10日掛けて、バグ潰し・機能追加・広告追加などの手を加えました。 […]