釣りゲームプロトタイプ「孤独な釣り人」について
1.Ogaki Mini Maker Faire出展を終えて
Ogaki Mini Maker Faire 2024(以下、OMMF2024)への出展を無事終えました。おかげさまで、出展の目玉に据えた釣りゲームプロトタイプ「孤独な釣り人(The Lone Angler)」は、段ボール製の食べられないおさかなさんに加えて「うまい棒」をちりばめたことが奏功し、2日間で約150人の来場者に操作をご体験いただきました。おいでになった方には、この場でも感謝申し上げます。
大垣どころかメイカーフェア出展自体が初めてで、どうなることかと非常に緊張して初日を迎えました。でも、ブースに閑古鳥が鳴くことはなく、他のマニアな出展者さんからの技術的な質問にもなんとか回答し、多くのお客さまに「面白い!」と言っていただきました。ゲーム機は横浜→長岡→大垣と続いた長距離移送にも見事に耐え、会場でもクリティカルな誤作動を起こすことなく無事に2日間動き続けました。ホッと胸をなで下ろしました。
今年は1月のファブアカデミー受講開始から始まり、早期退職(3月)と引越し(5月)、メキシコ渡航(7~8月)、白内障手術(8月)、ファブアカデミー後のものづくり(9~10月)と、記憶に残る1年間を過ごしてきましたが、ファブアカデミーでの課題製作作品をもって行ったOMMF2024出展で、今年の主だった行事はすべて終了しました。なんか気が抜けた感じで、再びエンジンをかけるには時間がかかりそうです―――な~んて書いたのがいけなかったのか、後回しにしていた懸案事項が週明けからワーッと浮上して来て、今軽いパニックに陥っているところです。
2.出展作品「The Lone Angler」
このゲームの説明動画はOMMF2024のイベントサイトにも掲載していますし、当日会場でも動画としてループ上映していたので、ご覧になられた方は多いと思いますが、再掲します。
このゲームは、ファブアカデミー第10週「機構設計・機械設計(Mechanical Design, Machine Design)」の課題として製作し、本記事のサムネ画像と動画も、その課題の一環として作りました。3Dプリンターにも応用されているCoreXYという機構を使い、2週間で何か動くものを作れという課題でした。
「2週間」とは言いましたが、その前週のローカルセッションでアイデア出しを行い、方針を決めた上で、翌週と翌々週で都合3回ローカルセッションでファブラボ関内(横浜)に集まり、製作とプログラミングを進めました。他の日もウェブサイトでのドキュメンテーション等、課題関連の活動は行っていましたが、実際にゲームを構築するという作業は実質3日間でした。
3.操作方法
「孤独な釣り人」は、現状、ラップトップの画面上でのマウス操作のみで動かします。Modular Thingsというブラウザベースのプラットフォームを使い、Javascriptで書いたコードをコピペして、すべての配線を終えてPCと接続確立した後、Runすると右側の操作パネルが表示されるという仕組みです。
OMMF2024会場では、ラミネート加工したこんな操作ガイドを用意しました。順番待ちのお客さんには、これを用いて事前説明を行いました。
4. 設計データ
OMMF2024会場で、私は多くのお客様に、「設計データは、プログラムコードも含めて全部公開しているので、どんどんハックして改良を加えて下さい」とお伝えしました。しかし、それがどこで公開されているのか、ちゃんとお示ししていなかったので、この場でご紹介します。
総合的記録はファブラボ関内のファブアカデミー2024ウェブサイト、それに、私個人のドキュメンテーション用に構築したファブアカデミーのウェブサイトです(英文でごめんなさい)。これら2つのサイト内で言及されているリンクも併せてご参照下さい。
CoreXYはXY平面上の座標を指定してそこにキャリッジ(台)を動かす機構です。私たちはこのキャリッジにもう1つステッピングモーターを横置きして糸車を回して糸を上げ下げする仕組みを考えました。一方、ファブラボ関内で同じ課題に取り組んだもう1つのチームは、キャリッジにペンホルダーとサーボモーターを付け、ペンプロッターを作りました。
時間的に断念した他のアイデアとしては、キャリッジにシリンジとソレノイドを置き、ソレノイドを利用して栓を開閉してカラーインクを下に敷いた紙に滴下して一品もののアート作品を作るとか、いっそCoreXYを縦置きして、指定したポイントにコンテナを収納するような機構を作るとかの案もありました。
機械製作は上記サイトでご確認いただけます。プログラムコードも上記ウェブサイトで公開しています。Modular Thingsを立ち上げて、ウェブサイトで公開しているコードをそこにコピペすれば使用環境はとりあえず確立できるでしょう。
ただ、ここで1点だけ補足があります。
CoreXYを動かすステッピングモーター3台の制御のために使ったSeeed Xiao RP2040のボードの製作とArduinoによるコード書き込みは、ファブアカデミーで私たちがお世話になったインストラクターが1月に参加されたInstructor Bootcampで作ったページを参照いただく必要があります。電子回路基板製作のためのデータやソースコードの出所は、ここに書かれています。
もしわからないところがあれば、noteのクリエイターページに登録しているFacebookのメッセージ機能(messenger)を使ってご連絡下さい。
5.反省点/今後の改善点
「孤独な釣り人」は、釣る魚としてうまい棒や軽いアクセサリー小物などを用意しておくと、会場では多くの子どもの注目を集めることは確認されました。OMMF2024がデビューとなりましたが、呼んでいただければまたどこかのイベントに持って行って、「釣り」をご体験いただけるものと確信しております。
ただ、それにあたっては、今後も少しずつ改良を重ねて行く必要があるようにも思います。製作過程で感じていた課題もまだ克服できていないところもありますし、OMMF2024会場で運用してみて、「ああ、そういう操作をするんだ」と私たちが気付かされたポイントもあります。気付いたところでいくつかご紹介しておきましょう。
(1)可動域
水槽いっぱいのスペースを使えそうに見えますが、座標原点よりも先のプラスのXY座標でしか動かせないので、釣り人の可動域は意外と狭いです。今回見ていて、手前の獲物を狙って釣り人を後退ないし左に寄せようと頑張ってクリックを続けた子が結構いましたが、XYともリミットスイッチが効いて、釣り人がそれ以上先に進めず「もがく」シーンが度々起きました。
もっと大きなCoreXYモジュールを作ればこの問題は克服できそうですが、今回OMMF2024でいただいたテーブルの幅は140cm、奥行きは70cmしかなく、すでに奥行きが76cmある水槽は、テーブルからはみ出します。そう考えると、CoreXYモジュールを拡張するより、使える領域を予め明示してそこにしか獲物を置かないようにするのが善後策となるでしょう。
(2)マウスの長押し
今回のプログラムは、マウスをクリックするごとに、X、Yの座標Aから、座標Bへと移動するというコードの書き方になっています。そして、釣り糸をたらすポイント(XY座標)を例えばC(1, 1)と決めたら、今度はZを加えた座標D(1, 1, 4)に向けて3つめのステッピングモーターを動かすことになります。座標のポイントの間隔により、小まめに移動させられるかどうかが決まって来ます。但し、毎回移動にはクリックが必要です。
クリックを小まめにして下さいと念を押しても、気付けば長押ししていたという利用者さんはかなり多く、特に釣り糸を引き上げる時には注意が必要です。「長押しで操作できないのか」との声を、会場では多く寄せられました。この辺は、キャリッジの移動スピードの設定を下げたりする方法を今後試してみたいと思いますが、テーブルの大きさに制約があるため、器はこれ以上大きくするのは難しいという制約条件の中で考える必要があります。
(3)マウス以外の操作とUI
今回の初出展でいちばん驚いたのは、今どきの子どもたちはタブレット端末での操作に慣れていて、マウスは使ったことがないということでした。すごい世代ギャップを感じました。
ということは、ラップトップ&マウスという組合せでは、そもそも想定利用者の属性とうまく合致していないということになります。前述の通り、このゲームはわずか2週間(実質作業日数は3日)で製作しているので、時間的制約もあってUI(ユーザーインターフェース)にあまり時間を割いていません。現状のコントロールパネルも、前後左右の矢印アイコンに比べ、糸の上げ下げの矢印アイコンと原点回帰のためのHOMEボタンが小さくてわかりにくいという欠点もあります。また、それ以前に、ファブアカデミーの課題として製作したという経緯もあって、UIが全部英語表記になっているのも、実装する段階ではハードルとなります。
従って、先ずはコントロールパネルのデザインを改良した上で、ラップトップ&マウスでの操作ではなく、ラップトップ上の矢印キーか、あるいはタブレット端末上のタップでで操作できるように改良できるといいでしょう。
また、タブレットで操作できるようになれば、このゲームの展示上の大きな課題―――釣りゲームに対してラップトップは真横から操作しないといけない(OMMF2024をはじめ、多くの展示会では出展者テーブルが横に並んでいて、ラップトップ上で真横から操作するのは不可能)という問題を克服することができます。
そのあたりの改善を、これから図っていかねばなりませ。課題がはっきりしたという意味で、初出展は私たちにとっても大きな収穫がありました。
6.おまけ~プレゼンテーション資料
ところで、OMMF2024では、「世界ファブラボ会議(FAB24)&ファブシティチャレンジ―メキシコ旅行記-」というプレゼンも初日午後のステージでやらせていただいたのですが、その時に使ったスライドもここで公開しておきます。以前noteで書いた記事もあわせてご笑覧いただくとよいでしょう。
追記(2024年12月7日)
とりあえず、UIを日本語化しておこうと思い、Javaのコードを少々書き換えてみました。近々また実演する機会をいただいたので、少しだけ改良して臨もうと思っています。
改良したコードを念のためこちらで記録しておきます。
//Code_yuichi4
// warning: without a powered usb-hub, currentScale > 0.5 are likely to fail
//
//Open browsers DevTools console with
// cmd + shift + j (Linux)
// ctrl + shift + j (win)
// command + option + j (mac)
//<button id="Start" style="background-color:yellow;"> Start</button>
//https://fabacademy.org/2024/labs/kannai/Instruction/tips/machine_building/
//<img src="https://fabacademy.org/2024/labs/kannai/images/logo2022-yoko-w_transp.png" alt="FabLab Kannai" width="200">
//<audio id="audio" src="https://fabacademy.org/2024/labs/kannai/Machine_Building_Project_JPN/code/baby-shark.mp3" preload="auto"></audio>
// <img id="countdownImage" src="https://fabacademy.org/2024/labs/kannai/Machine_Building_Project_JPN/code/3.png" alt="Countdown Image" width="70">
// UI in View tab
const el = document.createElement("div");
el.style = `
padding: 10px;
`
el.innerHTML = `
<audio id="audio" src="https://fabacademy.org/2024/labs/kannai/Machine_Building_Project_JPN/code/baby-shark.mp3" preload="auto"></audio>
<h2><font color="#5B9BAF">Fab Academy 2024</font></h2>
<h3><font color="#5B9BAF">「Modular ThingsとCore XYを用いた機械製作」(グループJP)</font></h3>
<img src="https://fabacademy.org/2024/labs/kannai/Machine_Building_Project_JPN/code/greatwave.png" alt="FabLab Kannai" width="100">
<br>
<p>
<hr>
<table>
<tbody>
<tr>
<td></td>
<td><button id="yPlus"> <img src="https://fabacademy.org/2024/labs/kannai/Machine_Building_Project_JPN/code/up.jpg" width="50" /></button></td>
<td></td>
<td> </td><!-- space in 3rd column-->
<td><button id="Up"> Z+ </button></td>
</tr>
<tr>
<td><button id="xMinus"> <img src="https://fabacademy.org/2024/labs/kannai/Machine_Building_Project_JPN/code/left.jpg" width="50" /> </button></td>
<td></td>
<td><button id="xPlus"> <img src="https://fabacademy.org/2024/labs/kannai/Machine_Building_Project_JPN/code/right.jpg" width="50" /> </button></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td><button id="yMinus"> <img src="https://fabacademy.org/2024/labs/kannai/Machine_Building_Project_JPN/code/down.jpg"width="50" /> </button></td>
<td></td>
<td></td>
<td><button id="Down"> Z- </button></td>
</tr>
</tbody>
</table>
<hr>
<p>
<button id="Fishing"> Go Fishing!!</button>
                
<button id="down" style="background-color:cyan;"> 糸↓</button>
<button id="up" style="background-color:cyan;"> 糸↑</button>
<button id="Home" style="background-color:cyan;"> HOME</button><p>
◆◆◆ゲームの進め方(マウスの左クリックで操作)◆◆◆<p>
1. 釣り人を前後左右に移動させます。(最初は前進または右方向への移動で!)<p>
2. 糸を下げます。釣れたら、糸を巻き上げます。<p>
3. HOMEで原点にもどります。<p>
<button id="DrawSquare"> DrawSquare</button>
<button id="DrawStar"> DrawStar</button>
<button id="Draw"> Draw</button>
<button id="Test"> Test</button>
<hr>
<p>
Open browsers DevTools console with<br>
cmd + shift + j (Linux)<br>
ctrl + shift + j (win)<br>
command + option + j (mac)
</p>
`
//test button
//================================
el
.querySelector("#Test")
.addEventListener("click", () => {
delay(100);
test();
//drawPolugon();
})
function test() {
machine.setPosition([0, 0]);
goTo(10,10 );
}
//================================
//Synchronizer of two motors
const machine = createSynchronizer([motorA, motorB]);
//Definition of CoreXY Motion
async function goTo(x,y){
console.log(`Moving to (${x}, ${y})`);
await machine.absolute([1*(x+y),1*(x-y)]); // The reason why "-1" is multiplied may be due to the wiring and origin position.
}
// Set button ID and click_event
el
.querySelector("#xPlus")
.addEventListener("click", () => {
xPlus();
})
el
.querySelector("#xMinus")
.addEventListener("click", () => {
xMinus();
})
el
.querySelector("#yPlus")
.addEventListener("click", () => {
yPlus();
})
el
.querySelector("#yMinus")
.addEventListener("click", () => {
yMinus();
})
el
.querySelector("#Fishing")
.addEventListener("click", () => {
Fishing();
})
el
.querySelector("#up")
.addEventListener("click", () => {
up();
})
el
.querySelector("#down")
.addEventListener("click", () => {
down();
})
el
.querySelector("#Home")
.addEventListener("click", () => {
goToHome();
})
el
.querySelector("#DrawSquare")
.addEventListener("click", () => {
delay(100);
drawSquare();
})
el
.querySelector("#DrawStar")
.addEventListener("click", () => {
delay(100);
drawStar();
})
el
.querySelector("#Draw")
.addEventListener("click", () => {
delay(100);
draw();
})
render(el);
// When you start running this javascript code, the machine runs from here
//motor setting
motorA.setCurrent(1);
motorA.setStepsPerUnit(5);
motorA.setAccel(20);
motorB.setCurrent(1);
motorB.setStepsPerUnit(5);
motorB.setAccel(20);
machine.setPosition([0, 0]);// set present position as (X0,Y0)
//machine.setPosition(0, 0);// CHECK this syntax works or not
motorC.setCurrent(0.8);
motorC.setStepsPerUnit(5);
//motorC.setAccel(50);
//const isAtEndStopX = false;
//console.log(isAtEndStopX);
async function Fishing(){
playAudio(); //Play Audio
// motorC.absolute(1000);
motorC.relative(1150);
console.log('down');
await delay(15000);
// motorC.absolute(0);
motorC.relative(-1150);
console.log('up');
await delay(15000);
console.log('mute');
muteAudio();//Mute Audio and
goToHome();
}
async function up(){
motorC.relative(-100);
await delay(1000);
}
async function down(){
motorC.relative(100);
await delay(1000);
}
// Function definition
async function goToHome(){
while(await motorB.getLimitState()){ // Limit switch at X- as Normally-Open
motorA.velocity(-10);//move motorA CW -> CCW
motorB.velocity(-10); //move motorB CW -> CCW
}
while(await motorA.getLimitState()){ // Limit switch at Y- as Normally-Open
motorA.velocity(-10); //positive value means CW -> CCW
motorB.velocity(10);//negative value means CCW -> CW
}
motorA.velocity(0);
motorB.velocity(0);
machine.setPosition([0, 0]);
await delay(1000);
goTo(10,10 );
// machine.setPosition([0, 0]);
await delay(1000);
}//end of goToHome
function xPlus() {
//zUp();
machine.setPosition([0, 0]);
for ( i = 0; i<10; i++){
goTo(i,0 );
}
}
function xMinus() {
//zUp();
machine.setPosition([0, 0]);
for ( i = 0; i<10; i++){
goTo(-i,0 );
delay(200);
}
}
function yPlus() {
//zUp();
machine.setPosition([0, 0]);
for ( i = 0; i<10; i++){
goTo(0,i );
}
}
function yMinus() {
//zUp();
machine.setPosition([0, 0]);
for ( i = 0; i<10; i++){
goTo(0,-i );
}
}
async function drawSquare(){
//down();
for (let i = 0; i < ptsSquare.length; i++){
await goTo(ptsSquare[i][0], ptsSquare[i][1]);
await delay(200);
}
//zUp();
}
async function drawStar(){
//down();
for (let i = 0; i < ptsStar.length; i++){
await goTo(ptsStar[i][0], ptsStar[i][1]);
await delay(200);
}
//zUp();
}
async function draw(){
//down();
for (let i = 0; i < pts.length; i++){
await goTo(pts[i][0], pts[i][1]);
await delay(200);
}
//zUp();
}
// Array of [x,y] positions of drawing design
//Square
var ptsSquare = [[50,0],[50,50],[0,50],[0,0]];
//Star
var ptsStar = [[20,10],[30,50],[40,10],[10,40],[50,40],[20,10]];
//TestDraw
var pts = [[62.2, 26.2], [29.7, 26.2], [29.7, 49], [61, 49], [61, 62.5], [29.7, 62.5], [29.7, 92.7], [29.7, 107.9], [29.7, 130.7], [60.5, 98.5],
[69.9, 107.9], [42.6, 135.8], [85, 187.9], [66.1, 187.9], [32.6, 145.6], [29.7, 148.5], [29.7, 174.4], [15.6, 174.4], [15.6, 107.9],
[15.6, 92.7], [15.6, 12.7], [62.2, 12.7], [62.2, 26.2]];
//SVG -> ChatGPT -> vertex coordinates of the shape//This is NOT tested yet
var polygon1 = [[10,10],[10,20],[20,20],[20,10],[10,10]];
var polygon2 = [[30,30],[30,40],[40,40],[40,30],[30,30]];
async function drawPolugon() {
machine.setPosition([0, 0]);// set present position as (X0,Y0)
//zUp();
//about polygon1
await goTo(polygon1[0][0], polygon1[0][1]);//move to first position of polygon1
//zDown();
for (let i = 1; i < polygon1.length; i++){
await goTo(polygon1[i][0], polygon1[i][1]);//finish drawing polygon1
await delay(200);
}
//zUp();
//about polygon2
await goTo(polygon2[0][0], polygon2[0][1]);//move to first position of polygon2
//zDown();
for (let i = 1; i < polygon2.length; i++){
await goTo(polygon1[i][0], polygon1[i][1]);//finish drawing polygon2
await delay(200);
}
//zUp();
//finished drawing all polygons
goTo(0,0);//move back to (X0,Y0)
}
function playAudio() {
// var audio = document.getElementById("audio");
// audio.addEventListener('ended', function() {
// audio.currentTime = 0;
audio.play();
// }, false);
}
function muteAudio() {
// var audio = document.getElementById("audio");
audio.pause(); // Pause playback
//audio.currentTime = 0; // Return playback position to beginning
}