プチコン ドット絵入力装置の制作

プチコン ドット絵入力装置の制作

moriyan6001
moriyan6001 (ID507) 2014/12/25
0
https://www.youtube.com/watch?v=hy1G9o8ykIc

プチコンという、3DS上で動くBASIC開発環境があります。3DS下画面のタッチパネル面に表示されたキーボードをプチプチとクリックしてプログラムを入力し、実行(RUN)すると、3DS上でプログラムが動き出します。

BASICとはいえ、3DSの特徴であるタッチパネルや2画面液晶、裸眼立体視、ジャイロセンサー等々を利用したプログラムを自分で作って手元で楽しめます。

プチコン上で作り始めてみると、BlueToothキーボードを使いたい、パソコン上のエディタで予めプログラムを書いておいてから3DSに転送したいと思うようになるのですが、そこはオトナの事情で出来ないようになっています。

私も最初は入力の不自由さにストレスを感じたのですが、無い物ねだりをしたところでしょうがないし、プチコンを使っているうちに、この小さな開発環境の規模にあったプログラミングをするのなら、タッチパネルの仮想キーボードでも充分なように思えてきました。(プチコンの基本スペックだけでみたらドラクエくらいの規模のゲームは作れそうですが)

そもそもプチコンプログラミングは仕事ではないですし、趣味でプログラムを書くのはとても楽しいので、開発環境にあったプログラミングをするには充分楽しめる環境です。プチコンは、プログラムを書いてすぐ実行、すぐに直してまたすぐ実行、を手軽に繰り返せるので、プログラミング自体にゲームのような楽しさがあります。

ただですね、やはり3DSでプログラミングをするわけですから、色々と絵を、キャラクターを動かしたくなります。そうなると動かす絵が必要になるわけですが、これもまた、3DS上で入力する必要があり、そのためのグラフィックエディタが用意されているのですが、ドット絵をひたすら打ち込んでいくというツールなのです。その辺りを考慮してか、プチコンにはあらかじめ、かなりの数のキャラクタデータがプリセットされてはいるのですが、やはりそれは自分が動かしたい絵ではないのです(プリセット画像のクオリティが低いという意味ではないです)。

プログラマとしてプログラミングは楽しいけど、ドット絵を打ち続けるのはさすがにつらい・・・じゃあ、そこは技術的に解決しよう!というわけで、面倒なところは機械任せにしてみた、というのが半分で、もう半分は機械が3DSの画面をクリックしてたら絵的に面白そうだし、サーボモータの制御は未経験だったので試してみたかったのです。

アイディアの元になったのはこちらの動画です。

http://vimeo.com/93852159

もしかして、私のFacebookのページについてるイイネ!も、誰かが機械で自動的に押してるのでは・・・と疑心暗鬼になりそうな装置ですが、サーボモーターだけでタッチパネルの接触判定が出来るのであれば、3DSにも応用できそうです。

私はソフトウェアエンジニアで、あと、電子回路はそれなりにはわかるのですが、モーターのような機械的なものを扱ったことがありません。サーボモータを制御して、3DSのタッチパネルをクリックする仕組みは漠然と想像が付きますが、機械工学として正しいのかどうかはわからないので試行錯誤で。

サーボモータの制御には、手元にあるArduino Unoを使いました。LEDをチカチカさせたことくらいはあります。

サーボモータは、2個で800円くらいの安いものを購入しました。過去にサーボモータを使ったことがないので価格差による性能の違いを知りませんし、1個400円だったら何かやらかして壊してしまっても勉強代としては安いものです。

機械的な仕組みは、手元にたくさんあるLEGOを使いました。LEGOは柔軟性があるので無茶な構造にも多少は耐えてくれますし、実際、購入したサーボモータはLEGOの規格とはズレがあるので、綺麗に収まりませんでしたが、サーボモータをLEGOで囲って隙間を詰めればなんとかなります。

このような、サーボモータの知識が無い、安いサーボモータなので怪しい、LEGO規格との整合性が微妙にとれてない、というグダグダな前提条件からもわかるように、精度を要求するようなものは作れない(作れない)ので、その条件で動作仕様を決めていきます。

3DSのタッチパネルとプチコンの仕様も事前に理解しておく必要があります。まず、3DSのタッチパネルは1箇所の接触のみしか判別できません。マルチタッチが出来ないのです。
プチコンのタッチパネル命令は、押されている時間(単位不明)と押されている箇所(座標)を取得するというものです。押されていなければ得られる時間は0になります。
また、押されたり、離されたりした瞬間のタイミングに合わせて何かしらの処理を実行することはできません。もちろん、タッチパネル命令を常に実行していれば瞬間的な判定は出来ますが、他の処理が実行できなくなります。ただ、今回は、タッチパネルの接触判定以外の処理を平行して実行する必要は無いので、タッチパネルの常時監視でも問題ありません。

以上を踏まえて、仕様を次のように決めました。

・WindowsPCから画像データをArduinoに送信し、サーボモータを制御して3DSへの入力を行う
・3DSのタッチパネル画面を左右2分割として、左を0、右を1とした1bit入力とする(マルチタッチ判定ができないので左右同時押しの判断ができないゆえ)
・時間情報は扱わない。タッチしている時間の長さを情報として扱わない

サーボモータを2つも使って1bitというのは随分と非効率だと思いますが、現状の精度では、ここが落としどころだと思いました。画面を4分割してサーボモータを4つ使えば一度に2bitの情報を送れるようになりますが、サーボ4つというのはちょっと大げさかなと。

サーボモータは円運動なので、LEGOのアームを上下させるには、円運動を直線運動に変換しなければなりません。

LEGOを使った円運動から直線運動への変換は、「レゴのしくみで遊ぶ本」(五十川芳仁著/ソフトバンクパブリッシング)を参考にしました。この本では、運動変換の方法が書かれていて、ラックというパーツを使う方法、楕円の円盤を使う方法、それから、今回採用した方法の説明がありました。ラックを使う方法も試してみたのですが、ギアとラックの摩擦係数が大きくなるからか、あまりスムーズに動かなかったので見送りました。ウォームギアを使う方法もあるのですが、回転の負荷が高くなり、やはり、うまく動きませんでした。

ラックパーツを使った円運動から直線運動への変換

後になって考えてみると、3DSのタッチパネルは斜め方向から圧力をかけても接触判定されるようなので、最初に紹介したイイネ!を延々とし続ける動画のように、円運動のままでもよかったような気がします。

3DSのタッチパネルはかなりの負荷をかけても壊れないそうですが、それでもLEGOの棒パーツが3DSの液晶を突き破るような最悪の事態を避けるために、アームの最下段(サーボモータの180度の位置)をタッチパネルの接触位置としています。
また、パネルの保護と精度の誤魔化し用に、LEGOの棒の先端に耳栓を付けました。最初は消しゴムを切り出して貼り付けていたのですが、あまり柔軟性がなく、代用品を100均ショップで探してみたところ、耳栓を発見したので採用しました。今回の装置では本来の用途とは違った抜群のぐにょぐにょ性能を発揮してくれています。

このようにして装置を組み立て、Arduinoからアームを上下させる信号を送ってみたところ、最初から精度は切り捨てたこともあってか、想像していた以上に上手く動作してしまいました。
この手の機構に詳しい人からしてみたら当たり前の動きなのかもしれませんが、初めてのサーボモータ工作なので、ギーコギーコと動く様を眺めているだけでも楽しいものです。

次の問題は、データの転送時間です。

最初の動作設定では、アームのダウンに500ms、アームのアップに500msとしました。1bitのデータ送信に1秒です。
プチコンの内部画像データは、16bitColor(Alpha1,RGB各5bit)で、上の液晶は400×240ドットです。画面1枚の絵は1536000bitですから、全てのデータを送るには17.7日かかる計算だと気がついたのは装置が完成してからです。データ送信指示を開始してから一晩くらいほっとけば朝には完了してるんじゃないかな?と雑に想像していたのですが、見積もりが甘いにもほどがありますね。

速度向上のためにアームの動作間隔を短くすると、接触判定で取りこぼしをするようになってしまいます。調整を繰り返してみたところ、上げ下げ動作それぞれで180msくらいが限界でした。これで2.7倍のスピードアップですが、18日かかっていたものが6日になってもあまり解決した感じがしません。

別の手段として、動作の効率化ができます。左側のアームを下げた状態から上げている間、次のアームの動作が右側の場合、左を上げつつ右を下げてもいいのです。つまり、0→1か1→0というbit変化の時には処理のスピードアップが望めます、が、左が離れつつある瞬間に、右が触れ始めるタッチパネルの誤判定の可能性があったので、この機能は見送りました。

入力速度の問題は根本的には解決できませんでしたし、そもそもこの装置はネタ度が高めではあるのですが、それでも実用性がまるで無いものにも思えなかったので、現実的な仕様として16x16ドット16色データを送るようにしてみました。16色なら1ドット当たり4bitの情報で済みます。

この手の装置はハードウェアだけではなく、ソフトウェアもそれなりに作らないといけません。タッチパネルから入力される0か1のbit情報を読み取り画素情報に変換する箇所はプチコンBASIC、WindowsPCからCOMポートを通して送られてくる情報を受け取り、サーボモータを制御するArduino言語、画像情報の送信はWindowsPC上のVisualC#を用いたプログラムという、3つの言語を使った3種類のプログラムが必要になります。

全体的にやってることは単純で低精度なので、プログラムもシンプルなものですが、それぞれの開発環境でプログラムの組み方が違ってきますから、頭の切り替えの方が一苦労です。

Arduino - Arduino言語

Arduinoはライブラリが豊富で、そのおかげでサーボモータの制御も非常に簡単になっています。実際、私は本来のサーボの制御方法を知らないまま、命令一つでサーボモータの角度を指定しているだけです。

Servo servo;
servo.attach(2);
servo.write(180);

これだけでArduinoのデジタル2番ピンに接続したサーボが180度まで動きます。

回路も単純です。

プログラムも単純で、PCからのデータの受信待ち、サーボの動作、PCへの処理終了の通知だけです。

#include <Servo.h>

Servo servoL;
Servo servoR;
const int C_RESET_DEG = 45;
const int C_UP_DEG = 110;
const int C_DOWN_DEG = 180;

const int C_SERVO_WAIT_UP = 180;
const int C_SERVO_WAIT_DOWN = 180;

void setup()
{
  Serial.begin(9600);

  servoR.attach(2);
  servoL.attach(3);
  statReset();
}

void statReset()
{
  // リセット状態の位置へ移動
  servoL.write(C_RESET_DEG);
  servoR.write(C_RESET_DEG);
  delay(500);
}

// 入力開始位置
void statStartPos()
{
  servoL.write(C_UP_DEG);
  servoR.write(C_UP_DEG);
  delay(500);
}

// debug用:両方ダウン
void statDoubleDown()
{
  servoL.write(C_DOWN_DEG);
  servoR.write(C_DOWN_DEG);
  delay(500);
} 

// バイトデータ受信(必要なデータは下位4bit分)
void receiveData(int v)
{  
  int i;
  int b;
  int mask = 0x08;
  
  // 4bit分を上位Bitから処理
  for (i = 3; i >= 0; i--) {
    b = (v & mask) >> i;
    mask = mask >> 1;
    if (0 == b) {
      // left arm down, up
      servoL.write(C_DOWN_DEG);
      delay(C_SERVO_WAIT_DOWN);
      servoL.write(C_UP_DEG);
    } else {
      // right arm down, up
      servoR.write(C_DOWN_DEG);
      delay(C_SERVO_WAIT_DOWN);
      servoR.write(C_UP_DEG);
    }
    delay(C_SERVO_WAIT_UP);
  }
  // アーム処理完了の通知
  Serial.println("rdy");
}

void loop() { 
  if ( Serial.available() > 0 ) {
    delay(10);

    char cmdStr[8];
    serialReadStr(cmdStr, 8);

    char cmd = cmdStr[0];

    if ('i' == cmd) {
      statReset();
      Serial.println("rdy");
    }
    if ('s' == cmd) {
      statStartPos();
      Serial.println("rdy");
    }
    if ('z' == cmd) {
      statDoubleDown();
    }

    if ( ('0' <= cmd && '9' >= cmd) || ('a' <= cmd && 'f' >= cmd) ) {
      char *e;
      long v = strtol(cmdStr, &e, 16);
      // 受信したデータを元にアームを動かす
      receiveData(v);
    }
  }
}

void serialReadStr(char* buf, int len) {
  for (int i = 0; i < len; i++) {
    buf[i] = Serial.read();
    if (buf[i] == '\0' || buf[i] == '\n' || buf[i] == -1) {
       buf[i] = 0x00;
       break;
    }
  }
}

気になるのは、左右2つのサーボに対して、角度指定の命令を2つ並べても、サーボの動作にズレが生じている点です。おそらく、サーボの角度指定を指定するservo.write()関数が、角度指定のパルスを出し終わるまで戻ってこないからだと思いますが、今回は2つのサーボをシンクロさせる必要は無いので弊害はありません。

3DS - プチコン3号BASIC

タッチパネルの状態を常時監視して、押された位置から0か1かを判定しています。タッチパネルの押下検出はこんな感じで。

DEF TOUCHCHK()
@TC0
TOUCH OUT I,X,Y
IF I<6 THEN GOTO @TC0
@TC1
TOUCH OUT I,X,Y
IF I>0 THEN GOTO @TC1
IF X<160 THEN R=1 ELSE R=2
RETURN R
END

これは、一定時間押されていた状態から、離された状態へと変化したら、押されたと判断しているためです。タッチパネルに触れる瞬間や離れる瞬間に、断続的に押される→離される→押される(チャタリング)が発生する可能性があるので、連続して押されている状態で判断しています。
もちろん、LEGOでなくて手でタッチパネルを交互に押して0と1を入力させることもできます。音ゲーを遊んでいると思っていたらデータを入力していたという新しい遊び方の提案ができるようなできないような。

'16COLOR PALETTE
DATA &HFF000000,&HFF000000
DATA &HFF000000,&HFF000000
DATA &HFF000000,&HFF000000
DATA &HFF000000,&HFF000000
DATA &HFF000000,&HFF000000
DATA &HFF000000,&HFF000000
DATA &HFF000000,&HFF000000
DATA &HFF000000,&HFF000000

'PALETTEヨミコミ
DIM PAL[16]
FOR I=0 TO 16
 READ C
 PAL[I]=C
NEXT

'SCREENセッテイ
ACLS
XSCREEN 3
DISPLAY 0:GPAGE 1,1:GCLS

'SPRITE
SPSET 0,0
SPOFS 0,200-8,16
DISPLAY 1:GPAGE 0,0:GCLS

'ヘンスウ
BITCNT=4
READCNT=0
BITCOUNT=0
READVAL=0
DX=0:DY=0

@MAIN

RS=TOUCHCHK()

READVAL=READVAL*2

LOCATE 0,0

IF RS==1 THEN
 PRINT "LEFT  "
 GFILL 20,20,159,220,RGB(255,0,0)
 GFILL 160,20,290,220,RGB(0,0,0)
ELSE
 PRINT "RIGHT"
 GFILL 160,20,290,220,RGB(0,0,255)
 GFILL 20,20,159,220,RGB(0,0,0)
 READVAL=READVAL+1
ENDIF

BITCOUNT=BITCOUNT+1
LOCATE 0,1:PRINT "BIT COUNT:";BITCOUNT

BITCNT=BITCNT-1
IF BITCNT==0 THEN
 '4BIT READ
 BITCNT=4
 READCNT=READCNT+1
 LOCATE0,3:PRINT "READ COUNT:";READCNT
 LOCATE0,4:PRINT "       "
 LOCATE0,4:PRINT "VAL:";HEX$(READVAL)
 GOSUB @DRAWBOX
 'SPRITE PAGE(GRP4)
 GPAGE 0,4
 GPSET DX,DY,PAL[READVAL]
 GPAGE 0,0

 READVAL=0
 DX=DX+1
 IF DX==16 THEN
  DX=0
  DY=DY+1
  IF DY==16 THEN END
 ENDIF
ENDIF

GOTO @MAIN

'1DOTブン ノ ハコ ヲ ビョウガ
@DRAWBOX
DISPLAY 0
GFILL 120+DX*10,40+DY*10,120+DX*10+10,40+DY*10+10,PAL[READVAL]
DISPLAY 1
RETURN

'TOUCHハンテイ
DEF TOUCHCHK()
@TC0
TOUCH OUT I,X,Y
IF I<6 THEN GOTO @TC0
@TC1
TOUCH OUT I,X,Y
IF I>0 THEN GOTO @TC1
IF X<160 THEN R=1 ELSE R=2
RETURN R
END

プログラムを見ても分かるように、パレット情報は事前に手打ちしておく必要があり、スプライト管理番号は0番固定という手抜きです。この辺は改良の余地がありますが、インタフェースの作り込み作業は面白みに欠けるので後回しです。

Windows - C#

外部からの画像ファイル読み込みや展開、データの送受信と、やることが多いのと、Windowsアプリとして動作させる都合上、ちょっとだけプログラムが長くなってしまいますが、C# & .NET環境だとマウスでポチポチしてるだけでも必要最小限の画面とインタフェース周りが出来るのが助かります。プチコンやArduinoと違って、イベントが非同期で発生するのと、イベント内の処理で、無限ループで常に通信ポートを監視するようなことが許されないので、その辺はWindowsのプログラミングの流儀に従っています。データの送信はスレッドを起こしてCOMポートの状態を監視しています。この辺の処理は、無限ループで延々と変数を監視するのではなく、MutexとかInterlockedとか使うのが正しいのだと思います。

ハマリどころというか勘違いをしていたのが、Arduinoから送られてくるデータの受信処理です。今回の処理では、ArduinoからWindows側に送られてくる文字列は"rdy"+CR+LF(改行コード)の5バイトなのですが、Windows側では都合良く改行を一区切りとした受信イベントが発生するわけではなく、"rd"だけ送られてきて、その直後に別のイベントとして"y"+CR+LFが送られてくるようなケースもあるのです。受信イベントが発生したらバッファに貯め置き、受信データの解析スレッド側でバッファから改行までのテキストを切り出すようにします。

ソースコードは長くなるので引用しませんが、そのうち気が向いたらgithubにでもUpしようかなと。

完成

ね、簡単でしょう?というくらい、通販で申し込んだサーボが手元に届いたその日には、基本的なところまで完成しました。アームの先端に付けた消しゴムが綺麗に固定できなかったりボロボロと壊れたりしたのが悩ましかったくらいです。
16×16ドット16色を送るのに要する時間は約6分30秒でした。基本的に放置なので転送時間は気にならないのですが、動作音が非常にやかましいですね、これ。

改善の余地考察

実装の手抜き加減はさておき、この装置を発展させるとしたら、どのような事が考えられるでしょうか。

プチコンでは、プログラム自体もテキストデータとして扱うことが出来るので、外部からプログラムを送る事も不可能では無いです。ただ、そこは自分の手で打ち込んで楽しみたい部分でもあります。

現状の問題点としては、タッチの検出ミスがあると、それ以降のビットが全部狂うことになるという、割と致命的な欠点があります。例えばパリティやCRCによるエラーチェックを実装しても、3DS側からArduinoやPCに通知する手段が用意されていません。通知手段としては、3DSのスピーカやヘッドホン端子を利用した音を使う方法や、3DSの画面をフラッシュさせ、Arduino側の光センサで検出する方法が考えられます。単方向通信だったものを双方方向にするのは次の課題として面白そうです。

根本的なところで、プチコンにデータを送る手段は他にもあり、マイク端子を使った方法で実装している方がいます。データの転送レートも処理方法も堅実なので、私のようなタッチパネルをガシガシとクリックする装置よりも随分とスマートだと思います。

今回の装置を作った後に、2つのサーボを使って情報を送る方法を色々と考えてみました。ちょっと欲張って2bit入力です。

下2つの案は、左端からタッチパネルに接触開始して右方向に動かし、その距離でbit判定を行うという方法です。この方法だと、ずっとタッチパネルに触れたままでも判定が出来るというメリットもあります(左方向への移動は戻り動作なので読み捨てる)。
右2つの案は、そもそもサーボモータは回転運動なので、最初から円運動として情報を送るという方法です。

データの送信量を増やす方法は他にもあります。サーボの動作精度を上げる方法以外に、3DSの物理ボタンを使う方法です。タッチパネルは1点の読み取りしかできませんが、ABXY等々のボタンは複数同時押しを検出できますから、シフトキーのように扱って、タッチパネルから得られる情報を倍にすることができます。

こういった機械工作はとても楽しいですね。もっと基本的な知識を身につけたいので勉強会とかあれば参加してみたいです。

ミクのドット絵は、ニコニ・コモンズのMSR3DS様作成の「初音ミク ドット絵」を利用させて頂きました。

出典- http://commons.nicovideo.jp/material/nc76457

参考にしてくれた記事

記事が登録されていません。
この記事を参考にして、新しく記事を投稿しよう!

違反について