Skip to content

Repository files navigation

暴躁肉团子

一个运行在浏览器中的俯视角城市破坏游戏原型。玩家操控由寄生体缠绕形成的血肉团块,在城市中滚动、捕食、成长,并用触手和撞击破坏环境。

部署与运行

环境要求:Node.js >=22.13.0

npm install
npm run dev       # 开发:http://localhost:30100/
npm test          # 构建并运行测试
npm run start     # 生产预览:http://localhost:30101/

项目使用 vinext / Vite 构建,产物面向 Cloudflare Workers。正式部署前运行 npm test,再通过 Cloudflare Workers 或 Sites 发布。开发和预览端口启用了 strictPort,端口被占用时会直接报错,而不会静默切换端口。

为什么这样开发

这个项目的目标不是先搭建一套通用游戏引擎,而是尽快验证一个明确的体验:一个不断变重、越来越难控制的肉团,在城市里制造逐步升级的混乱。因此开发顺序始终围绕“玩家能否立刻感受到变化”展开:先完成移动、镜头和吞噬,再加入成长、触手、破坏、车辆与救援单位,最后补足计分、新闻播报和视觉反馈。

技术上选择 React 19、Three.js 和 TypeScript,是有意把职责分开:React 只管理页面入口、菜单与 HUD,Three.js 负责高频更新的场景、物理近似和游戏循环。这样既保留 Web 项目的部署便利,也避免让每帧变化的大量对象进入 React 渲染链。Cloudflare Workers 则让原型可以用接近静态站点的方式发布,不必为一个纯客户端游戏维护独立游戏服务器。

项目大量使用程序化几何体和生成式贴图,而不是从一开始就建立庞大的美术资源管线。这一方面适合快速迭代城市布局、建筑、树木、车辆和碎片,另一方面也促成了统一的粗粝风格。这里追求的不是写实,而是让轮廓、运动和破坏反馈在俯视镜头下足够清楚。

开发中真正困难的部分

下面按问题本身归类,而不是按提交顺序罗列,避免把同一问题的多次调参重复讲述。

1. 成长必须改变手感,而不只是改变模型尺寸

最早、也最关键的问题,是“变大”很容易退化成一次整体缩放。视觉上虽然更大,玩家却感受不到吞噬带来的代价与力量。

最终做法是把成长拆成几个相互关联但独立的量:吞噬会增加外围寄生体,已有寄生体只在上限内变宽,实际体积决定碰撞范围与阴影,质量再影响加速度、最大速度和制动距离。这样成长同时改变外形、占地和操控惯性。经验是:成长系统应优先改变玩家的决策和操作节奏,数值与动画只是表达这种变化的手段。

2. 触手既要像活物,又不能穿过整个城市

触手不是单纯的攻击特效。它会随团子滚动、寻找生物、争抢目标、拉扯碎片,还必须受地面、树木、车辆和建筑约束。若只做端点插值,触手会穿模;若按完整软体物理模拟,浏览器中的成本又过高。

解决方式是采用受约束的分段运动与近似碰撞,并给目标竞争设置规则,例如限制同一人物可被锁定的触手数量。人体碎裂后,碎片转为独立目标,复用同一套回收流程,而不是另写一套“吸收动画”。这说明复杂表现未必需要完整物理模拟;先找出玩家能观察到的约束,再为这些约束建立稳定、可控的近似模型,通常更适合原型。

3. 城市不能只是背景,它必须支持破坏规则

程序化城市一开始看似只是摆放道路和建筑,但玩法很快要求系统回答更多问题:树应该出现在哪里、车辆如何沿道路运动、建筑按什么尺度生成、哪些对象能破坏、哪些能吸收、单位离开城市后如何处理。

项目把生成过程整理为道路与路口、道牙、地块、人行区域、空地与植被、建筑、交通和行人的空间层级。树木只从地块空地中取样,避免落在道路或路口;建筑依据地块面积生成;动态单位则使用统一的城市边界规则。经验是:程序化生成不要只输出“看起来像城市”的画面,还要输出可供玩法查询的空间语义,否则每增加一种互动都会补一层例外。

4. 车辆行为难在异常状态,而不是正常行驶

车辆沿路行驶并不难,真正耗时的是撞击、失控、残骸、起火、救援调度,以及驶出边界后仍被渲染等边缘情况。开发过程中曾分别出现已派遣车辆和普通车辆越界后仍可见、火焰在远处缩成亚像素后闪烁、火焰切片朝向错误等问题。

这些问题最终没有靠逐个隐藏具体对象解决,而是收敛为共用的生命周期与可见性规则:所有车辆统一检查城市边界;残骸、火焰和烟雾拥有明确状态;小于有效显示尺寸的效果停止渲染;面片根据摄像机方向组织。经验是:动态对象一旦存在“正常—受损—销毁—清理”过程,就应尽早把它视作状态机,而不是持续往正常逻辑后面追加条件。

5. 碎片和血迹很有反馈,也最容易拖垮性能

人体、树木、车辆和建筑都能产生碎片。如果所有碎片都永久保留、持续碰撞并投射阴影,场景复杂度会随游戏时间单调增长,短时间试玩正常,长时间运行却必然恶化。

项目给不同对象设定不同的破坏与清理规则:人体碎成有限数量的块并逐步回收;树木碎片会在固定时间继续分裂并最终清理;血迹、动态碎片和临时资源设数量或时间上限。关键经验是:任何“生成效果”的代码都要同时设计“何时停止、何时回收”。生命周期不是后期优化,而是特效功能的一部分。

6. 低清风格不等于把整个页面变模糊

早期最直接的像素化方案会连 HUD 一起损失清晰度,也容易让小型火焰、行人和碎片完全消失。后来改为保持场景的完整渲染分辨率,通过关闭 MSAA、硬边阴影、色阶量化、锐化、噪点和像素化程序贴图形成低质量转播感,同时让 HUD 绕过场景后处理。

这带来一个通用经验:风格化应分层处理。世界画面可以有意粗糙,信息界面必须首先可读;低清效果也要在实际镜头距离下验证,不能只看静态近景。

7. 系统增加后,规则一致性比新内容更难维护

警察、消防员、车辆、行人和碎片陆续加入后,曾出现怪物能攻击普通人却不能攻击救援单位的问题;武器加入弹药后,也需要明确射击、耗尽、装填与恢复之间的状态关系。这类缺陷不是缺少一个特效,而是同类对象没有共享完整规则。

修复方向是让可攻击目标、伤害、武器状态和清理条件尽量经过公共路径,并在增加新单位时检查它属于哪些既有集合。经验是:每加入一种实体,不只要问“它会做什么”,还要问“哪些旧系统应该认识它”。后一个问题决定了系统能否继续扩展。

给后续游戏开发爱好者的经验

  1. 先做可玩的核心循环。 一次完整的移动、捕食、成长和反馈,比十个孤立系统更能暴露方向是否正确。
  2. 把手感写成可调参数。 加速度、制动、锁定数量、碎片寿命等都应有明确含义和边界,避免散落的魔法数字。
  3. 用近似换取稳定。 浏览器游戏不必复刻完整刚体或软体物理;玩家看得见、规则一致、帧率稳定,通常比理论精确更重要。
  4. 生成时保留空间语义。 道路、地块、空地和边界不是装饰标签,而是 AI、碰撞和生成规则的共同依据。
  5. 新增对象时先画生命周期。 创建、激活、受损、失效、回收分别由谁负责;没有清理条件的临时对象迟早会成为性能问题。
  6. 从正常路径之外测试。 高速撞击、目标已销毁、单位越界、镜头拉远、运行数分钟后,往往比正常流程更容易发现系统性缺陷。
  7. 把反馈分层。 轮廓和运动负责第一眼可读性,音画特效负责力度,HUD 负责精确信息;不要让风格效果破坏操作信息。
  8. 小步提交,并记录原因。 功能、修复和平衡调整分开提交,日后才能从历史中判断某条规则是在解决什么问题,而不是只看到最终数字。
  9. 先统一规则,再增加内容。 新敌人、新车辆或新碎片若需要复制旧逻辑,通常意味着应该先抽象公共入口。
  10. 持续做真实时长的运行验证。 构建通过只能证明代码可编译,不能证明三分钟后对象数量、边界状态和画面仍然正确。

当前玩法与操作

  • 吞噬生物会增加寄生体数量、体积和质量;质量越大,操控越迟缓。
  • 触手会感知附近生物,攻击后回收人体碎片。
  • 树木可被触手或团子撞碎,但不会被吸收。
  • 建筑、车辆、树木和人体碎片采用不同的破坏与清理规则。
  • 3 分钟内按破坏金额、连击、伤亡和建筑摧毁数量结算分数。
操作 按键
前后左右移动 W A S D / 方向键
加速 Shift
旋转视角 鼠标水平拖动

移动方向始终相对于当前摄像机视角。手机模式会自动启用左右双摇杆,并使用低开销渲染方案。

项目结构

app/
├── game.tsx       # Three.js 场景、游戏循环和玩法系统
├── globals.css    # HUD、菜单和响应式样式
├── layout.tsx     # 页面元数据与根布局
└── page.tsx       # 游戏入口
worker/
└── index.ts       # Cloudflare Worker 入口

常用命令

npm run dev         # 启动开发服务
npm run build       # 构建 Cloudflare 兼容产物
npm run start       # 启动生产预览
npm test            # 构建并运行项目测试
npm run lint        # 静态检查

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages