Unityのセーブデータとタイトルには書いてありますが、実際はいろいろ保存できます。
参考:http://puyooboe.blogspot.jp/2015/12/blog-post.html
Unityでセーブデータっていうと基本的にPlayerPrefがあると思いますが、ローカルに保存するよりはデータがオンラインで見えてたらいろいろ情報がわかって嬉しいことってきっとあります。
(例えばシナリオ番号などをDatastoreで保存すれば、ユーザが全体的にどんなところで詰まっているのか、ログイン時間、セーブ時間を保存するようにすれば見ればログイン率はどうかとか見れそうだなという感じです)
自前でサーバ作っても良いと思いますけど、サーバ作ったりチューニングしたりデータベース作ったり…しかも運用まであるとするとなかなか手が出るものではないかなと思ってます。
GAEはインフラ知識があまりなくてもサーバサイドを構築できてしまうのでそういう意味でもとっつきやすさはあると思いますね。
これから書く内容を要約すると大きく分けて2つ:
サーバサイド(GoogleAppEngine/Go with Datastore)
・Datastoreに格納するためのRESTAPIサーバを作る。
・GoogleAppEngineにデプロイする。
クライアント側
データを書き込みたい時:
・Unityで保存したいデータ(今回はUserIDと位置データのみ)をKeyValuePairでまとめる
・WWWFormを用いて前述のAPIサーバにPOSTする
データを読み込みたい時:
・保存したいデータを読み込みたいときはDatastoreにKeyを渡してデータを引っ張ってくる。
という話をします。
注意:
セキュリティ関係は本質ではないので記事では省きますが、実際こういうことをするとしたら対策はほぼ必須でしょう。
#これからやること
こんな感じ
— Negipoyoc (@CST_negi) 2016年6月28日
SaveButtonを押すとDatastoreにそのキャラクターの位置情報が書き込まれます。
LoadButtonを押すとキャラクターの位置情報をDatastoreから拾ってキャラクターの位置を修正します。
#サーバサイドを構築する
いつものようにGo+ginで書きます。
application: hogehoge(ここはProjectIDを書いて)
version: 1
runtime: go
api_version: go1
handlers:
- url: /.*
script: _go_app
builtins:
- remote_api: on
↑app.yamlは https://console.cloud.google.com/iam-admin/projects で作ったプロジェクトに対してデプロイするときに必要になる。
package unitygae
import (
"ds4unity"
"log"
"net/http"
"github.com/gin-gonic/gin"
)
func init() {
server := gin.Default()
server.POST("/save", SaveToDS)
server.GET("/load/:id", LoadFromDS)
http.Handle("/", server)
}
func SaveToDS(g *gin.Context) {
httpRequest := g.Request
httpRequest.ParseForm()
ids := httpRequest.Form["id"][0] //UnityからPOSTされたname=idのValueを拾う
pos := httpRequest.Form["pos"][0] //UnityからPOSTされたname=idのValueを拾う
if err := DataStoreManager.PutSaveData(ids, pos, g); err != nil {
log.Fatalf("Error Occured to Put SaveData: %v", err)
}
//g.Stringで文字列を結果に出す必要はないが、表示させることでUnityでのデバッグがしやすい。
g.String(200, "Success")
}
func LoadFromDS(g *gin.Context) {
ids := g.Param("id")
var dat *DataStoreManager.SaveData
var err error
if dat, err = DataStoreManager.GetSaveData(ids, g); err != nil {
log.Println("Error Occured to Load from ds : %v", err)
return
}
//データストアに格納していたデータを文字列で返す(PositionのVector3データだけ)
g.String(200, dat.PositionData)
}
↑server.goとappyamlは$GOPATH以下に置かない。$GOPATH以下に置くとデプロイできない。
↑importの"ds4unity"は自作のライブラリ群です。以下に示します。
package DataStoreManager
import (
"fmt"
"log"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/net/context"
"google.golang.org/appengine"
"google.golang.org/appengine/datastore"
)
func PutSaveData(userID string, positionData string, g *gin.Context) error {
c := appengine.NewContext(g.Request)
saveTime := time.Now()
if userID == "" {
userID, _ = AllocateID(c, "SaveData")
}
saveKey := datastore.NewKey(c, "SaveData", userID, 0, nil)
saveData := SaveData{UserID: userID, PositionData: positionData, SaveTime: saveTime}
if _, err := datastore.Put(c, saveKey, &saveData); err != nil {
log.Fatalf("Error Occured when datastore.put: %v", err)
return err
}
log.Println("Data Put Success!")
return nil
}
func GetSaveData(userID string, g *gin.Context) (*SaveData, error) {
c := appengine.NewContext(g.Request)
saveKey := datastore.NewKey(c, "SaveData", userID, 0, nil)
saveData := SaveData{}
if err := datastore.Get(c, saveKey, &saveData); err != nil {
return nil, err
}
return &saveData, nil
}
↑セーブデータをDatastoreに書き込むPutSaveDataメソッド(userIDをKeyとしたセーブデータEntityを作ってdatastoreにPutしている)
↑セーブデータをDatastoreから読み込むGetSaveDataメソッド(userIDをKeyとしてPutしたのでセーブデータEntityをそのKeyからGetしてきている)
package DataStoreManager
import (
"time"
)
type (
//jsonタグ、bindingタグは別にいらなくて、将来の自分用に残しているだけです。
SaveData struct {
UserID string `datastore:"-" json:"id" binding:"required"`
PositionData string `datastore:"PositionData" json:"pos" binding:"required"`
SaveTime time.Time `datastore:"SaveTime"`
}
)
↑セーブデータの構造体。UserIDをKeyとして位置情報とセーブ時間を格納している。
これらをつくって
server.goとapp.yamlが入っているフォルダに
$cd [server.goとapp.yamlが入っているフォルダ]
$appcfg.py -A [GAEに作ったプロジェクトのID] -V v1 update --no_cookies ./
でデプロイしましょう。これでRESTAPIサーバがGAE上に構築されました。
http://[プロジェクトID].appspot.com/saveにPOSTするとセーブデータがdatastoreに書き込まれて
http://[プロジェクトID].appspot.com/load/15とGETリクエストを送るとuserIDが15の人のセーブデータが文字列で帰ってきます!
#クライアント側(以下全部Unityの話)
Unityは要所だけ見せる感じで
##データを書き込みたい時:Save
###Saveボタンをおした時の処理を書く
public void Save(){
var vec3str = CharacterManager.Instance.transform.position.ToString("G4");
//Unityで保存したいデータ(今回はUserIDと位置データのみ)をKeyValuePairでまとめる
//"id"にはID変数を、posには(-4.843, 3.883, -12.97)のような変数をString化したものをひも付けている
var dic = dic = new Dictionary<string, string> () {
{"id", NetworkManager.Instance.testID.ToString()},
{"pos", vec3str}
};
//Datastoreに書き込む処理を呼び出す。
NetworkManager.Instance.SaveToDS(dic);
}
###RESTAPIサーバにPOSTを行う処理を書く
public string testID = "25";//testなのでハードコーディングしてしまう。
public string GETURL{get;set;}
public string POSTURL{get;set;}
// Use this for initialization
void Start () {
GETURL = "http://hogehoge-1111.appspot.com/load/";
POSTURL = "http://hogehoge-1111.appspot.com/save";
}
public void SaveToDS(Dictionary<string,string> post){
WWWForm form = new WWWForm();
foreach(KeyValuePair<string,string> postReq in post) {
form.AddField(postReq.Key, postReq.Value);
}
WWW www = new WWW(POSTURL, form);
//↓でPOSTを送信している
StartCoroutine(WaitForRequest(www));
}
private IEnumerator WaitForRequest(WWW www) {
yield return www;
// check for errors
if (www.error == null) {
Debug.Log("WWW Ok!: " + www.text);
} else {
Debug.Log("WWW Error: "+ www.error);
}
}
↑でPOSTができる。
SaveToDSは、POSTのフォームにKeyとValueをくっつけて行く作業をした後それを特定のURLへ投げている。
その結果はコルーチンで吐き出されるLogから分かる。
##データを読み込みたい時
###Loadボタンを押した時の処理を書く
//UserIDが25の最後にセーブした場所を取りに行くという処理をしたい。
public void Load(){
NetworkManager.Instance.LoadFromDS("25");
}
public void LoadFromDS(string userID){
Debug.Log("Loading");
StartCoroutine(GetData(userID));
}
private IEnumerator GetData(string userID){
var tmp = GETURL + userID;//http://[プロジェクトID].appspot.com/load/25というURLができる
WWW www = new WWW(tmp);
yield return www;
if (www.error == null) {
Debug.Log(www.text);
var str = www.text;//これが引っ張ってきたデータ "(-4.843, 3.883, -12.97)"のような値がかえってくる
Character.transform.position = makeVec3FromStr(str);//文字列を再びVector3化する
}
}
//文字列化したVector3を再びVector3化して返す。Linqをちゃんと使うともっと綺麗なコードになりそう。
private Vector3 makeVec3FromStr(string vec3str){
var removeChars = new char[] { '(', ')' };
foreach (var c in removeChars)
{
vec3str = vec3str.Replace(c.ToString(),"");
}
var xyz = vec3str.Split(',');
var x = float.Parse(xyz[0]);
var y = float.Parse(xyz[1]);
var z = float.Parse(xyz[2]);
return new Vector3(x,y,z);
}
↑Coroutineは同期処理ではないのでreturn hogehoge
というコードでの返り値はとれないのだが、UniRxを使うと返り値が取れるのでもしメソッド化したいときはUniRxを使うと良いですね。
以上によって
セーブボタンを押した時はキャラクターの位置をDatastoreに書き込む
ロードボタンを押した時はキャラクターの位置をDatastoreの値から修正する。
という動作を達成できます。
Datastoreではデータはこんな風に見えます。(実際のスクショ)
ほんとはJson(LitJsonやJsonUtilityなど)を扱いたかったのですが、簡易な形でということで最初はGETPOSTでやってみました。
動画を見ればわかると思いますが、SaveもLoadもちょっと遅いのでリアルタイムなものでは使いづらいのですが、ボタン一発ではなくて、ちゃんとセーブUIとか作ってそれなりにセーブに時間を取るようにすれば読み書きに関しては安定して使えると思います。
来年になるとGoogleCloudPlatformのTokyoリージョンができるらしいのでもっと早くなるかもしれない…
自前でサーバを用意することなく、そしてそのメンテナンスも難しいことはGoogleの技術者に任せつつ、規模が大きなっても勝手にスケーリングしてくれるGAEは結構有能ですしdatastoreというデータベースもカジュアルに使えるということで、選択肢の1つとして考えても悪く無いと思います。
(自分はこの方法を使っていろいろ作っていきたいと思ってます。)
終わり!