
はじめに
1990年代にパソコン通信※1で流行った「うねうね」というソフトを覚えている人はいますか?うねうねと気持ち悪い物体が画面上を動くだけの、スクリーンセーバーみたいなフリーソフトです。なぜか突然うねうねを思い出したので、M5Stackでうねうねを作ってみました。
※1…パソコン通信とはインターネットが普及する前にあった通信手段で、電話回線を介してアナログモデムでBBS(今で言うサーバー)に接続して、BBSの利用者と交流をしていました。
うねうねってどんなだっけ?
「うねうね」ってどんなだっけ?記憶では肌色の芋虫みたいな気持ち悪い物体がうねうねと動いている、というのが脳裏に焼き付いています。ネットで検索すると、うねうねについて書かれている記事を見つけました。
そうか、そんな感じだったか。ちょっと記憶との違いを感じつつも、そういえばそんなだったかもなぁ…という気もしてきました。当時のうねうねはもう忘れてしまいましたが、その雰囲気を再現したいと思いました。
目次
(1) M5Stack版 うねうね
(2) PC-98版 うねうね
(3) M5Stack版 うねうね プログラム詳細
(4) カスタマイズ方法
(1) M5Stack版 うねうね

ワームのような気持ち悪い生き物が蠢いてます!
プログラム
プログラムは GitHub にアップしました。
M5Stack Core 2を想定していますが、M5Unifiedを使用しているので、他の機種でも動くと思います。M5Stack BasicやCoreS3 SE、M5StickC Plus2でも一応動きました。Tab5ではディスプレイの解像度が高いのでちょっと小さく表示されます。
プログラムの詳細については (3) で解説します。
インタラクティブ機能
せっかくタッチパネルがあるので、うねうねと触れ合える機能を追加しました。画面に触れると高速でうねうねしながら指の方向に向いてきます。餌がもらえると思ったのでしょうか?かわいいですね(かわいくない)
本当は指を中心に向かせたかったのですが、負荷対策でディスプレイを分割表示している関係で、向く方向は同じ方向になっています。
うねうね切り替え
ボタンAを押すと別のうねうねが表示されます。現在 image.h には6種類のうねうねが収録されていて、Aボタンを押すたびに切り替わるようになっています。ボタンBはスピードダウン、ボタンCはスピードアップです。
FPS表示
デフォルトでは画面右下に “20 / 30” のようにfpsが表示されます。左側が実際のfps、右側が設定上限のfps値です。うねうねベンチみたいなの作れたら面白いかなぁ。
動画
(2) PC-98版 うねうね
PC-98エミュレーターで実際にうねうねを動かしてみました。
PC-98エミュレーターのインストール
エミュレーターはT98-NEXTを使用させていただきました。T98NEXT20100525版をダウンロードしてインストールしました。OSはFreeDOS(98)を使用させていただきました。
うねうねのダウンロード
なんと!令和7年においてもまだダウンロードできるようです。ベクターさんが歴史的作品を保全してくれていました。これを見ると、うねうねにはいくつか種類があるようですね。
・うねうね 2.0 (95.12.22公開 13K)
・うねうねのバリエーション (95.12.22公開 25K)
・うねうね(花火バージョン) 1.4 (95.12.22公開 10K)
ファイルはLHZ形式で圧縮されているので、Lhasaなどで解凍します。EXEファイルが現れますが、残念ならがWindowsでは実行できません。
フロッピーディスクの準備
T98-NEXTのFDDファイルはNFDという形式なのですが、この形式のファイルを扱える仮想FDDドライバを見つけることができませんでした。そこで、リアルなフロッピーディスクドライブを使用することにしました。なぜか持ってるUSB接続のFDD。

Windows 11に接続してフロッピーディスクを挿入すると・・・

これは!幻の… A: ドライブではないか!!
今でも存在したんだな!!
マウントしたらうねうねのファイルをFDに保存し、T98-NEXTのFD 2のNew→FDをイメージに変換 A 1.44M を選択するとNFD形式のFDイメージが作成されます。
PC-98を起動
ドライブ1にfd98_144.img、ドライブ2に先ほどのFDイメージを指定して起動します。
実行してみると、あれ…こんなやつでしたっけ?もっとキモかったような。。。うねうね 2.0には4つのサンプルが収録されていて、上記デモではそれぞれ実行しています。3つ目の白い口を開けたワームみたいなのは記憶にありますね。NIKONIKO.EXEは一番記憶に近いけど色が違うのと顔がある。KEHAE.EXEが記憶にあったやつかなぁ。もうちょっとツルツルだったような…。うーん。イメージはカスタマイズできるので、もしかしたらいろんなカスタムイメージが流通していたのかもしれません。
(3) M5Stack版 うねうね プログラム詳細
はじめにM5Stackで実現するために、どのように作っていくか考えました。PC-98に比べればメモリもCPUも遥かに潤沢ではありますが、ディスプレイのサイズやメモリの制約を考えなくてはなりません。また、どうやってキモイ動きにするかも課題でした。
気持ち悪い動き
うねうねの特徴といえば、あのクネクネした気持ち悪い動きです。オリジナルのうねうねを見ていると、スプライトを上に重ねていくことで立体的な表現をしているようです。しかしうね(うねうねの1匹の部分を単数形でうねと呼ぶことにします)の根本と胴体と頭は直線状に並んでいるのではなく、くねっとしています。これをどのように座標を計算しているのか考えてみました。

頭(1)が動いたあと、胴体(2)は次のステップで(1)に追従します。胴体(3)は胴体(2)に追従します。先頭の動きに1サイクル分遅延して胴体が動いていきます。しかし移動範囲は前後の中間点なので、頭が急に反対方向に向かっても胴体が飛び出てしまうことはなく、下に行くほど緩やかに動きます。
// 通常の移動
ux[head] += move_x;
uy[head] += move_y;
// 胴体の座標を計算
for (int i=num-2; i>=1; i--) {
ux[i] = (ux[i+1] + ux[i-1]) / 2;
uy[i] = (uy[i+1] + uy[i-1]) / 2;
}
キャンバスの構成

以前ディスプレイやキャンバスの描画速度をテストして、ディスプレイへの出力には時間がかかることがわかりました。大量のスプライトを直接出力するの不向きなようです。またディスプレイに直接出力すると画面のチラツキが目立ちます。そんなときに便利なのがキャンバス。M5UnifiedにはCanvasという仮想ディスプレイがあります。今回はキャンバスを複数使用して合成することで、高速に表示するようにしました。
canvasUneは24×24ピクセルのキャンバスで、うね1匹分の画像データを格納しています。canvasFixedは160×120ピクセルのキャンバスで、ディスプレイの縦横ともに半分のサイズになっています。canvasPaddingは160×120の周りにうね1匹分の余白を含めたサイズです。これらは全て高速なDMA転送が可能な内蔵メモリ内にあります。本当は320×240のキャンバスが作れれば良かったのですが、作成可能なメモリサイズを超えるため、分割せざるを得ません。PSRAMであれば可能ですが速度が遅いというデメリットもあります。

描画手順としてはまずはじめにcanvasPaddingにうねうねを描画していきます。うねうねは動くので160×120の枠からはみ出ますが、canvasPaddingははみ出る範囲も描画エリアになっています。全ての描画が終わったら、canvasPaddingをcanvasFixedにコピーします。この際ははみ出る部分は範囲外なので、コピーされるのは中央部分だけになります。
int pz = -padding_une;
// キャンバスの中央部分をコピーする
canvasPadding.pushSprite(&canvasFixed, pz, pz);
次にはみ出た部分canvasPaddingをcanvasFixedの反対側の端に、透過モードで合成していきます。透過モードは指定した透明色(今回は0x0000)以外をコピーするモードで、これにより元の絵の上に背景を除いたうねうねだけが重ねてコピーされます。透過モードは単純なコピーよりも処理が重いため、このように複数回に分けてコピーするエリアを分割して処理しました。
// canvasPaddingの左はみ出し部分をcanvasFixedの右に合成する
canvasPadding.pushSprite(&canvasFixed, pz+canvasFixed.width(), pz, transparent);
// canvasPaddingの上はみ出し部分をcanvasFixedの下に合成する
canvasPadding.pushSprite(&canvasFixed, pz, pz+canvasFixed.height(), transparent);
最後にcanvasFixedをディスプレイに敷き詰めていきます。普通にディスプレイに敷き詰めた場合、例えばうねうねが右に動くと画面から欠けてしまいますが、それだと境目が欠けた状態で並んでしまい非常に目立ちます。そこで境目を目立たなくするために、左端を右端に、上端を下端に合成しています。よく見ると境目がわかりますが、ぱっと見ではそれほど目立ちません。
// LCDに描画
for (int x=0; x<m5_w; x+=canvasFixed.width()) {
for (int y=0; y<m5_h; y+=canvasFixed.height()) {
canvasFixed.pushSprite(&M5.Display, x, y);
}
}
(4) カスタマイズ方法
最後にうねうねをカスタマイズする方法を説明します。
基本設定の変更
const int split_width = 160; // ブロックの幅(px) = canvasFixed.width()
const int split_height = 120; // ブロックの高さ(px) = canvasFixed.height()
const float une_density = 1.0; // うねうねの密度(1.0で160x120に約24匹)
float une_speed_default = 0.10; // うねの移動速度
float une_moving = 0.7; // うねの最大移動範囲(0.0~1.0)
int fps_limit = 30; // FPS制限
bool show_fps = true; // FPSを表示
une_density を変更するとうねうねの密度を下げたり上げたりできます。ただしパフォーマンスに影響しますので、密度を上げると表示スピードが遅くなってしまいます。うねうねが動くスピードを変えるにはune_speed_defaultを調整します。
une_moving は根本の位置からどのくらいの範囲まで動くかの設定で、1.0で頭1個分です。大きくしすぎると画面からはみ出して、キャンバスの境目が目立つようになるので注意してください。
show_fps=falseにすればFPS表示は消えます。いずれにしてもFPSや1フレームの処理時間はシリアルコンソールで確認することができます。
画像の変更
画像データは image.h にRGB565形式(16bitカラー)で保存します。Lang-shipさんの 画像データImageData化 ツールを使うと便利です。24×24ピクセルのPNG画像を作ってこのツールで変換すればOKです。画像を作る際は黒(R:00 G:00 B:00)が透過処理されるので、背景を黒にしておきます。
// 画像情報のテーブル
ImageInfo imgUnes[] = {
{ imgBin_HoleGreen, 24, 24 }, // 緑うねうね 穴
{ imgBin_HolePink, 24, 24 }, // ピンクうねうね 穴
{ imgBin_Green, 24, 24 }, // 緑うねうね のっぺら
{ imgBin_Pink, 24, 24 }, // 毛がないうねうね 口なし
{ imgBin_Mouth, 24, 24 }, // 毛がないうねうね 口あり
{ imgBin_Poison, 24, 24 } // 毛が生えてるうねうね
};
最後に imgUnes[] に加えてあげれば、上から順番に表示されるようになります。