ゲームというとUnityなどのゲームエンジンで実装するイメージが強いですが、Swift製のゲームもかなり多いです。SwiftであればGameKitも利用できますが、Androidとそれぞれネイティブで実装しているならば、NCMBでランキング保存をして、共通化するのもお勧めです。
今回はちょっとしたゲームにランキング機能を実装してみます。コードはNCMBMania/Swift_Ranking_Demo: Swift SDKを使って実装したランキング機能のデモですにアップしてあります。
今回のプロジェクト
今回は言語がSwift、インタフェースがSwiftUI、ライフサイクルはSwiftUI Appとしています。
SDKのインストール
FileメニューからSwift Packages > Add Package Dependencyと選択します。
出てきたダイアログでSwift SDKのGitリポジトリURLを入力します。GitHubのリポジトリでHTTPSとして取得できるもの、または下記URLになります。
https://github.com/NIFCLOUD-mbaas/ncmb_swift.git
バージョンは最新のものでかまいません。
後はFinishボタンを押せば完了です。
初期化
今回はSwiftUIを利用しています。ライフサイクルもSwiftUIです。
まずSDKをインポートします。
import SwiftUI
import NCMB
次に scenePhase
を追加します。
@main
struct forumApp: App {
// 追加
@Environment(\.scenePhase) private var scenePhase
後は body 内で onChange
を使って初期化します。
var body: some Scene {
WindowGroup {
ContentView()
}.onChange(of: scenePhase) { scene in
switch scene {
case .active:
// キーの設定
let applicationKey = "YOUR_APPLICATION_KEY"
let clientKey = "YOUR_CLIENT_KEY"
// 初期化
NCMB.initialize(applicationKey: applicationKey, clientKey: clientKey)
case .background: break
case .inactive: break
default: break
}
}
}
匿名認証
今回は匿名認証(ID、パスワードを使わない、デバイス固有に生成したUUIDを使った認証)を利用します。そこで、初期化した後にSwift SDKの匿名認証を有効にします。
NCMBUser.enableAutomaticUser()
認証状態の確認
次に認証状態を確認する checkAuth
関数を呼び出します。内容は次の通りです。 NCMBUser.currentUser
が nil の場合は認証されていないので、匿名認証を実行します。
func checkAuth() -> Void {
// 認証データがあれば処理は終了
if NCMBUser.currentUser != nil {
return;
}
// 匿名認証実行
_ = NCMBUser.automaticCurrentUser()
}
セッションの有効性チェック
認証されている場合でも、セッションの有効性は確認していません。ローカルにある認証データを復元している状態のためです。そこで、データストアに一度アクセスを行い、API通信の有効性を確認します。
func checkSession() -> Bool {
var query : NCMBQuery<NCMBObject> = NCMBQuery.getQuery(className: "Todo")
query.limit = 1 // レスポンス件数を最小限にする
// アクセス
let results = query.find()
// 結果の判定
switch results {
case .success(_): break
case .failure(_):
// 強制ログアウト処理
_ = NCMBUser.logOut()
return false
}
return true
}
上記コードでログアウト処理を実行していますが、これは必ず失敗します。セッションが無効になっているため、ログアウトAPIの実行もまた、失敗するためです。そこで NCMB/NCMBUser.swift
を開いて logOutInBackground
を次のように修正します。
public class func logOutInBackground(callback: @escaping NCMBHandler<Void>) -> Void {
NCMBLogoutService().logOut(callback: {(result: NCMBResult<NCMBResponse>) -> Void in
// レスポンスに関係なくログアウト処理
deleteFile()
_currentUser = nil
switch result {
case .success(_):
callback(NCMBResult<Void>.success(()))
break
case let .failure(error):
callback(NCMBResult<Void>.failure(error))
break
}
})
}
bodyのコードは次のようになります。
var body: some Scene {
WindowGroup {
ContentView()
}.onChange(of: scenePhase) { scene in
switch scene {
case .active:
// キーの設定
let applicationKey = "YOUR_APPLICATION_KEY"
let clientKey = "YOUR_CLIENT_KEY"
// 初期化
NCMB.initialize(applicationKey: applicationKey, clientKey: clientKey)
NCMBUser.enableAutomaticUser()
checkAuth()
if checkSession() == false {
checkAuth()
}
case .background: break
case .inactive: break
default: break
}
}
}
ゲームの実装について
今回は10秒間タップして、その回数を競うという簡単なアプリになっています。ゲーム部分は主ではないので説明は省きますが、こんな感じです。
struct ContentView: View {
// 10秒間のカウントタイマー
@State private var timer: Timer? = nil
// タップした回数
@State private var count = 0
// ゲームが開始したら true。時間切れになったら false
@State private var enabled = true
// 開始したかどうかのフラグ
@State private var start = false
// ゲーム秒数
@State private var limit = 10
// ランキングを表示するかどうかのフラグ
@State private var showRanking = false
// ゲームユーザ名
@State private var userName = ""
// メッセージ表示用のフラグ
@State private var showAlert = false
// メッセージ内容
@State private var message = ""
var body: some View {
VStack {
Text("連打アプリ")
.padding(10)
.font(.title)
Text("\(count)回")
.padding(10)
Text(limitText())
.padding(10)
HStack {
Button("連打!", action: add)
.disabled(!enabled)
Button("リセット", action: reset)
.disabled(enabled)
}
if count > 0 && limit == 0{
VStack {
HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: nil, content: {
TextField("名前", text: $userName)
.padding(20)
})
HStack {
Button("記録する", action: record)
Button("ランキングを見る", action: {
showRanking = true
})
}
}
}
}.sheet(isPresented: $showRanking, content: {
RankingView()
})
.alert(isPresented: $showAlert, content: {
Alert(title: Text(message))
})
}
// リセットボタンをタップした際の処理
func reset() {
count = 0
enabled = true
start = false
limit = 10
}
// 連打ボタンをタップした際の処理
func add() {
// 終わっていればすぐ終了
if !enabled {
return
}
// 開始していなければタイマー開始
if !start {
startTimer()
}
// タップ回数のカウントアップ
count = count + 1
}
// ゲーム開始時に一度だけ実行する
func startTimer() {
// フラグを立てる
start = true
// ゲームユーザ名を取得しておく
let user = NCMBUser.currentUser
if let name:String = user!["displayName"] {
userName = name
}
// 10秒間のカウントダウンタイマー用
let cal = Calendar(identifier: .japanese)
let endDate = Calendar.current.date(byAdding:.second,value:limit,to:Date())
timer = Timer.scheduledTimer(withTimeInterval:0.01, repeats: true){ _ in
let timeVal = cal.dateComponents([.day,.hour,.minute,.second], from: Date(),to: endDate!)
limit = timeVal.second!
// 0になったら終了のフラグを立てて、タイマーを止める
if limit == 0 {
enabled = false
timer?.invalidate()
}
}
}
// 残り何秒表示用の文字列を返す
func limitText() -> String {
if limit == 0 {
return "終了!"
} else {
return "残り\(limit)秒"
}
}
// ゲームスコアの記録用
func record() {
}
// 何位か表示するためにカウントを取る
func getMyRanking() -> Int {
}
}
ゲームスコアの記録
ゲームスコアは record
関数で保存します。まずゲームユーザ名を確認します。
if userName == "" {
message = "ユーザ名を入力してください"
showAlert = true
return
}
次にランキングデータを作成して、ゲームユーザ名とスコアをセットします。
// ランキングデータの作成
let ranking = NCMBObject(className: "Ranking")
// ゲームユーザ名とスコアを設定
ranking["displayName"] = userName
ranking["score"] = count
不特定多数にデータを改ざんされることがないよう、アクセス権限を設定します。
// ACL(アクセス権限)を作成・設定
var acl = NCMBACL.empty
let user = NCMBUser.currentUser
// ゲームした本人のみ読み書き可能
acl.put(key: user!.objectId!, readable: true, writable: true)
// 他の人は閲覧のみ可
acl.put(key: "*", readable: true, writable: false)
ranking.acl = acl
そして保存します。
// 保存
let results = ranking.save()
ユーザ名の保存
設定したユーザ名をアプリの再起動後も表示できるように保存しておきます。
// 名前を保存しておく
user!["displayName"] = userName
_ = user!.save()
ランキングを取得する
保存したら、現在の順位を取得してアラートで表示するようにします。
switch results {
case .success(_): // 保存が成功した場合
let num = getMyRanking() // ランキングを取得
message = "あなたのランキングは\(num)位です!" // アラート用メッセージ
default: // 保存失敗した場合
message = "ランキングを保存できませんでした" // アラート用メッセージ
}
showAlert = true // アラートを表示するフラグを立てる
getMyRankingについて
getMyRankingはスコアがランキングで何位だったか取得する関数です。まずランキングデータ検索用のクエリオブジェクトを作成します。
// ランキングデータ検索用のクエリオブジェクトを作成
var query = NCMBQuery.getQuery(className: "Ranking")
そして検索条件を指定して実行します。greatherThanで、現在のスコアより大きいデータだけを指定します。countを使えば件数を取得できます。
// 検索条件を指定
query.where(field: "score", greaterThan: count)
// 検索実行(countで件数が取得できます)
let results = query.count()
その結果を取得して、 +1 したものを返します。もし一番良いスコアを取った場合には上位データが存在しないので0件になります。そこで +1 して1位という結果を返します。
switch results {
case let .success(num): // 検索できれば引数に件数が返ってきます
return num + 1 // 件数 + 1が順位
case .failure(_):
return 0
}
ランキングの表示
ランキングはRankingViewで表示します。
// ContentView.swift
}.sheet(isPresented: $showRanking, content: {
RankingView()
})
ランキングの表示自体は、とても簡単なものです。
struct RankingView: View {
@State private var rankings: [NCMBObject] = []
var body: some View {
VStack {
Text("ランキング")
.font(.title)
if rankings.count > 0 {
List {
ForEach(rankings.indices) { i in
Text(listString(index: i))
}
}
}
}.onAppear {
getRanking()
}
}
}
画面を表示した際に getRanking
を実行し、ランキングの上位100件のデータを取得しています。先ほどは count を使いましたが、今回はデータ自体が欲しいので find を使っています。並び順は -score
とすることでスコアの降順(高いスコアが上。徐々に小さくなる)になります。
// ランキングデータを取得します
func getRanking() {
// ランキングデータ検索用のクエリオブジェクトを作成
var query = NCMBQuery.getQuery(className: "Ranking")
// 並び順はスコアの高い順です
query.order = ["-score"]
// 100件のデータを取得します
query.limit = 100
// 検索実行
let results = query.find()
switch results {
case let .success(ary):
// 検索成功したら、それをrankingsに適用します
rankings = ary
case .failure(_): break
}
}
そして取得したデータを listString
で文字列にしています。
// ランキングの順位、ゲームユーザ名、スコアを返します
func listString(index: Int) -> String {
let ranking = rankings[index]
let displayName: String = ranking["displayName"] ?? ""
let score: Int = ranking["score"] ?? 0
return "\(index + 1)位 \(displayName)さん (\(score)点)"
}
画面表示の際に rankings.indices
を使っているのは、NCMB自体には順位のデータは存在せず、配列のインデックスからダイナミックに順位を生成しているからです。同じスコアの場合には別途計算がいるので注意してください(@Stateに入れて次の配列要素へ持ち回すのが良さそうです)。
まとめ
このような形でランキング機能が実装できます。本格的なゲームに限らず、ランキング機能が使える場面は多いと思いますので、ぜひ役立ててください。
コードはNCMBMania/Swift_Ranking_Demo: Swift SDKを使って実装したランキング機能のデモですにアップしてありますので、参考にしてください。