こくぶん研究室

スマホの加速度センサを使う

はじめに

スマホの加速度センサ

スマホにはたくさんのセンサが付いています。なかでも「知らず知らずに」よく使っているのが「加速度センサ」でしょう。スマホの画面を自動で回転させたり、カメラアプリでスマホが縦向きか横向きかを判断したり、万歩計やヘルスケアアプリなどで使われています。加速度センサはユーザの動きを測るのに便利なセンサです。

加速度とは

加速度というのは、速度の時間変化のことです。例えば自動車や電車が止まった状態(速度ゼロ)から動き出すと(速度が上がると)、自動車や電車が動くのとは反対方向に体が持っていかれます。自動車や電車が一定のスピードで走っている時にブレーキをかけると(速度が下がると)、体が前につんのめります。急アクセルや急ブレーキほど(速度の変化が大きいほど)、体は強く持っていかれます。その度合いが加速度です

もっというと、私たちが地球に立っていられるのも、ものが落ちるのも、地球があらゆる物体を約 9.8 m/s2 の加速度で地球の中心に向かって引っ張っているからです。加速度は「G(ジー)」とも呼ばれます。地球の重力による加速度(約 9.8 m/s2)を「1G」と言います。

さぁ 遊んでみましょう

難しい話はここまでにして、さっそく自分のプログラムでスマホの加速度センサを使ってみましょう。加速度センサを使えば、スマホを傾けたり振ったりする動作をつかまえることができます。これらをアプリに組み込むと、いろいろ面白いことができますよ。

▲TOP

今回の内容

目標

スマホを傾ける動作でプレイヤーを操作して、向かってくる(降ってくる)隕石をよけ続ける時間を競うゲームを作ります。

今回の目標
今回の目標

使うもの

ステップ

  1. 加速度の値を得る
  2. 加速度を見える化
  3. 傾きコントローラ
  4. 隕石をよけろ!
  5. おまけ:無重力に挑戦!

▲TOP

1. 加速度の値を得る

なにはともあれ、加速度センサの値を取得して、そのまま値を表示してみましょう。

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

プログラミング

Visual Studio Code(または好みのエディタ)を立ち上げて、以下のコードを入力しましょう。こちらで作ったテンプレートをもとに書き始めると効率的です。ファイルは、C:\xampp\htdocs フォルダの中に「acc1」というフォルダを作って、その中に「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 aX = 0, aY = 0, aZ = 0;                     // 加速度の値を入れる変数を3個用意

// 加速度センサの値が変化したら実行される devicemotion イベント
window.addEventListener("devicemotion", (dat) => {
    aX = dat.accelerationIncludingGravity.x;    // x軸の重力加速度(Android と iOSでは正負が逆)
    aY = dat.accelerationIncludingGravity.y;    // y軸の重力加速度(Android と iOSでは正負が逆)
    aZ = dat.accelerationIncludingGravity.z;    // z軸の重力加速度(Android と iOSでは正負が逆)
});

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

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

</body>
</html>

動作確認

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

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

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

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

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

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

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

iPhone(または iPad や iPod touch) の場合、右に傾けると X はプラスの値に、奥に傾けると Y がプラスの値になります。iPhone を水平な場所に置くと Z はだいたい -9.8 くらいの値を示します。

Android スマホの場合、右に傾けると X はマイナスの値に、奥に傾けると Y がマイナスの値になります。スマホを水平な場所に置くと Z はだいたい 9.8 くらいの値を示します。

iPhone と Android では得られる値の符号(+/-)が反対ですが、スマホを水平に置いた時、左右方向の動きが X、前後(奥/手前)方向の動きが Y、上下方向の動きが Z です。この値を上手に使えば、スマホの動きや傾きでいろいろなことに応用できます。

以下の二つの図では、スマホを動かす方向を矢印で示しています。例えば iOS のデバイスを右に動かすと X はマイナスになります。あれ? iOS では右に傾けるとプラスではなかったの? はい。デバイスを右に動かすと、加速度は左方向に発生します(左に傾けたのと同じになります)。と、考えているとちょっとややこしいですが、プラスとマイナスの方向は、プログラムを作って試しながら決めていけば問題ありません。

加速度の値(iOS の場合)
加速度の値(iOS の場合)
加速度の値(Android の場合)
加速度の値(Android の場合)

特に Z の 9.8(m/s2)という値がポイントです。これは地球の重力(1G)そのものです。スマホを水平に置いていれば、真下に向かって 1G が発生しています。スマホを右か左に傾けていって、90度傾ければ、X の値が 9.8 になります。同様に、奥か手前に傾けていって、90度傾ければ、Y の値が 9.8 になります。

解説

プログラムのポイントは、16~20行目の devicemotion イベントです。window.addEventListener("devicemotion", コールバック関数) とすると、加速度センサの値が変化するたびに、ここのコールバック関数が呼ばれます。コールバック関数の中で、accelerationIncludingGravity という属性を参照しています。「重力を含んだ加速度」という意味ですが、ざっくりと「私たちが普段感じている加速度」と思っておいて構いません。さらに accelerationIncludingGravity.x のように x, y, z を指定することで、各方向の加速度の値を取得することができます。

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

参考

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

▲TOP

2. 加速度を見える化

加速度の値を数字で見るだけではイメージがわかないので、加速度の大きさに応じて動くボールのアニメーションをプログラミングしてみましょう。

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

プログラミング

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

<title>加速度を見える化</title>
</head>

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

<script>
var aX = 0, aY = 0, aZ = 0;                     // 加速度の値を入れる変数を3個用意
var canvas = document.getElementById('canvas'); // ★canvas要素を取得 
var context = canvas.getContext('2d');          // ★絵を描く部品を取得

// 加速度センサの値が変化したら実行される devicemotion イベント
window.addEventListener("devicemotion", (dat) => {
    aX = dat.accelerationIncludingGravity.x;    // x軸の重力加速度(Android と iOSでは正負が逆)
    aY = dat.accelerationIncludingGravity.y;    // y軸の重力加速度(Android と iOSでは正負が逆)
    aZ = dat.accelerationIncludingGravity.z;    // z軸の重力加速度(Android と iOSでは正負が逆)
});

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

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

// ★canvasにボール(円)を描く drawBall 関数
function drawBall() {
    var centerX = canvas.width  / 2;            // canvasの中心のX座標
    var centerY = canvas.height / 2;	        // canvasの中心のY座標
    var ballRad = 50;                           // ボールの半径
    var ballColor = "rgb(0, 0, 255)";           // ボールの色
    var g  = 9.80665;                           // 1Gの時の重力加速度[m/s^2]
    var d  = centerX / g;                       // 1m/s^2 あたりでボールが動く量
    var os = navigator.platform;                // OS名の取得
    if(os === "iPhone" || os === "iPad" || os === "iPod") {     // iOSなら
        aX *= -1;                               // 加速度の正負を反転させる
        aY *= -1;                               // a *= b は a = a * b の意味
        aZ *= -1;
    }
    context.clearRect(0, 0, canvas.width, canvas.height);   // canvasの内容を消す clearRect(x, y, w, h)
    context.beginPath();                        // 描画開始
    context.arc(centerX - d * aX,               // 円を描く arc(x, y, 半径, 開始角度, 終了角度)
                centerY + d * aY,               // 加速度xとyに、1m/s^2あたりで動く量dをかける
                ballRad - 3 * aZ,               // ボールの半径(zに掛けた係数3はテキトー)
                0, 2 * Math.PI);                // 角度の単位はラジアン(2π = 360度)で指定
    context.fillStyle = ballColor;              // 塗りつぶす色の設定
    context.fill();                             // 塗る
}
</script>

動作確認

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

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

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

スマホを手に持って、様々な方向に傾けたり、揺らしたりしてみてください。傾けた大きさ、揺らした強さでボールが動きます。表裏(Z 方向)の加速度の大きさはボールの大きさに反映されます。

解説

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

まず、43~47行目で navigator.platform という機能を使ってスマホの OS を調べ、iOS の場合に加速度の値を反転しています。Android の場合に反転させても良いのですが、navigator.platform で得られる値が、iOS のほうが判別しやすいためです。

X 軸方向は 1G つまり 9.8 m/s2 の時に左右いっぱいにボールが動くようにしています。

参考

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

▲TOP

3. 傾きコントローラ

acc2 は、加速度(傾き)の大きさでボールの「位置」を変えていました。このボールをプレイヤーにして何かゲームに使えそうですが、水平に戻すとボールは勝手に真ん中に戻るので、なんだかつまらなそうです。加速度(傾き)の大きさでボールの「速度」を変えると、ボールを画面じゅう動かせるようになります。「位置」や「速度」と難しく書いてしまいましたが、とにかくやってみましょう。

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

プログラミング

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

<title>傾きコントローラ</title>
</head>

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

<script>
var aX = 0, aY = 0, aZ = 0;                     // 加速度の値を入れる変数を3個用意
var canvas = document.getElementById('canvas'); // canvas要素を取得 
var context = canvas.getContext('2d');          // 絵を描く部品を取得

// ★ボールをクラス化
class Ball {
    constructor() {                             // コンストラクタ(宣言した時に実行される関数)の定義
        this.x = 150;                           // x座標の初期値
        this.y = 200;                           // y座標の初期値
        this.radius = 20;                       // 半径の初期値
        this.color = "rgb(0, 0, 255)";          // 色の初期値
        this.sp = 3;                            // 速さの係数
    }
}
var player = new Ball();                        // ★Ballクラスのインスタンス player を宣言

// 加速度センサの値が変化したら実行される devicemotion イベント
window.addEventListener("devicemotion", (dat) => {
    aX = dat.accelerationIncludingGravity.x;    // x軸の重力加速度(Android と iOSでは正負が逆)
    aY = dat.accelerationIncludingGravity.y;    // y軸の重力加速度(Android と iOSでは正負が逆)
    aZ = dat.accelerationIncludingGravity.z;    // z軸の重力加速度(Android と iOSでは正負が逆)
});

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

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

// ★プレイヤを表示する drawPlayer 関数
function drawPlayer() {
    var os = navigator.platform;                // OS名の取得
    if(os === "iPhone" || os === "iPad" || os === "iPod") {     // iOSなら
        aX *= -1;                               // 加速度の正負を反転させる
        aY *= -1;                               // a *= b は a = a * b の意味
        aZ *= -1;
    }
    player.x -= player.sp * aX;                 // プレイヤのx座標を更新(a -= b は a = a - b の意味)
    player.y += player.sp * aY;                 // プレイヤのy座標を更新(a += b は a = a + b の意味)
    if(player.x < 0) {                          // xが0未満なら
        player.x = 0;                               // xを0にする(それより左に行かない)
    } else if(player.x > canvas.width) {        // xがcanvasの幅以上なら
        player.x = canvas.width;                    // xをcanvasの幅の値にする(それより右に行かない)
    }
    if(player.y < 0) {                          // yが0未満なら
        player.y = 0;                               // yを0にする(それより上に行かない)
    } else if(player.y > canvas.height) {       // yがcanvasの高さ以上なら
        player.y = canvas.height;                   // yをcanvasの高さの値にする(それより下に行かない)
    }
    context.clearRect(0, 0, canvas.width, canvas.height);   // canvasの内容を消す clearRect(x, y, w, h)
    context.beginPath();                        // 描画開始
    context.arc(player.x, player.y, player.radius,  // 円を描く arc(x, y, 半径, 開始角度, 終了角度)
                0, 2 * Math.PI);                // 角度の単位はラジアン(2π = 360度)で指定
    context.fillStyle = player.color            // 塗りつぶす色の設定
    context.fill();                             // 塗る
}
</script>

動作確認

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

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

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

スマホを手に持って、様々な方向に傾けてみてください。さきほどの acc2 とは違って、傾けるとボールが動き出して、水平に戻しても真ん中には戻らず、その場でボールが止まります。大きく傾けるほどボールは速く動きます。

解説

14~22行目で Ball という「クラス」(ひながた)を作っています。acc2 では、ボールの位置や色の変数を drawBall 関数の中でバラバラに宣言していました。今回はあらかじめクラスを作って、その中に各種設定を入れました。この後 acc4 で、画面上にボールをたくさん作る予定です。たくさんのボールの座標や色などを別々の変数で宣言していくとゴチャゴチャします。クラスにしておけば、23行目のように new Ball() とすることで、ひながたから一式まとめて複製(インスタンス)が簡単に作れます。

また、acc2 では drawBall としていた関数を、drawPlayer としています。ボールを動かしていることは変わりませんが、acc2 とはボールの動かし方が違っています。

acc2 では例えば、centerX - d * aX という式でボールの位置を決めていました。canvas の中心位置から加速度の値(aX)を引いています。つまり、傾きの大きさ=ボールの「位置」でした。

この acc3 では、player.x -= player.sp * aX としています。ボール(player)の位置からさらに傾きの大きさ aX を引いています。player 自体の位置を変化させています。「位置の変化」は「速さ」です。つまり、傾きの大きさを「速さ」に反映させています。

さらに、56~65行目で、ボールが canvas の外に出ていってしまわないような処理を加えています。

なにはともあれ、画面上をボールが動き回れるようになりました。次でゲームらしいプログラムにしていきましょう。

参考

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

▲TOP

4. 隕石をよけろ!

画面の上から向かってくる様々な大きさ・速さの隕石をよけ続け、その時間を競うゲームを作ります。

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

プログラミング

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

<title>隕石をよけろ!</title>
</head>

<body>
<div id="txt">ここにスコアを表示</div>             <!-- スコアを表示するdiv要素 -->
<canvas id="canvas" width="300" height="400"></canvas>  <!-- ボールを描くcanvas要素 -->

<script>
var aX = 0, aY = 0, aZ = 0;                     // 加速度の値を入れる変数を3個用意
var canvas = document.getElementById('canvas'); // canvas要素を取得 
var context = canvas.getContext('2d');          // 絵を描く部品を取得

// ボールをクラス化
class Ball {
    constructor() {                             // コンストラクタ(宣言した時に実行される関数)の定義
        this.x = 150;                           // x座標の初期値
        this.y = 200;                           // y座標の初期値
        this.radius = 20;                       // 半径の初期値
        this.color = "rgb(0, 0, 255)";          // 色の初期値
        this.sp = 3;                            // 速さの係数
    }
}
var player = new Ball();                        // Ballクラスのインスタンス player を宣言

var meteoNum = 5;                               // ★隕石の数
var meteo = new Array(meteoNum);                // ★隕石の配列を宣言
generateMeteo();                                // ★隕石を生成する generateEnemies を実行

var gameTime = 0;                               // ★ゲーム時間(スコア)

// 加速度センサの値が変化したら実行される devicemotion イベント
window.addEventListener("devicemotion", (dat) => {
    aX = dat.accelerationIncludingGravity.x;    // x軸の重力加速度(Android と iOSでは正負が逆)
    aY = dat.accelerationIncludingGravity.y;    // y軸の重力加速度(Android と iOSでは正負が逆)
    aZ = dat.accelerationIncludingGravity.z;    // z軸の重力加速度(Android と iOSでは正負が逆)
});

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

// ★ゲーム時間(スコア)を表示する displayTime 関数
function displayTime() {
    gameTime += 1;
    var txt = document.getElementById("txt");   // データを表示するdiv要素の取得
    txt.innerHTML = "Score: " + gameTime;
}

// プレイヤを表示する drawPlayer 関数
function drawPlayer() {
    var os = navigator.platform;                // OS名の取得
    if(os === "iPhone" || os === "iPad" || os === "iPod") {     // iOSなら
        aX *= -1;                               // 加速度の正負を反転させる
        aY *= -1;                               // a *= b は a = a * b の意味
        aZ *= -1;
    }
    player.x -= player.sp * aX;                 // プレイヤのx座標を更新(a -= b は a = a - b の意味)
    player.y += player.sp * aY;                 // プレイヤのy座標を更新(a += b は a = a + b の意味)
    if(player.x < 0) {                          // xが0未満なら
        player.x = 0;                               // xを0にする(それより左に行かない)
    } else if(player.x > canvas.width) {        // xがcanvasの幅以上なら
        player.x = canvas.width;                    // xをcanvasの幅の値にする(それより右に行かない)
    }
    if(player.y < 0) {                          // yが0未満なら
        player.y = 0;                               // yを0にする(それより上に行かない)
    } else if(player.y > canvas.height) {       // yがcanvasの高さ以上なら
        player.y = canvas.height;                   // yをcanvasの高さの値にする(それより下に行かない)
    }
    context.clearRect(0, 0, canvas.width, canvas.height);   // canvasの内容を消す clearRect(x, y, w, h)
    context.beginPath();                        // 描画開始
    context.arc(player.x, player.y, player.radius,  // 円を描く arc(x, y, 半径, 開始角度, 終了角度)
                0, 2 * Math.PI);                // 角度の単位はラジアン(2π = 360度)で指定
    context.fillStyle = player.color            // 塗りつぶす色の設定
    context.fill();                             // 塗る
}

// ★隕石を生成する generateMeteo 関数
function generateMeteo() {
    for(var i = 0; i < meteo.length; i++) {     // 全ての隕石について
        meteo[i] = new Ball();                  // Ball クラスのインスタンスとして初期化
        meteo[i].x = i * canvas.width / meteoNum + (canvas.width / meteoNum / 2);   // 隕石の横方向の位置
        meteo[i].color = "rgb(0, 0, 0)";        // 隕石の色
        randomizeMeteo(meteo[i])                // それ以外のプロパティはランダムに決める
    }
}

// ★隕石のプロパティをランダムに決める randomizeMeteo 関数
function randomizeMeteo(obj) {
    obj.y = Math.random() * (0 + canvas.height) - canvas.height;            // 隕石のy座標
    obj.radius = Math.random() * (canvas.width / meteoNum / 2 - 10) + 10;   // 隕石の半径
    obj.sp = Math.random() * (15 - 1) + 1;                                  // 隕石の速さ
}

// ★隕石を描画する drawMeteo 関数
function drawMeteo() {
    for(var i = 0; i < meteo.length; i++) {     // 全ての隕石について
        meteo[i].y += meteo[i].sp;              // それぞれのスピードで動かす
        if(collision(player, meteo[i]) === true) {  // 衝突判定結果がtrueなら
            window.clearInterval(timer);            // タイマーを止める
        }
        if(meteo[i].y > canvas.height + meteo[i].radius) {  // もし隕石が画面から消えたら
            randomizeMeteo(meteo[i]);           // 位置やサイズを初期化・ランダム化する
        }
        context.beginPath();                    // 描画開始
        context.arc(meteo[i].x, meteo[i].y, meteo[i].radius,  // 円を描く arc(x, y, 半径, 開始角度, 終了角度)
                    0, 2 * Math.PI);            // 角度の単位はラジアン(2π = 360度)で指定
        context.fillStyle = meteo[i].color      // 塗りつぶす色の設定
        context.fill();                         // 塗る
    }
}

// ★プレイヤ(obj1)と隕石(obj2)の衝突判定を行う collision 関数(true/false を返す)
function collision(obj1, obj2) {
    var dX = obj1.x - obj2.x;                   // プレイヤと各隕石の中心の横方向の距離
    var dY = obj1.y - obj2.y;                   // プレイヤと各隕石の中心の縦方向の距離
    var distance  = Math.sqrt(Math.pow(dX, 2) + Math.pow(dY, 2));   // プレイヤと隕石の中心の直線距離
    if(distance < (obj1.radius + obj2.radius)) {    // 直線距離がプレイヤの半径と隕石の半径の和より小さければ
        return true;                            // 衝突(true)を返す
    } else {                                    // そうでなければ
        return false;                           // falseを返す
    }
}
</script>

動作確認

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

スマホで以下のように表示されます。すぐにゲームが始まります。

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

画面の上から向かってくる様々な大きさ・速さの隕石をよけ続けましょう。時間経過とともに「Score」が上がっていきます。隕石に衝突するとゲームオーバーで、画面が止まります。

もう一度プレイする場合は、ブラウザで再読込(リロード)してください。

解説

25~27行目で隕石に関する処理をしています。meteoNum は隕石の数です(今回は5個に設定)。meteo は meteoNum 個ぶんの隕石の情報を入れる配列です。その後、generateMeteo 関数(81~88行目)を呼んでいます。

generateMeteo 関数の中では、meteo[i] = new Ball(); とすることで、Ball クラスのインスタンスとして隕石を作っています。隕石の x 位置は、画面を5分割した位置に固定していて、隕石はまっすぐに向かってくる(曲がらない)設定としました。また、最初の y 位置、半径、速さは randomizeMeteo 関数で Math.random() を使ってランダムに決めています。

Math.random() 関数は呼ばれるたびに 0 ~ 1 の間のランダムな小数を返します。Math.random() * (最大値 - 最小値) + 最小値 という式を使うと、自分の好きな最小値~最大値の間の値を作ることができます。

setInterval タイマーの中で drawMetro 関数を呼んで隕石を描画しています。drawMeteo 関数(98~113行目)では、隕石を y 方向(画面の下方向)に移動させた後、プレイヤーと隕石の衝突判定をする collision 関数(116~125行目)を呼んでいます。衝突判定の方法は後述するとして、衝突していると判定されたら clearInterval でタイマーを止めています。さらに、隕石が画面の外まで動いたら、randomizeMeteo 関数でその隕石の y 位置、半径、速さを変えて、また向かってくるようにしています。

衝突判定の方法は以下の図に示します。

衝突判定の方法
衝突判定の方法

まず、プレイヤーの中心と隕石の中心の座標をもとに、横方向の距離(dx)と縦方向の距離(dy)を求めています。プレイヤーの中心と隕石の中心の直線距離(distance)は、三平方(ピタゴラス)の定理で求めます。プレイヤーと隕石はどちらも「円」ですから、この distance が、プレイヤーの半径と隕石の半径の和よりも短かったら、衝突していることになります。

得点は、displayTime 関数内で gameTime という変数をカウントアップして、表示しています。displayTime 関数は、setInterval タイマーから 33 ms ごとに呼ばれますから、1秒間で約 30 点になります。

参考

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

さぁ、今回はこれで完成です。ゲームを Netlify Drop などのホスティングサービスで公開して、友人・知人と得点を競い合いましょう!

ゲームの難易度は、randomizeMeteo 関数内で隕石の大きさや速さの範囲を変えたり、隕石の数を増やしたりして調整できます。また、プレイヤーの大きさを大きくしたり、プレイヤーの速さを遅くするなどしてもよいでしょう。

さらに、もっと難易度を高める方法として、プレイヤーの制御方法を変える方法を以下の「おまけ」で紹介します。

▲TOP

5. おまけ:無重力に挑戦!

acc4 の状態でもじゅうぶんに楽しめますが、プレイヤーの動きを工夫して、ゲームをもっと難しく、チャレンジングなものにしてみます。

acc3 と acc4 ではプレイヤーは、スマホの傾きの大きさに応じた「速度」で動かしていました。スマホを水平に戻せば止まる(速度がゼロになる)ので、慣れてくるとけっこう簡単です。これを「速度制御」といいます。

ところで、自動車や電車などの乗り物は、アクセルをはなしてもずっと進んでいきます。慣性の法則というやつですね。実は、自動車のアクセルペダルは「踏み込んだ量=速度」ではなくて、「踏み込んだ量=加速度」です。今走っている速度に、アクセルを踏んだぶんだけ速度が加わるという意味です。これを「加速度制御」といいます。自動車に限らず、船も、ロケットや宇宙船も、私たちの身のまわりの乗り物はほとんど「加速度制御」で動いています。

ここでは、acc4 をほんの少し改造して、プレイヤーの動かし方を加速度制御にした隕石ゲームを作ります。まるで無重力状態でふわふわしていて操作が難しくなりますが、むしろそれがリアルな世界での動きです。

プログラミング

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

acc4 から変更があるのは、コメントに★印を付けた部分だけです。

<title>無重力に挑戦!</title>
</head>

<body>
<div id="txt">ここにスコアを表示</div>             <!-- スコアを表示するdiv要素 -->
<canvas id="canvas" width="300" height="400"></canvas>  <!-- ボールを描くcanvas要素 -->

<script>
var aX = 0, aY = 0, aZ = 0;                     // 加速度の値を入れる変数を3個用意
var canvas = document.getElementById('canvas'); // canvas要素を取得 
var context = canvas.getContext('2d');          // 絵を描く部品を取得

// ボールをクラス化
class Ball {
    constructor() {                             // コンストラクタ(宣言した時に実行される関数)の定義
        this.x = 150;                           // x座標の初期値
        this.y = 200;                           // y座標の初期値
        this.radius = 20;                       // 半径の初期値
        this.color = "rgb(0, 0, 255)";          // 色の初期値
        this.sp = 3;                            // 速さの係数
        this.sx = 0;                            // ★x方向の加速
        this.sy = 0;                            // ★y方向の加速
    }
}
var player = new Ball();                        // Ballクラスのインスタンス player を宣言

var meteoNum = 5;                               // 隕石の数
var meteo = new Array(meteoNum);                // 隕石の配列を宣言
generateMeteo();                                // 隕石を生成する generateEnemies を実行

var gameTime = 0;                               // ゲーム時間(スコア)

// 加速度センサの値が変化したら実行される devicemotion イベント
window.addEventListener("devicemotion", (dat) => {
    aX = dat.accelerationIncludingGravity.x;    // x軸の重力加速度(Android と iOSでは正負が逆)
    aY = dat.accelerationIncludingGravity.y;    // y軸の重力加速度(Android と iOSでは正負が逆)
    aZ = dat.accelerationIncludingGravity.z;    // z軸の重力加速度(Android と iOSでは正負が逆)
});

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

// ゲーム時間(スコア)を表示する displayTime 関数
function displayTime() {
    gameTime += 1;
    var txt = document.getElementById("txt");   // データを表示するdiv要素の取得
    txt.innerHTML = "Score: " + gameTime;
}

// プレイヤを表示する drawPlayer 関数
function drawPlayer() {
    var os = navigator.platform;                // OS名の取得
    if(os === "iPhone" || os === "iPad" || os === "iPod") {     // iOSなら
        aX *= -1;                               // 加速度の正負を反転させる
        aY *= -1;                               // a *= b は a = a * b の意味
        aZ *= -1;
    }
    player.sx += 0.2 * aX;                      // ★x方向に加速(係数はテキトー)
    player.sy += 0.2 * aY;                      // ★y方向に加速(係数はテキトー)
    player.x -= player.sp * aX + player.sx;     // ★プレイヤのx座標を更新(加速付き)
    player.y += player.sp * aY + player.sy;     // ★プレイヤのy座標を更新(加速付き)
    if(player.x < 0) {                          // xが0未満なら
        player.x = 0;                               // xを0にする(それより左に行かない)
        player.sx = 0;                              // ★x方向の加速を0にする
    } else if(player.x > canvas.width) {        // xがcanvasの幅以上なら
        player.x = canvas.width;                    // xをcanvasの幅の値にする(それより右に行かない)
        player.sx = 0;                              // ★x方向の加速を0にする
    }
    if(player.y < 0) {                          // yが0未満なら
        player.y = 0;                               // yを0にする(それより上に行かない)
        player.sx = 0;                              // ★y方向の加速を0にする
    } else if(player.y > canvas.height) {       // yがcanvasの高さ以上なら
        player.y = canvas.height;                   // yをcanvasの高さの値にする(それより下に行かない)
        player.sx = 0;                              // ★y方向の加速を0にする
    }
    context.clearRect(0, 0, canvas.width, canvas.height);   // canvasの内容を消す clearRect(x, y, w, h)
    context.beginPath();                        // 描画開始
    context.arc(player.x, player.y, player.radius,  // 円を描く arc(x, y, 半径, 開始角度, 終了角度)
                0, 2 * Math.PI);                // 角度の単位はラジアン(2π = 360度)で指定
    context.fillStyle = player.color            // 塗りつぶす色の設定
    context.fill();                             // 塗る
}

// 隕石を生成する generateMeteo 関数
function generateMeteo() {
    for(var i = 0; i < meteo.length; i++) {     // 全ての隕石について
        meteo[i] = new Ball();                  // Ball クラスのインスタンスとして初期化
        meteo[i].x = i * canvas.width / meteoNum + (canvas.width / meteoNum / 2);   // 隕石の横方向の位置
        meteo[i].color = "rgb(0, 0, 0)";        // 隕石の色
        randomizeMeteo(meteo[i])                // それ以外のプロパティはランダムに決める
    }
}

// 隕石のプロパティをランダムに決める randomizeMeteo 関数
function randomizeMeteo(obj) {
    obj.y = Math.random() * (0 + canvas.height) - canvas.height;            // 隕石のy座標
    obj.radius = Math.random() * (canvas.width / meteoNum / 2 - 10) + 10;   // 隕石の半径
    obj.sp = Math.random() * (15 - 1) + 1;                                  // 隕石の速さ
}

// 隕石を描画する drawMeteo 関数
function drawMeteo() {
    for(var i = 0; i < meteo.length; i++) {     // 全ての隕石について
        meteo[i].y += meteo[i].sp;              // それぞれのスピードで動かす
        if(collision(player, meteo[i]) === true) {  // 衝突判定結果がtrueなら
            window.clearInterval(timer);            // タイマーを止める
        }
        if(meteo[i].y > canvas.height + meteo[i].radius) {  // もし隕石が画面から消えたら
            randomizeMeteo(meteo[i]);           // 位置やサイズを初期化・ランダム化する
        }
        context.beginPath();                    // 描画開始
        context.arc(meteo[i].x, meteo[i].y, meteo[i].radius,  // 円を描く arc(x, y, 半径, 開始角度, 終了角度)
                    0, 2 * Math.PI);            // 角度の単位はラジアン(2π = 360度)で指定
        context.fillStyle = meteo[i].color      // 塗りつぶす色の設定
        context.fill();                         // 塗る
    }
}

// プレイヤ(obj1)と隕石(obj2)の衝突判定を行う collision 関数(true/false を返す)
function collision(obj1, obj2) {
    var distanceX = obj1.x - obj2.x;            // プレイヤと各隕石の中心の横方向の距離
    var distanceY = obj1.y - obj2.y;            // プレイヤと各隕石の中心の縦方向の距離
    var distance  = Math.sqrt(Math.pow(distanceX, 2) + Math.pow(distanceY, 2)); // プレイヤと隕石の中心の直線距離
    if(distance < (obj1.radius + obj2.radius)) {    // 直線距離がプレイヤの半径と隕石の半径の和より小さければ
        return true;                            // 衝突(true)を返す
    } else {                                    // そうでなければ
        return false;                           // falseを返す
    }
}
</script>

動作確認

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

遊び方は acc4 の時と同じです。画面の上から向かってくる様々な大きさ・速さの隕石をよけ続けましょう。時間経過とともに「Score」が上がっていきます。隕石に衝突するとゲームオーバーで、画面が止まります。

もう一度プレイする場合は、ブラウザで再読込(リロード)してください。

解説

どうですか? プレイヤーの操縦がなんだか難しくなったと思います。スマホを水平に戻しただけではプレイヤーは止まらない(動き続ける)ので、いったん反対方向に傾けてブレーキをかける必要があります。また、傾けているとプレイヤーがどんどん速く移動してしまいます。自動車のアクセル・ブレーキ・ハンドルの操作や、ロケットや宇宙船の軌道変更も、実はこういう制御をしています。リアルになったぶん、難しくなります。

21・22行目で sx, sy という変数を定義しています。これらは加速の量(速度を加える量)を入れる値です。具体的には、drawPlayer 関数内の 62・63行目で、スマホを傾けたぶん sx, sy の値を増やしています。そして、64・65行目でさきほどの速度制御の式に対して、sx, sy を加えています。まさに速度制御にさらにスマホを傾けたぶんの速度を加えています。

ちなみに、62・63行目の係数 0.2 を 0.5 のようにするともっと俊敏に加速します(レーシングカーのようになります)が、そのぶんコントロールも難しくなります。いろいろ試してみましょう。

参考

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

▲TOP

まとめ

スマホの加速度センサを使ったアプリ、いかがでしたか?

「速度」や「加速度」と聞くと算数の時間を思い出して、なんだか難しいと思ってしまうかもしれません。でも、要はスマホを傾ける大きさや振る強さです。今回のプログラムにはいくつかの計算式がありましたが、実はいずれも小・中学校レベルのものです。位置、時間、速度、加速度、三平方(ピタゴラス)の定理... いずれも学校で習っている時は「なんの役に立つの?」だった人も多いかもしれません。でも、使いこなせると、いろいろと楽しいものが作れます。

スマホは手に持って使うものですから、スマホの傾きや動きをとらえる加速度センサはいろいろなことに使えます。今回はユーザが意図的にスマホを動かす動作を使いましたが、ユーザが意図していない(何気ない・自然な)動作を加速度センサで測って、便利なことに使うこともできます。万歩計や、姿勢や体のバランスを測ったり、自動車の運転の荒さ・優しさを測ったりもできます。

まずはゲーム作りなどで楽しんで、どんどん応用を考えてみましょう!

補足

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

▲TOP