1章:サーバサイドと仲良くなろう
サーバサイドって何をしてるの
クライアントサイド、あるいは通信を含まないゲームを作っている人からすると、サーバサイドの人は「何か凄い事をやってる人!」という印象になりがちです。 もちろん凄い事はやっているのですが 一体どんなことを知っておくとサーバサイドの人、あるいはサーバと話が出来るようになるでしょうか。
正確な知識を求める場合は基本情報を勉強したり、ネットワーク通信の技術書を読むのが良いですが、この章は最低限、クライアント側が覚えておくと嬉しい事に絞って書いていきます。
通信の選択肢
「独自実装の通信」か「HTTP規格に従った通信」の2択ですが、現状は「HTTP通信」が一般的です。(HTTPとHTTPSは同一扱いとします)
むかしはレスポンスや通信データ容量の削減のため独自実装の通信を採用することもありましたが、最近は様々な要因により、HTTP通信を使用します。
それに併せて開発関連のソフトウェアもHTTP通信を使うことが前提となっています。
HTTP通信って何
一言で言うとどんなもの?
サーバに対して何らかの情報を送って、サーバから何らかの情報を受け取る、という時の標準プロトコルです。
例えばWebブラウザは「僕はGoogle Chrome です。Yahooから検索してこのサイトに飛んできました」という情報を Github.com に送って、Githubのサーバはあなたにサイトの中身のhtmlを返しています。
クライアントのC# + Unityに対してサーバサイドはPHPやGoやRubyやPerlなど、様々な別の言語で書かれています。これら別の言語で書かれたサーバと通信できるように決めた約束事の一つがHTTP通信(HTTP1.1)だと思っておくと良いです。(注:正確には違います)
この規格があるおかげで、サーバとクライアントは別の言語で書いても大丈夫です。
Unityの場合、 HTTP通信をするときに C#(.NET)のHttpClientクラスを使うことも可能ですが、大体の場合UnityWebRequestを使うことが多いです。
メモリ的にも、UnityAPIとの親和性的にもUnityWebRequestがおすすめです。
(UnityWebRequestをベースにしたhttpクラスの実装例: https://github.com/sassembla/Autoya/blob/master/Assets/Autoya/Connections/HTTP.cs )
HTTP通信の概要例
- お知らせ一覧(例: https://example.com/api/information )を取得する場合
ここで使う用語
用語 | 意味 |
---|---|
送信 | 電話の発信、連絡したい先へお問い合わせをすること |
受信 | 送信した相手から、返事が返ってくること |
- unity側でhttp通信を送信する
- 番号付きリスト1-1
- 番号付きリスト1-2
- お問い合わせ窓口であるサーバから受信した内容を解析する
- ゲームの画面に反映する(Loadingが終わってリストが並ぶイメージ
受信したデータの例(うわ、謎の書式だ!と思ってビックリした人、大丈夫です。下にあるJson のところで説明しますので!)
{
"list": [
{
"id":1,
"title": "夏祭りイベント開始",
"description": "運営です。 夏祭りイベントを開始いたしました。",
},
{
"id":1,
"title": "花嫁イベントのお詫び石送付のお知らせ",
"description": "詫び文章がずっと続く",
},
]
}
Unityの画面上の反映例はこんな風になったりします。
お知らせカテゴリ | タイトル |
---|---|
お知らせ | 夏祭りイベント開始 |
お知らせ | 花嫁イベントのお詫び石送付のお知らせ |
これの理解や実装が不完全だと何が起きるの?
サーバエンジニアと話が通じなくなります。
詳しく知りたい人は、この辺の単語でググってほしい
- HTTP1.1
- UnityWebRequest
HTTPのヘッダ、ボディ、request、response
HTTP通信は様々な要素があって大変ですが、Unityでサーバと通信するときは GETとPOSTの2種の HTTP Request Method を覚えておけばほとんどの場合足ります。 (PUTとDELETEはUnityバージョンによって実装が怪しかったことがあった気がします)
HTTP1.1の規格自体に踏み込んだ話はしないので、凄く大雑把な説明をすると
クライアントからサーバに情報を送る時は Request(リクエスト)通信と言い、サーバがそのお返事をクライアントに返す時はResponse(レスポンス)通信と言います。
また、リクエスト、レスポンス通信にはそれぞれヘッダ(追加でkey-valueを好きなだけ詰められる場所)とボディ(本文)の二か所に情報を詰めることができます。
このHTTP1.1通信をサーバと行う、というのをAPI通信と言ったりします。そしてAPI通信を行う相手のサーバをAPIサーバと呼びます。クライアントエンジニアが一番多く対話するサーバは、このAPIサーバです。
通信形式はGET(リクエストボディが無い時に使う)かPOST(リクエストボディがある時に使う)の2種がメインです。ソーシャルゲームでは100個APIがあるとしたら、POSTが80個くらいを占めることも多いです。
以下にUnityWebRequestで言うとヘッダはどこに入れるの、ボディはどこにあるの、というサンプルを示します。
string userStatus = JsonUtility.ToJson<PlayerStatus>(someData);//こうして、リクエストボディ(本文)を文字列にします
var request= UnityWebRequest.Post(url,userStatus);//Postの第二引数がリクエストボディの文字列です
request.SetRequestHeader(dic.Key, dic.Value);//こうしてリクエストヘッダを付与する
var p = request.SendWebRequest();//ここでPostアクセスする
//ここでpの完了を待つ
var responseCode = (int)request.responseCode;//レスポンスコード、200は成功。404は見つからない、401が認証エラー、503がサーバが混んでる、みたいなやつ
var responseHeaders = request.GetResponseHeaders();//レスポンスヘッダはこうして取得します。
var result = Encoding.UTF8.GetString(request.downloadHandler.data);//全然直感的じゃないですが、ここにレスポンスの本文が入ってます。
//ここでresultをまたJsonUtilityなどを使ってデシリアライズする。
会話例「このAPIってGETになってるけど、クライアントから現在の所持コイン数を渡してあげたいからPOSTにした方が良くないですか?」
通信エラーのリトライ処理をしよう
日本国内でスマートフォン向けのゲームを展開する場合、地下鉄含む通勤時のユーザを無視できません。 HTTP1.1は規格上、送信経路でデータが壊れないような仕組みや、通信が断続的でもなんとか通信完了しようと粘り強く再送信、再受信を行うようになっています。
しかし通勤中のスマートフォンは、電波が悪くなることもあるため、Http通信をおこなっていても、一度の通信では失敗(Timeout)することがあります。
なので一度Timeoutしても再度同じ内容でhttp通信を行う仕組みを作っておくことが一般的です。Timeoutが5秒だとすると、最長10秒くらいはユーザが我慢してくれそうです。
(ここまで読まれた方、UnityWebRequestを直接使うのは結構しんどいな…と気付いた方も多いかと思います。大体の場合、このUnityWebRequestをラップした、自前HttpClientクラスみたいなのを作ってリトライ処理などを隠蔽します)
なお、リトライは不慮の事故により同じリクエストが複数回送られてしまうことがあります。
その場合、サーバーエラーが帰ったり、1回使ったはずが2回使ったことになってしまうなど様々な現象が発生するのでクライアントが送っているリクエスト自体にIDを割り当てる、敢えてリトライしない通信と分けるなど対策が必要な場合もあります。
アクセストークンを毎回変えて一個前のトークンとレスポンス全文を保持しておくと、リトライで届いた通信は一個前のトークンから判断して一緒に保存したレスポンス全文をもう一回送信する事でサーバー内のデータを全く操作せずにリトライへの対応を簡単に行えます。
Jsonって何
GETメソッドやPOSTメソッドなどを使ってサーバと通信するわけですが、それだけでは情報が足りないことが多々あります。(Google検索したときのURLみたいにする手もあるんですが、人間に優しくないので...)
そんな時は、 リクエスト、レスポンスのボディに情報を追加 してやり取りを行います。
その情報の形式としてメジャーなのがJsonになります。
Unityの組み込みクラスで言うと、 JsonUtilityというクラスが、このJson形式を取り扱うクラスです。
Jsonは平たく言うと「クラスの変数一覧を特定の書式に変形して作られた文字列string」です。 特定の書式、というルールがあるので
C#のHogeクラス→Hogeクラスの変数をまとめたJson文字列(Jsonのシリアライズ、と言います)
Hogeクラスの変数をまとめたJson文字列→C#のHogeクラス(Jsonのデシリアライズ、と言います)
それだけではなく Hogeクラスの変数をまとめたJson文字列→PHP(あるいはRuby,Goなどサーバサイド)で定義したHogeクラス
への変換も特定の書式、というルールがあるので実行可能です。すごい!
つまり、以下のように相互変換が出来ます。
C#上のHogeクラス⇔Hogeクラスの変数をまとめたJson文字列⇔PHPやGo上で定義したHogeクラス
以下に例を書きます。
つまり、C#でこうやって作ったクラスは
[Serializable]
public class PlayerStatus
{
public string player_name;
public int player_exp;
public int player_money;
}
jsonはこうなります
{
"player_name": "taro",
"player_exp": 1000,
"player_money": 65534
}
goだと
type PlayerStatus struct {
Name string `json:"player_name"`
Exp int `json:"player_exp"`
PlayerMoney int `json:"player_money"`
}
こうなります。これらの間で相互変換が出来ます。
最初にこの仕組みを見たときに僕は感動しました。
注意 なおJsonのデータ構造がサーバとクライアントで違う場合、受け取った側がエラーを吐くので、データ構造のすり合わせはきちんとしましょう。(ユーザーIDのリストが欲しいのに、ユーザーIDが10個個別に送られて来てパース失敗!!みたいな話です)
tips:プロジェクトによって、Jsonを取り扱うクラスがJsonUtilityじゃなくて独自だったり、utf8Jsonなど外部ライブラリの事もあります。あるいは メッチャ最新のJsonみたいなgRPC という別の仕組みを使ってる可能性もあります。参加したプロジェクトはJsonライブラリに何を使っているか確認して、なるべくそのライブラリを使いましょう!(データ構造の仕様書を書く →「フォーマットの選択肢」を参照ください)
Unity標準のJSONパーサーは型定義が分かっていればstructにスポンと入れてくれますが、任意形式のJSONには全く対応出来ないためサードパーティのパーサーを用いる必要があります
データ構造の仕様書を書く
データ構造の仕様を考えることそのものは難しくありませんが、複数人でその仕様を理解するために仕様書を書くことは大切です。しかし仕様書を一から書くのは大変で、プロジェクトによって仕様書の書き方がまちまち違うことがあります。
この問題を解決する便利なフレームワークがあります。代表的なのは OpenAPI(Swagger) と API Blueprint です。これらは直接アプリケーションに組み込むわけではありませんが、書式の構造がシンプルで覚えやすく、エディタが豊富でモックサーバも簡単に作れるので、ぜひ利用してみてください。使う前にサーバエンジニアにどのフレームワークを使っているか確認しておきましょう。
これの理解や実装が不完全だと何が起きるの?
ワードやエクセルを駆使して頑張って仕様書を作ることになり、仕様変更のたびに苦労することになります。
フォーマットの選択肢
最もメジャーなテキストフォーマットはJsonですが、バイナリーフォーマットのMessagePack や Protocol Buffers も採用することがあります。
こちらの方が通信量やパフォーマンスの観点では優れている場合が多いです
(デバッグしやすさにおいてはjsonに軍配が上がります)
開発中はデバッグが容易なjsonで行い、開発末期にパフォーマンス・チューニングのためにバイナリーフォーマットが変わることはしばしばありました。
※なお、Protocol Buffers はIDL(インタフェース定義言語)が必須のためデータ構造の仕様書のような役割も果たします。
詳しく知りたい人は、この辺の単語でググってほしい
- OpenAPI(Swagger)
- API Blueprint
- RESTful API
- Json
- MessagePack
- ProtocolBuffers
通信が永続的か永続的じゃないか
いきなり永続的、と言われてもびっくりしてしまいますよね。
ネットワーク通信は、便宜上永続的なものと、永続的じゃないものがあります。
永続的、というのは「送ったデータをサーバ側で受け取ったあと、サーバ側で常に保持する必要がある通信」だと思ってください。
例えば野球で考えると、ピッチャーが何球目にカーブを投げて、バッターはバットを1.67秒後に振り始めて1.92秒時点でバットに当たって二遊間を抜けて… のような細かい情報は、全部サーバに保存していたらログが大変なことになってしまいます。
なので「巨人対阪神は6/20日の試合で4:1で決着」みたいな試合結果だけは永続的に保存しておこう。みたいな感じです。
リズムゲームだったら「この曲を難易度HARDで遊ぶ」「結果のスコアは4780点」みたいな通信は永続的です。
リアルタイム対戦系のゲームであれば、対戦中の様々な情報は大体が非永続的なデータで、試合結果だけが永続的、となります。 非永続的なデータはUNETだったりPHOTONだったりMONOBITだったり自前のリアルタイムサーバで処理して、永続的なデータだけはHTTP1.1のPOSTやGETで通信します。
リアルタイム対戦系のゲームではない場合は、ほとんど全てのサーバとの通信は永続的になります。 リアルタイム対戦系のゲームの場合は、 この通信処理は永続的にすべきかどうか 、を考えなければいけません。
tips:開発チームに参加したら 「このゲームってリアルタイム通信はありますか?」 と聞いてみましょう。無いようであれば、あなたが考えるべきことは、永続的な通信だけになります!
非永続のリアルタイム通信
上で説明したようにHTTP1.1の通信はヘッダ情報などの様々な追加データが載っている為、FPSのキャラ移動など秒間10回以上更新されるリアルタイム情報に使うにはオーバーヘッドが大きすぎます。 なので、FPSゲームなどではリアルタイム通信を永続的(HTTP1.1)ではない普通のUDPやTCPベースで通信します。後述するPhotonなどがこの方式です。
Photonか自社製か
マッチメイキング
暗号化通信(SSL通信)
ところで、大体のソーシャルゲームはJsonという書式の文字列をUnityクライアント側とサーバの間でHTTP通信を使ってやりとりします。
ということは、パケットキャプチャで見たら文字列はダダ漏れでは?という疑問がわいてくる方も居るかと思います。
はい、そのままではダダ漏れです。具体的に言うと、船を擬人化したソーシャルゲームがリリース当初はダダ漏れでした。
Jsonの文字列を解析すると「サーバに何のデータを渡しているか、サーバからどんなデータを受け取っているか」が分かるので、解析を試みたり、自動化ソフトを作られたりします。 あまり嬉しくないですね。
そのため、現在においてはソーシャルゲームの通信暗号化をするのが一般的です。
クライアント側で大まかに「どのくらいセキュリティ的に気を遣ってるのかな」をなんとなく把握するための指標を以下に書きます。
レベル1:ダダ漏れ
レベル2:SSL通信でサーバとクライアントがやりとりする(サーバのurlがhttp:// ではなく https:// になってたらレベル2です)
レベル3:レベル2に加えて、request bodyやresponse bodyにAESなどの暗号化したJsonを渡す
というレベル順に強固になります。 レベル2も証明書偽造したWireSharkでは覗かれてしまうので、それでも大丈夫であればレベル2,セキュリティを重視するならレベル3、という選択になるかと思います。
tips:もしチームに入って「APIサーバのURLはここ http:// hogehoge.com 」と教えて貰ったら「本番はhttpsになりますよね?」とサーバエンジニアの人に確認しておきましょう。万が一、本番もhttpのまま進みそうだったら、かなりヤバい予感がします。
最近は実際の端末でも Let's Encrypt が発行する証明書で https 通信を行えるし、HTTP通信を行う対象ホストを登録したりと手間もかかるので、LAN内にサーバーがあってインターネットから通信出来ないなど特別な理由が無ければ開発用サーバーでも https を使用する事をお勧めします。
クライアント上のデータは信頼してはいけない
これまでのゲーム運営の歴史上、クライアント側にセーブデータを置くと改ざんされてしまい運営が成り立たないことが分かっています。 このため、基本的にサーバ側がデータを持っていて、クライアント側におくものは書き換えられてもそれほど問題ないものと、各ユーザの認証に関わるデータとなります。
例えば、魔法石の所持数データを表示の為にクライアント上に保存(キャッシュ)してもよいが、実際に使う場合は必ずサーバ側に問い合わせるようにします。 ここでそのリクエストが不正なものでないことを確認するための手法として次の「認証」があります。
認証って何
一言で言うとどんなもの?
ソーシャルゲームはセーブデータという概念がなく、大体サーバ上でセーブされています。 このセーブデータがAさんのもの、という保証をするために、認証というのが必要になります。
歴史的経緯として、端末固有の(アプリ再インストールでも消えない)固有の長い文字列(UDID)というハードウェアに紐づくデータを、ユーザ認証に使えた時代がありました。異なるサービスを運営していても、同じUDIDが使われていれば同じユーザーによるものであることが推測できるので便利でした。
しかし昨今のプライバシー保護の観点から、AppStore,GooglePlayStoreともに、UDIDを取得するアプリは審査時に却下されます。
UDIDの代わりに使われるようになったのが UUIDという物で、文字列の長さはUDIDと同じくらい長いですがハードウェアに紐づかない(端末初期化とかでも変わる)値です。そのためプライバシー保護も満たしています。
Unityでこれら一意に決まっている値を使う場合は
SystemInfo.deviceUniqueIdentifier
を端末識別に使っている人が多いと思います。これは最新のiOSやGooglePlayStoreの規約に合わせて取得できる値が変わっていて、Unity5.x以降ではUUIDが取得できます。
これの理解や実装が不完全だと何が起きるの?
AさんのアカウントにBさんがログインできて、ヤバい事になります。
あるいは、Aさんが昨日遊んだゲームを今日再開したら、また初めからになってしまいます。
詳しく知りたい人は、この辺の単語でググってほしい
- 匿名ログイン
- UUID
認証方式CookieとJWT
上に書いたように、サーバ側でユーザを特定しておかないと、データの不整合が出てしまいます。
でも、毎回メールアドレスとパスワードを使ってログインさせると、インターネットのセキュリティ的に嬉しくありません。(そんなに頻繁に変更しないセキュリティ情報を、頻繁にインターネット経由で送らない、というのはネットワークセキュリティの原則です)
なので、メールアドレスとパスワードの代わりにCookieとJWTという仕組みを使うことがソーシャルゲームでは多いです。以下で説明します。
Cookieって何
Cookie(クッキー)とは HTTP通信の時にヘッダに付けて送信される短いテキストデータです。 CookieはWebブラウザでもっとも簡易にセッションを識別する方法として用いられます。 セッションとは、Webサーバが接続クライアントを忘れないように付けておく仮の名前です。
まず、Webブラウザが最初にアクセスしてきたときにWebサーバーが「じゃあ、君は次から”7月7日のUnity太郎”と名乗りなさい」とレスポンスヘッダで伝えます。
Webブラウザが次のアクセスの時に、その通りに名乗れば、Webサーバは覚えていてくれて、認証したユーザー情報やショッピングカートなどの状態を引き継ぐことができます。
このときにもしWebブラウザが名乗らなければ、前回の情報はなかったこととなり、また新しい名前が付けられます。
ここで覚えておいてほしいのは、Cookieはただの個体識別情報で、認証情報が記憶されているわけではない、ということです。 認証情報を持っているのは、あくまでWebサーバー側です。
詳しく知りたい人は、この辺の単語でググってほしい
- Cookie
- Cookie セキュア
JWTって何
JWTは JSON Web Token の略称で、「ジョット」と発音されます。 改ざん検知が出来る文字データの形式です。
JWT自体は、文字列の書式で、Base64エンコードされたヘッダ、ペイロード、署名を”.”(ピリオド)で繋げたものです。 JWT自体は暗号化されているわけではないので、受け取れば誰でも読むことができます。
署名は、ヘッダで決められた計算方法で、ヘッダとペイロードを元に計算される文字列で、ペイロードが改ざんされていないか検知するのに用います。 一度JWTを発行した後にペイロードだけ変えてしまうと、署名が合わなくなり、改ざんが行われたことがバレます。
詳しく知りたい人は、この辺の単語でググってほしい
- JWS
- JWT
- RFC7519
JWTを認証に使う
JWTだけだとただの文字列なので、これをどう使えば認証として使えるか考えていきましょう。 サーバーがJWTのペイロードに、発行時のIDと有効期限を書き込んで、クライアント側に定期的に更新させます。 一度発行したJWTは有効期限が切れるまでサーバー側で有効とされます。 例えばGoogleのサービスの一つ、FirebaseではJWTの有効期限は1時間とされています。
また、モバイルアプリとブラウザの連携など、cookieを直接渡せない時でも、毎回のやりとりにJWTを付与すれば、認証情報の引き回しが可能となります。
ただ、自前でこの仕組みを実装するとなると、かなりの労力を要するので、アプリケーション開発者であれば、既にあるライブラリを便利に使うほうが安全です。 (例: https://github.com/monry/JWT-for-Unity )
参加したプロジェクトで「アクセストークン」「リフレッシュトークン」という言葉が聞こえてきたら、JWTを使っていると思っておきましょう。
詳しく知りたい人は、この辺の単語でググってほしい
- リフレッシュトークン
- RFC7523
- JWE
- ステートレスJWTとステートフルJWT
- Firebase認証
まとめ
ブラウザの機能が内包されていて、外部との連携を行わない場合、Cookieを用いるのが楽で確実です。 Cookieでは乗り越えられない制約を乗り越える時に、JWTなどOAuth2の仕組みを使うと良いでしょう。
匿名ログイン
ゲームを開始したユーザはメールアドレスとかパスワードをわざわざ登録したくないです。(アカウント登録を必須にすると初期離脱率がすごいことになります)
一方で、開発者としてはユーザのアカウント情報(というより、同一端末かどうか)をセーブデータ保持の点からも確認させて貰いたい。
という二律背反が有ります。
これを解決する仕組みが匿名ログインです。いろいろなパターンがありますが、以下に処理内容の一例を挙げます。
SugoiGameという凄いゲームがあって、 SugoiGameStudio.com で運営しているとします。
- ユーザAさんはゲームを起動した初回起動時に
SystemInfo.deviceUniqueIdentifier
を取得してAPIサーバのアカウント登録APIにPOSTします。(request bodyに入れてPOSTします) - APIサーバはAさん用に a_sanSenyou_sugokunagai_mailaddress@sugoigamestudio.com という仮メールアドレスと、 sugokumuzukasiipasswordde_nagasa128moji みたいな仮パスワードを生成します。(実際は乱数を混ぜますよ!)
- APIサーバは2.で生成した仮メールアドレスと仮パスワードをアカウント登録APIのresponse bodyに含めてresponseを返します。
- Aさんは3.で仮メールアドレスと仮パスワードをAPIサーバから受け取りました。Aさんの端末内に 暗号化した 仮メールアドレスと仮パスワードを保持します。
- 以降のゲーム起動時は、4.の仮メールアドレスと仮パスワードを元にAPIサーバのログインAPIにログインをすることが出来ます。
- とは言え、基本的にはこの仮メールアドレスと仮パスワードを使ったログインは毎回ではなく、大体はクッキーあるいはJWTでのログインをします。あくまでこれは保険ですね。
すごい仕組み!便利!!
しかし、このログイン方式ではプレイヤーの同一性を保証できないので機種変更時の引継ぎに対応できません。
機種変更時の引継ぎ対応にはメールアドレスとパスワードを登録してもらう必要があります。そのため、ゲーム内のアカウント情報のところからTwitter連携やメールアドレス登録などが出来るようになっています。
(お手持ちのゲームを見てみてください。大体そうなっていませんか?)
認証を含むHTTP通信
ソーシャルログインをしよう
ユーザがゲームを遊ぶ時の障壁を出来るだけ下げて、インストールして遊んでもらう人を増やす。というのがソーシャルゲームの大前提になります。 そのためには、ゲームの為だけに専用のメールアドレスとパスワードを登録してもらう、と言うのは相性が悪いです。
「とりあえずゲームを始めてもらう」ということであれば、上にあげた匿名ログインの仕組みによって実現できますが、そのままでは機種変更や(うっかりすると)再インストールなどの引継ぎが出来ないという問題があります。
そのため、メールアドレスとパスワードではなく、多くのユーザが既に使っているサービスのアカウントを使ってゲームにログイン/登録できるようにする、というアイデアが生まれました。 これがソーシャルログインです。
現在において、日本国内で配信するゲームでよく検討されるのは
- Twitterログイン
- LINEログイン
- facebookログイン
- Apple Game Center
- SignInWithApple
- GooglePlayServices
あたりです。
iOS用アプリで外部認証を用いる場合は SignInWithApple も対応させないとリジェクトされる可能性があるため、これは対応しておきましょう。
クライアントエンジニアは実装の詳細を知る必要はありませんが、どういった事に気を付けておくと良いかを以下に書きます。
mBaasを使う
自社基盤を作るか使う
SignInWithApple
GooglePlayServices
Twitter
詳しく知りたい人は、この辺の単語でググってほしい
- OAuth2
時刻の取り扱い
一言で言うとどんなもの?
アプリ単体で完結しないソーシャルゲームは、サーバーで開催されるイベントなどリアル時間と紐づいた挙動を行います。
プランナーが用意したゲームの挙動を切り替える時刻データは、必ずタイムゾーンを含んだ形で保持しましょう。これはサーバー、クライアント両方のプログラムが対象です。
Unityだと DateTimeOffset.UtcDateTime を使うことが多いです。
これの理解や実装が不完全だと何が起きるの?
- スマホの端末時刻を操作すると何回もデイリーボーナスがもらえる凄いバグが生まれます
- 夏時間(サマータイム)がある国だけイベント時刻がずれて面白い問い合わせが増えます
詳しく知りたい人は、この辺の単語でググってほしい
- タイムゾーン
- RFC3339
例
「0:00」にデイリーボーナスを配布する!と考えた場合、それは日本時間の0:00である、と考えがちです。 しかし、昨今日本以外のユーザもいます。そのようなユーザに取って日本時間0:00というのは次のようになります。
- 日本時間の1/1 0:00にイベント発生!→北京12/31 23:00,ロンドン12/31 15:00,ニューヨーク12/31 10:00,ロサンゼルス12/31 07:00
- 日本時間の8/1 0:00にイベント発生!→北京7/31 23:00,ロンドン7/31 16:00(夏時間),ニューヨーク7/31 11:00(夏時間),ロサンゼルス7/31 08:00(夏時間)
デイリーボーナスなどのイベントを発生させる場合、世界一律のタイミングとするか、ユーザのスマホ上の時刻を基準にするかを決めておきましょう。
日跨ぎ対応
デイリーボーナスの配布などは、ログイン時のAPIで叩くことが多いと思われます。 そのため、日付またぎをクライアント側で検知した際にタイトル画面やホーム画面へ遷移させ、ログイン処理からやり直させる必要があります。 ただし、全ての画面から強制的にタイトルへ戻してしまうと、途中中断などによりユーザー体験を損ねてしまうこともあるため、キリの良い画面で再ログインをさせるなどの配慮があるとより好ましいでしょう。
また、日またぎの時間は必ずしも午前0時ではなく、例えば午前4時などに設定されているタイトルも多く存在します。 ログイン中のユーザーが少ない時間に敢えて日またぎのタイミングをズラすことにより、前述したユーザー体験の低下やサーバ負荷などを避ける効果が見込めます。
クライアントプログラムでの時刻の取り扱い
ユーザのスマホが動いている場所は地球上のどこか分かりません。なのでユーザーに見せる時刻情報はローカルタイムに変換して画面に表示する様にしましょう。日本の時刻と違っても泣かない様に注意して実装しましょう。
サーバープログラムでの時刻の取り扱い
なにはともあれ、サーバーの時計を合わせましょう。
ゲームシステムが用いる時刻は、APIコールされた時の時刻をずっと使いまわす事をお勧めします。
APIが叩かれてから返信文を生成するまでには結構時間がかかります。その途中で何か起きるたびに現在時刻を計測しなおして都度使うと、1回のAPIコール中に複数の情報を保存した際にその時刻が微妙にずれてしまいます。
そうすると1回の命令中に記録された情報なのか判別出来なくなり、トラブル対策などで手間が多くなります。
1回のAPIコールで起きた事を時刻付きで保存する際は、その時刻はAPIが叩かれたときに1度計測した時刻情報を使いまわしし、必ず同じ時刻が保存される様にしましょう。
データベースによっては、データベースプロセス内の現在時刻を自動で挿入する便利な命令がありますが、これで生成記録する時刻は参考程度にして、ゲームシステムには関与しない情報として取り扱いましょう。
クライアントの時刻合わせ
クライアントはAPIを叩く時に自分が思っている現在時刻を必ず投げる様にし、サーバーは届いた時刻が自分の物と大きくずれてたらエラーを起こしてゲームが進行しない様にしましょう。
アプリバージョンアップをしよう
一言で言うとどんなもの?
サーバに対してクライアントは自分のアプリバージョン情報とOS情報を送って、APIサーバから「このアプリは最新か」の結果を受け取ります。
最新ではない(あるいは、最低バージョン以下)だった場合、「アプリをバージョンアップしてください」とダイアログを出してストアのURLに誘導します。
ソーシャルゲームでよく遭遇するアレです。大体の開発現場では、Application.version ではなく独自にAndroidとiOSで別のバージョンを内部で持っている気がします。
Application.version は一度appleやgoogle等のプラットフォーマーに申請すると、次の申請時には必ず数字が大きくなってないと受け付けてもらえません。ですが、審査入りした後に社内でバグを見つけた時などは審査中のバージョンはスキップしてバグを直したパッケージをリリースするべく動く事があります。この様な場合には、内部バージョンとApplication.versionを別にしておくと、サーバーと絡む内部バージョンをずらす必要が無くなり都合が良いです。
これの理解や実装が不完全だと何が起きるの?
- ゲーム中にスリープして、一カ月後に再開したユーザが古いクライアントアプリのままAPIサーバにアクセス。APIの仕様が変わっている為謎のエラーをサーバが連発!
- リリースしたバージョンに致命的なチート可能バグを発見!Androidだけ先に審査を通ったけど強制アップデート機構が動かなかった!!死ぬ!
などがあります。
詳しく知りたい人は、この辺の単語でググってほしい
強制アップデートのチェックするタイミング
大体の場合は専用の [GET] /ios/version を使うより、httpのresponse headerで判定 することが多い気がします。 専用APIで判定する場合、スリープ復帰の関係でうっかりすり抜けることがある為です。(前項参照)
リソースバージョンとアプリバージョン
ソーシャルゲームの場合、先にシナリオやアイテムのデータだけを更新する。イベントのためにリソースデータだけ更新する。
という事が起きます。なので、UnityのApplication.versionではなく独自定義した
- AppVersion
- ResourceVersion(AssetBundleVersion)
- MasterDataVersion
を持っていることが多いです。
In-App-Purchase(アプリ内課金,IAP)
一言で言うとどんなもの?
楽しいアプリ内課金です。基本プレイ無料のソーシャルゲームにおいて、きわめて大事なマネタイズポイントです。
大体はUnity IAPのpackageをベースに自前で拡張します。
歴史的な、というか偉大なるプラットフォームパワーによって30%の手数料を取られるのがつらいですが、しかし課金迂回はあまりにリスキーです。 あきらめてプラットフォーム推奨の課金処理に従いましょう。
これの理解や実装が不完全だと何が起きるの?
- 100魔法石を買おうとした人が「このアイテムは購入できません」と出て死ぬ
- 1万円分の魔法石を購入失敗したユーザがアプリの再インストールをした結果、魔法石が消滅。会計処理も死ぬしユーザは激おこ
- 「僕は500万円分の魔法石を買ったのに付与されてない、補填してください」というユーザさんが出て死ぬ
詳しく知りたい人は、この辺の単語でググってほしい
- レシート検証
- transaction queue
レシート検証
主にサーバサイドでAppStoreやGoogleStoreに対してユーザがIAP支払いをした結果のレシートを受け取り、正規の支払いであるかを検証します。 レシート検証サーバを知識無しから作ると、1週間や2週間では足りないくらいに面倒くさいです。サーバエンジニアの人に感謝しましょう。 そしてクライアントアプリ側もレシート検証サーバに対して十分な情報を伝えるように努力しましょう。
例えばソーシャルゲームにおいてはクライアント側の課金では
- 「今からこの商品を買うよ」(これはAPIサーバに伝える)
- この商品を買います(これはAppStoreやGooglePlayStoreに伝える)
- (この辺で各ストアが決済を完了する。クライアントエンジニアは何も考えなくて良い)
- (この辺で各ストアのレシートをAPIサーバが受け取って、正規の支払いであることを確認する。クライアントエンジニアは何も考えなくて良い)
- 「この商品を無事買えたよ」(これはAPIサーバに伝える)
- (この辺でサーバ側で商品をプレイヤーに紐付ける。クライアントエンジニアは何も考えなくて良い)
- プレイヤーが最新の魔法石個数をAPIサーバから取得する
みたいな流れが正常系の通信になるかと思います。UnityのIAPライブラリは2.とレシートをクライアントアプリが直接受け取りする部分しか備えていないので、各社が自前でラップしたライブラリを作っているかと思います。
サブスクリプションの注意
一回限りの魔法石を買う、ではなくて毎月500円払うと便利にゲームを進められる。みたいなマネタイズがあります。
この場合は正常系である限り通常のIAPと考え方は同じですが、異常系のパターンが一気に増えるので、導入を行う際は異常系に思いを馳せてください。
例えば
- AppStoreと紐付けたクレカの有効期限切れ
- キャリア決済との乗り入れ
- 機種変更、iOSからAndroidやAndroidからiOSへの乗り換え時のサブスクリプション契約更新
などが出てきます。頑張っていきましょう!!!