Unityでソシャゲを作りたい#9 いい感じのタイミングで自動データ更新してくれちゃう上ロード画面も表示してくれる仕組みを作る
大改革でだいぶ処理がすっきりしました。
メリット
初めに本改革によるメリットをご紹介しておきます。
最大のメリットは簡単な呼び出しでデータ更新とロード画面表示を勝手にやってくれること。
案外手動実装だとこの辺めんどくさいと思います。
今回作った仕組みではデータマネージャがデータのインスタンスをメンバに持ってる状態で、外部からはそこにアクセスする形です。
データの使用時はこのように呼び出します
QestDataList QL = Manager.GameDataManager.DataPack.QestDataList; //データの取得
QL.ChangeFlg=true; //何かしら変更があった場合とか強制更新させたいときはこう 更新後は自動でfalseに戻る
await QL.Update(); //非同期で更新処理を走らせられる
//以降はQL更新済みの状態で使えるさ
さらになななんと、これだけでawaitしてる間はロード画面が表示され触れなくできます。
しかもその間アニメーションは止まらない。がんばったくね?
内容
では内容のご紹介。
まずデータアクセスをUnityWebReqestからHTTPclientに変更しました。
これでメインスレッドからしかアクセスできないよエラーは解消します。
サーバから取得した情報を受け取っていい感じに成形するためのデータ構造を示すクラスを作っていましたが、それら全部に共通して継承するクラスを増設しました。
//自己更新能力を有するデータクラスが継承する抽象クラス
public abstract class SOdatas{
Manager Manager{
get{return GameObject.Find("Manager").GetComponent<Manager>();}
}
DateTime lst_upd;
public bool ChangeFlg=true;
protected abstract string URL{get;}
public abstract void Sync(JObject Data);
//サーバアクセスが必要か判定してくれる
public bool NeedSync(){
//変更されてたら更新
if(ChangeFlg){
return true;
}
//未更新なら更新
if(lst_upd == null){
return true;
}
//データが入ってないなら更新
foreach (var field in this.GetType().GetFields()) {
if(field.GetValue(this)==null){
lst_upd=DateTime.Now;
return true;
}
}
//最終更新から一分以上たってたら更新
TimeSpan tsData = new TimeSpan(0, 0, 1, 0);
if(lst_upd+tsData < DateTime.Now){
return true;
}
//データもある、最終更新から1分経ってないなら別に更新しなくていい
return false;
}
//外から呼ぶやつ。更新しろって言いたいとき使う
public async Task<bool> Update(){
//ロード画面を呼び出す
Manager.PhaseManager.Loading=true;
try{
if(NeedSync()){
JObject res = await Task.Run(()=> ServerAccess());
Sync(res);
lst_upd=DateTime.Now;
}
}catch(Exception e){
//ロード画面を消す
Manager.PhaseManager.Loading=false;
return false;
}
//ロード画面を消す
Manager.PhaseManager.Loading=false;
return true;
}
//サーバにアクセスしてJObjectにしたデータをとってきてくれる
public async UniTask<JObject> ServerAccess(){
//アカウント認証用データはどっかから渡さないと内部では受け取れないかも
var parameters = new Dictionary<string, string>(){
{ "id","1"},
{ "secid","1234"},
};
var content = new FormUrlEncodedContent(parameters);
using (var client = new HttpClient()){
var response = await client.PostAsync(URL, content);
Thread.Sleep(1000);
Debug.Log("サーバアクセス完了"+response.StatusCode);
if (response.StatusCode!=HttpStatusCode.OK){
return null;
}else{
JObject Obj = JObject.Parse(await response.Content.ReadAsStringAsync());
ChangeFlg=false;
return Obj;
}
}
}
}
こんな感じで、外からUpdateを呼ぶと勝手に更新するような抽象クラスを作りました。
これを、保持しておきたいデータのクラスに継承させます。
//ユーザ固有情報
[DataContract]
public class UserConstData : SOdatas{
[DataMember(Name="Stone")]
public int Stone; //所持石
[DataMember(Name="Coin")]
public int Coin; //所持金
[DataMember(Name="Stamina")]
public int Stamina; //スタミナ
[DataMember(Name="Name")]
public string Name; //ユーザ名
[DataMember(Name="Rank")]
public int Rank; //ランク
[DataMember(Name="EXP")]
public int EXP; //経験値
protected override string URL{get{ return 取得先のURL;}}
public override void Sync(JObject res){
UserConstData d = new UserConstData();
string jsonStr = JsonConvert.SerializeObject(res, Formatting.None);
d = JsonUtility.FromJson<UserConstData>(jsonStr);
this.Stone=d.Stone;
this.Coin=d.Coin;
this.Stamina=d.Stamina;
this.Name=d.Name;
this.EXP=d.EXP;
this.Rank=d.Rank;
}
}
例えばこんな感じ。
他にも、いろんな情報をリストで受け取りたい場合なんかは、リストの状態のものに対して継承させます。
//クエスト情報
[DataContract]
public class QestData{
[DataMember(Name="image")]
public string image;
[DataMember(Name="popimage")]
public string popimage;
[DataMember(Name="qestname")]
public string qestname;
[DataMember(Name="qestid")]
public string qestid;
[DataMember(Name="limitdate")]
public DateTime limitdate;
[DataMember(Name="difclass")]
public int difclass;
[DataMember(Name="floor")]
public int floor;
[DataMember(Name="cost")]
public int cost;
}
//クエスト情報一覧 こっちで更新してもらいたいのでこっちで継承
public class QestDataList : SOdatas{
public List List;
protected override string URL{get{ return 取得用URL;}}
public override void Sync(JObject res){
List nl = new List();
foreach(var news in res){
string jsonStr = JsonConvert.SerializeObject(news.Value, Formatting.None);
QestData N = JsonUtility.FromJson(jsonStr);
nl.Add(N);
}
List=nl;
}
}
Sync()の部分が、帰ってきたJObjectをクラスの形に成形する処理になります。
クラスごとに受け取り方とかいろいろあると思うので、そこはクラスごとに実装します。
public class DataPack {
public UserConstData UserConstData;
public QestDataList QestDataList;
}
public class GameDataManager : MonoBehaviour{
//status 読み込めてない:0 読み込み完了:1 読み込み中:2
DataPack __datapack;
public DataPack DataPack{
get{
if(this.__datapack == null){
this.__datapack = new DataPack();
}
return this.__datapack;
}
}
}
こいつらをこんな感じにまとめて、外部からはマネージャのメンバかのように扱ってあげます。
public async void SetQestPanelData(){
//呼び出すよ
QestDataList QL = Manager.GameDataManager.DataPack.QestDataList;
QL.ChangeFlg=true;
await QL.Update();
//この時点で更新済みになる
foreach(var Q in QL.List){
GameObject Q_ct = (GameObject)Resources.Load(ConfigDatas.prefabTempPath+"Home/Qest/Qest_ct");
GameObject QCT =Instantiate(Q_ct) as GameObject;
float btw = Screen.width*0.9f;
QCT.GetComponent<RectTransform>().sizeDelta = new Vector2(btw , btw/4f);
QCT.transform.SetParent(Qcontent, false);
QCT.transform.GetChild(0).GetComponent<Image>().sprite = Tool.CreateSpriteFromBytes(Convert.FromBase64String(Q.image));
QCT.transform.GetChild(0).GetComponent<Button>().onClick.AddListener(() => OnQestBT(Q.qestid));
}
}
例えば、クエスト一覧画面を作成しようと思ったらこんな感じでasyncにしてあげたクラス内でawaitをつけて欲しいデータ.Update()を呼び出します。
これで非同期で処理が進行して、画面が止まることなくデータのアップデートができます。
ロード画面も自動で出してほしいので、前回作ったPhaseManagerのローディングステータスに処理を追記します。
public class PhaseManager : MonoBehaviour{
//割愛
public int __loadingcount=0;//ロード中の件数
public bool Loading{
set{
if(value){
//trueをセットされたとき
__loadingcount+=1;
//ロード画面の呼び出し処理
Manager.ViewPrintManager.StartLoading();
}else{
//falseをセットされた場合
__loadingcount-=1; if(__loadingcount<0){__loadingcount=0;}
//ロード画面の削除処理
if(__loadingcount==0){
Manager.ViewPrintManager.EndLoading();
}
}
}
}
//割愛
継承クラスのUpdate処理内にLoading=trueみたいなとこがあったと思いますが、あれでロード画面が呼び出せます。
PhaseManagerのLoadingは
true値をセットすると今ロード中の件数を一件増やしてロード画面を表示する処理を要求、
false値をセットすると今ロード中の件数を一件減らして0になった場合はロード画面を消す処理を要求します。
取得は今のところないのでほっといてます。
で、画面描画関連担当のマネージャーはこんな感じで、
public class ViewPrintManager : MonoBehaviour{
//割愛
public void StartLoading(){
GameObject Canvas=GameObject.Find("Loading");
Canvas.GetComponent<Image>().raycastTarget=true;
if(Canvas.transform.childCount==0){
GameObject LO_prefab = (GameObject)Resources.Load(ConfigDatas.prefabTempPath+"Loading/Loading");
GameObject LO =Instantiate(LO_prefab) as GameObject;
LO.transform.SetParent(Canvas.transform, false);
}
}
public void EndLoading(){
GameObject Canvas=GameObject.Find("Loading");
foreach(Transform OB in Canvas.transform){
Destroy(OB.gameObject);
}
Canvas.GetComponent<Image>().raycastTarget=false;
}
}
ローディング画面を実際に描画してくれるわけですね。
ロード開始時の生成処理は生成済みなら生成しないようになってます。
また、ローディング画面表示窓は事前に最上階層に設置しておりRaycastTargetを表示中だけOnにしてるので、表示中は画面が触れなくなります。
動作はこんな感じ
今はローカルテストなので一瞬でロード終わっちゃうため意図的に時間をかけさせて遅い状態にしています。
結構頑張ったと思います。
だいぶすっきりしてきたので次はポップアップシステムですね。
頑張らねば。
この記事が気に入ったらサポートをしてみませんか?