--:-- LUA API

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辺のマス数 | nilworld.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 ayw // 2, h - 1足元の点(範囲の左上から)
rw / 64(0.05〜0.49)当たりの箱の半径(ブロックの単位)。絵の横幅の画面の幅が 64 * r
heighth / 16当たりの箱の高さ(ブロック1段が16px)
flipfalse左右反転
visibletrue隠す
shadowtrue足元に影を落とす
shadow_color0影の色(パレット番号)
barなしHPバー(0〜100 の割合)。絵の上に、ブロックにも他の人や物にも隠れず一番上に描く。false で消す
bar_color11HPバーの色
markなし選択の印(頭の上の小さな下向きの三角)の色。一番上に描く。false で消す
tag0分類(0〜31)。iso.nearby で絞り込むのに使う
crowd"none"押し合い(iso.crowd): "move" 押し合って動く / "fixed" 動かず押し返すだけ(タワー等) / "none" 関わらない
mass1押し合いの重さ(重いほど押されにくい)
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_up11歩で登れる段数(これより高い段差は通れない)
max_down21歩で降りられる段数
height2体の高さ(マス)。低いトンネルを通るかが変わる
diagonalfalse斜めにも進む(斜めの1歩は step × √2)
swimfalse体のマスに水があってもよい(水の中を歩く)
step11歩の値段(正の数)
up_cost01段登るごとに足す値段(段差の大きい道ほど高くつく = 避けられる)
down_cost01段降りるごとに足す値段
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_nodes1024調べる点の上限(1〜4096)。作業場所は探す間だけ使う(1点あたり約26バイト)
partialfalse着けないとき、目的地に一番近づける所までの道を返す(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_up max_down height diagonal swim step up_cost down_cost avoid pass block_cost body_cost。max_nodes partial edge は使えません)。登りと降りの向きは「歩く側から見た向き」で判定します
  • 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を呼ぶので、そのぶん遅くなります。