WebGLゲームの開発の特徴Digital Trip

画像



こんにちは、Habr! この記事では、WebGLゲームDigital Tripの開発における私自身の経験を共有したいと思います。 WebGLに加えて、ゲームはWebAudio API、WebSockets、getUserMedia、Vibration API、DeviceOrientationなどのテクノロジーを使用し、three.js、hedtrackr.js、socket.ioライブラリなども使用します。この記事では、最も興味深い実装の詳細について説明します。 ゲームエンジン、モバイルを使用した制御、Webカメラの制御について説明します。また、dogecoinデーモンと連動して、node.jsのバックエンドについていくつか説明します。

記事の最後に、使用するライブラリ、GitHubのソースコード、ゲームの説明、およびゲーム自体へのリンクがあります。

猫の下で興味のある方、どうぞ。





ゲームプレイは非常にシンプルです。指定された軌道に沿って飛行し、コインとボーナスを収集し、石をかわします。 プレイヤーのポジションは3つのオプションに制限されています。 ボーナスには、シールド(HTML5)、スローダウン(猫)、ライフリストア(唇)の3種類があります。 ゲームの終わりに、受け取ったコインをドゲコインウォレットに引き出すことができます。



画像



ゲームの開発の目的は、ブラウザの機能について話し、スキルをアップグレードし、経験を共有し、プロセスを楽しむことです。

次に、実装の機能について詳しく説明します。



ゲームエンジンと詳細



グローバル変数DTは、ユーティリティ関数、クラスコンストラクター、インスタンス、およびハンドラー関数、さまざまなパラメーターなどにアクセスできる名前空間として使用されます。



プリローダー


3つのスクリプトがページに接続されています。

<script src="js/vendor/jquery.min.js"></script> <script src="js/vendor/yepnope.1.5.4-min.js"></script> <script src="js/myYepnope.min.js"></script>
      
      





他のスクリプトをロードするには、 yepnopeリソースローダーを使用します。

myYepnope.jsを実行すると、ブラウザーはWebGLサポートを確認します。

 var isWebGLSupported, canvas = document.getElementById('checkwebgl'); if (!window.WebGLRenderingContext) { // Browser has no idea what WebGL is isWebGLSupported = false; } else if (canvas.getContext("webgl") || canvas.getContext("webGlCanvas") || canvas.getContext("moz-webgl") || canvas.getContext("webkit-3d") || canvas.getContext("experimental-webgl")) { // Can get context isWebGLSupported = true; } else { // Can't get context isWebGLSupported = false; }
      
      





ブラウザがWebGLをサポートしている場合、myYepnopeはリソースの読み込みを表示する関数を定義し、残りのスクリプトを読み込みます。

ここで、プリローダーが機能し始めます。 視覚的には、これはゲームのぼやけた開始インターフェイスであり、読み込みに伴ってぼかし半径が減少します。





ぼかし効果は、cssプロパティ-webkit-filter: blur()



を使用して実現されます。 プロパティは完全にアニメーション化されています。 Firefoxでは、svgフィルターが使用され、その半径はcss-property filter: 'url()'



形式で動的に変更されて適用されfilter: 'url()'



data url



スクリプトによって生成され、負荷の20%ごとに更新されます。

コード
 if (isWebGLSupported) { var $body = $('body'), $cc = $('.choose_control'), maxBlur = 100, steps = 4, isWebkitBlurSupported; if ($body[0].style.webkitFilter === undefined) { isWebkitBlurSupported = false; $cc.css({filter: "url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\"><filter id=\"blur-overlay\"><feGaussianBlur stdDeviation=\"" + maxBlur + "\"/></filter></svg>#blur-overlay')"}); } else { isWebkitBlurSupported = true; $body[0].style.webkitFilter = 'blur(' + maxBlur + 'px)'; } $('#loader').css({display: 'table'}); $cc.css({display: 'table'}); yepnope.loadCounter = 0; yepnope.percent = 0; yepnope.showLoading = function (n) { yepnope.percent += maxBlur/steps; yepnope.loadCounter += 1; $(".loader").animate({minWidth: Math.round(yepnope.percent)+"px"}, { duration: 1000, progress: function () { var current = parseInt($(".loader").css("minWidth"), 10) * 100/maxBlur; $("title").html(Math.floor(current) + "% " + "digital trip"); if (isWebkitBlurSupported) { $body[0].style.webkitFilter = 'blur('+ (maxBlur - current)+ 'px)'; } if (!isWebkitBlurSupported && current % 20 === 0) { $cc.css({filter: "url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\"><filter id=\"blur-overlay\"><feGaussianBlur stdDeviation=\"" + (maxBlur - maxBlur/(steps+1)*n) + "\"/></filter></svg>#blur-overlay')"}); } if (current === 100) { $("title").html("digital trip"); if (!isWebkitBlurSupported && current % 20 === 0) $cc.css({filter: "url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\"><filter id=\"blur-overlay\"><feGaussianBlur stdDeviation=\"" + 0 + "\"/></filter></svg>#blur-overlay')"}); } }, complete: function () { if (n === steps) { DT.runApp(); } } }); }; yepnope([{ load: [ "js/vendor/three.min.js", "js/DT.min.js", "../socket.io/socket.io.js" ], callback: {} }]); } else { $('#nogame').css({display: 'table'}); }
      
      







ロード後、3つの制御方法のいずれかを選択してゲームを開始できます。



イベント


ゲーム内のオブジェクト間の相互作用は、標準およびカスタムイベントに基づいています。

イベントリスト
'blur' //フォーカスの喪失

'focus' //フォーカスの外観



'socketInitialized' // socket.ioを初期化する

'externalObjectLoaded' //外部モデルのロードを終了



'startGame' //ゲームを開始

'pauseGame' //一時停止

'resumeGame' //ゲームを再開

'gameOver' //ゲームの終了

'resetGame' //ゲームパラメータをリセット



'updatePath' //ゲーム空間内の位置を更新します(パイプ)

'update' //ゲームオブジェクトを更新する



'changeSpeed' //外部速度の変更

'showHelth' //ヘルスの変化を表示

'showInvulner' //脆弱性の変化を表示(シールド)

'showScore' //変更点を表示

'showFun' //減速モードの変更の表示(cat)

'changeHelth' //健康の変化

'bump' //オブジェクトとの衝突

'blink' //球を点滅

'hit' //石との衝突

'changeScore' //ポイントごとに変更

'catchBonus' //キャッチボーナス

'makeInvulner' //不死身モードの変更(シールド)

'makeFun' //減速モードを有効にする(cat)

'showBonuses' //キャッチされたボーナスの反映

'stopFun' //猫モードをオフにする



'paymentCheck' //支払いのクライアントチェックステータス

'paymentMessage' //支払いメッセージを受信

'transactionMessage' //トランザクションメッセージを受信

'checkup' //チェックを開始



document



要素でイベントが発生し、適切なハンドラーを呼び出します。次に例を示します。

 DT.$document.trigger('gameOver', {cause: 'death'}); DT.$document.on('gameOver', function (e, data) { if (data.cause === 'death') { DT.audio.sounds.gameover.play(); } });
      
      





イベント'blur'



'focus'



window



トリガーされ、ゲームでウィンドウのフォーカスが失われたときにサウンドをオフにし、一時停止をオンにします。

 DT.$window.on('blur', function() { if (DT.game.wasStarted && !DT.game.wasPaused && !DT.game.wasOver) { DT.$document.trigger('pauseGame', {}); } DT.setVolume(0); });
      
      





ゲームワールドの初期化


three.js



プロジェクトでは、すべてが標準です。シーン、カメラ、ゲームスペース、光源、背景が作成されます。



シーン

 DT.scene = new THREE.Scene();
      
      





カメラ

 DT.splineCamera = new THREE.PerspectiveCamera( 84, window.innerWidth / window.innerHeight, 0.01, 1000 );
      
      





再生スペースは、THREE.CurvesセットのTorusKnotカーブに沿ったパイプです

 var extrudePath = new THREE.Curves.TorusKnot(); DT.tube = new THREE.TubeGeometry(extrudePath, 100, 3, 8, true, true);
      
      





光源

 DT.lights = { light: new THREE.PointLight(0xffffff, 0.75, 100), directionalLight: new THREE.DirectionalLight(0xffffff, 0.5) };
      
      





シームレスな接続を実現するために、同じ色の境界線を持つ内面に引き伸ばされた写真を備えた、遊び場の周りの球形の背景。

背景

 var geomBG = new THREE.SphereGeometry(500, 32, 32), matBG = new THREE.MeshBasicMaterial({ map: THREE.ImageUtils.loadTexture('img/background5.jpg'), }), worldBG = new THREE.Mesh(geomBG, matBG); worldBG.material.side = THREE.BackSide;
      
      







クラス


ゲームにはいくつかの主要なクラスがあります:ゲーム( DT.Game



)、プレイヤー( DT.Player



)およびゲームオブジェクト( DT.GameObject



)。 これらには、イベントのトリガーに対応して対応するハンドラーによって呼び出される独自のメソッド(更新、ダンプなど)があります。 ゲームオブジェクトには、さまざまなパラメーター(速度、加速度)、定数( wasStarted



の最小距離とその状態に関する情報( wasStarted



wasPaused



)が含まれます。プレーヤーオブジェクトには、プレーヤーの現在の状態(スコア、ライフ、不死身状態)、およびプレーヤーモデルの状態に関する情報が含まれます(球体、球体の周りのリング(健康状態を示す輪郭)。)他のすべてのオブジェクトは、ゲームオブジェクトのサブクラス(粒子、プレーヤーの盾、ボーナス)です。



内部モデルと外部モデル


ゲームには2種類のモデルがあります:three.jsツールを使用して作成される内部(単純)モデル(球体、ヘルスインジケーター(リング/輪郭)、石とコイン)と外部(複雑な)モデル(球体の周りのボーナスとHTML5シールド)が読み込まれます対応するローダーによる.obj形式。

球体はプレーヤーのオブジェクトの一部であり、2つのオブジェクトを表します。他のオブジェクトとの衝突を計算するための物理的な球体(シーンには追加されません)

球体
 this.sphere = new THREE.Mesh(new THREE.SphereGeometry(0.5, 32, 32), new THREE.MeshPhongMaterial({}));
      
      





Fireworksパーティクルシステム用のエンジンベースのパーティクルシステム。







パーティクルシステム

 this.emitter = Fireworks.createEmitter({nParticles : 100}) .effectsStackBuilder() .spawnerSteadyRate(30) .position(Fireworks.createShapePoint(0, 0, 0)) .velocity(Fireworks.createShapePoint(0, 0, 0)) .lifeTime(0.2, 0.7) .renderToThreejsParticleSystem({ ... }) .back() .start();
      
      





ボーナスモデルは、同じ数の頂点を持つ2つのオブジェクトの形式で読み込まれます(変換用)。



モデルリスト
 DT.listOfModels = [{ name: 'bonusH1', scale: 0.1, rotaion: new THREE.Vector3(0, 0, 0), color: 0xff0000, }, { name: 'bonusI', scale: 0.02, rotaion: new THREE.Vector3(0, 0, 0), color: 0x606060, '5': 0xffffff, 'html': 0xffffff, 'orange': 0xD0671F, 'shield': 0xC35020, }, { name: 'bonusE1', scale: 0.75, rotaion: new THREE.Vector3(0, 0, 0), color: 0x606060, }, { name: 'bonusH2', scale: 0.1, rotaion: new THREE.Vector3(0, 0, 0), color: 0xff0000, }, { name: 'shield', scale: 0.16, rotaion: new THREE.Vector3(0, 0, 0), color: 0x606060, }, { name: 'bonusE2', scale: 0.75, rotaion: new THREE.Vector3(0, 0, 0), color: 0x606060, } ];
      
      







ブートローダー
 var manager = new THREE.LoadingManager(), loader = new THREE.OBJLoader(manager); manager.onProgress = function (item, loaded, total) { console.info('loaded item', loaded, 'of', total, '('+item+')'); }; DT.listOfModels.forEach(function (el, i, a) { loader.load('objects/' + el.name + '.obj', function ( object ) { object.traverse( function ( child ) { var color = el[child.name] || el.color; child.material = new THREE.MeshPhongMaterial({ color: color, shading: THREE.SmoothShading, emissive: new THREE.Color(color).multiplyScalar(0.5), shininess: 100, }); }); if (i === 1) { a[i].object = object } else { a[i].object = object.children[0]; } DT.$document.trigger('externalObjectLoaded', {index: i}); }); });
      
      







ロード後、外部モデルはリンクDT.listOfModels[index].object



利用可能になり、ボーナスコンストラクターで使用されます。



変換(変換)と後処理


ゲームにはいくつかの変換があります。ゲームの終了時のヘルスインジケータ、ボーナス、グリッチエフェクト(または壊れたテレビのエフェクト)です。



健康指標とボーナス変換はmorphTargetsに基づいています







オブジェクトを作成するとき、標準状態はこのオブジェクトのジオメトリに保存されます。 残りの状態は、特別なジオメトリプロパティmorphTargets



保存されます。 オブジェクトの現在の状態は、オブジェクトのmorphTargetInfluences



レベルによって決定されます。



球の周囲のヘルスインジケータ(リング/輪郭)は2つのオブジェクトで、それぞれのジオメトリは180の頂点(内側と外側に60)で構成されています。



リング/輪郭は、円、5、4、および三角形で、頂点の数は常に180のままです。

各状態の頂点の数が同じであり、それらの座標ベクトルが(目的の変換に従って)「正しく」変化することが重要です。そうでない場合、変換は正しく機能しないか、まったく機能しません。



このために、ヘルスインジケータのジオメトリ(リング/輪郭)を作成する特別な関数が作成されました。



ヘルスインジケータのジオメトリ
 DT.createGeometry = function (circumradius) { var geometry = new THREE.Geometry(), x, innerradius = circumradius * 0.97, n = 60; function setMainVert (rad, numb) { var vert = []; for (var i = 0; i < numb; i++) { var vec3 = new THREE.Vector3( rad * Math.sin((Math.PI / numb) + (i * ((2 * Math.PI)/ numb))), rad * Math.cos((Math.PI / numb) + (i * ((2 * Math.PI)/ numb))), 0 ); vert.push(vec3); } return vert; } function fillVert (vert) { var nFilled, nUnfilled, result = []; nFilled = vert.length; nUnfilled = n/nFilled; vert.forEach(function (el, i, arr) { var nextInd = i === arr.length - 1 ? 0 : i + 1; var vec = el.clone().sub(arr[nextInd]); for (var j = 0; j < nUnfilled; j++) { result.push(vec.clone().multiplyScalar(1/nUnfilled).add(el)); } }); return result; } // set morph targets [60, 5, 4, 3, 2].forEach(function (el, i, arr) { var vert, vertOuter, vertInner; vertOuter = fillVert(setMainVert(circumradius, el).slice(0)).slice(0); vertInner = fillVert(setMainVert(innerradius, el).slice(0)).slice(0); vert = vertOuter.concat(vertInner); geometry.morphTargets.push({name: 'vert'+i, vertices: vert}); if (i === 0) { geometry.vertices = vert.slice(0); } }); // Generate the faces of the n-gon. for (x = 0; x < n; x++) { var next = x === n - 1 ? 0 : x + 1; geometry.faces.push(new THREE.Face3(x, next, x + n)); geometry.faces.push(new THREE.Face3(x + n, next, next + n)); } return geometry; };
      
      







同じ理由で、ボーナスモデルは2つの.objオブジェクトの形式でインポートされ、エディターで特定の方法で事前に変更されます(予想される変換(変換)アニメーションに必要です。このために3ds MaxとBlenderを使用しました。







リップモデルには興味深い点が1つあります。 通常の状態では、唇はアニメーション化されます(分割されて閉じられます)。 この場合、2組の頂点(開いた唇と閉じた唇)からの頂点の影響に単純に変化があります。 three.jsのドキュメントによれば、各頂点セットのmorphTargetInfluence値は[0、1]の範囲内にある必要があります。 この場合、1より大きい力を使用すると、「ハイパーインフルエンス」の影響が発生します。 そのため、たとえば、値5のmorphTargetInfluenceを猫モデルの頂点セットに適用すると、モデルは「裏返し」になります。 唇モデルの場合、「口を開けている」ように見えます。

この動作は、追加の外部モデルのインポートを回避する「リップ」ボーナスの吸収の効果に基づいています。

ゲームの終わりをアニメーション化するために使用されるグリッチ効果(または壊れたテレビの効果)は、 シェーダーを使用した後処理の例です。







エフェクトを作成

コード
 DT.effectComposer = new THREE.EffectComposer( DT.renderer ); DT.effectComposer.addPass( new THREE.RenderPass( DT.scene, DT.splineCamera ) ); DT.effectComposer.on = false; var badTVParams = { mute:true, show: true, distortion: 3.0, distortion2: 1.0, speed: 0.3, rollSpeed: 0.1 } var badTVPass = new THREE.ShaderPass( THREE.BadTVShader ); badTVPass.on = false; badTVPass.renderToScreen = true; DT.effectComposer.addPass(badTVPass);
      
      







フレームごとにレンダリングします

コード
 DT.$document.on('update', function (e, data) { if (DT.effectComposer.on) { badTVPass.uniforms[ "distortion" ].value = badTVParams.distortion; badTVPass.uniforms[ "distortion2" ].value = badTVParams.distortion2; badTVPass.uniforms[ "speed" ].value = badTVParams.speed; badTVPass.uniforms[ "rollSpeed" ].value = badTVParams.rollSpeed; DT.effectComposer.render(); badTVParams.distortion+=0.15; badTVParams.distortion2+=0.05; badTVParams.speed+=0.015; badTVParams.rollSpeed+=0.005; }; });
      
      







イベント'gameOver'



後に効果が有効になります

コード
 DT.$document.on('gameOver', function (e, data) { DT.effectComposer.on = true; });
      
      







対応するイベントが発生するとリセットされます

コード
 DT.$document.on('resetGame', function (e, data) { DT.effectComposer.on = false; badTVParams = { distortion: 3.0, distortion2: 1.0, speed: 0.3, rollSpeed: 0.1 } });
      
      







後処理を使用すると、フレームのレンダリング時間が大幅に増加するため、ゲームの終了時に後処理が短時間使用されます。



音楽の視覚化


音楽は、演奏空間内の粒子(ほこり)の脈動によって視覚化されます。



このために、望ましい可視化頻度が決定されました。 目的の周波数の音の存在レベル( DT.audio.valueAudio



)は、現在、視覚化バッファーで次のように定義されています

コード
 var getFrequencyValue = function(frequency, bufferIndex) { if (!DT.isAudioCtxSupp) return; var nyquist = DT.audio.context.sampleRate/2, index = Math.round(frequency/nyquist * freqDomain[bufferIndex].length); return freqDomain[bufferIndex][index]; }; var visualize = function(index) { if (!DT.isAudioCtxSupp) return; freqDomain[index] = new Uint8Array(analysers[index].frequencyBinCount); analysers[index].getByteFrequencyData(freqDomain[index]); DT.audio.valueAudio = getFrequencyValue(DT.audio.frequency[index], index); };
      
      





DT.audio.valueAudio



の値は、パーティクルの透明度の状態を更新するために使用されます。

コード
 DT.$document.on('update', function (e, data) { DT.dust.updateMaterial({ isFun: DT.player.isFun, valueAudio: DT.audio.valueAudio, color: DT.player.sphere.material.color }); });
      
      





updateMaterial



メソッドupdateMaterial





コード
 DT.Dust.prototype.updateMaterial = function (options) { if (!this.material.visible) { this.material.visible = true; } this.material.color = options.isFun ? options.color : new THREE.Color().setRGB(1,0,0); this.material.opacity = 0.5 + options.valueAudio/255; return this; };
      
      





WebAudio APIの詳細については、 こちらをご覧ください



ファビコンアニメーション


ファビコンのデジタル旅行は、デフォルトでは猫の白黒画像です。

スローモード(猫モード)では、アイコンの色が変わり始めます。

Firefoxであなたが置くことができる場合

 <link rel="icon" type="image/gif" href="fav.gif">
      
      





この方法はChromeでは機能しません。 Chromeでは、動的なファビコンpngスプーフィングが使用されました。

一般的な実装は次のようになります。

コード
 var favicon = document.getElementsByTagName('link')[1], giffav = document.createElement('link'), head = document.getElementsByTagName('head')[0], isChrome = navigator.userAgent.indexOf('Chrome') !== -1; giffav.setAttribute('rel', 'icon'); giffav.setAttribute('type', 'image/gif'); giffav.setAttribute('href', 'img/fav.gif'); DT.$document.on('update', function (e, data) { if (isChrome && DT.player.isFun && DT.animate.id % 10 === 0) favicon.setAttribute('href', 'img/' + (DT.animate.id % 18 + 1) + '.png'); }); DT.$document.on('showFun', function (e, data) { if (!data.isFun) { if (isChrome) { favicon.setAttribute('href', 'img/0.png'); } else { $(giffav).remove(); head.appendChild(favicon); } } else { if (!isChrome) { $(favicon).remove(); head.appendChild(giffav); } } });
      
      





'update'



'showFun'



オブジェクトの状態を更新するイベント、 'showFun'



シールモードの開始のイベント(減速)、 DT.player.isFun



シールモードの状態、 DT.animate.id



現在のフレーム(フレーム)の番号。 ファビコンには19のオプションがありますが、残念ながら、Safariにはファビコンアニメーションはありません。



モバイルコントローラー



ゲームにはモバイルデバイスを制御する機能があります。

モバイルデバイスをコントローラーとして接続するには、リンクをたどるか、 プラグインによって生成されたQRコードを使用する必要があります



制御は、ジャイロスコープとイベント'deviceOrientation'



を使用して実行されます。 ジャイロスコープがない場合、またはジャイロスコープにアクセスできない場合、制御ボタンを押すことによる制御が使用されます。



フォールバックとハンドラー:

コード
 // Technique from Juriy Zaytsev // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ var eventSupported = function( eventName ) { var el = document.createElement("div"); eventName = "on" + eventName; var isSupported = (eventName in el); if ( !isSupported ) { el.setAttribute(eventName, "return;"); isSupported = typeof el[eventName] === "function"; } el = null; return isSupported; }; // device orientation function orientationTest (event) { if (!turned && event.gamma) turned = true; window.removeEventListener('deviceorientation', orientationTest, false); window.removeEventListener('MozOrientation', orientationTest, false); } window.addEventListener('deviceorientation', orientationTest, false); window.addEventListener('MozOrientation', orientationTest, false); setTimeout(function () { if (!turned) { $("#btnLeft").on('touchstart',function () { socket.emit("click", {"click":"toTheLeft"}); }); $("#btnRight").on('touchstart',function () { socket.emit("click", {"click":"toTheRight"}); }); $status.html("push buttons to control"); } else { $status.html("tilt your device to control"); } if (!eventSupported('touchstart')) { $status.html("sorry your device not supported"); } }, 1000);
      
      





'deviceOrientation'



サポートの確認は、 'deviceOrientation'



をサポートするデバイス(たとえばHTC One V)がありますが、イベント自体は発生しないため、 eventSupported



とはeventSupported



setTimeout



を介して実装されます。 実際、ある時間間隔の間、イベントが発生するのを待っています(間違いなく発生するはずです)。発生しない場合、イベントはサポートされていないと判断します。 このようなチェックは実際にはハックです。



一部の電話(Windows Phone搭載のHTCなど)では、標準ブラウザー(モバイルIE)は'touchstart'



イベントをサポートしていませんが、より高いレベルの'click'



イベントをサポートしてい'click'



'click'



イベントを使用した場合の応答時間(300ミリ秒)は'touchstart'



の応答時間よりもはるかに長く、そのようなデバイスを使用した監視に必要な応答レベルを提供できないため、このようなデバイスのサポートを拒否しました。



ところで、HDDを搭載した一部のMacbook Proモデルのユーザーは、ジャイロスコープを備えているため、このモードでラップトップを使用できます。



Android 4.0以上を搭載したデバイスのユーザーには、小さなボーナスがあります。石に遭遇した場合(振動100ミリ秒)またはコインを拾った場合(振動10ミリ秒)のコントローラーの応答です。 これを行うには、Vibration APIを使用します(最新の標準ブラウザー、モバイルChromeまたはFirefoxが必要です)。 こちらからVibration APIの詳細をご覧ください



デバイスの傾きを制御する場合、ユーザーは長時間画面に触れないことがあり、デバイスがロックされ、画面が空白になり、ブラウザーがジャイロスコープからのデータ送信を停止します。 この動作を防ぐために、オーディオループであるハックが使用されました。10秒間のサイレントトラックは、周期的に再生され、ボタンが押されると開始、再開、一時停止します。



 <audio id="audioloop" src="../sounds/loop.mp3" onended="this.play();" autobuffer></audio>
      
      





 $('#btnSphere').on('touchstart',function () { socket.emit('click', {'click':'pause'}); $('#audioloop').trigger('play'); });
      
      





同時に、Android OSを搭載したデバイスでは、音声ループが1秒になり、iOSを搭載したデバイスでは、より長いトラックが必要になります。 iOSでは、Safariブラウザーは無限にトラックを再生せず、サイクル数は約100であるため、10秒のトラック長が選択されました。



ウェブカメラ制御



WebcamコントロールはgetUserMedia()



メソッドに基づいています。

Webカメラ制御の例をいくつか見てきました。 1つのオプションは、この例のように仮想キーを押すことです







正確性が不十分であることが判明したため、拒否しました。



別のオプションは、ヘッドアングルとheadtrackr.jsライブラリを使用することです。 それはより興味深いことが判明し、首を伸ばして緊張を和らげるのに役立ちましたが、角度は常に正しく決定されていませんでした。 その結果、Webカメラを使用して制御するために、頭の位置と画面の中央に対する相対的な動きが使用されます(headtrackr.jsも使用)。



この制御方法は、キーボードやモバイルよりも1桁複雑であるため、Webカメラ制御モードでのゲーム速度が低下します。



バックエンド



ゲームサーバーはnode.jsで実行されます。 使用されるモジュールは、express、socket.io、mongoose、node-dogecoin、およびhookshotです。



ここではすべてが非常に簡単です。socket.ioはトランスポートを提供し、expressはルートと静的を担当し、mongooseはクライアントをデータベースに保存します。 フックショットは、VPSへの変更をすばやく展開するために使用されます。



 app.use('/webhook', hookshot('refs/heads/master', 'git pull'));
      
      





バックエンドで最も興味深いのは、同じサーバーにデプロイされたdogecoinデーモンとの相互作用です。 これは本格的なdogecoinウォレットであり、node-dogecoinモジュールを使用して次のように相互作用が行われます。



 dogecoin.exec('getbalance', function(err, balance) { console.log(err, balance); });
      
      





さらに、サーバーはクライアントの不正をチェックします。 ここでは、クライアントがダイヤルしたコインの数がチェックされ、このセッション中に収集できるコインの最大数と比較されます。

コード
 var checkCoins = function (timeStart, timeEnd, coinsCollect) { var time = (timeEnd - timeStart)/1000, maxCoins = calcMaxCoins(time); // if client recieve more coins than it may return coinsCollect <= maxCoins; }; var calcMaxCoins = function (time) { var speedStart = 1/60, acceleration = 1/2500, maxPath = 0, maxCoins = 0, t = 0.25, // coins position in the tube dt = 0.004, // coins position offset n = 10; // number of coins in a row maxPath = (speedStart + acceleration * Math.sqrt(time * 60)) * time; maxCoins = Math.floor(maxPath / (t + dt * (n - 1)) * n)/10; console.log('time:' + time, 'maxCoins:' + maxCoins, 'maxPath:' + maxPath); return maxCoins; };
      
      





また、1つのUID(Cookie)と1つのIPからの2つの最も近いゲーム間の時間を使用して、1つのIPからの支払い回数の検証を実装します。

コード
 var checkClient = function (clients, currentClient) { console.log("Handle clients from Array[" + clients.length + "]") var IPpaymentsCounter = 0, UIDpaymentsCounter = 0, IPtimeCounter = 60 * 1000, checkup = null; clients.forEach(function(client, i) { if (client.clientIp === currentClient.clientIp && client.paymentRequest) { IPpaymentsCounter += client.paymentRequest; if (currentClient.timeEnd && currentClient.cientId !== client.cientId) { Math.min(IPtimeCounter, currentClient.timeEnd - client.timeEnd); } } if (client.cookieUID === currentClient.cookieUID && client.paymentRequest) { UIDpaymentsCounter += client.paymentRequest; } // console.log("handle client #" + i); }); console.log('IPtimeCounter', IPtimeCounter); if (currentClient.checkup === false || currentClient.maxCoinsCheck === false || IPpaymentsCounter > 1000 || UIDpaymentsCounter > 100 || IPtimeCounter < 20 * 1000) { checkup = false; } else { checkup = true; } return checkup; };
      
      





これは、便宜の原則に基づく単純な防御です。



おわりに



この記事では、開発中に出会った最も興味深い点をリストしました。この情報がお役に立てば幸いです。



すべての開発はGitHubで実行されましたコードはこちらにあります



リンク:githubプロジェクトゲームの説明ゲームの



使用ツールとライブラリ:




All Articles