简介:这是一款由英国《金融时报》制作的叙事性新闻游戏源码,围绕优步司机的经济处境与零工经济体验展开,玩家需在一周内尝试赚取1000美元,并在真实司机访谈面前做出抉择。资源面向对数据新闻、交互叙事与前端开发感兴趣的开发者与学习者,可用于研究新闻游戏的设计思路与实现方式。压缩包共78个文件,约517KB,以JavaScript为主(28个),辅以HTML页面、JSON配置、YML流程文件及TypeScript、SCSS样式等,涵盖客户端组件、服务端逻辑、测试脚本与构建配置,目录结构清晰。项目基于npm或yarn安装依赖,支持本地开发与CI自动构建、测试和部署,并附带MIT许可说明。目前已有300人学习下载,适合希望借鉴叙事游戏架构、了解新闻交互产品工程化实践的前端开发者参考。
1. 叙事性新闻游戏到底怎么做:从优步司机经济困境到可交互系统
把「优步司机的经济和生活」做成一款叙事性新闻游戏,核心不是复刻一个打车模拟器,而是用系统规则把司机每天面对的真实取舍翻译成玩家能亲手操作的决策。玩家接单、算油费、还车贷、被平台抽成,最后发现跑满十二小时仍然剩不下钱——这种「算完账才懂」的体验,比读一篇长报道更有说服力。这类作品属于严肃游戏与新闻叙事的交叉地带,适合独立开发者、数据新闻团队、交互设计学生,以及想用游戏讲清楚一个经济结构问题的从业者。它不需要 3A 预算,但需要你把真实数据、系统循环和叙事节奏三样东西拧在一起,否则很容易做成一个既不好玩也不可信的半成品。
2. 先立住设计骨架:系统循环、数据来源与叙事节奏怎么定
做这类游戏最容易翻车的地方,是一上来就写代码,结果做到一半发现「经济模型算不平」或者「玩家根本感受不到压力」。我一般会先把三件事定死:核心循环、数据锚点、叙事触发点。这三样决定了后面所有代码和内容的边界。
2.1 核心循环:一天的时间预算怎么变成可玩机制
优步司机的真实处境可以抽象成一个资源分配问题:每天有限的时间、有限的油量、不断变化的订单价格、固定的车辆成本。玩家每个决策都在消耗或换取资源。常见做法是把一天切成若干时间段,每个时间段玩家选择接单、空驶、休息或收车。
这个循环的关键参数是「时间粒度」。粒度太粗,玩家感受不到取舍;太细,操作会变得琐碎。我的经验是每 15 分钟游戏内时间作为一个决策点,一天 24 小时就是 96 个决策点,但实际玩家只会做 20 到 40 次有效选择,其余时间自动推进。下面是一个最小循环的状态定义:
# 司机每日状态:核心资源与约束 driver_state = { "cash": 120.0, # 手头现金,单位元 "fuel": 0.75, # 油箱剩余比例 0-1 "fatigue": 0.0, # 疲劳值 0-1,超过 0.8 触发强制休息 "time_left": 16, # 剩余可运营小时 "car_payment_due": 7, # 距离车贷扣款还有几天 "rating": 4.85, # 平台评分,低于 4.6 减少派单 } # 每个决策点的收益计算 def take_order(state, order): fuel_cost = order["distance"] * 0.6 # 每公里油费约 0.6 元 platform_cut = order["fare"] * 0.25 # 平台抽成 25% net = order["fare"] - platform_cut - fuel_cost state["cash"] += net state["fuel"] -= order["distance"] * 0.08 state["fatigue"] += order["duration"] * 0.02 return state这段代码里每个参数都不是随便写的。油费 0.6 元每公里对应的是普通燃油车在城市工况下的实际开销,平台抽成 25% 是行业里常见的区间中值,疲劳值每小时涨 0.02 意味着连续跑 10 小时才会接近强制休息线。这些数字你可以按自己调研到的数据调整,但必须保证它们之间是自洽的——如果油费太高而订单价格太低,玩家第一天就会破产,游戏直接失去可玩性。
2.2 数据锚点:让经济模型可信的三个来源
新闻游戏和纯虚构游戏最大的区别,是玩家会下意识判断「这数字是真的吗」。所以经济模型不能拍脑袋。我一般从三个地方找锚点:公开的行业报告里的收入区间、司机社区里讨论的真实成本结构、以及平台公开的定价规则说明。
具体做法是建一张参数表,把每个数字的来源和调整空间标清楚:
| 参数 | 基准值 | 可调范围 | 说明 |
|---|---|---|---|
| 平台抽成比例 | 25% | 20%-30% | 不同城市和时段有差异 |
| 每公里油费 | 0.6 元 | 0.45-0.75 | 受车型和油价影响 |
| 日均订单数 | 18 单 | 12-25 | 取决于城市和在线时长 |
| 车辆日折旧 | 45 元 | 30-60 | 按车价和年限折算 |
| 平均时薪 | 22 元 | 15-30 | 扣除成本后的净值 |
这张表的作用不是让你精确复刻现实,而是让你在调整难度时有据可依。比如你想让玩家感受到「跑得越多单位时间赚得越少」的边际递减效应,就可以把疲劳值对收入的影响做成非线性曲线,而不是简单的线性扣减。
2.3 叙事节奏:把新闻信息拆进决策反馈里
叙事性新闻游戏最怕做成「弹窗读文章」。玩家在操作中获得的反馈本身就是叙事。比如玩家连续接了三个短途低价单之后,系统弹出一句「你今天已经跑了 4 小时,收入 68 元,油费花了 31 元」——这句话不需要任何文学修饰,数字本身就是故事。
我通常会把叙事内容分成三层:即时反馈(每单结束后的收支明细)、日终结算(当天总收入、总成本、净收入对比)、周级事件(车贷扣款、车辆维修、平台规则变化)。每层的信息密度不同,但都指向同一个核心问题:这份工作到底能不能养活一个人。
提示:叙事文本不要写成说教。把事实摆出来,让玩家自己算。玩家算出来的结论比你写出来的更有冲击力。
3. 把系统跑起来:最小可玩版本的实现路径
设计骨架定完之后,下一步是做一个能跑通的最小版本。这个版本不需要美术资源,不需要完整剧情,只需要验证核心循环是否成立。我一般用 Python 加一个简单的命令行界面就能跑,等数值调稳了再考虑换引擎做表现层。
3.1 用 Python 搭一个命令行原型
最小版本的目标是:玩家能接单、能看到收支、能感受到一天结束后的结果。下面是一个可运行的骨架:
import random class DriverGame: def __init__(self): self.cash = 120.0 self.fuel = 0.75 self.fatigue = 0.0 self.hours_left = 16 self.total_earned = 0.0 self.total_cost = 0.0 def generate_order(self): # 订单价格和距离呈弱相关,短途单价低但周转快 distance = random.uniform(2, 15) base_fare = 8 + distance * 2.2 surge = random.choice([1.0, 1.0, 1.0, 1.2, 1.5]) fare = round(base_fare * surge, 1) duration = round(distance * 3 + random.uniform(2, 6), 1) return {"distance": distance, "fare": fare, "duration": duration} def take_order(self, order): fuel_cost = order["distance"] * 0.6 platform_cut = order["fare"] * 0.25 net = order["fare"] - platform_cut - fuel_cost self.cash += net self.total_earned += order["fare"] self.total_cost += platform_cut + fuel_cost self.fuel -= order["distance"] * 0.08 self.fatigue += order["duration"] * 0.02 self.hours_left -= order["duration"] / 60 return net def daily_report(self): print(f"今日总收入: {self.total_earned:.1f} 元") print(f"今日总成本: {self.total_cost:.1f} 元") print(f"净收入: {self.total_earned - self.total_cost:.1f} 元") print(f"剩余现金: {self.cash:.1f} 元") print(f"疲劳值: {self.fatigue:.2f}") def run(self): while self.hours_left > 0 and self.fatigue < 0.9: order = self.generate_order() print(f"\n新订单: {order['distance']:.1f} 公里, " f"车费 {order['fare']} 元, 预计 {order['duration']} 分钟") choice = input("接单? (y/n): ") if choice.lower() == "y": net = self.take_order(order) print(f"本单净收入: {net:.1f} 元") else: self.hours_left -= 0.1 # 空驶等待消耗时间 self.daily_report() if __name__ == "__main__": game = DriverGame() game.run()这段代码的逻辑很直白:生成订单、玩家选择、更新状态、循环直到时间耗尽或疲劳过高。关键在generate_order里的参数——base_fare = 8 + distance * 2.2决定了起步价和每公里价格的关系,surge的随机分布决定了高峰溢价的出现频率。你可以把surge改成按时段触发的确定性逻辑,比如早晚高峰必出 1.5 倍,这样玩家就能学会「等高峰再出车」的策略。
3.2 数值调参:让「跑满一天仍然亏钱」变得可信
原型跑通之后,最重要的工作是调参。目标是让玩家在正常操作下,一天结束后的净收入在 50 到 150 元之间波动,偶尔出现亏损。这个区间既能让人感到压力,又不至于让人立刻放弃。
调参的顺序我一般是这样:先固定订单价格分布,再调油费和抽成,最后调疲劳和时间的约束。如果发现玩家总是能轻松赚钱,就提高空驶概率或降低高峰溢价;如果发现玩家总是破产,就降低车辆固定成本或提高短途订单的周转率。
# 调参用的模拟脚本:跑 1000 次一天,看净收入分布 import statistics def simulate_day(): game = DriverGame() while game.hours_left > 0 and game.fatigue < 0.9: order = game.generate_order() # 模拟一个理性玩家:净收入为正就接 fuel_cost = order["distance"] * 0.6 platform_cut = order["fare"] * 0.25 if order["fare"] - platform_cut - fuel_cost > 0: game.take_order(order) else: game.hours_left -= 0.1 return game.total_earned - game.total_cost results = [simulate_day() for _ in range(1000)] print(f"平均净收入: {statistics.mean(results):.1f}") print(f"中位数: {statistics.median(results):.1f}") print(f"亏损天数占比: {sum(1 for r in results if r < 0) / 10:.1f}%")跑完这个脚本,你就能看到当前参数下司机的收入分布。如果亏损天数占比低于 10%,说明压力不够;高于 40%,说明太苛刻。我一般会把目标定在 20% 到 30% 之间,让玩家偶尔翻车,但不会一直翻车。
3.3 从命令行到可交互界面:什么时候该换引擎
命令行原型验证的是数值和循环,不是体验。当你确认核心循环成立之后,就可以考虑换到 Godot 或 Unity 做表现层。换引擎的时机很关键:太早,你会在美术和动画上浪费时间;太晚,命令行版本的交互方式会限制你的叙事设计。
我的判断标准是:当你能用一句话说清楚「玩家在什么情况下会感到焦虑」并且这个焦虑来自数值而不是来自操作不便时,就可以换引擎了。比如「玩家发现车贷还有三天到期但现金不够」这种焦虑,在命令行里也能感受到,那就说明数值设计到位了,可以进入表现层开发。
4. 避坑指南:叙事新闻游戏最容易翻车的五个地方
这类项目看起来简单,实际上坑很密集。我踩过的和见别人踩过的,主要集中在下面五个地方。
4.1 经济模型算不平,玩家第一天就破产
现象:玩家按照正常策略接单,一天跑下来净收入是负数,连续几天后直接放弃。
原因:固定成本(车贷、保险、折旧)设得太高,或者订单价格设得太低,导致边际收益无法覆盖边际成本。常见于直接照搬现实中的极端案例,忽略了游戏需要给玩家留出学习空间。
解决:先跑 1000 次模拟,看净收入分布。如果中位数是负数,就把固定成本砍掉 30% 到 50%,或者把订单均价提高 20%。记住,游戏的可玩性优先于数据的精确性,你可以用「这是普通城市的一天」来解释偏差。
4.2 叙事文本变成说教,玩家直接跳过
现象:玩家不读弹窗里的文字,快速点击继续,完全没接收到你想传达的信息。
原因:叙事文本写成了新闻报道或评论文章,信息密度太高,和玩家的操作节奏脱节。
解决:把叙事拆成短句,绑定在操作反馈上。比如玩家接完一单后,不弹窗,而是在收支明细里加一行「本单耗时 22 分钟,净收入 6.3 元」。玩家自己会算时薪。日终结算时再给一句总结性的话,比如「你今天工作了 11 小时,净收入 87 元,相当于时薪 7.9 元」。不要加任何形容词。
4.3 疲劳系统做成惩罚,玩家觉得被针对
现象:玩家连续接单后触发强制休息,导致当天收入骤降,玩家感到挫败而不是理解。
原因:疲劳值的增长曲线太陡,或者强制休息的惩罚太重,让玩家觉得是系统在阻止自己赚钱,而不是在模拟真实约束。
解决:把疲劳设计成渐进式影响,而不是硬性开关。比如疲劳值超过 0.6 后,每单的净收入打九折;超过 0.8 后,订单生成频率降低。这样玩家会自己权衡「再跑一单值不值」,而不是被系统强行打断。
4.4 平台抽成比例写死,失去讨论空间
现象:玩家发现抽成永远是 25%,觉得这个数字是开发者随便定的,不信任整个模型。
原因:抽成比例没有变化,也没有任何解释,玩家无法判断这个数字是否合理。
解决:让抽成比例随时段和订单类型浮动,比如高峰时段抽成 20%,平峰 28%,长途单抽成 22%。同时在游戏内提供一个「平台规则」页面,用中性语言说明抽成的计算方式。玩家可以不同意,但至少知道你不是瞎写的。
4.5 只做收入侧,忽略支出侧的叙事潜力
现象:游戏只关注司机赚了多少,玩家感受不到「为什么赚了还是没钱」。
原因:支出项目太少或太隐蔽,玩家看不到钱花在哪里。
解决:把支出做成可见的、有节奏的事件。比如每天结束时列出「今日支出:油费 62 元、平台抽成 48 元、车辆折旧 45 元、保险均摊 12 元」。玩家看到这些数字,自然会理解「毛收入和净收入是两回事」。这比任何文字说明都有效。
5. 进阶技巧:用真实数据驱动事件系统与验证方法
当核心循环和数值都稳定之后,你可以开始做真正让这类游戏出彩的部分:用真实数据驱动的事件系统。这不是简单地往游戏里塞新闻,而是让数据本身成为叙事引擎。
5.1 把公开数据变成游戏内事件
我一般会建一个事件表,每个事件绑定一个触发条件和一组数值影响。触发条件可以是游戏内状态(比如现金低于 50 元、疲劳值高于 0.7、连续三天净收入下降),也可以是外部数据(比如油价上涨、平台调整抽成规则)。
# 事件系统:根据游戏状态触发叙事事件 events = [ { "id": "fuel_price_hike", "condition": lambda s: s["days_played"] % 7 == 0, "effect": {"fuel_cost_multiplier": 1.15}, "text": "本周油价上涨 15%,每公里油费相应增加。" }, { "id": "car_repair", "condition": lambda s: s["mileage"] > 500 and random.random() < 0.3, "effect": {"cash": -180}, "text": "车辆需要保养,支出 180 元。" }, { "id": "platform_bonus", "condition": lambda s: s["rating"] > 4.9 and s["weekly_orders"] > 80, "effect": {"cash": 200}, "text": "本周完成 80 单以上且评分优秀,获得平台奖励 200 元。" } ] def check_events(state): for event in events: if event["condition"](state): apply_effect(state, event["effect"]) show_narrative(event["text"])这个事件系统的关键不是事件本身有多复杂,而是每个事件都能让玩家重新评估自己的策略。油价上涨后,玩家会考虑是否少跑长途;车辆维修后,玩家会意识到折旧是真实存在的成本。这些事件不需要频繁触发,一周一次就足够让玩家保持警觉。
5.2 用玩家行为数据验证叙事效果
游戏做出来之后,怎么知道玩家有没有理解你想传达的东西?我一般会埋几个行为指标:平均每日在线时长、接单率、空驶时间占比、日终结算页面的停留时间。如果玩家在结算页面停留时间很短,说明他们不关心收支明细,叙事就没到位;如果接单率一直很高但净收入很低,说明玩家没有学会筛选订单,可能需要调整订单信息的展示方式。
更直接的验证方法是做 A/B 测试:一组玩家看到完整的收支明细,另一组只看到净收入。然后对比两组玩家在后续游戏中的策略差异。如果看到明细的玩家更倾向于减少空驶、避开低价单,那就说明数据展示确实影响了决策。
5.3 一个具体技巧:用「时薪」作为核心反馈指标
在所有可能的反馈指标里,我认为最有效的是「时薪」。不是总收入,不是总单数,而是净收入除以在线时间。这个数字直接回答了「这份工作值不值得做」的问题。
我通常会在日终结算时把时薪放在最显眼的位置,并且和历史数据对比。比如「今日时薪 8.2 元,过去七天平均时薪 11.5 元」。玩家看到这个数字下降,自然会想「是不是今天接的单太便宜了」或者「是不是空驶时间太长了」。这种自我反思比任何教程都有效。
def calculate_hourly_wage(total_net, hours_online): if hours_online == 0: return 0 return total_net / hours_online # 日终结算时展示 hourly = calculate_hourly_wage(net_income, hours_online) print(f"今日时薪: {hourly:.1f} 元") print(f"过去七天平均时薪: {seven_day_avg:.1f} 元") if hourly < seven_day_avg * 0.8: print("今天效率明显低于平均水平,检查一下接单策略。")这个技巧的好处是它不需要任何额外的美术或叙事资源,只需要一个简单的除法。但它的信息量极大,玩家会自己从中读出「这份工作的经济现实」。
我自己做这类项目最大的教训是:不要试图在游戏里给出答案。把数据摆出来,把系统跑起来,让玩家自己算。玩家算出来的结论,比你写一万字都管用。希望帮到你。
本文还有配套的精品资源,点击获取