はじめに
2026年秋、Xの投稿にあるようなPlayloadの素晴らしいコンテンツ、作ってみたいなと思うところです。Playloadを学ぶ上でのサンプルをいくつか、作ってみました。いろいろ学ぶところですが、やってみて、検証して、数値を変えたり、動きを確認したり、
今回、高校生のための教育用ツールとして、Pyxelから、Playloadへ変換できるように、pyxel2playload.pyを試作してみました。jsに変換し、node.jsを使ってurlを出力するようにしています。
node tools\plcheck.mjs .\work\Metasprites.js --input "_30 R60 A1" --monkey 600 --text --url
- ブラウザのPlayloadでは、コードを修正して保存はブックマークになりますが、たくさんブックマークができてしまうので、一行目のタイトルにバージョンを明記するとか工夫が要ります
- ローカル環境で使う際は、editorで変更した後、chromeに、「The code was edited in the browser. Replace it with a file?」と出て、押すと、runします。学校では、この方法が良いかもしれません
以下、サンプル検証してみてください。修正しながら理解していくのが、一番面白いところでもあります。ありがとうございます。
01. Helloと表示する
Hello表示の解説
何をするプログラム?
画面のどこかに「Hello」という文字が出ます。約 0.7 秒ごとに、出る場所と色がランダムに変わります。Ⓐボタンを押すと「ピッ」と音が鳴ります。もとは Pyxel(Python)で書いたものを、変換ツールで Playload(JavaScript)に書き直しています。
① 最初の部分
-
C:色の対応表です。Pyxel は 16 色、Playload は 8 色しかないので、「Pyxel の色番号 → 近い Playload の色」に置きかえます。たとえばC[8](Pyxel の赤)は3(赤橙)になります -
rndi(a, b):a 以上 b 以下の整数をランダムに返します。Pyxel のpyxel.rndiと同じ働きです -
F:フレームを数える変数です。tick が 1 回動くたびに 1 増えます
② 速さをそろえる工夫(30fps と 60fps)
Pyxel は 1 秒に 30 回、Playload は 1 秒に 60 回画面を更新します。そのまま動かすと 2 倍速になってしまいます。
そこで、tick() の中の if (F % 2 === 0) で、update は 2 回に 1 回だけ 動かしています。
ただ、そうすると問題が出ます。update が休んでいるフレームでボタンを押すと、その入力を見逃してしまいます。これを防ぐのが P です。
-
P |= 1 << b:ボタン b が押されたら、P の b 番目のビットを 1 にしてメモしておきます -
hit(b):メモの b 番目のビットが 1 かを調べます - update が終わったら
P = 0でメモを消します
③ App クラス
- constructor:最初の位置(12, 17)と色を決めます
-
update:Ⓐ(
hit(4))が押されたら音を鳴らします。'T240V12O5N1c'は「テンポ 240、音量 12、真ん中のオクターブで、短いド」という意味です -
draw:
cls(0)で画面を黒にします。F >> 1は F を 2 で割った数(Pyxel で数えたフレーム数)です。それが 20 の倍数になったときに、場所と色を選び直します。最後にsprで Hello を描きます
spr で描いた文字は 1 フレームで消えます。だから draw は毎回呼んでいます。
④ まだ直せる所
-
if (false) { ... }:Pyxel のquit()(ゲーム終了)は Playload にはありません。なので中身が空のまま残っています。この部分は消してかまいません - TODO の音:とりあえず「ド」が入っています。好きな音に書きかえましょう
-
はみ出し:Playload の文字は 8 ドット幅なので、Hello は 40 ドットの幅になります。
rndi(0, 160)だと、右にはみ出して見えないことがあります。rndi(0, 120)、rndi(0, 136)にすると、いつも画面の中に収まります -
細かい点:draw は毎フレーム動きます。そのため
Fが 40 と 41 のように、2 フレーム続けて場所の選び直しが起きます。見た目にはほとんど分かりませんが、気になる人はF % 40 === 0に変えると 1 回だけになります
02. rectを表示する
プログラム「rect1」の解説
何をするプログラム?
黄色い四角(30×30 ドット)が、画面のあちこちにパッパッと現れ続けます。毎フレーム場所が変わるので、1 秒に 60 回ワープしているように見えます。
① rect 関数(変換で追加した部分)
Pyxel には四角を塗る pyxel.rect がありますが、Playload にはありません。そこで、█(全部ぬりつぶした文字)を 8 ドットずつ並べて 四角を作っています。
-
cell:x・y・幅・高さがすべて 8 の倍数かどうかを調べます-
8 の倍数のとき → マスにぴったり合うので
putで書きます(消すまで残ります) -
そうでないとき → マスからずれるので
sprで描きます(そのフレームだけ表示されます)
-
8 の倍数のとき → マスにぴったり合うので
- 2 重の
forで、横に 8 ドットずつ、縦に 8 ドットずつ █ を並べます
今回は幅が 30 で 8 の倍数ではないので、spr が使われます。横は i = 0, 8, 16, 24 の 4 個、縦も 4 個、合わせて 16 文字 です(1 フレーム 128 文字までなので大丈夫です)。
② App クラス
- constructor:最初の位置と色を決めます(ただし、すぐ上書きされます)
-
update:中身は空です。
if (false)は Pyxel のquit()の名残りなので、消してかまいません -
draw:
cls(0)で画面を黒にします。次に位置をランダムに決めて(x は 5〜130、y は 5〜90)、四角を描きます
③ 気をつける所・直せる所
-
色がちがう:Pyxel の色 6 は「水色」です。ところが、ここでは Playload の色 6 の「黄」になっています。前のプログラムにあった色の対応表
Cを通していないからです。水色に近づけたいなら、4(青)にしましょう - 少し大きい:█ は 8 ドット単位なので、30 ではなく 32×32 ドットになります。x = 130 のときは右端が 162 になり、2 ドットだけ画面からはみ出します
-
速すぎる:Pyxel(30fps)のときより 2 倍の速さでワープします。前のプログラムのように、フレーム数
Fを数えて動きを調整するとよいです。たとえばif (F % 2 === 0)の中で位置を変えれば同じ速さになり、F % 30 === 0にすれば 0.5 秒ごとに変わります -
使っていない定数:
WIDTH・HEIGHTはどこでも使っていません。しかもHEIGHT = 120は Pyxel の画面サイズで、Playload の画面は 144 です。消すか、144 に直しましょう
03. rectノイズあり、四角いゴミみたいなもの
「ノイズ」に見える原因は主に3つあります。どれも pyxel2playload.py の変換が Playload の仕組みに合っていないことから来ています。
何が起きているか
1. spr の上限(1 フレーム 128 文字)を大きく超えている
rectb() は 1 マスごとに spr を 2 回呼びます(▀ と ▄)。それを 20×15 = 300 マスで呼ぶので、1 フレームに 600 文字になります。上限の 128 文字を超えた分は描かれないので、ふちが付くのは最初の 64 マスくらい(左の 4〜5 列)だけで、残りはふちのない四角になります。上限を超えたときに Playload がどの順で文字を捨てるかは仕様に書かれていません。表示が乱れたりちらついたりするのは、ここが一番怪しいです。
2. 1 ドットの線が 4 ドットの太さになっている
Pyxel の rectb は太さ 1 ドットのふちです。変換後は ▀(上半分)と ▄(下半分)を使っています。マスの高さが 8 なので y + h - 8 が y と同じになり、2 つの文字が同じ場所に重なります。上半分と下半分が重なると █ と同じになり、中の色をぜんぶ隠してしまいます。また ▌▐ を描くループは j = 8; j < 0 なので 1 回も動きません。
3. 毎フレーム全部を描き直している
draw() は 1 秒に 60 回、cls(0) で全部消してから 300 マスに put し直しています。put で書いた文字は消すまで残るので、これは 1 回で十分です。
そのほかに、次の 2 つも気になります。
- 色の対応表
Cでは、奇数列の中(Pyxel の 9)もふち(Pyxel の 8)も同じ 3 になるので、ふちが見えません -
HEIGHT = 120なので 15 行しか使わず、下の 3 行は黒いままです(Playload の画面は 144 ドットです)
直し方
ふち付きの四角を pcg で 1 文字として作り、最初に 1 回だけ put で並べます。これで spr は 0 文字になり、ふちも 1 ドットの太さになります。
// @title rect4
// ふち付きの四角を pcg で作り、最初に 1 回だけ並べる
pcg['a'].pat = 'ff818181818181ff'; // ふちだけの四角(1 ドットの線)
pcg['a'].col = [1, 5]; // ふち紫、中は緑
pcg['b'].pat = 'ff818181818181ff';
pcg['b'].col = [3, 6]; // ふち赤橙、中は黄
cls(0);
for (let cy = 0; cy < 15; cy++) {
let row = '';
for (let cx = 0; cx < 20; cx++) row += cx % 2 ? 'b' : 'a';
put(0, cy, row); // 1 行ずつまとめて書く
}
// 何も動かないので、tick は空でよい
function tick() {}
- 色は 8 色しかないので、Pyxel のオレンジ(9)は黄(6)にしました。ふちと中が同じ色にならないようにするためです
- 画面全体を使いたいときは、
cy < 15をcy < 18にしてください - ほかの Pyxel の
rectbを変換するときも、太さ 1 ドットのふちは▀▄▌▐ではなく、pcgで形を作るとうまくいきます
修正版
// @title rect4
// Pyxel の rect4 を Playload 用に直したもの
const WIDTH = 160;
const HEIGHT = 120; // 元の Pyxel と同じ高さ(画面全体なら 144)
class App {
constructor() {
// ふち付きの四角を pcg で 2 種類作る(1 ドットの線)
pcg['a'].pat = 'ff818181818181ff';
pcg['a'].col = [1, 5]; // ふち紫、中は緑
pcg['b'].pat = 'ff818181818181ff';
pcg['b'].col = [3, 6]; // ふち赤橙、中は黄
this.draw(); // put は残るので、描くのは最初の 1 回だけ
}
update() {
// 動くものがないので、することはない
}
draw() {
cls(0);
for (let j = 0; j < HEIGHT / 8; j++) {
let row = '';
for (let i = 0; i < WIDTH / 8; i++) row += i % 2 ? 'b' : 'a';
put(0, j, row); // 1 行ずつまとめて書く
}
}
}
const app = new App();
// Playload は tick() を 1 秒に 60 回呼ぶ
function tick() {
app.update();
}
元の App クラスの形はそのまま残しました。draw() は最初に 1 回だけ呼び、tick() では update() だけを呼びます。そのため spr は 1 文字も使わず、128 文字の上限を超えることはなくなりました。ふちは pcg で作ったので、Pyxel の rectb と同じ 1 ドットの太さになります。画面の下まで埋めたいときは、HEIGHT を 144 にしてください。
04. rect修正版
sprは、ありません。
'ff818181818181ff' という文字列
コンピュータの世界で8×8ピクセルのドット絵を表現するための暗号のようなものです。
これを解読するには、文字列を「2文字ずつ」に区切って、16進数(0〜9とa〜fで表す数字)から、ドットのON/OFFを表す2進数(0と1)に変換します。
分かりやすく表と図で解説します。
1. 文字列を8つの行に分ける
16文字あるので、2文字ずつ8個のグループに分けます。これがドット絵の「上から1行目〜8行目」に対応します。
ff818181818181ff
2. 「16進数」を「ドット(2進数)」に変換する
-
fは2進数で1111です。なのでffは11111111となり、「8個ぜんぶ色を塗る(■)」という意味になります。 -
8は2進数で1000、1は0001です。なので81は10000001となり、「両端だけ色を塗る(■)、真ん中6個は塗らない(□)」という意味になります。
3. 図解:8×8のドット絵にしてみよう
これを1行目から8行目まで積み重ねると、以下のような「枠線だけの四角形」が浮かび上がります。
| 行 | 16進数 | 2進数(1=塗る, 0=塗らない) | ドット絵(■=枠線の色, □=中身の色) |
|---|---|---|---|
| 1行目 | ff |
1111 1111 |
■■■■■■■■ |
| 2行目 | 81 |
1000 0001 |
■□□□□□□■ |
| 3行目 | 81 |
1000 0001 |
■□□□□□□■ |
| 4行目 | 81 |
1000 0001 |
■□□□□□□■ |
| 5行目 | 81 |
1000 0001 |
■□□□□□□■ |
| 6行目 | 81 |
1000 0001 |
■□□□□□□■ |
| 7行目 | 81 |
1000 0001 |
■□□□□□□■ |
| 8行目 | ff |
1111 1111 |
■■■■■■■■ |
このように、一番上と一番下の行(ff)で横線を引いて、間の行(81)で縦線を引くことで、「1ドットのふち付き四角(中身は空洞)」のスタンプを作り出しています。
プログラムの pcg['a'].col = [1, 5]; という部分は、この図の「■(枠線)を1番の色に、□(中身)を5番の色にしなさい」と指示していたわけです。
05. 8bitworkshopからNESのメタスプライト
// @title Metasprites
// 2×2 の文字で 16×16 のキャラを作る(NES のメタスプライト)
pcg['A'].pat = '071f3f7f73f3f3ff'; // 左上
pcg['B'].pat = 'ffdfef703f1f0700'; // 左下
pcg['C'].pat = 'e0f8fcfececfcfff'; // 右上
pcg['D'].pat = 'fffbf70efcf8e000'; // 右下
// メタスプライト: [x のずれ, y のずれ, 文字]
const meta = [
[0, 0, 'A'],
[0, 8, 'B'],
[8, 0, 'C'],
[8, 8, 'D'],
];
const NUM = 16; // キャラの数(16 × 4 = 64 文字 < 128)
const cols = [2, 3, 4, 5, 6, 7];
const actors = [];
// 位置・速さ・色をランダムに決める
for (let i = 0; i < NUM; i++) {
actors.push({
x: Math.floor(Math.random() * 160),
y: Math.floor(Math.random() * 144),
dx: Math.floor(Math.random() * 8) - 3,
dy: Math.floor(Math.random() * 8) - 3,
col: cols[i % cols.length],
});
}
cls(1); // 背景は紫(NES の 0x03 に近い色)
// メタスプライトを 1 つ描く
function metaSpr(x, y, col) {
for (const m of meta) spr(x + m[0], y + m[1], m[2], col);
}
function tick() {
for (const a of actors) {
metaSpr(a.x, a.y, a.col);
a.x += a.dx;
a.y += a.dy;
// 画面の外に出たら反対側へ(NES の byte の折り返しの代わり)
if (a.x < -16) a.x += 176;
if (a.x >= 160) a.x -= 176;
if (a.y < -16) a.y += 160;
if (a.y >= 144) a.y -= 160;
}
}
16 個の 16×16 ドットのにこにこ顔が、ばらばらの向きと速さで画面を流れ、端から出ると反対側から戻ってきます。
NES のタイル 0xd8〜0xdb の代わりに、pcg で A・B・C・D の形を描き換えています。meta の表は元の metasprite[] と同じ並び(x のずれ、y のずれ、文字)です。
oam_hide_rest は必要ありません。spr はそのフレームだけ描かれるからです。NES の 0〜255 の折り返しは、画面の大きさに合わせて「端から出たら反対側へ」に置き換えました。
node tools\plcheck.mjsで、開発元urlが表示されますが、何らかのデータがuploadされているのでしょうか。ブラウザ上で、修正したあと、保存はできるのでしょうか。再度、同じURLをクリックしても、もとに戻ってしまいます。教室では、生徒マシン、個々、ローカル環境でLoadしてテストしていくのが良いかなと思います。
plcheckがどこかにアップロードすることはありません。URLは手元のPCの中で作っているだけです。Playload公式アプリのソースも確認したので、仕組みを説明します。
URLの仕組み(アップロードはしていない)
PlaylodのURLは、プログラムの文字そのものを圧縮して、#v1. の後ろに詰め込んだものです。
https://kitao.dev/playload/app/#v1.(ここにゲームのプログラムが丸ごと入っている)
-
node tools\plcheck.mjs --urlは、手元のPCの中で圧縮してURLの文字列を作り、表示するだけです。どこにも送信しません。 - ブラウザでこのURLを開いても、
#より後ろはWebの仕組み上サーバーに送られません。開発元のサーバーが返すのはエンジン(アプリ本体)だけで、プログラムはブラウザの中でURLから取り出されます。 - 公式アプリのソースを見ても、送信したりサーバーに保存したりするコードはありませんでした。
ブラウザで直したものの保存
再び同じURLをクリックすると元に戻るのは、古いURLには古いプログラムが入っているからです。
公式アプリは、エディタで書き換えるたびに(約0.25秒後)アドレスバーのURLを新しいプログラム入りのものに自動で書き換えます。保存するには次のどれかをします。
- アドレスバーの今のURLをコピーして、どこかに貼っておく(またはブックマークする)
- アプリの共有ボタン(コピー)でURLをコピーする
- エディタの全文をコピーして
.jsファイルとして保存する
アプリはブラウザの中にも保存しないので、URLを控えずにタブを閉じると、直した内容は消えます。生徒には「直したら、アドレスバーのURLを提出用のメモに貼る」と最初に教えておくと安心です。
教室でのローカル運用
生徒のPCそれぞれで試すやり方は、playload-labだけで成り立ちます。
- エディタ(VS Codeやメモ帳)で
work\game.jsを書いて保存する(ファイルが手元に残る) -
node tools\plcheck.mjs work\game.js --textで確かめる(ネット不要) - 遊ぶときだけ
--urlでURLを作り、ブラウザで開く - 提出はURLか
.jsファイル。先生はまとめてnode tools\plcheck.mjs 提出フォルダでチェックする
ネットにつながっていなくても、確認(2)まではできます。実際に遊ぶ(3)には、kitao.devからアプリ本体を読み込むためにネット接続が要ります。
参考資料


