Xây dựng công cụ theo dõi nhiệm vụ nhẫn Elden
Tôi yêu Skyrim. Tôi vui vẻ dành hàng trăm giờ để chơi và chơi lại nó. Vì vậy, gần đây tôi nghe nói về một trò chơi mới, Skyrim của những năm 2020 , Tôi đã phải mua nó. Vì vậy, bắt đầu câu chuyện của tôi với Elden Ring, game nhập vai thế giới mở lớn với sự hướng dẫn câu chuyện từ George R.R. Martin.
Trong vòng một giờ đầu tiên của trò chơi, tôi đã biết được rằng trò chơi Souls có thể tàn bạo như thế nào. Tôi len lỏi vào những hang động thú vị bên vách đá chỉ để chết bên trong đến mức không thể lấy được xác của mình.
Tôi đã mất tất cả các rune của mình.
Tôi há hốc mồm trong sự kinh ngạc chờ đợi khi đi thang máy xuống sông Siofra, chỉ để thấy rằng cái chết rùng rợn đang chờ đợi tôi, cách xa địa điểm ân sủng gần nhất. Tôi dũng cảm bỏ chạy trước khi tôi có thể chết một lần nữa.
Tôi đã gặp những nhân vật ma quái và những NPC hấp dẫn, những người đã cám dỗ tôi bằng một vài câu thoại… mà tôi ngay lập tức quên ngay khi cần.
10/10, rất khuyến khích.
Một điều đặc biệt về Elden Ring khiến tôi khó chịu - không có công cụ theo dõi nhiệm vụ. Tôi đã mở một tài liệu Ghi chú trên iPhone của mình. Tất nhiên, điều đó gần như là chưa đủ.
Tôi cần một ứng dụng để giúp tôi theo dõi các chi tiết chơi RPG. Không có gì trên App Store thực sự phù hợp với những gì tôi đang tìm kiếm, vì vậy rõ ràng tôi sẽ cần phải viết nó. Nó có tên là Shattered Ring và hiện có trên App Store.
Lựa chọn công nghệ
Hàng ngày, tôi viết tài liệu cho Realm Swift SDK. Gần đây, tôi đã viết một ứng dụng mẫu SwiftUI cho Realm để cung cấp cho các nhà phát triển mẫu khởi động SwiftUI để xây dựng, hoàn chỉnh với các quy trình đăng nhập. Nhóm Realm Swift SDK đã liên tục vận chuyển các tính năng SwiftUI, điều này đã khiến nó - theo quan điểm có lẽ là thiên vị của tôi - một điểm khởi đầu đơn giản chết người để phát triển ứng dụng.
Tôi muốn thứ gì đó mà tôi có thể xây dựng siêu nhanh - một phần để tôi có thể quay lại chơi Elden Ring thay vì viết ứng dụng và một phần để đánh bại các ứng dụng khác trên thị trường trong khi mọi người vẫn đang nói về Elden Ring. Tôi không thể mất nhiều tháng để tạo ứng dụng này. Tôi đã muốn nó ngày hôm qua. Realm + SwiftUI sẽ biến điều đó thành khả thi.
Mô hình hóa dữ liệu
Tôi biết rằng tôi muốn theo dõi các nhiệm vụ trong trò chơi. Mô hình nhiệm vụ rất dễ dàng:
class Quest: Object, ObjectKeyIdentifiable {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var name = ""
@Persisted var isComplete = false
@Persisted var notes = ""
}
Tất cả những gì tôi thực sự cần là một cái tên, một công cụ để chuyển đổi khi nhiệm vụ hoàn thành, một trường ghi chú và một số nhận dạng duy nhất.
Tuy nhiên, khi nghĩ về cách chơi của mình, tôi nhận ra rằng tôi không chỉ cần nhiệm vụ - tôi còn muốn theo dõi các vị trí. Tôi tình cờ đến - và nhanh chóng thoát khỏi khi tôi bắt đầu chết - rất nhiều nơi thú vị có thể có các nhân vật không phải người chơi (NPC) thú vị và chiến lợi phẩm tuyệt vời. Tôi muốn có thể theo dõi xem tôi đã xóa một vị trí hay chỉ chạy khỏi vị trí đó, vì vậy tôi có thể nhớ quay lại sau và kiểm tra nó khi tôi có thiết bị tốt hơn và nhiều khả năng hơn. Vì vậy, tôi đã thêm một đối tượng vị trí:
class Location: Object, ObjectKeyIdentifiable {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var name = ""
@Persisted var isCleared = false
@Persisted var notes = ""
}
Hừ! Điều đó trông rất giống với mô hình nhiệm vụ. Tôi đã thực sự cần một đối tượng riêng biệt? Sau đó, tôi nghĩ về một trong những địa điểm đầu tiên tôi đến thăm - Nhà thờ Elleh - nơi có một cái đe thợ rèn. Tôi chưa thực sự làm bất cứ điều gì để cải thiện thiết bị của mình, nhưng có thể rất tuyệt nếu biết những địa điểm nào có đe thợ rèn trong tương lai khi tôi muốn đến một nơi nào đó để nâng cấp. Vì vậy, tôi đã thêm một bool khác:
@Persisted var hasSmithAnvil = false
Sau đó, tôi nghĩ về cách mà cùng một địa điểm cũng có một thương gia. Tôi có thể muốn biết liệu một vị trí có người bán trong tương lai hay không. Vì vậy, tôi đã thêm một bool khác:
@Persisted var hasMerchant = false
Tuyệt quá! Đối tượng vị trí đã được sắp xếp.
Nhưng có một cái gì đó khác. Tôi tiếp tục nhận được tất cả những mẩu chuyện thú vị này từ các NPC. Và điều gì đã xảy ra khi tôi hoàn thành một nhiệm vụ - tôi có cần quay lại gặp NPC để nhận phần thưởng không? Điều đó sẽ yêu cầu tôi biết ai đã giao cho tôi nhiệm vụ và họ đang ở đâu. Đã đến lúc thêm một mô hình thứ ba, NPC, sẽ gắn kết mọi thứ lại với nhau:
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 = ""
}
Tuyệt quá! Bây giờ tôi có thể theo dõi các NPC. Tôi có thể thêm ghi chú để giúp tôi theo dõi những mẩu chuyện thú vị đó trong khi chờ xem điều gì sẽ diễn ra. Tôi có thể liên kết các nhiệm vụ và địa điểm với NPC. Sau khi thêm đối tượng này, rõ ràng đây là đối tượng kết nối những đối tượng khác. NPC có mặt tại các địa điểm. Nhưng tôi biết từ một số đọc trực tuyến rằng đôi khi các NPC di chuyển xung quanh trong trò chơi, vì vậy các vị trí sẽ phải hỗ trợ nhiều mục nhập - do đó mới có danh sách. NPC đưa ra các nhiệm vụ. Nhưng đó cũng nên là một danh sách, bởi vì NPC đầu tiên tôi gặp đã cho tôi nhiều hơn một nhiệm vụ. Varre, ngay bên ngoài Nghĩa địa bị vỡ khi bạn lần đầu tiên tham gia trò chơi, đã bảo tôi “Hãy làm theo những sợi dây của ân sủng” và “hãy đến lâu đài”. Đúng, đã sắp xếp!
Bây giờ tôi có thể sử dụng các đối tượng của mình với các trình bao bọc thuộc tính SwiftUI để bắt đầu tạo giao diện người dùng.
SwiftUI Views + Realm’s Magical Property Wrappers
Vì mọi thứ đều treo ngược với NPC, tôi sẽ bắt đầu với các lượt xem NPC. @ObservedResults
trình bao bọc thuộc tính cung cấp cho bạn một cách dễ dàng để thực hiện việc này.
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)
}
}
}
Bây giờ tôi có thể lặp lại danh sách tất cả các NPC, có một onDelete
tự động hành động để xóa NPC và có thể thêm việc triển khai .searchable
của Realm khi tôi đã sẵn sàng thêm tìm kiếm và lọc. Và về cơ bản nó là một dòng để kết nối nó với mô hình dữ liệu của tôi. Tôi đã đề cập đến Realm + SwiftUI có tuyệt vời không? Thật dễ dàng để thực hiện điều tương tự với Vị trí và Nhiệm vụ, đồng thời giúp người dùng ứng dụng có thể đi sâu vào dữ liệu của họ thông qua bất kỳ đường dẫn nào.
Sau đó, chế độ xem chi tiết NPC của tôi có thể hoạt động với @ObservedRealmObject
trình bao bọc thuộc tính để hiển thị chi tiết NPC và giúp dễ dàng chỉnh sửa 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()
}
}
}
Một lợi ích khác của @ObservedRealmObject
là tôi có thể sử dụng $
ký hiệu để bắt đầu viết nhanh, vì vậy trường ghi chú sẽ có thể chỉnh sửa được. Người dùng có thể nhấn vào và chỉ cần thêm các ghi chú khác và Realm sẽ chỉ lưu các thay đổi. Không cần chế độ xem chỉnh sửa riêng biệt hoặc mở một giao dịch viết rõ ràng để cập nhật ghi chú.
Tại thời điểm này, tôi đã có một ứng dụng đang hoạt động và tôi có thể dễ dàng vận chuyển nó.
Nhưng… tôi đã có một suy nghĩ.
Một trong những điều tôi yêu thích ở các game RPG thế giới mở là chơi lại chúng dưới dạng các nhân vật khác nhau và với các lựa chọn khác nhau. Vì vậy, có lẽ tôi muốn chơi lại Elden Ring như một lớp khác. Hoặc - có thể đây không phải là một trình theo dõi Elden Ring cụ thể, nhưng có lẽ tôi có thể sử dụng nó để theo dõi bất kỳ trò chơi RPG nào. Còn các trò chơi D&D của tôi thì sao?
Nếu tôi muốn theo dõi nhiều trò chơi, tôi cần thêm thứ gì đó vào mô hình của mình. Tôi cần một khái niệm về một thứ gì đó như một trò chơi hoặc một trò chơi.
Lặp lại trên Mô hình Dữ liệu
Tôi cần một số đối tượng để bao gồm các NPC, Vị trí và Nhiệm vụ là một phần của cái này playthrough, vì vậy tôi có thể giữ chúng tách biệt với các playthrough khác. Vậy nếu đó là một Trò chơi thì sao?
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>()
}
Ổn thỏa! Tuyệt quá. Giờ đây, tôi có thể theo dõi các NPC, Vị trí và Nhiệm vụ trong trò chơi này và giữ chúng khác biệt với các trò chơi khác.
Đối tượng Trò chơi rất dễ hình dung, nhưng khi tôi bắt đầu nghĩ về @ObservedResults
theo quan điểm của tôi, tôi nhận ra rằng điều đó sẽ không còn hiệu quả nữa. @ObservedResults
trả về tất cả các kết quả cho một loại đối tượng cụ thể. Vì vậy, nếu tôi muốn chỉ hiển thị các NPC cho trò chơi này, tôi cần phải thay đổi quan điểm của mình. *
- Swift SDK phiên bản 10.24.0 đã thêm khả năng sử dụng cú pháp Truy vấn Swift trong
@ObservedResults
, cho phép bạn lọc kết quả bằngwhere
tham số. Tôi chắc chắn đang cấu trúc lại để sử dụng cái này trong một phiên bản trong tương lai! Nhóm SDK Swift đã liên tục phát hành các tính năng mới của SwiftUI.
Ồ. Ngoài ra, tôi cần một cách để phân biệt các NPC trong trò chơi này với các NPC trong các trò chơi khác. Hrm. Bây giờ có thể là lúc để xem xét liên kết ngược. Sau khi quay vòng trong Tài liệu SDK Realm Swift, tôi đã thêm cái này vào mô hình NPC:
@Persisted(originProperty: "npcs") var npcInGame: LinkingObjects<Game>
Bây giờ tôi có thể liên kết ngược các NPC với đối tượng Trò chơi. Nhưng, than ôi, bây giờ quan điểm của tôi trở nên phức tạp hơn.
Cập nhật Chế độ xem SwiftUI cho các Thay đổi Mô hình
Vì bây giờ tôi chỉ muốn một tập hợp con các đối tượng của mình (và đây là trước @ObservedResults
cập nhật), tôi đã chuyển chế độ xem danh sách của mình từ @ObservedResults
tới @ObservedRealmObject
, quan sát trò chơi:
@ObservedRealmObject var game: Game
Bây giờ tôi vẫn nhận được lợi ích của việc viết nhanh để thêm và chỉnh sửa NPC, Vị trí và Nhiệm vụ trong trò chơi, nhưng mã Danh sách của tôi phải cập nhật một chút:
ForEach(game.npcs) { npc in
NavigationLink {
NPCDetailView(npc: npc)
} label: {
NPCRow(npc: npc)
}
}
.onDelete(perform: $game.npcs.remove
Vẫn không phải là xấu, nhưng một mức độ khác của các mối quan hệ để xem xét. Và vì điều này không sử dụng @ObservedResults
, Tôi không thể sử dụng triển khai Vương quốc của .searchable
, nhưng sẽ phải tự mình thực hiện. Không phải là một vấn đề lớn, nhưng nhiều công việc hơn.
Đối tượng đông lạnh và thêm vào danh sách
Bây giờ, cho đến thời điểm này, tôi có một ứng dụng đang hoạt động. Tôi có thể gửi cái này như hiện tại. Mọi thứ vẫn đơn giản với trình bao bọc thuộc tính Realm Swift SDK thực hiện tất cả công việc.
Nhưng tôi muốn ứng dụng của mình làm được nhiều hơn thế.
Tôi muốn có thể thêm Địa điểm và Nhiệm vụ từ chế độ xem NPC và tự động thêm chúng vào NPC. Và tôi muốn có thể xem và thêm người cho nhiệm vụ từ chế độ xem nhiệm vụ. Và tôi muốn có thể xem và thêm NPC vào các vị trí từ chế độ xem vị trí.
Tất cả những điều này đều yêu cầu thêm rất nhiều vào danh sách và khi tôi bắt đầu cố gắng thực hiện việc này bằng cách viết nhanh sau khi tạo đối tượng, tôi nhận ra rằng điều đó sẽ không hiệu quả. Tôi phải chuyển các đối tượng xung quanh và nối chúng theo cách thủ công.
Những gì tôi muốn là làm một cái gì đó như thế này:
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)
}
}
Đây là nơi mà điều gì đó không hoàn toàn rõ ràng đối với tôi khi một nhà phát triển mới bắt đầu cản đường tôi. Tôi chưa bao giờ thực sự phải làm bất cứ điều gì với các đối tượng phân luồng và đóng băng trước đây, nhưng tôi đã gặp sự cố mà thông báo lỗi khiến tôi nghĩ rằng điều này có liên quan đến điều đó. May mắn thay, tôi nhớ đã viết một ví dụ mã về việc làm tan băng các đối tượng bị đóng băng để bạn có thể làm việc với chúng trên các chuỗi khác, vì vậy, nó đã quay trở lại tài liệu - lần này là trang Phân luồng bao gồm các Đối tượng Đông lạnh. (Các cải tiến khác mà nhóm Realm Swift SDK đã thêm vào kể từ khi tôi tham gia MongoDB - yay!)
Sau khi truy cập các tài liệu, tôi có một cái gì đó như thế này:
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)
}
}
Điều đó có vẻ đúng, nhưng vẫn bị lỗi. Nhưng tại sao? (Đây là lúc tôi tự nguyền rủa bản thân vì đã không cung cấp một ví dụ mã kỹ lưỡng hơn trong tài liệu. Làm việc trên ứng dụng này chắc chắn đã tạo ra một số vé để cải thiện tài liệu của chúng tôi trong một số lĩnh vực!)
Sau khi quay vòng trên các diễn đàn và tham khảo ý kiến của người tiên tri tuyệt vời của Google, tôi tình cờ gặp một chủ đề nơi ai đó đang nói về vấn đề này. Hóa ra, bạn phải làm tan băng không chỉ vật bạn đang cố gắn vào mà còn phải làm tan vật bạn đang cố nối vào. Điều này có thể rõ ràng với một nhà phát triển có kinh nghiệm hơn, nhưng nó đã khiến tôi vấp phải một thời gian. Vì vậy, những gì tôi thực sự cần là một cái gì đó như thế này:
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)
}
}
Tuyệt quá! Vấn đề đã được giải quyết. Giờ đây, tôi có thể tạo tất cả các funcs mà tôi cần để xử lý thủ công việc nối thêm (và loại bỏ, khi nó xảy ra) của các đối tượng.
Mọi thứ khác chỉ là SwiftUI
Sau đó, mọi thứ khác mà tôi phải học để tạo ra ứng dụng chỉ là SwiftUI, như cách lọc, cách làm cho bộ lọc có thể người dùng chọn và cách triển khai phiên bản .searchable
của riêng tôi .
Chắc chắn có một số điều tôi đang làm với điều hướng kém hơn mức tối ưu. Tôi vẫn muốn thực hiện một số cải tiến về trải nghiệm người dùng. Và chuyển đổi trò chơi @ObservedRealmObject var game: Game
của tôi quay lại @ObservedResults
với công cụ lọc mới sẽ giúp thực hiện một số cải tiến đó. Nhưng nhìn chung, trình bao bọc thuộc tính Realm Swift SDK đã làm cho việc triển khai ứng dụng này trở nên đơn giản đến mức ngay cả tôi cũng có thể làm được.
Tổng cộng, tôi đã xây dựng ứng dụng trong hai ngày cuối tuần và một số ít các đêm trong tuần. Có lẽ một ngày cuối tuần của thời gian đó là tôi bị mắc kẹt với vấn đề liên quan đến danh sách, đồng thời tạo trang web cho ứng dụng, nhận tất cả ảnh chụp màn hình để gửi lên App Store và tất cả những thứ “kinh doanh” đi kèm với việc trở thành một nhà phát triển ứng dụng độc lập.
Nhưng tôi ở đây để nói với bạn rằng nếu tôi, một nhà phát triển ít kinh nghiệm với chính xác một ứng dụng trước đó mang tên tôi - và điều đó với rất nhiều phản hồi từ trưởng nhóm của tôi - cũng có thể tạo ra một ứng dụng như Shattered Ring. Và thật dễ dàng hơn rất nhiều với SwiftUI + các tính năng SwiftUI của Realm Swift SDK. Hãy xem phần Bắt đầu nhanh SwiftUI để biết ví dụ điển hình để xem nó dễ dàng như thế nào.