おそるおそるSwift 4-5 ハイローゲームを作る ~ハイスコアを記録する~
Swift5+Xcode11.6
今回のゴール
ハイスコアを記録して表示するために、まずはゲームごとにスコアを保存できるようにします。
どこへ保存するか
保存先候補は、アプリ内部かデータフォルダのどちらかとなります。アプリ内部の方はアプリ削除とともにデータが消去されてしまうというデメリットがある一方で、データの隠蔽性が高いので、ユーザが勝手に書き換えることができにくいというメリットがあるようです。ゲームのハイスコアを書き換える物好きはいないと思いますが、今回はアプリ内部に保存してみることにします。
UserDefaultsはどこ?
アプリ内部のデータ保存領域にはUserDefaultsでアクセスすることができます。話が若干前後しますが、データの隠蔽性が災いしてUserDefaultsに書き込まれたデータをリセット(消去)したくても、どこにあるのかよくわかりません。プロジェクトファイルのどこかにあるのだろうと探しましたが結局見つからなかったので、効率性重視で「リセットボタン」を作ってしまいました。
後で説明しますが、スコア履歴は「highScore」というキー(識別子)でUserDefaultsに保存することにしています。また、下のコードはprocessorx.swift内の一部です。
//processorx.swift func reset() { UserDefaults.standard.removeObject(forKey: "highScore") }
配列の読み書き
配列の読み書き方法は、StackOverFlowで探しました(正確にはGoogleで検索したら行き着きました)。こう言った掲示板を見ていても、コードスキルは人それぞれだなとつくづく思います。参考にしたのは、下の画像のスレです。
Computedプロパティとは?確か参考書にあったと思い確認すると、定数と変数で定義するプロパティ(Storedプロパティという)と異なり、「関数を介して値をやり取りするプロパティ」で、「プロパティへの参照に対してはgetブロックで値を返し、値の設定に対してはsetブロックで応じます」とのことです。他にも方法はあるのでしょうが、これが一番クールな感じがしたので、今回はこれをお手本にしてみました。
現在の時刻を書き出すだけなのに
スコアの履歴は「プレイした日時、賞金、成功回数」の3つをタプルにしたものとすることにしました(計算式上、成功回数は賞金額から割り出すことができるので本来不要なのですが、練習のためです)。
最初の難関はまさかの現在時刻の書き出しでした。現在時刻はDate()で簡単に取得できるのですが、それを日本のローカル時刻に変換するのがやっかいで、GMTからJSTへの換算をSwift上で行わなければならないようなのです。そのために使用するのが、DateFormatter()ですが、構文が長すぎて好きになれそうもありません。下のコードは、porcessorx.swiftの一部ですが、最終的にrecordDate変数に現在時刻を代入しています。
dateformatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddHHmmss", options: 0, locale: Locale(identifier: "ja_JP")) recordDate = dateformatter.string(from: Date())
配列が思うように書き出せない
配列は多次元配列を使うことにし、下のコードを組んでみたのですが、デバッグウィンドウに表示された結果を見ると、どうも意図したとおりになっていないようです。
import Foundation class Processor : ObservableObject { @Published var history :[Int] = [5] //history:抽選数字履歴保持用配列 @Published var result: String = "" @Published var reward: Int = 1 var randNum: Int = 0 //randNum:乱数 let dateformatter = DateFormatter() var recordDate = "" var highScore:[Any] { get { return UserDefaults.standard.array(forKey: "highScore") ?? [] } set { UserDefaults.standard.set(newValue, forKey: "highScore") } } // func judge(choice:String) -> String { func judge(choice:String) { randNum = Int.random(in:1...9) history.append(self.randNum) for (index, his) in history.enumerated() { print("history[\(index)]: \(his)") } print("endIndex:\(history.endIndex)") let prevNum:Int = history[history.endIndex - 2] //prevNum:1つ前の抽選数字 print ("prevNum:\(prevNum)") let nowNum:Int = history[history.endIndex - 1] //nowNum:今回の抽選数字 print ("NowNum:\(nowNum)") if (nowNum == prevNum) || ((nowNum >= prevNum)&&(choice == "high")) || ((nowNum <= prevNum)&&( choice == "low")) { result = "Win" reward *= 2 } else { result = "Lose! You Lost All Money!!" dateformatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddHHmmss", options: 0, locale: Locale(identifier: "ja_JP")) recordDate = dateformatter.string(from: Date()) highScore.append([recordDate,reward,history.endIndex - 1]) print (highScore) reward = 1 history = [5] } } func num(times:Int) -> String { if history.endIndex >= times { return String(history[history.endIndex - times]) } else { return "-" } } func reset() { UserDefaults.standard.removeObject(forKey: "highScore") } }
多次元配列にしたかったのですが、どうもそうなっていないようで、何やら見えてはいけないものが見えてしまっているようにも思えます。
いろいろ調べてみた結果、配列の宣言の時に多次元配列にするよと明示すればうまくいくことができました(10行目〜の宣言文を下のように変更しました)。
var highScore:[[Any]] { get { return UserDefaults.standard.array(forKey: "highScore") as? [[Any]] ?? [] } set { UserDefaults.standard.set(newValue, forKey: "highScore") } }
一瞬、デバッグウインドウだから__NSArrayIとか表示されてしまうのかなと思いましたが、自分にとって都合のいいように解釈するのはやはりダメですね。
as?とは。??とは。
さて、上のコードのas? …はなんでしょう。Swiftにしても、他の言語にしてもこの辺の記号が出てくると、途端に人間味がなくなるので苦手です。調べたところ「…型で書き出して(キャスト)してみて。だめだったら、nilを返して」という意味のようです。似たものとしてas ! …がありますが、こちらは「ダメ元で…型でキャストしてよ」という意味のようで、後者の場合はダメだった場合クラッシュするそうです。通常はクラッシュ回避のためにas?を使った方がよさそうですね。
また、?? …は、もしnilだった場合は、代わりに…を返してという意味のようです。必須とまではいきませんが、エラー回避のために入れておいた方がよさそうですね。
?記号は、オプショナル型(nilが代入されることを許容する)の変数、定数の宣言でも使われ、オプショナル型の値(オプショナルバリュー)を使う時はアンラップ(包みから取り出す。サランラップから取り出すのと同じイメージでしょうか)しなければならないことになっているようです。多分、次に使う時までにすっかり忘れてしまうので、そのときまたおさらいすることにします。
次回は
UserDefaultsに保存されたスコア履歴を利用します。