--- title: ブラウザWebVRで遊ぶ「おにごっこ」をA-frameとp5jsで作ってみた tags: VR JavaScript HTML A-Frame p5.js author: canonno slide: false --- #おにごっこしたい 最近はいろんな技術がすすみ、VR・AR・MRといった言葉も市民権を得てきましたよね。 昔ドラえもんで出てきた秘密道具が「__あれこれスマホでできるじゃん__」みたいなものだったり。 当時は「ドラえもん的」だと思われていたものがどんどん現実になってきていて、本当に技術の進歩ってすごいなあと思います。 どこでもドアホッシィィィイイイイ。 先日投稿した「[光センサで遊ぶ「かくれんぼ」をobnizとp5jsで作ってみた](https://qiita.com/canonno/items/00738c7d928c3ec655d5)」の中で、ブラウザで遊べるかくれんぼゲームを作りました。 A-frameを使えばサクッとVRが作れるということを知り、__VR版おにごっこ__を作ってみました。 ただ逃げ回るだけも面白くないなあと思ったので、白黒世界でめちゃめちゃ見えにくい世界を作ってみました。 コードはGistに載せてあるので是非実装してみてくださいね。 アプリもNetlifyに載せたので是非遊びに行ってみてください。 難易度がいまいちわからなかったので、3つ作ってみました。 Gistは[こちらから](https://gist.github.com/canonno/71ba35a59bcceaa81ea4b83f950c01e8) Appは やさしいモードは[こちらから](https://focused-nightingale-fcc7a6.netlify.app/) ふつうモードは[こちらから](https://naughty-beaver-64b32a.netlify.app/) くらやみモードは[こちらから](https://sleepy-wing-36aa7e.netlify.app/) ※VRをお持ちの方、__今回はブラウザで操作すること前提で実装してしまったため、VRで操作する仕様になっていない可能性があります(theクソ仕様)__。それを踏まえたうえでお楽しみください。 #完成デモ ##基本ルール おにごっこと同じく、__おにから逃げ続けるゲーム__です。 おには暗闇に溶け込みそうな真っ黒な箱。 こいつに追いつかれるか、場外に出てしまうとゲームオーバーになります。

a-frameとp5jsで鬼ごっこを作りました。真っ暗闇を黒い箱から逃げ回ってください。操作はクリックで進み続けるだけ。箱は日に日に速く動くようになります。5日目を超えられたら教えてください。https://t.co/ABG5CHaYY7#p5js #javascript #html #protoout pic.twitter.com/MVLLFiC9XX

— canonno (@canonno_blog) September 16, 2020
__黒い箱は近くにプレイヤーを見つけると接近してきます__。 検知の範囲はそこまで広くないので正面にいる場合は大丈夫だと思いますが、木々からヌッと出てくることもあるので要注意です。 また__黒い箱は一日ごとに足が速くなります__。 3日目ぐらいでプレイヤー同等ぐらいのスピードになるので、うまく先読みをしながら逃げ回ってください。 ##朝と夜 __ゲーム内には時間の概念がありまして夜になるほど空が暗くなってきます__。 この真っ暗な間は黒い箱は背景に隠れてしまう上に、木々が邪魔で見通しが悪くなります。 この間も黒い箱に近づくと接近してくるので要注意です。

真夜中はこんな感じ。
なあんにも見えません。#protoout pic.twitter.com/iy1ooZdkfF

— canonno (@canonno_blog) September 16, 2020
##ゲームオーバー タッチされる・場外に出ると記録が表示されます。

タッチされたとき・場外に出たときに記録が出ます。
5日目まで逃げ切れたら教えてください。#protoout pic.twitter.com/2gY0QVnHLv

— canonno (@canonno_blog) September 16, 2020
##ちなみに(余談) 今回__初めてのVR制作__でして、__VRのヘッドセットを使うことも初めて__。 今回は__Oculus Go__を一からセットアップしてやってみました。 動画も取ったんですが、なぜか起動ができなくなってしまってデータが抜けず断念。。。 DSC_0096.JPG テンションの上がるぼく 基本的な操作はブラウザと同じく、手元のコントローラーのクリックでどうにかなりそう。 クリックする必要が無く周りが見渡せるので、もしかしたらこちらの方が簡単かもしれません。 ただ今回「__ゲームオーバーしたらRボタンを押してね__」仕様にしているので、ゲームオーバーのたびにURLを叩く必要があるのが難点。 この辺りはヘッドセットの種類に応じて実装を変える必要があるんですかね? 今回はOculus Go自体初めてだったので、もう少し突き詰めてみたいところですね。 #実装こまごま ##基本アーキテクチャ 今回は __・index.html (画面表示用)__ __・sketch.js (常に処理し続ける内容の記述)__ __・components.js (基本オブジェクトのクラス・メソッドを記述)__ の3つで構成されています。 index.htmlはほとんどライブラリの呼び出しの記述のみで、メインはsketch.jsになります。 A-frameは__a-skyやa-entityのようなタグを記述することで3Dオブジェクトをラクラク生成を可能__にしてくれる便利なライブラリです。 今回はcreateElementなどを駆使しながら、a-sceneタグの子要素としてこれらのオブジェクトを入れ込んでいます。 詳しくは[公式ドキュメント](https://aframe.io/docs/1.0.0/introduction/)や[A-frameまわりのQIita記事](https://qiita.com/name_yy/items/9144f612a8e15d14f9ff)をご覧になりながら、是非ハンズオンしてみてくださいね。 ```html   ここにオブジェクトのタグが入りまくるイメージ ``` ##オブジェクトの生成処理 今回は[こちらのサイト](https://leoouyang.com/let-it-snow)をかなり参考にさせていただきました。 __こちらのサイトにあるコードを読み解いて、実装に利用した形になります。__ p5jsのお作法については[先日の記事](https://qiita.com/canonno/items/00738c7d928c3ec655d5)を見つつ実装しています。 今回は備忘録的にA-frameに関するお作法をメモしておきます。 p5jsではブラウザが読み込まれると同時にsetup()が実行されます。 setup()ではまずworldクラスという各オブジェクトを管理するためのクラスを生成します。 worldクラスで必要そうなメソッドはcomponents.jsに実装されています。 ```js //sketch.js function setup() { noCanvas(); world = new World('VRScene'); // //以下省略 // } //components.js function World(id) { console.log("A-FrameP5 v0.1 (Craig Kapp, 11/8/2016)"); if (id == undefined) { id = "VRScene"; } this.scene = document.getElementById(id); this.flying = false; this.setFlying = function(v) { this.flying = v; this.camera.setWASD(v); } this.getFlying = function() { return this.flying; } this.camera = new Camera(); this.scene.appendChild(this.camera.holder); this.add = function(entity) { this.scene.appendChild(entity.tag); } this.remove = function(entity) { this.scene.removeChild(entity.tag); } this.removeall = function(){ while(this.scene.firstChild){ this.scene.removeChild(this.scene.firstChild); } } // //その他様々な処理を定義 // } ``` このworldクラスへオブジェクトをどんどん追加していきます。 今回の敵役である黒い箱に着目すると、 ```js //sketch.js //setup()の中 enemy = new Box({x:x, y:0.5, z:z, width:1, height:1, depth:1, red:0, green:0, blue:0, }); world.add(enemy); //components.js function Box(opts) { // store desired options setEntityOptions(opts, this); // store what kind of primitive shape this entity is this.prim = 'box'; // setup geometry parameters if (!('width' in opts)) { opts.width = 1; } if (!('depth' in opts)) { opts.depth = 1; } if (!('height' in opts)) { opts.height = 1; } this.width = opts.width; this.height = opts.height; this.depth = opts.depth; // set geometry setGeometry(this); // set material processMaterial(this); setMaterial(this); // set scale setScale(this.opts, this); // set position setPosition(this.opts, this); // set rotation setRotation(this.opts, this); // set visibility setVisibility(this.opts, this); // set click handler setClickHandler(this); // init common setters / getters initializerSettersAndGetters(this); } ``` components.js中のsetEntityOptions()でタグを生成し、setGeometry()・setMaterial()・setScale()などによって属性情報の挿入処理が走るようです。 setEntityOptions()のcreateElement()でタグが生成します。 ```js function setEntityOptions(opts, entity) { // store desired options if (opts == undefined) { opts = {}; } entity.opts = opts; // create a tag for this box entity.tag = document.createElement('a-entity'); // setup a "children" array entity.children = []; } ``` その後の処理で属性情報が挿入されていきます。 setGeometryを代表例として示しますが、オブジェクトの形状に応じてタグの属性情報が変わるのでelse if(caseのほうが良いんでしょうか?)の嵐ですね。 ここでsetAttributeすることでタグへ属性情報を挿入することができます。 ```js function setGeometry(entity) { if (entity.prim == 'sphere') { entity.tag.setAttribute('geometry', 'primitive: sphere; radius: ' + entity.radius + '; segmentsWidth: ' + entity.segmentsWidth + '; segmentsHeight: ' + entity.segmentsHeight + '; phiStart: ' + entity.phiStart + '; phiLength: ' + entity.phiLength + '; thetaStart: ' + entity.thetaStart + '; thetaLength: ' + entity.thetaLength); } else if (entity.prim == 'circle') { entity.tag.setAttribute('geometry', 'primitive: circle; radius: ' + entity.radius + '; segments: ' + entity.segments + '; thetaStart: ' + entity.thetaStart + '; thetaLength: ' + entity.thetaLength); } else if (entity.prim == 'ring') { entity.tag.setAttribute('geometry', 'primitive: ring; radiusInner: ' + entity.radiusInner + '; radiusOuter: ' + entity.radiusOuter + '; segmentsTheta: ' + entity.segmentsTheta + '; segmentsPhi: ' + entity.segmentsPhi + '; thetaStart: ' + entity.thetaStart + '; thetaLength: ' + entity.thetaLength); } else if (entity.prim == 'cone') { entity.tag.setAttribute('geometry', 'primitive: cone; height: ' + entity.height + '; openEnded: ' + entity.openEnded + '; radiusBottom: ' + entity.radiusBottom + '; radiusTop: ' + entity.radiusTop + '; segmentsRadial: ' + entity.segmentsRadial + '; segmentsHeight: ' + entity.segmentsHeight + '; thetaStart: ' + entity.thetaStart + '; thetaLength: ' + entity.thetaLength); } else if (entity.prim == 'torus') { entity.tag.setAttribute('geometry', 'primitive: torus; radius: ' + entity.radius + '; radiusTubular: ' + entity.radiusTubular + '; segmentsRadial: ' + entity.segmentsRadial + '; segmentsTubular: ' + entity.segmentsTubular + '; arc: ' + entity.arc); } else if (entity.prim == 'torusKnot') { entity.tag.setAttribute('geometry', 'primitive: torusKnot; radius: ' + entity.radius + '; radiusTubular: ' + entity.radiusTubular + '; segmentsRadial: ' + entity.segmentsRadial + '; segmentsTubular: ' + entity.segmentsTubular + '; p: ' + entity.p + '; q: ' + entity.q); } else if (entity.prim == 'cylinder') { entity.tag.setAttribute('geometry', 'primitive: cylinder; radius: ' + entity.radius + '; height: ' + entity.height + '; openEnded: ' + entity.openEnded + '; segmentsRadial: ' + entity.segmentsRadial + '; segmentsHeight: ' + entity.segmentsHeight + '; thetaStart: ' + entity.thetaStart + '; thetaLength: ' + entity.thetaLength); } else if (entity.prim == 'box') { entity.tag.setAttribute('geometry', 'primitive: box; depth: ' + entity.depth + '; height: ' + entity.height + '; width: ' + entity.width); } else if (entity.prim == 'plane') { entity.tag.setAttribute('geometry', 'primitive: plane; height: ' + entity.height + '; width: ' + entity.width); } else if (entity.prim == 'octahedron' || entity.prim == 'tetrahedron' || entity.prim == 'dodecahedron') { entity.tag.setAttribute('geometry', 'primitive: ' + entity.prim + '; radius: ' + entity.radius); } } ``` setGeometryだとかsetMaterialだとか、それぞれがどんな役割をしているのかについてはまだまだ理解できていません。。。 A-frameのEntity-Component-Systemあたりが関係していそうなんですがいまいちわからず。。。 ちょっとずつ勉強していこうと思います。 ##おにごっこの処理 一通りオブジェクトの生成がすんだら、残りはオブジェクトをどう動かすかの処理になります。 p5jsのお作法ではdraw()が常に実行され続けるので、この部分におにごっこのロジックを書いていきます。 細かいロジックについては[先日の記事](https://qiita.com/canonno/items/00738c7d928c3ec655d5)を参考にしてみてください。 コード全体は[こちらから](https://gist.github.com/canonno/71ba35a59bcceaa81ea4b83f950c01e8#file-components-js-L1798)ご覧いただけますので、是非是非実装してみてください! 参考にしたサイトの方も[こちらから](https://leoouyang.com/let-it-snow)ご覧ください。 #今後やりたいこと A-frame面白い。。。楽しい。。。 みんなが作った3Dモデルの共有サイトもあるらしく、そこから良いモデルをインポートすることもできる様子ユメガヒロガルゥゥゥウウ VR周りはもっと勉強していきたい。 最後までご覧いただきありがとうございました!! LGTMつけていただけると励みになります、よろしくお願いします!