【PlayFab+Unity】Playfabが最新バージョンへアップデート出来ない場合の対処法
UnityのEditor内からPlayFabを最新バージョンへアップデートする事が可能ですが、不具合によりこの方法ではアップデート出来ませんでした。
上記サイトの通り、Editor内の「Upgrade to~」のボタンを押してもエラーがコンソールに出力されてアップデート出来ないという不具合が発生しています(オレンジ色のボタンです)
この不具合の解決方法は上記サイトにも記してある通り、公式のGithubページからプロジェクトをcloneしてローカルに保存し、Unityへ直接インポートするという物です。
このGithubページからプロジェクトをcloneし、UnitySDK→ExampleTestProject→Assetsフォルダ内の「PlayFabSDK」を探します。UnityのProjectインスペクターからPlayFabSDKフォルダを削除し、同名フォルダをドラッグ&ドロップでインポートさせると無事アップデート出来ました。
最新バージョンへアップデートするとEditorの画面もこのようになり、確認が可能です。
【自作ゲーム】対戦STG進捗 その1
対戦STGを作ってます
自作STG 友人と新UIで対戦
— ぜろどっと(武内ラウンジ) (@tkch_vit) 2021年3月23日
(OnlineBattle Shoot'em)#indiedev pic.twitter.com/ouuMUVAmmi
↑ 雑魚敵を倒しアイテムを集め、おじゃま敵をお互いに送り合って先にミス(被弾)した方が負けです。
自作STG(メニュー~マッチング) pic.twitter.com/21hmBW7gXQ
— ぜろどっと(武内ラウンジ) (@tkch_dex) 2021年3月21日
↑ メニュー画面
自作STG(マッチング~対戦相手からおじゃま受信) pic.twitter.com/F56pVLBoOZ
— ぜろどっと(武内ラウンジ) (@tkch_dex) 2021年3月21日
↑ 新メニュー画面&UIでのバトル試作(テストプレイヤーが居ないので対戦できない…)
現状&今後の方針
こんな感じで現在仮素材でタイトル画面からバトル終了まで一通りプレイできる状態まで作りました。プログラミングとデバッグでここまで長かったですが、キャラ絵や3DモデルやらBGMが全く無い状態なので素材作りの時間が必要な気がします。
年内に無料β版公開を現在の目標にしています。リリース前に進捗、紹介動画も作ったりするので素材作りは結構重要だと思うんですよねー。しかしプログラミングの方も実装できていない機能が山程あるので(ログインとか)、一通り素材が作れたタイミングor同時進行でコードも書くかなとか思っています。
やる事は多いですが個人制作でそこまで凝っても仕方ないので、ちょいちょい完成させてさっさと出します。応援よろしくお願いします。
Unity上でストリートビューをパノラマ表示&ミニマップを使い移動する方法【Unity+Googleストリートビュー】
はじめに
過去にVR空間を使った避難訓練環境の開発をテーマにした研究を行い、実験のために現実世界の風景内を移動できるVRシステムを構築しました。
基本設計はゲームエンジンUnity内でOculusRift DK2を使い、ユーザーが指定した現実世界での緯度経度に対応するGoogle ストリートビューのパノラマ写真を表示するという仕組みです。
研究内容や実装方法は以下の解説動画で述べているのでこちらも参考にして下さい。
youtu.be
本記事ではUnityへストリートビューを実装した方法と、その方法が使えなくなってしまった経緯、今後計画している新たな実装方法について解説します。
目次
実装した方法
私の実装環境は動画内で説明しているように
- Unity2018.2.14f1
- Oculus Rift DK2
です。後に解説するC#のスクリプトはUnity2019.4.14f1での稼働も確認してます。
プログラムはUnity上で動作するC#のコードのみです。VR空間上にストリートビューの画像を表示するために、プログラムが行う具体的な動作は大きく分けて
- UIにミニマップを表示する
- クリックされたミニマップ上の座標から移動先の緯度経度を計算する
- 移動先の座標のパノラマ画像データがサーバーに存在するか確認する
- パノラマ画像データをサーバーから取得して表示させユーザーがその位置に移動する
の四段階です。これらを順番に説明します。
UIにミニマップを表示する
このプログラムではVR空間上にミニマップを表示し、そのミニマップをOculusコントローラーでポインティングすることで指定した位置へユーザーが移動出来るようにしてます。
そのため、まずはじめにGoogleのStatic Maps API を利用してUI上にミニマップを表示させる必要があります。
developers.google.com
Static Maps APIの利用にはAPIキーが必要になります。後にも触れますが、2018年の規約改定でGoogleマップ系APIの登録方法が大きく変更されています。クレジットカードの支払い情報登録が必須などの条件がありますが、日本語の解説ページも多く見つけられるので参考にしながら登録して下さい。
www.webdesignleaves.com
APIを使う準備が出来たらAPIキーを使い、初期地点を中心としたミニマップを表示させます。
以下のソースコードでUnity上にマップ画像が表示出来ます。
private IEnumerator LoadMinimap() { string url = "https://maps.googleapis.com/maps/api/staticmap?center=" + lat + "," + lng + "&zoom="+ mapZoom + "&size=512x512" + "&markers=color:red%7Clabel:S%7C" + lat + "," + lng + "&key=" + APIKey; WWW www = new WWW(url); // 画像ダウンロード完了を待機 yield return www; minimap.texture = www.textureNonReadable; }
WWWクラスを使い画像ダウンロードを待機するためにコルーチンを使用しています。初期地点の緯度経度の座標をそれぞれlatとlngに直接指定します。mapZoomで表示するマップの拡大率を、sizeでマップの画像サイズを指定します。有効化したAPIKeyをkeyから指定します。
minimapはRawImage型のデータです。正しくURLからリクエストが出来ればCanvas上に指定した座標の地図が表示されます。
URLに指定する各種パラメータは以下のサイトが参考になります。
maps.multisoup.co.jp
クリックされたミニマップ上の座標から移動先の緯度経度を計算する
ミニマップが表示出来たので、次はマップ上のクリックされた位置から緯度経度情報を計算します。以下のソースコードでクリックされた位置を緯度経度情報へ変換しています。
public void MinimapClicked(float xpos,float ypos) { /* * x=180°/300/2^zoom*vx * y=180/300/2^zoom*vy*cos(radx) * zoom = 0~19 */ //latlng to world pixel double lat_rad = lat * Mathf.Deg2Rad; double lng_rad = lng * Mathf.Deg2Rad; float R = 128 / Mathf.PI; double worldCoordy = -R / 2 * Math.Log((1 + Math.Sin(lat_rad)) / (1 - Math.Sin(lat_rad)) ) + 128; double worldCoordx = R * (lng_rad + Mathf.PI); //world coord double pixelCoordx = worldCoordx * Mathf.Pow(2, mapZoom); double pixelCoordy = worldCoordy * Math.Pow(2, mapZoom); // vx vy = -256 ~ 256 pixelCoordx += xpos; pixelCoordy -= ypos; worldCoordx = pixelCoordx / Mathf.Pow(2, mapZoom); worldCoordy = pixelCoordy / Math.Pow(2, mapZoom); lat_rad = Math.Atan(Math.Sinh((128 - worldCoordy) / R)); lng_rad = worldCoordx / R - Mathf.PI; float newlat = (float)(lat_rad * Mathf.Rad2Deg); double newlng = lng_rad * Mathf.Rad2Deg; if (!isPanoramaChecking) { StartCoroutine(CheckPanorama(newlat, newlng)); } }
プログラムは以下のサイトの内容を参考に実装しました。
www.mapli.net
おおまかな流れとしては、現在表示しているパノラマ画像データの座標lat,lngを、Googleが使用している測地系WGS-84にならいピクセル座標x,y座標へ変換し、クリックされたミニマップ上の座標xpos,yposを足し合わせて再度緯度と経度へ変換しています。ミニマップがクリックされメソッドが呼び出された時、はじめに現在表示しているパノラマ画像の緯度経度を世界座標worldCoordx,yに変換します。そして、求めた世界座標に2のマップの拡大率乗を乗算することでピクセル座標pixelCoordx,pixelCoordyが求まります。(以下のサイトで拡大率とピクセル座標の対応が解説されています)
www.mapli.net
計算で求めたpixelCoordx,pixelCoordyにミニマップ上でクリックされた座標(画像サイズが512x512のため範囲は-256~256)を加算し、クリックされた位置のピクセル座標が求まります。最後にピクセル座標を緯度経度に逆変換し、CheckPanoramaメソッドで求めた座標のパノラマ画像データがサーバーに存在するか確認する次のステップへ移ります。
移動先の座標のパノラマ画像データがサーバーに存在するか確認する
緯度と経度の計算が終了したら、今度はその座標のパノラマ画像データがGoogleのサーバーに存在するか確認する必要があります。
パノラマ画像データの有無はGoogleのStreet View Static APIを使って確認することが出来ます。
developers.google.com
パノラマ画像データがGoogleのサーバーに存在する場合は、APIからパノラマIDが取得でき、パノラマ画像が存在しない場合は対応する画像が無いとサーバーから応答されます。
パノラマIDとはパノラマ画像データを指し示すIDです。
パノラマ画像の有無確認、パノラマIDの取得を行うC#のコードは以下のようになります。
private IEnumerator CheckPanorama(double tmplat, double tmplng) { if(isPanoramaChecking) yield break; isPanoramaChecking = true; //latlng check string url = "https://maps.googleapis.com/maps/api/streetview/metadata?size=512x512&location=" + tmplat + "," + tmplng + "&heading=" + viewangle + "&pitch=0&fov=110&key=" + APIKey + "&source=outdoor"; Debug.Log(url + " : Pano "); WWW www = new WWW(url); yield return www; var jsonDict = Json.Deserialize(www.text) as Dictionary<string, object>; if (!((string)jsonDict["status"]).Equals("OK")) { PrintTextLog("Panorama Not Found"); isPanoramaChecking = false; yield break; } previous_lat = lat; previous_lng = lng; lat = tmplat; lng = tmplng; //Success StartCoroutine(LoadMinimap()); PrintTextLog("Loading lat=" + lat + " lng=" + lng + "\n Date:" + (string)jsonDict["date"]); Debug.Log("Lat:" + lat + "Lng:" + lng); Debug.Log((string)jsonDict["pano_id"]); panoID = (string)jsonDict["pano_id"]; isPositionChanged = true; isPanoramaChecking = false; yield break; }
HTTPリクエストをGoogleへ送信し、Street View Static APIからパノラマIDを取得しています。
サーバーからの応答結果はjson形式で記述されているため、jsonファイルのパースにはMiniJSONを使用しJson.DeserializeでDictionary型へ変換しています。
Unity3D: MiniJSON Decodes and encodes simple JSON strings. Not intended for use with massive JSON strings, probably < 32k preferred. Handy for parsing JSON from inside Unity3d. · GitHub
jsonファイルを読み込み、statusの値がOKだった場合はパノラマIDを取得し次のパノラマ画像データ取得段階へ進みます。
パノラマ画像データをサーバーから取得してVR空間に表示する
パノラマIDからパノラマ画像データを取得し、Unity上へ読み込むメソッドのコードを以下に示します。
private IEnumerator LoadPanorama() { RawImage rawImage = GetComponent<RawImage>(); string url = null; WWW www = null; bool isHRimageExists = false; do { isHRimageExists = false; isPositionChanged = false; Debug.Log("do"); //check resolution url = "https://geo0.ggpht.com/cbk?cb_client=maps_sv.tactile&authuser=1&hl=ja&gl=jp&panoid=" + panoID + "&output=tile&x=7&y=3&zoom=3&nbt&fover=0&heading=5"; UnityWebRequest request = UnityWebRequest.Get(url); //SendWebRequestを実行し、送受信開始 yield return request.SendWebRequest(); if (request.responseCode != 400) isHRimageExists = true; if (isHRimageExists) { Debug.Log("HRImageExists"); //zoom1 Debug.Log("Hzoom2"); for (int i = 0; i < 2; i++) { url = "http://maps.google.com/cbk?output=tile&panoid=" + panoID + "&zoom=1&x=" + i + "&y=0" + "&heading=0"; www = new WWW(url); yield return www; www.LoadImageIntoTexture(textures[0 + i * 1]); atlas.SetPixels(0 + i * 512, 0, 512, 512, textures[0 + i * 1].GetPixels()); } if (isPositionChanged) continue; atlas.Apply(false); highpanotex_zoom1.SetPixels(0, 0, 1024, 512, atlas.GetPixels(0, 0, 1024, 512)); highpanotex_zoom1.Apply(false); RenderSettings.skybox.SetTexture("_MainTex", highpanotex_zoom1); //zoom2 Debug.Log("Hzoom2"); for (int i = 0; i < 4; i++) { for (int j = 0; j < 2; j++) { url = "http://maps.google.com/cbk?output=tile&panoid=" + panoID + "&zoom=2&x=" + i + "&y=" + j + "&heading=0"; www = new WWW(url); yield return www; www.LoadImageIntoTexture(textures[j + i * 2]); atlas.SetPixels(0 + i * 512, 512 * 1 - j * 512, 512, 512, textures[j + i * 2].GetPixels()); } if (isPositionChanged) break; } if (isPositionChanged) continue; atlas.Apply(false); highpanotex_zoom2.SetPixels(0, 0, 2048, 1024, atlas.GetPixels(0, 0, 2048, 1024)); highpanotex_zoom2.Apply(false); RenderSettings.skybox.SetTexture("_MainTex", highpanotex_zoom2); isPanoramaLoading = false; yield break; } //low res Debug.Log("HRImage Not Exists"); //zoom1 832, 416, Debug.Log("Lzoom1"); for (int i = 0; i < 2; i++) { url = "http://maps.google.com/cbk?output=tile&panoid=" + panoID + "&zoom=1&x=" + i + "&y=0" + "&heading=0"; Debug.Log(url); www = new WWW(url); yield return www; www.LoadImageIntoTexture(textures[0 + i * 1]); atlas.SetPixels(0 + i * 512, 0, 512, 512, textures[0 + i * 1].GetPixels()); } atlas.Apply(false); lowpanotex_zoom1.SetPixels(0, 0, 832, 416, atlas.GetPixels(0, 512 - 416, 832, 416)); lowpanotex_zoom1.Apply(false); RenderSettings.skybox.SetTexture("_MainTex", lowpanotex_zoom1); if (isPositionChanged) continue; //zoom2 1664x832 Debug.Log("Lzoom2"); for (int i = 0; i < 4; i++) { for (int j = 0; j < 2; j++) { url = "http://maps.google.com/cbk?output=tile&panoid=" + panoID + "&zoom=2&x=" + i + "&y=" + j + "&heading=0"; www = new WWW(url); yield return www; www.LoadImageIntoTexture(textures[j + i * 2]); atlas.SetPixels(0 + i * 512, 512 * 1 - j * 512, 512, 512, textures[j + i * 2].GetPixels()); } if (isPositionChanged) break; } if (isPositionChanged) continue; atlas.Apply(false); lowpanotex_zoom2.SetPixels(0, 0, 1664, 832, atlas.GetPixels(0, 1024-832, 1664, 832)); lowpanotex_zoom2.Apply(false); RenderSettings.skybox.SetTexture("_MainTex", lowpanotex_zoom2); // wwwクラスのコンストラクタに画像URLを指定 //低解像度zoom3パターン Debug.Log("Lzoom3"); for (int i = 0; i < 7; i++) { for (int j = 0; j < 4; j++) { url = "http://maps.google.com/cbk?output=tile&panoid=" + panoID + "&zoom=3&x=" + i + "&y=" + j + "&heading=0"; www = new WWW(url); // 画像ダウンロード完了を待機 yield return www; //otiru www.LoadImageIntoTexture(textures[j + i * 4]); // webサーバから取得した画像をRaw Imagで表示する //rawImage.texture = www.textureNonReadable; atlas.SetPixels(0 + i * 512, 512 * 3 - j * 512, 512, 512, textures[j + i * 4].GetPixels()); } if (isPositionChanged) break; } if (isPositionChanged) continue; atlas.Apply(false); lowpanotex_zoom3.SetPixels(0, 0, 3328, 1664, atlas.GetPixels(0, 2048 - 1664, 3328, 1664)); lowpanotex_zoom3.Apply(false); RenderSettings.skybox.SetTexture("_MainTex", lowpanotex_zoom3); } while (isPositionChanged); isPanoramaLoading = false; PrintTextLog("Loading Complete"); }
ソースコードが長く、同じ処理の繰り返しが多いため、部分的に解説を行います。基本的にはGoogleのサーバー分割されて保存されたパノラマ画像を複数枚取得しつなぎ合わせて表示する処理を複数回繰り返しています。パノラマ画像の取得と表示を繰り返している理由は、Googleのサーバー上に高画質な物から低画質な物まで複数バージョンのパノラマ画像が保存されているためです。
今回作成したプログラムは、低画質版の読み込みと表示が終了次第、次に高画質なバージョンのパノラマ画像の取得と表示を開始し、最高画質のパノラマ画像の取得と表示が終了するまでリクエストを繰り返すという仕組みです。
一度のWebリクエストで取得できるパノラマ画像のサイズは512x512サイズの小画像です。この小画像を横に2枚、または縦に2枚と横に4枚、という様に並べる事で、
- 1024 x 512
- 2048 x 1024
という具合にそのまま連結しパノラマ画像が完成、表示が可能になります。
その一方で、512x512の画像領域全てを使わず、画像の一部が黒塗りの低画質なパノラマ画像が保存されている場合は、
- 832 x 416
- 1664 x 832
- 3328 x 1664
という解像度でサーバーにパノラマ画像が保存されています。そのため、例えば832x416のパノラマ画像を表示するためには、512x512の画像を二枚横に並べ1024x512の画像を作った上で黒塗りされた無駄な領域を切り取る、という様に512x512の画像をより多くつなぎ合わせてパノラマ画像を完成させる必要があります。
doループの先頭ではHTTPリクエストのレスポンスコードを確認して、黒塗り部分を含む多くの画像がサーバー上に存在するか確認し、直接並べるだけでパノラマ画像が成立するタイプ、もしくは黒塗り部分を削除しなければならないタイプか、上記のどちらの解像度タイプか調べています。(黒塗りされた部分がない場合はisHRimageExistsフラグをtrueに設定する)
高画質なパノラマ画像データが存在する場合(isHRimageExistsがtrueの場合)、高画質な画像を読み込み、そうでない場合は低画質版のパノラマ画像を読み込みます。forループ内では、上記の複数解像度用にほぼ同じテクスチャのつなぎ合わせ処理を繰り返しています。テクスチャの結合、表示に使われているatlasやhighpanotexなどは全てUnityのTexture2D型変数です。取得したパノラマ画像を、SetPixelsメソッドでスプライトアトラス上に位置をずらしながらコピーし、タイルを敷き詰める要領でテクスチャをつなぎ合わせ一枚のパノラマ画像を作っています。一枚のパノラマ画像が完成次第、RenderSettings.skybox.SetTextureでパノラマ画像をSkyboxへ設定し表示しています。
・VRコントローラーでの操作に関して追記
Oculus Rift DK2のコントローラーで表示したパノラマ画像を回転させるスクリプトを以下に記載します。右コントローラーの人差し指ボタンを押し込んでいる間だけ、コントローラーを左右に振って、パノラマ画像の表示を回転させられます。SDKのバージョンは古いものですが参考にして下さい。(UnityのVRサポートシステムは更新頻度が高く、様々な機能が現れたり非サポートになったりしているそうです)
//Rotation if (OVRInput.Get(OVRInput.RawButton.RIndexTrigger)) { Vector3 v3 = OVRInput.GetLocalControllerAngularVelocity(OVRInput.Controller.RTouch); float y = v3.y / 60f; viewangle = (viewangle - y) < 0 ? viewangle - y + 360 : viewangle - y; if (viewangle > 360) { viewangle -= 360; } RenderSettings.skybox.SetFloat("_Rotation", viewangle); compass.eulerAngles = new Vector3(90, 270, viewangle); }
以前の方法が使えなくなってしまった経緯
以上の方法でUnity上にGoogleストリートビューの画像をパノラマ表示し、Oculusとミニマップで移動する事が可能になります。
今回解説した方法で使われている512x512の小画像を取得するURLのように、Googleがストリートビューを表示するために用意したURLがAPIのエンドポイントのように使われていました。printart.isr.ist.utl.pt
www.tfzx.net
例えば、上記サイトではHTTPリクエストでxmlファイルを取得し、そのパノラマ位置からどの方角に移動すれば、次のパノラマ画像データが得られるか、というストリートビューで利用されている情報が取得できています。
medium.com
また、上記サイトではHTTPリクエストによって、ストリートビューのパノラマ画像データが持つ深度マップ(Depth Map)を取得しています。
しかし、2018年の6月11日より「Googleの提供するスクリプトの外部からの使用禁止」という規約が課せられ、現在はURLにアクセスしてもエラーページが表示され、ストリートビューに関する情報は取得できません。
stackoverflow.com
Note: by doing this you will be breaking Google's terms of serviceと記載されています。
Googleサーバー側の変更によって、上記のパノラマ画像に付随するメタデータの取得や、関連するstack overflowなどに集められたかなりの量の情報が使い物にならなくなってしまいました。自社のサービス専用の機能を勝手に使われて新サービスを作られたり、論文も出たりしているのでGoogleとしても問題だと考えた結果だと思います。
www.researchgate.net
https://senseable.mit.edu/papers/pdf/20190330_Li-Ratti_UsingGoogleStreetView_Mathematics.pdf
(上記のURIを利用した論文2つ。現在は使えない方法ですが参考になります)
ブラウザ画面組み込みによる新たな実装方法
実は今でも開発は継続中で、来年までに実験&論文作成をしなければなりません。このままだと卒業出来ないので新たな実装方法を探しています。
現在試行中の手法は、Unity内でブラウザ画面を表示してブラウザ画面から直接ストリートビューを表示&操作するという方法です。
StreetView in Unity(For Research) pic.twitter.com/FoKt1MrY3j
— Tech&Shumi Blog (@TechShumi) 2021年2月23日
参考文献
www.newtonscannon.com
パノラマ画像を集めて結合、表示する基礎原理について
【Unity】PlayFab経由でPaypal決済時にユーザーの購入を確認する方法
はじめに
先日、以下の記事を見ながらUnityからPaypal決済画面を開く機能を実装しました。これだけ読めばUnity内でPaypalや他のサービスで課金出来るゲームが作れるようになりますのでぜひ読んで下さい。
simplestar-tech.hatenablog.com
これらの記事を見た上で「ユーザーがPaypalとかで決済した後に決済確認処理したいけど、いつ決済が終わるか分からなくない?」という点についてUnity側の処理で一応の解決方法を示すのが本記事です。
ユーザーがゲームに戻って来たタイミングを取得する方法
ユーザーがブラウザの画面で決済処理を終えたタイミングでゲーム画面にフォーカスを戻すので、MonoBehaviour.OnApplicationFocusを使いゲーム画面にユーザーが戻ってきたタイミングを取得し、その後決済処理をゲーム側で確認します。
サンプルコードは以下のようにになります。
if(result.PurchaseConfirmationPageURL != null) Application.OpenURL(result.PurchaseConfirmationPageURL); // 決済画面からフォーカスが戻ってくるまで待機する PollPurchase(); //ゲーム画面に戻ってきたので購入されたか確認する OnSuccessOfPayForPurchase(result);
void PollPurchase() { while (isPaused) { //待機 } } void OnApplicationFocus(bool hasFocus) { isPaused = !hasFocus; }
whileループで処理が止まり、ユーザーが画面に戻ってくると処理が再開します。多少強引ですがUnity側のコールバックで決済処理の確認処理に戻ってくれる方法です。
ユーザーが決済処理を終えずにゲームに戻って来たり、決済画面を何重にも開いたりする場合があるので、決済画面を閉じるようゲーム画面でメッセージを出す必要がありそうです。
その他の方法
unity-webviewを使う
github.com
未だに使われているという伝家の宝刀unity-webviewを使う方法です。決済画面をアプリ内に表示して、画面が閉じたタイミングで決済を確認すれば確実ですよね。
Windowsだと実装が難しいらしく今回は断念しました。
という訳でOnApplicationFocusを使ってみてはいかがでしょうか。
おまけ・アイテムやバンドルの価格が0だった場合の挙動
アイテムやバンドルの価格設定がRM=0、つまり0USドルの場合に決済画面を表示する処理を行うと、決済画面が表示されず購入が即確定します。
1ドル以上の決済はトランザクションIDの欄にIDが入ります。価格が0の場合はこのIDが発行されていません。
アイテム等の価格0の場合、PlayFabClientAPI.PayForPurchaseメソッドで取得したURLがNullになります。私がURLがNullのままチェックせずApplication.OpenURLを実行した時は、ゲームプログラムが置いてあるディレクトリがエクスプローラーで開きました。予期しない動作が起こるので、URLをNullチェックして、決済画面のURLが発行された時だけOpenURLしましょう。