こくぶん研究室

スマホのジャイロを使う

はじめに

スマホのジャイロセンサ

スマホにはたくさんのセンサが付いています。「ジャイロセンサ」は、加速度センサのようにスマホの動きを測るセンサです。加速度センサはスマホのまっすぐな動きを測りますが、ジャイロセンサはスマホの「回転」や「向き」を測ります。パノラマ写真撮影、地図アプリ、ポケモンGO などに欠かせないセンサです。

ジャイロとは

ジャイロは昔から、方向を見失ってしまう大海原で船の向きを測るために使われていました。現代でも、飛行機、ロケットや宇宙船、カーナビ、スマホ、ロボット、ドローン、VR など、動きを伴うものの制御には欠かせないセンサのひとつです。その仕組みは自分で調べてもらうとして、できることは、回転や向きを測ることです。

向きを測る道具として「方位磁石(コンパス)」が思い浮かぶかもしれません。方位磁石は、地球上で、かつ周りに磁力や金属がない環境なら役立ちます。宇宙には地球の地場がないから使えませんね。それどころか、私たちが普段暮らしている環境では、様々な機器が発する磁力や、金属に囲まれた空間ばかりで、方位磁石はあまり役に立ちません。

さぁ 遊んでみましょう

難しい話はここまでにして、さっそく自分のプログラムでスマホのジャイロセンサを使ってみましょう。ジャイロセンサを使えば、スマホの回転や向きをつかまえることができます。これらをアプリに組み込むと、いろいろ面白いことができますよ。

▲TOP

今回の内容

目標

自分のまわり 360度にいる 5人の人魚を探して、タッチして人魚を救うゲームを作ります。

今回の目標
今回の目標

使うもの

ステップ

  1. ジャイロの値を得る
  2. ジャイロを見える化
  3. 人魚を救え!

▲TOP

1. ジャイロの値を得る

なにはともあれ、ジャイロセンサの値を取得して、そのまま値を表示してみましょう。

今から作るもの
今から作るもの

プログラミング

Visual Studio Code(または好みのエディタ)を立ち上げて、以下のコードを入力しましょう。こちらで作ったテンプレートをもとに書き始めると効率的です。ファイルは、C:\xampp\htdocs フォルダの中に「gyro1」というフォルダを作って、その中に「index.html」として保存してください。それぞれのコードが何をしているのか考えながら入力していきましょう。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ジャイロの値を得る</title>
</head>

<body>
<div id="txt">ここにデータを表示</div>             <!-- データを表示するdiv要素 -->

<script>
var alpha = 0, beta = 0, gamma = 0;             // ジャイロの値を入れる変数を3個用意

// ジャイロセンサの値が変化したら実行される deviceorientation イベント
window.addEventListener("deviceorientation", (dat) => {
    alpha = dat.alpha;  // z軸(表裏)まわりの回転の角度(反時計回りがプラス)
    beta  = dat.beta;   // x軸(左右)まわりの回転の角度(引き起こすとプラス)
    gamma = dat.gamma;  // y軸(上下)まわりの回転の角度(右に傾けるとプラス)
});

// 指定時間ごとに繰り返し実行される setInterval(実行する内容, 間隔[ms]) タイマーを設定
var timer = window.setInterval(() => {
    displayData();      // displayData 関数を実行
}, 33); // 33msごとに(1秒間に約30回)

// データを表示する displayData 関数
function displayData() {
    var txt = document.getElementById("txt");   // データを表示するdiv要素の取得
    txt.innerHTML = "alpha: " + alpha + "<br>"  // x軸の値
                  + "beta:  " + beta  + "<br>"  // y軸の値
                  + "gamma: " + gamma;          // z軸の値
}
</script>
</body>
</html>

動作確認

XAMPP Control Panel で Apache を Start させてください(不明な場合はこちらを参考にしてください)。

プログラムを書いているパソコンと、動作確認するスマホが、同じ LAN につながっていることを確認します(不明な場合はこちらを参考にしてください)。

Windows の「コマンド プロンプト」で ipconfig して、パソコンの IP アドレスを調べてください(不明な場合はこちらを参考にしてください)。

スマホのブラウザ(Safari や Chrome)を開き、アドレス欄に以下のように入力します。以下の「10.11.52.81」の部分は上で調べた IP アドレスです。なお、Android 版の Firefox では動作しないようです。

スマホで以下のように表示されます。

アクセスすると
アクセスすると

スマホを水平な場所に置いて、回してみたり、左右や前後(奥/手前)に傾けてみて、値の変化を注意深く見てみましょう。この際、スマホ画面の自動回転は OFF にしておくと動作確認しやすいです。

iOS でも Android でも、スマホを反時計回りに回すと alpha の値がプラスに、手前に傾けると beta の値がプラスに、右に傾けると gamma の値がプラスになります。

ジャイロセンサの値
ジャイロセンサの値

傾けることで値が変わるのは加速度センサと似ていますが、ジャイロセンサのは角度の値を取得することができます。単位は馴染み深い「度」です。加速度センサもジャイロセンサも動きを測りますが、加速度センサは動く強さ、ジャイロセンサは角度です。

解説

プログラムのポイントは、16~20行目の deviceorientation イベントです。window.addEventListener("deviceorientation", コールバック関数) とすると、ジャイロセンサの値が変化するたびに、このコールバック関数が呼ばれます。コールバック関数の中で、alpha, beta, gamma という属性を参照することで、X, Y, Z それぞれの軸まわりの回転の角度を取得することができます。

なお、deviceorientation イベントが起きるたびにコールバック関数の中で値を表示させてもよいのですが、この後ゲーム(アニメーション)を作っていくので、描画のためのタイマーがあったほうが便利です。そこで、23~25行目で setInterval を使ってタイマーを動かして、その中から値を表示させる関数を呼んでいます。

参考

完成品をこちらに置いてありますからスマホで開いてみてください。

▲TOP

2. ジャイロを見える化

ジャイロの値を数字で見るだけではイメージがわかないので、ジャイロの alpha の値(スマホを水平に置いて回す動き)に応じて方位磁石のようなアニメーションが動くプログラムを作ってみましょう。

今から作るもの
今から作るもの

プログラミング

Visual Studio Code(または好みのエディタ)を立ち上げて、さきほどの gyro1 の index.html をもとに以下のコードを入力しましょう。<title> タグ ~ </script> タグ の部分のみを掲載します(このままコピペするだけでは動きません)。ファイルは、C:\xampp\htdocs フォルダの中に「gyro2」というフォルダを作って、その中に「index.html」として保存してください。それぞれのコードが何をしているのか考えながら入力していきましょう。

<title>ジャイロを見える化</title>
</head>

<body>
<div id="txt">ここにデータを表示</div>             <!-- データを表示するdiv要素 -->
<canvas id="canvas" width="300" height="400"></canvas>  <!-- ★絵を描くcanvas要素 -->

<script>
var alpha = 0, beta = 0, gamma = 0;             // ジャイロの値を入れる変数を3個用意
var canvas = document.getElementById("canvas"); // ★canvas要素を取得 
var context = canvas.getContext("2d");          // ★絵を描く部品を取得

// ジャイロセンサの値が変化したら実行される deviceorientation イベント
window.addEventListener("deviceorientation", (dat) => {
    alpha = dat.alpha;  // z軸(表裏)まわりの回転の角度(反時計回りがプラス)
    beta  = dat.beta;   // x軸(左右)まわりの回転の角度(引き起こすとプラス)
    gamma = dat.gamma;  // y軸(上下)まわりの回転の角度(右に傾けるとプラス)
});

// 指定時間ごとに繰り返し実行される setInterval(実行する内容, 間隔[ms]) タイマーを設定
var timer = window.setInterval(() => {
    displayData();      // displayData 関数を実行
    drawOrientation();  // 方向を描く
}, 33); // 33msごとに(1秒間に約30回)

// データを表示する displayData 関数
function displayData() {
    var txt = document.getElementById("txt");   // データを表示するdiv要素の取得
    txt.innerHTML = "alpha: " + alpha + "<br>"  // x軸の値
                  + "beta:  " + beta  + "<br>"  // y軸の値
                  + "gamma: " + gamma;          // z軸の値
}

// コンパスのような絵を描く drawOrientation 関数
function drawOrientation() {
    var centerX = canvas.width  / 2;            // canvasの中心のX座標
    var centerY = canvas.height / 2;	        // canvasの中心のY座標
    var radius  = 100;                          // 枠円の半径および針の長さ
    var radianAlpha = alpha * Math.PI / 180;    // 角度をラジアンに変換
    context.clearRect(0, 0, canvas.width, canvas.height);   // canvasの内容を消す clearRect(x, y, w, h)
    context.beginPath();                        // 描画開始
    context.arc(centerX, centerY, radius, 0, 2 * Math.PI);  // 枠円を描く
    context.strokeStyle = "rgb(0, 0, 0)";       // 枠円の線の色
    context.lineWidth = 2;                      // 線の太さ
    context.stroke();                           // 線を描画
    context.beginPath();                        // 描画開始
    context.moveTo(centerX, centerY);           // 中心に移動
    // 線を引く(cosでx座標、sinでy座標が得られる。長さradiusを掛ける。-90度すると真上に向く。)
    context.lineTo(centerX + Math.cos(radianAlpha - Math.PI / 2) * radius,
                   centerY + Math.sin(radianAlpha - Math.PI / 2) * radius);
    context.strokeStyle = "rgb(255, 0, 0)";     // 針の線の色
    context.lineWidth = 5;                      // 線の太さ
    context.stroke();                           // 線を描画
}
</script>

動作確認

スマホのブラウザ(Safari や Chrome)を開き、アドレス欄に以下のように入力します。以下の「10.11.52.81」の部分はさきほど調べた IP アドレスです。なお、Android 版の Firefox では動作しないようです。

スマホで以下のように表示されます。

アクセスすると
アクセスすると

スマホを水平な場所において、左右に回してみましょう。スマホを回しても、赤い針は常に同じ方向を向きます。

Android の機種によっては、赤い針の方向が安定するまでに時間がかかる場合があります(理由はあとで補足します)。

解説

gyro1 から加えられたいちばん大きな部分は 35~54行目の drawOrientation 関数です。

まず、39行目で「度」単位で得られた alpha の値を「ラジアン」に変換しています。JavaScript に限らず、多くのプログラミング言語では角度をラジアンで扱います。なお、ラジアンは高校数学Ⅱ (?) で扱います。角度を円周率 π(パイ)を使って表す単位で、180度が π、360度が 2π です。

その後、40~45行目では枠になる円を描いています。このあたりは単純ですね。

46行目から、赤い針を描いています。moveTo(x1, y1) で線を描き始める座標(枠の円の中心)に移動して、lineTo(x2, y2) で線を引いています。alpha が示す角度の方向に向かって赤い線を描こうとしています。でも、alpha はあくまで角度ですから、赤い針の先端の X 座標と Y 座標をなんとかして求める必要があります。そこで lineTo() の中で sin() と cos() を使っています。三角関数です。これも高校数学Ⅱ (?) で扱います。詳しくはネット等で検索してほしいのですが、ざっくり、以下のように覚えておきましょう。

なお、cos(角度[ラジアン]) やsin(角度[ラジアン]) の () 内で、ラジアン単位の角度 radianAlpha から π / 2(つまり90度)を引いています。これは、canvas 要素で arc で円を描く時は、X 軸の右側が 0 度で、そこから時計回りに角度を指定するからです。ジャイロセンサの alpha が「0 度」の時に赤い針を真上に向けたかったので、90度を引きました。針の向きにこだわらなければ、ここで 90度を引く必要はありません。

と、だいぶ数学を使ってしまいましたが、数学が得意でなくとも、「習うより慣れろ」方式で、作りながら徐々に慣れていきましょう。

参考

完成品をこちらに置いてありますからスマホで開いてみてください。

補足

Android の機種によっては、赤い針の方向が安定するまでに時間がかかると書きました。iOS の場合、Web アプリの実行が開始されて、最初に deviceorientation イベントが呼ばれた瞬間の alpha を 0度にします。つまり、iOS では alpha は相対的な角度です。Android の機種によっては、方位センサ(電子的な方位磁石)の値も一緒に使って、北の方角を 0度にしようとします。しかし、方位センサは安定するまでに時間がかかったり、周囲に磁力を発する機器があるとなかなか安定しません。いずれにせよ、alpha の値については「相対的」「参考程度」と思っておきましょう。

▲TOP

3. 人魚を救え!

スマホを置きっぱなしではつまらないので、今度はスマホを手に持って全身で動いて遊ぶゲームを作りましょう。自分の周り 360度に架空の海を作って、その中に人魚を配置します。スマホをレーダーのようにして海の中の人魚を探して、人魚をタッチして救い出すゲームです(あくまでそういう「イメージ」で)。5人の人魚をすべて救い出すまでの時間が短いほうが得点が高くなります。

今から作るもの
今から作るもの

プログラミング

Visual Studio Code(または好みのエディタ)を立ち上げて、さきほどの gyro1 か gyro2 の index.html をもとに以下のコードを入力しましょう。<title> タグ ~ </script> タグ の部分のみを掲載します(このままコピペするだけでは動きません)。ファイルは、C:\xampp\htdocs フォルダの中に「gyro3」というフォルダを作って、「index.html」として保存してください。それぞれのコードが何をしているのか考えながら入力していきましょう。

<title>人魚を救え!</title>
</head>

<body>
<div id="txt">ここにスコアを表示</div>          <!-- データを表示するdiv要素 -->
<canvas id="canvas" width="300" height="400"></canvas>  <!-- 絵を描くcanvas要素 -->
<canvas id="aid" width="300" height="48"></canvas>      <!-- 救った人魚を置くcanvas要素 -->

<script>
var alpha = 0, beta = 0, gamma = 0;             // ジャイロの値を入れる変数を3個用意
var canvas = document.getElementById("canvas"); // canvas要素を取得 
var context = canvas.getContext("2d");          // 絵を描く部品を取得

// 人魚クラス
class Mermaid {
    constructor() {                             // コンストラクタ(初期化処理)の定義
        this.img    = new Image;                // 人魚の画像を入れる Image オブジェクト
        this.left   = 0;                        // 人魚のx座標
        this.top    = 0;                        // 人魚のy座標
        this.cLeft  = 0;                        // 人魚のcanvas上のx座標
        this.cTop   = 0;                        // 人魚のcanvas上のx座標
        this.width  = 0;                        // 人魚の画像の幅
        this.height = 0;                        // 人魚の画像の高さ
        this.visible = true;                    // 人魚の表示/非表示
    }
    load(imgName) {                             // 人魚の画像を読み込む load メソッドの定義
        this.img.src = imgName;                 // 読み込み
        this.img.addEventListener("load", () => {   // 読み込みが完了したら
            this.width   = this.img.width;          // 幅を設定
            this.height  = this.img.height;         // 高さを設定
        });
        this.left    = Math.floor( Math.random() * (0 + 3300 + 1) ) - 3300; // x座標をランダム化
        this.top     = Math.floor( Math.random() * (400 - 0 + 1) ) + 0;     // y座標をランダム化
    }
}

var mermaidNum = 5;                             // 人魚の数
var mermaid = new Array(mermaidNum);            // 人魚の配列を宣言
generateMermaid();                              // 人魚を生成する generateMermaid を実行

var touching = false;                           // タッチパネルに触れているか否かの変数
var score = 1000;                               // スコア(減点していく)
var captured = 0;                               // 救った人魚の数

// 人魚を生成する generateMermaid 関数
function generateMermaid() {
    for(var i = 0; i < mermaid.length; i++) {   // 全ての人魚について
        mermaid[i] = new Mermaid();             // 人魚クラスのインスタンスとして初期化
        mermaid[i].load(i + ".jpg");            // 人魚の画像を読み込む
    }
}

// ジャイロセンサの値が変化したら実行される deviceorientation イベント
window.addEventListener("deviceorientation", (dat) => {
    alpha = dat.alpha;  // z軸(表裏)まわりの回転の角度(反時計回りがプラス)
    beta  = dat.beta;   // x軸(左右)まわりの回転の角度(引き起こすとプラス)
    gamma = dat.gamma;  // y軸(上下)まわりの回転の角度(右に傾けるとプラス)
});

// 指定時間ごとに繰り返し実行される setInterval(実行する内容, 間隔[ms]) タイマーを設定
var timer = window.setInterval(() => {
    drawMermaid();      // drawMermaid 関数を実行
    displayScore();     // displayScore 関数を実行
}, 33); // 33msごとに(1秒間に約30回)

// 人魚を描画する drawMermaid 関数
function drawMermaid() {
    context.fillStyle = "rgb(153, 217, 234)";   // 塗りつぶし色 
    context.fillRect(0, 0, canvas.width, canvas.height);    // canvas全体に矩形を描く
    for(var i = 0; i < mermaid.length; i++) {   // 全ての人魚について
        if(mermaid[i].visible === false) {      // その人魚が既に救助されていたら
            continue;                           // 次の人魚へ
        }
        // alpha と beta を使って人魚の座標をcanvas上の座標に変換する処理
        mermaid[i].cLeft = mermaid[i].left + alpha * 10;        // 人魚のcanvas上のx座標を計算
        mermaid[i].cTop  = mermaid[i].top  + (beta - 60) * 10;  // 人魚のcanvas上のy座標を計算
        context.drawImage(mermaid[i].img, mermaid[i].cLeft, mermaid[i].cTop);   // 人魚を描画
    }
}

// スコアを表示する displayScore 関数
function displayScore() {
    score -= 1;                                 // スコアを減点
    var txt = document.getElementById("txt");   // データを表示するdiv要素の取得
    txt.innerHTML = "Score: " + score;          // スコアを表示
    if(captured === mermaidNum) {               // もし全員救っていたら
        window.clearInterval(timer);            // タイマーを止める
    }
}

// タッチ開始イベントが起きたら
canvas.addEventListener("touchstart", (e) => {
    e.preventDefault();                         // 画面をスクロールさせない
    var touchX = e.touches[0].pageX - canvas.offsetLeft;    // タッチしたx座標
    var touchY = e.touches[0].pageY - canvas.offsetTop;     // タッチしたx座標
    for(var i = 0; i < mermaid.length; i++) {   // 全ての人魚について
        if(mermaid[i].visible === false) {      // その人魚が既に救助されていたら
            continue;                           // 次の人魚へ
        }
        // 人魚へのタッチ判定(タッチした座標が人魚画像の中なら)
        if( touchX >= mermaid[i].cLeft
         && touchX <= mermaid[i].cLeft + mermaid[i].width
         && touchY >= mermaid[i].cTop
         && touchY <= mermaid[i].cTop + mermaid[i].height) {
            mermaid[i].visible = false;                 // 人魚を消す
            var aid = document.getElementById("aid");   // 下側のcanvasを取得
            var ctx = aid.getContext("2d");             // コンテキストの取得
            ctx.drawImage(mermaid[i].img, i * mermaid[i].width, 0); // 救助した人魚画像の表示
            captured += 1;                              // 救った人魚の数を加算
        }
    }
});
</script>

動作確認

このアプリは 5個の人魚の画像ファイルが必要です。以下の 5個の画像をダウンロードしてください(パソコンのブラウザで画像を右クリックして「名前をつけて画像を保存」などの方法で)。ダウンロードした 5個の画像を「gyro3」フォルダの中に入れてください(index.html と同じ階層に)。

スマホのブラウザ(Safari や Chrome)を開き、アドレス欄に以下のように入力します。以下の「10.11.52.81」の部分はさきほど調べた IP アドレスです。なお、Android 版の Firefox では動作しないようです。

スマホで以下のように表示されます。

アクセスすると
アクセスすると

スマホをやや立てる感じで手に持って、ゆっくりと、自分の周囲 360度、様々な方向に向けてください。すると海の中を泳いでいる人魚が見つかります。人魚にタッチすると画面の下のほうに人魚を助け出すことができます(助け出すというより捕獲している感じですが...)。

5人の人魚すべてを救い出すと「Score」の数字が止まります。この Score は、ゲームが始まった瞬間からカウントダウンされています。できるだけ早く 5人の人魚を救い出すほど高得点になります。

なお、このゲームはスマホを持って体ごとぐるぐる回ることになりますから、周りに人や物がないか注意してやりましょう。また、目が回りやすい人は、あまり速く動くとクラクラするかもしれませんから、無理はしないようにしましょう。

ゲームをもう一度やる場合は、ブラウザでアプリをリロード(再読込)してください。

解説

15~35行目で Mermaid という「クラス」(ひながた)を作っています。「スマホの加速度センサを使う」の acc3 でもクラスを使いましたが、クラスにすることで、48行目のように new Mermaid() とすることで、ひながたから一式まとめてたくさんの変数やメソッドを複製して簡単に作れます。Mermaid クラスには、各人魚の位置や大きさなどの変数に加えて、load() という関数(メソッド)も定義しています。こうすることで、いちいちプログラムの中で様々な処理を何度も書く必要がなくなります。mermaid.load(画像ファイル名); とするだけで画像ファイルを読み込んで、さらに人魚の大きさや、居場所(left, top)をランダムに設定するところまで自動でやってくれます。

38行目で人魚 5人分の mermaid という配列変数を作り、39行目で generateMermaid 関数を実行して、46~51行目の generateMermaid 関数内で 5人の人魚を作っています。

67~79行目の drawMermaid で人魚を描画しています。特に 75行目が肝心な部分です。人魚の位置は mermaid.load() メソッドでランダムに決めていますが、X 座標は 0~3300 の間のランダムな値にしています。canvas の幅は 300 ピクセルです。3300 + 300 で 3600です。alpha は 0~360度の範囲ですが、alpha を 10倍して 0~3600 の値にしています。つまり、スマホを持って 1回転する(360度回る)ことでジャイロから 0~3600 の値を得て、人魚がいる 0~3600 の範囲を探せるようにしています。ややこしいですが、図を描いたりして、なにをやっているのか確認してみましょう。

82~89行目の displayScore 関数は、ゲーム開始から得点をカウントダウンしています。また、全ての人魚が救助されていたらタイマーを止めてゲームを終了させています。

92~112行目は人魚をタッチして救助する処理です。canvas に touchstart イベントを設定しています。人魚をタッチしたか否かは、人魚の画像の左上の X, Y 座標と、画像の幅 width と高さ height を使って、タッチした座標がそれらの範囲にあるかどうかを判定しています。人魚の画像がタッチされたら、海の中に人魚を表示しないようにして、画面下側に置いたもうひとつの canvas の中に画像を表示させています。これで、助けた人魚(助けていない人魚)が一目瞭然ですね。

参考

完成品をこちらに置いてありますからスマホで開いてみてください。

補足

ここで使った人魚の画像はGATAG | フリーイラスト素材集からダウンロードした画像をもとに、サイズ調整や背景の加工などを行いました。

▲TOP

まとめ

スマホのジャイロセンサを使ったアプリ、いかがでしたか?

ラジアンや三角関数など、やや数学を使うことが多かったですね。高校レベルの数学ですので、ちょっと難しかったかもしれません。また、座標の変換などがややこしかったかもしれません。ラジアンや三角関数、高校で習っている時は「なんの役に立つの?」だった人も多いかもしれません。でも、今回のような回転の動きにはよく使う操作です。使いこなせると面白いゲームが作れますから、苦手な人は、楽しみながら徐々に克服できるとよいでしょう。

ところで今回の「人魚を救え!(gyro3)」は、使ったのは平面の canvas で、2次元の人魚の画像でした。しかし、ぐるぐる回って人魚を探すことができました。2次元ですが、一種のバーチャルリアリティ(VR)のようなものです。実は VR を実現するために大事な要素のひとつが、今回遊んだジャイロセンサなのです。VR ゴーグルを身につけて左右や上下を見回すと、左右や上下の映像を見ることができます。これは、VR ゴーグルの中にジャイロセンサが入っていて、頭の回転を測っているのです。今回のゲームも、原理はほとんど同じです。

また、gyro2 では、赤い針が常に同じ方向を指していました。ジャイロは船や自動車などの道しるべに使われていると言いましたが、まさにこの、回しても常に同じ方向を指し示すのがジャイロの特徴です。例えばこのコンパスのようなものを「人魚を救え!」の画面の端に表示して、人魚がいる方向を示すレーダーのようにすることもできそうですね。あれ? なんだか「ポケモンGO」みたいですね。はい、まさに「ポケモンGO」では、ジャイロの機能を使ってポケモンの居場所を指し示したり、振り向いたらポケモンがいたりするように演出しています。

今回は少しとっつきにくい印象があったかもしれませんが、このようにジャイロは近ごろとてもよく使われています。まずはゲーム作りなどで楽しんで、慣れて、どんどん応用を考えてみましょう!

補足

上で作ったコードはすべてこちら(GitHub)に置いてありますから、参考にしてください。

▲TOP