0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

授業で使うPlayload練習帳 -- part01

0
Last updated at Posted at 2026-11-07

はじめに

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 は毎回呼んでいます。

④ まだ直せる所

  1. if (false) { ... }:Pyxel の quit()(ゲーム終了)は Playload にはありません。なので中身が空のまま残っています。この部分は消してかまいません
  2. TODO の音:とりあえず「ド」が入っています。好きな音に書きかえましょう
  3. はみ出し:Playload の文字は 8 ドット幅なので、Hello は 40 ドットの幅になります。rndi(0, 160) だと、右にはみ出して見えないことがあります。rndi(0, 120)、rndi(0, 136) にすると、いつも画面の中に収まります
  4. 細かい点: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 で描きます(そのフレームだけ表示されます)
  • 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)、四角を描きます

③ 気をつける所・直せる所

  1. 色がちがう:Pyxel の色 6 は「水色」です。ところが、ここでは Playload の色 6 の「黄」になっています。前のプログラムにあった色の対応表 C を通していないからです。水色に近づけたいなら、4(青)にしましょう
  2. 少し大きい:█ は 8 ドット単位なので、30 ではなく 32×32 ドットになります。x = 130 のときは右端が 162 になり、2 ドットだけ画面からはみ出します
  3. 速すぎる:Pyxel(30fps)のときより 2 倍の速さでワープします。前のプログラムのように、フレーム数 F を数えて動きを調整するとよいです。たとえば if (F % 2 === 0) の中で位置を変えれば同じ速さになり、F % 30 === 0 にすれば 0.5 秒ごとに変わります
  4. 使っていない定数:WIDTH・HEIGHT はどこでも使っていません。しかも HEIGHT = 120 は Pyxel の画面サイズで、Playload の画面は 144 です。消すか、144 に直しましょう

03. rectノイズあり、四角いゴミみたいなもの

rect4-f900.png

「ノイズ」に見える原因は主に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は、ありません。

rect4-f900.png

'ff818181818181ff' という文字列

コンピュータの世界で8×8ピクセルのドット絵を表現するための暗号のようなものです。

これを解読するには、文字列を「2文字ずつ」に区切って、16進数(0〜9とa〜fで表す数字)から、ドットのON/OFFを表す2進数(0と1)に変換します。

分かりやすく表と図で解説します。

1. 文字列を8つの行に分ける

16文字あるので、2文字ずつ8個のグループに分けます。これがドット絵の「上から1行目〜8行目」に対応します。

  • ff
  • 81
  • 81
  • 81
  • 81
  • 81
  • 81
  • ff

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 の折り返しは、画面の大きさに合わせて「端から出たら反対側へ」に置き換えました。

Metasprites-f900.png


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だけで成り立ちます。

  1. エディタ(VS Codeやメモ帳)で work\game.js を書いて保存する(ファイルが手元に残る)
  2. node tools\plcheck.mjs work\game.js --text で確かめる(ネット不要)
  3. 遊ぶときだけ --url でURLを作り、ブラウザで開く
  4. 提出はURLか .js ファイル。先生はまとめて node tools\plcheck.mjs 提出フォルダ でチェックする

ネットにつながっていなくても、確認(2)まではできます。実際に遊ぶ(3)には、kitao.devからアプリ本体を読み込むためにネット接続が要ります。

参考資料

0
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?