SpriteKit与Swift:如何获取测试者的关卡完成时间数据?
Great question—collecting playtest data is absolutely crucial to tuning those medal times so they feel fair, rewarding, and aligned with what your actual players can achieve. Let’s break down how to implement this in your SpriteKit/Swift iOS game, with a focus on making it easy for testers to share their data:
1. Track Level Completion Time in Your Code
First, you’ll need to log when a level starts and calculate how long it takes the player to finish. Add this to your level’s SKScene subclass:
private var levelStartTime: Date? private let currentLevelID = "level_01" // Replace with your actual level identifier override func didMove(to view: SKView) { super.didMove(to: view) // Record the exact moment the level loads levelStartTime = Date() } // Call this function when the player reaches the level's end trigger func handleLevelCompletion() { guard let startTime = levelStartTime else { print("Error: Level start time wasn't recorded") return } // Calculate time in seconds (rounded to 1 decimal for readability) let completionTimeInSeconds = round(Date().timeIntervalSince(startTime) * 10) / 10 // Save this time for later sharing saveCompletionTime(forLevel: currentLevelID, time: completionTimeInSeconds) // Proceed with your existing level completion logic (show medals, load next level, etc.) }
2. Store Data Locally & Let Testers Share It
You don’t need a backend for testing—just save the data to a file in the app’s documents directory, and add a simple way for testers to share that file with you. Here’s how to implement the save and share functionality:
private func saveCompletionTime(forLevel levelID: String, time: TimeInterval) { // Get the path to the app's documents folder guard let docsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } let dataFileURL = docsDirectory.appendingPathComponent("playtest_level_times.plist") // Load existing data (if any) or start with an empty dictionary var playtestData: [String: [TimeInterval]] = [:] if let existingData = try? Data(contentsOf: dataFileURL), let existingDict = try? PropertyListSerialization.propertyList(from: existingData, format: nil) as? [String: [TimeInterval]] { playtestData = existingDict } // Add the new completion time to the level's entry if var levelTimes = playtestData[levelID] { levelTimes.append(time) playtestData[levelID] = levelTimes } else { playtestData[levelID] = [time] } // Save the updated data back to the file do { let data = try PropertyListSerialization.data(fromPropertyList: playtestData, format: .binary, options: 0) try data.write(to: dataFileURL) } catch { print("Failed to save playtest data: \(error.localizedDescription)") } } // Add a share button to your UI (call this in didMove(to:)) private func setupPlaytestShareButton() { let shareButton = UIButton(type: .system) shareButton.setTitle("📤 Share Playtest Data", for: .normal) shareButton.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .semibold) shareButton.addTarget(self, action: #selector(sharePlaytestDataTapped), for: .touchUpInside) // Position the button (adjust frame to fit your game's UI) let safeAreaInsets = view?.safeAreaInsets ?? .zero shareButton.frame = CGRect( x: 20, y: safeAreaInsets.top + 20, width: (view?.frame.width ?? 375) - 40, height: 44 ) view?.addSubview(shareButton) } @objc private func sharePlaytestDataTapped() { guard let docsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } let dataFileURL = docsDirectory.appendingPathComponent("playtest_level_times.plist") guard FileManager.default.fileExists(atPath: dataFileURL.path) else { print("No playtest data to share yet") // Optional: Show an alert to the tester return } // Use UIActivityViewController to let testers share via email, Messages, etc. let activityVC = UIActivityViewController(activityItems: [dataFileURL], applicationActivities: nil) // Present from the root view controller (adjust if your game uses a different setup) if let rootVC = view?.window?.rootViewController { rootVC.present(activityVC, animated: true) } }
3. Add Context to Make Data More Useful
To get better insights, consider adding a way for testers to share additional context alongside their times:
- A quick difficulty rating (1-5 scale) for each level
- A text field for notes (e.g., "Got stuck on the moving platform section")
- A toggle for their experience level (e.g., "New to platformers" / "Experienced player")
You can append this info to a separate text file or include it in the plist as a nested dictionary.
4. Analyze the Data to Set Medal Times
Once you’ve collected data from a handful of testers, use these guidelines to set your medal thresholds:
- Bronze: Aim for the 90th percentile (90% of testers finish faster than this time)—this ensures most players feel rewarded for completing the level.
- Silver: 75th percentile—rewards players who play efficiently without perfect execution.
- Gold: 30-50th percentile (median or lower)—reserve this for players who master the level’s shortcuts and optimal paths.
Don’t forget to check for outliers (e.g., a tester who finished way faster than everyone else) and adjust if a level’s time distribution is skewed—this might mean the level itself is too easy or hard, not just the medal times.
5. Keep It Simple for Testers
Make sure your testers know:
- The game automatically tracks their times—no manual input needed.
- They can share data anytime, but it’s most useful after they’ve played each level a few times.
- Encourage them to include feedback alongside the data file.
内容的提问来源于stack exchange,提问作者Discoveringmypath

