ゴマちゃんフロンティア

アザラシが大好きなエンジニアの開発日記です

【Unity】WebGLからS3へ画像をアップロードしTwitterの投稿に表示させたお話

time 2019/11/16

以前、「WebGLからTwitterの投稿画面を開く」方法を紹介しました。

【Unity】WebGLからTwitterのツイート投稿画面を表示する方法

実際にUnity1週間ゲームジャムに参加して組み込んでみると、まあ動くには動くのですが、ゲーム画像がなくてちょっと物足りない感じに。

「ゲームであればやっぱり画像がほしい!」ということで、今回はAWSのS3とLambdaを使って実装してみました。
後述しますが、単に画像ファイルのURLを指定してもTwitter上では表示されないので、ちょっと一工夫入れる必要あります。

ちなみにunityroomさんの方でライブラリが用意されているので、それを使えば簡単に実現できるかと思います。特にこだわりのない方はこちらを使わせていただきましょう。
https://github.com/naichilab/unityroom-tweet

前知識

「Unityでスクリーンショットの取得」「WebGLからLambdaへリクエスト」など、必要となる知識があれこれあります。
それらすべてを1つの記事に書くと長くなるので分割しました。なので本記事を読む前に、各記事を一読いただけると幸いです。

【Unity】WebGLでゲーム画面のキャプチャ(スクリーンショット)を取得する方法

【Unity】AWS Lambdaを使用してWebGLからS3に画像をアップロードしたお話

Lambda関数の作成

APIへは以下のパラメータが送信されることを期待して実装します。

パラメータ名 用途
title ゲームタイトル
gameUrl ゲームのURL
S3_bucketName S3のアップロード先バケット名
S3_bucketPath S3のアップロード先パス
dirName アップロード先として作成するディレクトリ名(ランダム文字列)
encodedImage 画像データをbase64エンコードした文字列

アップロードの仕組み自体は前述の記事と同様ですが、以下の点が異なります。

  • バケットの特定のパスにディレクトリを作成
  • ↑のディレクトリ内にHTMLファイルと画像ファイルを配置
  • アップロード先のバケットやパスはリクエストで指定

それらを踏まえてソースコードを修正します。

using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using Amazon.S3;
using Amazon.S3.Model;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

namespace UnityTwitterImageCommand_Upload
{
    public class Function
    {
        IAmazonS3 S3Client { get; set; }

        public Function()
        {
            S3Client = new AmazonS3Client(Amazon.RegionEndpoint.APNortheast1);
        }

        public Function(IAmazonS3 s3Client)
        {
            this.S3Client = s3Client;
        }
        
        public async Task<APIGatewayProxyResponse> FunctionHandler(LambdaRequest lambdaRequest, ILambdaContext context)
        {
            var request = JsonConvert.DeserializeObject<UnityTwitterImageRequest>(lambdaRequest.Body);

            try
            {
                // アップロード画像のStream用意
                string encodedImage = request.encodedImage;
                encodedImage = encodedImage.Replace('-', '+').Replace('_', '/').PadRight(4 * ((encodedImage.Length + 3) / 4), '=');
                byte[] imageByte = Convert.FromBase64String(encodedImage);
                var stream = new MemoryStream(imageByte);
                stream.Write(imageByte, 0, imageByte.Length);

                // 画像アップロード
                var imageObjectRequest = new PutObjectRequest()
                {
                    BucketName = request.S3_bucketName,
                    Key = request.S3_bucketPath + request.dirName + "/image.png",
                    ContentType = "image/png",
                    InputStream = stream,
                    CannedACL = S3CannedACL.PublicRead
                };

                await S3Client.PutObjectAsync(imageObjectRequest);

                // HTMLアップロード
                string endPoint = string.Join("",
                    "https://" + request.S3_bucketName + ".",
                    Amazon.RegionEndpoint.APNortheast1.GetEndpointForService(S3Client.Config.RegionEndpointServiceName).Hostname
                );
                string imageUrl = endPoint + "/" + request.S3_bucketPath + request.dirName + "/image.png";
                string htmlUrl = endPoint + "/" + request.S3_bucketPath + request.dirName + "/index.html";
                string contentBody = GetImageHtmlString(request.title, request.gameUrl, htmlUrl, imageUrl);
                var htmlObjectRequest = new PutObjectRequest()
                {
                    BucketName = request.S3_bucketName,
                    Key = request.S3_bucketPath + request.dirName + "/index.html",
                    ContentType = "text/html",
                    ContentBody = contentBody,
                    CannedACL = S3CannedACL.PublicRead
                };

                await S3Client.PutObjectAsync(htmlObjectRequest);

                var response = new APIGatewayProxyResponse
                {
                    StatusCode = (int)HttpStatusCode.OK,
                    Body = JsonConvert.SerializeObject(new UnityTwitterImageResponse() {
                        result = "Upload Executed"
                    }),
                    Headers = new Dictionary<string, string>() { { "Access-Control-Allow-Origin", "*" } }
                };

                return response;
            }
            catch(Exception e)
            {
                context.Logger.LogLine(e.Message);
                context.Logger.LogLine(e.StackTrace);
                throw;
            }
        }

        private string GetImageHtmlString(string title, string gameUrl, string htmlUrl, string image)
        {
            string html = @"<!DOCTYPE html>
<html>
    <head>
        <meta charset=""utf-8""/>
        <meta name=""robots"" content=""noindex"">
        <meta property=""og:title"" content=""%title%"" />
        <meta property=""og:type"" content=""website"" />
        <meta property=""og:url"" content=""%url%"" />
        <meta property=""og:image"" content=""%image%"" />
        <meta property=""og:site_name""  content=""%title%"" />
        <meta property=""og:description"" content=""%description%"" />
        <meta name=""twitter:title"" content=""%title%"" />
        <meta name=""twitter:description"" content=""%description%"" />
        <meta name=""twitter:card"" content=""summary_large_image"">
        <meta name=""twitter:site"" content=""@riberunn"" />
        <meta name=""twitter:image"" content=""%image%"" />
    </head>
    <body>
        <script>location.href = '%gameUrl%'</script>
    </body>
</html>";
            html = html.Replace("%title%", title);
            html = html.Replace("%url%", htmlUrl);
            html = html.Replace("%image%", image);
            html = html.Replace("%description%", "アクセスするとゲームのURLへ遷移します");
            html = html.Replace("%gameUrl%", gameUrl);

            return html;
        }
    }

    public class LambdaRequest
    {
        [JsonProperty(PropertyName = "body")]
        public string Body { get; set; }
    }

    public class UnityTwitterImageRequest
    {
        public string title;
        public string gameUrl;
        public string S3_bucketName;
        public string S3_bucketPath;
        public string dirName;
        public string encodedImage;
    }

    public class UnityTwitterImageResponse
    {
        public string result;
    }
}

「なんでHTMLファイル?」と思うかもしれませんが、これはTwitterはS3の画像を展開してくれないためです。なので単に画像のオブジェクトURLを指定するだけでは実現できません。
そこでTwitter用にOGタグを記載したHTMLファイルを画像ファイルと一緒に作成し、投稿時はHTMLファイルのURLを載せます。こうすることでTwitterがOGタグを認識し、TwitterCardの形に展開してくれます。
加えてHTMLファイルのbodyタグにはゲームURLへのリダイレクト処理を入れておき、カードクリック時に自然とゲーム画面へ飛べるようにしました。

動作の注意点として、Lambda→S3のレスポンスを待たずにWebGL側へレスポンスを返却しています。なのでWebGLにレスポンスが返ってきたタイミングでS3にアップロードされているかの保証はありません。
ただこの後には「Twitter投稿画面が開く→ユーザーの投稿操作」があるため、それまでにはS3に上がっているだろうと考えます。

Unity側の実装

リクエスト用データクラスの拡張

UnityからHTTPリクエストを送信する仕組みは以下の記事の「Unity側の実装」をご参照ください。

【Unity】ランキング機能を自作してみたお話

今回やりたいことに合わせて、リクエストで使用するデータ用クラスのフィールドを増やします。

using System;

[Serializable]
public class RequestUploadTweetImage
{
    public string title;
    public string gameUrl;
    public string S3_bucketName;
    public string S3_bucketPath;
    public string dirName;
    public string encodedImage;
}

画像のアップロード処理

次にアップロード処理を書き換えます。フィールド currentScreenShotTexture にキャプチャ済みのTexture2Dクラスのインスタンスが入っている想定で実装しています。
保存先のパスはランダム文字列から生成します。自前で実装しても良いですが、GUIDを利用すると簡単に作れます。

[DllImport("__Internal")]
private static extern void OpenTweetWindow(string text, string hashtags, string url);

public IEnumerator UploadTweetImage()
{
    byte[] imageBytes = currentScreenShotTexture.EncodeToPNG();

    // ファイル名をランダムで生成
    string dirName = Guid.NewGuid().ToString("N").Substring(0, 16);

    var requestUploadTweetImage = new RequestUploadTweetImage()
    {
        title = "ゲーム名",
        gameUrl = "ゲームURL",
        S3_bucketName = "S3バケット名",
        S3_bucketPath = "S3バケットパス",
        dirName = dirName,
        encodedImage = Convert.ToBase64String(imageBytes)
    };

    // サーバへ登録リクエスト送信
    var httpPost = HttpService.PostJson<ResponseUploadTweetImage>(
        "API GatewayのURL",
        JsonUtility.ToJson(requestUploadTweetImage)
    );

    yield return StartCoroutine(httpPost);
    var responseUploadTweetImage = httpPost.Current as ResponseUploadTweetImage;
    Debug.Log(responseUploadTweetImage.result);

    // Twitter投稿画面を開く
    if (Application.platform == RuntimePlatform.WebGLPlayer) {
        string imageUrl = string.Join(
            "",
            "https://" + "S3のバケット名" + ".s3.amazonaws.com/",
            "S3のバケットパス",
            dirName + "/index.html"
        );
        string text = “投稿欄に表示するテキスト”;
        string hashTag = "ゲーム名”;
        string url = UnityWebRequest.EscapeURL(imageUrl);

        OpenTweetWindow(text, hashTag, url);
    }
}

外部へリクエストを送る関係上、どうしてもレスポンスが悪くなってしまうのが課題ですね。こればかりはしょうがないので、画面に「処理中です…」といった表示を出したりしてカバーしたいところ。
上手くいくとLambdaが動き、S3にHTMLファイルと画像ファイルが追加されているはずです。

オブジェクトURLからHTMLファイルと画像ファイルにアクセスできることを確認してみましょう。

ツイートへの画像URL組み込み

.jslibファイルを修正し、URLをGETパラメータに含めるようにします。修正後は適当なPluginフォルダに配置しましょう。

mergeInto(LibraryManager.library, {

  OpenTweetWindow: function(text, hashtags, url) {
    window.open('https://twitter.com/intent/tweet?url=' + Pointer_stringify(url) + '&text=' + Pointer_stringify(text) + '&hashtags=' + Pointer_stringify(hashtags));
  }

});

ここまでできたら実際にWebGLビルドを行い、Twitterへの投稿を試してみます。

S3上のHTMLファイルに書かれたOGタグによってTwitterCard形式で表示されていますね。画像の表示も問題なさそうです。

あとがき

そんなわけで、WebGLからTwitter投稿時の画像表示をAWSと組み合わせて実現してみました。
テキストだけより画像もあったほうが断然見栄えがいいです。どんなゲームかも伝えることができますね。
ゲームを作るだけではなく、いかに広めていくかも考えていきたいと感じた今日この頃です。

down

コメントする