2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

unityのWebGLからAWSのDynamoDbをつついた話

Last updated at Posted at 2020-11-03

#この記事はこんな人向け
・unityでWebGL産ゲーム公開したい!
 尚且つ、他者様の公開サイトを利用したい!
 (自分の場合はUnityRoom!こちら→https://unityroom.com/)
・そのうえで、データはAWSに保存したい!
・個人スコアとかでなく、マスタ類は外だしにしたい!AWSに持って行きたい!
・Unityしか使いたくないけど、とにかくAWS使いたい!
・余計なプラグインやSDKを入れずに動かしたい!
 完全外付けで、今のプロジェクトに影響を与えない
・WebGLとスマホ何かでネットワークやDBを統合したい!
 今回自分の動機はこれでした。スマホだったらこっちのコードとDB、WebGL版だったらこっちのコードとDBと言うのを避けたい!って成ると、本稿の内容か、Ncmbで全部賄うの2択になります。
・後、備忘禄的に私向け

##それってそんなに難しいの?何か問題あるん?一杯記事あるやん?
はい、難しいです。
WebGLからAWSのDynamoDBにアクセスしようとすると、色々躓きます。
AWSのSDKを使ってビルドすると普通に通ります。
を、行けた行けたと思っていざWebGL向けにビルドするとビルド自体は通ってアップロードする事ができますが、いざ実行すると
大体ここでコケます。

AWS_SDKの初期化んところ
UnityInitializer.AttachToGameObject(this.gameObject);
AWSConfigs.HttpClient = AWSConfigs.HttpClientOption.UnityWebRequest; 

理由はunity SDKがC#によるWebGLビルドをサポートしておらずjava書いてねと言う結論に成ります。
そして、javaを書いていくと次にCorsを解決しなければなりません。Corsヘッダは他者様サイトを使う場合はどうしようも無くなり、諦めるか…と言うところまで行きます。自分のサイトで公開する場合などは自由書けけますね!
余談ですが、対抗馬のFirebase等でもSDKはUnityのWebGLビルドをサポートしておらず、こちらのFireStore等もSdkからは使えません。
残った選択肢としてSdkから気軽につつけるncmbが候補に上がります。ncmbってすごいね!
こちらはWebGLからもSdkサポートしてますし、WebGLを扱う以上金字塔的な触りやすいDBに成っております。

##ncmbだけじゃダメなん?
本来ならコレだけでいっかと思ったのですが、3ヵ月Apiにアクセスが無いとクラスが消えます。(3か月ゲームされなかったらって感じかな)

個人的には、もうそんな頻度のゲームデータはかえって消えた方がいっか、と思ってます。
特にスコアとかのイベントデータはプレイした時に発生させれば問題無いと思います。

ですが、マスタデータをncmbで管理してる場合、過去の古いゲームを引き出しから引っ張り出してきて、あの時どうだっけ?と動かそうとするとマスタが消えてて動作すらしないと言う羽目にも成りかねません(汗

##データ管理はアセットバンドルじゃダメなん?
アセットバンドルはWebGlだとAudio(Oggファイル)が動かない問題があったりで、WebGLだと本体ごと入れ替えた方がマシだったりします。そうなると、入れ替えや版管理が結構メンドく、気軽に即時反映出来るようにしたいなら、外のDBに頼りたい所です。

##と言うことで戻ってきました。
SDKもダメ、ncmbも使わないという縛りなので残る選択肢は余りありません。
と言うことで以下の方針を設定します。
・UnityWebrequestを使ってApiGatewayを経由し、AWSのLambdaでDynamoDBの値を引っ張る
です。

何やら怪しい呪文が一杯ありますが、今は無視して取り敢えず手を動かしてみます。

ここから先はある程度覚悟が必要です。AWSを使いますので

AWSは課金と一体になったアカウント管理をしているので、気が付いたら課金されている!?と言う事態に成りかねません。実際アカウントを作成するときにカードの入力が要求されると思います。

以下の記事はAWS前提で書いてますので、安いとは言え課金が発生する可能性を覚悟してください!

##最初はすっとばします。
まず、AWSの管理画面にアクセスできるようになってください。
https://aws.amazon.com/jp/
こちらの右上オレンジマークのコンソールにサインインを押して手続きをしてください。

#AWS側設定
##DynamoDBを触ろう
DynamoDBとは俗に言うNoSQLってやつです。検索条件とかソートとかはあまり柔軟ではありません。イメージとしてはDictionaryとかHashTableに近いだろうか。
プライマリキーと複合キーとしてソートキーと言う物が使えます。これだけがソートを基本機能でできる感じです。2キーのHashTableみたいな?検索の時はプライマリのキーが必須になります。使い方によっては、範囲検索なんて出来るかもしれませんが、成るべくなら検索の柔軟性にこだわらないのがキモです。

初めに言っておきますと、昔ながらの用途別にテーブルを建てるようなテーブル設計だと破綻します。
と言うのも課金がキャパシティユニットと言い…細かく言うと記事が書けてしまう程なのでざっくり言います。

1か月の読み書き数上限をテーブルに設定してその分課金になります。
単位はキャパシティユニットで、25キャパシティユニット無料です。
足が出た分が課金になります。25と言うと相当量ですが細かく切ると(デフォルトが5)5テーブルで無料分が無くなってしまいます。
いつの間にか課金ヘルな状態に成りかねません。
ゲーム用途位なので1テーブルで納めるようなテーブル設計を目指します。
プライマリキーにアプリ名、ソートキーにテーブル名位で良いんじゃないかなと思ってます。
多分相当数のゲームのマスタ分納めても、マスタのアクセス位だったら無料で賄えます。
(容量だけだったら25Gまで無料で保存できます。)

コンソールの検索画面にDynamoって打つと選択肢に出るので押下
image.png

まずは手を動かすと言うことで
image.png

テーブルの作成を押下します。
image.png

テーブル名
管理したいテーブルを記載します。

テーブルが作成できたらこんなタブがでます
image.png

項目を押すと
image.png
管理画面ぽい!
ここにデータを蓄えたり、ここからのデータを引っ張るのを目指します。
この画面でも直にデータを書いたり作ったり消したり、検索したりできます。

次にデータを抜き出したり、書いたりするプログラムをAWS側に作ります。

##その前にちょっと権限を
プログラムからDynamoにアクセスできる権限を作ります。
image.png
左上AWSマークから

image.png
左ペインのロール

image.png
ロールの作成

image.png
Lambdaを選択

image.png
右下の次のステップへ

image.png
検索窓にDynamoと打ち込み出てきたAmazonDynamoDBFullAccessを選択

image.png
次へ

image.png
タグオプションは無視して次へ

image.png
名前を付けてロールの作成
このロールは次のLambdaで使います
LambdaがDynamoDBを触る為の権限になります。

##Lambdaを触ろう
いよいよプログラムです。Lambdaと言うのはAWS上でサーバーを建てずにプログラムだけ動くという優れものです。
俗に言うマネージドサービスと言われてる物の一部です
課金は動かすのに使うメモリ*時間 & リクエスト数で決まりますが、ホント安いです。他の箇所の課金に気を配った方が良いですね。
月間100万リクエストと40万GB秒無料なので、相当無料で済みます。

これを使って最終的にはDynamoDBのデータを引っ張ります。

左上のAWSマークを押して最初の画面に戻って
image.png
Lambdaを押下します。

右上関数の作成を押下します。
image.png

image.png
関数名は適当
ランタイムはPython3.8でやってみます
右下の関数の作成を押下
image.png

しばらくするとLambdaのエディタが開きますので、ここでコーディング

Lambda側
import boto3
import json
from boto3.dynamodb.conditions import Key

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table("GameDatas")
def op_query(transaction):
    key = 'application'
    sortkey = 'table'
    queryData = table.query(
        KeyConditionExpression = Key(key).eq(transaction[key]) & Key(sortkey).begins_with(transaction[sortkey])	
    )
    return queryData

def lambda_handler(event, context):	#//Lambdaから最初に呼びされるハンドラ関数
    op = event['Operation']
    try:
        if op == 'QUERY':
            transaction = event['Keys']
            return op_query(transaction)
        elif op == 'HELLO':
            return 'HELLO'
    except Exception as e:
        print("Error Exception.")
        print(e)

一つづつ解説しますね。


def lambda_handler(event, context): #//Lambdaから最初に呼びされるハンドラ関数

下の方の関数定義ですが、これがLambdaで最初に呼び出される関数になります。
unityで言うところのStartとかに相当します。

   op = event['Operation']

一行目ですがopと言う変数の中にUnityから送る予定のOperationのを設定します。eventはUnityから渡される一連のパラメータが詰まってます。Dict型の変数で[]の中はキーです。


        if op == 'QUERY':
            transaction = event['Keys']
            return op_query(transaction)
        elif op == 'HELLO':
            return 'HELLO'

この一角ですが、Unityとか呼び出し元からHELLOを渡すとHELLOが返ってくるテストと、QUERYを渡すとクエリの内容がズラーーー!っとかえって来る2種類のオペレーションに答えられるようにしています。
慣れてきて、もっと専用の命令を増やしたかったらココを増やしてみてください。

def op_query(transaction):
    key = 'application'
    sortkey = 'table'

QUERYを設定した時にこの関数が呼ばれます。
transactionの中身はUnityから渡す予定のKeys下のappilication,tableがそれぞれdict型で入ってます。

    queryData = table.query(

ついに来ました。実際にDynamoDBへの問い合わせです。
tableと言うのは説明していませんでしたが、上の方にある
table = dynamodb.Table("GameDatas")
この行で定義しています。dynamodbから作ったGameDatasテーブルを使いますって言う感じ
これに対してクエリを投げます。

        KeyConditionExpression = Key(key).eq(transaction[key]) & Key(sortkey).begins_with(transaction[sortkey]) 
    )

検索条件のキー設定です。プライマリキーは必須になります。
& Key(sortkey).begins_with(transaction[sortkey])
こっちのソートキーは任意で省略すると、キーに該当するデータを全て引っ張ってきます。
最終的にreturn queryDataこれでDynamoDBからの返り値がDict型でズラっ入り予備元に返します。
これを受け取れる所を目指します。
Lambda側のプログラムはこれで終わりです。

image.png
Lambdaが今回必要な権限を設定します。
上の方のアクセス権限タブから
image.png
実行ロール→編集
image.png
前に作ったロールを充てます。

【テスト】
image.png
上側のテストの←のドロップボックスでテストケースを設定できます。

テストイベントの設定をクリック
image.png
テストケースを設定→保存

テストの中身
{
  "Operation": "HELLO"
}

image.png
この状態で右上のテストをクリックすると、上の方に
image.png
実行結果ログが出ます。緑っぽければ正常終了赤っぽければアウトです。
結果に”HELLO”と出ていればひとまず起動OKです。

##ApiGatewayを触ろう
ApiGatewayとは、サーバーレスで立ってるApiサーバー(???)見たいなイメージです。プロセスだけ浮いてるみたいな。
UnityWebRequestからこれにPostしてLambdaをキックし、返り値をUnityWebRequestに返す所を目指します。
課金についてですが、これまでDynamoDB、Lambdaとほぼほぼ無料で賄えました。
が!こやつが唯一無料枠が無い存在になります。どのような感じかと言うと
100万リクエストで4.25$とちょっとお高いです。今回はマスタダウンロードなのでゴミみたいな価格になりますが、それでも課金発生要素なので注意が必要になります。
又、データ転送量も1Gで10円位なので、ちょくちょく大量データを落とす仕組みだと地味に聞いてきます。マスタならシーンの最初に落とす位が良いですね

早速作っていきます。
又左上AWSマークから、検索窓→Apiと打ちます。
image.png
ほんで、ApiGatewayを選択

image.png
Api作成

image.png
色々ある中からRestApiを選択。
(本当はHTTP Apiが安くていいです。お値段1/3。ただ認証キーが又別建てでLambda関数を作る必要がありますので、今回は簡単の為こちら)

image.png
Api名を適当に付けます。

image.png
アクション→メソッドの作成

image.png
POSTを選んで〇チェックボタンを押下します。

image.png
先ほど作ったLambdaDynamoTestを入力
(Lと入れると一覧が出てきますので、選択)
これがAPIコール=Lamdaキックの設定

image.png
権限を言うがまま追加→APIの作成ボタン押下

image.png
この時点で大体ApiGatewayが出来てますので、テストできます。
左ペインでPOSTを選択した状態で
ちょっと分かり辛いかも知れませんが、画面の「-POST - メソッドの実行」のPの文字の下あたりにある「テスト」と言う青文字をクリック

image.png
リクエスト本文に
{"Operation":"HELLO"}
を入れてテストボタンを押下

image.png
右側にLambdaからの結果で”HELLO”が出ていれば疎通成功!

image.png
CORSの有効化をします。
リソース→POST→CORSの有効化
これをしないと、ビルドして実行の段でコケます

image.png
「CORSを有効にして既存のCORSヘッダーを置換」押下
image.png
「はい、既存の値を置き換えます」押下

image.png
ここまで出来たら、デプロイします。
POSTを選択→アクション→APIのデプロイ

image.png
デプロイされるステージを新しいステージに選ぶと入力欄が色々でます。
ステージ名(ApiのUrl名につきます)だけ入れてデプロイ

image.png
画面上部に表示されたリンクがApiのUrlになります。赤い部分は作ったAPIによて異なります。メモしておいてください。

##セキュリティ高めたい人向け
以下はUrl野ざらしでだいじょうび??って人向けです。
左側ペインの
image.png
Apiキーから

image.png
Apiキーの作成

image.png
適当に名前を付けて保存

image.png
出てきた画面のAPIキーの横の青時の「表示」を押します。
出てきたキーをメモ帳とかに控えます。

image.png
又APIに戻ってリソース→POST→右側の四角の中のメソッドリクエストの青字を押下します。

image.png
APIキーの必要性の鉛筆マークを押下して、そのままTrueにします。
APIキーを効かせるAWS側の設定は以上です。
POST時にキーを入れないとアクセスできない状態になります。

以上長かったですが、AWS側設定になります。

#Unity側設定
unityからはUnityWebRequestでApiコールするだけです。
注意点としては、WebGLをターゲットにする為、Taskやその他の便利グッズは使わず、メインスレッドを意識しましょう。メインスレッド一本となると、どうせUIの更新は止まるので古きよきコルーチンで回すのが良いです。

このスクリプトを何かのオブジェクトに張り付けて実行してください。
スタートと同時にApiGatewayに接続に行きます。

ApiGatewayCall→Lambda→Dynamo→Lambda→Api→Unity
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.Serialization;
using UnityEngine.UI;

public class AwsTest : MonoBehaviour
{
    // Start is called before the first frame update
    [SerializeField] private string Operation = "HELLO";
    [SerializeField] private string application;
    [SerializeField] private string table;
    [SerializeField] private DynamoResponse response;
    [SerializeField] private Text responseText;
    
    [Serializable]
    public class DynamoIftest{
       
        public string Operation;
        public DynamoQueryKey Keys;
        [Serializable]
        public class DynamoQueryKey{
            public string application;
            public string table;
        }
    }

    [Serializable]
    public class DynamoResponse{
       
        public List<DynamoResponseItems> Items;
        [Serializable]
        public class DynamoResponseItems{
            public string application;
            public string table;
            public string test1;
            public string test2;
        }
    }
    void Start(){
        StartCoroutine(AwsApiTest());
    }

    private IEnumerator AwsApiTest () {
        var awsElement = new DynamoIftest() { Operation = Operation
            , Keys = new DynamoIftest.DynamoQueryKey(){application = application,table = table}
        };
        string jsonStr = JsonUtility.ToJson(awsElement);
            
        var request = new UnityWebRequest();
        
        //ApiGatewayで作成したApiのUrl
        request.url = "https://xrecu2qu6l.execute-api.ap-northeast-1.amazonaws.com/prod";
        var body = Encoding.UTF8.GetBytes(jsonStr);
        request.uploadHandler = new UploadHandlerRaw(body);
        request.downloadHandler = new DownloadHandlerBuffer();

        request.SetRequestHeader("Content-Type", "application/json");
        request.method = UnityWebRequest.kHttpVerbPOST;
        this.responseText.text = "Send Start";
        yield return request.Send();

        if(request.isNetworkError) {
            Debug.Log(request.error);
            this.responseText.text = request.error + ":" + request.uploadProgress.ToString() + ":" + request.downloadProgress.ToString();
        }
        else {
            if (request.responseCode == 200) {
                var responseJsonText = request.downloadHandler.text;
                this.responseText.text = responseJsonText;
                Debug.Log(responseJsonText);
                response = JsonUtility.FromJson<DynamoResponse>(responseJsonText);
            } else {
                Debug.Log ("failed");
                this.responseText.text = request.error + "*" + request.uploadProgress.ToString() + "*" + request.downloadProgress.ToString();
            }
        }
    }        
}

又一つづつ解説しますね。

        StartCoroutine(AwsApiTest());

Startでこれが呼ばれます。1フレームで収まらない可能性も当然あるのでコルーチンで投げます。

        var awsElement = new DynamoIftest() { Operation = Operation
            , Keys = new DynamoIftest.DynamoQueryKey(){application = application,table = table}
        };

Lambdaで使うパラメータを設定します。今回はオペレーションと、問い合わせる際のアプリケーションとテーブルを設定します。

        string jsonStr = JsonUtility.ToJson(awsElement);

投げられるように平文にします。

        var request = new UnityWebRequest();
        
        //ApiGatewayで作成したApiのUrl
        request.url = "https://*********.amazonaws.com/prod";
        var body = Encoding.UTF8.GetBytes(jsonStr);
        request.uploadHandler = new UploadHandlerRaw(body);
        request.downloadHandler = new DownloadHandlerBuffer();

        request.SetRequestHeader("Content-Type", "application/json");
        request.method = UnityWebRequest.kHttpVerbPOST;
        this.responseText.text = "Send Start";
        yield return request.Send();

投げて待ち
request.urの部分は自分で作成した時に作られたApiGatewayのURLに置き換えてください。

            if (request.responseCode == 200) {
                var responseJsonText = request.downloadHandler.text;
                this.responseText.text = responseJsonText;
                Debug.Log(responseJsonText);

他のエラーの所は置いておいてresponseCodeが200(成功)でかえって来たところを見てみてください。
ここで、取れた後何するって言う処理を書きます。

                response = JsonUtility.FromJson<DynamoResponse>(responseJsonText);

帰って来た値のクラス化。
平文が帰ってきます。

    [Serializable]
    public class DynamoResponse{
       
        public List<DynamoResponseItems> Items;
        [Serializable]
        public class DynamoResponseItems{
            public string application;
            public string table;
            public string test1;
            public string test2;
        }
    }

dynamoDbの返り値は
Items[テーブル]
見たいな形でかえって来ます。
Itemsは固定です。その下は任意のテーブルの型の配列でかえってきます。
シリアライズ可能なItemsと言う項目名のリスト型であれば、その下は考えなくても同名の項目につっこんでくれます。
では、いよいよ繋いでいきます。

image.png
クエリは置いておいて取り敢えず、疎通確認

ヒエラルキー上にテキストを置きます。(ただの確認用)
image.png
設定はこんな感じで大きめに

image.png
ResponseTextにテキスト項目をヒエラルキーから設定

image.png
OperationにHELLOを入れて実行
image.png
image.png
テキスト項目やログにHELLOがでればOK

image.png
クエリを実行してみます。
試しにDynamoに何か登録してみます。
dynamoに戻って、青い項目の作成と言うボタンがデータ登録ボタンです。
適当にデータを入れます。

image.png
+ボタンを押して列を追加できます。
前述したとおり、Keyが重要になってきます。キー二つは入れないとダメです。

image.png
逆にそれ以外はどうでもよく、ある行はtest1と言う列を持っている

image.png
ある行はtest2と言う列を持っているなんていうイカシタ構成もOKです

image.png
全容

image.png
この状態で実行

image.png
image.png
入った入った。applicationがaaaでtableがoreoreから始まる全行を取得できました!

image.png
ビルドして実行
無事WeBGLからデータ引っ張れました!
パズルゲームや地形データなんか、ここに入れてグニグニ変えて遊んでもらったりデイリーで配信なんて事も可能ですね!

う~んWebからDB引っ張れるとゲームに限らず色々夢が広がる!

次回はもう少し応用編書いてみます。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?