かばちんのエンジニアブログ

日々の経験の中で培った内容を備忘録も兼ねて記録していくブログです。少しでも誰かの役に立つために頑張って続けていけたらなと思います。

2022 最新版!「GAS + Twitter API v2」でリツイートBOTを作る

※TwitterAPIの有料化により無料で利用することが出来なくなりました。

GAS と Twitter API v2 を使用してサーバレスでリツイートBOTを作る方法

リツイート BOT とは?

リツイート BOT を作るために必要なもの

  1. Twitter アカウント
  2. Google アカウント

Twitterデヴェロッパーアカウントの作成

まずは Twitter Developer Portal にアクセスしてアカウントを作成します
作成方法は検索すると良質なサイトがたくさん見つかるのでなんとかなると思います
(既に作成済みの方は飛ばして頂いて問題ないです)

Twitter Developer Portal
developer.twitter.com


登録が完了したら BOT 用にアプリケーションを登録します。
こちらの登録方法も検索すると良質なサイトがたくさん見つかります。

この作業で必要なものは作成したアプリケーションについての以下の3点が取得出来ればOKです

  1. API Key」
  2. 「Secret Key」
  3. 「Bearer Token」


(参考サイト)
qiita.com
zenn.dev
auto-worker.com

作成したアプリケーションの設定

アプリケーションページの下の方に User authentication settings という欄があるので、設定ページを開きます。

User authentication settings
User authentication settings

使用するOAuth設定とアプリケーションタイプの設定は以下のように指定すれば問題ないです。

OAuthの設定を行います

アプリケーション権限は「Read and Write」に設定します。

アプリケーションに権限を与える

必須項目である「Callback URI」については後ほど説明しますのでひとまず何か入れておいてください。

Website URL」「Team of service」「Privacy policy」には Twitter アカウントの
プロフィールページ URL を入れておけばとりあえず大丈夫そうです。

Callback URIや必要な項目を埋める

以上で「Callback URI」以外の設定が完了しました。
続いて Google 側の設定を行なっていきましょう。

Googleスプレッドシートを作成

今回はサーバを利用せず Google スプレッドシートスクリプト機能である GAS (Google Apps Script) を
利用してリツイート BOT を作成するためまずはスプレッドシートを作成します。

今回はこんな感じで「検索ワード(ハッシュタグ)」、「いいね or RT」の種別と、
重複して投稿をしてしまわないように「最新TweetID」を保持するようにしてあります。

Google スプレッドシートを作成

GAS (Google Apps Script) を作成

メニューの「拡張機能」>「Apps Script」を選択して GAS (Google Apps Script) 編集画面を表示します。

Apps Script を起動

ライブラリの設定

GAS (Google Apps Script) を作成したらまず、利用するライブラリの設定を行います。
今回は最も利用されているであろうライブラリ「TwitterWebService」と「OAuth1」、
それからスプレッドシートを操作したいので「Sheet」サービスを登録します。

ライブラリの設定

「TwitterWebService」の登録方法

「ライブラリ +」の「+」マークを押して検索ウィンドウが表示されたら、スクリプトID に
1rgo8rXsxi1DxI_5Xgo_t3irTw1Y5cxl2mGSkbozKsSXf2E_KBBPC3xTF」を入力して検索します。
画像のように正常に検索できたら「追加」ボタンを押して登録します。
バージョンは「2」で問題ありません。

TwitterWebServiceの登録

「OAuth1」の登録方法も同様でスクリプトID は「1CXDCY5sqT9ph64fFwSzVtXnbjpSfWdRymafDrtIZ7Z_hwysTY7IIhi7s」です。
バージョンは「18」で問題ありません。

OAuth1を登録

Twitter の Callback URI を設定

先ほど飛ばしていた Twitter 側の Callback URI の設定に戻りましょう。
Callback URI は以下のような形式で入力する必要があります。

https://script.google.com/macros/d/{SCRIPT ID}/usercallback

{SCRIPT ID} の部分ですが、GAS (Google Apps Script) ページ URL
「〜/projects/XXXXXX/edit」の projects と edit に挟まれた部分になります。

https://script.google.com/macros/d/XXXXXX/usercallback    // ←こんな感じのイメージ(実際はもっと長い)

スクリプトを書く(全文掲載コピペOK)

ここまでの設定が出来たらあとはスクリプトを書けば完成です!
今回はスクリプト全文を載せておくので、必要箇所を書き換えてご自由にお使いください。

// ベアラートークンと認証用インスタンス
var bearerToken = 'ベアラートークン'
var twitter = TwitterWebService.getInstance(
  'Consumer API Key',         // 作成したアプリケーションのConsumer Key
  'Consumer API Secret Key'  // 作成したアプリケーションのConsumer Secret
);
var userID = '9999999999999999999'    // Twitter ユーザID

// 認証周り
function authorize() { twitter.authorize(); }                            // 認証
function reset() { twitter.reset(); }                                    // 認証解除
function authCallback(request) { return twitter.authCallback(request); } // 認証後のコールバック

// シートを取得
var sheetData = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1"); // 「シート1」はシート名

// APIコール時のオプション(GET)
const getOption = {
  method: 'get',
  contentType: 'application/json',
  muteHttpExceptions: true,
  headers: { Authorization: 'Bearer ' + bearerToken }
}
// APIコール時のオプション(POST)
const postOption = {
  method: 'post',
  contentType: 'application/json',
  muteHttpExceptions: true,
  headers: { Authorization: 'Bearer ' + bearerToken },
}

function main()
{
  var service = twitter.getService();

  var searchWords = pickUpSearchWords();
  for (var i = 0; i < searchWords.length; i++)
  {
    var searchWord = searchWords[i][0];
    var type = searchWords[i][1];
    var lastTweetId = searchWords[i][2];

    var tweetList = findTweets(service, searchWord, lastTweetId);
    if (tweetList == null)
    {
      continue
    }
    
    for (var j = 0; j < tweetList.length; j++) {
      var tweet = tweetList[j];
      if (tweet.id > lastTweetId) {
        lastTweetId = tweet.id;
      }
      if (type == 'いいね') {
        putFavorite (service, tweet);
      } else if (type == 'RT') {
        putRetweet (service, tweet);
      }
    }

    var titleRow = 1;
    var lastTweetIdCol = 3;
    var updateCell = sheetData.getRange(i + 1 + titleRow, lastTweetIdCol, 1, 1);
    updateCell.setValue(lastTweetId);
  }
}

// 検索ワードをスプレッドシートから取得する
function pickUpSearchWords()
{
  var titleRow = 1;
  var startRow = 1 + titleRow;
  var startCol = 1;
  var endRow = sheetData.getLastRow() - titleRow;
  var endCol = 3;
  
  return sheetData.getRange(startRow, startCol, endRow, endCol).getValues();
}

// ツイートを検索する
function findTweets(service, searchWord, lastTweetId)
{
  // API URL
  var getPoint = 'https://api.twitter.com/2/tweets/search/recent?query='

  // 検索キーワードとパラメータ
  var keyWord = encodeURIComponent(searchWord)
  var params = '&tweet.fields=author_id,id,text,created_at&max_results=20'
  params += '&since_id=' + lastTweetId

  // アクセスURL組み立て
  var url = getPoint + keyWord + params

  var response = service.fetch(url, getOption);
  var result = JSON.parse(response)
  return result.data
}

// いいね
function putFavorite(service, tweet)
{
  targetTweet = { 'tweet_id': tweet.id }
  postOption.payload = JSON.stringify(targetTweet)
  service.fetch('https://api.twitter.com/2/users/' + userID + '/likes', postOption);
}

// リツイート
function putRetweet(service, tweet)
{
  targetTweet = { 'tweet_id': tweet.id }
  postOption.payload = JSON.stringify(targetTweet)
  var result = service.fetch('https://api.twitter.com/2/users/' + userID + '/retweets', postOption);
}

スクリプトの細かい説明

変更する必要のある設定項目は以下の4つです。

  1. Bearer Token
  2. Consumer API Key
  3. Consumer API Secret Key
  4. Twitter ユーザID

「Bearer Token」「Consumer API Key」「Consumer API Secret Key」は、
Twitter 側で取得したものをそのまま貼り付けてもらえれば問題ありません。

Twitter ユーザID は設定ページを探したのですが見当たらなかったため、
Twitter ID」というWEBサービスを利用して抽出しました。
tweeterid.com

// ベアラートークンと認証用インスタンス
var bearerToken = 'ベアラートークン'
var twitter = TwitterWebService.getInstance(
  'Consumer API Key',         // 作成したアプリケーションのConsumer Key
  'Consumer API Secret Key'  // 作成したアプリケーションのConsumer Secret
);
var userID = '9999999999999999999'    // Twitter ユーザID


続いて、TwitterWebService に関連するメソッドを定義していきます。
ここは特に変更する必要はないため、決まり事として書いてあれば問題ありません。

// 認証周り
function authorize() { twitter.authorize(); }                            // 認証
function reset() { twitter.reset(); }                                    // 認証解除
function authCallback(request) { return twitter.authCallback(request); } // 認証後のコールバック


次に、スプレッドシートから検索ワードを抽出するためシートを操作できるようにします。
「シート1」というシート名で検索し、データを操作するための変数を作成します。
シート名をデフォルトから変更した場合はこちらも合わせて修正が必要になります。

// シートを取得
var sheetData = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1"); // 「シート1」はシート名


Twitter API v2 では、アクセスに Bearer Token が必要なため、TwitterWebService の
fetch メソッドで指定するオプションを定義しています。
GET と POST をそれぞれ分けましたが、method が get か post かという差しかないため 1 つに統一しても良いかもしれません。
ここで最初に設定した Bearer Token が使用されます。

// APIコール時のオプション(GET)
const getOption = {
  method: 'get',
  contentType: 'application/json',
  muteHttpExceptions: true,
  headers: { Authorization: 'Bearer ' + bearerToken }
}
// APIコール時のオプション(POST)
const postOption = {
  method: 'post',
  contentType: 'application/json',
  muteHttpExceptions: true,
  headers: { Authorization: 'Bearer ' + bearerToken },
}


いよいよ、スクリプトのメイン処理に入ります。
main メソッドでは以下のような流れで処理を行なっています。

  1. TwitterWebService の定義
  2. 検索ワードの取得
  3. 対象ツイートの検索
  4. ツイートが無ければ次の検索ワードへ
  5. ツイートがあれば「いいね」or「リツイート
  6. 最新 Tweet ID を更新

上記 2〜6 を検索ワードが無くなるまで繰り返します。

function main()
{
  var service = twitter.getService();

  var searchWords = pickUpSearchWords();
  for (var i = 0; i < searchWords.length; i++)
  {
    var searchWord = searchWords[i][0];
    var type = searchWords[i][1];
    var lastTweetId = searchWords[i][2];

    var tweetList = findTweets(service, searchWord, lastTweetId);
    if (tweetList == null)
    {
      continue
    }
    
    for (var j = 0; j < tweetList.length; j++) {
      var tweet = tweetList[j];
      if (tweet.id > lastTweetId) {
        lastTweetId = tweet.id;
      }
      if (type == 'いいね') {
        putFavorite (service, tweet);
      } else if (type == 'RT') {
        putRetweet (service, tweet);
      }
    }

    var titleRow = 1;
    var lastTweetIdCol = 3;
    var updateCell = sheetData.getRange(i + 1 + titleRow, lastTweetIdCol, 1, 1);
    updateCell.setValue(lastTweetId);
  }
}


実際にツイートを「検索」「いいね」「リツイート」しているのは以下の部分になります。

「検索」

Twitter API v2 のツイート検索エンドポイントは https://api.twitter.com/2/tweets/search/recent です。
GET でのアクセスになるため、query にそれぞれパラメータを付与してアクセスします。

パラメータには、検索ワード、取得フィールド(ユーザID、ツイートID、内容、日時)、最大取得件数の指定と、
重複防止のためにスプレッドシートに記録していた最新ツイートIDを使用して、
どのツイートIDのツイート以降を検索対象とするかを since_id で指定しています。

レスポンス形式は JSON です。

// ツイートを検索する
function findTweets(service, searchWord, lastTweetId)
{
  // API URL
  var getPoint = 'https://api.twitter.com/2/tweets/search/recent?query='

  // 検索キーワードとパラメータ
  var keyWord = encodeURIComponent(searchWord)
  var params = '&tweet.fields=author_id,id,text,created_at&max_results=20'
  params += '&since_id=' + lastTweetId

  // アクセスURL組み立て
  var url = getPoint + keyWord + params

  var response = service.fetch(url, getOption);
  var result = JSON.parse(response)
  return result.data
}


「いいね」「リツイート

それぞれのエンドポイントは以下の通り
いいね:https://api.twitter.com/2/users/{UserID}/favorites
リツイートhttps://api.twitter.com/2/users/{UserID}/retweets

UserID には WEBサービスTwitter ID」で取得したユーザIDを指定します。
オプションの payload に、どのツイートIDを対象にするかをセットします。

成功すると JSON で { "retweeted": true } こんな文字列(リツイートの場合)が返ってきます。

// いいね
function putFavorite(service, tweet)
{
  targetTweet = { 'tweet_id': tweet.id }
  postOption.payload = JSON.stringify(targetTweet)
  service.fetch('https://api.twitter.com/2/users/' + userID + '/favorites', postOption);
}

// リツイート
function putRetweet(service, tweet)
{
  targetTweet = { 'tweet_id': tweet.id }
  postOption.payload = JSON.stringify(targetTweet)
  var result = service.fetch('https://api.twitter.com/2/users/' + userID + '/retweets', postOption);
}

アプリ認証が必要

スクリプトを紹介する前に「スクリプトを書けば完成」と言いましたが実は最後にもう1つやることがあります。
Twitter に投稿させるには GAS (Google Apps Script) にアプリ認証をさせる必要があります。

アプリ認証方法

GAS (Google Apps Script) 画面の上部、「実行」「デバッグ」と書かれているところのすぐ右に、
プルダウンでメソッドを選択できるものがあると思います。
ここで「authorize」を選択して「実行」ボタンを押します。

すると、レスポンスに Twitter 認証用の URL が返ってくるのでブラウザでその URL にアクセスして認証を行います。
Callback URI の設定が正しく行われていれば正常に認証が完了するはずです。

実際に動かしてみる

初回は最新 Tweet ID がない状態なため、適当に何かツイートしてそのツイートIDを指定しておきます。
先ほどメソッド「authorize」を選択したところを「main」に変更して「実行」ボタンを押せば作動します。

検索ワードに設定されたキーワードに引っかかったツイートを自動的に
「いいね」又は「リツイート」してくれるようになったと思います。

定期的に実行させる

作成したいのは手動のツールではなく BOT なため、定期的に自動で実行するように設定します。
GAS (Google Apps Script) にはトリガーと呼ばれる自動実行用の仕組みが既に存在しているため、
そちらを利用して定期的に実行されるように設定を行います。

トリガー設定

GAS (Google Apps Script) 画面の左のメニューから「トリガー」画面を開きます。
画面右下の「トリガーを追加」ボタンを押して設定画面を開きましょう。

トリガーの詳細設定

今回それぞれの設定は以下のように設定を行いました。

実行する関数:main
実行するデプロイ:Head(最新)
イベントのソース:時間主導型
トリガータイプ:分ベースのタイマー
時間の感覚:1分おき

エラー通知設定:1週間おきに通知
(1分おきの実行でエラーが発生した場合頻繁に通知されることが想定されるため長めに設定)

リツイート BOT 完成

お疲れ様でした!
これで本当にリツイート BOT が完成しました!!

あとは Twitter アカウントを宣伝してフォローしてもらい、
実際にリツイート機能を Twitter ユーザに使ってもらえばいいだけですね。
(実はそこが一番大変なところ)

まとめ

今回リツイート BOT を作ろうと思ったきっかけは、BOT を求めているツイートをたまたま見かけて、
「そういえばリツイート BOT 作ったことないなぁ」なんて思ったのがきっかけです。

そこから実際に作りきるところまでやったわけですが、Twitter API v2 関連の情報が少ない印象でした。
やっぱり最終的に行き着くところは公式ドキュメントでした。公式最強!

2021年11月15日に Twitter API v2 が主要 API に変わったばかりということもあり、
TwitterWebService を利用して Twitter API v2 をコールする記事がとても少なかった気がします。

今回利用した API は「検索」と「いいね」「リツイート」のみですが、要領は同じだと思うので
他の API を利用するのにも少しは役に立つ内容になったのではないかなと思います。

おまけ

実際に作ったリツイート BOT はこちら
twitter.com

UnityIAP Android におけるコンビニ決済への対応方法

Google Play Billing Library 3.0 からコンビニ決済への対応が必須になっている

コンビニ決済(遅延決済)とは?

  • アプリから課金しようとした時にコンビニ支払いが選択可能になった
  • 支払いコードが発行され指定の手順でコンビニのレジにて支払い
  • 対応コンビニは今のところ次の3つ「ファミリーマート」「デイリーヤマザキ」「セイコーマート
  • レジでの支払い後一定時間(数分)待つと Google 側での決済が完了しアプリ側での購入処理が動く

結論、コンビニ決済への対応方法

課金処理が実装されているアプリには IStoreListener を継承しているクラスが存在するはずなので、
そのクラスに OnPurchaseDeferred メソッドを追加してイベントを処理する必要がある

// 遅延決済が開始されたことを通知
private readonly Subject<HogeHoge> pendingSubject = new Subject<HogeHoge>();
public IObservable<HogeHoge> OnPurchasePending => pendingSubject;

// 遅延決済が行われた通知を処理する
void OnPurchaseDeferred(Product product)
{
    // 支払いが保留中であることをアプリに伝える
    pendingSubject.OnNext(HogeHoge);
}

// UnityIAP 初期化時に IGooglePlayStoreExtensions を取得しハンドラを指定
public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
{
   〜〜〜省略〜〜〜

   // GooglePlayStore の拡張クラス
   m_GoogleExtensions = extensions.GetExtension<IGooglePlayStoreExtensions>();
   m_GoogleExtensions?.SetDeferredPurchaseListener(OnPurchaseDeferred);

   〜〜〜省略〜〜〜
}

■OnPurchaseDeferred が担えること

遅延決済が開始されたことの通知

コンビニ決済が開始されると通知されるがこの時点ではレシート情報が存在しないため、
基本的には遅延決済が開始されたという事実のみをこのメソッドでは扱うことが可能。


遅延決済が存在しなかった時には基本的に決済開始したら、即座に成功またはキャンセルを含め失敗が返って来るため、
このレスポンスを以て通信中の保護ダイアログを出していたりするケースが多いと思う。


しかし、遅延決済を行うとこの OnPurchaseDeferred が呼ばれてから、実際にコンビニ決済が完了するまでの
約数分間の間何もレスポンスがないため、その間ずっと通信中状態になってしまうなどの弊害が起きる。


さらに厄介な点が、支払い手続きを進めたあと GooglePlay 側でキャンセルを行なってしまったりすると、
それ以降 Google 側からの通知は一切無くなってしまう。


その為、OnPurchaseDeferred が実行された時には通信中状態を解除せざるを得ない。

通信中状態を解除した場合の弊害について

通信中状態を解除するとさらなる問題が浮上してくる。


今までは1つの課金アイテムを購入しようとした場合、成功か失敗するまでシーケンシャルに行えていたが、
OnPurchaseDeferred で通信状態を解除するということはアプリが通常通り操作可能な状態に戻るため、
支払いが保留状態のものがあるにも関わらず他の課金アイテムをさらに購入することが出来てしまう。


なので決済が完了した通知である ProcessPurchase メソッドが非同期に何個も同時に来る可能性がある。
その点に配慮して処理を実装する必要がある。

レシートに含まれる purchaseState に Pending (4) 状態が追加

遅延決済が実装されたことにより GooglePurchaseState に Pending が追加された。


実際の enum 定義を見てみよう。

public enum GooglePurchaseState
{
    Purchased,
    Cancelled,
    Refunded,
}


...はい。ありません。無いんです...。だけど Pending 状態を表す (4) が来ます。
なので ProcessPurchase 内で purchaseState をチェックする if 文はこう書くしかありません。

var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), AppleTangle.Data(), Application.identifier);
try
{
    var result = validator.Validate(product.receipt);
    foreach (IPurchaseReceipt productReceipt in result)
    {
        var google = productReceipt as GooglePlayReceipt;
        if (google != null)
        {
            if ((int)google.purchaseState == 4) // ←ココ
            {
                return PurchaseProcessingResult.Pending;
            }
        }
    }
}
catch (IAPSecurityException)
{
    return PurchaseProcessingResult.Pending;
}

このチェックをしっかりしないと、まだ決済処理が完了していない購入情報が、
購入が完了したものとして処理されてしまい実際にアイテム付与処理が走ります。


実際にはおそらくサーバ側でレシート検証などをしていると思うので、
そこでレシートの検証に失敗して弾かれるとは思いますが、しっかりと対応しておくべきです。

purchaseState が Pending (4) 状態で来るケース

コンビニ支払いの手続きを開始して OnPurchaseDeferred が呼ばれた以降、
実際に支払っていようが支払っていまいがアプリを再起動したり、UnityIAP が初期化されるタイミングになると
ProcessPurchase が呼ばれて purchaseState が Pending (4) 状態のレシートが付与された状態で飛んでくる。

■まとめ

他にもいろいろと細かい問題があったような気がしますが、ひとまず覚えている範囲でメモとして残しました。


課金周りの実装はアプリごとに異なるため、OnPurchaseDeferred を実装してその通知をどう扱うかなどは、
それぞれのアプリの設計によるため、おそらくすべての課金部分開発者に関係するであろう部分のみを抜粋し、
今後コンビニ決済対応を行う方に役立つ情報となるように書いたつもりです。


コンビニ決済は日本とメキシコしか対応していないらしいです。
Google に申し立てをしてコンビニ決済を無効にしてもらっているアプリも多く見受けられます。


コンビニ決済への対応はかなり工数がかかるため、いっそのこと無効にしてしまったほうがコスパは良さそう


というわけで、誰かのお役に少しでもなれば幸いです。

Unity 2019.3 以降へのバージョンアップ TIPS

■Unity アップデート後に iOS でビルドは出来るが起動時にクラッシュする

症状

  • アップデート前と同じように iOS ビルドはでき、実機へのインストールも可能
  • インストールしたアプリを起動しようとすると黒い画面でフリーズのような挙動
  • 一定時間その状態が続いたあとにアプリがクラッシュ

エラー内容

iOS のホーム画面を管理している SpringBoard がアプリの異常を検知してクラッシュしていた。

SpringBoard
[application<com.yarukiman.appid>:9999] Watchdog termination request provided: <FBSProcessTerminationRequest: 0x000000000;
label: "watchdog provision violated";
exceptionCode: "Watchdog Violation (0x00000000)";
reportType: CrashLog;
explanation: "process-launch watchdog transgression: application<com.yarukiman.appid>:9999 exhausted real (wall clock) time allowance of 20.00 seconds">

■ここからが解決方法

UnityFramework の登場

Unity 2019.3 から UnityFramework が分離されるようになりました。

・UaaL(Unity as a Library)
forum.unity.com

Unity as a Library とは

簡単に説明すると、Unity のエンジン部分が切り離され、
ネイティブアプリに部分的に Unity を組み込めるようになる仕組みです。

この仕組みが導入されたことにより、iOS ビルドの Xcode 吐き出し時の挙動が大きく変わりました。
これまでは Xcode 上に Unity-iPhone というプロジェクトが作成され、その中に Unity エンジンが内包されていました。

Unity 2019.3 以降では Unity-iPhone と UnityFramework という2つのプロジェクトが作成され、
Unity エンジン関連はすべて UnityFramework 側に格納されるようになります。

そのため、PostProcess で PBXProject ファイルを操作する際のターゲットを変更する必要があります。

PostProcessBuild の処理を変更する

変更を加える点は以下の2点です。

1.設定するターゲットプロジェクトを Unity-iPhone から UnityFramework に変更
2.Unity-iPhone プロジェクトから UnityFramework を参照出来るようにフレームワーク参照を追加

実際の PostProcessBuil の処理はこんな感じに変更になります。

[PostProcessBuild]
public static void OnPostProcessBuild(BuildTarget buildTarget, string path)
{
    var pbxProjectPath = PBXProject.GetPBXProjectPath(path);
    var pbxProject = new PBXProject();
    pbxProject.ReadFromString(File.ReadAllText(pbxProjectPath));

    // これまで Unity-iPhone をターゲットにしていろいろ変更していたところを、
    // UnityFramework をターゲットに変更する
-   var target = pbxProject.TargetGuidByName("Unity-iPhone");
+   var target = pbxProject.GetUnityFrameworkTargetGuid();

    // Unity 2019.3 以降の UnityFramework 分離対応(Unity-iPhone から UnityFramework を参照できるように追加)
+   pbxProject.AddFrameworkToProject(pbxProject.GetUnityMainTargetGuid(), "UnityFramework.framework", false);

    File.WriteAllText(pbxProjectPath, pbxProject.WriteToString());
}

■まとめ

知っていればなんてことのない変更ではありますが、これについての日本語の情報が極端に少なく
解決するまでにかなりの時間を費やしてしまったため、同じ境遇の方の助けに少しでもなれば幸いです。

Automatine を使ってみた

■ピュア C# で動く Timeline

Automatine というピュア C# で動くユーティリティが公開されたので使ってみました。
github.com

■Automatine ってなに?

一言で言うと Timeline のようなものですが、Unity に標準搭載されている Timeline とは違います。
Timeline はどちらかと言うと AdobeFlash に近い感じなのですが、
Automatine はフレーム単位で状態を管理できるステートマシン?(ともちょっと違うかな?)という印象です。

■それって何がいいの?

例えばフレームで管理されている代表例として格ゲーで言うと、
「必殺技を出した時、10フレーム目から20フレーム目まで無敵」とか
「通常技を出した時、5フレーム目から20フレーム目までの間だけ、次の技のコマンド入力を受け付ける」とか
そういった制御が GUI で設定出来るようになっています。

■他にも良い点が

コードがピュア C# & データが JSON というところが便利でよかったです。

リアルタイム通信のゲームを作る時、不正を防ぐためにクライアントと同じ処理をサーバ側でもやるような作りにしたりするのですが
このような手法で開発を行う場合、Unity 自身に搭載されている機能を使ってしまうとサーバ側で同じコードが動かなかったりします。

ピュア C# だと同じ処理をクライアントとサーバで動かせるので、1ソース化ができ開発がとてもやりやすいかつバグりにくくなります。
そしてデータ形式JSON なので、クライアントで動かしているデータをサーバでも使うことができます。

■導入方法

github に UnityPackage が置いてあるので、これを入れるのが手っ取り早いと思います。
github.com

■データの作り方

UntiyPackage を入れると Window -> Automatine というメニューが出てくるので、そこから編集画面を表示できます。
この Automatine 画面で Auto データを作ります。

Auto データとは Automatine で作成するタイムラインデータのことで、
「○フレーム目から○フレーム目までは A という状態」
「○フレーム目から○フレーム目まで、B という Coroutine を毎フレーム実行させる」
といった情報が入っています。

■Autoデータを作ってみる

ここではキャラクターを移動させるような簡単な Auto データ (Locomotion_Auto) を作ってみたいと思います。
Auto (+) から Auto を作成。
f:id:kabatin:20190806141123p:plain

Automatine ウィンドウの空白を右クリックで「Add New Timeline」

Timeline 右クリックで「Add New Tack」
Locomotion はループするようなモーションなので、Unlimited Span (Tack を選択すると Inspector に出てきます)をチェック
f:id:kabatin:20190806141206p:plain

■移動する処理を入れる

Coroutines (+) から Coroutine を生成。
f:id:kabatin:20190806144044g:plain

(Open を押すとエディタが起動してソースを編集できます)
ここで作られた Coroutine が、Tack に設定した帯の長さ(フレーム数)だけ呼ばれます。
最後に [Export As Runtime Code] をクリックすると、動的にクラスを生成してくれます。

■動かしてみる

新しいシーンを作ってテキトーに Cube を作り、以下のコードをアタッチ。

using UnityEngine;
public class Player : MonoBehaviour
{
    Info info;
    Vector3 pos = new Vector3();
    void Start()
    {
        info = new Info();
    }
    void Update()
    {
        var speed = Input.GetAxis("Vertical");
        info.OnUpdate(speed);
        
        pos.x = info.position.x;
        pos.z = info.position.z;
        transform.localPosition = pos;
    }
}

上記コード中の Info クラスは以下のものを使っています。

using Automatine;
public class Info
{
    public Position position = new Position();
    int frame;
    Auto<Position, Position> auto;
    public Info()
    {
        auto = new Locomotion_Auto<Position,Position>(frame, position);
    }
    public void OnUpdate(float speed)
    {
        position.speed = speed;
        auto.Update(frame, position);
        frame++;
    }
}

public class Position
{
    public float x;
    public float z;
    public float speed;
}

「移動する処理を入れる」章で作った Coroutine を以下のコードに修正

using System.Collections;
using Automatine;
public partial class RoutineContexts <InitialParamType, UpdateParamType> : RoutineBase<InitialParamType, UpdateParamType> {
    
    public IEnumerator Locomotion_Coroutine (InitialParamType initialParam) {
        var pos = initialParam as Position;
        while (true)
        {
            pos.z += pos.speed;
            yield return null;
        }
    }
}

実際に表示するものは Player.cs で、位置などの管理は Info.cs に分けられました。
最後に [Export As Data] を押すと JSON が生成されます。
出力先は「Automatine > Runtime > Generated > Json

auto を変更する時は

auto = new Hoge_Auto<Fuga, Fuga>(frame, fuga);

という感じで auto 自体を入れ替えてしまえば良いです。
auto を JSON から読み込む時は

string json = "JSONデータ";
RuntimeAutoData data = AutomatineJsonConverter.JsonToAutoData (json);
auto = Automatine.Auto<Fuga, Fuga>.RuntimeAutoGenerator (data);

とやればいいみたいです。

■まとめ

今回は動かしてみるだけの簡単なものでしたのが、普通にゲームを作るとかなり複雑な状態管理が必要になるので、
そういった時に Automatine の真価が発揮されるのではないかなと思います。

カジュアルアプリにAppLovinのSDKを導入してみた

[もくじ]

AppLovin広告SDKの導入

 久しぶりの投稿になります。

 さて、タイトルの通りですが新しく全世界向けに配信することになったカジュアルゲーム
AppLovin の広告SDKを導入してみたので、導入の仕方から実際のリリースに至るまでの一連を備忘録として残したいと思います。

[開発に使用した環境]

なぜAppLovinにしたのか?

 これまでに、アドフリくん、アイモバイル、UnityAds、Fello、AdGenerator などいろいろな広告SDKを導入してきました。
今回 AppLovin を選んだ理由としては以下になります。

  • 全世界配信前提なので海外に対応していること(必須)
  • 導入説明を見る限り比較的簡単に導入できそうだったこと
  • バナー広告と動画広告の両方があったこと
  • 逆に自社で広告を出す場合に費用対効果が高いと評判がよかったこと


以上が、今回AppLovinを選んだ主な理由です。

AppLovin の公式サイトはこちら
www.applovin.com



SDKの導入

 SDKの導入自体はとても簡単でした。
公式サイトからダウンロードできる UnityPackage を入れれば基本的には特に何もすることはありません。
気をつけなければいけない点を挙げるとすれば、AndroidManifest.xml が既に存在する場合は自分でマージが必要になります。
それ以外は特に気にすることはないので Import してしまって大丈夫です。

しかしながらSDK導入時にはよくあることですが、ハマったポイントもあるので解説していきたいと思います。


Xcode で Archive したあとの Validation が通らない

 順調に Unity での開発を終え、Xcode プロジェクトを書き出し、Xcode でビルドし実機にて動作確認をします。
ここまでは非常にスムーズに行うことができました。

その後課金周りのテストをしたかったため、一度 AppStore へアップロードしようと思い、
ひとまず Validation を実行したのですが、「Symbols tool failed」というエラーが発生して Validation が失敗します。

f:id:kabatin:20190322185045p:plain
エラー画面

このエラーがなかなか厄介で、ググっても全く情報が見つかりませんでした。
そもそもですが、AppLovin 自体の情報が他の広告会社と比べるとかなり少ない気がします。

海外の広告会社だから日本語のサイトが少ないのは分かるのですが、英語で検索しても本当に少ない。
それが、この記事を書こうと思った大きな理由でもあります。

やっとの思いでヒントになるページを見つけました。
stackoverflow.com

このページの下のほうにこんな書き込みをされている方がいました。

Basically some framework has been added to Copy Bundle Resources. Remove it to resolve the issue!

要約すると「既にバンドルリソースはコピーされているからそれを削除しましょう。」


Xcode の BuildPhases にある Embed Frameworks に libAppLovinSdk.a が追加されている状態なので、
それを削除して再度 Archive を行ってから Validation をしたら成功しました。


動画広告再生時のクラッシュ問題

 他のアプリを Xcode でビルドする際にいつもお決まりで設定している項目があるのですが、
AppLovin を導入しているアプリではそれが原因で、動画再生時にクラッシュすることがあったので記述しておきます。


[Build Settings]

  • Enable Bitcode を NO にする
  • Architectures を Standard architectures(armv7,arm64) にする
  • Build Active Architecture Only を NOにする

[Build Phases]

  • Compile Sources の AL で始まるソースに -fno-objc-arc フラグを設定する (ALAppLovinLogger.m以外)


この中のどれが主な原因になってクラッシュを起こしているのかまでは追跡しませんでしたが、
簡単に言うと AppLovin に関しては UnityPackage を入れたままの状態で正常に動作します。

これはとても素晴らしいことではあるのですが、公式サイトの導入説明には -fno-objc-arc を設定する
指定があったので、マニュアルの状態も最新の状態にしておいて欲しいなと思いました。


まとめ

 今回初めて AppLovin の広告SDKを使ってみましたが、総評としては良かったと思います。
海外向けに広告モデルでアプリを開発する際には導入もしやすく、もしバズった場合などに
追加で広告を出したい状況などが出てきたら恩恵を受けられるのではないかなと思いました。

Unity 5.5 バージョンアップに伴うエラー解消事例

f:id:kabatin:20170209114218p:plain

Unity バージョンアップ時エラー解消について

Unity 5.3.5p8 から Unity 5.5.1p2 にバージョンアップした際に発生したエラーの解消方法のメモです。

事の発端

Unity 5.5.1p2 へアップデートしてもろもろの動作確認を行っていたところ、PC 上では正常に動作が確認できた。
念のため実機でも確認しておこうと思い APK ファイルを作成して実行してみたところ Unity ロゴが出る以前の段階で
画面が真っ暗な状態のままフリーズ。

原因調査

adb logcat で E/Unity を grep しながらログを確認したところ以下のようなエラーが発生していた。

E/Unity   (XXXXX): Unable to find AudioPluginOculusSpatializer

ネットで検索

"Unable to find AudioPluginOculusSpatializer" というキーワードでいろいろ検索すると海外サイトが多数ヒット。
しかし、具体的な解決策は見つからずに途方にくれていたところ、Unity 上で OculusSpatializer という文言にたどり着く。
[Editor] → [Project Settings] → [Audio] から AudioManager を表示すると、Spatializer Plugin というプルダウンがある。
その中に Oculus Sptializer というのがあったので、これを選択した状態で再度ビルドを試みてみた。

が、失敗。

Unity 上で発生していたエラー

着目点を少し変えて Unity を起動した直後に表示されていた以下のエラーを調査。
[XX] の部分の数字が数パターンあり、いくつかのプラグインが正しく読み込めていないような感じ。

InitializeUnityExtensions: Must have a valid path for the plugin [XX]
Extension was not registered, that means it relies on default UnityExtensions settings, please register extension and apply necessary settings for it in the callback.


いろいろ検索した結果、以下のサイトを参考に [Reimport All] を試したところ無事解消!
fantom1x.blog130.fc2.com


エラー解消手順

Unity エディターの Project ウィンドウ上で右クリックし、[Reimport All] を実行する。
自分の場合は Unity がクラッシュして落ちたけど、再起動したらアセットの再読み込みが始まり無事に起動。
InitializeUnityExtensions のエラーも消えており、この状態で APK を作成して実機確認したところ正常に動作した。

もしくは、試してはいないが Library フォルダを一度全削除して再起動することでも直ると思われる。


原因

公式な文書は見つけられなかったので完全に推論にはなってしまうが、
Unity ではバージョン間でのキャッシュファイルのフォーマット差があるので、そのキャッシュが悪さしていたのではないかと思う。

最新の Unity で発生している事象なので同じような状況の方の手助けに少しでもなれば幸いです。

アプリ開発に役立つ便利サイト集

自分がアプリ開発する時に活用する便利サイトの紹介です。

随時追加していきたいと思います。



素材系

無料写真素材集

人物を含む高画質な写真をいろいろと提供してくれるフリー素材サイト
www.pakutaso.com



サウンド

魔王魂

言わずと知れたサウンドフリー素材サイト
maoudamashii.jokersounds.com


Music is VFR

質の高いBGM・SEをかなりの数提供してくれている
musicisvfr.com


あみたろの声素材工房

ボイス素材をたくさん公開してくれている
www14.big.or.jp