皆さん、こんな経験ありませんか?
・リリースしたゲームのエラー情報を見たり、統計を取ったりしたい
・特定のバグが何故かエディタ上では発生しないので、原因の特定が難しい
今回実装する事
・リリースした後のゲーム内で発生したエラーを取得し、外部のサーバーに保存します
今回はPOSTするサーバーにAWSのLambdaを採用し、無料利用枠内であれば完全無料で実現します
保存する外部サーバーはAPIアクセスが可能なものなら何でもOKですが、今回はDropboxを採用します
なぜ直接Dropboxに保存せずに、
Lambdaを経由するのか?といった疑問を持つ方もいると思いますが
Unityのスクリプトは、基本的にビルド後に解析可能となっています
これはIL2CPPビルドでも同じで、多少の難読化は出来ますが防ぐことはできません
今回のケースではDropboxに保存するため、
どこかしらにDropboxのアクセストークンを書く事になります
もしUnityのスクリプト内に直接書いたら...?
結果として攻撃者はアクセストークンを入手し
Dropbox内のファイルを好き勝手できるようになってしまいます
そこでLambdaを使用し、外部から見えない場所にアクセストークンを置こうというものです
実際に試した環境
・Unity 2021.3.0 LTS
・Visual Studio2017
・ビルドターゲットはWindows
・AWS Lambda Python3.9
この方法では、プレイヤーがインターネット接続されたPCで遊んでいる必要があります
速度面では、1Mbpsもあればゲーム動作に支障はありません
予めログの出力でErrorを有効化する必要があります
(PlayerSettings→other→Loggingの中のErrorなどをONにしておく)
実装方法
①新規にスクリプトを作成します(名前は何でもいいです)
②スクリプト作成時に書いてあるStart関数とUpdate関数を消して、usingに以下の項目を追加します
using System.IO;
using System;
using UnityEngine.Networking;
③変数を追加します
string FilePath; //エラー情報を一時的に保存するファイルパスです
string URL; //サーバーにPOST通信するURLです
string Send_Text; //実際のエラー情報を格納する変数です
④初期化処理を記載します
この初期化でログの検出ができるようになります
public void Start()
{
Application.logMessageReceived += OnReceiveLog;
}
void OnDestroy()
{
Application.logMessageReceived -= OnReceiveLog; //これ忘れると地獄になります
}
⑤実際にエラーが発生した際の処理を記載します
private void OnReceiveLog(string logText, string stackTrace, LogType logType)
{
try
{
if(Application.isEditor == false)
{
if (logType == LogType.Error | logType == LogType.Exception)
{
Send_Text = "Error Log:" + "\n";
Send_Text = Send_Text + "Game Name:" + Application.productName + "\n";
Send_Text = Send_Text + "Game Ver:" + Application.version + "\n";
Send_Text = Send_Text + "Error Type:" + logType + "\n";
Send_Text = Send_Text + "Error Text:" + logText + "\n";
Send_Text = Send_Text + "Error Stack:" + stackTrace;
//他にもエラーが起きたScene名など含めたい情報はじゃんじゃん入れましょう、情報は多い方が得です
Send_Text = Send_Text + ":Log End" + "\n" + "\n";
Error_Send();
}
}
}
catch(Exception e)
{
Debug.Log(Send_Text);
Debug.Log("エラーログの送信に失敗" + e.Message);
}
}
ここの処理のポイントとしては、ログの種類をエラー級のものに限定しています
さすがに頻繁に呼ばれると、ゲーム体験に影響を与えるほどのパフォーマンス悪化が発生します
また、Editor上で発生したログは無視しています
⑥エラーを送る準備をします
public void Error_Send()
{
FilePath = Application.dataPath + ""; //見つからないサブフォルダ等に置くのが無難です
if (!File.Exists(FilePath))
{
using (File.Create(FilePath)) { }
}
File.AppendAllText(FilePath, Send_Text);
StartCoroutine(Wait_File());
}
今回は必要最低限の記述なので省略していますが
今のままだとエラー情報のファイル名が同一であるため、ファイルが重複します
そのため基本的にファイル名には、プレイヤー名+日時の形式を取る事をお勧めします
Steam APIではSteamFriends.GetPersonaName()
でプレイヤー名の取得が可能です
なお、プレイヤー名を使用する際は
特殊文字をエスケープする処理を忘れないようにする必要があります
⑦実際にPOST通信を実施します
public IEnumerator Wait_File()
{
yield return new WaitForSecondsRealtime(1.0f);
if (File.Exists(FilePath))
{
byte[] bytes = File.ReadAllBytes(FilePath); //バイトに変換します
StartCoroutine(Send(bytes));
}
}
public IEnumerator Send(byte[] bytes)
{
string FileName = Path.GetFileName(FilePath);
var form = new WWWForm();
form.AddBinaryData("ErrorData", bytes, FileName, "application/octet-stream");
using (var req = UnityWebRequest.Post("POST通信先のURL", form))
{
yield return req.SendWebRequest();
}
if (File.Exists(FilePath))
{
File.Delete(FilePath); //証拠隠滅しましょう
}
}
POST通信先のURLには、
パラメータとしてファイル名、プレイヤー名、プラットフォーム名を
追加した想定で以降の操作が進みます
⑧AWSのアカウントを作成しましょう
本来はこの記事でもスクリーンショット付きで解説したかったのですが
私は既にAWSアカウントを持っているので、公式の記事を引用させて頂きます
⑨アカウントを作成すると、ダッシュボードが表示されすぐに開発できるようになりますが
まずは一旦落ち着いて必ずやっておくべき事を実施しましょう
以下のサイトにわかりやすくまとめられていますが、
中には有料のものもあるため慎重に行いましょう
有料の設定はアカウントのセキュリティを高めてくれますが
果たしてエラーデータごときにそこまでやる必要があるのか?については判断が必要です
私は以下の設定を実施しました(全て無料です)
・デフォルトVPCの削除
・無料利用枠アラートの設定
・MFA認証の有効化
・アカウントの秘密の質問の設定
・AWS Budgetsの予算の作成
・リージョンの設定→基本的に東京リージョンで問題ありません
⑩AWSマネジメントコンソールからIAMを選択し、Lambdaに使用するロールを作成します
与える権限としては、logs:CreateLogGroup
とlogs:CreateLogStream
とlogs:PutLogEvents
です
以下のサイトに詳しくロールの作成方法が記載されています
⑪AWSマネジメントコンソールからLambdaを選択し、関数を作成します
下のサイトに詳しくLambdaの作成方法が記載されています
この際、以下の設定を実施します
・関数URL : あり(IAM認証無し)
・⑩で作成したロールの割り当て
・メモリ使用量を512MBに変更
・タイムアウトを30秒に変更
⑫LambdaからDropboxにアクセスするために、Lambdalayerを追加します
以下のサイトを参考にLambdalayerの設定画面まで進み
arn:aws:lambda:ap-northeast-1:770693421928:layer:Klayers-p39-dropbox:9
と入力する事でLambda内でDropbox APIが使用可能になります
⑬⑪で作成したLambdaの内容を変更し、layerには先ほど追加したDropboxのものを追加します
⑭Dropbox側の設定を実施し、アクセストークンを持ってきます
以下のサイトの内容を実施します
APP_KEY
APP_SECRET
REFRESH_TOKEN
はLambda内で使用するのでメモしておいてください
⑮Lambdaの内容を修正します
一旦Lambdaの内容は全部消して、最初に必要なモジュールをインポートします
import json
import io
import requests
import base64
import os
import cgi
import dropbox
次にアクセストークンを取得する関数を作ります
def Get_Dropboxtoken():
APP_KEY = 'ご自身のAPP_KEY'
APP_SECRET = 'ご自身のAPP_SECRET'
REFRESH_TOKEN = 'ご自身のREFRESH_TOKEN'
data = {
'grant_type': 'refresh_token',
'refresh_token': REFRESH_TOKEN
}
response = requests.post('https://api.dropbox.com/oauth2/token', data=data, auth=(APP_KEY, APP_SECRET))
access_token = response.json()['access_token']
return access_token
次にメイン関数を記載します
def lambda_handler(event, context):
#ファイル名を取得、POST時にfilenameで渡す必要がある
FileName = event['queryStringParameters']['filename']
#プレイヤー名を取得、POST時にPlayerNameで渡す必要がある
PlayerName = event['queryStringParameters']['PlayerName']
#プラットフォーム名を取得、POST時にPlatformで渡す必要がある
PlatformName = event['queryStringParameters']['Platform']
body = event['body']
body_byte = base64.b64decode(body).decode()
#ファイルの書き込み
path = '/tmp/' + FileName
Write_text = body_byte
with open(path, mode='w') as f:
f.write(Write_text)
#ファイルの読み込み
file = open(path , 'rb')
result = file.read()
file.close()
Dropbox_BasicPath = '/Error' #ここにはファイルをぶち込みたいDropBOXのフォルダ名を入れましょう
Dropbox_token = Get_Dropboxtoken()
dbx = dropbox.Dropbox(Dropbox_token)
Dropbox_BasicPath = Dropbox_BasicPath + '/' + PlatformName
try:
dbx.files_create_folder(Dropbox_BasicPath)
except Exception as e:
print('プラットフォームフォルダは既にある')
print(e)
Dropbox_FolderPath = Dropbox_BasicPath + '/' + PlayerName
try:
dbx.files_create_folder(Dropbox_FolderPath)
except Exception as e:
print('プレイヤーフォルダは既にある')
print(e)
dbx.files_upload(result, Dropbox_FolderPath + '/' + FileName + '.txt')
return {
'statusCode': 200,
'body': event
}
⑯Lambdaをデプロイし、関数URLをUnityのスクリプト内に貼り付けパラメータを付与します
⑰あとは実機でエラーを起こせばレポートがDropbox内に保存されます
エディタ上で確認したい際はUnityのスクリプト内で
Application.isEditor == false
をtrueに変えましょう
以上です!
エラーを分析して、ゲームをもっとレベルアップしていきましょう!!
開発者のTwitterでは、開発時の知見をアップしています!