🐉
toio で「イライラ棒」的なものを作った
概要
Unity は慣れていないので toio SDK for Unity ではなく toio.js を使っています。
来年は Unity をちゃんと使おうと思います🙃
ということで、
Node.js 上で PS4のコントローラー(DUALSHOCK4) の入力をハンドリングして toio.js と連携させて、toio の衝突を検知したら DUALSHOCK4 のバイブレーションを発動させることで、
イライラ棒的な遊びができるものを作りました。
まずは早速、完成動画から
(音量を出していただくと、最後にコントローラーがブルブルしているのがわかります)
開発環境
- PC: Mac OSX 11.2.3
- Node: v14.18.2
- yarn: 1.22.17 (npm: 8.3.0)
- npm package: toio.js, node-gamepad
- その他: toio キューブ, DUALSHOCK4
システム構成図
完成したソースコード
先に完成コードを貼っておきますが、後段で順を追って説明を記述しています。
完成したソースコード
index.js
const { NearestScanner } = require('@toio/scanner');
const GamePad = require('node-gamepad');
const DEFAULT_BUFFER = [5, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0];
const INDEXES = {
rumbleLeft: 4,
rumbleRight: 5,
red: 6,
green: 7,
blue: 8,
flashOn: 9,
flashOff: 10
};
// https://github.com/rdepena/node-dualshock-controller から拝借
const setExtra = (controller, data) => {
const buff = DEFAULT_BUFFER.slice();
Object.keys(data).forEach((k) => {
buff[INDEXES[k]] = data[k];
});
controller._usb.write(buff);
};
let speedLeft = 0;
let speedRight = 0;
async function main() {
const cube = await new NearestScanner().start();
await cube.connect();
// 第二世代(CUH-ZCT2)の DUALSHOCK4 なので、第二引数を自分で設定する必要があった
// 第一世代(CUH-ZCT1)の場合は、第二引数は不要
const controller = new GamePad('ps4/dualshock4', {
vendorID: 0x054c,
productID: 0x09cc
});
controller.connect();
// 左スティックのイベントをハンドリング
controller.on('left:move', (evt) => {
// スティックの中央が 128 なので、中央が 0 になるように計算する
// また、0〜100で表現できるように正規化する
const x = ((evt.x - 128) / 128) * 100;
const y = -1 * ((evt.y - 128) / 128) * 100;
// スティックの小さな傾きは誤差として何もしない
if (Math.abs(x) < 10 && Math.abs(y) < 10) {
speedLeft = 0;
speedRight = 0;
return;
}
// なんか角度を計算して頑張る!
// ベストな計算かはわからないけど、トライ&エラーで良い感じに動いてる!
const angle = Math.atan2(y, x);
speedLeft = Math.abs(y) * Math.sin(angle) + Math.abs(x) * Math.cos(angle);
speedRight = Math.abs(y) * Math.sin(angle) - Math.abs(x) * Math.cos(angle);
});
// https://github.com/toio/toio.js/blob/master/packages/cube/src/cube.ts#L325 からコードを拝借
const threshold = 1;
cube.configurationCharacteristic.characteristic.write(Buffer.from([0x06, 0x00, threshold]), false);
cube.on('sensor:collision', (evt) => {
// isCollisionDetected が false に戻らない場合もある
console.log(evt);
if (evt.isCollisionDetected) {
cube.playPresetSound(4);
setExtra(controller, { rumbleLeft: 128, rumbleRight: 128 });
setTimeout(() => {
setExtra(controller, { rumbleLeft: 0, rumbleRight: 0 });
}, 1000);
}
});
// 走り続けさせるために定期的に0.1秒間動かす
setInterval(() => {
cube.move(speedLeft, speedRight, 100);
}, 10);
}
main();
作り方
移動制御編
- toio.js で toio に命令を出す
- node-gamepad で DUALSHOCK4 の入力をハンドリングする
- [1] と [2] を合わせてゴニョゴニョする
1. toio.js で toio に命令を出す
移動のサンプル
const { NearestScanner } = require('@toio/scanner');
async function main() {
const cube = await new NearestScanner().start();
await cube.connect();
const speedLeft = 70;
const speedRight = 70;
const durationMs = 1000;
cube.move(speedLeft, speedRight, durationMs);
}
main();
2. node-gamepad で DUALSHOCK4 の入力をハンドリングする
左スティックの入力ハンドリングのサンプル
const GamePad = require('node-gamepad');
function main() {
// 第二世代(CUH-ZCT2)の DUALSHOCK4 なので、第二引数を自分で設定する必要があった
// 第一世代(CUH-ZCT1)の場合は、第二引数は不要
const controller = new GamePad('ps4/dualshock4', {
vendorID: 0x054c,
productID: 0x09cc
});
controller.connect();
// 左スティックのイベントをハンドリング
controller.on('left:move', (evt) => {
console.log(evt);
});
}
main();
3. [1] と [2] を合わせてゴニョゴニョする
index.js
const { NearestScanner } = require('@toio/scanner');
const GamePad = require('node-gamepad');
let speedLeft = 0;
let speedRight = 0;
async function main() {
const cube = await new NearestScanner().start();
await cube.connect();
// 第二世代(CUH-ZCT2)の DUALSHOCK4 なので、第二引数を自分で設定する必要があった
// 第一世代(CUH-ZCT1)の場合は、第二引数は不要
const controller = new GamePad('ps4/dualshock4', {
vendorID: 0x054c,
productID: 0x09cc
});
controller.connect();
// 左スティックのイベントをハンドリング
controller.on('left:move', (evt) => {
// スティックの中央が 128 なので、中央が 0 になるように計算する
// また、0〜100で表現できるように正規化する
const x = ((evt.x - 128) / 128) * 100;
const y = -1 * ((evt.y - 128) / 128) * 100;
// スティックの小さな傾きは誤差として何もしない
if (Math.abs(x) < 10 && Math.abs(y) < 10) {
speedLeft = 0;
speedRight = 0;
return;
}
// なんか角度を計算して頑張る!
// ベストな計算かはわからないけど、トライ&エラーで良い感じに動いてる!
const angle = Math.atan2(y, x);
speedLeft = Math.abs(y) * Math.sin(angle) + Math.abs(x) * Math.cos(angle);
speedRight = Math.abs(y) * Math.sin(angle) - Math.abs(x) * Math.cos(angle);
});
// 走り続けさせるために定期的に0.1秒間動かす
setInterval(() => {
cube.move(speedLeft, speedRight, 100);
}, 10);
}
main();
衝突検出編
- toio.js で衝突検出をする
- toio.js でサウンドを鳴らす (衝突検出時)
- DUALSHOCK4 にバイブレーション命令を出す
- [1] と [2] と [3] を合わせてゴニョゴニョする
1. toio.js で衝突検出をする
衝突検出のサンプル
const { NearestScanner } = require('@toio/scanner');
async function main() {
const cube = await new NearestScanner().start();
await cube.connect();
cube.on('sensor:collision', (evt) => {
if (evt.isCollisionDetected) {
console.log('衝突判定');
}
});
}
main();
2. toio.js でサウンドを鳴らす (衝突検出時)
衝突検出のサンプル
const { NearestScanner } = require('@toio/scanner');
async function main() {
const cube = await new NearestScanner().start();
await cube.connect();
cube.on('sensor:collision', (evt) => {
if (evt.isCollisionDetected) {
console.log('衝突判定');
+ cube.playPresetSound(4);
}
});
}
main();
3. DUALSHOCK4 にバイブレーション命令を出す
バイブレーション発動のサンプル
const GamePad = require('node-gamepad');
// https://github.com/rdepena/node-dualshock-controller から拝借
// コントローラーは USB接続であること
const setExtra = (controller, data) => {
const DEFAULT_BUFFER = [5, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0];
const INDEXES = {
rumbleLeft: 4,
rumbleRight: 5,
red: 6,
green: 7,
blue: 8,
flashOn: 9,
flashOff: 10
};
const buff = DEFAULT_BUFFER.slice();
Object.keys(data).forEach((k) => {
buff[INDEXES[k]] = data[k];
});
controller._usb.write(buff);
};
function main() {
// 第二世代(CUH-ZCT2)の DUALSHOCK4 なので、第二引数を自分で設定する必要があった
// 第一世代(CUH-ZCT1)の場合は、第二引数は不要
const controller = new GamePad('ps4/dualshock4', {
vendorID: 0x054c,
productID: 0x09cc
});
controller.connect();
// ×ボタンを押したときのイベントをハンドリング
controller.on('x:press', (evt) => {
setExtra(controller, { rumbleLeft: 128, rumbleRight: 128 });
setTimeout(() => {
setExtra(controller, { rumbleLeft: 0, rumbleRight: 0 });
}, 1000);
});
}
main();
3. [1] と [2] と [3] を合わせてゴニョゴニョする
index.js
const { NearestScanner } = require('@toio/scanner');
const GamePad = require('node-gamepad');
// https://github.com/rdepena/node-dualshock-controller から拝借
const setExtra = (controller, data) => {
const DEFAULT_BUFFER = [5, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0];
const INDEXES = {
rumbleLeft: 4,
rumbleRight: 5,
red: 6,
green: 7,
blue: 8,
flashOn: 9,
flashOff: 10
};
const buff = DEFAULT_BUFFER.slice();
Object.keys(data).forEach((k) => {
buff[INDEXES[k]] = data[k];
});
controller._usb.write(buff);
};
async function main() {
const cube = await new NearestScanner().start();
await cube.connect();
// 第二世代(CUH-ZCT2)の DUALSHOCK4 なので、第二引数を自分で設定する必要があった
// 第一世代(CUH-ZCT1)の場合は、第二引数は不要
const controller = new GamePad('ps4/dualshock4', {
vendorID: 0x054c,
productID: 0x09cc
});
controller.connect();
cube.on('sensor:collision', (evt) => {
if (evt.isCollisionDetected) {
cube.playPresetSound(4);
setExtra(controller, { rumbleLeft: 128, rumbleRight: 128 });
setTimeout(() => {
setExtra(controller, { rumbleLeft: 0, rumbleRight: 0 });
}, 1000);
}
});
}
main();
衝突検出の閾値編
上記を見ると10段階のレベルで閾値を設定でき、デフォルトは7になっているとのこと。
衝突検出の閾値設定のサンプル
const { NearestScanner } = require('@toio/scanner');
async function main() {
const cube = await new NearestScanner().start();
await cube.connect();
// https://github.com/toio/toio.js/blob/master/packages/cube/src/cube.ts#L325 からコードを拝借
const threshold = 1;
cube.configurationCharacteristic.characteristic.write(Buffer.from([0x06, 0x00, threshold]), false);
}
main();
完成したソースコード(再掲)
index.js
const { NearestScanner } = require('@toio/scanner');
const GamePad = require('node-gamepad');
const DEFAULT_BUFFER = [5, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0];
const INDEXES = {
rumbleLeft: 4,
rumbleRight: 5,
red: 6,
green: 7,
blue: 8,
flashOn: 9,
flashOff: 10
};
// https://github.com/rdepena/node-dualshock-controller から拝借
const setExtra = (controller, data) => {
const buff = DEFAULT_BUFFER.slice();
Object.keys(data).forEach((k) => {
buff[INDEXES[k]] = data[k];
});
controller._usb.write(buff);
};
let speedLeft = 0;
let speedRight = 0;
async function main() {
const cube = await new NearestScanner().start();
await cube.connect();
// 第二世代(CUH-ZCT2)の DUALSHOCK4 なので、第二引数を自分で設定する必要があった
// 第一世代(CUH-ZCT1)の場合は、第二引数は不要
const controller = new GamePad('ps4/dualshock4', {
vendorID: 0x054c,
productID: 0x09cc
});
controller.connect();
// 左スティックのイベントをハンドリング
controller.on('left:move', (evt) => {
// スティックの中央が 128 なので、中央が 0 になるように計算する
// また、0〜100で表現できるように正規化する
const x = ((evt.x - 128) / 128) * 100;
const y = -1 * ((evt.y - 128) / 128) * 100;
// スティックの小さな傾きは誤差として何もしない
if (Math.abs(x) < 10 && Math.abs(y) < 10) {
speedLeft = 0;
speedRight = 0;
return;
}
// なんか角度を計算して頑張る!
// ベストな計算かはわからないけど、トライ&エラーで良い感じに動いてる!
const angle = Math.atan2(y, x);
speedLeft = Math.abs(y) * Math.sin(angle) + Math.abs(x) * Math.cos(angle);
speedRight = Math.abs(y) * Math.sin(angle) - Math.abs(x) * Math.cos(angle);
});
// https://github.com/toio/toio.js/blob/master/packages/cube/src/cube.ts#L325 からコードを拝借
const threshold = 1;
cube.configurationCharacteristic.characteristic.write(Buffer.from([0x06, 0x00, threshold]), false);
cube.on('sensor:collision', (evt) => {
// isCollisionDetected が false に戻らない場合もある
console.log(evt);
if (evt.isCollisionDetected) {
cube.playPresetSound(4);
setExtra(controller, { rumbleLeft: 128, rumbleRight: 128 });
setTimeout(() => {
setExtra(controller, { rumbleLeft: 0, rumbleRight: 0 });
}, 1000);
}
});
// 走り続けさせるために定期的に0.1秒間動かす
setInterval(() => {
cube.move(speedLeft, speedRight, 100);
}, 10);
}
main();
ステージを作る
苦労したところ
- toio.js の npm package が GitHub に比べて古かったこと
- 衝突状態が true になったあと false に戻らないことがあること
- 2回目の衝突が上手く反応しなくて困っています
- 上手く反応しなくなったら
node index.js
で再起動!
- node-dualshock-controller が yarn (or npm install) で失敗したこと
- そのため、一部のコードだけ拝借させてもらいました
- DUALSHOCK4 のバイブレーション発動が USB 接続で行う必要があった
- ボタンやスティックは Bluetooth 接続でハンドリングできました
- 撮影と操作を1人でやってるので難しかった!
- 衝突風景を撮影したいのに何度かゴールしちゃった!
最後に
GitHub のリポジトリに置きました。
記事の内容に加えて、以下の機能を追加しました。
- 閾値レベルを変更する機能
- 操作パターンの切り替え機能
- 「左右のスティックで操作」する機能が増え、左スティックで左タイヤを、右スティックで右タイヤを操作するような挙動にしています。
Discussion
こんにちは。衝突検出をやる場合は、evt.isCollisionDetectedを参照しないようにするとうまくいくと思います!
コメントいただきありがとうございます!
isCollisionDetected が true でコールバックされたあと、すぐに false がコールバックされることもあるので、
そのパターンの場合は
isCollisionDetected
をハンドリングしないと、処理が2回走ってしまうのが若干気になりますね。。。かと言って false がコールバックされない場合もあるので、そのときに2回目以降の衝突判定が上手いこと発生しなくなるんですよね。。。
そうなんですね。isCollisionDetectedはキューブから最後に通知された衝突検出フラグとイコールなため、
推測ですが、衝突検出が来た後すぐに、ダブルタップ検出が来ると、falseに上書かれてしまっている気がします。
こんなイメージです。
isCollisionDetectedは参照するとちょっと怪しいので、
こちらをやるのであれば、'sensor:collision'のイベントを処理した後、一定時間マスクするような処理を書いた方がいい気がします。
推測ですので、あくまでもご参考までですがお役に立てますと幸いです。
ご返信いただきありがとうございます。
なるほどです!
それで、何度 こちら で取得してみても、false に戻らなかったわけですね。そして
ダブルタップ検出
も同じセンサーで動くのでそのときに false に戻っている可能性があるってことですねー。すごくしっくり来た感じがあります。
たくさんご説明いただき本当にありがとうございます!
ご参考になれましたら良かったです!
順番前後しちゃいましたが、いらいら棒とても楽しそうですね!次回作も楽しみにしております!
大事な情報をお伝えし忘れていました。toio.jsのコードを見ると
'sensor:collision'のイベントはisCollisionDetectedがtrueの時しか通知されない仕様になっています。
補足情報まで。
おお。。。これは見逃しておりました。
参考情報ありがとうございます。