見出し画像

インベーダーたたき kintone

kintone カスタマイズに興味がある初心者向けコミュニティ、imoniCamp(いもキャン)というのがあり、私も参加しています。

そこで、参加者によるJSカスタマイズに関するLT大会を開催するとことになりました。さてなにしようかな。うーん。

まー私はそんなすごい技術もないので、イロモノでいくか。となるとゲームだな。

ゲーム概要

10匹のインベーダーを撃破し君は地球を救えるか!

シューティングゲームを期待した方ごめんなさい。タイトーさんの名作「スペースインベーダー」ではなく、いわゆる「もぐらたたき」です。絵文字にもぐらがなかったのでインベーダーにしました。

かわいいインベーダーのアイコンは、DOTOWNさん。ありがとうございます。

動画ではこんな感じ

あそびかたは一目瞭然ですね😅。合計10匹のインベーダーが1秒ごとに現れるのでマウスで撃破してください!

kintoneアプリ解説

kintoneのフォームはこんな感じ。

縦4x横4、計16のスペースフィールドを用意
要素ID spc0~spcF 

数値フィールドで、スコア、ハイスコア、カウントを用意
フィールド名とフィールドコードは同じ
初期値は、スコア 0、ハイスコア 0、カウント -3

まあみたまんまです。

あとはJSカスタマイズで用意したスペースフィールドにボタンを設置。
ボタンにインベーダーをランダムに表示してマウスでたたくという仕組みです。

JSカスタマイズの参考にしたのは@juri_donさんの2つのQiita。@juri_donさん、有効な情報ありがとうございます。

kintoneのJSカスタマイズをググると、だいたい@juri_donさんの記事にたどりつきますね。今回、私はアロー関数をはじめてマトモに使ってみました。

【kintone】アプリの「スペース」フィールドにボタンを設置する

スペースフィールドにボタンを設置する方法はこれでOK!

【kintone】ボタンをクリックしてフィールドの値を書き換えてみよう!

そうなんです。スコアなどフィールドの値を書き換えるには、getsetで更新する必要があって、おそらくコレ処理的には重い処理だと思います。業務システムでは通常起こらない頻度の画面更新をゲームではすることになってしまうので、ちょっとkintoneには負担をかけてしまいますね。🙏

        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);

詳しくは以下
「kintone開発前によくある質問」に解説されています。https://developer.cybozu.io/hc/ja/articles/213504206

4. データベースへの直接接続は不可
kintone のデータ操作には基本的に kintone REST API を使用します。
kintone の画面上であれば kintone JavaScript API を利用して一部データの値を取得することも可能です。
上記の2パターンがデータ操作の方法となり、一般的なデータベースのようなデータベースへの直接接続はできません。

そのため、操作対象のデータが多かったり、API を使って実現する処理が複雑な場合には、API リクエストの都度発生するオーバーヘッドを考慮する必要があります。

ではでは次からkintoneでゲームを作る際のポイントを説明します。

乱数

業務プログラムではあまり必要にはなりませんが、ゲームプログラムには必須の要素。乱数です。敵の攻撃パターンやサイコロの出目がやるたびに同じだと面白くないですよね。ので、コンピューターにデタラメな数値を発生してもらいます。

実は指定通りに同じ処理を繰り返すのが得意なコンピューターにとって乱数は結構難しい処理なのです。さらには厳密な乱数の定義って難しくて、実際ゲームでは疑似乱数なるものを使ったりします。まあなんとなくそんなもんだという事で。私もそれ以上よくわかりません。

で、kintoneで乱数を利用した例はないかとググったらcybozu developer networkにありました!ありがとうございます。

kintoneアプリで抽選カスタマイズhttps://developer.cybozu.io/hc/ja/articles/360000073743

ランダムに抽選を行うロジックの作成
Math.random()関数Math.floor()関数を用いて、乱数を作成して抽選を行います。

Math.random()関数、これが乱数の元です。

Math.random()関数は、 0 以上 1 未満 (0 は含むが、 1 は含まない) の範囲で浮動小数点の擬似乱数を返します。

でも、ランダムな数値が 0以上1未満の範囲では、例えばサイコロは作れないですよね。ではどうするか。小数点以下の乱数を倍率として使うのです。
乱数の結果が0.5だと

0.5 * 6 = 3

これで、0~5の6種類の乱数が取得できます。

上記の例だと答えがちょうど整数で出ますが、小数点以下が出る場合もあるので、ここを細工します。Math.floor()関数、まあ小数点以下の切り捨てですね。

Math.floor() 関数は与えられた数値以下の最大の整数を返します。

で、サイコロの場合だと以下でOK

const rand = Math.floor(Math.random() * 6) + 1;

0~5のランダムな数値ができるので、それに1を足して1~6の目がでるサイコロの完成です!

当たり判定

こちらもゲームでは定番の処理。

当たり判定を検出するロジックはゲームによりさまざま。シューティングゲームなどで使ってるオーソドックスな判定方法としては座標を用いるのですが、今回はインベーダーへのヒットをマウスクリックで判定するので、押したボタンがインベーダー👾なら当たりとしました。

    btn0.onclick = () =>  {
      if (btn0.textContent === '👾' || btn0.textContent === '💥') {
        btn0.textContent='💥';

あれ?インベーダー👾かどうかだけでなく爆発💥も当たりとしてますね。
ふふ、これがまさに「インベーダーたたき」のゲーム性をアップしている工夫です!

そう、インベーダーは1秒間現れるのですが、その間たたき続ける(連射する)と、その分得点が加算されるのです。目指せ16連射!

インターバル処理

こちらもゲームには必須。仕事のkintoneアプリはなにもしなければなにも変わらないのがフツーですが、アクションゲームの場合はリアルタイムに画面が変わってもらいたい!それにはwindow.setInterval()関数を使います。

timer = window.setInterval(

ココの詳細は以前note記事に書きましたので参考にしてください。

今回はこれで1秒ごとにインベーダーが出現するようにしてみました。
以上、kintoneでアクションゲームを作るときに必要な要素

・乱数
・当たり判定
・インターバル処理

のさらっと解説おわります。

その他

あと細かい所では、ハイスコア時にはおめでとうメッセージをだしたり、リスタート時3秒ウェイトするようカウントを-3にしてたり、まあちょっとした工夫はしてますが、たいしたことはしてません。いつもの「すごくなくてもいい」です。

そもそもkintoneはゲーム開発ツールではないので(当たり前)、ゲームを作るにあたっては配列処理や座標処理、画面の書き換え処理などをkintoneの機能でどうしようか考えないといけないのですが、そこを工夫するところが面白い所。

とりあえずJSカスタマイズの技術的な話はともかく、遊んでもらってちょっとでも面白いと思ってもらえたらうれしいです。以下にアプリテンプレートとJSのコードをアップします。

(() => {
  'use strict';

  kintone.events.on(['app.record.create.show',
                     'app.record.edit.show'
                    ],(event) => {

    //ボタン用スペース作成
    const spc0 = kintone.app.record.getSpaceElement('spc0');
    const spc1 = kintone.app.record.getSpaceElement('spc1');
    const spc2 = kintone.app.record.getSpaceElement('spc2');
    const spc3 = kintone.app.record.getSpaceElement('spc3');
    const spc4 = kintone.app.record.getSpaceElement('spc4');
    const spc5 = kintone.app.record.getSpaceElement('spc5');
    const spc6 = kintone.app.record.getSpaceElement('spc6');
    const spc7 = kintone.app.record.getSpaceElement('spc7');
    const spc8 = kintone.app.record.getSpaceElement('spc8');
    const spc9 = kintone.app.record.getSpaceElement('spc9');
    const spcA = kintone.app.record.getSpaceElement('spcA');
    const spcB = kintone.app.record.getSpaceElement('spcB');
    const spcC = kintone.app.record.getSpaceElement('spcC');
    const spcD = kintone.app.record.getSpaceElement('spcD');
    const spcE = kintone.app.record.getSpaceElement('spcE');
    const spcF = kintone.app.record.getSpaceElement('spcF');

    //ボタン作成
    const btn0 = document.createElement('button');
    const btn1 = document.createElement('button');
    const btn2 = document.createElement('button');
    const btn3 = document.createElement('button');
    const btn4 = document.createElement('button');
    const btn5 = document.createElement('button');
    const btn6 = document.createElement('button');
    const btn7 = document.createElement('button');
    const btn8 = document.createElement('button');
    const btn9 = document.createElement('button');
    const btnA = document.createElement('button');
    const btnB = document.createElement('button');
    const btnC = document.createElement('button');
    const btnD = document.createElement('button');
    const btnE = document.createElement('button');
    const btnF = document.createElement('button');

    //ボタン表示初期化
    btn0.textContent=' ';
    btn1.textContent=' ';
    btn2.textContent=' ';
    btn3.textContent=' ';
    btn4.textContent=' ';
    btn5.textContent=' ';
    btn6.textContent=' ';
    btn7.textContent=' ';
    btn8.textContent=' ';
    btn9.textContent=' ';
    btnA.textContent=' ';
    btnB.textContent=' ';
    btnC.textContent=' ';
    btnD.textContent=' ';
    btnE.textContent=' ';
    btnF.textContent=' ';

    //スペースにボタン追加
    spc0.appendChild(btn0);
    spc1.appendChild(btn1);
    spc2.appendChild(btn2);
    spc3.appendChild(btn3);
    spc4.appendChild(btn4);
    spc5.appendChild(btn5);
    spc6.appendChild(btn6);
    spc7.appendChild(btn7);
    spc8.appendChild(btn8);
    spc9.appendChild(btn9);
    spcA.appendChild(btnA);
    spcB.appendChild(btnB);
    spcC.appendChild(btnC);
    spcD.appendChild(btnD);
    spcE.appendChild(btnE);
    spcF.appendChild(btnF);

    //インターバル処理
    const timer = window.setInterval(() => {

      //カウント加算
      const rec = kintone.app.record.get();
      rec.record['カウント'].value = parseInt(rec.record['カウント'].value,10) + 1;
      kintone.app.record.set(rec);

      //ゲーム終了処理
      if (rec.record['カウント'].value > 10) {

        //スコア・ハイスコア取得
        const rec = kintone.app.record.get();
        const sc = parseInt(rec.record['スコア'].value, 10);
        const hisc = parseInt(rec.record['ハイスコア'].value, 10);

        //ゲーム終了メッセージ処理
        if (sc > hisc) {
          rec.record['ハイスコア'].value = rec.record['スコア'].value;
          window.alert('おめでとう!ハイスコア ' + rec.record['ハイスコア'].value);
        } else {
          window.alert('ゲームオーバー ' + rec.record['スコア'].value);
        }

        //スコア・カウント初期化
        rec.record['スコア'].value = 0;
        rec.record['カウント'].value = -3;
        kintone.app.record.set(rec);
      }

      //ボタン表示初期化
      btn0.textContent=' ';
      btn1.textContent=' ';
      btn2.textContent=' ';
      btn3.textContent=' ';
      btn4.textContent=' ';
      btn5.textContent=' ';
      btn6.textContent=' ';
      btn7.textContent=' ';
      btn8.textContent=' ';
      btn9.textContent=' ';
      btnA.textContent=' ';
      btnB.textContent=' ';
      btnC.textContent=' ';
      btnD.textContent=' ';
      btnE.textContent=' ';
      btnF.textContent=' ';

      //疑似乱数発生
      let rand = Math.floor(Math.random() * 16);

      //開始ウエイト処理
      if (rec.record['カウント'].value <= 0) {
        rand = -1;
      }

      //インベーダー出現
      switch (rand) {
        case 0:
          btn0.textContent='👾';
          break;
        case 1:            
          btn1.textContent='👾';
          break;
        case 2:            
          btn2.textContent='👾';
          break;
        case 3:            
          btn3.textContent='👾';
          break;
        case 4:            
          btn4.textContent='👾';
          break;
        case 5:            
          btn5.textContent='👾';
          break;
        case 6:            
          btn6.textContent='👾';
          break;
        case 7:            
          btn7.textContent='👾';
          break;
        case 8:            
          btn8.textContent='👾';
          break;
        case 9:            
          btn9.textContent='👾';
          break;
        case 10:            
          btnA.textContent='👾';
          break;
        case 11:            
          btnB.textContent='👾';
          break;
        case 12:            
          btnC.textContent='👾';
          break;
        case 13:            
          btnD.textContent='👾';
          break;
        case 14:            
          btnE.textContent='👾';
          break;
        case 15:            
          btnF.textContent='👾';
          break;
        case -1:      //ウエイト
          break;
        default:
          window.alert(rand);
        }  
      }
    ,1000); //1秒ごと更新

    //当たり判定
    btn0.onclick = () =>  {
      if (btn0.textContent === '👾' || btn0.textContent === '💥') {
        btn0.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    btn1.onclick = () =>  {
      if (btn1.textContent === '👾' || btn1.textContent === '💥') {
        btn1.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    btn2.onclick = () =>  {
      if (btn2.textContent === '👾' || btn2.textContent === '💥') {
        btn2.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    btn3.onclick = () =>  {
      if (btn3.textContent === '👾' || btn3.textContent === '💥') {
        btn3.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    btn4.onclick = () =>  {
      if (btn4.textContent === '👾' || btn4.textContent === '💥') {
        btn4.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    btn5.onclick = () =>  {
      if (btn5.textContent === '👾' || btn5.textContent === '💥') {
        btn5.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    btn6.onclick = () =>  {
      if (btn6.textContent === '👾' || btn6.textContent === '💥') {
        btn6.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    btn7.onclick = () =>  {
      if (btn7.textContent === '👾' || btn7.textContent === '💥') {
        btn7.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    btn8.onclick = () =>  {
      if (btn8.textContent === '👾' || btn8.textContent === '💥') {
        btn8.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    btn9.onclick = () =>  {
      if (btn9.textContent === '👾' || btn9.textContent === '💥') {
        btn9.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    btnA.onclick = () =>  {
      if (btnA.textContent === '👾' || btnA.textContent === '💥') {
        btnA.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    btnB.onclick = () =>  {
      if (btnB.textContent === '👾' || btnB.textContent === '💥') {
        btnB.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    btnC.onclick = () =>  {
      if (btnC.textContent === '👾' || btnC.textContent === '💥') {
        btnC.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    btnD.onclick = () =>  {
      if (btnD.textContent === '👾' || btnD.textContent === '💥') {
        btnD.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    btnE.onclick = () =>  {
      if (btnE.textContent === '👾' || btnE.textContent === '💥') {
        btnE.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    btnF.onclick = () =>  {
      if (btnF.textContent === '👾' || btnF.textContent === '💥') {
        btnF.textContent='💥';
        const rec = kintone.app.record.get();
        rec.record['スコア'].value = parseInt(rec.record['スコア'].value,10) + 1;
        kintone.app.record.set(rec);
      }
    };

    return event;
  });  

})();

この記事が気に入ったらサポートをしてみませんか?