【Playdate】パーリンノイズを使って濃淡に連続性あるタイルマップを作る

![capture_221225.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100912/f45227bb-1cbb-4af2-39b4-30dc3849de58.png)
Playdate SDKにはパーリンノイズに関するAPIもあります。
パーリンノイズを使うと上のキャプチャのように濃淡に連続性がある描画を行うことができます。
今回はパーリンノイズを使ってフィールドのタイルマップを生成してみます。

タイルマップの画像データは縦横20pxの2x2でレイアウトされた下の画像を使います。
![tile-table-20-20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100912/d7c32179-93d0-ce83-4bb8-fc2c07d75288.png)
`tile-table-20-20.png`という名称で`source/Images`に格納してください。

```main.lua
import "CoreLibs/object"
import "CoreLibs/graphics"
import "CoreLibs/sprites"
import "CoreLibs/timer"

local gfx <const> = playdate.graphics

-- タイル
local tilesize <const> = 20
local tileCol = 400 / tilesize
local tileRow = 240 / tilesize
local tileTable = {
	1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,
	
	1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,
	
	1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,
	1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1
}

-- パーリンノイズ
-- cf. https://chrisdownie.net/software/2022/03/31/playdate-perlin-noise/
local repeatValue = 0 -- 繰り返さない
local octaves = 5 -- 5つのPerlin値を組み合わせる
local persistence = 0.5 -- 各値は、前の値の半分の重さとして計算

-- tableから最小値最大値取得
local function getMinMaxNumTable(table)
	local key, min, max = 1, table[1], table[1]
	for k, v in ipairs(table) do
		if  table[k] < min then
			min = v
		elseif table[k] > max then
			max = v
		end
	end
	return min, max
end

function myGameSetUp()
	-- タイルセッティング
	local tileImageTable = gfx.imagetable.new("Images/tile")
	local tileMap = gfx.tilemap.new()
	tileMap:setImageTable(tileImageTable)
	tileMap:setTiles(tileTable,tilesize)
	-- tileCol, tileRow = tileMap:getSize() -- 正確に縦横列の数を取得したい場合はtileMap:getSize()で代入しても良い

    -- パーリンノイズ配列作成
    local perlinNoiseTable = gfx.perlinArray(tileCol * tileRow,
        0.5, 1,
        0.5, 0,
        0, 0,
        repeatValue,
        octaves,
        persistence)

    local min, max = getMinMaxNumTable(perlinNoiseTable)
    local thread = (max - min)/4
    local thread1 = min + thread
    local thread2 = min + thread*2
    local thread3 = min + thread*3
    local thread4 = min + thread*4
    
    for j=1, tileRow do
        for i=1, tileCol do
            local tileIndex = 1
            local index = (j-1)*tileCol + i 
            if perlinNoiseTable[index] < thread1 then
                tileIndex = 1
            elseif perlinNoiseTable[index] < thread2 then
                tileIndex = 2
            elseif perlinNoiseTable[index] < thread3 then
                tileIndex = 3
            elseif perlinNoiseTable[index] < thread4 then
                tileIndex = 4
            end
            tileMap:setTileAtPosition(i, j, tileIndex)
        end
    end

	gfx.sprite.setBackgroundDrawingCallback(
		function( x, y, width, height )
			tileMap:draw(0, 0)
		end
	)

end

myGameSetUp()

function playdate.update()
	gfx.sprite.update()
	playdate.timer.updateTimers()

end
```

まず、タイルに関する変数を記述します。
タイルのサイズ、横・縦列のタイル数、タイルの表示情報のtableを記述しています。tableの値はタイル画像のインデックス値(1〜4)が入ります。

次にパーリンノイズの変数を記述しています。playdateにおけるパーリンノイズの説明については以下ページに詳細が記載されています。

https://chrisdownie.net/software/2022/03/31/playdate-perlin-noise/

先に`myGameSetUp`関数に記載されているスクリプトから説明します。
タイルパーツとなる2x2のタイル画像を`gfx.imagetable.new("Images/tile")`で読み込みます。
次に`gfx.tilemap.new()`でタイルマップオブジェクトを生成し、`tileMap:setImageTable(tileImageTable)`でタイルのimageTableをセットします。
`tileMap:setTiles(tileTable,tilesize)`ではタイルの表示情報のtableとタイルのサイズをセットしています。

次にパーリンノイズ配列を作成します。
`gfx.perlinArray([個数], x, dx, y, dy, z, dz, [繰り返し回数],[オクターブ],[持続性])`で指定したパラメータの結果を0〜1の範囲で数値の配列を返します。
続いて生成したパーリンノイズの配列の最小値と最大値を前行で作成した`getMinMaxNumTable`関数を使って取得します。
この取得した最小値と最大値を4つの範囲に区切り、範囲内の値を元にタイル画像のインデックス値を割り振ります。(愚直にやってるthread1〜4が範囲の区切り)
割り振ったタイル画像のインデックスは、`tileMap:setTileAtPosition(i, j, tileIndex)`でタイル画像を再設定します。
最後に`tileMap:draw(0, 0)`でタイルマップの描画を行います。