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の日本向けゲーム開発のきっかけになればと思います!