Playdateでプラットフォーム・ゲームっぽい何かの実装例、タイルマップの使い方について

Playdateでのタイルマップの使い方について記事にします。

開発環境や事前準備は以前の記事を参考にしてください。

目次

コードと実行結果

先にコード例と実行結果を載せます。

まず実行結果は以下の通りになります。

タイルマップの実装例

地面のブロックが画面に並べられてプレイヤーがその上を歩いているのが分かります。また動かせるブロックのギミックもあります。動かせるブロックはタイルマップではなく前回の記事で作成したBlockオブジェクトです。

プロジェクトのフォルダ構成は以下の通りです。

.
├── LICENSE
├── README.md
├── run.sh
└── source
    ├── actors
    │   ├── actor.lua
    │   ├── block.lua
    │   ├── hart.lua
    │   └── player.lua
    ├── images
    │   └── spritesheets
    │       ├── block
    │       │   └── idle-table-16-16.png
    │       ├── hart
    │       │   └── idle-table-16-16.png
    │       ├── player
    │       │   ├── idle-table-16-16.png
    │       │   └── walk-table-16-16.png
    │       └── tilemap-table-16-16.png
    ├── main.lua
    └── maps
        └── map.lua

LICENSEとREADME.mdは無視して大丈夫です。前回の記事と違い追加されたのはmaps/map.luaです。

source/actors/actor.luaソースコードは以下の通りです。

---@diagnostic disable: undefined-global
class('Actor').extends()
function Actor:init(_stageRect, _zindex, _groupIds)
    self.zindex    = _zindex
    self.stageRect = _stageRect
    self.groupIds  = _groupIds
    self.currentImagetable = nil
    self.currentLoop = nil
    self.currentSprite = nil
    self.state = 0
    self.x = 0
    self.y = 0
    self.floorPosition = 0
end

function Actor:commonSpriteSettings()
    if self.currentSprite then
        self.currentSprite:remove()
    end

    self.currentSprite = playdate.graphics.sprite.new(self.currentLoop:image())
    self.currentSprite:setCenter(0, 0)
    self.currentSprite:setZIndex(self.zindex)
    self.currentSprite:setGroups(self.groupIds)
    self.currentSprite:setCollidesWithGroups(self.groupIds)
    -- https://sdk.play.date/2.1.1/Inside%20Playdate.html#m-graphics.sprite.setCollideRect
    self.currentSprite:setCollideRect(0, 0, self.currentSprite:getSize())
    self.currentSprite.collisionResponse = playdate.graphics.sprite.kCollisionTypeFreeze

    self.currentSprite:moveTo(self.x, self.y)
end

function Actor:move(_ignore)
    local cx, cy, _, count = self.currentSprite:checkCollisions(self.x, self.y)
    if count > 0 then
        if not _ignore then
            self.currentSprite:moveTo(cx, cy)
            self.x = cx
            self.y = cy
            self.floorPosition = cy -- Keep floor coordinates.
        end
    else
        self.currentSprite:moveTo(self.x, self.y)
    end
end

function Actor:setPosition(_cellx, _celly)
    local spriteWidth, spriteHeight = self.currentSprite:getSize()
    self.x = _cellx * spriteWidth
    self.y = _celly * spriteHeight
    self.currentSprite:moveTo(self.x, self.y)
end

source/actors/player.luaソースコードは以下の通りです。

import "actors/actor"
---@diagnostic disable: undefined-global
class('Player').extends(Actor)
function Player:init(_stageRect, _zindex, _groupIds, _jumpPower, _speed)
    Player.super.init(self, _stageRect, _zindex, _groupIds)
    self.states = {
        idle = 1;
        walk = 2;
    }

    self.directions = {
        right = 1;
        left  = 2;
    }

    self.direction = self.directions.right

    self.idleFrametime = 200
    self.walkFrametime = 200
    self.idlePath = "images/spritesheets/" .. self.className:lower() .. "/idle"
    self.walkPath = "images/spritesheets/" .. self.className:lower() .. "/walk"

    self.jumpping = false
    self.dropping = false

    self.jumpReimitHeight = 0
    self.jumpPower = _jumpPower or 4
    self.speed = _speed or 5
end

function Player:turnLeft()
    self.direction = self.directions.left
end

function Player:turnRight()
    self.direction = self.directions.right
end

function Player:lowestPoint()
    local _, spriteHeight = self.currentSprite:getSize()
    self.jumpReimitHeight = (spriteHeight * 2.5)
    return self.stageRect.height - spriteHeight
end

function Player:jump()
    local maxHeight = self.floorPosition - self.jumpReimitHeight

    if not self.jumpping then
        self.jumpping = true
    end

    if self.jumpping then

        if not self.dropping then
            self.y = self.y - self.jumpPower
        else
            local y = self.y + self.jumpPower
            local _, _, _, count = self.currentSprite:checkCollisions(self.x, y)
            if count <= 0 then
                self.y = self.y + self.jumpPower
            else
                self.dropping = false
                self.jumpping = false
            end
        end

        if self.y <= maxHeight then
            self.dropping = true
            self.y = maxHeight
        end
    end
end

function Player:drop()
    self.y = self.y + self.jumpPower
end

function Player:isFloorPresent()
    local y = self.y + self.jumpPower
    local _, _, _, count = self.currentSprite:checkCollisions(self.x, y)
    if count > 0 then
        return true
    end
    return false
end

function Player:pushBlock(_blocks)
    local _, _, collsitions, count = self.currentSprite:checkCollisions(self.x, self.y)
    for i=1, count do
        local collision = collsitions[i]
        for j=1, #_blocks do
            local block = _blocks[j]
            if collision.other == block.currentSprite then
                local _, _, _, count = block.currentSprite:checkCollisions(self.x, self.y)
                if count > 0 then
                    if self.direction == self.directions.left then
                        block.x = block.x - 1
                    else
                        block.x = block.x + 1
                    end

                    block:move(true)
                end
            end
        end
    end
end

function Player:waitMove()
    self:drop()
    self:idle()
    self:move()
end

function Player:jumpMove()
    self:jump()
    self:move()
end

function Player:rightMove(_block)
    self.x = self.x + self.speed
    self:turnRight()
    self:walk()

    if _block then
        self:pushBlock(_block)
    end

    self:move()
    if not self:isFloorPresent() then
        self:drop()
    end
end

function Player:leftMove(_block)
    self.x = self.x - self.speed
    self:turnLeft()
    self:walk()

    if _block then
        self:pushBlock(_block)
    end

    self:move()
    if not self:isFloorPresent() then
        self:drop()
    end
end

function Player:rightJumpMove(_speed)
    self.x = self.x + self.speed
    self:turnRight()
    self:walk()
    self:move()
    self:jump()
end

function Player:leftJumpMove(_speed)
    self.x = self.x - self.speed
    self:turnLeft()
    self:walk()
    self:move()
    self:jump()
end

function Player:idle()
    if not (self.state == self.states.idle) then
        self.currentImagetable = playdate.graphics.imagetable.new(self.idlePath)
        self.currentLoop = playdate.graphics.animation.loop.new(self.idleFrametime, self.currentImagetable, true)
        self:commonSpriteSettings(idlePath, self.walkFrametime, true)
    end

    self.currentSprite.update = function()
        self.currentSprite:setImage(self.currentLoop:image())
        if not self.currentLoop:isValid() then
            self.currentSprite:remove()
        end
    end

    self.currentSprite:add()

    self.state = self.states.idle
end

function Player:walk()
    if not (self.state == self.states.walk) then
        self.currentImagetable = playdate.graphics.imagetable.new(self.walkPath)
        self.currentLoop = playdate.graphics.animation.loop.new(self.walkFrametime, self.currentImagetable, true)
        self:commonSpriteSettings()
    end

    self.currentSprite.update = function()
        self.currentSprite:setImage(self.currentLoop:image())
        if self.direction == self.directions.left then
            self.currentSprite:setImageFlip("flipX")
        end

        if not self.currentLoop:isValid() then
            self.currentSprite:remove()
        end
    end

    self.currentSprite:add()

    self.state = self.states.walk
end

source/actors/hart.luaソースコードは以下の通りです。

import "actors/actor"
---@diagnostic disable: undefined-global
class('Hart').extends(Actor)
function Hart:init(_stageRect, _zindex, _groupIds)
    Hart.super.init(self, _stageRect, _zindex, _groupIds)
    self.states = {
        idle = 1;
    }

    self.idleFrametime = 200
    self.idlePath = "images/spritesheets/" .. self.className:lower() .. "/idle"
end

function Hart:idle()
    if not (self.state == self.states.idle) then

        if self.currentSprite then
            self.currentSprite:remove()
        end

        self.currentImagetable = playdate.graphics.imagetable.new(self.idlePath)
        self.currentLoop = playdate.graphics.animation.loop.new(self.idleFrametime, self.currentImagetable, true)
        self:commonSpriteSettings()
    end

    self.currentSprite.update = function()
        self.currentSprite:setImage(self.currentLoop:image())
        if not self.currentLoop:isValid() then
            self.currentSprite:remove()
        end
    end

    self.currentSprite:add()

    self.state = self.states.idle
end

function Hart:collideWithPlayer(_player)
    -- https://sdk.play.date/2.1.1/Inside%20Playdate.html#m-graphics.sprite.alphaCollision
    if _player.currentSprite:alphaCollision(self.currentSprite) then
        self.currentSprite:remove()
    end
end

source/actors/block.luaソースコードは以下の通りです。

import "actors/actor"
---@diagnostic disable: undefined-global
class('Block').extends(Actor)
function Block:init(_stageRect, _zindex, _groupIds, _weight)
    Block.super.init(self, _stageRect, _zindex, _groupIds)
    self.states = {
        idle = 1;
    }

    self.idleFrametime = 200
    self.idlePath = "images/spritesheets/" .. self.className:lower() .. "/idle"

    self.weight = _weight or 5
end

function Block:dropMove()
    self.y = self.y + self.weight
    self:move()
end

function Block:idle()
    if not (self.state == self.states.idle) then

        if self.currentSprite then
            self.currentSprite:remove()
        end

        self.currentImagetable = playdate.graphics.imagetable.new(self.idlePath)
        self.currentLoop = playdate.graphics.animation.loop.new(self.idleFrametime, self.currentImagetable, true)
        self:commonSpriteSettings()
    end

    self.currentSprite.update = function()
        self.currentSprite:setImage(self.currentLoop:image())
        if not self.currentLoop:isValid() then
            self.currentSprite:remove()
        end
    end

    self.currentSprite:add()

    self.state = self.states.idle
end

source/maps/map.luaソースコードは以下の通りです。

---@diagnostic disable: undefined-global
class('Map').extends()
function Map:init(_mapArray, _zindex, _displayWidth, _groudIds)
    self.mapArray     = _mapArray
    self.zindex       = _zindex
    self.displayWidth = _displayWidth
    self.groudIds     = _groudIds
    self.path = "images/spritesheets/tilemap"

    self.x = 0
    self.y = 0

    self.complete = false

    local tilemapImage = playdate.graphics.imagetable.new(self.path)
    self.tilemap = playdate.graphics.tilemap.new()
    self.tilemap:setImageTable(tilemapImage)

    local tileWidth, _ = self.tilemap:getTileSize()
    self.tilemap:setTiles(self.mapArray, (self.displayWidth / tileWidth))

    self.currentSprite = playdate.graphics.sprite.new(self.tilemap)
    self.currentSprite:setCenter(0, 0)
    self.currentSprite:moveTo(self.x, self.y)
    self.currentSprite:setZIndex(self.zindex)
    self.currentSprite:add()
end

function Map:setCollsions(emptyIds)
    if not self.complete then
        -- https://sdk.play.date/2.2.0/Inside%20Playdate.html#f-graphics.sprite.addWallSprites
        local collisions = playdate.graphics.sprite.addWallSprites(self.tilemap, emptyIds)
        for i=1, #collisions do
            collisions[i].collisionResponse = playdate.graphics.sprite.kCollisionTypeFreeze
            collisions[i]:setGroups(self.groudIds)
            collisions[i]:setCollidesWithGroups(self.groudIds)
        end
        self.complete = true
    end
end

そしてsource/main.luaソースコードは以下の通りです。

if not import then import = require end

import "CoreLibs/graphics"
import "CoreLibs/object"
import "CoreLibs/sprites"
import "CoreLibs/animation"

import "maps/map"
import "actors/player"
import "actors/hart"
import "actors/block"

local function clearDisplay()
    playdate.graphics.clear()
    playdate.graphics.setBackgroundColor(playdate.graphics.kColorBlack)
end

local groups = {
    collision = 1,
    item = 2
}

local mapArray = {
--  0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16  17  18  19  20  21  22  23  24
    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,  -- 0
    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,  -- 2
    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,  -- 3
    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,  -- 4
    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,  -- 5
    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,  -- 6
    12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1,  1,  1,  1,  1,  1,  1,  1,  -- 7
    1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  12, 12, 12, 1,  1,  1,  1,  1,  1,  -- 8
    1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  12, 12, 1,  1,  1,  1,  1,  -- 9
    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,  -- 10
    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,  -- 11
    1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  12, 12, 12, 12, 12, 1,  12, 12, 12, 12, -- 12
    1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, -- 13
    12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, -- 14
}
local mapEmptyIds = { 1 }

local playerGroupIds = { groups.collision }
local mapGroupIds    = { groups.collision }
local hartGroupIds   = { groups.item }
local blockGroupIds  = { groups.collision }

local mapIndex = -1
local playerIndex = 1
local hartIndex   = 1
local blockIndex  = 1

local stageRect = playdate.geometry.rect.new(0, 0, playdate.display.getWidth(), playdate.display.getHeight())
local map = Map(mapArray, mapIndex, stageRect.width, mapGroupIds)

local player = Player(stageRect, playerIndex, playerGroupIds)
player:idle()
player.y = player:lowestPoint()
player:setPosition(0, 14)

local hart = Hart(stageRect, hartIndex, hartGroupIds)
hart:idle()
hart:setPosition(1, 6)

local block_1 = Block(stageRect, blockIndex, blockGroupIds)
block_1:idle()
block_1:setPosition(21, 11)

local block_2 = Block(stageRect, 1, blockGroupIds)
block_2:idle()
block_2:setPosition(18, 11)

local blocks = { block_1, block_2 }

function playdate.update()
    clearDisplay()

    -- https://sdk.play.date/2.1.1/Inside%20Playdate.html#f-graphics.sprite.update
    playdate.graphics.sprite.update()

    map:setCollsions(mapEmptyIds)

    if playdate.buttonIsPressed(playdate.kButtonRight) and playdate.buttonIsPressed(playdate.kButtonA) then
        player:rightJumpMove()
    elseif playdate.buttonIsPressed(playdate.kButtonLeft) and playdate.buttonIsPressed(playdate.kButtonA) then
        player:leftJumpMove()
    elseif playdate.buttonIsPressed(playdate.kButtonRight) then
        player:rightMove(blocks)
    elseif playdate.buttonIsPressed(playdate.kButtonLeft) then
        player:leftMove(blocks)
    elseif playdate.buttonIsPressed(playdate.kButtonA) then
        player:jumpMove()
    else
        player:waitMove()
    end

    block_1:dropMove()
    block_2:dropMove()

    hart:collideWithPlayer(player)
end

タイルマップの解説

タイルマップの読み込みと設定を行っているのは、map.luaMapクラスです。

    local tilemapImage = playdate.graphics.imagetable.new(self.path)
    self.tilemap = playdate.graphics.tilemap.new()
    self.tilemap:setImageTable(tilemapImage)

tilemapのスプライトシートがあるパスを指定して、ImageTableオブジェクトとして読み込みます。 そして、Tilemapオブジェクトを新規に作成して、:setImageTable()で読み込んだImageTableオブジェクトを引数に指定することでタイルマップとして扱うことができるようになります。

TilemapオブジェクトにImageTableオブジェクトを読み込ませた時点でPlaydateでは以下のようにタイルマップを扱うよう設定されます。

タイルマップのスプライトシート

まずこれは今回の実装例で読み込んだスプライトシートです。

タイルマップのインデックス

上図のように原点から右方向に1次元配列としてタイルに1,2,3,4…とインデックス番号が割り振られます。今回の実装例では12番の番号が振られたタイルを使いマップを作成しています。

タイルのインデックス番号はカスタマイズも可能みたいです。今回の例では使用していません。

    local tileWidth, _ = self.tilemap:getTileSize()
    self.tilemap:setTiles(self.mapArray, (self.displayWidth / tileWidth))

タイルマップからマップを作成している個所です。:setTiles(self.mapArray, (self.displayWidth / tileWidth))にマップを表現している1次元配列と横一列に並べるタイルの最大幅を指定します。

この際の最大幅はピクセル数ではなくタイル数です。そのため、上記の例ではPlaydateの解像幅400pxに対して、1タイルの幅16pxを割り25タイルが指定されています。

折り返しまでの最大幅が分かれば以下の様な配列でマップを表現することが可能です。

local mapArray = {
--  0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16  17  18  19  20  21  22  23  24
    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,  -- 0
    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,  -- 2
    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,  -- 3
    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,  -- 4
    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,  -- 5
    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,  -- 6
    12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 1,  1,  1,  1,  1,  1,  1,  1,  -- 7
    1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  12, 12, 12, 1,  1,  1,  1,  1,  1,  -- 8
    1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  12, 12, 1,  1,  1,  1,  1,  -- 9
    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,  -- 10
    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,  -- 11
    1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  12, 12, 12, 12, 12, 1,  12, 12, 12, 12, -- 12
    1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, -- 13
    12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, -- 14
}

横25タイル、高さ15タイルの1次元配列です。コードの見た目では2次元配列の形に整形しているだけなのに注意してください。番号は表示するタイルのインデックス番号を表しています。

(コメント箇所はプレイヤーやブロック、ハートを配置する際に座標値を分かりやすくするために振っています。)

    self.currentSprite = playdate.graphics.sprite.new(self.tilemap)
    self.currentSprite:setCenter(0, 0)
    self.currentSprite:moveTo(self.x, self.y)
    self.currentSprite:setZIndex(self.zindex)
    self.currentSprite:add()

TilemapオブジェクトもSpriteオブジェクトに変換できます。Spriteオブジェクトに変換して:add()メソッドで画面に表示されます。

タイルマップへ衝突検知を設定する

タイルマップもSpriteオブジェクトに変換できているため、setCollideRect()を設定可能です。ですが1タイルずつsetCollideRect()すると大変です。衝突検知が必要なタイルのみに衝突判定を行う矩形領域を設定できる便利なメソッドがあります。

function Map:setCollsions(emptyIds)
~ 省略 ~
        -- https://sdk.play.date/2.2.0/Inside%20Playdate.html#f-graphics.sprite.addWallSprites
        local collisions = playdate.graphics.sprite.addWallSprites(self.tilemap, emptyIds)
~ 省略 ~
end

:addWallSprites()メソッドです。

第2引数のemptyIdsは衝突検知が必要ないタイルのインデックス番号を指定します。配列形式なので複数指定可能です。今回の実装例は1番と12番のタイルのみでマップを構成していて、12番のみ床と壁として機能して欲しいので1番を指定しています。

function playdate.update()
    ~ 省略 ~
    -- https://sdk.play.date/2.1.1/Inside%20Playdate.html#f-graphics.sprite.update
    playdate.graphics.sprite.update()

    map:setCollsions(mapEmptyIds)
    ~ 省略 ~
end

main.luaからの抜粋です。:addWallSprites()sprite.update()が行われた後に呼び出すようにしています。こうしないとタイルマップに設定される衝突範囲がずれます。map:setCollsions()メソッドでは:addWallSprites()を1度のみしか呼び出さないようフラグで制御するよう実装しています。参考にしてください。

衝突検知を設定したタイルの上を歩行させる

前回の記事ではmoveWithCollisions()メソッドを使用して、Spriteオブジェクト同士の衝突を検知してめり込まないよう実装していました。ただ、moveWithCollisions()メソッドは位置補正も行う都合上、衝突した際の挙動を細かく制御できません。そこでタイルの上を歩行させるためにcheckCollisions()を使用します。

actor.luaからの抜粋です。

function Actor:move(_ignore)
    local cx, cy, _, count = self.currentSprite:checkCollisions(self.x, self.y)
    if count > 0 then
        if not _ignore then
            self.currentSprite:moveTo(cx, cy)
            self.x = cx
            self.y = cy
            self.floorPosition = cy -- Keep floor coordinates.
        end
    else
        self.currentSprite:moveTo(self.x, self.y)
    end
end

Spriteオブジェクトを通常移動させる仕組みはすべてここに集約させています。checkCollisions()メソッドが返却する戻り値はmoveWithCollisions()メソッドと一緒です。違いは戻り値を使った位置補正を勝手に行いません。

また、今回はマップの上を歩行させるために衝突検知の種別を以下のように設定しています。

function Actor:commonSpriteSettings()
    ~ 省略 ~
    self.currentSprite.collisionResponse = playdate.graphics.sprite.kCollisionTypeFreeze

    self.currentSprite:moveTo(self.x, self.y)
end

これは、衝突した際に返却する補正座標をどのように算出するかの種別を指定しています。

4種類ほど列挙型が設定されているので作成するゲームに合わせて設定します。今回は、すべてのオブジェクトが衝突した時点での座標があればよいのでkCollisionTypeFreezeが設定されます。

マップのタイル上の歩行方法のロジックはシンプルで、checkCollisions()メソッドのcountの戻り値で1件以上衝突が確認出来たら、戻り値の座標にSpriteオブジェクトを移動させる。衝突が確認できなければ移動しようとしていた。self.x, self.yの位置にそのまま移動させています。

まとめ

他にも、ジャンプやブロックを動かしたりしていますが記事の主題ではないので解説自体は省略致します。でも今回はリポジトリを用意したのでPlaydateのSDKをインストールされているのであればリポジトリをクローンして挙動を試してみて頂ければと思います。

Playdateでのゲーム開発で気づいたことがあれば自分の為にも随時記事にしようと思います。