SDKのドキュメントをしっかり読むと記事タイトルにあるような機能はしっかり用意されているのでその使い方を記録した記事です。記事にしておかないと忘れてしまいます…
ちなみに、気づくまで自力で実装しようとして頑張っていたのは内緒です。
目次
- 事前準備
- スプライトシートでキャラクターをアニメーションさせる
- スプライトシートとSpriteを使用してアニメーションさせる
- アニメーションを状態遷移を伴いながら左右へ移動させる
- 衝突を検知する
- まとめ
事前準備
開発環境はLinuxかmacOSを想定します。Windowsでもほぼ同じです。プロジェクトでコンパイルを便利にするためのBashファイルはWindowsでは使用できないのでそこは適宜読み替えてください。
サンプルに使用する素材は以下からダウンロードします。
ライセンスはCC0なので安心して使用できます。ダウンロード時に寄付を求められますが寄付せずとも落とせます。
ダウンロードした素材をドット絵エディタ等で以下のように加工します。
![]()
![]()
![]()
※ 背景の#000000の箇所は透明にしてください。はてなの仕様で透明箇所が正しく表示されないのでこの記事では背景を#000000にしています。
プロジェクトを以下に示すディレクトリ構成にします。
root
├── run.sh
└── source
├── images
│ └── spritesheets
│ ├── hart
│ │ └── idle-table-16-16.png
│ ├── player
│ │ ├── idle-table-16-16.png
│ │ └── walk-table-16-16.png
│ └── tilemap-table-16-16.png
└── main.lua
加工した画像はsource/images/spritesheet/<任意のディレクトリ名>/に保存します。
画像のファイル名は必ず<任意のプリフィックス>-table-<1セルの幅>-<1セルの高さ>.pngの形式に従って命名する必要があります。
- Image table
- ドキュメントでは連番による命名方法もありますがこの記事では上記形式で進めます
また加工元になったスプライトシート画像はタイルマップとしてsource/images/spritesheet/の箇所にtilemap-table-16-16.pngのファイル名で保存します。
※ この記事ではタイルマップの解説までは行いません。別記事を作成予定です。
run.shの中身は以下をコピペします。
currentDirectory=`pwd | awk -F "/" '{ print $NF }'`
rm -rf "$currentDirectory.pdx"
pdc -k source "$currentDirectory.pdx"
PlaydateSimulator "$currentDirectory.pdx"
これはエミュレーターでプロジェクトを実行する際にコマンドを複数回打つのが面倒くさいのでまとめたスクリプトになります。環境ごとに適宜読み替えてください。
以上の構成をベースにして解説を行います。
スプライトシートでキャラクターをアニメーションさせる
一番簡単な例を示します。ドキュメントの以下のリンクにもほぼ同じ例が提示されています。
if not import then import = require end import "CoreLibs/graphics" import "CoreLibs/animation" local function clearDisplay() playdate.graphics.clear() playdate.graphics.setBackgroundColor(playdate.graphics.kColorBlack) end local frameTime = 200 local playerTable = playdate.graphics.imagetable.new("images/spritesheets/player/idle") local playerLoopIdle = playdate.graphics.animation.loop.new(frameTime, playerTable, true) local cell = playerTable:getImage(1) local _, height = cell:getSize() local playerX = 10 local playerY = playdate.display.getHeight() - height -- -- コールバック -- -- ゲームループ function playdate.update() clearDisplay() playerLoopIdle:draw(playerX, playerY) end
main.luaに上記コードをコピペして、プロジェクトのルートディレクトリにて./run.shを実行してください。エミュレーターが起動して以下のGIFのように動作するはずです。

基本的にはこれでスプライトシートによるアニメーションを実現できます。:draw()メソッドに与える座標値で移動も可能です。とはいえ、この方式はタイトル画面だったり見た目だけアニメーションで表示したい画像に有効でしょう。
いざ、ゲームとして組み込むとなると色々足りないことに気づきます。
- 状態遷移を行いたい
- 例えば待機のアニメーションから歩行のアニメーションへの切り替え
- 壁やアイテムとぶつかった時の挙動
- いわゆる衝突判定
- 画像が重なった時に背面や前面を考慮したい
- 奥行きの概念
こういった事例に対応する方法としては考えられるのはまずは自前での実装です。画面に画像を描画する機能さえあれば大抵のゲームは開発可能です。アニメーションまでは表示できているのだからこの状態に肉付けすれば良いと考えられます。
とはいえさすがに自前での実装は辛いとなるのが世の常です。ということでSDKにはこれらを実現するための機能が提供されています。
その機能を使用するためにはPlaydateにおけるImageオブジェクトとSpriteオブジェクトの違いを知る必要があります。以下にまとめました。
ImageとSpriteの違い
Image
:draw()メソッドの呼び出しによる描画- 呼び出した時点での最前面に描画される
- SDK標準の衝突判定の設定ができない
Sprite
- Imageから生成する
:add()した時点で描画されるz-indexを設定できる- 奥行きのこと
- 数値で画像の重ね順を制御できる
- 奥行きのこと
:remove()で描画を消せる- SDK標準の衝突判定の設定できる
アニメーションに使用したImageTableオブジェクトはImageオブジェクトの派生です。そのためSpriteオブジェクトにある機能は持っていません。
Spriteオブジェクトの最大のポイントはz-indexと衝突判定を設定できる点です。まさに欲しい機能を自前で実装する必要がなくなるのはかなり大きいためゲームで使用する画像は極力Spriteオブジェクトで描画したいです。
ということで、スプライトシートをどのように読み込みコーディングすればSpriteオブジェクトとしてアニメーションさせることができるのか?その実例を紹介します。
スプライトシートとSpriteを使用してアニメーションさせる
まずはスプライトシートでキャラクターをアニメーションさせるの状態をSpriteオブジェクトで再現するまでのコーディング例を示します。
ディレクトリ構成は以下のようになります。
root
├── run.sh
└── source
├── actors
│ ├── actor.lua
│ └── player.lua
├── images
│ └── spritesheets
│ ├── hart
│ │ └── idle-table-16-16.png
│ ├── player
│ │ ├── idle-table-16-16.png
│ │ └── walk-table-16-16.png
│ └── tilemap-table-16-16.png
└── main.lua
sourceディレクトリ配下にactorsディレクトリが作成され、actor.luaとplayer.luaが配置されています。なおこのディレクトリ名やファイル名、このあと記述するクラス名等は単なる筆者の好みなので適宜読み替えてください。
actor.luaは以下の通りです。
---@diagnostic disable: undefined-global class('Actor').extends() function Actor:init(_stageRect, _zindex) self.zindex = _zindex self.stageRect = _stageRect self.currentImagetable = nil self.currentLoop = nil self.currentSprite = nil self.state = 0 self.idlePath = "images/spritesheets/" .. self.className:lower() .. "/idle" end function Actor:commonSpriteSettings() self.currentSprite:setCenter(0, 0) self.currentSprite:setZIndex(self.zindex) end
続いてplayer.luaは以下の通りです。
import "actors/actor" ---@diagnostic disable: undefined-global class('Player').extends(Actor) function Player:init(_stageRect, _zindex) Player.super.init(self, _stageRect, _zindex) self.states = { idle = 1; walk = 2; } self.directions = { right = 1; left = 2; } self.direction = self.directions.right self.idleFrametime = 200 self.walkFrametime = 200 self.walkPath = "images/spritesheets/" .. self.className:lower() .. "/walk" self.x = 10 self.y = 0 end function Player:turnLeft() self.direction = self.directions.left end function Player:turnRight() self.direction = self.directions.right end function Player:move(_x, _distanceMovedY) self.x = _x or self.x local _, spriteHeight = self.currentSprite:getSize() if _distanceMovedY then self.y = self.stageRect.height - spriteHeight - _distanceMovedY else self.y = self.stageRect.height - spriteHeight end self.currentSprite:moveTo(self.x, self.y) end function Player: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.currentSprite = playdate.graphics.sprite.new(self.currentLoop:image()) 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 Player:walk() if not (self.state == self.states.walk) then if self.currentSprite then self.currentSprite:remove() end self.currentImagetable = playdate.graphics.imagetable.new(self.walkPath) self.currentLoop = playdate.graphics.animation.loop.new(self.walkFrametime, self.currentImagetable, true) self.currentSprite = playdate.graphics.sprite.new(self.currentLoop:image()) 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
そしてmain.luaは以下の通りに書き換わります。
if not import then import = require end import "CoreLibs/graphics" import "CoreLibs/object" import "CoreLibs/sprites" import "CoreLibs/animation" import "actors/player" local function clearDisplay() playdate.graphics.clear() playdate.graphics.setBackgroundColor(playdate.graphics.kColorBlack) end local stageRect = playdate.geometry.rect.new(0, 0, playdate.display.getWidth(), playdate.display.getHeight()) local player = Player(stageRect, 1) player:idle() player:move(10) -- -- コールバック -- -- ゲームループ function playdate.update() clearDisplay() -- https://sdk.play.date/2.1.1/Inside%20Playdate.html#f-graphics.sprite.update -- SpriteのCallbackを呼び出すために必要 playdate.graphics.sprite.update() end
同じことをするだけなはずなのに一気にコーディング量が増えました。状態遷移も実現するためにオブジェクト指向で組んでいるためです。
とりあえずコピペをして頂き、./run.shを打ち込めばスプライトシートでキャラクターをアニメーションさせると同じ状態が表示されるはずです。
Spriteオブジェクトを使用してアニメーションさせようとするだけで大分傾向が変わりました。解説を行います。
まずActorクラスはゲーム内に配置するプレイヤー及びキャラクターや物体等のギミック的な何かの共通処理を担う基底クラスとして定義しています。状態遷移やアニメーション、衝突検知に必要な挙動はある程度共通化できるためです。行っていることは初期化時に必要なメンバーを用意しているのと、共通処理のメソッドが1つあるのみです。
続いてPlayerクラスの解説です。
function Player:init(_stageRect, _zindex) self.states = { idle = 1; walk = 2; } self.directions = { right = 1; left = 2; } self.direction = self.directions.right self.idleFrametime = 200 self.walkFrametime = 200 self.walkPath = "images/spritesheets/" .. self.className:lower() .. "/walk" self.x = 10 self.y = 0 end
コードの抜粋です。ここのポイントはActorクラスを継承しています。そしてPlayerクラスのみで使用するメンバーを定義しています。
function Player:idle() -- ~ 略 ~ end
アニメーションのポイントは上記メソッドになります。
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.currentSprite = playdate.graphics.sprite.new(self.currentLoop:image()) self:commonSpriteSettings() end
Player:idle()メソッドからの抜粋です。
self.stateがidleでなければ、self.currentImagetable = playdate.graphics.imagetable.new(self.idlePath)でImageTableを生成して、self.currentLoop = playdate.graphics.animation.loop.new(self.idleFrametime, self.currentImagetable, true)からアニメーションを生成するところまではスプライトシートでキャラクターをアニメーションさせると同じです。
self.currentSprite = playdate.graphics.sprite.new(self.currentLoop:image())ここでアニメーションループの:image()メソッドを使いSpriteオブジェクトを生成しています。
Spriteオブジェクトは前述の通り、生成するだけでは描画されません。:add()する必要があります。
-- 描画リストに追加
self.currentSprite:add()
このコード箇所にて追加していることが分かります。アニメーションの必要がないのであればこれだけで画面上に描画されます。
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
Spriteオブジェクトをアニメーションさせる肝はこの箇所です。Spriteクラスはupdateというコールバックを持っています。この箇所でそのコールバック関数をオーバーライドしています。
そして、self.currentSprite:setImage(self.currentLoop:image())にてコールバックが呼ばれる度にself.currentSpriteのSpriteオブジェクトの画像を張り替えています。これによりSpriteオブジェクトでもアニメーションが行われます。
local stageRect = playdate.geometry.rect.new(0, 0, playdate.display.getWidth(), playdate.display.getHeight()) local player = Player(stageRect, 1) player:idle() player:move(10)
main.luaからの抜粋です。Playerを生成して:idle()で初期状態を決め:move(10)で配置をしています。
-- -- コールバック -- -- ゲームループ function playdate.update() clearDisplay() -- https://sdk.play.date/2.1.1/Inside%20Playdate.html#f-graphics.sprite.update -- SpriteのCallbackを呼び出すために必要 playdate.graphics.sprite.update() end
重要なのはplaydate.graphics.sprite.update()です。playdate.update()でこれを呼び出さなければSpriteオブジェクトのupdateは呼び出されないため記述を忘れないようにしましょう。
アニメーションを状態遷移を伴いながら左右へ移動させる
すでに状態遷移(idle⇔walk)への切り替えを行うために必要なコードは定義しています。main.luaを以下のように書き換えてください。
if not import then import = require end import "CoreLibs/graphics" import "CoreLibs/object" import "CoreLibs/sprites" import "CoreLibs/animation" import "actors/player" local function clearDisplay() playdate.graphics.clear() playdate.graphics.setBackgroundColor(playdate.graphics.kColorBlack) end local stageRect = playdate.geometry.rect.new(0, 0, playdate.display.getWidth(), playdate.display.getHeight()) local player = Player(stageRect, 1) player:idle() player:move(10) local gravityPower = 5 local distanceMovedMax = 45 local distanceMovedY = 0 local jumpFlag = false local dropping = false local function jump() if not jumpFlag then jumpFlag = true end if jumpFlag then if not dropping then distanceMovedY = distanceMovedY + gravityPower else distanceMovedY = distanceMovedY - gravityPower if distanceMovedY < 0 then dropping = false end end if distanceMovedY > distanceMovedMax then if not dropping then dropping = true end end end end local function down() distanceMovedY = distanceMovedY - gravityPower if distanceMovedY < 0 then distanceMovedY = 0 end end -- -- コールバック -- -- ゲームループ function playdate.update() clearDisplay() if playdate.buttonIsPressed(playdate.kButtonRight) and playdate.buttonIsPressed(playdate.kButtonA) then player.x = player.x + 5 player:turnRight() player:walk() jump() elseif playdate.buttonIsPressed(playdate.kButtonLeft) and playdate.buttonIsPressed(playdate.kButtonA) then player.x = player.x - 5 player:turnLeft() player:walk() jump() elseif playdate.buttonIsPressed(playdate.kButtonRight) then player.x = player.x + 5 player:turnRight() player:walk() down() elseif playdate.buttonIsPressed(playdate.kButtonLeft) then player.x = player.x - 5 player:turnLeft() player:walk() down() elseif playdate.buttonIsPressed(playdate.kButtonA) then jump() else player:idle() down() jumpFlag = false end player:move(player.x, distanceMovedY) -- https://sdk.play.date/2.1.1/Inside%20Playdate.html#f-graphics.sprite.update -- SpriteのCallbackを呼び出すために必要 playdate.graphics.sprite.update() end
./run.shを実行します。

16x16の小さいドット絵なせいもありとても見辛いですが歩いているときは歩く(work)モーションになり止まっているときは待機(idle)のモーションに遷移しています。
ジャンプも単純ではありますがロジックを入れてみました。
状態を切り変えた際にSpriteオブジェクトに貼るImageTableオブジェクトを貼り変えることで見た目で状態の変化をつけることができます。この例ではジャンプの時はその時点での状態のままですが、変更したければPlayerクラスにもう1つジャンプ用のメソッドを追加してself.currentSpriteにジャンプのImageTableオブジェクトを貼ることで実現できると思います。
衝突を検知する
次は衝突検知を入れます。ここからようやくSpriteオブジェクトの恩恵を受けることができます。まずはHartクラスを作成します。source/actorsディレクトリ配下にhart.luaを作成して以下の内容をコピペして下さい。
import "actors/actor" ---@diagnostic disable: undefined-global class('Hart').extends(Actor) function Hart:init(_stageRect, _zindex) Hart.super.init(self, _stageRect, _zindex) self.states = { idle = 1; } self.idleFrametime = 200 self.x = 0 self.y = 0 end function Hart:move(_x, _distanceMovedY) self.x = _x or self.x local _, spriteHeight = self.currentSprite:getSize() if _distanceMovedY then self.y = self.stageRect.height - spriteHeight - _distanceMovedY else self.y = self.stageRect.height - spriteHeight end self.currentSprite:moveTo(self.x, self.y) 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.currentSprite = playdate.graphics.sprite.new(self.currentLoop:image()) 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
ハートの状態は待機(idle)のみです。
main.luaに以下を追記してください。
import "actors/hart"
local hart = Hart(stageRect, 1) hart:idle() hart:move(100)
./run.shを実行すると以下のように表示されるはずです。

ハートも表示できました。まだ衝突を検知する仕組みが入っていないため素通りです。
さて!ここでSDKに用意されたSpriteオブジェクトの機能を使います。
alphaCollision
actor.luaの:commonSpriteSettings()メソッドを以下のように書き換えてください。コメント部分は消して大丈夫です。
function Actor:commonSpriteSettings() self.currentSprite:setCenter(0, 0) self.currentSprite:setZIndex(self.zindex) -- https://sdk.play.date/2.1.1/Inside%20Playdate.html#m-graphics.sprite.setCollideRect self.currentSprite:setCollideRect(0, 0, self.currentSprite:getSize()) end
追加されたのはself.currentSprite:setCollideRect(0, 0, self.currentSprite:getSize())です。
このメソッドはSpriteオブジェクトに衝突判定を行う矩形領域を設定します。この引数だとself.currentSpriteと同じ位置、サイズの矩形領域を設定しています。
hart.luaに以下のメソッドを追加します。
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
:alphaCollision()メソッドを使い衝突判定を行っています。Playerオブジェクトに設定されているSpriteオブジェクトとHartオブジェクトに設定されているSpriteオブジェクトがピクセルレベルで衝突したらtrueを返却します。ifブロック内ではtrueだった場合にHartオブジェクトに設定されたスプライトを消しています。プレイヤーとハートが接触したらハートが消える挙動となっています。

素晴らしいですね。:setCollideRect()メソッドで接触領域を設定さえすれば:alphaCollision()メソッドによりSpriteオブジェクト同士の接触を真理値で取得することができます。
moveWithCollisions
ここまでの例ではプレイヤーがハートを通り抜けることができます。ですが、もう1つ欲しい衝突があります。それは壁や特定の物体を通り抜けられないようにする衝突です。その方法もSDKに機能が用意されています。説明のために以下の画像を用意します。
![]()
以下のように追加します。
.
├── 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
source/images/spritesheets/block/idle-table-16-16.pngとsource/actors/block.luaが追加されました。
block.luaのコードは以下の通りです。
import "actors/actor" ---@diagnostic disable: undefined-global class('Block').extends(Actor) function Block:init(_stageRect, _zindex) Block.super.init(self, _stageRect, _zindex) self.states = { idle = 1; } self.idleFrametime = 200 self.x = 0 self.y = 0 end function Block:move(_x, _distanceMovedY) self.x = _x or self.x local _, spriteHeight = self.currentSprite:getSize() if _distanceMovedY then self.y = self.stageRect.height - spriteHeight - _distanceMovedY else self.y = self.stageRect.height - spriteHeight end self.currentSprite:moveTo(self.x, self.y) 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.currentSprite = playdate.graphics.sprite.new(self.currentLoop:image()) 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
Hartクラスとほぼ同じです。
main.luaに以下を追記してください。それと一旦Hartオブジェクトは表示しないようにするため衝突を検知するでmain.luaに追加したコードはコメントアウトしてください。
import "actors/block"
local block = Block(stageRect, 1) block:idle() block:move(150)
./run.shを実行します。

まだこの時点では通り抜けできてしまいます。
そこでPlayer.luaの:move()メソッドを以下のように修正します。
function Player:move(_speed, _distanceMovedY) local x, _ = self.currentSprite.x, self.currentSprite.y x = x + _speed local _, spriteHeight = self.currentSprite:getSize() if _distanceMovedY then self.y = self.stageRect.height - spriteHeight - _distanceMovedY else self.y = self.stageRect.height - spriteHeight end -- 現在X位置を更新しておく self.x = x self.currentSprite:moveWithCollisions(x, self.y) end
まずlocal x, _ = self.currentSprite.x, self.currentSprite.yでself.currentSpriteに設定されている座標値を取得します。
引数の_xを_speedに変更し、Y軸と同じ方法でX座標位置を計算します。
self.x = xこの行は忘れないように入れておいてください。
:moveTo()の移動を:moveWithCollisions()に変更します。与える引数は同じです。
Actorクラスの共通処理にも追記を入れます。
actor.luaの:commonSpriteSettings()以下のように修正してください。
function Actor:commonSpriteSettings() self.currentSprite:setCenter(0, 0) self.currentSprite:setZIndex(self.zindex) -- https://sdk.play.date/2.1.1/Inside%20Playdate.html#m-graphics.sprite.setCollideRect self.currentSprite:setCollideRect(0, 0, self.currentSprite:getSize()) self.currentSprite:moveTo(self.x, self.y) end
self.currentSprite:moveTo(self.x, self.y)を追記しました。これは状態が切り替わった際に位置ずれを起こさないようにするために追記しています。
main.luaを以下のように書き換えます。
if not import then import = require end import "CoreLibs/graphics" import "CoreLibs/object" import "CoreLibs/sprites" import "CoreLibs/animation" import "actors/player" import "actors/hart" import "actors/block" local function clearDisplay() playdate.graphics.clear() playdate.graphics.setBackgroundColor(playdate.graphics.kColorBlack) end local stageRect = playdate.geometry.rect.new(0, 0, playdate.display.getWidth(), playdate.display.getHeight()) local player = Player(stageRect, 1) player:idle() player:move(10) -- local hart = Hart(stageRect, 1) -- hart:idle() -- hart:move(100) local block = Block(stageRect, 1) block:idle() block:move(150) local gravityPower = 5 local distanceMovedMax = 45 local distanceMovedY = 0 local jumpFlag = false local dropping = false local speed = 0 local function jump() if not jumpFlag then jumpFlag = true end if jumpFlag then if not dropping then distanceMovedY = distanceMovedY + gravityPower else distanceMovedY = distanceMovedY - gravityPower if distanceMovedY < 0 then dropping = false end end if distanceMovedY > distanceMovedMax then if not dropping then dropping = true end end end end local function down() distanceMovedY = distanceMovedY - gravityPower if distanceMovedY < 0 then distanceMovedY = 0 end end -- -- コールバック -- -- ゲームループ function playdate.update() clearDisplay() if playdate.buttonIsPressed(playdate.kButtonRight) and playdate.buttonIsPressed(playdate.kButtonA) then speed = 5 player:turnRight() player:walk() jump() elseif playdate.buttonIsPressed(playdate.kButtonLeft) and playdate.buttonIsPressed(playdate.kButtonA) then speed = - 5 player:turnLeft() player:walk() jump() elseif playdate.buttonIsPressed(playdate.kButtonRight) then speed = 5 player:turnRight() player:walk() down() elseif playdate.buttonIsPressed(playdate.kButtonLeft) then speed = - 5 player:turnLeft() player:walk() down() elseif playdate.buttonIsPressed(playdate.kButtonA) then jump() else down() jumpFlag = false player:idle() speed = 0 end player:move(speed, distanceMovedY) -- hart:collideWithPlayer(player) -- https://sdk.play.date/2.1.1/Inside%20Playdate.html#f-graphics.sprite.update -- SpriteのCallbackを呼び出すために必要 playdate.graphics.sprite.update() end
./run.shを実行します。

:moveWithCollisions()メソッドに移動先の座標値を入れることで、移動先に接触領域があるとめり込まないように自動で表示位置を調整してくれます。便利ですね!
ただ、欠点としてブロックの上に乗ろうとしても同様の自動調節が働くためこのままだとブロックには乗れないという欠点があります。もう1つこのままだと微妙な点があります。
main.luaの以下のコードをコメントアウトして復活させてください。
-- local hart = Hart(stageRect, 1) -- hart:idle() -- hart:move(100)
-- hart:collideWithPlayer(player)
./run.shを実行します。

:moveWithCollisions()の影響でハートを通り抜けられなくなっています。これはイケてないので次はこの問題を解決しようと思います。
setCollidesWithGroups
Spriteオブジェクトには衝突判定のグループを設定することができます。同じグループに所属している場合のみ:moveWithCollisions()を機能させることができます。
まずは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.idlePath = "images/spritesheets/" .. self.className:lower() .. "/idle" end function Actor:commonSpriteSettings() 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:moveTo(self.x, self.y) end
:init()に_groupIdsの引数を追加します。メンバーにself.groupIdsを加え、引数の_groupIdsで初期化します。
commonSpriteSettings()メソッドにself.currentSprite:setGroups(self.groupIds)とself.currentSprite:setCollidesWithGroups(self.groupIds)を追記します。
これでオブジェクトに対して衝突判定のグループを設定することができます。
基底クラスの修正に伴い、Player、Hart、Blockもそれぞれ以下の箇所を修正します。
player.lua
---@diagnostic disable: undefined-global class('Player').extends(Actor) function Player:init(_stageRect, _zindex, _groupIds) Player.super.init(self, _stageRect, _zindex, _groupIds) ~ 省略 ~
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) ~ 省略 ~
block.lua
import "actors/actor" ---@diagnostic disable: undefined-global class('Block').extends(Actor) function Block:init(_stageRect, _zindex, _groupIds) Block.super.init(self, _stageRect, _zindex, _groupIds) ~ 省略 ~
そしてmain.luaで以下のように修正します。
if not import then import = require end import "CoreLibs/graphics" import "CoreLibs/object" import "CoreLibs/sprites" import "CoreLibs/animation" import "actors/player" import "actors/hart" import "actors/block" local function clearDisplay() playdate.graphics.clear() playdate.graphics.setBackgroundColor(playdate.graphics.kColorBlack) end local stageRect = playdate.geometry.rect.new(0, 0, playdate.display.getWidth(), playdate.display.getHeight()) local player = Player(stageRect, 1, {1}) player:idle() player:move(10) local hart = Hart(stageRect, 1, {2}) hart:idle() hart:move(100) local block = Block(stageRect, 1, {1}) block:idle() block:move(150) ~ 省略 ~
groupIdsの値がテーブルの配列になっていることに注意してください。Spriteオブジェクトは複数のグループに所属することも可能です。
./run.shを実行します。

ハートを通り抜けられるようになりました。また:moveWithCollisions()メソッドは:alphaCollision()メソッドと併用できることも確認できます。
まとめ
まだ備忘録として残したい実装はたくさんありますが、一旦ここで区切らせて頂きまとめとします。
ですが概ねこの記事に記述した機能を応用することで大抵のゲームは開発できるはずです。SDKですからそりゃそうですよね!
Playdateのドキュメントはシンプルにまとまっているのは素晴らしいのですが…やはり実例が少ないのとメソッドを使うと視覚的にどうなるのかの例がもっと欲しいですね。
この記事が少しでもPlaydateの日本向けゲーム開発のきっかけになればと思います!