地图上全是同色块,走了一周跟没走一样
去年开始做一个把真实行走变成像素格占领的 iOS 项目。最早的版本里,所有走过的路线用同样亮度渲染到地图上,结果连续走了一周之后打开 App,满屏同色方块糊成一片,新路线和三天前的旧路线完全分不清。
当时就觉得这事儿不对——地图应该有"时间层次感",让人一眼看出哪些是今天的成果、哪些是历史足迹。围绕这个问题,前后折腾了热力衰减、Zone 征服判定、GPS 漂移过滤这几块。项目叫「像素征途」,目前 App Store 评分 5 分,体量还很小,但技术上踩的坑值得记一下。
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 而不是 64?因为城市环境里,一个 Zone 总有几个格子物理上走不到——河道中间、围墙内侧、高架桥正下方。我在上海测了 12 个不同类型的街区(老城区窄巷、浦东写字楼群、滨江步道、住宅小区等),一个"体感上全走遍了"的区域,实际点亮率在 88%-93% 之间。取 90%(58/64)作为征服线,64 全亮给"完美"徽章。
这个数字调过好几版。最早设 80%(51 格),结果开车路过一个区域就能触发征服,太廉价了。
热力衰减:让地图"有呼吸感"
回到开头说的问题。加了时间维度的衰减参数之后,效果好了很多:
enum MapHeatRules {
static let routeGlowSoftDecayDay = 4 // 4天后开始变暗
static let routeGlowDecayDay = 7 // 7天后明显变暗
static let zoneEmphasisDecayDay = 14 // 14天后区域高亮消失
static let tileResidualDecayDay = 30 // 30天后只剩残影
static let tileResidualOpacity = 0.12 // 残影透明度
}
思路是:打开地图一眼区分"这周活动区域"和"历史足迹"。过去的路线不会完全消失,保留 12% 透明度维持历史感。
routeGlowDecayDay 最早设的 3 天,结果周末一过周一打开地图就"暗"了大半,心理上挺打击的。改成 7 天后,即使一周没重复走同一条路,领地视觉上也不会给人"在流失"的感觉。
GPS 漂移过滤:速度阈值比卡尔曼滤波管用
城市环境里 GPS 漂移是老问题。人站在楼宇间不动,坐标可能在 10-30 米范围内随机跳。不过滤的话,站着不动也会"解锁"周围格子,破坏游戏性。
方案分两层:
第一层:速度阈值硬过滤。 相邻两个定位点算瞬时速度,低于 0.3m/s 直接丢弃。正常缓步走大约 0.8-1.2m/s,0.3 以下基本是噪声。
第二层:一维卡尔曼滤波做平滑。 经纬度分别维护滤波器,过程噪声和测量噪声的比值根据运动状态动态调——移动中信任 GPS 多,静止时信任预测值多。
说实话卡尔曼在这里效果没想象中大。真正管用的是第一层速度阈值,过滤掉 95% 以上的误解锁。滤波主要让轨迹线视觉上更平滑,美观用的。
渲染性能:2000 格以上的聚合策略
当点亮格子超过 2000 之后,直接用 MapKit 的 MKOverlay 逐格渲染帧率从 60fps 掉到 35fps 左右(iPhone 13 实测),滑动地图能感觉到明显掉帧。
解决思路是按 zoom level 分级渲染。核心逻辑:远景(zoom < 15)只画 zone 级别的色块,一个 zone 对应一个矩形 overlay,数量从几千降到几百;拉近到 zoom ≥ 15 时才切换为单格细节渲染,此时一屏可见格子大约 80-120 个,压力可控。
切换时做了 0.3 秒的 alpha 渐变过渡,避免"啪"一下跳变。优化后同样 2000+ 格子的场景,远景帧率回到 58-60fps,近景稳定在 55fps 以上。
有一个坑:zoom level 切换时如果用户正在快速缩放,可能在一帧内触发两次 overlay 重建。我加了 100ms 的 debounce 才解决这个闪烁问题。
格子循环对留存的影响
连击倍率(连续 3-4 天 1.5x,5 天以上 2.0x)加上已占领格子重复经过可获得碎片奖励,这两个机制上线后,我自己和 5 个内测用户的日均打开次数从 1.2 次变成了 2.8 次。通勤路线不再"走过就没意义了",每天重复走也能刷收益。
连击有一天的容错(graceDays = 1)。这个容错是被用户教育出来的——最早没有,有人周末下雨没出门,周一连击从 7 天归零,给我发了一大段抱怨。加了之后这类反馈消失了。
对了,如果你也在做类似的 LBS 项目——你们处理 GPS 漂移用什么方案?纯软件滤波够用吗,还是得配合加速度计做融合定位?