mcpField
P
playdate-dev

playdate-dev

github.com/ckorhonen开发2253 次安装5 周前更新
代码仓库

关于 playdate-dev

Playdate Dev 是使用官方 Playdate SDK 以 Lua 开发游戏的 skill,帮助开发者应对设备的显示、输入、内存和性能限制,并指导从项目搭建到模拟器及硬件测试。

主要功能

  1. 01指导 Lua 项目搭建与构建
  2. 02涵盖按键、摇杆和加速度计
  3. 03说明图形、精灵与碰撞
  4. 04包含音频、计时器和存档
  5. 05介绍性能与无障碍设计

适用场景

  1. 01启动 Playdate Lua 游戏项目
  2. 02实现摇杆驱动的操作机制
  3. 03在硬件上分析性能
  4. 04准备启动器素材与元数据

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/ckorhonen/claude-skills --skill playdate-dev

安装后在 Claude Code 中输入/playdate-dev确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

assets/lua-starter/Source/
main.lua
pdxinfo
references/
designing-for-playdate.md
inside-playdate-lua.md

详细介绍

Playdate 开发

概述

使用官方 Playdate SDK,以 Lua 开发 Playdate 游戏。Playdate 是一款小巧的黄色掌上游戏机,配备 400×240 的 1 位显示屏、物理摇杆、A/B 按钮和方向键。了解其限制和独特的输入方式至关重要。

硬件规格:

  • 显示屏:400×240 像素,1 位(仅黑白)
  • 内存:约 16 MB RAM(峰值用量目标低于 8 MB)
  • CPU:180 MHz Cortex-M7(设备速度低于模拟器,务必在硬件上进行性能分析)
  • 输入:A 按钮、B 按钮、方向键(上/下/左/右)、摇杆、菜单按钮
  • 音频:44.1kHz 立体声,Synth + SamplePlayer API
  • 加速度计:3 轴,可选择启用以节省电量

快速入门工作流

  1. 明确请求范围(游戏玩法目标、目标设备还是模拟器、SDK 版本、发布版本还是原型)。
  2. 选择输入方式和无障碍选项(按钮、摇杆、加速度计;提供摇杆以外的替代操作;遵循减少闪烁设置)。
  3. 选择渲染方式(精灵还是即时绘制、图像尺寸、刷新率、1x 还是 2x 缩放)。
  4. 实现核心循环(定义 playdate.update(),更新游戏状态,并在使用时调用 playdate.graphics.sprite.update() 和 playdate.timer.updateTimers())。
  5. 添加元数据和启动器资源(pdxinfo、buildNumber、启动卡片和图标尺寸)。
  6. 在模拟器和硬件上测试(屏幕可读性、摇杆手感、音频平衡、性能)。

入门项目

  • 将 assets/lua-starter 复制到新的项目文件夹。
  • 将 Source/main.lua 和 Source/pdxinfo 保留在源代码根目录中。
  • 替换 pdxinfo 中的占位值,并扩展更新循环。

使用以下命令构建:

pdc Source GameName.pdx      # compile to .pdx bundle
# Then open GameName.pdx in the Simulator, or drag to device

核心游戏循环

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

local gfx <const> = playdate.graphics

-- Game state
local playerX, playerY = 200, 120

function playdate.update()
    -- 1. Handle input
    if playdate.buttonIsPressed(playdate.kButtonLeft) then
        playerX -= 2
    elseif playdate.buttonIsPressed(playdate.kButtonRight) then
        playerX += 2
    end

    -- 2. Handle crank
    local crankDelta = playdate.getCrankChange()  -- degrees since last update
    playerY += crankDelta * 0.1                    -- map crank to movement

    -- 3. Update sprites and timers (required each frame if used)
    gfx.sprite.update()
    playdate.timer.updateTimers()

    -- 4. Draw (if not using sprites)
    gfx.clear()
    gfx.fillCircleAtPoint(playerX, playerY, 10)
end

输入 API

按钮
-- Check if button is held this frame
playdate.buttonIsPressed(playdate.kButtonA)
playdate.buttonIsPressed(playdate.kButtonB)
playdate.buttonIsPressed(playdate.kButtonUp)
playdate.buttonIsPressed(playdate.kButtonDown)
playdate.buttonIsPressed(playdate.kButtonLeft)
playdate.buttonIsPressed(playdate.kButtonRight)

-- Single-frame press/release events
local pressed, released = playdate.getButtonState()
if pressed & playdate.kButtonA ~= 0 then
    -- A was pressed this frame
end

-- Callbacks (simpler for menu-style code)
function playdate.AButtonDown()
    -- A pressed
end
function playdate.AButtonUp()
    -- A released
end
摇杆
-- Angle in degrees (0-359.99, clockwise = positive)
local angle = playdate.getCrankPosition()

-- Delta since last frame (positive = clockwise)
local delta = playdate.getCrankChange()

-- Detect if crank is docked (folded away)
if playdate.isCrankDocked() then
    -- Show "pull out crank" hint or use button fallback
end

-- Request dock/undock alerts
playdate.setCrankSoundsDisabled(true)  -- suppress built-in clicks
加速度计
-- Must opt-in (saves battery when off)
playdate.startAccelerometer()

-- Read in update()
local x, y, z = playdate.readAccelerometer()
-- x,y,z each in range ~[-1, 1] (1G = 1.0)
-- x: tilt left/right, y: tilt front/back, z: up/down

-- Remember to stop when not needed
playdate.stopAccelerometer()

图形

绘图模式
local gfx <const> = playdate.graphics

-- Screen is black (0) and white (1) only
gfx.setColor(gfx.kColorBlack)  -- or kColorWhite, kColorClear, kColorXOR
gfx.setImageDrawMode(gfx.kDrawModeFillBlack)  -- for rendering images

-- Immediate drawing (clears each frame)
function playdate.update()
    gfx.clear(gfx.kColorWhite)  -- clear to white
    gfx.drawRect(10, 10, 100, 50)
    gfx.fillRect(20, 20, 80, 30)
    gfx.drawLine(0, 0, 400, 240)
    gfx.drawCircleAtPoint(200, 120, 40)
    gfx.fillCircleAtPoint(200, 120, 40)
    gfx.drawText("Hello Playdate!", 10, 10)
end
图像
-- Load from .png file (must be in Source/)
local img = gfx.image.new("images/player")  -- no extension needed

-- Draw image
img:draw(x, y)
img:drawCentered(x, y)

-- Flip/transform
img:draw(x, y, gfx.kImageFlippedX)

-- Image tables (sprite sheets)
local table = gfx.imagetable.new("images/walk")  -- walk-table-32-32.png format
local frame = table:getImage(frameIndex)  -- 1-indexed
字体和文本
-- System fonts
local font = gfx.font.new("fonts/Roobert-10-Bold")  -- SDK includes several
gfx.setFont(font)
gfx.drawText("Score: " .. score, 10, 10)

-- Centered text
gfx.drawTextInRect("Hello!", 0, 100, 400, 30, nil, nil, kTextAlignment.center)
精灵系统
-- Create sprite
local playerSprite = gfx.sprite.new()
playerSprite:setImage(gfx.image.new("images/player"))
playerSprite:moveTo(200, 120)
playerSprite:setZIndex(10)
playerSprite:add()  -- add to sprite list

-- Custom sprite class (OOP pattern)
class('Player').extends(gfx.sprite)

function Player:init()
    Player.super.init(self)
    self:setImage(gfx.image.new("images/player"))
    self:add()
end

function Player:update()
    if playdate.buttonIsPressed(playdate.kButtonRight) then
        self:moveBy(2, 0)
    end
end

-- In playdate.update():
gfx.sprite.update()  -- required every frame
碰撞检测(通过精灵)
-- Set collision rect
playerSprite:setCollideRect(0, 0, playerSprite:getSize())

-- Query collisions after move
local actualX, actualY, cols, len = playerSprite:moveWithCollisions(newX, newY)

-- Collision response
for i = 1, len do
    local col = cols[i]
    print("Hit:", col.other:getTag())  -- other sprite's tag
end

音频

-- Sample playback
local sfx = playdate.sound.sampleplayer.new("sounds/jump")  -- .wav or .aif
sfx:play()

-- Background music (loops by default)
local music = playdate.sound.fileplayer.new("sounds/bgm")
music:play(0)  -- 0 = loop forever
music:setVolume(0.7)

-- Synthesizer (procedural audio)
local synth = playdate.sound.synth.new(playdate.sound.kWaveformSquare)
synth:setFrequency(440)
synth:setVolume(0.5)
synth:playNote("A4", 0.5, 0.25)  -- note, volume, duration

计时器

import "CoreLibs/timer"

-- One-shot timer (fires after 2000ms)
playdate.timer.performAfterDelay(2000, function()
    print("Two seconds passed!")
end)

-- Repeating timer
local t = playdate.timer.new(500, function()
    -- fires every 500ms
end)
t.repeats = true

-- Value timer (lerp a value over time)
local vt = playdate.timer.new(1000, 0, 100)  -- 1000ms, from 0 to 100
-- In update: use vt.value

-- IMPORTANT: Must call every frame
playdate.timer.updateTimers()

pdxinfo 元数据

# Source/pdxinfo (required)
name=My Game
author=Your Name
description=A short game description
bundleID=com.yourname.mygame
version=1.0.0
buildNumber=1
imagePath=images/
launchSoundPath=sounds/launch
contentWarning=Contains flashing lights

必需的启动器资源(放入 images/ 或通过 imagePath 配置):

  • launcher/card.png — 350×155 像素
  • launcher/card~highlight.png — 350×155 像素(高亮状态)
  • launcher/icon.png — 32×32 像素
  • launcher/icon~highlight.png — 32×32 像素

性能提示

  • 设备上以 30fps 为目标(最高 50fps,但标准帧率为 30fps)
  • 限制使用 gfx.clear() — 改用 gfx.sprite.update() 进行脏矩形渲染
  • 移动对象优先使用精灵系统;这样可以避免全屏重绘
  • 复用对象 — 避免每帧都创建新的表或对象
  • 如果不需要 50fps,使用 playdate.display.setRefreshRate(30)
  • 在设备上进行性能分析 — 模拟器速度是硬件的 2–3 倍
-- Check frame time
playdate.display.setRefreshRate(30)

-- Draw FPS overlay (for profiling)
playdate.drawFPS(0, 0)

无障碍

-- Respect "Reduce Flashing" system setting
if playdate.getReduceFlashing() then
    -- Avoid strobing effects, use slower transitions
end

-- Always provide button fallback for crank actions
if playdate.isCrankDocked() then
    showCrankHint = false  -- hide crank UI hints
    -- Let D-pad substitute for crank
end

摇杆 UI 指示器

import "CoreLibs/ui"

-- Show built-in crank indicator (hint to pull out crank)
local crankIndicator = playdate.ui.crankIndicator

function playdate.update()
    if playdate.isCrankDocked() then
        crankIndicator:draw()  -- draws in corner
    end
end

菜单集成

-- Add items to the system pause menu
local menu = playdate.getSystemMenu()

-- Checkmark toggle
local soundItem, err = menu:addCheckmarkMenuItem("Sound", true, function(value)
    soundEnabled = value
end)

-- Options list
local diffItem, err = menu:addOptionsMenuItem("Difficulty", {"Easy","Normal","Hard"}, "Normal", function(value)
    difficulty = value
end)

保存/加载数据

-- Simple key-value store (persists between sessions)
-- Save
local data = {score = 1234, level = 5}
playdate.datastore.write(data)

-- Load
local data = playdate.datastore.read()
if data then
    score = data.score or 0
end

-- Delete save
playdate.datastore.delete()

常见模式

场景管理
-- Simple scene switcher
local currentScene = nil

function switchScene(newScene)
    if currentScene and currentScene.leave then
        currentScene:leave()
    end
    gfx.sprite.removeAll()
    currentScene = newScene
    if currentScene.enter then
        currentScene:enter()
    end
end

-- Define scenes as tables
local titleScene = {}
function titleScene:enter() ... end
function titleScene:update() ... end

-- In playdate.update():
function playdate.update()
    if currentScene and currentScene.update then
        currentScene:update()
    end
    gfx.sprite.update()
    playdate.timer.updateTimers()
end
摇杆驱动的机制
-- Accumulate crank ticks for grid-based movement
local TICKS_PER_STEP = 12  -- degrees per step

local crankAccumulator = 0

function playdate.update()
    local delta = playdate.getCrankChange()
    crankAccumulator += delta

    while crankAccumulator >= TICKS_PER_STEP do
        crankAccumulator -= TICKS_PER_STEP
        moveRight()
    end
    while crankAccumulator <= -TICKS_PER_STEP do
        crankAccumulator += TICKS_PER_STEP
        moveLeft()
    end

    -- Button fallback (for docked crank)
    if playdate.buttonJustPressed(playdate.kButtonLeft) then moveLeft() end
    if playdate.buttonJustPressed(playdate.kButtonRight) then moveRight() end
end

资源

  • references/designing-for-playdate.md — 屏幕、文本、输入、音频、UI 和启动器指南
  • references/inside-playdate-lua.md — 完整 Lua API 名称、文件布局和工作流详情
  • assets/lua-starter/ — 入门项目模板
  • 官方 SDK 文档 — 权威 Lua API 参考
  • SDK 下载 — Panic 免费提供
  • Playdate 开发者论坛 — 社区问答和示例

评论

支持 Markdown