Membangun Pelacak Misi Cincin Elden
Aku mencintai Skyrim. Saya dengan senang hati menghabiskan beberapa ratus jam bermain dan memutar ulang. Jadi ketika saya baru-baru ini mendengar tentang game baru, Skyrim of the 2020s , saya harus membelinya. Maka mulailah kisah saya dengan Elden Ring, RPG dunia terbuka besar dengan panduan cerita dari George R.R. Martin.
Dalam satu jam pertama permainan, saya belajar betapa brutalnya game Souls. Saya merangkak ke gua-gua menarik di sisi tebing hanya untuk mati begitu jauh di dalam sehingga saya tidak dapat mengambil mayat saya.
Saya kehilangan semua rune saya.
Aku ternganga kagum saat aku naik lift ke Sungai Siofra, hanya untuk menemukan bahwa kematian yang mengerikan menungguku, jauh dari situs rahmat terdekat. Aku dengan berani melarikan diri sebelum aku bisa mati lagi.
Saya bertemu sosok hantu dan NPC menarik yang menggoda saya dengan beberapa baris dialog… yang langsung saya lupakan begitu dibutuhkan.
10/10, sangat direkomendasikan.
Satu hal khusus tentang Elden Ring membuatku kesal - tidak ada pelacak pencarian. Pernah menjadi olahraga yang bagus, saya membuka dokumen Notes di iPhone saya. Tentu saja, itu tidak cukup.
Saya membutuhkan aplikasi untuk membantu saya melacak detail permainan RPG. Tidak ada di App Store yang benar-benar cocok dengan apa yang saya cari, jadi sepertinya saya perlu menulisnya. Namanya Shattered Ring, dan sekarang tersedia di App Store.
Pilihan Teknologi
Pada siang hari, saya menulis dokumentasi untuk Realm Swift SDK. Saya baru-baru ini menulis aplikasi template SwiftUI untuk Realm untuk memberi pengembang template pemula SwiftUI untuk dibangun, lengkap dengan alur login. Tim Realm Swift SDK terus mengirimkan fitur SwiftUI, yang membuatnya - menurut pendapat saya yang mungkin bias - titik awal yang sederhana untuk pengembangan aplikasi.
Saya menginginkan sesuatu yang dapat saya buat dengan sangat cepat - sebagian agar saya dapat kembali memainkan Elden Ring daripada menulis aplikasi, dan sebagian untuk mengalahkan aplikasi lain ke pasar sementara semua orang masih membicarakan Elden Ring. Saya tidak bisa menghabiskan waktu berbulan-bulan untuk membuat aplikasi ini. Aku menginginkannya kemarin. Realm + SwiftUI akan mewujudkannya.
Pemodelan Data
Saya tahu bahwa saya ingin melacak pencarian dalam game. Model pencariannya mudah:
class Quest: Object, ObjectKeyIdentifiable {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var name = ""
@Persisted var isComplete = false
@Persisted var notes = ""
}
Yang saya butuhkan hanyalah sebuah nama, sebuah bool untuk beralih ketika pencarian selesai, bidang catatan, dan pengenal unik.
Namun, saat memikirkan gameplay saya, saya menyadari bahwa saya tidak hanya membutuhkan pencarian - saya juga ingin melacak lokasi. Saya tersandung ke - dan dengan cepat keluar dari saat saya mulai sekarat - begitu banyak tempat keren yang mungkin memiliki karakter non-pemain (NPC) yang menarik dan jarahan yang luar biasa. Saya ingin dapat melacak apakah saya telah membersihkan suatu lokasi, atau hanya melarikan diri darinya, jadi saya dapat mengingat untuk kembali lagi nanti dan memeriksanya setelah saya memiliki perlengkapan yang lebih baik dan lebih banyak kemampuan. Jadi saya menambahkan objek lokasi:
class Location: Object, ObjectKeyIdentifiable {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var name = ""
@Persisted var isCleared = false
@Persisted var notes = ""
}
Hmm. Itu sangat mirip dengan model pencarian. Apakah saya benar-benar membutuhkan objek terpisah? Kemudian saya memikirkan salah satu lokasi awal yang saya kunjungi - Gereja Elleh - yang memiliki landasan pandai besi. Saya belum benar-benar melakukan apa pun untuk meningkatkan perlengkapan saya, tetapi mungkin menyenangkan mengetahui lokasi mana yang memiliki landasan pandai besi di masa depan ketika saya ingin pergi ke suatu tempat untuk melakukan peningkatan. Jadi saya menambahkan bool lain:
@Persisted var hasSmithAnvil = false
Kemudian saya berpikir tentang bagaimana lokasi yang sama juga memiliki pedagang. Saya mungkin ingin tahu di masa depan apakah suatu lokasi memiliki pedagang. Jadi saya menambahkan bool lain:
@Persisted var hasMerchant = false
Besar! Objek lokasi diurutkan.
Tapi… ada hal lain. Saya terus mendapatkan semua informasi menarik ini dari NPC. Dan apa yang terjadi ketika saya menyelesaikan quest - apakah saya harus kembali ke NPC untuk mengumpulkan hadiah? Itu akan mengharuskan saya untuk mengetahui siapa yang memberi saya pencarian dan di mana mereka berada. Saatnya menambahkan model ketiga, NPC, yang akan menyatukan semuanya:
class NPC: Object, ObjectKeyIdentifiable {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var name = ""
@Persisted var isMerchant = false
@Persisted var locations = List<Location>()
@Persisted var quests = List<Quest>()
@Persisted var notes = ""
}
Besar! Sekarang saya bisa melacak NPC. Saya bisa menambahkan catatan untuk membantu saya melacak berita menarik itu sambil menunggu untuk melihat apa yang akan terungkap. Saya bisa mengaitkan pencarian dan lokasi dengan NPC. Setelah menambahkan objek ini, menjadi jelas bahwa ini adalah objek yang menghubungkan yang lain. NPC berada di lokasi. Tapi saya tahu dari beberapa bacaan online bahwa terkadang NPC bergerak di dalam game, jadi lokasi harus mendukung banyak entri - itulah daftarnya. NPC memberikan quest. Tapi itu juga harus daftar, karena NPC pertama yang saya temui memberi saya lebih dari satu quest. Varre, tepat di luar Shattered Graveyard saat pertama kali memasuki game, menyuruhku untuk “Ikuti utas rahmat” dan “pergi ke kastil.” Benar, diurutkan!
Sekarang saya dapat menggunakan objek saya dengan pembungkus properti SwiftUI untuk mulai membuat UI.
Tampilan SwiftUI + Pembungkus Properti Ajaib Realm
Karena semuanya menggantung dari NPC, saya akan mulai dengan tampilan NPC. @ObservedResults
pembungkus properti memberi Anda cara mudah untuk melakukan ini.
struct NPCListView: View {
@ObservedResults(NPC.self) var npcs
var body: some View {
VStack {
List {
ForEach(npcs) { npc in
NavigationLink {
NPCDetailView(npc: npc)
} label: {
NPCRow(npc: npc)
}
}
.onDelete(perform: $npcs.remove)
.navigationTitle("NPCs")
}
.listStyle(.inset)
}
}
}
Sekarang saya dapat mengulangi daftar semua NPC, memiliki onDelete
otomatis tindakan untuk menghapus NPC, dan dapat menambahkan implementasi Realm dari .searchable
ketika saya sudah siap untuk menambahkan pencarian dan pemfilteran. Dan pada dasarnya adalah satu baris untuk menghubungkannya ke model data saya. Apakah saya menyebutkan Realm + SwiftUI luar biasa? Cukup mudah untuk melakukan hal yang sama dengan Lokasi dan Misi, dan memungkinkan pengguna aplikasi menyelami data mereka melalui jalur apa pun.
Kemudian, tampilan detail NPC saya dapat bekerja dengan @ObservedRealmObject
pembungkus properti untuk menampilkan detail NPC, dan membuatnya mudah untuk mengedit NPC:
struct NPCDetailView: View {
@ObservedRealmObject var npc: NPC
var body: some View {
VStack {
HStack {
Text("Notes")
.font(.title2)
Spacer()
if npc.isMerchant {
Image(systemName: "dollarsign.square.fill")
}
Spacer()
Text($npc.notes)
Spacer()
}
}
}
Manfaat lain dari @ObservedRealmObject
adalah saya bisa menggunakan $
notasi untuk memulai penulisan cepat, sehingga bidang catatan hanya dapat diedit. Pengguna dapat mengetuk dan menambahkan lebih banyak catatan, dan Realm hanya akan menyimpan perubahan. Tidak perlu tampilan edit terpisah, atau membuka transaksi tulis eksplisit untuk memperbarui catatan.
Pada titik ini, saya memiliki aplikasi yang berfungsi dan saya dapat dengan mudah mengirimkannya.
Tapi… aku punya pikiran.
Salah satu hal yang saya sukai dari game RPG dunia terbuka adalah memainkannya kembali sebagai karakter yang berbeda, dan dengan pilihan yang berbeda. Jadi mungkin saya ingin memutar ulang Elden Ring sebagai kelas yang berbeda. Atau - mungkin ini bukan pelacak Elden Ring secara khusus, tapi mungkin saya bisa menggunakannya untuk melacak game RPG apa pun. Bagaimana dengan game D&D saya?
Jika saya ingin melacak beberapa game, saya perlu menambahkan sesuatu ke model saya. Saya membutuhkan konsep sesuatu seperti game atau playthrough.
Iterasi pada Model Data
Saya membutuhkan beberapa objek untuk mencakup NPC, Lokasi, dan Misi yang merupakan bagian dari ini playthrough, jadi saya bisa memisahkannya dari playthrough lainnya. Jadi bagaimana jika itu adalah Game?
class Game: Object, ObjectKeyIdentifiable {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var name = ""
@Persisted var npcs = List<NPC>()
@Persisted var locations = List<Location>()
@Persisted var quests = List<Quest>()
}
Baiklah! Besar. Sekarang saya dapat melacak NPC, Lokasi, dan Misi yang ada di game ini, dan membedakannya dari game lain.
Objek Game mudah dipahami, tetapi ketika saya mulai memikirkan @ObservedResults
dalam pandangan saya, saya menyadari bahwa itu tidak akan berhasil lagi. @ObservedResults
mengembalikan semua hasil untuk jenis objek tertentu. Jadi jika saya hanya ingin menampilkan NPC untuk game ini, saya perlu mengubah pandangan saya.*
- Swift SDK versi 10.24.0 menambahkan kemampuan untuk menggunakan sintaks Swift Query di
@ObservedResults
, yang memungkinkan Anda memfilter hasil menggunakanwhere
parameter. Saya pasti refactoring untuk menggunakan ini di versi mendatang! Tim Swift SDK terus merilis pernak-pernik SwiftUI baru.
Oh. Juga, saya perlu cara untuk membedakan NPC di game ini dari yang ada di game lain. Hm. Sekarang mungkin saatnya untuk melihat backlinking. Setelah menjelajahi Realm Swift SDK Docs, saya menambahkan ini ke model NPC:
@Persisted(originProperty: "npcs") var npcInGame: LinkingObjects<Game>
Sekarang saya bisa menautkan kembali NPC ke objek Game. Tapi, sayangnya, sekarang pandangan saya menjadi lebih rumit.
Memperbarui Tampilan SwiftUI untuk Perubahan Model
Karena saya hanya menginginkan sebagian dari objek saya sekarang (dan ini sebelum @ObservedResults
pembaruan), saya mengalihkan tampilan daftar saya dari @ObservedResults
ke @ObservedRealmObject
, mengamati permainan:
@ObservedRealmObject var game: Game
Sekarang saya masih mendapatkan manfaat dari penulisan cepat untuk menambah dan mengedit NPC, Lokasi, dan Quest dalam game, tetapi kode Daftar saya harus sedikit diperbarui:
ForEach(game.npcs) { npc in
NavigationLink {
NPCDetailView(npc: npc)
} label: {
NPCRow(npc: npc)
}
}
.onDelete(perform: $game.npcs.remove
Masih tidak buruk, tetapi tingkat hubungan lain yang perlu dipertimbangkan. Dan karena ini tidak menggunakan @ObservedResults
, saya tidak bisa menggunakan implementasi Realm dari .searchable
, tetapi harus menerapkannya sendiri. Bukan masalah besar, tetapi lebih banyak pekerjaan.
Objek Beku dan Menambahkan ke Daftar
Sekarang, hingga saat ini, saya memiliki aplikasi yang berfungsi. Saya bisa mengirimkan ini apa adanya. Semuanya masih sederhana dengan pembungkus properti SDK Realm Swift melakukan semua pekerjaan.
Tapi saya ingin aplikasi saya berbuat lebih banyak.
Saya ingin dapat menambahkan Lokasi dan Misi dari tampilan NPC, dan menambahkannya secara otomatis ke NPC. Dan saya ingin dapat melihat dan menambahkan pemberi pencarian dari tampilan pencarian. Dan saya ingin dapat melihat dan menambahkan NPC ke lokasi dari tampilan lokasi.
Semua ini membutuhkan banyak penambahan ke daftar, dan ketika saya mulai mencoba melakukan ini dengan penulisan cepat setelah membuat objek, saya menyadari bahwa itu tidak akan berhasil. Saya harus meneruskan objek secara manual dan menambahkannya.
Yang saya inginkan adalah melakukan sesuatu seperti ini:
func addLocationToNpc(npc: NPC, game: Game, locationName: String) {
let realm = try! Realm()
let thisLocation = game.locations.where { $0.name == locationName }.first!
try! realm.write {
npc!.locations.append(thisLocation)
}
}
Di sinilah sesuatu yang tidak sepenuhnya jelas bagi saya sebagai pengembang baru mulai menghalangi saya. Saya tidak pernah benar-benar harus melakukan apa pun dengan threading dan objek beku sebelumnya, tetapi saya mendapatkan crash yang pesan kesalahannya membuat saya berpikir ini terkait dengan itu. Untungnya, saya ingat pernah menulis contoh kode tentang mencairkan objek beku sehingga Anda dapat bekerja dengan mereka di utas lain, jadi kembali ke dokumen - kali ini ke halaman Threading yang mencakup Objek Beku. (Lebih banyak peningkatan yang telah ditambahkan tim Realm Swift SDK sejak saya bergabung dengan MongoDB - ya!)
Setelah mengunjungi dokumen, saya memiliki sesuatu seperti ini:
func addLocationToNpc(npc: NPC, game: Game, locationName: String) {
let realm = try! Realm()
Let thawedNPC = npc.thaw()
let thisLocation = game.locations.where { $0.name == locationName }.first!
try! realm.write {
thawedNPC!.locations.append(thisLocation)
}
}
Itu terlihat benar, tetapi masih macet. Tapi kenapa? (Ini adalah saat saya mengutuk diri sendiri karena tidak memberikan contoh kode yang lebih menyeluruh di dokumen. Bekerja pada aplikasi ini pasti menghasilkan beberapa tiket untuk meningkatkan dokumentasi kami di beberapa area!)
Setelah menjelajahi forum dan berkonsultasi dengan oracle hebat Google, saya menemukan utas di mana seseorang membicarakan masalah ini. Ternyata, Anda tidak hanya harus mencairkan objek yang ingin Anda tambahkan tetapi juga benda yang ingin Anda tambahkan. Ini mungkin jelas bagi pengembang yang lebih berpengalaman, tetapi itu membuat saya tersandung untuk sementara waktu. Jadi yang benar-benar saya butuhkan adalah sesuatu seperti ini:
func addLocationToNpc(npc: NPC, game: Game, locationName: String) {
let realm = try! Realm()
let thawedNpc = npc.thaw()
let thisLocation = game.locations.where { $0.name == locationName }.first!
let thawedLocation = thisLocation.thaw()!
try! realm.write {
thawedNpc!.locations.append(thawedLocation)
}
}
Besar! Masalah terpecahkan. Sekarang saya dapat membuat semua fungsi yang saya perlukan untuk menangani penambahan (dan menghapus, ternyata) objek secara manual.
Yang Lain Hanya SwiftUI
Setelah ini, semua hal lain yang harus saya pelajari untuk menghasilkan aplikasi hanyalah SwiftUI, seperti cara memfilter, cara membuat filter dapat dipilih pengguna, dan cara menerapkan versi .searchable
saya sendiri. .
Pasti ada beberapa hal yang saya lakukan dengan navigasi yang kurang optimal. Ada beberapa perbaikan UX yang masih ingin saya lakukan. Dan mengganti @ObservedRealmObject var game: Game
kembali ke @ObservedResults
dengan hal-hal penyaringan baru akan membantu dengan beberapa perbaikan tersebut. Namun secara keseluruhan, pembungkus properti Realm Swift SDK membuat penerapan aplikasi ini cukup sederhana sehingga saya pun bisa melakukannya.
Secara total, saya membangun aplikasi dalam dua akhir pekan dan beberapa malam hari kerja. Mungkin pada suatu akhir pekan saat itu saya terjebak dengan masalah penambahan ke daftar, dan juga membuat situs web untuk aplikasi, mendapatkan semua tangkapan layar untuk dikirimkan ke App Store, dan semua hal "bisnis" yang sejalan dengan menjadi pengembang aplikasi indie.
Tetapi saya di sini untuk memberi tahu Anda bahwa jika saya, pengembang yang kurang berpengalaman dengan tepat satu aplikasi sebelumnya atas nama saya - dan dengan banyak umpan balik dari pimpinan saya - dapat membuat aplikasi seperti Shattered Ring, Anda juga bisa. Dan ini jauh lebih mudah dengan SwiftUI + fitur SwiftUI dari Realm Swift SDK. Lihat Mulai Cepat SwiftUI untuk contoh yang bagus untuk melihat betapa mudahnya.