我做了一款「用脚步占领城市」的地图游戏:网格征服判定与热力衰减实现
每次GPS回调要判定64个格子的状态还不能卡这是我做「像素征途」时最头疼的问题。玩法很简单你在现实中走路GPS轨迹实时映射到地图的像素格上走过的格子就被点亮变成你的领地。整张地图被切成 8×8 64 格的区域Zone每个格子对应现实中大约 8-10 米见方的区域。走满一定数量就算征服这个区域。听起来不复杂但 CoreLocation 的回调频率在步行场景下大概每秒 1-2 次每次回调都得算当前坐标落在哪个格子、这个格子所在区域的征服进度有没有变化。如果每次都遍历整个区域的 64 个格子密集区域直接卡成幻灯片——我第一版就是这么干的在测试机上肉眼可见地掉帧。征服判定阈值简单增量更新才是关键征服规则本身很直白一个 8×8 区域里点亮 58 格算「征服」64 格全亮算「完美征服」。58 这个数字调了很久太低没挑战太高让人绝望——毕竟现实中有些角落真的走不到围墙、河道、施工区留 6 格的容错基本刚好。判定函数就是个阈值比较没什么花头staticfuncevaluate(litTiles:Int,conqueredThreshold:Int,// 58perfectThreshold:Int// 64)-ZoneConquestEvaluation{returnZoneConquestEvaluation(isConquered:litTilesconqueredThreshold,isPerfect:litTilesperfectThreshold)} 真正要解决的是 litTiles 这个数怎么高效维护。 我的做法是给每个Zone维护一个 SetString存的是已点亮格子的 tileKey。GPS回调进来时先算坐标对应的 tileKey查一下这个 key 是不是已经在Set里——如果已经存在什么都不做这次回调的成本就是一次哈希查找O(1)。只有当 tileKey 是新的才插入Set、更新 litTiles 计数、触发征服判定。 说白了就是增量更新绝大多数GPS回调你在同一个格子里走动的计算量几乎为零只有跨格子的那一瞬间才触发真正的逻辑。改成这个方案之后密集区域的卡顿彻底消失了。 ## 连击系统的状态设计 光占领不够还得让人每天都想出门。我加了一套连击倍率连续走3天奖励 ×1.55天以上 ×2.0中间允许断1天graceDays1不清零连击。 为什么是1天而不是2天我统计了TestFlight阶段的数据断了2天以上的用户有大概70%后面就不再打开了基本可以认为已经流失。给1天容错是照顾周末宅了一天、周一继续这种正常节奏再多就不值得保护了。 核心状态就三个字段consecutiveDays、lastActiveDate、dailyEffectiveContribution。每天第一次GPS回调时拿当前日期和 lastActiveDate 做差-差1天consecutiveDays1正常连击--差2天在 graceDays 范围内连击不断但当天不算连击增长--差3天以上连击归零 倍率计算很简单连续5天以上给2.03-4天给1.5其余1.0。另外每天有个贡献上限50防止有人开车刷格子——TestFlight阶段真有人试过一天刷了300多个格子数据直接炸了。加了 cap 之后这种情况就不存在了。 ## 热力衰减渲染时实时计算不做后台遍历 这是我个人最喜欢的一个设计。走过的路线不是永远高亮而是随时间衰减。具体的衰减阶梯和对应透明度|时间范围|视觉表现|透明度||---------|---------|-------||0-4天|路线发光最亮|100%||5-7天|光晕开始消退|~65%||8-14天|区域强调消失|~35%||15-30天|逐步淡出|35%→12%线性过渡||30天以上|微弱残留|12%固定|实现上我纠结过两个方案。一开始想做后台定时任务每天凌晨批量遍历所有格子、更新透明度值。但算了一下一个活跃用户可能有几千上万个已点亮格子每天全量遍历一次写入太重了而且用户不打开App的话这些计算全浪费。 最后的方案是每个格子只存 lastVisitDate衰减值在渲染时实时算。地图可见区域内的格子拿当前时间减去 lastVisitDate 得到天数差按上面的阶梯映射到透明度。可见区域外的格子根本不算。这样计算量完全跟屏幕上可见的格子数挂钩跟总数据量无关。 灵感来自GitHub的贡献热力图——绿格子越多越有成就感但一段时间不提交热度就冷下去。放在这个App里长时间不去的区域会慢慢暗下来有一种领地在流失的紧迫感会驱动你去巡视老地盘。 ## 格子循环系统TileLoop做了三版 后来我觉得格子只有亮/灭两个状态太单薄就加了格子循环系统每个格子有自己的等级level、路线阶层roadTier、访问次数、冷却时间。反复经过同一个格子会提升等级高等级格子每天能产出碎片奖励。 这里说一下 roadTier——它表示格子所处的道路类型主干道、支路、小巷分别对应不同的阶层。主干道上的格子因为通行频率天然更高升级更快、产出更多而小巷里的格子虽然升级慢但有稀有度加成。这个设定是为了鼓励用户既走大路也钻小巷不然所有人都沿着主干道来回刷就没意思了。 这套东西做了三版前两版都废了。 第一版太简单就一个 visitCount每次经过1到了某个数升级。问题是用户感受不到升级有什么用。 第二版加了冷却时间cooldownSeconds、每日产出上限todayYieldClaimCap、还有重生机制respawn。规则太多我让三个朋友试用没有一个人搞懂重生收集是什么意思。 第三版砍掉了大部分用户不需要理解的字段只在界面上暴露三个信息当前等级、下次产出还需要几次访问、今天已经领了多少碎片。底层数据结构还是保留了完整的状态但用户看到的是简化后的提示文本。 经验教训就是底层可以复杂但暴露给用户的信息一定要克制。我试过把所有字段都展示在UI上结果用户觉得这是什么表格软件。 ##CoreLocation后台定位的一个坑精度漂移 做LBSApp绕不开后台定位。这里分享一个折腾了我挺久的问题用户静止不动时CoreLocation偶尔会吐出漂移坐标偏差几十甚至上百米。如果不处理用户放着手机不动地图上会莫名其妙地多出几个点亮的格子。 我的处理方式比较粗暴但有效连续两次回调的坐标距离如果小于8米前面提到每个格子大约8-10米见方8米差不多是一个格子的边长就判定为静止状态直接丢弃。如果距离大于阈值但速度为0location.speed0也丢弃——这种大概率是漂移。 这个方案不完美偶尔会漏掉用户很慢速移动的情况。但在误判静止和误判移动之间我选择宁可少记几个格子也不要让用户看到鬼打墙式的假轨迹。 ## 接下来的计划 目前App刚上架不久有用户希望能导入更早年份的照片位置数据现在只支持最近3年这个排在下个版本。探索排行榜也有 bug 在修。 下篇准备写GPS轨迹平滑的三种方案对比卡尔曼滤波、滑动窗口均值、贝塞尔插值在像素征途里实测下来效果差异挺大的。你们在做LBS类应用时GPS漂移是怎么处理的评论区聊聊特别想知道有没有人试过纯加速度计辅助修正的方案。