记一次 iOS 地图格子游戏的开发踩坑:热力衰减、GPS 漂移过滤与渲染优化

起因:满屏同色方块,走了一周跟没走一样

去年我开始做一个个人项目——把真实世界的行走轨迹变成像素格占领游戏,iOS 平台,SwiftUI + MapKit。最初的想法很简单:走过的路点亮对应的地图格子,积少成多就有成就感。

结果第一版做出来,连续走了一周后打开 App,满屏同色方块糊成一片。今天走的路和三天前走的路完全分不清,毫无层次感。这让我意识到地图需要"时间维度"——哪些是今天的成果,哪些是历史痕迹,得一眼区分出来。

围绕这个问题,前后折腾了热力衰减、Zone 征服判定、GPS 漂移过滤、大量格子的渲染性能这几块。项目叫「像素征途」,目前在 App Store 上线,体量还小,但技术上踩的坑确实值得记录一下。

Zone 征服判定:为什么阈值是 90% 而不是 100%

系统把地图切成 8×8 的格子区域(Zone),玩家走过的格子被"点亮",当一个 Zone 里点亮格子达到阈值就算"征服"。

enum TerritoryRules {
    static let zoneSideCount = 8
    static let zonePerfectTileThreshold = zoneSideCount * zoneSideCount // 64
    static let zoneConqueredTileThreshold = 58 // ~90%
}

enum ZoneConquestRules {
    static func evaluate(
        litTiles: Int,
        conqueredThreshold: Int,
        perfectThreshold: Int
    ) -> ZoneConquestEvaluation {
        let clamped = max(0, litTiles)
        return ZoneConquestEvaluation(
            isConquered: clamped >= conqueredThreshold,
            isPerfect: clamped >= perfectThreshold
        )
    }
}

征服线为什么是 58(约 90%)而不是 64(100%)?因为城市环境里,一个 Zone 总有几个格子物理上走不到——河道中间、围墙内侧、高架桥正下方。我在上海测了 12 个不同类型的街区(老城区窄巷、浦东写字楼群、滨江步道、住宅小区等),一个"体感上全走遍了"的区域实际点亮率在 88%-93% 之间。所以取 90% 作为征服线,64 格全亮则给"完美"徽章。

这个数字调过好几版。最早设 80%(51 格),结果开车路过一个区域就能触发征服,太廉价了,完全没有成就感。

热力衰减:让地图有呼吸感

核心思路是给格子和路线加时间维度的透明度衰减:

  • 4 天内:全亮
  • 7 天后:开始明显变暗
  • 14 天后:区域高亮消失
  • 30 天后:只剩 12% 透明度的残影

routeGlowDecayDay 最早设的 3 天,结果周末一过周一打开地图就"暗"了大半,心理上挺打击的。改成 7 天后,即使一周没重复走同一条路,领地视觉上也不会给人"在流失"的压迫感。而 30 天后保留的 12% 残影,是为了保持历史感——你去过的地方永远不会完全消失。

实现上就是每次打开地图时根据 Date 差值计算当前 opacity,用 SwiftUI 的 .opacity() modifier 直接绑定,逻辑很轻。

GPS 漂移过滤:速度阈值比卡尔曼滤波管用

城市环境里 GPS 漂移是老问题了。人站在楼宇间不动,坐标可能在 10-30 米范围内随机跳。不过滤的话,站着不动也会"解锁"周围格子,直接破坏游戏性。

我的方案分两层:

第一层:速度阈值硬过滤。 相邻两个 CLLocation 算瞬时速度,低于 0.3m/s 直接丢弃。正常缓步走大约 0.8-1.2m/s,0.3 以下基本都是噪声。这一层过滤掉了 95% 以上的误解锁。

第二层:一维卡尔曼滤波做平滑。 经纬度分别维护滤波器,过程噪声和测量噪声的比值根据运动状态动态调整——移动中信任 GPS 多一些,静止时信任预测值多一些。

说实话,卡尔曼在这个场景里效果没想象中大。真正管用的就是第一层速度阈值。滤波器主要是让轨迹线视觉上更平滑,属于美观层面的优化。

渲染性能:2000 格以上怎么办

当点亮格子超过 2000 之后,直接用 MapKit 的 MKOverlay 逐格渲染,帧率从 60fps 掉到 35fps 左右(iPhone 13 实测),滑动地图能感觉到明显卡顿。

解决思路是按 zoom level 分级渲染:

  • 远景(zoom < 15):只画 zone 级别的色块,一个 zone 对应一个矩形 overlay,数量从几千降到几百
  • 近景(zoom ≥ 15):切换为单格细节渲染,此时一屏可见格子约 80-120 个,压力可控

切换时做了 0.3 秒的 alpha 渐变过渡,避免突兀跳变。优化后远景帧率回到 58-60fps,近景稳定在 55fps 以上。

踩了一个坑:zoom level 切换时如果用户正在快速双指缩放,可能在一帧内触发两次 overlay 重建导致闪烁。最后加了 100ms 的 debounce 解决。

留存相关的设计思考

上线后发现一个问题:通勤路线走过一次就"点亮"了,之后每天重复走同一条路毫无反馈,打开频次很快下降。

加了两个机制:

  1. 连击倍率:连续 3-4 天走路给 1.5x 奖励,5 天以上 2.0x
  2. 重复经过奖励:已占领格子重复走可获得碎片奖励

上线后日均打开次数从 1.2 次变成了 2.8 次(5 个内测用户+我自己的数据)。

连击有 1 天的容错(graceDays = 1)。这个是被用户教育出来的——最早没有容错,有人周末下雨没出门,周一连击从 7 天归零,给我发了一大段吐槽。加了之后这类反馈消失了。

小结

整个项目技术栈是 Swift + SwiftUI + MapKit + CoreLocation,没用第三方地图 SDK。核心挑战不在于单个技术点有多难,而是各种参数需要在真实环境中反复实测调整——征服阈值、衰减天数、速度过滤值,这些数字背后都是一轮轮的实地步行测试。

项目叫「像素征途」,App Store 可以搜到,目前评分 5 分(虽然评价数还很少)。如果你也在做 LBS 相关的项目,欢迎交流——特别是 GPS 漂移处理这块,纯软件滤波在高楼密集区还是会有漏网之鱼,不知道大家有没有配合加速度计做融合定位的实践经验?

posted @ 2026-05-01 16:00  samexxx  阅读(2)  评论(0)    收藏  举报