バーチャルパッドをぐりぐりして遊べるスマホ向けブラウザゲームを作りたい!!!
という激レアな人向けにJavaScriptのバーチャルパッドのサンプルを公開します。
バーチャルパッドの部分はHTMLとJavaScriptのみで作っているので他のライブラリでもほぼそのまま使えると思います。
サンプルのバーチャルパッドはスマホでしか表示されません。スマホで確認するか、デベロッパーツールでスマホ表示に切り替えて確認してください。
サンプルのバーチャルパッドは縦向きだとこんな感じで画面下に表示します。

画面を横に向けると左右にボタンが表示されます。

あと、このサンプルは一つ前のこちらの記事で紹介したサンプルに追加する形になっています。
HTMLタグでバーチャルパッドを作る
今回紹介するバーチャルパッドはHTMLで作ります。
ゲームはcanvasにJavaScriptで作りますがバーチャルパッドはHTMLタグで別に作ります。
理由としてはJavaScriptではマルチタッチの処理が面倒だからです。
さらに私が使っているPixiJSではiPhoneがマルチタッチに対応してませんでした。
HTMLタグではマルチタッチについて特に何か記述しなくても対応しています。
調べてはいませんがPICO-8やゲームアツマールも同じような作り方だと思います。
cssファイルを作る
HTMLで作るのでデザインについてはcssを使います。
JavaScriptで変更する部分もありますが基本的な部分はcssファイル(style.css)に記述しておいてJavaScriptでidやクラス名を当てて適用させます。
cssがあまり良くわからない人もいるかもしれませんが慣れてください。
私も勉強したというよりはいじくりまわしながら調べて慣れただけって感じです。
バーチャルパッドのプログラム
バーチャルパッドの部分のプログラムだけ載せておきます。
class Vpad {
constructor(input){
this.input = input;//InputManagerのinput
this.resizePad();
// リサイズイベントの登録
window.addEventListener('resize', ()=>{this.resizePad();});
}
//画面サイズが変わるたびにvpadも作り変える
resizePad(){
let styleDisplay = "block";//ゲームパッド対策
//すでにあれば一度削除する
if(this.pad != undefined){
styleDisplay = this.pad.style.display;//ゲームパッド対策
while(this.pad.firstChild){
this.pad.removeChild(this.pad.firstChild);
}
this.pad.parentNode.removeChild(this.pad);
}
const screen = document.getElementById("game-screen");//ゲーム画面
//HTMLのdivでvpad作成
const pad = document.createElement('div');
document.body.appendChild(pad);
this.pad = pad;
pad.id = "pad";
pad.style.width = screen.style.width;
pad.style.display = styleDisplay;
//タッチで拡大とか起こるのを防ぐ
pad.addEventListener("touchstart", (e) => {
e.preventDefault();
});
pad.addEventListener("touchmove", (e) => {
e.preventDefault();
});
//横長の場合位置変更
if(window.innerWidth > window.innerHeight){
pad.style.width = `${window.innerWidth}px`;
pad.style.position = "absolute";//画面の上にかぶせるため
pad.style.backgroundColor = "transparent";//透明
pad.style.bottom = "0";//下に固定
}
const height = Number(screen.style.height.split('px')[0]) * 0.5;//ゲーム画面の半分の高さをゲームパッドの高さに
pad.style.height = `${height}px`;
//方向キー作成
new DirKey(this.pad, this.input, height);
//Aボタン作成
let style = {
width: `${height * 0.5}px`,
height: `${height * 0.5}px`,
right: `${height * 0.5}px`,
top: `${height * 0.4}px`,
borderRadius: "50%"
}
new ActBtn(this.pad, this.input, "A", "A", style);
//Bボタン作成
style = {
width: `${height * 0.5}px`,
height: `${height * 0.5}px`,
right: `${height * 0.05}px`,
top: `${height * 0.1}px`,
borderRadius: "50%"
}
new ActBtn(this.pad, this.input, "B", "B", style);
//STARTボタン作成
style = {
width: `${height * 0.3}px`,
height: `${height * 0.15}px`,
right: `${height * 0.65}px`,
top: `${height * 0.05}px`,
borderRadius: `${height * 0.15 * 0.5}px`
}
new ActBtn(this.pad, this.input, "Start", "START", style);
}
}
//方向キークラス
class DirKey {
constructor(parent, input, padHeight) {
this.isTouching = false;
this.originX = 0;
this.originY = 0;
//HTMLのdivでキーのエリアを作成
const div = document.createElement('div');
parent.appendChild(div);
div.className = "dir-key";
div.style.width = div.style.height = `${padHeight * 0.8}px`;
div.style.left = `${padHeight * 0.05}px`;
div.style.top = `${padHeight * 0.1}px`;
this.maxRadius = padHeight * 0.15;//中心移動させる半径
this.emptySpace = padHeight * 0.05;//あそび
//十字キーのボタン(張りぼて。タッチイベントはない)
const up = document.createElement('div');
up.className = "dir up";
div.appendChild(up);
const left = document.createElement('div');
left.className = "dir left";
div.appendChild(left);
const right = document.createElement('div');
right.className = "dir right";
div.appendChild(right);
const down = document.createElement('div');
down.className = "dir down";
div.appendChild(down);
const mid = document.createElement('div');
mid.className = "dir mid";
div.appendChild(mid);
const circle = document.createElement('div');
circle.className = "circle";
mid.appendChild(circle);
//タッチイベント
div.addEventListener("touchstart", (e) => {
e.preventDefault();
this.isTouching = true;
//タッチした位置を原点にする
this.originX = e.targetTouches[0].clientX;
this.originY = e.targetTouches[0].clientY;
});
div.addEventListener("touchmove", (e) => {
e.preventDefault();
if(!this.isTouching) return;
dirReset();//からなず一度リセット
//タッチ位置を取得
const posX = e.targetTouches[0].clientX;
const posY = e.targetTouches[0].clientY;
//原点からの移動量を計算
let vecY = posY - this.originY;
let vecX = posX - this.originX;
let vec = Math.sqrt(vecX * vecX + vecY * vecY);
if(vec < this.emptySpace)return;//移動が少ない時は反応しない(遊び)
const rad = Math.atan2(posY - this.originY, posX - this.originX);
const y = Math.sin(rad);
const x = Math.cos(rad);
//移動幅が大きいときは中心を移動させる
if(vec > this.maxRadius){
this.originX = posX - x * this.maxRadius;
this.originY = posY - y * this.maxRadius;
}
const abs_x = Math.abs(x);
const abs_y = Math.abs(y);
if(abs_x > abs_y){//xの方が大きい場合左右移動となる
if(x < 0){//マイナスであれば左
input.keys.Left = true;
}else{
input.keys.Right = true;
}
if(abs_x <= abs_y * 2){//2yがxより大きい場合斜め入力と判断
if(y < 0){//マイナスであれば上
input.keys.Up = true;
}else{
input.keys.Down = true;
}
}
}else{//yの方が大きい場合上下移動となる
if(y < 0){//マイナスであれば上
input.keys.Up = true;
}else{
input.keys.Down = true;
}
if(abs_y <= abs_x * 2){//2xがyより大きい場合斜め入力と判断
if(x < 0){//マイナスであれば左
input.keys.Left = true;
}else{
input.keys.Right = true;
}
}
}
});
div.addEventListener("touchend", (e) => {
dirReset();
});
const dirReset = () => {
input.keys.Right = input.keys.Left = input.keys.Up = input.keys.Down = false;
}
}
}
//アクションボタンクラス
class ActBtn {
constructor(parent, input, key, name, style) {
//HTMLのdivでボタンを作成
const div = document.createElement('div');
div.className = "button";
parent.appendChild(div);
div.style.width = style.width;
div.style.height = style.height;
div.style.right = style.right;
div.style.top = style.top;
div.style.borderRadius = style.borderRadius;
//ボタン名を表示
const p = document.createElement('p');
p.innerHTML = name;
div.appendChild(p);
//タッチスタート
div.addEventListener("touchstart", (e) => {
e.preventDefault();
input.keys[key] = true;
});
//タッチエンド
div.addEventListener("touchend", (e) => {
input.keys[key] = false;
});
}
}
バーチャルパッドのクラスを作る
サンプルではバーチャルパッド用のクラスVpadを作ります。
リサイズに対応する
Vpadクラスのコンストラクタには
window.addEventListener('resize', ()=>{this.resizePad();});
という部分があります。
これは画面のサイズが変わったとき、特にスマホで画面を縦にしたり横にしたりした場合に起こる「resize」というイベントです。
バーチャルパッドが縦画面と横画面でデザインが違うのでこのイベント時に作り変えるようになっています。
resizePadメソッド
ここでバーチャルパッドを作っています。
すでに作っていた場合は一度削除して作り直しています。
大雑把に説明するとHTMLタグのDIVを作成してそれにCSSを適用させてボタンを作っています。
ややこしいですがサンプルプログラムにコメントをたくさん入れているのでなんとなくわかると思います。
ActBtnクラス
Aボタン、Bボタン、スタートボタンはActBtnクラスというので作っています。
これはシンプルなHTMLのDIVタグです。
タッチイベントでキー入力の時に作ったinputManagerのキーのフラグに真偽値を入れます。
後はinputManagerで処理されます。
DirKeyクラスについて
Dirkeyクラスが十字キーのクラスです。
これもHTMLのDIVタグで作っていますが、見えている十字キーは張りぼてで、その下にあるDIVタグでタッチイベントを処理しています。
これもActBtnと同じようにイベントでフラグだけ操作して処理はinputManagerでしています。
十字キーの方向の判断について
十字キーの入力方向を判断するのはちょっとややこしいです。
まず、入力方向をグラフで考えます。

タッチした位置の座標が原点から見てどこにあるかで方向が出せます。
この時X、Yの絶対値で考えると上の図のようになります。
これでYの方が大きければ上、Xの方が大きければ右が入力されたと判断します(上の図で考えた場合)。
後は各座標の+-で上下左右を判断します。
これをもう一段細かく条件(Y < 2Xなど)を追加すると8方向にできます。
ただ、十字キーの入力で原点を固定していると操作しているときに指の位置がズレてうまく操作できないことが多いです。
そのためタッチした位置を原点としてそこから指を動かした長さで方向を出しています。
そして一定以上指を動かした場合は原点もズラすようにしています。
ただし、これには欠点があって最初に触れた場所が原点となるのでそこから指を動かさなければ方向を入力できないため「ちょんちょん」って感じの操作ができません。
そういう操作の場合は原点を固定しておくやり方が向いてます。
この辺はゲームによって変えると良いと思います。
スマホ時のみバーチャルパッドを表示
バーチャルパッドはPCで遊ぶ際にはまったく必要ないので、ゲームが何で表示されているか判断して必要なら表示するようにします。
InputManager(main.jsの195行目くらい)に条件が書かれています。
if (navigator.userAgent.match(/iPhone|iPad|Android/)) {
this.vpad = new Vpad(this.input);
}
正直、この辺は知識が無い&Apple製品持ってないのでうまくいっているかどうかわかりません。
問題あったら教えてください(^^;)
まとめ
前回の記事で作ったキー入力のプログラムに追加する形なので処理自体は少なくできました。
ただ、CSSでデザインしたり、十字キーの入力をうまく作るのはなかなか難しいです。
cssではbackground-imageとか使うとボタンの背景画像とかも指定できるので自分好みにバリバリに作りたい人はやってみてください。
