ページへ戻る

− Links

 印刷 

Google App Engine for Python その2 のバックアップソース(No.6) :: NJF Wiki

xpwiki:Google App Engine for Python その2 のバックアップソース(No.6)

« Prev[5]  Next »[6]
前の記事はこちら。[[Google App Engine for Python]]

*ブラウザ上のカジュアルゲームデータの保存の概要 [#u36cbcbd]

Flashで動作するゲームのユーザーデータは以前はSharedObjectで保存できましたが、セキュリティなどの問題から現在はSharedObjectはデフォルトでは定期的に消えるようになりました。

そこで、サーバー上で保存するわけですが、そのために必要なデータは最低限以下の物が必要です。

-ユーザーIDとパスワード
-ゲームデータ

ユーザーIDとパスワードはユーザーを区別するために必要です。メールアドレスや他のサービスのオープンIDを使ってパスワード忘れ対策などを行うと良いかも知れませんが、カジュアルゲーム程度でメールアドレスや他サービスのIDを必要とすると、ユーザーがいやがる場合があります。また、そこまでしても特に個人情報などの重要データがあるわけでも無くそもそもセキュリティ的なリスクはありません。さらにほとんどのユーザーは一通りゲームをプレイすると再び同じゲームをプレイすることはなく、SharedObjectが消える期間を超えてプレイする人は少数派です。どの程度まで実装するかは微妙なところです。

ここでは最も簡単な、サーバー側でIDとパスワードを発行してユーザー側でメモしてもらう方法をとります。パスワード変更処理やパスワード忘れ処理は無しにします。IDは通番にし、パスワードはランダムな整数にします。

ゲームデータはjson形式で送ることにします。FlashだとAMFという優れたデータシリアライズ方法があり、GAEでも利用できるのですが、Flash以外ではそのままでは使えないという欠点があります。たとえばアプリ版は他の言語で作る必要が出たときに面倒なことになります。Flashがだんだんと使われなくなってきている状況を鑑みて、汎用性からjsonでやり取りすることにします。XMLでも良いです。ただjsonだと数値や論理型が型付きで送れたり大抵データの容量が少なくなるので私はこちらを使っています。また、GAEにはjsonデータをそのまま保存するデータ型が用意されているのでこちらの方が便利です。

GAEのデータ設計において注意しないといけないのは、1レコード読み込みあたりに課金されると言うことです。また、複数レコードを読み込むと処理も重くなります。よって一度の処理で読み込むレコード数は少ない方が望ましいことから、1ユーザー1レコードが理想です。通常のリレーショナルデータベースとは異なり、なるべく正規化はしないようにしましょう。

すると必要なテーブルは次の二つです。

-ユーザーID
-ユーザーデータ

ユーザーIDテーブルの方では通番のユーザーIDを作成するために最後のユーザーIDを保存しておきます。
ユーザーデータの方はユーザーIDとパスワードとゲームのデータを保存します。
*処理の流れ [#z08eead8]
全体の処理の流れは以下の通りです。

**登録処理 [#n8d8c653]
+ユーザーが新規登録を選んだ場合、サーバーで登録処理を行う
+サーバーから戻されたIDとパスワードをクライアントはSharedObjectで保存する

**ログイン処理 [#vfb97bf8]
+ShareObjectでローカルにIDとパスワードが保存されている場合や、ユーザーが手動でIDとパスワードを入力した場合、サーバーでログイン処理を行う
+サーバーから戻されたゲームデータでゲームを開始する
+ユーザーがIDとパスワードを入力していた場合は、それをSharedObjectに保存しておく

**データの保存 [#n64bc982]
+IDとパスワードとゲームデータをサーバーへ送信する
+サーバーではデータの整合性をチェックし、保存して結果を戻す


よって、ゲーム側には新規登録画面、ユーザーIDとパスワードを表示する画面、ユーザーIDとパスワードを入力してログインする画面が最低限必要です。もちろん、エラー表示画面なども必要に応じて作成します。

また、サーバー側には登録処理、ログイン処理、セーブ処理をするAPIが必要です。

*URL構成 [#r4de93f6]
以上のことを踏まえて、URLの構成は以下のようになります。

 /crossdomain.xml
 /api/entry
 /api/save
 /api/load

crossdomain.xmlはflashを別サーバーにWebで公開するときに必要です。それ以外のアプリなどでは必要ありません。
上記のものは最低限必要な構成で、実際にはルート以下にfavicon.icoやrobot.txtなどいろいろと入れたくなるので、煩雑にならないようapiは別ディレクトリにまとめます。

URLと実ファイルの対応は別途定義できるので、実際にはapi以下のURLは1つのファイルで処理します。

*静的ファイルの取り扱い [#p9129989]
crossdomain.xmlのような静的ファイルはapp.yamlの中でURLと実ファイルの対応を定義します。
具体的には、/crossdomain.xmlにアクセスがあった場合、static/crossdomain.xmlというテキストファイルが読み込まれるようにするには、app.yamlの中で以下のように定義します。

 - url: /crossdomain.xml
   mime_type: text/xml
   static_files: static/crossdomain.xml
   upload: static/crossdomain.xml

他にもいろいろな割り振り方がありますが、ゲーム保存用のAPIサーバーには静的ファイルはあまりないはずなので、これだけ覚えておけば十分かと思います。GAEでも静的ファイルは取り扱えますが、大量にある場合はそれを置くために通常のWebサーバーを用意した方が便利で安くつきます。

*フレームワークの導入 [#w185f5fa]

HTMLの入出力部分などを自分で一から作るのは面倒なので、GAEで用意されているフレームワークを使います。いろいろな物が用意されていますが、ここではwebapp2を使います。これを選んだのにはそれほど深い意味はありません。Googleの公式チュートリアルでこれが採用されていて資料が見つかりやすいのが理由です。ゲームデータを保存するだけで実際のWebページを作成するわけでは無いので、フレームワークについてはどれを使ってもそれほど差は出ないと思います。

これを使って前回の記事で作ったHello Worldを作り直すと以下のようになります。
まず、app.yamlです。

 application: hello-world-app
 version: 1
 runtime: python
 api_version: 1

 handlers:
 - url: /.*
   script: helloworld.application

ここで大きく変わったのはscriptのところがファイル名では無く、ファイル名+「.」+変数名となっているところです。これに対応したhelloworld.pyは

 import webapp2

 class HelloWorld(webapp2.RequestHandler):
     def get(self):
         self.response.headers['Content-Type'] = "text/plain; charset=utf-8"
         self.response.write("Hello, world!")

 application = webapp2.WSGIApplication(
                                      [('/', HelloWorld),
                                      ],
                                      debug=True)

となります。app.yamlでhelloworld.applicationと指定したせいで、helloworld.py内のapplicationというオブジェクトが呼び出されることになります。そのオブジェクトの中で"/"にはHelloWorldクラスを対応させているので、ルートへのアクセスに対して、HelloWorldクラスの処理が呼び出されます。

HelloWorldクラスにはgetメソッドが定義してあり、GETリクエストに対してはこれが呼び出されることになります。もちろんpostメソッドを定義することも可能です。

getメソッドの中は単にヘッダと本文をクライアント向けに書き出しているだけです。

元のHello Worldのサンプルに比べて複雑になったように見えますが、GETやPOST、ヘッダーの指定などをフレームワークでやってくれる上に、最後のapplicationの定義を変えていけばapp.yamlを変更せずにURLと処理の対応を変更していけるので、ある程度規模が大きくなるとこちらのやり方の方がずっと楽になります。

*GAEのデータ保存 [#v7706534]

GAEではデータはBigtableというものに保存されます。通常のリレーショナルデータベースに似ていますが、集計や表の結合などは自分でスクリプトを書かないと基本的には出来ません。1レコードごとに検索のためのキーがついているだけのデータの集まりの様な物で、どちらかというとファイルに近いかも知れません。公式ではリレーショナルデータベースと区別するためか1データの事をエンティティと呼んでいます。また、テーブル定義に当たる物はモデルと呼びます。リレーショナルデータベースに慣れている人には分かりづらいと思うので、このWikiでは基本的に1データはレコードと呼んでいます。モデルもテーブルと呼ぶかも知れません。他の資料に当たるときには注意してください。

カジュアルゲームのデータ保存の場合、集計や表結合は普通無いのであまり問題ないと思います。しかし、ランキングだけはちょっと面倒です。これについては別途機会を設けて解説する予定です。

また、GAEはデータの排他処理があまり得意ではありません。同時に1つのデータに大量にアクセスするような、たとえばアクセスカウンターのような物だとエラーが出る場合があります。これを回避する方法もまた別途解説する予定です。ただ、こちらもカジュアルゲームではそれほど使わないと思うのでさほど問題では無いでしょう。

一方、リレーショナルデータベースと違って、後からテーブルの列を加えたりするのは簡単にできます。また、データが分散されていて互いにバックアップされているので、サーバーの不具合でテーブルまるごと消えたりすることはまずありません。テーブルのサイズにも制限はありません。このあたりが1データがあたかもファイル的に扱われている所です。

では、テーブル定義にあたるモデルの定義を見てみましょう。ユーザーIDとパスワードとユーザーデータを保存するためのモデルは以下のようになります。

 from google.appengine.ext import ndb

 class UserData(ndb.Model):
     userId = ndb.IntegerProperty()
     password = ndb.IntegerProperty()
     data = ndb.JsonProperty()

これでユーザーIDとパスワードが整数、dataがJSONであるようなモデルとなります。
どんな型が使えるかはGAEのリファレンスで調べられます。
(
https://cloud.google.com/appengine/docs/python/ndb/properties
)
よく使うのは上記の整数とJson型、あとは以下の日付とString型ぐらいです。

 ndb.DateTimeProperty()
 ndb.StringProperty()

使い方は、保存はインスタンス化して値を設定してputするだけです。

 userData = UserData() #インスタンス作成
 userData.userId = 1
 userData.password = 12345
 userData.data = jsonData
 userData.put() #保存

読み出しはいろいろな方法がありますが、IDが1のものを1つだけ取り出すなら、

 userDatas = UserData.query(UserData.userId==1).fetch(1)

とすると長さ最大1のUserData型の配列として取り出せます。

集計や結合などをしないなら、ほぼ通常のリレーショナルデータベースと同じです。

以前にも書きましたが、GAEでは1レコード取り出す毎に課金されます。また、取り出すレコードの数が多いとパフォーマンスも悪くなります。なるべく少ないレコード数で処理が終わるように、正規化などはせず少々重複があっても1レコードにデータを多く詰め込んだ方が良いです。そのようなことを踏まえてデータ設計を行いましょう。

*データモデル [#g6b90e90]

では、ゲームデータの保存に必要なモデルを考えます。
[[ブラウザ上のカジュアルゲームデータの保存の概要>#u36cbcbd]]
で解説したように、必要なモデルは次の二つです。

-ユーザーID
-ユーザーデータ

ユーザーIDはIDをカウントしていくために必要で、ユーザーデータはユーザーのIDとパスワードを整数で、ゲームデータをjsonで保存します。するとそのモデルは以下のようになります。

 class UserIdCounter(ndb.Model):
     userId = ndb.IntegerProperty()

 class UserData(ndb.Model):
     userId = ndb.IntegerProperty()
     password = ndb.IntegerProperty()
     data = ndb.JsonProperty()

実際には登録日や更新日時、ゲームのバージョンなど、管理用のデータを加えることになると思いますが、最低限必要なのはこれだけです。


準備中

« Prev[5]  Next »[6]