AOYAMA Koji's PROGRAMMING BLOG

JavaScriptでスワイプ操作の情報を取得するプログラム

2020/06/13

 先日ですが「FINAL FANTASY 7 REMAKE」をクリアしました! 思い出は美化されるものだと思いますが、オリジナルのFF7の思い出に負けない、むしろ上回る良さでしたね。 続きがとてもとてもしみです!
 オリジナルのFF7が出たのは私がまだハドソンに勤めていた時期でした。 PlayStationはここまでできるんだ!という思いとともに、スクウェアはすごい会社だなという衝撃を受けました。
 結局その数年後に私はスクウェアに転職し、しばらくして「ファイナルファンタジーXI」というオンラインゲームの基盤システム「PlayOnline」を担当します。 FFXIそのものを作っていたわけではないのですが、オンラインゲームの開発・運営に関して、とても貴重な経験をさせてもらいました。
 その後スクウェアエニックス合併してスクウェア・エニックスになります。 当時のRPGの双璧である、ドラゴンクエストを作っていたエニックスと、ファイナルファンタジーを作っていたスクウェアの合併という、 業界的にはきな変化だったと思います。
 その流れと、私自身はそれまでの経験をかす形で、エニックスのIPであるドラゴンクエストを、 スクウェアの技術を取り入れてオンライン化した「ドラゴンクエストX」のテクニカルディレクター、すなわち技術責任者を担当させてもらうことになりました。 そして私はその後、現職である「ドラゴンクエストX」のプロデューサーに就くという流れです。
 つまり、今の仕事ができているのはFF7のおかげとも言えるほど、FF7は思い出深いゲームなのです。 久しぶりにその世界に浸れて幸せでした!
 思い入れが強くて前置きがだいぶ長くなってしまいました(汗
 今回は操作に応じてプレイヤーキャラクターを動かすプログラミングに用いた、 Webブラウザー上でスワイプ操作した情報を、JavaScript取得するためのプログラムの、少々本格的な解説をしたいと思います。


スワイプの動作は、タップ→指を移動、だよね


 最初にスワイプ動作を習得する方針を立ててみます。
 操作としては図の通り、まずタップして、その状態で指を移動する、という形になるでしょう。 タップされた位置を起点に、移動先位置と差分を取ることで、移動量が取れます。 それによって方向も計算できるでしょう。
 その前にプログラムとしては、どのレイヤーというかdivエレメントが、タップの対象になるのかを指定する必要があります。

タップ対象のレイヤーオブジェクトの取得と保持


 まずHTMLはこんな感じでdivエレメントを定義します。 ゲームスクリーンに見立てたものになります。
<div 
 id="gamescreen" 
 style="width:320px; height:180px;">
ゲームスクリーン
</div>

 そしてJavaScriptとしては以下で準備します。 具体的にはoという変数に対象のレイヤーオブジェクトを入れておきます。
// target object
o = document.getElementById( 
  "gamescreen" 
);

document.getElementByIdによりid指定でエレメントを取得


 document.getElementByIdは比較的よく使うJavaScriptの関数です。 idで指定されたエレメントのオブジェクトを取得します。 HTMLのエレメントの中で「id=」で指定されたものなら、おそらくすべてそのエレメントのオブジェクトが取得可能だと思います。

タップ位置を得よう



 タップ位置を取得して(startX,startY)に代入したいと思います。 プログラムは以下の通り。
//********************************
// touch : start
//********************************
o.addEventListener( "touchstart" ,
  function(e) {
    e.preventDefault();
    tO = e.changedTouches[0];
    startX = movingX = tO.clientX;
    startY = movingY = tO.clientY;
    isMoving = true;
  }
);

 以下で順番に解説します。

oは前述のo


 最初の o は前述のエレメントのオブジェクトです。

addEventListener("touchstart",関数) でタップ時の処理を指定できる


 addEventListener( "touchstart" , 関数 ) の形式で、タップ時のイベントを処理する関数を指定することができます。 今回の例では直接functionを用いて処理を指定しています。
o.addEventListener( "touchstart" , 
  function(e) {
    (処理)
  }
);

 関数を別途定義して、ここでは名前を渡しても大丈夫です。 いずれにしてもこの書き方で、タップ時の処理を指定できます。

e.preventDefaultで他の処理を抑制する


 e.preventDefault()の e は、イベントを処理する関数に与えられる引数です。 それを用いてpreventDefaultを指定することで、そのイベント、今回はタップの処理について、元々定義されていた処理を実施しないようにできます。

タップが一箇所ならe.changedTouches[0]で情報が取得できる


 タップが一箇所ならe.changedTouches[0]にて、touch情報が入ったオブジェクトを取得できます。 それを以下のように tO に入れておきます。
  tO = e.changedTouches[0];

 ちなみにマルチタップされていれば[1]や[2]にも情報が入っているでしょうが、 今回のプログラムではそれらは無視しています。

clientXYで座標を取得してstartXYへ


 座標はtouch情報のオブジェクトtOのプロパティclientXとclientYで取得しています。 それを(startX,startY)に入れます。
  startX = movingX = tO.clientX;
  startY = movingY = tO.clientY;

 プログラムがこのようになっているのは、 指の移動で使用する座標(movingX,movingY)も、この時点で(startX,startY)に合わせておくためです。

isMovingというフラグで状態管理


 isMovingで、現在スワイプ中かどうかを管理しています。

指の移動位置からスワイプ情報を得よう



 次はスワイプ操作の指の座標を得て(movingX,movingY)として、(startX,startY)との差分で、そのベクトルを得ます。 プログラムは以下の通りです。
//********************************
// touch : move
//********************************
o.addEventListener( "touchmove"  ,
  function(e) {
    if ( isMoving ) {
      e.preventDefault();
      tO = e.changedTouches[0];
      movingX = tO.clientX;
      movingY = tO.clientY;
      setDxyByMovingXY();
    }
  }
);

addEventListener("touchmove",関数)で指の移動時の処理を定義


タップ後に指を移動したイベントの処理は o.addEventListener( "touchmove" , 関数 ) で指定できます。 前述の通り関数名無しで処理を直接記述可能です。

isMoving中に処理を限定


 予想外のときに処理が来ることを考えて、isMoving が true のときに処理を限定しています。

preventDefaultしないとスクロールしちゃう


 e.preventDefault()にて、デフォルトの処理を抑制します。 "touchstart"のイベントではこれが無い弊害は無いかも知れませんが、 "touchmove"でこれをしないと、Webブラウザーのページをスクロールしてしまうので、ここは必須です。

e.changedTouches[0]で情報取得


 e.changedTouches[0]でtouch情報を取得します。 前述とまったく同じなので省略しますね。

clientXYで座標を取得してmovingXYへ


 touch情報が入ったオブジェクト tO のプロパティ clientXとclinetYを使用して、 座標(movingX,movingY)を設定します。
    movingX = tO.clientX;
    movingY = tO.clientY;

setDxyByMovingXYで差分計算してdXYへ


 ここまで求められた情報をもとに差分を計算し、(dX,dY)に設定します。 単純に言えば以下のとおりです。
dX = movingX - startX;
dY = movingY - startY;

 ただ色々と扱いやすいように工夫をするために setDxyByMovingXY という別関数に処理を分けています。

setDxyByMovingXYで座標の差分を計算、正規化をしてdXYへ


 関数setDxyByMovingXYの定義は以下の通りです。 差分習得するだけのはずが随分いプログラムになっていますね。
//********************************
// set (dX,dY) with normalize
//********************************
function setDxyByMovingXY() {
  //d
  dx = movingX - startX;
  dy = movingY - startY;
  //len
  len = Math.sqrt( dx*dx + dy*dy );
  //too short
  if ( len < 16 ) {
    dX = 0;
    dY = 0;
    return;
  }
  // normalize and set
  if ( len < 256 ) len = 256;
  dX = dx / len;
  dY = dy / len;
}

 この関数により(dX,dY)が設定されます。

まずは差分取得してdxyに


 まず(dx,dy)=(movingX,movingY)-(startX,startY)の差分を計算しておきます。
  dx = movingX - startX;
  dy = movingY - startY;

移動距離を取得してlenに


 次に移動距離を取得します。 二次元の距離の公式を使用します。ピタゴラスの定理と言った方が良いでしょうか。
  len = Math.sqrt( dx*dx + dy*dy );

 これは数式で書くと \(len = \sqrt{dx^2+dy^2} \) です。

ほとんど移動していなければ無視


 タップして若干動いただけでプレイヤーが移動を開始してしまうと操作のしづらいゲームになってしまうので、 移動量が一定以下の場合は無視するようにしています。 これを遊びと呼びます。 自動車のハンドルなどにもありますよね。
  if ( len < 16 ) {
    dX = 0;
    dY = 0;
    return;
  }

 (dX,dY)がこの関数で設定すべき変数で、それを0にすることで、移動が無い状態にしています。

長さの最大値を1にするように正規化


 (dX,dY)の長さは、0から1と定義します。 また差分(dx,dy)の長さの最大は256とします。 それで正規化します。
 まずは数式で表してみます。 lenが256以上なら \( \vec{(dX,dY)} = \frac{\vec{(dx,dy)}}{len} \)、 256未満なら \( \vec{(dX,dY)} = \frac{\vec{(dx,dy)}}{256} \) を計算することになります。 len は前述で求めた(dx,dy)の長さですね。 それをプログラムで表すと以下になります。
  if ( len < 256 ) len = 256;
  dX = dx / len;
  dY = dy / len;


指が離されたときの処理も忘れずに


 最後に、指が離されたときに差分情報を忘れずにクリアしておきます。
//********************************
// touch : end
//********************************
o.addEventListener( "touchend"   , 
  function(e) {
    if ( isMoving ) {
      e.preventDefault();
      dX = 0;
      dY = 0;
      isMoving = false;
    }
  }
);

指が離れたイベントは"touchend"


 前述までと同様に、指が離れたときのイベントの処理を記述しています。 そのイベント名は "touchend"です。

移動情報をクリア


 以下のように移動の情報 (dX,dY) をクリアします。
    dX = 0;
    dY = 0;

 必要に応じて(startX,startY)や(movingX,movingY)もクリアした方が良いかも知れません。 今回は必要性がなかったので何もしていません。

移動状態フラグisMovingをクリア


    isMoving = false;

 にてisMovingをfalseにして、移動中ではない、という状態にします。 これで一連の処理は完了です。

きちんと動かすために:変数定義


 本記事で紹介したプログラムは、変数定義をきちんとしないと動きません。
//********************************
// global variables
//********************************
var movingX = 0;
var movingY = 0;
var startX  = 0;
var startY  = 0;
var isMoving = false;
var dX = 0;
var dY = 0;

 こんな感じですね。

きちんと動かすために:呼び出しタイミング


 最初の処理である以下のプログラムは、id="gamescreen" のエレメントが生成されたあとに呼ばれる必要があります。
o = document.getElementById( 
  "gamescreen" 
);

 そのため今回紹介したプログラムは、例えば以下のようにした上で、
//********************************
// initialize
//********************************
function initialize() {
  (今回紹介したプログラム)
}

 bodyのonLoadで呼び出すようにするか以下の形で、HTML側の処理完了後に呼び出すようにしておきます。
// on load
window.addEventListener( 'load', 
  initialize
);

まとめ


 最後に今回のプログラムをまとめておきます。
//********************************
// global variables
//********************************
var movingX = 0;
var movingY = 0;
var startX  = 0;
var startY  = 0;
var isMoving = false;
var dX = 0;
var dY = 0;
//********************************
// set (dX,dY) and normalize
//********************************
function setDxyByMovingXY() {
  //d
  dx = movingX - startX;
  dy = movingY - startY;
  //len
  len = Math.sqrt( dx*dx + dy*dy );
  //too short
  if ( len < 16 ) {
    dX = 0;
    dY = 0;
    return;
  }
  // normalize and set
  if ( len < 256 ) len = 256;
  dX = dx / len;
  dY = dy / len;
}
//********************************
// initialize
//********************************
function initialize() {
  // target object
  o = document.getElementById( 
    "gamescreen" 
  );
  //********************************
  // touch : start
  //********************************
  o.addEventListener( "touchstart" , 
    function(e) {
      e.preventDefault();
      tO = e.changedTouches[0];
      startX = movingX = tO.clientX;
      startY = movingY = tO.clientY;
      isMoving = true;
    }
  );
  //********************************
  // touch : move
  //********************************
  o.addEventListener( "touchmove"  , 
    function(e) {
      if ( isMoving ) {
        e.preventDefault();
        tO = e.changedTouches[0];
        movingX = tO.clientX;
        movingY = tO.clientY;
        setDxyByMovingXY();
      }
    }
  );
  //********************************
  // touch : end
  //********************************
  o.addEventListener( "touchend"   , 
    function(e) {
      if ( isMoving ) {
        e.preventDefault();
        dX = 0;
        dY = 0;
        isMoving = false;
      }
    }
  );
}
// on load
window.addEventListener( 'load', 
  initialize 
);

おまけ:Math.atan2で角度を求めてみる


 先日の記事、操作に応じてプレイヤーキャラクターを動かすプログラミングでは、 扱いやすいように方向の変数も設定していました。 具体的には Math.atan2 という関数を用いて、角度を求めています。
angle = Math.atan2( dY , dX );

 atan2は、三角関数のタンジェントの逆関数、アークタンジェントを発展させものです。 タンジェントは図の \( \frac{sin\theta}{cos\theta} \) です。
 つまり今回の例では、アークタンジェントに \( \frac{dY}{dX} \) を渡すことで、スワイプの角度を求めることができます。 しかしその場合、dXが0だと計算できませんし、両方プラスと両方マイナスの区別ができないなどの問題があります。 そのためプログラムでは atan2 という、除算後の値ではなく、X差分とY差分の両方を引数に取る関数が用意されています。
 これが結構便利ですので、覚えておいて損は無いと思います。 なお返ってくる値はラジアンで、 \( -\pi \) から \( \pi \) の範囲です。

補足

・趣味の範囲なので今回の説明がJavaScriptを専門に扱うプロの方々の目から正しいかは正直わかりません。
・当ブログのプログラムは悪意が無い範囲であれば自由に使っていただいて大丈夫です。ただしトラブル等が起きても責任は取れませんのでご了承ください。
・当ブログで実際に使用している同機能のプログラムはモジュール化されたライブラリになっていて、本ページで解説したものと厳密には異なります。
・ライブラリはlolib.jsにあります。頻繁に変更しますので使用する場合はそのまま読み込まず内容をコピーしてください。
・数式表現にMathJaxを使用しております。助かります!
・画像内のラスタライズ文字フォントにOpen Font LicenseZen Antiqueを使用しております。

カテゴリー:メガホンDEポン,JavaScript
著者プロフィール
青山公士(あおやま こうじ)
中学2年生からゲームプログラミングに明け暮れる。ゲーム開発者としての代表作に「スーパー桃太郎電鉄II」(ハドソン)メインプログラマー、[PR]『ドラゴンクエストX オンライン』(スクウェア・エニックス)テクニカルディレクター/プロデューサーなどがある。[PR]「ドラゴンクエストXを支える技術」(技術評論社)著者。本ブログは今までの経験を活かしプログラミングが楽しいと感じる人が少しでも増えるようなものにしたい。 @kojibm
株式会社ロジック推し
推し情報を論理的にわかりやすく紹介することで「世の中をちょっと楽しく」をミッションに活動中。 HP X Instagram
privacy policy
ピックアップ
Loading...
最新記事
Loading...
関連記事
Loading...