飽きっぽいので、やったことはその瞬間にアウトプットしておきたいなと思いました。調べたことなどを書きます。
お休みなので勉強がてらあのゲームを作りました。
「戦争」や「いっせーのせ」にはバラエティに富んだ名前があるのに、コイツの名前は聞いたことがないですね。とりあえずYubiGameとして開発を進めます。
こういうった遊びは名前もいろいろですがルールもいろいろ。私の地元では自分の両手の本数を分配できるルールがありました。が、これは実装していません。作るの飽きちゃったので。
WebSocket
Webの対戦ゲームなのでWebScoketを使います。
アーキテクチャは↑と同じですね。フロントエンド200行、バックエンド150行くらいでした。
素材
Canvasで動的に手の画像を生み出そうかと思いましたが、パターンも少ないので全通り自分で書きました。昨年Apple Pencilを購入したおかげで、こういった素材を自分で用意できるようになったことは進歩。素材だけでなく設計などの構想を描くのにも使えて便利。
ibisPaintを使ってみているのですがオススメがあったら知りたいです。
ドラッグ&ドロップ
「直感的に操作できたらオシャレじゃね?」と思ったのでドラッグ&ドロップで操作できるようにしました。
実装にはReact DNDを使います。手のコンポーネントがこんな感じ。
const Hand = ({ id, hand, name, canDrag, canDrop, callback }) => { // ドラッグ設定 const [, drag] = useDrag({ type: 'HAND', canDrag: (_) => canDrag, end: (_, monitor) => { const dropResult = monitor.getDropResult(); if (dropResult && name !== dropResult.name && id !== dropResult.id) { callback({ fromHand: hand, target: dropResult.id, toHand: dropResult.hand }); } } }); // ドロップ設定 const [, drop] = useDrop({ accept: 'HAND', canDrop: (_,) => canDrop, drop: () => ({ id, name, hand }), }); return ( <div ref={drop}> <img ref={drag} alt={name} src={`/image/${name}.png`} /> </div> ); };
今回のケースではドラッグする対象とドロップする対象が同一なので、このコンポーネントにどちらの定義も記載します。2021年9月のver14でuseDrag()
の引数に変更があったので注意。
Release 14.0.0 · react-dnd/react-dnd · GitHub
// v14より前 const [, drag] = useDrag({ item: { type: 'HAND' } }); // v14以降 const [, drag] = useDrag({ type: 'HAND' });
長押しアクションを無効化
スマホでドラッグ&ドロップしようとすると、長押しメニューが表示されてしまって煩わしい。これを無効化します。今回のケースとは逆ですが、コンテキストメニューの有効化などは過去に試していました。
ひとまずCSSはこれで。
img { -webkit-touch-callout: none; }
-webkit-touch-callout
はWebKit用のプロパティであり、Safari専用の非標準機能です。使わない方がいいですね。Safari用と書きましたが、iOSのブラウザのレンダリングエンジンは(多分)すべてWebKitです。ChromeもBlinkではなくWebKit。全人類iPhoneの世界を想定しているのでこれで大丈夫でしょう。
Tailwind CSS
今さら感はいなめないですが触ったことがなかったので導入。公式のサンプルどおりにログインフォームを作ってみます。
const Login = () => { const [ room, setRoom ] = useState(null); return ( <div className="text-center flex flex-col justify-center items-center"> <img alt="logo" src="/image/logo.png" /> <div className="flex items-center border-b border-blue-500 py-2"> <input className="appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none" placeholder="Room Name" onChange={(e) => setRoom(e.target.value)} /> <button className="flex-shrink-0 bg-blue-500 hover:bg-blue-700 border-blue-500 hover:border-blue-700 text-sm border-4 text-white py-1 px-2 rounded" disabled={!room} onClick={() => socket.emit(LOGIN, { room })} > Join </button> </div> </div> ) };
CSSクラスがカオスですね。「汎用的なクラスを組み合わせてデザインをカスタマイズするのがTailwindのやり方だ!」ってこと。コンポーネントにデザインをまとめられる点は好き。
Tailwindから逸れますが、socketが関数外部に依存しているのでヨクナイ。。
それから、あぁ、シンタックスハイライトが。スマホでは横スクロールもなくて読みづらい。この辺りのブログの設定を今年やりたいな。
roomを空にする
v4であればsocketsLeave
というメソッドが用意されているらしい。StackOverflowですがコピペしていないので大丈夫ですよ。*1
node.js - how to delete a room in socket.io - Stack Overflow
io.in("room1").socketsLeave("room1");
公式はこの辺ですね。
The Server instance | Socket.IO
Some utility methods were added in Socket.IO v4.0.0 to manage the Socket instances and their rooms:
v4万歳。修正コミットはこちら。
集合型: Set
socket.rooms
に参加しているroomの一覧が格納されているらしい。 この一覧にはsocket.id
も格納されているので、それを省いてroomを取得しようとしたらエラー。
// NG const room = socket.rooms.find(room => room !== socket.id);
このプロパティ、配列ではなく集合型でした。いろいろ参考。
// for for (const room of socket.rooms) {/* */} // forEach socket.rooms.forEach(room => {/* */});
競プロで「Set使おうねー」って学んだ気がするけど使った試しがないです。
おわりに
ゲームでもつくってバズろうと思ったんですが、このゲームってタイミングをあわせる必要もないしzoomなどで手軽にできるんですよね。対人ならレートが必要だし、強いCPUを作ったほうが良さそうだなと思いました。