見出し画像

小学生サッカー!簡単に使えるエリア分析システムを開発しました!

サッカーの試合を見ていると、どのエリアで試合が進んでいるのか、どのチームがどこで優勢なのか、気になることが多いですよね。特に、小学生の試合では、まだボールを遠くに飛ばす力がないため、ボールが集まりやすいエリアが戦術に与える影響は非常に大きいです。

そこで、今回は、小学生サッカーに特化したエリア分析システムを開発しました!このシステムは簡単に操作でき、試合中にリアルタイムでフィールド上のボールの動きを追跡し、どのエリアにどれだけの時間ボールが滞在しているかを計測できるツールです。これにより、戦術的な判断が瞬時にできるだけでなく、子供たち自身も自分たちのプレーを振り返ることができるようになります。


開発の背景:小学生サッカーの戦術を見える化!

小学生サッカーでは、プレーヤーがまだ成長途中であり、試合の展開が通常のサッカーとは少し異なります。子供たちはボールを遠くまで蹴る力が十分ではないため、試合が特定のエリアで展開されやすいのです。そのため、どのエリアでボールが長時間滞在しているかを知ることは、戦術的に重要だと考えました。

「守備ゾーン」「守備エリア中央」「攻撃エリア中央」「攻撃ゾーン」の4つのエリアにフィールドを分割し、それぞれのエリアでボールがどれくらいの時間滞在しているかを記録することで、チームの守備や攻撃のパフォーマンスを見える化できるエリア分析システムを開発しました。


全体の流れをMapifyでマインドマップ化!


エリア分析システムの開発手順書

ここからはこのシステムを実際に開発するための手順をご紹介します。このシステムはGoogle Apps Script(GAS)を使って、簡単にスプレッドシート上で管理できるようになっています。また、スマホでも使えるため、現場でもすぐに活用できます。

ステップ1:スプレッドシートをセットアップしよう

まず、Google スプレッドシートに試合データを記録するための表を用意します。以下の項目を参考にしてスプレッドシートを準備します。

スプレッドシートの登録項目

  • 対戦相手(セル:B2)

  • 試合日時(セル:B3)

  • ボールの位置(セル:B4)

  • 守備ゾーン滞在時間(秒)(セル:B5)

  • 守備エリア中央滞在時間(秒)(セル:B6)

  • 攻撃エリア中央滞在時間(秒)(セル:B7)

  • 攻撃ゾーン滞在時間(秒)(セル:B8)

  • 「その他」滞在時間(秒)(セル:B9)

  • 試合開始時間(セル:B10)

  • 試合終了時間(セル:B11)

シートは、下記の画像と同じように整えればOKです。スプレッドシートが用意できたら、次はGASでプログラムを組みます。

ステップ2:Google Apps Scriptのプログラムの作成

次に、Google スプレッドシートに紐づけられるスクリプトを作成します。スクリプトの目的は、試合中に各エリアにボールがどれくらいの時間滞在しているのかを自動的に記録することです。

プログラムの実装手順

  1. スプレッドシートを開きます

  2. 「拡張機能」→「Apps Script」 をクリックし、新しいスクリプトエディタを開きます。

  3. 以下のコードをスクリプトエディタに貼り付けます。

function doGet(e) {
  return HtmlService.createTemplateFromFile('index')
    .evaluate()
    .setTitle('試合管理')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1')
    .setSandboxMode(HtmlService.SandboxMode.IFRAME);
}

// エリアを更新する関数
function updateArea(area) {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var properties = PropertiesService.getScriptProperties();

  // スクリプトプロパティから startTime と lastArea を取得
  var startTime = properties.getProperty('startTime');
  var lastArea = properties.getProperty('lastArea');
  var matchStarted = properties.getProperty('matchStarted');

  // 試合が開始していない場合は何もしない
  if (matchStarted !== 'true') {
    SpreadsheetApp.getUi().alert('試合が開始されていません。開始ボタンを押してください。');
    return;
  }

  // 現在の時間を取得
  var now = new Date().getTime();

  // 前のエリアの滞在時間を計算して記録
  if (startTime && lastArea) {
    var timeSpent = (now - Number(startTime)) / 1000; // 秒単位で計算

    // エリア名とセルアドレスのマッピング
    var areaCellMap = {
      '守備ゾーン': 'B5',
      '守備エリア中央': 'B6',
      '攻撃エリア中央': 'B7',
      '攻撃ゾーン': 'B8',
      'その他': 'B9'
    };

    var cellAddress = areaCellMap[lastArea];
    if (cellAddress) {
      var range = sheet.getRange(cellAddress);
      var currentTime = range.getValue();
      if (currentTime == '') currentTime = 0;
      range.setValue(Number(currentTime) + timeSpent);
    } else {
      // エリア名がマッピングに存在しない場合のエラーハンドリング
      SpreadsheetApp.getUi().alert('エリア名が正しくありません: ' + lastArea);
    }
  }

  // 今回のエリアを新しく開始
  properties.setProperty('startTime', now.toString());
  properties.setProperty('lastArea', area);

  // ボールの現在の位置を表示
  sheet.getRange('B4').setValue('ボールが' + area + 'にあります');
}

// 各エリアの関数
function ballInDefenseZone() {
  updateArea('守備ゾーン');
}

function ballInDefenseMid() {
  updateArea('守備エリア中央');
}

function ballInAttackMid() {
  updateArea('攻撃エリア中央');
}

function ballInAttackZone() {
  updateArea('攻撃ゾーン');
}

function otherTime() {
  updateArea('その他');
}

// 試合開始時の処理
function startMatch() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var now = new Date(); // 現在の日時を取得
  var formattedTime = Utilities.formatDate(now, SpreadsheetApp.getActiveSpreadsheet().getSpreadsheetTimeZone(), 'yyyy/MM/dd HH:mm:ss'); // フォーマット
  sheet.getRange('B10').setValue(formattedTime); // 試合開始時刻を保存

  // 試合開始を記録
  var properties = PropertiesService.getScriptProperties();
  properties.setProperty('matchStarted', 'true');
  properties.setProperty('startTime', now.getTime().toString()); // 試合の開始時間
  properties.deleteProperty('lastArea');  // 前回のエリア情報をリセット
}

// 試合終了時の処理
function endMatch() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var now = new Date(); // 現在の日時を取得
  var formattedTime = Utilities.formatDate(now, SpreadsheetApp.getActiveSpreadsheet().getSpreadsheetTimeZone(), 'yyyy/MM/dd HH:mm:ss'); // フォーマット
  sheet.getRange('B11').setValue(formattedTime); // 試合終了時刻を保存

  var properties = PropertiesService.getScriptProperties();
  var startTime = properties.getProperty('startTime');
  var lastArea = properties.getProperty('lastArea');

  // 最後のエリアの滞在時間を計算して記録
  if (startTime && lastArea) {
    var endTime = now.getTime();
    var timeSpent = (endTime - Number(startTime)) / 1000; // 秒単位で計算

    // エリア名とセルアドレスのマッピング
    var areaCellMap = {
      '守備ゾーン': 'B5',
      '守備エリア中央': 'B6',
      '攻撃エリア中央': 'B7',
      '攻撃ゾーン': 'B8',
      'その他': 'B9'
    };

    var cellAddress = areaCellMap[lastArea];
    if (cellAddress) {
      var range = sheet.getRange(cellAddress);
      var currentTime = range.getValue();
      if (currentTime == '') currentTime = 0;
      range.setValue(Number(currentTime) + timeSpent);
    } else {
      // エリア名がマッピングに存在しない場合のエラーハンドリング
      SpreadsheetApp.getUi().alert('エリア名が正しくありません: ' + lastArea);
    }
  }

  // 各ゾーンのカウントを取得
  var countsRange = sheet.getRange('B5:B9');
  var counts = countsRange.getValues(); // [[値], [値], ...] の形式

  // ゾーン名を取得
  var zonesRange = sheet.getRange('A5:A9');
  var zones = zonesRange.getValues(); // [[名前], [名前], ...] の形式

  // 試合開始時刻と終了時刻を取得
  var matchStartTime = sheet.getRange('B10').getValue();
  var matchEndTime = sheet.getRange('B11').getValue();

  // ログ用のデータを準備
  var logData = [matchStartTime, matchEndTime];
  for (var i = 0; i < counts.length; i++) {
    logData.push(counts[i][0]); // カウント値を追加
  }

  // 「ログ」シートを取得または作成
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var logSheet = spreadsheet.getSheetByName('ログ');
  if (!logSheet) {
    logSheet = spreadsheet.insertSheet('ログ');
    // ヘッダーを設定
    var headers = ['試合開始時刻', '試合終了時刻'];
    for (var i = 0; i < zones.length; i++) {
      headers.push(zones[i][0]);
    }
    logSheet.appendRow(headers);
  }

  // データを「ログ」シートに追加
  logSheet.appendRow(logData);

  // 試合終了後にカウントと日時をリセット
  sheet.getRange('B5:B9').clearContent(); // カウントをリセット
  sheet.getRange('B10:B11').clearContent(); // 試合開始・終了時刻をリセット

  // 試合終了を記録
  properties.setProperty('matchStarted', 'false');
  properties.deleteProperty('startTime'); // カウントをリセット
  properties.deleteProperty('lastArea');  // 最後のエリア情報もリセット

  // ボールの位置情報をクリア
  sheet.getRange('B4').clearContent();
}

コードの主たる部分について解説になります。

1. doGet(e)関数

doGet 関数は、ウェブアプリケーションのエントリーポイントです。HtmlServiceを使用して、指定されたHTMLファイルをテンプレートとして読み込み、レンダリングします。さらに、レスポンスに対して、次のような設定を行っています。

  • setTitle: ウェブアプリのタイトルを「試合管理」に設定します。

  • addMetaTag: メタタグを追加して、ビューポートの幅をデバイスの幅に合わせ、表示の最適化を図っています。

  • setSandboxMode: サンドボックスモードを設定して、HTMLとGAS間の安全な通信を確保します。

2. updateArea(area) 関数

この関数は、ボールがどのエリアにあるかを更新し、前のエリアでボールがどれくらいの時間滞在したかを記録するためのものです。

  • sheet: アクティブなスプレッドシートを取得。

  • properties: スクリプトのプロパティ(試合の状態やエリア情報)を管理するためのオブジェクトです。

主な処理は次の通りです:

  • 試合が開始しているか確認: 試合が開始されていない場合、アラートを表示して処理を終了します。

  • エリアごとの滞在時間を記録: startTime と lastArea を基に、前のエリアでの滞在時間を計算し、それを該当するセルに記録します。

    • エリアごとに対応するセル(例: 「守備ゾーン」は B5)に滞在時間を追加します。

  • 新しいエリアを記録: ボールが新しいエリアに移動した時点で、そのエリア情報と開始時間をスクリプトプロパティに保存します。

3. 各エリアへの関数 (ballInDefenseZone, ballInDefenseMid, etc.)

これらの関数は、ボールが特定のエリア(「守備ゾーン」や「攻撃ゾーン」など)に入ったときに呼び出されます。それぞれ `updateArea` 関数を呼び出して、エリア情報を更新します。

4. 試合開始 (startMatch) 関数

  • 試合の開始時刻を記録: スプレッドシート上に試合の開始時間を保存し、スクリプトプロパティに試合が開始されたことを記録します。

  • プロパティのリセット: 試合の開始に伴い、前のエリアや試合開始時間を初期化します。

5. 試合終了 (endMatch) 関数

  • 滞在時間の最終更新: 最後にボールが滞在していたエリアについても、終了時点の時間で滞在時間を計算して記録します。

  • データのログ保存: スプレッドシートの「ログ」シートに、試合の開始・終了時間と各エリアでの滞在時間を保存します。シートが存在しない場合は、新たに作成されます。

  • 試合データのリセット: 試合が終了した後は、各エリアの滞在時間やボールの位置、試合開始・終了時刻などをクリアし、次の試合の準備をします。

ステップ3:HTMLでユーザーインターフェースを作成しよう

次に、「index.html」 という名前のファイルをスクリプトエディタで新規作成し、以下のコードを貼り付けます。

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    body {
      font-family: Arial, sans-serif;
      text-align: center;
    }
    button {
      width: 80%;
      padding: 15px;
      margin: 10px 0;
      font-size: 18px;
      border: none;
      border-radius: 10px;
      color: white;
      background-color: #f78da7;
      box-shadow: 0 4px #d66f85;
      cursor: pointer;
    }
    button:active {
      box-shadow: 0 2px #d66f85;
      transform: translateY(2px);
    }
    /* ボタンのアクティブ状態のスタイル */
    .active-button {
      background-color: lightblue !important;
      color: black !important;
    }
  </style>
  <script>
    // ボタンのスタイルをリセットする関数
    function resetButtonStyles() {
      document.getElementById('startMatchButton').classList.remove('active-button');
      resetAreaButtonStyles();
    }

    // エリアボタンのスタイルをリセットする関数
    function resetAreaButtonStyles() {
      var buttons = ['defenseZoneButton', 'defenseMidButton', 'attackMidButton', 'attackZoneButton', 'otherButton'];
      buttons.forEach(function(id) {
        document.getElementById(id).classList.remove('active-button');
      });
    }

    // 「試合開始」ボタンを押したときの処理
    function startMatch() {
      resetButtonStyles();
      document.getElementById('startMatchButton').classList.add('active-button');
      google.script.run.startMatch();
    }

    // 「試合終了」ボタンを押したときの処理
    function endMatch() {
      resetButtonStyles();
      google.script.run.endMatch();
    }

    // エリアボタンを押したときの処理
    function areaButtonClicked(areaName, buttonElement) {
      resetAreaButtonStyles();
      buttonElement.classList.add('active-button');

      switch(areaName) {
        case '守備ゾーン':
          google.script.run.ballInDefenseZone();
          break;
        case '守備エリア中央':
          google.script.run.ballInDefenseMid();
          break;
        case '攻撃エリア中央':
          google.script.run.ballInAttackMid();
          break;
        case '攻撃ゾーン':
          google.script.run.ballInAttackZone();
          break;
        case 'その他':
          google.script.run.otherTime();
          break;
      }
    }
  </script>
</head>
<body onload="resetButtonStyles()">
  <h1>試合管理</h1>
  <button id="startMatchButton" onclick="startMatch()">試合開始</button>
  <button id="endMatchButton" onclick="endMatch()">試合終了</button>
  <button id="defenseZoneButton" onclick="areaButtonClicked('守備ゾーン', this)">守備ゾーン</button>
  <button id="defenseMidButton" onclick="areaButtonClicked('守備エリア中央', this)">守備エリア中央</button>
  <button id="attackMidButton" onclick="areaButtonClicked('攻撃エリア中央', this)">攻撃エリア中央</button>
  <button id="attackZoneButton" onclick="areaButtonClicked('攻撃ゾーン', this)">攻撃ゾーン</button>
  <button id="otherButton" onclick="areaButtonClicked('その他', this)">その他</button>
</body>
</html>

コードの主たる部分について解説になります。

1. <style>タグ

この部分では、CSS を使ってボタンのデザインを設定しています。

  • フォントとテキストの整列: body`に対して Arial フォントと中央揃えのテキストを設定。

  • ボタンのスタイル:

    • 幅 80%、パディング 15px で、見やすく大きなボタンを作成。

    • 背景色をピンク系 (`#f78da7`) に設定し、影を使って立体的な効果を出しています。

    • :active 状態(ボタンが押された状態)では影を縮小し、押し込まれたような効果を出しています。

  • アクティブ状態のスタイル (.active-button):

    • ボタンがアクティブ状態の時は、背景色を水色 (lightblue)、文字色を黒に変更するスタイルを強制適用しています。

2. <script>タグ

この部分では、JavaScript を使ってボタンの操作や見た目の変化、Google Apps Script との連携を行っています。

  • resetButtonStyles関数:

    • 全てのボタンのスタイルをリセットし、アクティブ状態を解除します。

  • resetAreaButtonStyles 関数:

    • エリアボタンに関してのみ、アクティブ状態をリセットする関数です。これにより、エリアごとにボタンを押した際に、一つのボタンだけがハイライトされます。

  • startMatch`関数:

    • 「試合開始」ボタンを押すと、この関数が呼び出されます。

    • まず、すべてのボタンのスタイルをリセットし、次に「試合開始」ボタンをアクティブ状態にします。

    • 最後に、`google.script.run.startMatch()` を呼び出して、Google Apps Script 側で `startMatch` 関数が実行されます。

  • endMatch 関数:

    • 「試合終了」ボタンを押した時に呼び出され、ボタンのスタイルをリセットし、Google Apps Script 側の `endMatch` 関数を実行します。

  • areaButtonClicked 関数:

    • どのエリアにボールがあるかを表すボタン(例: 守備ゾーン、攻撃ゾーン)が押された際に呼び出されます。

    • まず、他のエリアボタンのアクティブ状態をリセットし、押されたボタンをアクティブ状態にします。

    • 次に、ボールの位置に応じた処理を実行するため、該当する Google Apps Script 関数を呼び出します。

3. <body> タグ

  • onload="resetButtonStyles()": ページがロードされた際に、全てのボタンのアクティブ状態をリセットします。

  • ボタン: 各ボタンは試合の操作やエリア変更を行うために設置されています。ボタンをクリックすると、それぞれの JavaScript 関数が実行され、Google Apps Script 側の対応する処理が呼び出されます。

ボタンの役割

  • 試合開始ボタン (startMatchButton): 試合を開始し、試合開始時刻を記録します。

  • 試合終了ボタン (endMatchButton): 試合を終了し、試合終了時刻とエリア滞在時間を記録します。

  • エリアボタン: それぞれのボタンをクリックすると、ボールの位置が対応するエリアに移動したことを記録し、スプレッドシートに反映されます。

ステップ4:Webアプリとしてデプロイしよう!

ここまで設定ができたら、次はWebアプリとしてデプロイします。これにより、スマホからでも簡単に操作できるシステムになります。

Webアプリのデプロイ手順

  1. 「デプロイ」→「新しいデプロイ」 をクリックします。

  2. デプロイタイプで 「ウェブアプリ」 を選択します。

  3. 「プロジェクトのバージョン」 でバージョンを作成します(「バージョン1」など)。

  4. 「WebアプリのURL」 が表示されるので、それをクリックしてアクセスします。

これで、スマホやPCからアクセスできるエリア分析システムが完成しました。あとは実際に試合中に使って、どのエリアにボールが長く滞在しているかを記録するだけです。




まとめ

この手順を使えば、簡単にサッカーのエリア分析システムを作成できます。Google スプレッドシートとGoogle Apps Scriptの組み合わせで、特別なツールやプログラミング知識がなくても、サッカーの戦術分析を効率よく進められるようになります。

ぜひこのシステムを使って、サッカーの試合をもっと楽しく、戦略的に分析してみてください。


いいなと思ったら応援しよう!