这事儿的起因
去年年底我开始琢磨一个问题:市面上的运动记录 App 全在卷步数、卡路里、配速,但说实话,这些数字对我这种「不讨厌走路但也没多大动力出门」的人来说,激励效果约等于零。
我想要的是那种——打开地图一看,「这片区域还是灰的,今天去把它点亮」的感觉。有点像小时候在纸上涂格子,涂满一整页的满足感。
于是就有了「像素征途」这个项目。核心概念很简单:你走过的地方会变成你的像素领地,一格一格地占领城市地图。
区域征服的规则设计
游戏化的东西如果规则太简单会无聊,太复杂又没人愿意学。我在这上面反复改了好几版。
最终落地的方案是把地图切成 8×8 的区域网格(zone),每个 zone 有 64 个 tile。走过一个 tile 就算点亮,但要「征服」整个 zone,得点亮 58 个以上;全部 64 个都点亮,算「完美征服」。
enum TerritoryRules {
static let zoneSideCount = 8
static let zonePerfectTileThreshold = zoneSideCount * zoneSideCount // 64
static let zoneConqueredTileThreshold = 58
}
enum ZoneConquestRules {
static func evaluate(
litTiles: Int,
conqueredThreshold: Int,
perfectThreshold: Int
) -> ZoneConquestEvaluation {
let normalized = max(0, litTiles)
return ZoneConquestEvaluation(
isConquered: normalized >= max(0, conqueredThreshold),
isPerfect: normalized >= max(max(0, conqueredThreshold), perfectThreshold)
)
}
}
为什么是 58 而不是 64?因为实际走路时,zone 边角的 tile 经常在河边、围墙后面、或者根本没有路的地方。如果要求 100% 才算征服,大部分人会在 90% 的时候放弃,体验很差。58/64 大概是 90.6%,留了一点容错空间,但又不会让人觉得太水。
说实话这个数字我试了 50、55、58、60 四个版本,最后选 58 是因为我自己在上海测试时,大多数街区走完正常路线刚好能到 58-62 个 tile 的范围,体感最舒服。
连击系统:让人「再多走一天」
光占领地图还不够。我发现测试期间自己都会出现「今天懒得出门」的情况,所以加了连击倍率:
enum RouteScoreRules {
static let dailyContributionCap = 50
static let graceDays = 1
static func multiplier(for consecutiveDays: Int) -> Double {
switch max(0, consecutiveDays) {
case 5...: return 2.0
case 3...4: return 1.5
default: return 1.0
}
}
}
连续走 3 天,分数 ×1.5;5 天以上,×2.0。每天贡献有上限(50),防止有人开着车刷。
graceDays = 1 是个我比较满意的设计——断了一天不会立刻清零连击,给用户一个「补救」的机会。这个参数最早是 0,结果我自己周末睡了个懒觉连击就断了,当场改成 1。
热力衰减:地图不是死的
一个纯粹的「点亮就永远亮」的地图其实没有长期生命力,因为几个月后到处都是亮的,没有新鲜感了。
我加了热力衰减机制——最近走过的路线会有明显的发光效果,4 天后开始减弱,7 天后路线光芒消失,14 天后区域强调也淡掉,30 天后 tile 只剩 12% 的残留透明度。
routeGlowSoftDecayDay = 4
routeGlowDecayDay = 7
zoneEmphasisDecayDay = 14
tileResidualDecayDay = 30
tileResidualOpacity = 0.12
这意味着你的领地是「活的」,需要偶尔回去走走来保持活跃度。类似于游戏里「领地维护」的概念,但不会逼你每天必须去,因为 tile 本身的点亮记录是永久保存的,衰减的只是视觉上的热力效果。
这个设计是参考了 GitHub 的贡献热力图。你的 commit 记录永远在,但那个绿色格子的视觉冲击力会随着时间推移被新的活动覆盖。
每日任务和 Tile 循环
为了给每天的出行一个明确的短期目标,我设计了四类每日任务:
unlock— 今天点亮 N 个新 tiledistance— 今天走够 N 米duration— 今天探索时长 N 分钟momentum— 保持连击天数
完成任务奖励「碎片」(fragments),碎片是游戏内的通用货币,可以用来解锁地图主题和视觉风格。
对了,每个 tile 还有自己的循环系统——反复经过同一个格子会提升它的等级(从「侦察」开始),有路线阶层(road tier)和冷却时间。这套东西说白了就是把 RPG 里「刷怪升级」的循环搬到了真实地图上。
关于定位落点的处理
做地图类 App 绕不开的问题是:用户第一次打开时定位还没拿到怎么办。
我的处理方式是根据 Locale.current.region 给一个合理的默认中心点。中国用户落在上海陆家嘴,日本用户落在东京站,美国用户落在旧金山……总之避开 (0, 0) 这个「Null Island」。GPS 信号拿到后再平滑切过去。
这个细节很小,但如果不处理,用户第一次打开会看到一个非洲几内亚湾的空白地图,体验直接崩掉。
每日探索摘要
每天结束后会生成一个 DailyExplorationDigest,汇总当天新点亮的 tile 数、行走距离、探索时长、主要区域等数据,还会按 tile 数量给一个 0-3 的强度等级(无活动 / 轻度 / 中度 / 重度)。
这个摘要主要用在两个地方:一是日历视图里像 GitHub 热力图那样展示历史活跃度,二是生成分享卡片时提供数据。
有用户(ID: answer丨)评价说「和世界迷雾一样好玩,解锁成就」,这个对比挺准确的。世界迷雾是「揭开迷雾」,我这边是「像素占领」,视觉语言不同,但探索驱动力是一样的。
目前的状态和一些遗憾
App Store 评分 5 分,但说实话样本量还很小。有用户提到想导入 3 年前的照片位置数据,目前我限制了只支持最近 3 年,主要是因为更早的照片 EXIF 位置数据质量参差不齐,直接导入容易产生大量噪点 tile。这个后续可能会做一个「手动确认」模式来解决。
还有用户说 App 图标看着像游戏图标。其实就是游戏图标……但我理解他的意思是在工具类 App 的语境下显得有点违和,这个确实要优化。
探索排行榜功能目前也有 bug,有用户反馈无法参与,还在排查。
我从这个项目学到的一件事
做游戏化产品最难的不是技术实现,是参数调优。58 还是 60 tile 才算征服、连击中断宽限 0 天还是 1 天、热力衰减 7 天还是 14 天——每一个数字背后都是「用户到底会不会因此感到挫败还是满足」的判断。
这些参数没有公式可以算,只能自己用、找人试、看反馈、再改。我觉得这可能是独立开发里最费时间但也最有意思的部分。