最近は全くソースコードを触らない日も増えてきた、開発チームリーダーをしている大原です。
iOS/Android向けのクライアントアプリの開発において、
以下用途でゲームデータ(DBデータ、画像など)を外部ストレージに設置したいことは多々あると思います。
・レベルデザイン
・モック開発
・デバッグ
・などなど
その際に、外部ストレージとしてGoogle Driveを使用すれば、
サーバー/インフラ構築が不要で簡単にやりたいことを実現できます。
今回の記事では、その仕組みを作る上での環境整備とサンプルコードについて説明したいと思います。
環境
- macOS 10.15.7
- Xcode Version 12.3
- Cocos2d-x v4.0.0
- Google Drive API Ver.3
Google Drive API(Ver.3)を使うまでの準備
プロジェクトの作成
Google Drive APIを使用するために、Google Cloud Platformにプロジェクトを作ります。
Google Cloud Platformにログインした後、ダッシュボードに向かいます。
作成が完了すると以下画面のようにプロジェクトのダッシュボードが表示されます。
これで、APIを管理するためのプロジェクトが作られました。
APIの有効化
Google Drive APIを使うために使うAPIに許可を与えます。
認証情報(クライアントID)作成
許可された人だけがアクセスできるようにアクセスの認証情報を作成します。
注:クライアントIDとシークレットはプログラムからAPIやアクセストークンをもらったりする時に使うので、メモをしてください。
Authorization Codeの取得(OAuth スコープの指定)
Authorization Codeを取得するには、スコープの指定が必要で、以下URLをchromeで開くことで出てきます。
https://accounts.google.com/o/oauth2/v2/auth?
response_type=code&client_id=<クライアントID>
&redirect_uri=urn:ietf:wg:oauth:2.0:oob&
scope=https://www.googleapis.com/auth/drive&access_type=offline
ブラウザに表示された文字列がAuthorization Codeになるので、メモっておきます。
Authorization Codeを使ってAccessTokenを取得する
クライアントアプリのプログラムからGoogle APIにHTTPアクセスしてアクセストークンを取得します。
以下は、cocos2d-xのHttpClientを使用したサンプルコードです。
//------------------------------------------------------------
// アクセストークンを発行する
cocos2d::network::HttpRequest* request = new cocos2d::network::HttpRequest();
request->setUrl("https://www.googleapis.com/oauth2/v4/token");
//HTTP問い合わせ結果のコールバック
request->setResponseCallback([this](cocos2d::network::HttpClient *sender, cocos2d::network::HttpResponse *response)
{
if (response->isSucceed() && response->getResponseCode() == 200) {
//成功
// json.
std::vector<char> *buffer = response->getResponseData();
CCLOG("response %s",buffer->data());
rapidjson::Document document;
if (document.Parse(buffer->data()).HasParseError()) {
CCLOG("parse error!!");
}
if (!document.IsObject()) {
CCLOG("invalid!");
}
//accessトークンの取得
_AccessTokenStr = document["access_token"].GetString();
CCLOG( "Aaccess Token = %s", _AccessTokenStr.c_str() );
} else {
//失敗
CCLOG("HttpRequest failed (%s)", response->getErrorBuffer());
}
});
//HTTPヘッダー
std::vector<std::string> headers;
request->setHeaders(headers);
std::string requestData;
requestData = "code=" AUTHORIZATION_CODE; //メモしておいた物を使用する
requestData += "&client_id=" CRIENT_ID; //認証情報でメモした物を使用
requestData += "&client_secret=" CRIENT_SECRET; //認証情報でメモした物を使用
requestData += "&redirect_uri=urn:ietf:wg:oauth:2.0:oob";
requestData += "&grant_type=authorization_code";
requestData += "&access_type=offline";
request->setRequestData(requestData.data(), requestData.size());
request->setRequestType(cocos2d::network::HttpRequest::Type::POST);
auto client = cocos2d::network::HttpClient::getInstance();
client->send(request);
サンプルコードを実行すると、Google APIから以下のような応答結果が返ってきます。
response {
"access_token": "取得したアクセストークン",
"expires_in": 3599,
"refresh_token": "取得したリフレッシュトークン",
"scope": "https://www.googleapis.com/auth/drive",
"token_type": "Bearer"
}
responseの"access_token"(アクセストークン)と"refresh_token"(リフレッシュトークン)を今後使っていきます。
注:2回目以降は取得できなくなるので、その時はAuthorization Codeを取るところからやり直しになります。
RefreshTokenを使って有効期限が切れたAccessTokenを再取得する方法
取得したアクセストークンには有効期限(レスポンスのexpires_inの数値が有効期限の秒数)があり、
有効期限が過ぎた場合は、リフレッシュトークンを使用してアクセストークンを再取得できます。
以下が再取得のサンプルコードです。
//------------------------------------------------------------
//アクセストークンを新しく取り直す
//アクセストークンの取得
cocos2d::network::HttpRequest* request = new cocos2d::network::HttpRequest();
request->setUrl("https://www.googleapis.com/oauth2/v4/token");
request->setResponseCallback([this](cocos2d::network::HttpClient *sender, cocos2d::network::HttpResponse *response)
{
if (response->isSucceed() && response->getResponseCode() == 200) {
//成功
// json.
std::vector<char> *buffer = response->getResponseData();
CCLOG("response %s",buffer->data());
rapidjson::Document document;
if (document.Parse(buffer->data()).HasParseError()) {
CCLOG("parse error!!");
}
if (!document.IsObject()) {
CCLOG("invalid!");
}
//accessトークンの取得
_AccessTokenStr = document["access_token"].GetString();
CCLOG( "Aaccess Token = %s", _AccessTokenStr.c_str() );
_RefreshTokenStr = document["refresh_token"].GetString();
CCLOG( "Refresh Token = %s", _RefreshTokenStr.c_str() );
} else {
//失敗
CCLOG("HttpRequest failed (%d) header(%s)", response->getResponseCode(),response->getResponseHeader()->data());
}
});
//HTTPヘッダー
std::vector<std::string> headers;
request->setHeaders(headers);
std::string requestData;
requestData = "client_id=" CRIENT_ID;
requestData += "&grant_type=refresh_token";
requestData += "&client_secret=" CRIENT_SECRET;
requestData += "&refresh_token=" REFRESH_TOKEN;
request->setRequestData(requestData.data(), requestData.size());
request->setRequestType(cocos2d::network::HttpRequest::Type::POST);
request->setTag("RefreshAccessToken");
auto client = cocos2d::network::HttpClient::getInstance();
client->send(request);
"grant_type"にrefresh_tokenを指定し、"refresh_token"に「Authorization Codeを使ってAccessToken取得した時のrefresh_token」を指定することで、
AccessTokenの更新を行うことができます。
response {
"access_token": "取得したアクセストークン",
"expires_in": 3599,
"scope": "https://www.googleapis.com/auth/drive",
"token_type": "Bearer"
}
Google Drive API(Ver.3)を使用してGoogle Driveのファイルにアクセスする方法
アプリにダウンロードする方法
最初に、Google Driveからアプリにファイルをダウンロードする方法について説明します。
用途としては、デバッグ等でテストデータやテスト画像をアプリに反映させることが挙げられます。
ダウンロードしたいファイルをGoogle Driveに設置します。
ファイルのリンクを開いて、そこのURLに書いてあるID部分をコピーしておきます。
https://drive.google.com/file/d/[ファイルID]/view
以下は、ファイルをダウンロードするサンプルコードです。
cocos2d::network::HttpRequest* request = new cocos2d::network::HttpRequest();
request->setResponseCallback([this,request](cocos2d::network::HttpClient *sender, cocos2d::network::HttpResponse *response)
{
if (response->isSucceed() && response->getResponseCode() == 200) {
//成功
//ファイルを保存するフォルダを作る
std::string dirPath = cocos2d::FileUtils::getInstance()->getWritablePath() + "save_dir/";
if(!cocos2d::FileUtils::getInstance()->isDirectoryExist(dirPath))
cocos2d::FileUtils::getInstance()->createDirectory(dirPath);
std::string filePath = dirPath + "test.txt";
cocos2d::Data downloadData;
downloadData.copy(reinterpret_cast<unsigned char*>(buffer->data()),buffer->size());
if(!cocos2d::FileUtils::getInstance()->writeDataToFile(downloadData, filePath))
{
CCASSERT(false,"書き込めない");
}
CCLOG("HttpRequest success (%d) header(%s)", response->getResponseCode(),response->getResponseHeader()->data());
} else {
//失敗
CCLOG("HttpRequest failed (%d) header(%s)", response->getResponseCode(),response->getResponseHeader()->data());
}
});
//Google Drive APIのファイルアクセス関連
std::string url = "https://www.googleapis.com/drive/v3/files";
url += "/[ファイルID]?alt=media";
request->setUrl(url);
//HTTPヘッダー
std::vector<std::string> headers;
headers.push_back("Authorization: Bearer " + _AccessTokenStr);
request->setHeaders(headers);
request->setRequestType(cocos2d::network::HttpRequest::Type::GET);
request->setTag("FileDownload");
auto client = cocos2d::network::HttpClient::getInstance();
client->send(request);
https://www.googleapis.com/drive/v3/files
のAPIに対してファイルIDとalt=mediaをつけてAPIに投げることでバイナリデータが返ってきます。
返ってきたバイナリデータを任意の場所にファイルとして保存します。
アプリからアップロードする方法
次に、アプリからGoogle Driveにファイルをアップロードする方法について説明します。
用途としては、アプリのステータスやログなどをGoogle Driveに転送することが挙げられます。
サンプルコードは以下になります。
//アップロード用のURL
cocos2d::network::HttpRequest* request = new cocos2d::network::HttpRequest();
request->setResponseCallback([](cocos2d::network::HttpClient *sender, cocos2d::network::HttpResponse *response)
{
if (response->isSucceed() && response->getResponseCode() == 200) {
//成功
} else {
//失敗
CCLOG("HttpRequest failed (%d) header(%s)", response->getResponseCode(),response->getResponseHeader()->data());
}
});
request->setUrl("https://www.googleapis.com/upload/drive/v3/files?uploadType=media");
std::string dirPath = cocos2d::FileUtils::getInstance()->getWritablePath() + "save_dir/";
if(!cocos2d::FileUtils::getInstance()->isDirectoryExist(dirPath))
cocos2d::FileUtils::getInstance()->createDirectory(dirPath);
std::string filePath = dirPath + "test.txt";
//テストデータの存在確認
if(! cocos2d::FileUtils::getInstance()->isFileExist(filePath))
{
CCLOG("ファイルがない");
return;
}
auto data = cocos2d::FileUtils::getInstance()->getDataFromFile(filePath);
std::string dataStr;
unsigned char* bytes = data.getBytes();
//文字列に変換
for(unsigned int i = 0; i < data.getSize() ; i++)
{
dataStr += bytes[i];
}
request->setRequestData(dataStr.data(), dataStr.size());
//HTTPヘッダー
std::vector<std::string> headers;
headers.push_back("Authorization: Bearer " + _AccessTokenStr);
headers.push_back("Content-Type: text/plain");
char moji[32]="";
sprintf(moji, "Content-length: %ld", data.getSize());
headers.push_back( moji );
request->setHeaders(headers);
request->setRequestType(cocos2d::network::HttpRequest::Type::POST);
request->setTag("SimpleUpload");
auto client = cocos2d::network::HttpClient::getInstance();
client->send(request);
Google Drive APIにuploadType=media
を指定することで、Google Driveにファイルをアップロードすることができます。
アップロードされる場所はクライアントIDを作った時に指定したアカウントのマイドライブ直下になります。
ファイル名を指定してアップロードする方法もありますが、
その場合はマルチパート形式で送信する必要があり複雑なため、今回の記事では割愛させていただきます。
まとめ
ゲーム開発をしているとダメージの係数調整やチュートリアルのデータ設定など頻繁に調整したいけどデータベース化してサーバー管理するほどじゃないものが結構あります。
そういう時に今回紹介したデバッグ機能を事前に作っておくと調整するプランナーやデザイナなどがエンジニアの手を借りなくても調整をできるので
コミュニケーションコストを下げたり、作業の並行化をして同時進行しやすくしたり、短い期間で開発できるものを増やすことにつながります。
ゲーム機能の開発も大事ですが、運営するに当たっての仕組みや機能の追加を行っていけると開発チームとしての効率をあげることができるので、
そういう視点を持って開発が行っていけるとみんなの力になれます。