デイリーガチャで学ぶVCIのストレージ機能

# 概要
この記事はVRサービス「VirtualCast」で使用できるVCIという仕組みについて扱った記事です。
今回はVCIの機能である「ストレージ」の概要とそれを用いて作成したVCIの仕組みの紹介です。
結構中級者向きの記事だと思います。

# ストレージ機能とは？
ざっくりいうとサーバーにデータを保存しておくことができる仕組みです。
(詳細は [公式ページ](https://developer.virtualcast.jp/vci-docs/manual/networking/vci_storage.html) 参照)

主に以下のような特徴があります
* VCIが消えてもデータが残り続け、次回VCIを出したときにデータを参照できる
* 同じVCIであれば、自分が複数出したり他の人が出したものでも共通のデータが参照できる

上記を利用することでランキングの管理などがやりやすくなります。

# ガチャVCIの説明
今回はこのストレージ機能を使用してみんなでコンプを目指すガチャを作成したので、そのなかでどうストレージ機能を使用したかを説明したいと思います。

VCIの主な仕様は以下のとおりです
* ガチャで出るのは1~100までの数字
* それぞれの数字をはじめに出した人の情報を名前を記録して表示している
* 回数が毎日決まった回数分ガチャを引ける回数が付与される。
* ストックできるガチャ回数は上限がある
* 上記の付与回数、ストック上限回数は設定で決まっている。

また、説明に先立ち把握しておいてほしいストレージの機能を少しだけ説明します。
* ストレージはkey-valueの形式で保存されており最大1000件まで保存可能
* データは任意のタグを付けることができ、取得時にはタグを指定することで複数のデータをまとめて取得可能

## ストレージに保存するデータ
以下の2つの情報を保存しています

* ガチャ設定：デイリー加算回数などの設定データ（全体で1つ）
* ガチャコンプデータ：ガチャのコンプ状況のデータ（ガチャの数字単位）
* 個人データ：引ける回数を管理するデータ（ユーザー単位）

データの構造は以下のような感じです。

```:ガチャ設定
Key："Config"
Tag：なし
Data:{
  "DailyPullNum", //1日に加算するガチャ回数
  "StockMaxNum", //累積で貯められるガチャ回数
}
```

```:ガチャコンプデータ
Key：ガチャの数字
Tag："GachaNumber"
Data:ユーザー名
```

```:個人データ
Key：ユーザーID
Tag："Player"
Data:{
    "info.lastAccessDate", //最後にガチャを引いた日
    "info.remainingPullNum", //引ける残回数
}
```
ガチャを数字ごとに分けて保存しているのは、同時にガチャを引いたときに更新の競合が起きる可能性を下げるためです。

## 実際の処理
上記のデータ構造を踏まえ、実際にどの用にストレージAPIを使用しているかを説明していきます。
他のところで定義しているモジュールや変数を使用しているためそのままコピペでは動かないです。ご了承ください

### 初期化処理
VCI読み込み時に3つのデータすべてを取得します。
詳細なフローは以下のシーケンス図のとおりです。

![シーケンス_init.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64877/182f9f1b-2b78-299c-c7f5-ee77891f264b.jpeg)

初めてVCIを使用するユーザーには個人データがなく取得できないため、その場合は初期データを作成しています。
コード的には以下のようになります。
```lua:個人データ取得
  vci.vc.storage.Get(EachOne.key,function(r, v)
    if r.isSuccess then
      -- 取得した情報をモデルに格納して画面に反映
      local data = json.parse(v)
      EachOne.gachaData = GachaInfo.__new(data)
      EachOne.gachaData:UpdateAccessInfo()
      Logger.Info("[getPlayerToStorage]success get storage data")
    else
      -- データがない場合は新しく個人でデータを作成
      if r.errorType == vci.vc.storage.errorTypes.KeyNotFound then
        EachOne.gachaData = GachaInfo.__new()
        EachOne.gachaData:UpdateAccessInfo()
        Logger.Info("[getPlayerToStorage]success create new data")
      else
        Logger.Error("[getPlayerToStorage]failed to get "..tostring(EachOne.key)..": "..r.errorMessage)
      end
    end
  end)
```

ガチャコンプデータを取得する場合は以下のようにタグの機能を使用して取得しています。
```lua:個人データ取得
  vci.vc.storage.GetValuesFromTag(StorageTag.GachaNumber,function(r, vList)
    if r.isSuccess and vList then
      -- ガチャの数字分の長さのから配列を作成
      for i=1,100 do
        EachOne.CompList[i] = ""
      end
      -- ガチャの数字ごとにストレージから取得したデータがあれば反映
      for index,keyVal in pairs(vList) do
        if keyVal.value ~= nil and keyVal ~= "" then
          local s_name = keyVal.value
          local number = tonumber(keyVal.key)
          if number then
            EachOne.CompList[number] = s_name
          end
        end
      end
      Logger.Info("success get gachaComp data")
    else
      if r.errorType == vci.vc.storage.errorTypes.TagNotFound then
        -- ストレージに1つもデータが無かったらから配列を作成
        for i=1,100 do
          EachOne.CompList[i] = ""
        end
        Logger.Info("success create gachaComp init data")
      else
        Logger.Error("failed to get "..StorageTag.GachaNumber..": "..r.errorMessage)
      end
    end
    refreshCompList()
    EachOne.compBook:refresh()
  end)
```
### ガチャを引く処理
ガチャを引く際には個人データの更新とガチャコンプデータの更新をしています。
詳細なフローは以下のシーケンス図のとおりです。
図に載せ忘れましたが、ガチャの数字決定のあとにガチャ残回数を減らす処理もしています。
![gacha-シーケンス_pull_Qiita.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64877/01b8081a-0045-77a0-af9e-5f1f2a819356.jpeg)

また、最後にガチャコンプデータ全件分を取得しているのは、現状だと他に取得するところがなくローカルのデータとストレージのデータの差異が広がっていってしまうのでそれを防ぐためです。

個人データの更新は以下のようなコードで行っています。
特に特殊な処理はありません

```lua:個人データ更新
  vci.vc.storage.SetWithTag(EachOne.key, dataStr,StorageTag.Player,function(r, v)
    if r.isSuccess then
      Logger.Info("success set player storage data")
    else
      Logger.Error("failed to get "..tostring(EachOne.key)..": "..r.errorMessage)
    end
  end)
```

ガチャコンプデータを更新する処理は以下の通りとなっています。
更新前に再度最新のデータを取得してデータが無いか再確認しています。
```lua:ガチャコンプデータ更新
  vci.vc.storage.Get(key,function(r, v)
  　-- ストレージからデータ再取得し、既にデータがあるかを判定（ある場合更新不可）
    local canUpdate = false
    if r.isSuccess then
      Logger.Info("key found. key:"..key.." value:"..tostring(v))
      local s_name = v
      if s_name == "" then
        canUpdate = true
      else
        Logger.Info("UpdataGachaToStorage:[Get] "..key.."is filled by "..tostring(s_name))
      end
    else
      if r.errorType == vci.vc.storage.errorTypes.KeyNotFound then
        Logger.Info("UpdataGachaToStorage:[Get] key not found")
        canUpdate = true
      end
    end
  
    -- 更新可の場合更新する。更新後再度すべてのガチャコンプデータを取得する
    if canUpdate then
      vci.vc.storage.SetWithTag(key,name,StorageTag.GachaNumber,function(r, v)
        if r.isSuccess then
          Logger.Info("set storage data:"..tostring(key))
          getGahcaFromStorage()
        else
          Logger.Info("UpdataGachaToStorage:[SetWithTag] failed to get "..key..": "..r.errorMessage)
        end
      end)
    end
    
  end)
```


# 注意点
今回はユーザーごとにkeyを分けてストレージに登録しましたが、ストレージは登録上限が1000件までという制限があるため、1000名を超えるユーザーの情報は登録できません。(実際には他のデータも保存してるため今回の場合は実質900人くらいまでです)

また、ストレージの取得・更新には数秒かかることがあるため、複数のVCIで同タイミングで処理が走ると整合性が取れない場合があります。
例)：
　1.ユーザーAがガチャVCIで数字①を引く
  2.ユーザーBが別のガチャVCIで数字①を引く(1との間隔は1秒くらい)
 　→本来ユーザーAが引いたという情報が優先的に保存されるべきであるが、ユーザーBが引いたという情報で上書きされてしまう。
根本的な対策は思いついてはいないです。なにか有効な対応策があれば教えてほしいです。