アットウィキロゴ

初心者向けガイドからゲーム作りの基本を学ぶ (part.2)



準備

前提条件として、初心者向けガイドからゲーム作りの基本を学ぶ (part.1)でセットアップが完了しているとします。
main.luaの中身を消去
main.luaの中身を [CTRL+A] で全選択などして [DELETE] キーで全削除します。
画像アセットの取得
ゲーム作成で使用する素材を入手するために、以下のページから画像アセットを取得します。
「Download ZIP」でダウンロードして、

sourceフォルダにimagesフォルダを作って、2つの画像ファイル ("capybara.png", "rock.png" ) をコピーしておきます。
補足:操作キャラクターの画像リソースを自作する場合は、最低でも "32px" はないと視認性が低くなってしまうとのことです。

画像を読み込んで描画する

画像を読み込んで描画するには以下のコードを記述します。
local pd = playdate
local gfx = pd.graphics
 
-- Player.
--- プレイヤーの位置.
local playerX = 40
local playerY = 120
--- プレイヤー画像の読み込み (".png" の指定はあってもなくても良いらしい).
local playerImage = gfx.image.new("images/capybara")
 
-- ゲーム更新.
function pd.update()
    -- プレイヤー画像の描画.
    playerImage:draw(playerX, playerY)
end
 
正常に実行できれば以下の表示となります。
■注意点:
プレイヤー画像の描画は「playerImage.draw()」と他の言語で見慣れた書き方「. (ピリオド)」にしてしまうとエラーとなります。
main.lua:14: bad argument #1 to 'draw' (playdate.graphics.image expected, got number)
stack traceback:
[C]: in field 'draw'
main.lua:14: in function <main.lua:12>
正しくは「:(コロン)」でメソッドを指定する必要があります。

クランクでプレイヤーを動かす

プレイヤーを動かしてみる
playdate.getCrankPosition()でクランクの位置を取得して、その位置でプレイヤーを上下移動させてみます。
local pd = playdate
local gfx = pd.graphics
 
-- Player.
--- プレイヤーの位置.
local playerX = 40
local playerY = 120
--- プレイヤーの速度.
local playerSpeed = 3
--- プレイヤー画像の読み込み.
local playerImage = gfx.image.new("images/capybara")
 
-- ゲーム更新.
function pd.update()
    -- クランクの位置で移動する.
    local crankPosition = pd.getCrankPosition()
    if crankPosition <= 90 or crankPosition >= 270 then
        -- クランクが水平方向より上にあるときはプレイヤーを上に移動させる.
        playerY -= playerSpeed
    else
        -- クランクが水平方向より下にあるときはプレイヤーを下に移動させる.
        playerY += playerSpeed
    end
    -- プレイヤー画像の描画.
    playerImage:draw(playerX, playerY)
end
 
プレイヤーが動くようになりましたが、残像のようなものが残ります。
残像を消す
これは画面を消去せずに再描画しているためで、残像を消すにはplaydate.graphics.clear()を呼び出すことで描画をクリアする必要があります。
...
-- ゲーム更新.
function pd.update()
    -- 画面をクリア.
    gfx.clear() -- これを追加.
 
    -- クランクの位置で移動する.
    local crankPosition = pd.getCrankPosition()
...
 
デバイス (Playdate本体) をコントローラーとして使用する
マウス操作ではなく Playdate本体をコントローラーとして使用するには、メニューから「Device > Use Device As Controller」にチェックを入れます。
シミュレーターの再起動
[CTRL+R] で再実行が可能です。
繰り返し動作を確認したいときに便利です。

スプライトを使う

スプライトの作成と登録
スプライトを作成するために import文とスプライトの生成処理を追加します。
import "CoreLibs/sprites" -- spriteの機能を使うために必要.
 
local pd = playdate
local gfx = pd.graphics
 
-- Player.
--- プレイヤーの開始位置.
local playerStartX = 40
local playerStartY = 120
--- プレイヤーの速度.
local playerSpeed = 3
--- プレイヤー画像の読み込み.
local playerImage = gfx.image.new("images/capybara")
--- プレイヤースプライトの作成.
local playerSprite = gfx.sprite.new(playerImage) -- 画像をもとに生成.
playerSprite:setCollideRect(0, 0, 64, 48) -- 64x48の当たり判定を設定.
playerSprite:moveTo(playerStartX, playerStartY) -- プレイヤーを開始位置に移動.
playerSprite:add() -- プレイヤースプライトをゲームに追加.
 
...
 
  • import文はなくても動作していましたが念のため追加しておきます。
  • setCollideRect()でエラーとなる場合、ピリオドとコロンを間違えていないかをチェックします。

移動と描画を含めたコード
スプライトの移動と描画を含めたコードは以下のとおりです。
import "CoreLibs/sprites" -- spriteの機能を使うために必要.
 
local pd = playdate
local gfx = pd.graphics
 
-- Player.
--- プレイヤーの開始位置.
local playerStartX = 40
local playerStartY = 120
--- プレイヤーの速度.
local playerSpeed = 3
--- プレイヤー画像の読み込み.
local playerImage = gfx.image.new("images/capybara")
--- プレイヤースプライトの作成.
local playerSprite = gfx.sprite.new(playerImage) -- 画像をもとに生成.
playerSprite:setCollideRect(0, 0, 64, 48) -- 64x48の当たり判定を設定.
playerSprite:moveTo(playerStartX, playerStartY) -- プレイヤーを開始位置に移動.
playerSprite:add() -- プレイヤースプライトをゲームに追加.
 
-- ゲーム更新.
function pd.update()
    -- spriteの描画だけであれば画面クリアは不要.
    -- gfx.clear()
 
    -- クランクの位置で移動する.
    local crankPosition = pd.getCrankPosition()
    if crankPosition <= 90 or crankPosition >= 270 then
        -- クランクが水平方向より上にあるときはプレイヤーを上に移動させる.
        playerSprite:moveBy(0, -playerSpeed)
    else
        -- クランクが水平方向より下にあるときはプレイヤーを下に移動させる.
        playerSprite:moveBy(0, playerSpeed)
    end
 
    -- すべてのスプライトを更新して描画.
    gfx.sprite.update()
end
 
なお、spriteシステムを使うことで内部で gfx.clear() が呼び出されるようでなので、gfx.clear() は不要となります。
コリジョンボックスの表示と修正
シミュレーターのメニューから「View > Show Sprite Collisions」を選びます。

すると、スプライトを使用したことで、コリジョンが設定されていることが確認できます。

見た目に対して大きいサイズになっているのでプログラムでコリジョンサイズを小さくします。
--- プレイヤースプライトの作成.
local playerSprite = gfx.sprite.new(playerImage) -- 画像をもとに生成.
playerSprite:setCollideRect(0, 0, 56, 40) -- 56x40の当たり判定を設定.
playerSprite:moveTo(playerStartX, playerStartY) -- プレイヤーを開始位置に移動.
 
コリジョンが少しだけ小さくなりました。

ゲームの状態遷移を作る

ゲーム開始前の状態の作成
変数「gameState」を追加して、ゲームの状態の遷移を実装します。
"stopped" (ゲーム開始前) は playdate.graphics.drawText()でテキスト描画を行い、playdate.buttonJustPressed() (ボタンを押した) でメインゲームを開始します。
... 
 
--- プレイヤースプライトの作成.
local playerSprite = gfx.sprite.new(playerImage) -- 画像をもとに生成.
playerSprite:setCollideRect(0, 0, 56, 40) -- 56x40の当たり判定を設定.
playerSprite:moveTo(playerStartX, playerStartY) -- プレイヤーを開始位置に移動.
playerSprite:add() -- プレイヤースプライトをゲームに追加.
 
-- Gameの状態を追加.
local gameState = "stopped"
 
-- ゲーム更新.
function pd.update()
 
    if gameState == "stopped" then
        -- "Press A to start" の文字を表示.
        gfx.drawText("Press A to Start", 200, 40)
        if pd.buttonJustPressed(pd.kButtonA) then
            -- Aボタンが押されたらゲームを開始.
            gameState = "playing"
        end
    elseif gameState == "playing" then
        -- ゲームが開始されたら、プレイヤーを移動させる.
        -- クランクの位置で移動する.
        local crankPosition = pd.getCrankPosition()
        if crankPosition <= 90 or crankPosition >= 270 then
            -- クランクが水平方向より上にあるときはプレイヤーを上に移動させる.
            playerSprite:moveBy(0, -playerSpeed)
        else
            -- クランクが水平方向より下にあるときはプレイヤーを下に移動させる.
            playerSprite:moveBy(0, playerSpeed)
        end
 
        -- すべてのスプライトを更新して描画.
        gfx.sprite.update()
    end
end
 
テキスト描画位置の調整とゲームオーバー判定
まず "Press A to Start" の文字を中央揃えに修正します。
  • import "CoreLibs/sprites" の定義がないと、kTextAlignmentが未定義エラーとなるため追加します。
import "CoreLibs/graphics" -- graphicsの機能を使うために必要.
import "CoreLibs/sprites" -- spriteの機能を使うために必要.
 
    ...
 
    if gameState == "stopped" then
        -- "Press A to start" の文字を中央揃えで表示.
        gfx.drawTextAligned("Press A to Start", 200, 40, kTextAlignment.center)
    ...
 
ゲームオーバーを判定するためにプレイヤーのY座標が -30以下、または 270以上になったら "stopped" に戻すようにしました。
...
    elseif gameState == "playing" then
        -- ゲームが開始されたら、プレイヤーを移動させる.
        -- クランクの位置で移動する.
        local crankPosition = pd.getCrankPosition()
        if crankPosition <= 90 or crankPosition >= 270 then
            -- クランクが水平方向より上にあるときはプレイヤーを上に移動させる.
            playerSprite:moveBy(0, -playerSpeed)
        else
            -- クランクが水平方向より下にあるときはプレイヤーを下に移動させる.
            playerSprite:moveBy(0, playerSpeed)
        end
 
        if playerSprite.y < -30 or playerSprite.y > 270 then
            -- プレイヤーが画面外に出たらゲームオーバー.
            gameState = "stopped"
            playerSprite:moveTo(playerStartX, playerStartY) -- プレイヤーを開始位置に戻す.
        end
 
        -- すべてのスプライトを更新して描画.
        gfx.sprite.update()
    end
...
 

障害物を追加する

岩画像の読み込みと生成・配置
ゲームの状態定義の下あたりに障害物(岩)の生成と配置を記述します。
...
-- Gameの状態を追加.
local gameState = "stopped"
 
-- 障害物.
local objstacleSpeed = 5 -- 移動速度.
local objstacleImage = gfx.image.new("images/rock") -- 岩画像の読み込み.
local objstacleSprite = gfx.sprite.new(objstacleImage) -- 岩スプライトの作成.
objstacleSprite:setCollideRect(0, 0, 48, 48) -- 48x48の当たり判定を設定.
objstacleSprite:moveTo(450, 240) -- 岩を右端に配置.
objstacleSprite:add() -- 岩スプライトをゲームに追加.
...
 
障害物をランダムな位置から出現させる
ゲーム開始処理(gameState に "playing" を代入するタイミング) で、障害物を Luaの標準関数を使ってY座標をランダムな位置に移動させます。
...
    if gameState == "stopped" then
        -- "Press A to start" の文字を中央揃えで表示.
        gfx.drawTextAligned("Press A to Start", 200, 40, kTextAlignment.center)
        if pd.buttonJustPressed(pd.kButtonA) then
            -- Aボタンが押されたらゲームを開始.
            gameState = "playing"
            -- 障害物をランダムな位置に移動.
            local randomY = math.random(40, 200)
            objstacleSprite:moveTo(450, randomY)
        end
 

続けて "playing" で障害物を動かします。
X座標が-20より小さくなったら画面外とみなして、再びランダムな位置から出現するようにします。
...
    elseif gameState == "playing" then
        -- ゲームが開始されたら、プレイヤーを移動させる.
        -- クランクの位置で移動する.
        local crankPosition = pd.getCrankPosition()
        if crankPosition <= 90 or crankPosition >= 270 then
            -- クランクが水平方向より上にあるときはプレイヤーを上に移動させる.
            playerSprite:moveBy(0, -playerSpeed)
        else
            -- クランクが水平方向より下にあるときはプレイヤーを下に移動させる.
            playerSprite:moveBy(0, playerSpeed)
        end
 
        -- 障害物の移動.
        objstacleSprite:moveBy(-objstacleSpeed, 0)
        -- 障害物が画面外に出たら、右端に戻す.
        if objstacleSprite.x < -20 then
            local randomY = math.random(40, 200)
            objstacleSprite:moveTo(450, randomY)
        end
 
        if playerSprite.y < -30 or playerSprite.y > 270 then
            -- プレイヤーが画面外に出たらゲームオーバー.
            gameState = "stopped"
            playerSprite:moveTo(playerStartX, playerStartY) -- プレイヤーを開始位置に戻す.
        end
 
        -- すべてのスプライトを更新して描画.
        gfx.sprite.update()
    ...
 

実行して障害物が右から繰り返し出現することを確認します。

コリジョンを実装する

でコリジョン判定を行う
コリジョンを実装するには moveTo() / moveBy() による移動ではなく、moveWithCollisions()で移動を行います。
...
    elseif gameState == "playing" then
        -- ゲームが開始されたら、プレイヤーを移動させる.
        -- クランクの位置で移動する.
        local crankPosition = pd.getCrankPosition()
        if crankPosition <= 90 or crankPosition >= 270 then
            -- クランクが水平方向より上にあるときはプレイヤーを上に移動させる.
            playerSprite:moveBy(0, -playerSpeed)
        else
            -- クランクが水平方向より下にあるときはプレイヤーを下に移動させる.
            playerSprite:moveBy(0, playerSpeed)
        end
 
        -- 衝突を考慮した障害物の移動.
        --objstacleSprite:moveBy(-objstacleSpeed, 0)
        local nextX = objstacleSprite.x - objstacleSpeed
        local nextY = objstacleSprite.y
        local actualX, actualY, collsions, length = objstacleSprite:moveWithCollisions(nextX, nextY);
        -- 障害物が画面外に出たら、右端に戻す.
        if objstacleSprite.x < -20 then
            local randomY = math.random(40, 200)
            objstacleSprite:moveTo(450, randomY)
        end
 
        if length > 0 or playerSprite.y < -30 or playerSprite.y > 270 then
            -- 衝突 or プレイヤーが画面外に出たらゲームオーバー.
            gameState = "stopped"
            playerSprite:moveTo(playerStartX, playerStartY) -- プレイヤーを開始位置に戻す.
        end
 
        -- すべてのスプライトを更新して描画.
        gfx.sprite.update()
    ...
 


ゲームオーバー時にプレイヤーがワープしてしまうので、ゲーム開始時にワープするように修正しました。
import "CoreLibs/graphics" -- graphicsの機能を使うために必要.
import "CoreLibs/sprites" -- spriteの機能を使うために必要.
 
local pd = playdate
local gfx = pd.graphics
 
-- Player.
--- プレイヤーの開始位置.
local playerStartX = 40
local playerStartY = 120
--- プレイヤーの速度.
local playerSpeed = 3
--- プレイヤー画像の読み込み.
local playerImage = gfx.image.new("images/capybara")
--- プレイヤースプライトの作成.
local playerSprite = gfx.sprite.new(playerImage) -- 画像をもとに生成.
playerSprite:setCollideRect(0, 0, 56, 40) -- 56x40の当たり判定を設定.
playerSprite:moveTo(playerStartX, playerStartY) -- プレイヤーを開始位置に移動.
playerSprite:add() -- プレイヤースプライトをゲームに追加.
 
-- Gameの状態を追加.
local gameState = "stopped"
 
-- 障害物.
local objstacleSpeed = 5 -- 移動速度.
local objstacleImage = gfx.image.new("images/rock") -- 岩画像の読み込み.
local objstacleSprite = gfx.sprite.new(objstacleImage) -- 岩スプライトの作成.
objstacleSprite:setCollideRect(0, 0, 48, 48) -- 48x48の当たり判定を設定.
objstacleSprite:moveTo(450, 240) -- 岩を右端に配置.
objstacleSprite:add() -- 岩スプライトをゲームに追加.
 
-- ゲーム更新.
function pd.update()
 
    if gameState == "stopped" then
        -- "Press A to start" の文字を中央揃えで表示.
        gfx.drawTextAligned("Press A to Start", 200, 40, kTextAlignment.center)
        if pd.buttonJustPressed(pd.kButtonA) then
            -- Aボタンが押されたらゲームを開始.
            gameState = "playing"
            -- プレイヤーを開始位置に戻す.
            playerSprite:moveTo(playerStartX, playerStartY)
            -- 障害物をランダムな位置に移動.
            local randomY = math.random(40, 200)
            objstacleSprite:moveTo(450, randomY)
        end
    elseif gameState == "playing" then
        -- ゲームが開始されたら、プレイヤーを移動させる.
        -- クランクの位置で移動する.
        local crankPosition = pd.getCrankPosition()
        if crankPosition <= 90 or crankPosition >= 270 then
            -- クランクが水平方向より上にあるときはプレイヤーを上に移動させる.
            playerSprite:moveBy(0, -playerSpeed)
        else
            -- クランクが水平方向より下にあるときはプレイヤーを下に移動させる.
            playerSprite:moveBy(0, playerSpeed)
        end
 
        -- 衝突を考慮した障害物の移動.
        --objstacleSprite:moveBy(-objstacleSpeed, 0)
        local nextX = objstacleSprite.x - objstacleSpeed
        local nextY = objstacleSprite.y
        local actualX, actualY, collsions, length = objstacleSprite:moveWithCollisions(nextX, nextY);
        -- 障害物が画面外に出たら、右端に戻す.
        if objstacleSprite.x < -20 then
            local randomY = math.random(40, 200)
            objstacleSprite:moveTo(450, randomY)
        end
 
        if length > 0 or playerSprite.y < -30 or playerSprite.y > 270 then
            -- 衝突 or プレイヤーが画面外に出たらゲームオーバー.
            gameState = "stopped"
        end
 
        -- すべてのスプライトを更新して描画.
        gfx.sprite.update()
    end
end
 

衝突の押し戻しを無効にする
あまり気にならないかもしれませんが、衝突した (GAME OVERになった) ときに岩が押し戻されることがあります。
これは衝突の種類が「押し戻し」が有効になっているためです。

押し戻しを無効にするには collisionResponse を kCollisionTypeOverlap (衝突検知のみ) にすると良いです。
...
-- 障害物.
local objstacleSpeed = 5 -- 移動速度.
local objstacleImage = gfx.image.new("images/rock") -- 岩画像の読み込み.
local objstacleSprite = gfx.sprite.new(objstacleImage) -- 岩スプライトの作成.
 
-- 衝突はオーバーラップ判定 (重なりのみ判定).
objstacleSprite.collisionResponse = gfx.sprite.kCollisionTypeOverlap
 
objstacleSprite:setCollideRect(0, 0, 48, 48) -- 48x48の当たり判定を設定.
objstacleSprite:moveTo(450, 240) -- 岩を右端に配置.
objstacleSprite:add() -- 岩スプライトをゲームに追加.
...
 

これで「押し戻し」をせずに衝突検知が行われるようになりました。

スコアの描画

まずスコア変数を宣言します。
...
-- Gameの状態を追加.
local gameState = "stopped"
 
-- スコア.
local score = 0
 
-- 障害物.
...
 
続けてゲーム開始時にスコアを初期化。
...
        if pd.buttonJustPressed(pd.kButtonA) then
            -- Aボタンが押されたらゲームを開始.
            gameState = "playing"
            -- スコア変数の初期化.
            score = 0
            -- プレイヤーを開始位置に戻す.
...
 
障害物が左端に到達したらスコアを加算。
...
        -- 障害物が画面外に出たら、右端に戻す.
        if objstacleSprite.x < -20 then
            local randomY = math.random(40, 200)
            objstacleSprite:moveTo(450, randomY)
            -- スコアを加算.
            score += 100
        end
...
 
最期にスコアを描画。
...
        -- すべてのスプライトを更新して描画.
        gfx.sprite.update()
    end
 
    -- スコアの描画 (右揃え).
    gfx.drawTextAligned("Score: " .. score, 390, 10, kTextAlignment.right)
...
 
注意点として、playdate.graphics.sprite.update()はそれまでの描画物をクリアするので、スプライトの更新・描画よりも後で呼び出す必要があります。

実行してスコアが表示・加算されることを確認します。

Playdateでのデバッグ方法

printデバッグ
コード内に print() 関数を入れて変数や文字列を指定するとその中身をコンソールウィンドウに表示できます。
if gameState == "stopped" then
        -- "Press A to start" の文字を中央揃えで表示.
        gfx.drawTextAligned("Press A to Start", 200, 40, kTextAlignment.center)
        if pd.buttonJustPressed(pd.kButtonA) then
            -- Aボタンが押されたらゲームを開始.
            gameState = "playing"
            -- スコア変数の初期化.
            score = 0
            -- プレイヤーを開始位置に戻す.
            playerSprite:moveTo(playerStartX, playerStartY)
            -- 障害物をランダムな位置に移動.
            local randomY = math.random(40, 200)
            objstacleSprite:moveTo(450, randomY)
 
            print("Game Started")
        end
...
 
例えば上記のようにゲーム開始時に "Game Started" というログを出力するように修正。

シミュレーターのコンソールボタンを押します (※ショートカットキーは [ALT+Y] )

するとゲーム開始時にログが出力されるようになります。
ブレークポイントとステップ実行
VSCode拡張の "Playdate Debug" がインストールされていれば、行番号の左側をクリックしてブレークポイントを配置。

実行時のブレークが可能となり、デバッグコンソールからの print出力も確認できます。

実行ファイルの作成と転送・配布

実行ファイルの作成方法
実行ファイルの作成に必要な設定ファイルは「pdxinfo」から行います。

pdxinfoは例えば以下のように指定します。
name=Capy Run
author=takashi-kun games
description=Super Capybara Action
bundleID=com.kakashi-kun-games.capyrun
version=1.0.0
buildNumber=1
項目名 説明
name アプリケーション名 (ゲームタイトル)
author 作者名 または 開発チーム名
description 詳細説明
bundleID ユニークなドメイン。
ゲームの識別子となるので被らないIDにする必要があります
buildNumber ゲームビルドバージョン。アップデート時にはインクリメントします
設定ファイルのpdxinfoを保存してゲームをビルドし直すと、buildsフォルダに ".pdx" が作成されているのが確認できます。

実行ファイルの転送
実行ファイル (.pdx) を転送するには、シミュレーターのメニューから「Device > Upload Game To Device」を選んで実機に転送するだけです。
※Playdate本体側のロックを解除する必要があります
実行ファイルの配布
buildsフォルダに作られる *.pdx を ZIP に圧縮すれば配布できるようです。

参考


関連ページ

最終更新:2026年06月07日 19:27