zawazawa雑記

【Unity】始めました

遅ればせながら感がすごい

VR/AR方面の研究や活動をしようとするとやっぱり必要になってくるというか、便利になってくるものがUnityですよね。

自分も今更ながらやり始めました。お手柔らかに。

参考書ベースでやります

 これを読み進めながらやっていきます。

Unity5 3D/2Dゲーム開発実践入門 作りながら覚えるスマートフォンゲーム開発

Unity5 3D/2Dゲーム開発実践入門 作りながら覚えるスマートフォンゲーム開発

 

 

やったこと

今回はこの参考書のChap3に書かれている内容を元にゲームをUnityで作成し、実際に実機(iPhoneX)で動かしてみるところまでやりました。

 

実機に転送する

踏まなければいけない手順がいくつかあります。まずビルドの流れですが、

Unityでビルド(プラットフォームをiPhoneにswitchした上で) => ビルドして作成されたフォルダからXcodeプロジェクトを開く => Xcodeから実機に移す

 

この際に困ったことを数点挙げます

 

・エラー箇所

ビルドしたプロジェクトをXcodeで開くとこのようなエラーが出ました。

f:id:zawazawahtn:20180530121025p:plain

 

自分の場合、これはiTunesを開いていたので発生してしまいました。

 

おしまーい

だいぶ雑な内容を書いてしまった。反省。

それにしてもこの参考書は結構役に立って、最低限必要な知識は付けさせてくれるように思います。

次回は面白い内容なので文章の方も頑張って書きます(いつも途中からめんどくさくなってしまう)。

 

【論文紹介】すごいと思った機械学習の論文

概要

Neural Best-Buddies- Sparse Cross-Domain Correspondence

著者:Kfir Aberman, Jing Liao, Mingyi Shi, Dani Lischinski, Baoquan Chen, Daniel Cohen-Or

こちらの論文を読みました。 これは、cross-domainな2つの画像のマッチするポイントを高精度に導出してくれる手法です。 たとえ入力した2つの画像同士の詳細な形や色、姿勢が違っていても、下の図のように対応点を示してくれます。

f:id:zawazawahtn:20180601185356p:plain

アルゴリズム

ざっくり言うと、下の図(a)の左下のような"深い"層から、2つの画像の"意味的"に一致する箇所(仮にpとする)を導出します。それを"浅い"層に持ち上げていくに従って点から面に範囲の広がったpに対してまた更に一致する箇所を導出していく、といった感じです。浅い層に行けば行くほど”意味的”な一致からエッジやコーナーといった"形式的"なシフトする、みたいな記述もありました。

f:id:zawazawahtn:20180601190529p:plain

すごいと感じた点

単純に、これだけの精度が上がると実現する社会実装の数が(image morphingの分野において)前例がないだろうと思いました。要するにこの技術に触れるユーザー数的な観点でインパクトが大きい、という意味です。

image morphingやHybridizationとcross-domain correspndenceは深く関わり合っています。以下に論文中で紹介されていたアプリケーションを示します。 画像全体のmorphingはもちろん、画像間の対応点も高精度に導出できているので、任意の範囲でのmorphingも可能になっています。

f:id:zawazawahtn:20180601191939p:plain

【VRChat】始めました

なんか流行ってるっぽい

f:id:zawazawahtn:20180531134831j:plain:w100

VRChatが巷(少なくとも自分のSNSではその流れが来てる)で流行ってるっぽいので自分も始めました(まだ遅くないはず...)。

そもそもVRChatとは

自分の知ってる限りで記述すると

  • ジャンルとしてはメタバース(meta + universe)と呼ばれるものらしいです

  • 自分はアバターになってWorldと呼ばれる仮想空間上で行動し、あれやこれやするといったコミュニケーションプラットフォームです

  • 要するに"Second Life"的なものです、やったことないから知らないけれど

参考にさせていただいたサイト

VRChat 日本wiki

こんな感じでアバターが作成されます。画像はUnityちゃん f:id:zawazawahtn:20180531140747j:plain

世界中の人がいるthe Hubと呼ばれるworldです。コミュニケーションはVoiceのみでtextベースのやり取りはできないみたいでした。 ちなみに自分はここでデッドプールにりんご食べさせてもらったり、虹色ミクにひたすらハート投げつけてたりしました(友達になりたかった)。 f:id:zawazawahtn:20180531140752j:plain

おわりに

今回は単に使ってみたってだけの記事でした。次は

  • 3Dモデルを実際に作る

  • HMDをかぶった上で遊んでみる(今回はPCディスプレイで遊んだ)

をやってみようと思います。

ちなみに某フリマアプリの研究開発?部門がVRChat上での採用面接活動を開始したみたいですよ笑

エンジニア向け2018夏インターンまとめ

神まとめです

大学の知り合いからこんなものをもらったので、せっかくなので共有・発信しておきます。(おそらくネットに載せて大丈夫だと思われる)

以下のGoogleスプレッドシートを参考に↓

docs.google.com

みなさん良い夏を。

[WordPress]MAMPで無料テンプレートを使ったHPを作ってみた

2018年の3分の1が終わりました

こんにちは、もう5月です。

機会があったので今更ながら、Wordpressのテンプレを使ったHPを作ってみました。 結論から言うと、コツさえ掴めば割と簡単にHP作れて良さげだな、って感じです。

そしてやはりHPを管理するにあたって、やはりローカル環境を構築したくなりますよね。そこで今回はMAMPを使ってみました。 MAMPはwebサーバを用いずに、自分のPCを擬似的なサーバとしてローカルでwordpressを使えるようにするやつです。 基本的に参考サイトの流れで良いが、詰まったところを記述します。

参考にしたサイト

MAMPを使ってローカル環境にWordPressをインストールする方法

ローカルで管理するのに必要なツール

MAMPの構築

f:id:zawazawahtn:20180501173355p:plain:w100

基本的に参考サイトの流れで大丈夫ですが、詰まったところとか差異のあった箇所を記述します。

  • おそらくverの違いで、preferenceから開いて設定します
  • ポート設定の時に、8888を使ったら「既に使ってるからダメだよ」と言われたので「ApacheMySQL のポートを 80 と 3306 に設定」を選択
  • document rootはデフォルトだとアプリケーション>MAMP>htdocsになりますが、githubで管理させたかったので、gitと連携させたディレクトリのwordpressを格納するディレクトリを用意し、選択

wordpressをローカルレポジトリで展開

document rootに設定したディレクトリにwordpressのファイルを全コピペする。 MAMPからサーバを起動させるとDB設定に移るので、適当な名前のDBを作成する。

Templateをwordpress内に配置

wp-content/themes/内に選択したテンプレのフォルダを設置

最終的にこのようなディレクトリ構成になりました f:id:zawazawahtn:20180501171752p:plain:w300

おわりに

以上でサクッと終わらせます。

HTMLとかCSSとかの基本的なwebコーディングの知識さえあればテンプレートのphpファイルもいじれます。

某大手総合コンサルのIT部門の人にOB訪問してみた

さて、雑記です。

先日、機会があったのでITコンサルタント(Tさん)のお仕事をしている方に連絡をとってOB訪問させていただきました。OB訪問1人目です。 そこで話したあれやこれやについて書きます。

おそろしく雑なOB訪問申請

f:id:zawazawahtn:20180425164634j:plain:w350

コンサルことはじめ

そもそも自分は「コンサルって何やるの?そもそも必要なの?」というところから入ったド初心者でした(ここでコンサルタントに殺される)。 そしてこのバカ失礼な質問をしてしまいました。そこでTさんはこのバカ失礼な質問に嫌な顔一つせず答えてくれました。

コンサルが必要な理由は3つある。まず、クライアントに時間がなくて、時間をお金で買う場合。次に、クライアントにそもそも知識がない場合。最後に...

3つ目忘れちゃいました。とここで、私はまたもバカ失礼に「なるほど。ってことは2つ目に関しては、ググればわかることをわざわざお金かけて人に頼んでるってことは、クライアントは自分より頭悪いってことでしょ?一緒に仕事してて楽しい?」という爆弾を投下しました(ここでクライアントからも殺される)。

これに対して、

クライアントは自分のドメインに特化していうる反面、外部からの視点というものが不足している。戦略とか新規事業提案とか、ドメインに凝り固まった脳ではそれを絞り出せない。それを補うためにコンサルがいる。

とのこと。なるほどなあ。さらに

コンサル側は逆にドメインについて詳しくないから依頼を受けたら必死でリサーチする。業務のうちの1割が会議、1割が資料作り、8割がGoogle検索だよ。

ともおっしゃっていました。もはやGoogle社員より検索回数多そうだな...

コンサルの種類

無知な私もこれでコンサルの実態が掴めてきました。次に気になるのが、その種類です。 そもそも、コンサル会社と呼ばれる会社は3つに分けられるとのことです。それは

  • 戦略コンサル
  • 総合コンサル
  • 専門コンサル

です。確かに**戦略コンサルとか**総合なんちゃらとか聞いたことある。というのも、コンサルは

戦略 -> 計画 -> 実行 -> 運用

という流れで業務が発生していて、戦略部分のみを行う会社が戦略コンサル、一連の流れを包括的に行う会社が総合コンサルって感じだそうです。

雑談

あとはまあ雑談です(疲れてきた)。

  • 攻めのITと守りのITがあること
  • 年収は最初は500くらいで30代では1000超えること
  • メディアは読んでおくこと
  • 事業会社のやってることが羨ましい
  • コンサルでもコーディングする部署はあること

とかとか。最後の方雑になっちゃいました。

とりあえず、美味しいご飯をありがとうございました!!!

次は誰にOB訪問しようかな!!!

【LINEbot】google apps scriptにFusionTablesを連携させて予定調整してもらった

モチベーション

大学のサークルの合宿の予定調整がクソダルいんすよね

というのも、自分はバンド系のサークルに入っていて、合宿にいくと0時~24時までを24区切りにして1バンド1時間を1単位にして練習を組むんですよ。 その時に10~18バンド組むとそれの予定調整が同時進行で行われて情報の更新作業をこまめに行わないといけない上に、バンドメンバー間の予定調整も必要になってきて、てんやわんや。

ということで、バンドグループラインに予定を一括で管理してくれるbotがいたら嬉しいな、っていうところから入りました。

githubにも上げてます。

友達追加はこちらか↓のQRコードから

機能

  • 予定を確認する
  • 予定を作成する
  • 予定を削除する
  • グループ内で予定を調整する(今のところこれは未実装)

「イベマネさん」がトリガー

f:id:zawazawahtn:20180421161047j:plain:w300

予定作成

f:id:zawazawahtn:20180421153126j:plain:w300

datetimepickerはこんな感じ

f:id:zawazawahtn:20180421153740j:plain:w300

さっき作成した予定の確認

f:id:zawazawahtn:20180421161207j:plain:w300

予定が12件ある場合の予定を削除

f:id:zawazawahtn:20180421153831j:plain:w300

コード

リファクタとか条件分岐とか思いついたまま書いたのでクソ雑な長いコードになってます。スクリプトのハイライトとかされないようなので、Atomとかに.gsファイルを作ってつっこんでください。

// LINEの認証を突破するために必要なお作法
// botのChannel基本設定の画面で発行した鬼のように長い文字列を""の中にセット
var secret_token = "******************"
var secret = "Bearer " + secret_token;
var docid = "***************";

function doPost(e) {
  try {
    handleMessage(e);
  } catch(error) {
    var postData = {
      "replyToken": token,
      "messages": [{
        "type": "text",
        "text": error.message
      }]
    };
    fetchData(postData);
  }
}

function handleMessage(e) {
  // LINEから送信されたデータを取得(テキストメッセージそのものではない。)
  var json = e.postData.getDataAsString();
  var json_content = JSON.parse(e.postData.contents);

  // LINEから送信されてきたデータから、リプライトークン(返信するために必要)を取得
  var token = JSON.parse(json).events[0].replyToken;

  //ユーザーIDの取得
  var userid = JSON.parse(json).events[0].source.userId;
  var username = getUsername(userid);

  //イベントタイプの取得
  var type = JSON.parse(json).events[0].type;

  //キャッシュの取得
  var cache = CacheService.getScriptCache();
  var eventseq = cache.get("eventseq");

  //キャッシュのクリアの動作
  if (type === "message") {
    if (JSON.parse(json).events[0].message.text === "キャンセル") {
      cache.remove("eventseq");
      var postData = {
      "replyToken": token,
      "messages": [{
        "type": "text",
        "text": "キャッシュをクリアしました"
      }]
    };
    fetchData(postData);
    }
  }

  //イベント作成・キャッシュで分岐
  if (eventseq === '1' || eventseq === '2' || eventseq === '3') {
    if (eventseq === '1' && type === "message") {
      cache.put("eventseq", 2);
    } else if (eventseq === '2' && type === "postback") {
      cache.put("eventseq", 3 );
    } else if (eventseq === '3'&& type === "postback") {
      if (JSON.parse(json).events[0].postback.data === "confirm") {
        //キャッシュからイベントをFusionTbalesに書き込み
        writeEvent(cache, userid, token);
        cache.remove("eventseq");
        var postData = {
          "replyToken": token,
          "messages": [{
            "type": "text",
            "text": "作成しました"
          }]
        };
        fetchData(postData);
      } else if (JSON.parse(json).events[0].postback.data === "cancel") {
        var postData = {
          "replyToken": token,
          "messages": [{
            "type": "text",
            "text": "キャンセルしました"
          }]
        };
        cache.remove("eventseq");
        fetchData(postData);
      }
    }
    createEvent(eventseq, cache, token, json);

  } else if (type==='postback' ) {
    var data = JSON.parse(json).events[0].postback.data;
    if (data.match("action=")) {
      //予定の確認、作成、削除へ
      post_back(json, token, username, userid, cache);
    } else if (data.match("delete=confirm=")) {
      //予定の削除へ
      DeleteFromTable(userid, token, data, cache);
    } else if (data.match("add_digit")) {
      deleteEvent(userid, token, cache, data);
    }
  } else {
    // 送信されてきたテキストを取り出し
    if (type === 'message') {
      var text = JSON.parse(json).events[0].message.text;
      //ユーザーくんがトリガー
      if (text==='イベマネさん') {
        userChoose(username, token);
      }
    }
  }
}

function userChoose(username, token) {
  var postData = {
    "replyToken": token,
    "messages": [{
      "type": "template",
      "altText": "イベマネです",
      "template": {
        "type": "buttons",
        "thumbnailImageUrl": "https://www.pakutaso.com/shared/img/thumb/SAYA160312370I9A3675_TP_V.jpg",
        "title": "予定を調整させていただきます",
        "text": username + "さんは何をしたいですか??",
        "actions": [{
            "type": "postback",
            "label": "予定の確認",
            "data": "action=lookup"
            //"text": "押しました"
         },
         {
           "type": "postback",
           "label": "予定の作成",
           "data": "action=create"
         },
         {
           "type": "postback",
           "label": "予定の削除",
           "data": "action=delete"
         },
         {
           "type": "postback",
           "label": "メンバー内の予定の調整",
           "data": "action=organize"
         }
       ]
     }
   }]
 };
 fetchData(postData);
}

function post_back(json, token, username, userid, cache) {
  //ユーザーがどのアクションをしたのか判別する
  var data = JSON.parse(json).events[0].postback.data;

  if (data === 'action=lookup') {

    var messages = getEvent(userid);

  } else if (data === 'action=create') {

    cache.put("eventseq", 1);
    var messages  = [{
      "type": "text",
      "text": "イベントの名前を入力してください"
    }]

  } else if (data === 'action=delete') {

    deleteEvent(userid, token, cache, data);

  } else if (data === 'action=organize') {
    var messages  = [{
      "type": "text",
      "text": "この機能はまだ実装されてないよ"
    }]
  }
  var postData = {
    "replyToken": token,
    "messages": messages
  };
  fetchData(postData);
}

function getEvent(userid) {
  var username = getUsername(userid);
  var message = username + 'さんの予定はこちらです';
  var user_id = userid;
  var sql_filter = "select * from "+ docid + " where Userid = '"+ user_id + "';";
  var res_filter = FusionTables.Query.sql(sql_filter);
  var result = '';
  if (typeof res_filter.rows === "undefined" ) {
    var messages = [{
      "type": "text",
      "text": "まだイベントが作成されていません"
    }]
  } else {
  for (var i = 0; i < res_filter.rows.length; i++){
    var starttime = res_filter.rows[i][2].split("T")[1];
    var endtime = res_filter.rows[i][3].split("T")[1];
    var startdate = res_filter.rows[i][2].split("T")[0].slice(5);
    var enddate = res_filter.rows[i][3].split("T")[0].slice(5);
    if (startdate !== enddate) {
      result += "\n" + res_filter.rows[i][1] + " " + startdate + " " + starttime + "〜" + enddate + ":" + endtime ;
    } else if (startdate === enddate) {
      result += "\n" + res_filter.rows[i][1] + " " + startdate + " " + starttime + "〜" + endtime ;
    }
  }
  var messages = [{
    "type": "text",
    "text": message + result
  }]
  }
  return messages;
}

function createEvent(eventseq, cache, token, json) {
      switch(eventseq) {
        case "1":
          var eventname = JSON.parse(json).events[0].message.text;
          var text = "開始時間を入力してください";
          var postData = {
            "replyToken": token,
            "messages": [{
              "type": "template",
              "altText": "開始時間",
              "template": {
                "type": "buttons",
                "title": "予定を作成させていただきます",
                "text": "開始時間を選んでください",
                "actions": [
                  {
                    "type":"datetimepicker",
                    "label":"開始時刻を選んでください",
                    "data":"endtime",
                    "mode":"datetime",
                    "max":"2020-03-31t23:59",
                    "min":"2018-04-01t00:00"
                  }
                ]
              }
           }]
          };
          cache.put("eventname", eventname);
          fetchData(postData);
          break;
        case "2":
          //開始時間の処理
          var starttime = JSON.parse(json).events[0].postback.params['datetime'];
          var text = "終了時間を入力してください";
          var postData = {
            "replyToken": token,
            "messages": [
              //{
              //  "type": "text",
              //  "text": starttime + "開始ですね?"
              //},
              {
              "type": "template",
              "altText": "終了時間",
              "template": {
                "type": "buttons",
                "title": "予定を作成させていただきます",
                "text": "次に終了時刻を選んでください",
                "actions": [
                  {
                    "type":"datetimepicker",
                    "label":"終了時刻を選んでください",
                    "data":"endtime",
                    "mode":"datetime",
                    "max":"2020-03-31t23:59",
                    "min":"2018-04-01t00:00"
                  }
                ]
              }
           }]
          };
          cache.put("starttime", starttime);
          fetchData(postData);
          break;
        case "3":
          var eventname = cache.get("eventname");
          var starttime = cache.get("starttime").split("T")[1];
          var endtime = JSON.parse(json).events[0].postback.params['datetime'].split("T")[1];
          var startdate = cache.get("starttime").split("T")[0].slice(5);
          var enddate = JSON.parse(json).events[0].postback.params['datetime'].split("T")[0].slice(5);
          cache.put("endtime", JSON.parse(json).events[0].postback.params['datetime']);
          var postData = {
            "replyToken": token,
            "messages": [
              {
              "type": "template",
              //"thumbnailImageUrl": "https://www.pakutaso.com/shared/img/thumb/SAYA072160011_TP_V.jpg",
              "altText": "確認/キャンセル",
              "template": {
                "type": "confirm",
                "text": eventname + " " + startdate + " " +  starttime + "~" + enddate + " " + endtime + "で確定させますか?",
                "actions": [
                  {
                    "type": "postback",
                    "label": "確定",
                    "data": "confirm",
                    "text": "確定"
                  },
                  {
                    "type": "postback",
                    "label": "キャンセル",
                    "data": "cancel",
                    "text": "キャンセル"
                  }
                ]
              }
            }]
          };
          fetchData(postData);
          break;
      }
}

function deleteEvent(userid, token, cache, data) {
  //どのイベントを削除しますか
  var username = getUsername(userid);
  var sql_filter = "select * from "+ docid + " where Userid = '"+ userid + "';";
  var sql_filter_getid = "select ROWID from "+ docid + " where Userid = '"+ userid + "';";
  var res_filter = FusionTables.Query.sql(sql_filter);
  var res_filter_getid = FusionTables.Query.sql(sql_filter_getid);
  var column = [];
  if (data.match("add_digit")){
    cache.put("event_digit", Number(cache.get("event_digit")) + 1);
  } else {
    cache.put("event_digit", 1);
  }
  var digit = Number(cache.get("event_digit"));

  if (typeof res_filter.rows === "undefined") {
    var postData = {
      "replyToken": token,
      "messages": [{
        "type": "text",
        "text": "まだイベントが作成されていません"
      }]
    };
    fetchData(postData);
  }

  if (res_filter.rows.length - (digit-1)*9 <= 10) {
    for (var i = (digit-1)*9; i < res_filter.rows.length; i++){
      var rowid = res_filter_getid.rows[i]
      var eventname = res_filter.rows[i][1];
      var starttime = res_filter.rows[i][2].split("T")[1];
      var endtime = res_filter.rows[i][3].split("T")[1];
      var startdate = res_filter.rows[i][2].split("T")[0].slice(5);
      var enddate = res_filter.rows[i][3].split("T")[0].slice(5);
      column.push({
        "title": eventname,
        "text": startdate + " " + starttime + "〜" + enddate + " " + endtime,
        "actions": [{
          "type": "postback",
          "label": "削除する",
          "data": "delete=confirm=" + rowid
        }]
      });
    }
  } else {
    for (var i = (digit-1)*10; i < (digit-1)*10+9; i++){
      var rowid = res_filter_getid.rows[i]
      var eventname = res_filter.rows[i][1];
      var starttime = res_filter.rows[i][2].split("T")[1];
      var endtime = res_filter.rows[i][3].split("T")[1];
      var startdate = res_filter.rows[i][2].split("T")[0].slice(5);
      var enddate = res_filter.rows[i][3].split("T")[0].slice(5);
      column.push({
        "title": eventname,
        "text": startdate + " " + starttime + "〜" + enddate + " " + endtime,
        "actions": [{
          "type": "postback",
          "label": "削除する",
          "data": "delete=confirm=" + rowid
        }]
      });
    }
    column.push({
        "title": "全",
        "text": "まだまだあるよ",
        "actions": [{
          "type": "postback",
          "label": "さらにイベントを表示",
          "data": "delete=add_digit"
        }]
    });
  }
  var message = {
    "type": "text",
    "text": "全" + res_filter.rows.length + "件のイベントがあります。どの予定を削除しますか?"
  }
  var postData = {
    "replyToken": token,
    "messages": [
      message,
      {
      "type": "template",
      "altText": "予定削除",
      "template": {
        "type": "carousel",
        "columns": column
      }
    }]
  }
  fetchData(postData);
}

function writeEvent(cache, userid, token) {
  var eventname = cache.get("eventname");
  var starttime = cache.get("starttime");
  var endtime = cache.get("endtime");
  var sql_insert = "insert into " + docid + " (Userid, Eventname, Starttime, Endtime) values ('" + userid + "','" + eventname + "','" + starttime + "','" + endtime + "');";
  var res_insert = FusionTables.Query.sql(sql_insert);
}

function DeleteFromTable(userid, token, data, cache) {
  var rowid = data.split("=")[2]
  var sql_delete = "DELETE FROM " + docid + " where ROWID = " + rowid + ";";
  var res_delete = FusionTables.Query.sql(sql_delete);
  var postData = {
      "replyToken": token,
      "messages": [{
        "type": "text",
        "text": "削除しました"
      }]
  };
  cache.remove("event_digit")
  fetchData(postData);
}

function fetchData(postData) {
  var options = {
    "method": "post",
    "headers": {
      "Content-Type": "application/json",
      "Authorization": secret
    },
    "payload": JSON.stringify(postData)
  };
  UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", options);
  return ContentService.createTextOutput(JSON.stringify({"content": "post ok"})).setMimeType(ContentService.MimeType.JSON);
}

function getUsername(userid) {
  var url = 'https://api.line.me/v2/bot/profile/' + userid;
  var response = UrlFetchApp.fetch(url, {
    'headers': {
      'Authorization':secret
    }
  });
  return JSON.parse(response.getContentText()).displayName;
}

工夫

cache導入してみた

LINEbotは一度のイベントに対して一度のポストバックしか送れません(おそらく)。なのでイベント作成のシーケンスにcacheを導入して前イベントの情報を保持させました。

UX面

ある程度ボタンクリックのための導線とかconfirmまでの流れとかを気にしてみました。ただbotとして制約が結構あって(文字数とかボタン数とかカルーセルのカラム数とか)、わりと最適なUXは追求しづらいのかなーと思ったり。

おわりに

という訳で今回はgasとFusiontablesを使ってLINEbotを作ってみました。webhookurlというものを初めて知って、どこまでできるか知らんけどとりあえずここまでのことは実現できて便利だなあ、といった感じ。レスポンスもそこまで遅くない。

javascriptにそこまで慣れてなくて、通常のデバッグも効かないみたいな感じだったのでここまで書くのにゆっくりやって2週間くらいかかってしまいました。どんな環境で開発するにせよ、デバッグ大事という一言につきますね。

余談

今回はフリー素材グラドルの茜さやさんにお世話になりました。 やっぱり可愛い女の子がアイコンだと作業が捗ります。ぜひみなさんもcheck it outしてください。

参考にさせていただいたサイト

公式ドキュメント

LINE BOTで「リッチメニュー」を表示してみる

LINE BOTからGoogleカレンダーの予定の取得・追加を行う