2.5Dの箱庭
pico.iso: 斜め上から見たボクセルの箱庭(ワールドの読み込み・生成・保存、影と松明の光つきの描画、タップ位置の引き当て、人や物を立てて置く、条件つきの経路探索)
pico.iso は、斜め上から見た(アイソメトリックの)マインクラフト風の箱庭を動かすエンジンです。重い描画とワールドの処理はOSがするので、アプリは画面の流れと操作だけを書きます。アプリ「ブロック」と「ゾンビTD」がこのエンジンで作られています。
- ワールド: 1辺 1024 マス(8x8 の柱 = 1チャンクが 128x128 個)× 高さ 16。見えている所のまわりのチャンク(最大 56 個。松明の明るさと、その計算の作業場所を含めて約80KB)だけを読み込み、離れたものは手放す。地形は種と位置だけで決まるので、SDへ書くのは書き換えたチャンクだけ
- 描画: 影(太陽は左奥の上。面を三角形2つに分けて半分だけの影も)、松明の光、昼と夜、半透明の水、水面は2px低く、手前のブロックに完全に隠れるブロックは描かない
- 引き当て: 画面のタップ位置に見えているブロックとその面
- 人や物: 好きな画像を箱庭の中に立てて置ける。ブロックとの前後(壁の裏に回ると隠れる)と足元の影はエンジンが描く
- 経路探索: 2点を結ぶ一番安い道(A*)。登れる段数・降りられる段数・体の高さ・避けるブロック・段差やブロックの値段・自前の判定を指定できる
ブロックの番号は 0=空気、1=水、2〜24 が石・草・土…、25=松明です(全部の名前は 定数 の iso.BLOCK_*)。
松明の光
松明(25)は光源です。松明のマスを明るさ7とし、光を通すマス(空気・水・葉・松明)を1マス進むごとに1ずつ暗くしながら広げます(マインクラフトと同じ考え方)。石などの透けないブロックは光を通さないので、壁の裏は暗いままです。光は6マス先まで届きます。
- 面の明るさは、その面が向いている隣のマス(上面なら真上、左面なら -x、右面なら -z)の明るさで決まります
- 明るさは4段階に丸めます: 2マス以内は日なたと同じ絵、3〜4マスは影の絵の上に日なたの絵を市松模様で重ね、5〜6マスは4画素に1つだけ重ねます
- 日の当たる面は、昼はもともと日なたなので変わりません。影になっている面と、いつも影の右面が明るくなります
iso.sunlight(false)で夜にすると、どの面も影の絵で描き、松明の光が届く所だけが明るく見えます- 松明は影を落とさず、後ろのブロックも隠しません。松明そのものはいつも同じ明るさで描きます
- 明るさは保存しません(チャンクを読み込むときに、まわりの松明から計算し直します)
はじめの一歩
local iso = pico.iso
local img = pico.image_load(pico.path_join(pico.app_dir(), "faces.pimg"))
iso.set_image(img) -- 面の絵(224x598、1種類につき高さ23pxの1段)
iso.sky(7) -- 空の色(パレット番号)
iso.view(0, 20, 240, 204) -- 表示範囲(Canvas と同じ矩形)
local dir = pico.path_join(pico.app_dir(), "worlds/A")
pico.sd_mkdir(dir)
local x, y, z = iso.create(dir, 0, 12345) -- 自然のワールド(種 12345)。始めのカーソルの位置
iso.cursor(x, y, z, true)
-- (x, y, z) を表示の真ん中へ
iso.origin(120 - 16 - 16 * (x - z), 122 - 16 + 8 * (x + z) + 16 * y)
local view = pico.create("Canvas")
pico.set(view, "x", 0); pico.set(view, "y", 20); pico.set(view, "w", 240); pico.set(view, "h", 204)
pico.on(view, "render", function() iso.render() end)
function loop(dt)
-- 見えている所のチャンクを少しずつ読み込む(視点が変わっていれば範囲を決め直す)
if iso.pump(8, 10) > 0 then pico.invalidate(view) end
end
ワールド
| 関数 | 説明 |
|---|---|
iso.create(dir, kind, seed[, k]) → x, y, z | nil, 理由 | 新しいワールド。kind: 0=自然 1=平ら 2=デモ 3=空 4=タワーディフェンス(下の「タワーディフェンスの地形」。始めの位置はベースの中心)。k は1辺のチャンク数(既定128、1〜128)。始めのカーソルの位置を返す |
iso.open(dir) → x, y, z, ブロック | nil, 理由 | dir/world.dat を開く。保存したカーソルの位置と選んでいたブロック |
iso.info(path) → 種類, 1辺のマス数 | nil | world.dat の見出しだけ読む(ワールドを選ぶ画面に) |
iso.save(x, y, z, ブロック) → bool | 書き換えたチャンクと見出しを書き出す |
iso.close() | 閉じる(書き出さない。置き場のメモリも返す) |
iso.get(x, y, z) / iso.set(x, y, z, ブロック) | ブロックを読む/書く。世界の外と読み込んでいないチャンクは空気。set は読み込んでいなければその場で読み込む |
iso.first_air(x, z) | 下から見て最初の空気の高さ |
iso.size() → 1辺, 16, 種類 | nil | 開いていなければ nil |
iso.pump(max[, ms]) → 読み込んだ数 | 予定のチャンクを最大 max 個読み込む(ms を過ぎたら途中でやめる)。視点が変わっていれば先に範囲を決め直す |
iso.pending() | 読み込む予定の残り |
iso.stats() | { chunks, bytes, faces, pending }(faces は直前の render で描いた面の数) |
dir は pico.sd_write と同じ権限の確認を通ります(permission_sd_outside_app_dir が無ければアプリのフォルダの中だけ)。ファイルは dir/world.dat と dir/c_<cx>_<cz>.dat(チャンクごと)です。
表示
| 関数 | 説明 |
|---|---|
iso.set_image(handle[, rows]) | 面の絵(pico.image_load したもの、224x598 以上)。透けないブロック(手前に置くと後ろを完全に隠し、光も通さない)を絵から調べておく。rows(ブロックの番号の配列、26 = カーソル)を渡すと、画像はその段だけを上から順に並べたもの(高さ 23×段の数 以上)でよく、載っていないブロックは描かない。使うブロックが少ないアプリで画像のメモリを減らせる(ゾンビTDは13段で約34KB。全部だと約67KB) |
iso.view(x, y, w, h) | 表示範囲。読み込む範囲と、描き直す範囲の切り取りに使う |
iso.origin([ox, oy]) → ox, oy | ブロック(0,0,0)の絵の左上。ブロック(x,y,z)の絵(32x31)の左上は (ox + 16*(x-z), oy - 8*(x+z) - 16*y) |
iso.cursor(x, y, z[, show]) | カーソルの位置と表示 |
iso.sky(色) | render が塗る空の色 |
iso.sunlight([on]) → on | 日の光。false で夜(描き直しは呼び出し側で pico.invalidate) |
iso.light(x, y, z) → 0〜3 | そのマスの松明の明るさ(0=届かない、3=日なたと同じ) |
iso.render([x, y, w, h]) | 空で塗ってワールドとカーソルを描く。Canvas の render の中で呼ぶ(省略すると今の描き直す範囲) |
iso.draw_icon(ブロック, x, y) | 1個のブロックをまるごと描く(ボタンや選ぶ画面に) |
iso.block_pos(x, y, z) → px, py | ブロックの絵の左上 |
iso.pick(px, py) → x, y, z, 面 | nil | 画面の点に見えている一番手前のブロックと面("top" "left" "right") |
iso.dirty_block(x, y, z) | そのブロックの絵の所を描き直す(カーソルを動かしたとき) |
iso.dirty_edit(x, y, z) | 置いた/壊したとき。そのブロックと、影が変わりうる面と、松明の明るさが変わった範囲だけを描き直す |
iso.culling(bool) | 隠れたブロックを描かない(既定 true) |
人や物(エンティティ)
箱庭の中の好きな位置(小数でよい)に、画像を立てて置けます(96個まで。少ししか置かなければメモリもそのぶんしか使いません)。人・動物・樽など、ブロックの格子に乗らないものに使います。
- 位置
(x, y, z)は足元の中心です(ブロックの単位。yは足の裏の高さ)。ブロック(bx, by, bz)の上に立たせるなら(bx + 0.5, by + 1, bz + 0.5) - 絵は「足元の点」(既定は絵の下の真ん中)がその位置に来るように置きます
- ブロックとの前後は「当たりの箱」(足元を中心にした半径
rの正方形 × 高さheight)で決めます。箱とブロックが離れている向きで、見る人の側(左手前-x・右手前-z・上+y)にある方が手前です。壁の裏に回ると隠れ、柱の手前に出ると柱を隠し、1段のブロックの奥では足元だけが隠れます。水の中では、水面より下だけが水越しに見えます - 影: 足元の下の地面(上面が足の裏以下の一番上のブロック)に、丸い影を市松模様で落とします。高く上がるほど小さくなります。壁の裏の地面に落ちた影は壁が隠します
- 位置や絵を変えると、前の所と今の所を自動で描き直します(
pico.invalidateは要りません) - 画像は
pico.image_load(透過つきの.pimg。0番が透過)やpico.image_create(w, h, true)で作ったもの。画像を解放すると、その人や物は描かれなくなります - ワールドを閉じる(
iso.close、iso.create、iso.open)と全部片付きます - 夜(
iso.sunlight(false))でも絵はそのままの明るさです
| 関数 | 説明 |
|---|---|
iso.entity_add(image, x, y, z[, opts]) → id | nil, 理由 | 置く。opts は下の表 |
iso.entity_set(id, opts) | 変える(opts に書いたものだけ。おかしな値ならエラーで、何も変えない) |
iso.entity_move(id, x, y, z) | 動かす(entity_set より軽い。毎フレーム呼んでよい) |
iso.entity_get(id) → x, y, z, 表 | nil | 位置と、今の opts の値をまとめた表 |
iso.entity_pos(id) → x, y, z | nil | 位置だけ(表を作らないので軽い。iso.crowd で押された後に位置を読み戻すのに使う) |
iso.entity_remove(id) → bool / iso.entity_clear() | 取り除く / 全部取り除く |
iso.entity_at(px, py) → id | nil | 画面の点にある一番手前の人や物(絵の不透明な画素で判定。ブロックに隠れているかは見ない) |
iso.ground(x, z[, y]) → 高さ | nil | 柱 (x, z) で、上面が y(省略すると一番上)以下にある一番上のブロックの上面の高さ。水も地面(水の中なら水の底)、松明は地面にならない |
iso.loaded(x, z) → bool | 柱 (x, z) のチャンクを読み込んでいるか。読み込んでいない所は空気に見えるので、人や物を動かす前に確かめる(確かめないと、視点を遠くへ動かしたときに足元が消えて落ちる) |
iso.to_screen(x, y, z) → sx, sy | 点の画面の位置(小数のまま) |
opts:
| キー | 既定 | 説明 |
|---|---|---|
x y z | — | 位置(entity_set のとき) |
image | — | 画像を差し替える(entity_set のとき) |
sx sy w h | 画像全体 | 画像の中の範囲(コマを並べた画像の1コマ)。大きさが変わると ax ay を決め直す |
ax ay | w // 2, h - 1 | 足元の点(範囲の左上から) |
r | w / 64(0.05〜0.49) | 当たりの箱の半径(ブロックの単位)。絵の横幅の画面の幅が 64 * r |
height | h / 16 | 当たりの箱の高さ(ブロック1段が16px) |
flip | false | 左右反転 |
visible | true | 隠す |
shadow | true | 足元に影を落とす |
shadow_color | 0 | 影の色(パレット番号) |
bar | なし | HPバー(0〜100 の割合)。絵の上に、ブロックにも他の人や物にも隠れず一番上に描く。false で消す |
bar_color | 11 | HPバーの色 |
mark | なし | 選択の印(頭の上の小さな下向きの三角)の色。一番上に描く。false で消す |
tag | 0 | 分類(0〜31)。iso.nearby で絞り込むのに使う |
crowd | "none" | 押し合い(iso.crowd): "move" 押し合って動く / "fixed" 動かず押し返すだけ(タワー等) / "none" 関わらない |
mass | 1 | 押し合いの重さ(重いほど押されにくい) |
local people = pico.image_load(pico.path_join(pico.app_dir(), "people.pimg")) -- 自分で用意した絵
local x, z = 520.5, 512.5
local y = iso.ground(x, z) -- 立てる地面の高さ
local id = iso.entity_add(people, x, y, z, { w = 12, h = 22 }) -- 1コマ目(12x22)
function loop(dt)
x = x + 1.5 * dt / 1000 -- 右奥(+x)へ 1.5 ブロック/秒
if not iso.loaded(x, z) then return end -- 読み込んでいない所では動かさない
local g = iso.ground(x, z, y + 1.01) -- 1段なら登れる
if g and g <= y + 1 then y = g end
iso.entity_move(id, x, y, z) -- 描き直しは自動
end
足元の正方形の四隅のどれかが高い段にかかっているなら、立つ高さは四隅で一番高い地面にしてください(登るときも落ちるときも同じ四隅で見る)。登るときだけ四隅、落ちるときは中心の柱だけで見ると、段の縁で「登る → 中心の下は低いので落ちる → また登る」を毎フレーム繰り返して、上下へ瞬間移動して見えます。エンジンの影も四隅で一番高い地面に落ちます。
Canvas の render で iso.render() を呼んでいれば、人や物もそこで描かれます。アプリ「ゾンビTD」(zombies.lua・soldiers.lua)が使い方の例です。
経路探索
local path, cost, how = iso.path(x0, y0, z0, x1, y1, z1 [, opts])
-- path = { {x=, y=, z=}, ... }(path[1] が出発点、path[#path] が到着点), cost = 値段, how = "found" | "partial"
-- 失敗: nil, 理由 ("no_path" 行ける所を全部調べても着けない / "limit" 調べる点の上限 / "start" 出発点に立てない / "goal" 目的地の柱に立てない
-- / "memory" 作業場所を確保できない。Luaのごみを集めて1回やり直してもだめなとき。少し後に探し直すこと)
箱庭の上を歩いて (x0, z0) から (x1, z1) へ行く、値段が一番小さい道を返します(A*)。値段は「歩いた距離」に、下の条件で足した分を合わせたものです。
- 立てる場所: 足元 (x, y-1, z) が空気・松明でないブロックで、体のマス (x, y 〜 y+height-1, z) が空気か松明(
swimなら水も)の所。yは足の裏の高さで、iso.ground()や人や物(エンティティ)のyと同じ数え方です。水も足元になります(水面を歩く。嫌ならavoidに1を入れる) - 1歩: 東西南北(
diagonalなら斜めも)の隣の柱へ。登るのはmax_up段まで、降りるのはmax_down段まで。登るときは今の柱の頭の上、降りるときは隣の柱の頭の上が空いている必要があります(天井を突き抜けない)。斜めは両脇の柱のどちらにも行けるときだけ(角をすり抜けない) y0/y1はnilなら柱の一番上、数ならその高さ以下で一番上の立てる所。y1がnilなら目的地の柱のどの高さに着いてもよい(橋の下へ行きたいときはy1を指定)。小数の座標は切り捨てるので、人や物の位置をそのまま渡せます- 読み込んでいないチャンクには立てません(空気に見えるため)。探すのは読み込んでいる所だけです
| opts | 既定 | 意味 |
|---|---|---|
max_up | 1 | 1歩で登れる段数(これより高い段差は通れない) |
max_down | 2 | 1歩で降りられる段数 |
height | 2 | 体の高さ(マス)。低いトンネルを通るかが変わる |
diagonal | false | 斜めにも進む(斜めの1歩は step × √2) |
swim | false | 体のマスに水があってもよい(水の中を歩く) |
step | 1 | 1歩の値段(正の数) |
up_cost | 0 | 1段登るごとに足す値段(段差の大きい道ほど高くつく = 避けられる) |
down_cost | 0 | 1段降りるごとに足す値段 |
avoid | なし | ブロック番号の配列。足元か体のマスにこのブロックがある所へは行かない(例 {1, 13} 水とTNT) |
pass | なし | ブロック番号の配列。体のマスにあってもよい(中を通り抜ける)が、足元にはならないブロック(バリケード等。下の「通り抜けられるブロック」) |
body_cost | なし | {[番号] = 値段}。行き先の体のマスにそのブロックがあるたびに足す値段(pass のブロックの中を通る値段) |
block_cost | なし | {[番号] = 値段}。足元がそのブロックの所へ入るたびに足す値段(例 {[11] = 3} 砂は歩きにくい) |
edge | なし | function(x, y, z, nx, ny, nz, floor) 1歩ごとに呼ぶ自前の判定。false を返すと通れない、数を返すとその値段を足す、nil なら何も足さない。floor は行き先の足元のブロック |
max_nodes | 1024 | 調べる点の上限(1〜4096)。作業場所は探す間だけ使う(1点あたり約26バイト) |
partial | false | 着けないとき、目的地に一番近づける所までの道を返す(how が "partial") |
-- 2段以上の段差は通れない・水とTNTの上は歩かない・段差が大きいほど高くつく・砂は歩きにくい
local path = iso.path(px, py, pz, gx, nil, gz, {
max_up = 1, max_down = 1,
avoid = {1, 13},
up_cost = 2, down_cost = 1,
block_cost = {[11] = 3},
diagonal = true,
})
if path then
for i, p in ipairs(path) do
-- p.x + 0.5, p.y, p.z + 0.5 がそのマスの真ん中に立つ位置
end
end
-- 自前の条件: 松明から3マス以内は通らない(edge は1歩ごとに呼ばれるので重い判定は避ける)
local path = iso.path(px, py, pz, gx, nil, gz, {
edge = function(x, y, z, nx, ny, nz, floor)
if iso.light(nx, ny, nz) >= 2 then return false end
return 0
end,
})
iso.stand(x, z [, y [, opts]]) は、同じ規則(height / swim / avoid / pass)で立てる高さを返します(y 以下で一番上。無ければ nil)。
値段について:
- 一番安い道を返すのは、1歩の値段がいつも
step以上のときです(up_cost/down_cost/block_cost/edgeの値はどれも0以上なので、普通に使えば満たされます) - 行けない所ばかりの広い範囲を探すと、
max_nodesを使い切って"limit"になります。遠い目的地はmax_nodesを増やすか、途中の点を目的地にして何回かに分けてください edgeは1歩を調べるたびに呼ばれ(1点につき最大で方向の数×高さの数)、Luaの命令数の上限にも数えられます
タワーディフェンス向けの道具
大勢の人や物を動かすゲーム(タワーディフェンス等)のための道具です。重い計算はどれもOSの中でします。
流れの場(フローフィールド)
iso.flow_build(goals, opts [, now]) -- → true | nil, 理由
iso.flow_step([budget]) -- → この呼び出しで出来上がったら true
iso.flow_get(x, z) -- → 値段, 次の柱の x, z, その立つ高さ | 値段, nil(目的地) | nil(届かない)
iso.flow_info() -- → { ready=, building=, revision=, failed=, bytes= }
iso.flow_clear()
目的地(柱の集まり。goals = {{x, z}, ...}、256個まで)から全部の柱へ、「着くまでの値段」と「次に進む隣の柱」を先に求めておきます。大勢のゾンビは、自分の柱の「次の柱」へ歩くだけでベースへ着きます(1体ずつ iso.path しない)。
- 規則(
opts)はiso.pathと同じです(max_upmax_downheightdiagonalswimstepup_costdown_costavoidpassblock_costbody_cost。max_nodespartialedgeは使えません)。登りと降りの向きは「歩く側から見た向き」で判定します - 1つの柱に立てる高さは1つだけ(柱の一番上の立てる所)。橋の下は扱いません
nowを付けるとその場で出来上がるまで作ります。付けなければiso.flow_step(budget)を毎フレーム呼んで少しずつ作ります(柱を1つ調べる/確定するのが1単位。既定512)。作っている間は前の結果を答え続け、出来上がった瞬間に入れ替わります(revisionが1増える)- 値段は 1/8 単位で持ちます(斜めの1歩は 1.375)。8191.75 で頭打ち
- メモリ: 出来上がった結果は1柱あたり3バイト(56x56 で約9.4KB)。作っている間だけ、その倍と数KBを余分に使います。
iso.flow_clear/iso.closeで全部返します。世界の幅は64まで - メモリが足りずに作れなかったときは、
iso.flow_buildがnil, "メモリが足りません"を返すか(作り始められなかった)、作っている途中で止まってiso.flow_info().failedがtrueになります。どちらも前の結果はそのまま残ります。bytesは今持っているメモリ(バイト)
-- ベース(柱 30,30 のまわり)へ向かう流れ。バリケード(ブロック番号 6)は通り抜けられるが、通ると高くつく
iso.flow_build(base_cells, { max_up = 1, max_down = 2, up_cost = 1, pass = {6}, body_cost = {[6] = 4} })
function loop(dt)
iso.flow_step(800) -- 作り直しを少しずつ進める
for _, z in ipairs(zombies) do
local d, nx, nz, ny = iso.flow_get(z.x, z.z)
if nx then -- 次の柱の真ん中へ向かう
local tx, tz = nx + 0.5, nz + 0.5
-- …… z.x, z.z を tx, tz へ近づける
end
end
end
通り抜けられるブロック(pass)
iso.path / iso.stand / iso.flow_build の pass(ブロック番号の配列)にしたブロックは、**体のマスにあってもよい(中を通り抜ける)が、足元にはならない(上には乗れない)**ブロックになります。バリケードのような「壁ではないが通ると遅い」ものに使います。body_cost = {[番号] = 値段} は、行き先の体のマスにそのブロックがあるたびに足す値段です(体の高さが2なら、2段積んだバリケードは2回数える)。
視線
iso.sight(x0, y0, z0, x1, y1, z1 [, pass]) -- → bool
線分がブロックに遮られないかを返します。空気・水・松明と、pass(ブロック番号の配列)のブロックは通します。出発点と到着点のマスは見ません(弓兵の立っている所・狙う相手の足元)。
近くの人や物
iso.nearby(x, z, range [, tags [, max]]) -- → { id, ... }(近い順)
中心 (x, z) から水平距離 range 以内の人や物を近い順に返します(32個まで。max で減らせる)。tags は tag の数1つか配列(省略すると全部)。
押し合い
iso.crowd([iterations [, { height =, pass = }]]) -- → 動いた数
crowd が "move" / "fixed" の人や物どうしを、水平に重ならない(中心の距離が r の和以上)よう押し離します。"fixed" は動きません。押し離す量は mass の逆数の比で分けます。高さが height(既定2)以上違うものは押しません。押された先の体のマスに、空気・水・松明・pass 以外のブロックがあれば、その軸は動かしません(壁へは押し込まない)。毎フレーム、人や物を動かした後に1回呼んでください(iterations は1〜8、既定2)。動いたものは自動で描き直します。高さ(y)は変えないので、押された後の地面の高さはアプリで合わせます。
弾
iso.shot_add(x, y, z, { target = id | tx =, ty =, tz =, speed =, arc =, color =, size =, tag = }) -- → id | nil, 理由
iso.shots_step(dt) -- dt は秒 → 当たった弾 { {id=, target=, tag=, lost=, x=, y=, z=}, ... }
iso.shot_remove(id) / iso.shot_clear() / iso.shot_count()
人や物ではない軽い点の弾です(64個まで。最初に撃ったときに約4.6KBのメモリを使います)。必ず当たります: target(人や物)なら、その体の中心(y + height/2)を追いかけて着いたときに当たり、target が途中で消えたら最後に見た位置へ飛んで lost = true で終わります。tx, ty, tz なら点へ飛びます。
speedはブロック/秒(既定8)、arcは山なりの高さ(水平距離に対する割合。0.5 なら10マス先へ投げると真ん中で5段高い。既定0)、color(既定0)、sizeは点の一辺の画素(1〜6、既定2)、tagは当たったときにそのまま返す数(ダメージの種類など)- 弾はブロックにも人や物にも隠れず、人や物の絵の上に描きます(視線が通るかは撃つ前に
iso.sightで確かめる) iso.shots_step(dt)を毎フレーム呼んでください。動いた弾は自動で描き直します
世界全体を読み込んだままにする
iso.keep_all(true) -- → bool(世界が 7x7 チャンク = 56x56 マスより広い、または置き場を確保できなければ false)
視点を動かしてもチャンクを手放しません(iso.pump で全部読み込みます)。マップ全体でゾンビが動き回るゲームでは、見ていない所の地面が空気に見えると困るので使います。
タワーディフェンスの地形(kind = 4)
local bx, by, bz = iso.create(dir, 4, seed, 7) -- 7x7 チャンク = 56x56 マス。ベースの中心(by は立つ高さ)
iso.keep_all(true)
local a = iso.arena() -- {base = {x=, y=, z=}, spawns = {{x=, y=, z=}, ...}}(y は立つ高さ)。ARENA でなければ nil
- 種から決まる、木の無いなだらかな地形(高さ4〜9。水面5より低い所は水で、底は砂)
- ベースは z の小さい端の真ん中(
x = 幅/2, z = 4)。まわりの 7x7 は平らな丸石で、さらに外側へなだらかにつなぐ - 出現位置は反対の端(
z = 幅 - 3)に3つ(x = 幅/4, 幅/2, 幅*3/4)。まわりの 3x3 は平らな砂利 - ベースと出現位置は水面より上。ただし間の地形で出現位置から道が無い種もありうるので、
iso.flow_buildで確かめて、 届かなければ種を変えて作り直すこと(例: アプリ「ゾンビTD」のgame.lua) - 何も書き出さないので、
dirはディレクトリが無くてもよい(地形をいじってiso.saveすれば書き出す) - メモリを減らすため、柱ごとに「地面の高さ・一番上のブロック・その上の1個」だけを持つ(56x56 で約13KB)。
iso.setで書き換えられるのは地面の上の1段(柵など)と地面の一番上の種類だけで、それ以外は無視される。松明の明るさも持たない(置けるが光らない)
速さの目安
描画と経路探索はOSの中で速く動きます。平らな所で端から端(30マス程度)を iso.path で探すのは一瞬です。ただし edge を渡すと1歩ごとにLuaを呼ぶので、そのぶん遅くなります。