做小游戏最头疼的不是写代码,而是"和人一起写代码"。之前我和朋友试过用Unity协作,不是传工程文件传到网盘崩溃,就是两个人改了同一个场景然后互相问"你那边看到的和我一样吗"。直到我翻到这个叫 superpowers 的开源项目,才发现"多人实时一起做游戏"这件事,居然可以做得这么轻。最近也有不少人搜"想要安装superpowers",正好把这段时间折腾出来的经验整理成一篇,从部署到实操踩坑都写清楚。
Superpowers 是一个基于浏览器的开源协作游戏开发环境。服务端跑在 Node.js 上,客户端只需要一个现代浏览器,打开网址就能进入编辑器。它比传统引擎轻太多,没有庞大的下载包,也没有复杂的工程配置,核心卖点就一个:多人实时协作,像 Google Docs 一样做游戏。适合独立游戏小团队、想拉朋友一起玩开发的业余爱好者,也适合用来给编程兴趣班上课——老师开一个项目,学生全部进浏览器动手改,谁在动哪个对象一目了然。
1. 项目概览:Superpowers 到底解决了什么问题
1.1 名字背后的定位
"Superpowers" 这名字听着中二,其实非常直白:给独立开发者一套"超能力"。这个项目的开发者是 Sparklin Labs,定位就是降低游戏开发协作的门槛,让一群不在同一物理位置的人,可以像坐在同一台电脑前一样,同步编辑场景、资源、代码。
整个系统分三层:Launcher(项目管理器)、Server(Node.js 服务端)、Client(浏览器编辑器)。Launcher 负责安装插件、创建项目、启动本地服务;Server 负责承载项目数据、处理多人实时同步;Client 就是你打开浏览器后看到的全套编辑器界面。这三层加在一起,形成了一个"开箱即用的协作游戏工作台"。
我最初被它吸引的原因很朴素:不需要装一个 20GB 的引擎编辑器,不需要配版本控制,不需要担心"我改了这版你改了那版"。Superpowers 在服务端天然维护着一份权威数据,所有人的操作都实时同步到服务端的项目里,这就绕开了传统协作里最折磨人的合并冲突问题。
1.2 和传统游戏引擎的差异化
用传统引擎做过协作开发的人,大概都经历过这些场面:Unity 的 .unity 场景文件本质是 YAML,两个人同时改一个场景,git 冲突一冲突就是几百行;Godot 虽然好一点,但多人实时编辑也做不到。而 Superpowers 把"协作"写进了底层架构,不是插件也不是后加的 feature。
几个关键差异点可以放在一起对比:
| 对比维度 | Unity / Godot 传统工作流 | Superpowers |
|---|---|---|
| 客户端安装 | 需要下载安装完整编辑器 | 任意现代浏览器即可 |
| 多人协作 | 依赖版本控制,冲突是常态 | 内置实时同步,无冲突概念 |
| 场景数据 | 二进制或复杂文本格式,难以合并 | 服务端统一存储,客户端只发操作指令 |
| 开发语言 | C# / GDScript | TypeScript(自带类型提示) |
| 上手成本 | 中等偏高,概念多 | 低,核心抽象只有场景、Actor、行为 |
| 发布平台 | 多平台原生导出 | HTML5 Web 为主 |
不是说 Superpowers 能替代主流引擎,它更像是一个"轻量协作原型机"。我和朋友用它做一个 48 小时 Game Jam 的 Demo,从建项目到能玩,大概一个下午;同样的活儿如果走 Unity + git 协作流程,光处理环境问题可能就要一天。
1.3 技术栈与架构速览
站在使用者的角度,理解它的技术底子有助于排错。Superpowers 的技术栈不算神秘:
- 服务端:Node.js,负责项目存储、WebSocket 通信、资源管理
- 客户端编辑器:浏览器端应用,用 Three.js 做 3D 场景渲染,2D 游戏也跑在同一套渲染管线里
- 脚本系统:TypeScript,运行在浏览器里,通过 Sup 命名空间调用引擎 API
- 实时同步:基于 WebSocket,客户端把操作指令发给服务端,服务端广播给所有连接的协作者
用大白话解释:服务端就像一间公共工作室,里面放着所有人共同维护的场景和数据;浏览器编辑器是每个人手里的答题板,你改一笔,服务端立刻把这一笔同步给其他人。这个架构的好处是,客户端的改动永远基于服务端的最新状态,天然不会有"版本分叉"的问题。
2. 部署安装:把 Superpowers 服务器跑起来
2.1 准备工作与依赖检查
如果你只想在本地单人试用,最低要求其实就一个:装好 Node.js。Superpowers 的服务端是纯 Node 环境,Launcher 也是用 Node 生态跑的。建议直接用 Node.js 的 LTS 版本,太老的版本会和依赖库不兼容,太新的版本偶尔会遇到原生模块编译问题,LTS 最稳。
安装前先检查环境:
node -v npm -v我见过不少人卡在第一步,就是因为机器上根本没有 Node,或者装的是某个 IDE 自带的旧版 Node。这里明确一下:新版 Node.js 安装包是自带 npm 的,装完 Node 就同时有了 npm,不需要另外装。
Linux 服务器上部署时,记得确认系统里有 build-essential 之类的编译工具链,因为部分 npm 依赖包需要从源码编译。macOS 上如果遇到编译报错,多半是没装 Xcode Command Line Tools:
xcode-select --install2.2 安装流程与启动服务
Superpowers 官方推荐的方式是通过 Launcher 来管理项目和插件。Launcher 本身可以作为一个 npm 全局包安装:
npm install -g superpowers-launcher装完之后启动:
superpowers-launcher首次启动会看到 Launcher 的本地管理界面,它会引导你安装"插件"——这里说的插件不是功能小挂件,而是"项目类型"。最常用的插件是 superpowers-game(2D/3D 游戏开发),还有一个是 superpowers-web(网页应用开发)。
如果你不想用 Launcher,直接跑服务端也是可以的,这种方式更适合部署到远程服务器:
git clone https://github.com/superpowers/superpowers.git cd superpowers npm install npm start服务启动后,默认监听本地的 4234 端口,浏览器打开 http://localhost:4234 就能访问。注意,初次启动时服务端会在控制台打印出一串配置信息,其中有一项是你的服务器 ID 和密钥,这些要收好,涉及到后面远程协作时的身份认证。
2.3 创建项目与邀请协作者
在 Launcher 界面里创建一个新项目,流程非常简单:填项目名,选择项目类型(superpowers-game),点击创建。创建完成后,项目会直接在当前浏览器里打开编辑器。
关键操作来了:编辑器的地址栏里除了有本机地址,还有一个"共享链接"。把这个链接发给别人,对方打开后就能加入同一个项目开始协作编辑。这个链接本质上是一个带项目密钥的 URL,服务端通过它来识别"谁有权限访问这个项目"。
我第一次用的时候犯过一个低级错误:把 localhost 链接直接发给了远程的朋友。对方打开当然是连不上本地服务的。正确的做法是,把服务端部署到一台局域网内所有人都能访问的机器上,或者部署到有公网 IP 的云服务器上,然后分享服务器的实际访问地址。
2.4 两种部署方式的选择
我把自己的使用场景分成两种,部署方式完全不同:
场景一:同一局域网内协作比如在办公室、学校机房,朋友之间都连着同一个路由器。这种情况下,在一台电脑上跑服务端,其他人在浏览器里输入http://这台电脑的局域网IP:4234就能加入。查局域网 IP 的方法,Windows 上是ipconfig,macOS/Linux 上是ifconfig或ip addr。
场景二:异地远程协作这种情况需要一台有公网访问能力的服务器。把 Superpowers 服务端装到云服务器上,通过npm start跑起来,然后所有人访问http://服务器IP:4234。这里必须做好两件事:一是安全组/防火墙要放行 4234 端口,二是服务端暴露到公网后,一定要改掉默认的管理员密码或者加上访问控制。Superpowers 默认的访问控制在局域网场景够用,但在公网环境,建议在服务端前面加一层反向代理,同时用 HTTPS 加密访问。
提示:无论哪种部署方式,首次启动服务端时生成的密钥都相当于项目权限凭证。泄露密钥等于把项目编辑权交给了别人,公网部署时务必谨慎保管。
3. 编辑器核心功能拆解
3.1 场景编辑器的操作逻辑
Superpowers 的编辑器界面和主流引擎长得不太一样,但核心概念通用。最上面是一排功能面板的开关,中间是 3D/2D 场景视图,左侧是资源列表,右侧是属性检查器。第一次用可能会觉得"东西怎么这么少",其实这正是它的设计理念:把不必要的复杂度藏起来。
场景里的每一个物体都是一个 Actor(演员)。Actor 可以嵌套,比如一个"玩家"Actor 下面挂着"身体"子 Actor 和"名字标签"子 Actor。选中任意 Actor,右侧会显示它的位置、旋转、缩放等 Transform 属性,直接改数值或者用鼠标在场景视图里拖拽都行。
2D 游戏和 3D 游戏在同一个场景里处理。做 2D 游戏时,你可以把摄像机设为正交投影,所有素材用 Sprite(精灵图)放在 XY 平面上;做 3D 游戏时,把摄像机设为透视投影,导入三维模型,用同样的 Actor 体系管理。这种"一套场景结构通吃 2D/3D"的设计,对原型开发来说非常省心。
3.2 资源管线:贴图、模型、音频
资源管理这块,Superpowers 的做法是把资源文件直接拖进编辑器。支持常见图片格式(PNG、JPG)、音频格式(OGG、WAV),3D 模型方面支持导入通用格式的模型文件。
重点说一下素材导入后的组织方式。编辑器里有一个 Assets 面板,所有导入的文件都会出现在这里。你可以建文件夹分类管理。图片导入后会自动生成一个 Asset 条目,把它拖到场景里就自动创建一个带 Sprite 渲染器的 Actor。
对于像素风游戏,编辑器还自带一个内置的像素画工具,可以直接在编辑器里逐像素画 Sprite,这个对快速画占位素材太有用了。我当时画角色就是用这个内置小工具先画了个 16x16 的方块人,五分钟就进入了玩法验证阶段,一点不夸张。
音频的处理比较基础,就是导入后作为 Sound Asset 使用,没有波形编辑器那种高级功能。HUD 音效、背景音乐都能处理,但如果你要做复杂的音效合成,那还是得用外部工具导完再导进来。
3.3 行为系统与 TypeScript 脚本
Superpowers 的脚本核心是"行为(Behavior)"体系。每个 Actor 可以挂多个行为脚本,每个行为脚本是一个继承自Sup.Behavior的 TypeScript 类。引擎会在特定时机调用生命周期方法:awake()在行为被创建时调用,start()在行为启动时调用,update()每帧调用,onDestroy()在行为销毁时调用。
一个最基础的玩家移动脚本长这样:
class PlayerBehavior extends Sup.Behavior { speed = 5; awake() { // 把主摄像机对准自己 Sup.getActor("Main Camera").setPosition(this.actor.getX(), this.actor.getY(), 10); } update() { // 读取键盘输入,注意这里用的是超级简单的虚拟轴 const horizontal = Sup.Input.getAxis("Horizontal"); const vertical = Sup.Input.getAxis("Vertical"); this.actor.moveX(horizontal * this.speed * Sup.Time.deltaTime); this.actor.moveY(vertical * this.speed * Sup.Time.deltaTime); } } Sup.registerBehavior(PlayerBehavior);这段代码里值得注意的点:Sup.Input.getAxis("Horizontal")读取的是"虚拟轴",轴和键盘按键的绑定是在编辑器的 Input 面板里配置的,不需要在代码里硬编码按键;Sup.Time.deltaTime是帧间隔时间,用它乘以速度才能保证不同帧率下移动速度一致。
行为脚本在编辑器里写好保存后,直接拖拽到 Actor 上就生效。多人协作时,一个人改脚本,另一个人那边会实时收到更新,不需要手动刷新页面,这是我非常喜欢的一点。
3.4 实时协作的真实体验
说了这么多协作,到底实时到什么程度?我实测下来是这样:你在场景里拖动一个 Actor,队友的编辑器视图里同一帧就能看到这个 Actor 跟着动;你新建了一个 Actor,队友的资源列表里立刻出现;你改了脚本的某一行代码,队友的编辑器里那行代码同步变化。
最有趣的是光标系统。每个协作者在编辑器里都有一个带自己名字颜色的光标,你能看到队友正在编辑哪个对象、鼠标在场景里停在哪。这种"看到对方在做什么"的体验,真的把远程协作的距离感拉低了很多。
不过也有需要注意的地方:同一时刻两个人改同一个脚本的同一行,仍然可能出现"后保存的覆盖先保存的"问题,因为代码文件的同步粒度是整个文件。Superpowers 避开了场景合并的红利,但代码文件层面本质还是一个文件共享,所以团队里最好还是养成"改不同脚本文件"的习惯。
4. 实操记录:用 Superpowers 做一个接球小游戏
4.1 需求与场景搭建
纸上谈兵没意思,我实际用 Superpowers 做了一个"接球"小游戏:屏幕上方不断落球,玩家用鼠标左右移动底部的挡板接住球,接住加分,漏掉扣命。需求拆成四块:场景搭建、挡板控制、球的生成与碰撞、计分 UI。
场景搭建就两步。第一步,创建一个空的 2D 项目,删除默认场景里的占位 Actor;第二步,新建一个 Sprite 当作挡板,再新建一个 Sprite 当作球。素材直接用内置像素画笔画的:挡板画一个 64x16 的白色长条,球画一个 16x16 的白色方块,背景在摄像机属性里改成深色。
这里有个关键操作:摄像机要设为正交投影,并且把位置放在场景坐标原点附近朝 Z 轴看。2D 游戏里如果摄像机不对,最常见的现象就是"物体在场景视图里看得见,但运行时屏幕上一片黑"。
4.2 挡板控制脚本
挡板控制用鼠标,核心逻辑是拿到鼠标在场景平面上的世界坐标,让挡板的 X 坐标跟过去。Superpowers 提供了射线检测的 API,把屏幕坐标换算成世界坐标:
class PaddleBehavior extends Sup.Behavior { offsetZ = 10; moveSpeed = 20; update() { // 从摄像机发射一条射线,检测鼠标在场景平面上的位置 const ray = Sup.Camera.getRayFromScreen(Sup.Input.getMousePosition()); const distance = this.offsetZ / ray.direction().z; const hitPoint = ray.origin().add(ray.direction().multiplyScalar(distance)); // 限制挡板移动范围 const x = Sup.Math.clamp(hitPoint.x, -8, 8); this.actor.setX(x); } } Sup.registerBehavior(PaddleBehavior);这里要注意offsetZ的选择。我把摄像机放在 Z=10 的位置朝原点看,挡板平面就在 Z=0,所以射线方向向量除一下 Z 分量,就能算出射线与 Z=0 平面的交点。这个值要根据你自己的摄像机位置调整,没有固定标准。
限制移动范围的Sup.Math.clamp很实用,直接把 X 坐标夹在 -8 到 8 之间,防止挡板移出屏幕边界。这个边界值也取决于你的正交摄像机视野宽度,调试时看着屏幕边缘调就行。
4.3 球的生成与碰撞
球的生成用"定时器 + 预制体"的思路。Superpowers 里虽然没有 Unity 那种经典预制体概念,但你可以把做好的球 Actor 作为模板存起来,每次需要时通过代码复制一份:
class BallSpawnerBehavior extends Sup.Behavior { spawnInterval = 2; private timer = 0; update() { this.timer -= Sup.Time.deltaTime; if (this.timer <= 0) { this.spawnBall(); this.timer = this.spawnInterval; } } spawnBall() { // 复制模板 Actor const ball = Sup.appendScene("Ball Prefab/Prefab", this.actor); ball.setPosition(Sup.Math.random.float(-6, 6), 8, 0); } } Sup.registerBehavior(BallSpawnerBehavior);Sup.appendScene可以把一个子场景作为模板实例化到当前场景里,这就是 Superpowers 的做法。注意生成的球需要往下落,这个逻辑放在球自己的行为脚本里:
class BallBehavior extends Sup.Behavior { fallSpeed = 5; update() { this.actor.moveY(-this.fallSpeed * Sup.Time.deltaTime); } } Sup.registerBehavior(BallBehavior);碰撞检测这里,我用了一个比较取巧的方式:直接比较坐标。挡板的 Y 坐标固定,球的 Y 坐标在每帧更新后,如果球的 X 坐标落在挡板范围内,且球的 Y 坐标低于挡板顶部,就算接住了。这种"区域判定"对于原型来说比物理引擎的碰撞体更快更直观,也免去了配置物理材质的麻烦:
// 挡板行为里增加接球判定 const ballActors = Sup.getActor("Balls").getChildren(); for (const ball of ballActors) { const ballPos = ball.getPosition(); if (ballPos.y < 0 && Math.abs(ballPos.x - this.actor.getX()) < 1) { ball.destroy(); // 加分逻辑在这里触发 } }4.4 计分与 UI
计分 UI 在 Superpowers 里可以直接用文本 Actor 实现。新建一个 Text Actor,在属性里设置字体大小、颜色、内容。然后写一个分数管理器行为,每当接住球就把分数加一,并更新文本 Actor 的内容:
class ScoreBehavior extends Sup.Behavior { private score = 0; addScore() { this.score += 1; Sup.getActor("Score Text").textRenderer.setText(`Score: ${this.score}`); } } Sup.registerBehavior(ScoreBehavior);注意,Superpowers 的 Text Actor 是用textRenderer组件渲染文本的,组件类型是 TextRenderer。如果你创建文本 Actor 时用的是旧的 Text 组件,API 名字可能不一样,建议新建时直接确认右边属性面板里的组件类型。
UI 还有一个隐藏的坑:文本 Actor 默认在 Z=0 平面,可能会被其他物体挡住。解决方法是把文本 Actor 的 Z 坐标稍微调高一点,比如设为 1,保证它永远在最上层显示。
4.5 导出与发布
做完之后如果要分享给别人玩,Superpowers 的发布能力比我想象中纯粹:它主要面向 HTML5 导出。在编辑器里有一个发布功能,把你的项目生成一份可部署的 Web 应用,包含 HTML、JS、资源文件。把整个发布目录放到任意静态服务器上(Nginx、Apache、或者对象存储的静态网站托管)就能运行。
实际发布时我遇到一个问题:默认发布的页面是全屏加载的,加载完成前是一张白屏。如果项目素材多,用户等待体验不好。我的处理办法是,在发布目录里自己加一个 loading 页,等主程序初始化完成后再切换到游戏画面。这个需要手动改一下发布模板,Superpowers 的发布目录结构是开放的,改起来不麻烦。
5. 常见问题与排查技巧实录
5.1 安装与启动类问题
问题一:npm install卡住不动。这个最常见,通常不是代码问题,而是网络源不稳定。解决办法是切换到国内镜像源:
npm config set registry https://registry.npmmirror.com然后重新npm install,速度会有明显改善。
问题二:服务启动后浏览器打不开 4234 端口。先确认服务端控制台有没有报错,然后检查防火墙。Linux 上执行:
firewall-cmd --list-ports如果没有放行 4234,执行firewall-cmd --add-port=4234/tcp --permanent && firewall-cmd --reload。云服务器的话还要检查安全组规则。
问题三:Launcher 打开后看不到项目。检查 Launcher 底部的服务状态,确认本地服务进程是否还活着。有时候是端口被占了,换个端口重启 Launcher 就行。
5.2 协作同步类问题
问题一:队友能看到项目列表,但打不开编辑器。大概率是项目密钥不匹配。检查共享链接里是否包含正确的项目 ID 和密钥,重新生成链接再分享一次。
问题二:一个人改了场景,另一个人没看到变化。先确认两个人连接的是同一个服务端地址。不少人会在本机开一个服务端,同时又在云服务器开一个,然后在两个项目之间混着连,这种问题排查起来特别容易绕晕。建议:项目内统一约定连接哪个服务端地址。
问题三:协作时移动物体出现明显延迟。这是网络质量导致的。Superpowers 的同步走 WebSocket,如果服务端在异地,延迟通常在 50ms 以内还好,超过 100ms 就会觉得"不跟手"。远程协作时建议把服务端放在地理位置上离多数成员较近的机房。
5.3 脚本与资源类问题
问题一:脚本保存后 Actor 没有反应。检查脚本是否注册了行为,也就是有没有调用Sup.registerBehavior(YourBehavior)。漏掉这行,类定义是有了,但引擎根本不知道它是个行为,自然不会被挂载。
问题二:导入的图片显示为紫色或空白。通常是图片格式不受支持,Superpowers 对某些 WebP 或特殊压缩的 PNG 兼容性不好。先用图片处理工具把图片转成标准 PNG 或 JPG 再导入。
问题三:音频在发布后不播放。大概率是浏览器自动播放策略限制。浏览器不允许页面在用户交互之前自动播放有声音的音频。解决办法是,在游戏开始的第一次点击/键盘事件里调用音频播放 API,主动建立"用户已交互"的上下文。
我把这些高频问题整理成一个速查表:
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| 服务起不来 | Node 版本过旧 | 升级到 Node LTS |
| 端口无法访问 | 防火墙/安全组未放行 | 放行 4234 端口 |
| 协作者看不到变化 | 连接了不同服务端 | 统一服务端地址 |
| 编辑器打开空白 | 浏览器缓存了旧资源 | 强制刷新 / 无痕模式重开 |
| 脚本不生效 | 未调用registerBehavior | 补上注册语句 |
| 图片显示异常 | 格式兼容问题 | 转标准 PNG/JPG |
| 音频发布后无声 | 自动播放策略限制 | 在用户交互事件中触发播放 |
5.4 实战避坑经验
除了上面这些能直接对号入座的问题,我再分享几条实操里攒下来的经验。
第一,多人协作时,每个人尽量负责独立的脚本文件和独立的场景层。比如一个人负责挡板和玩家控制,另一个人负责球的生成和碰撞,第三个人负责 UI。虽然 Superpowers 支持实时同步,但"代码文件级别"的冲突仍然存在,分工明确能少很多不必要的互相覆盖。
第二,用模板(Prefab)的时候,一定要先确认模板里的行为脚本已经注册完成。我踩过一次:把球的模板建好了,但球的行为脚本是在复制之后才写的,结果每次生成的球都是"死"的,不会下落。
第三,做 2D 游戏时,把所有游戏对象放在 Z=0 平面附近,UI 放在 Z=1,背景放在 Z=-1。这样能避免大量深度排序的麻烦。Superpowers 没有像某些引擎那样强大的层级渲染排序工具,靠 Z 坐标控制显示顺序是最直接的办法。
第四,发布前一定要在无痕模式里测试一遍。因为编辑器页面有缓存,你看到的行为可能不是最新代码的行为。无痕模式排除缓存因素,能测出真实的发布体验。
第五,定期用 Launcher 备份项目。Superpowers 的项目数据都存在服务端,如果服务端所在的机器硬盘坏了,项目就没了。Launcher 里可以导出项目压缩包,建议每次大版本改动之后导出一份。
6. 使用心得:这个工具适合谁,不适合谁
6.1 真实的使用边界
和成熟引擎相比,Superpowers 的定位非常明确,它不是一个要"替代 Unity"的工具,而是一个"让人能快速一起做原型"的环境。用它做了完整的小游戏之后,我的感受是:如果你的目标是做像素风、2D 的小体量游戏,而且团队成员分散在不同地方,Superpowers 的协作体验是目前我见过最顺畅的,没有之一。从打开浏览器到开始改代码,全程不需要装任何客户端,这对非技术背景的策划和美术来说尤其友好,给一个链接就能看到并参与项目。
但如果你要做的是大型 3D 项目、需要复杂物理模拟、或者要导出到 Switch/PlayStation 这类平台,Superpowers 不合适。它的渲染能力和发布渠道都定位在 Web 层,强行用来做重渲染需求的游戏会撞到天花板。用一句话概括:它是"从想法到可玩原型"的加速器,而不是"从原型到商业发行"的完整工业管线。
6.2 后续还可以怎么玩
Superpowers 的项目类型不止游戏,superpowers-web 插件可以做网页应用协作开发,相当于一个带场景编辑器的 Web IDE。我最近在琢磨的是用它做"交互式教学课件":把物理实验做成一个 3D 场景,学生进浏览器里操作,老师在另一端实时看到每个学生的进展,这种场景非常契合它的协作能力。
另外,Superpowers 的插件系统本身是开放的,官方提供了插件开发指南。如果有能力,你完全可以写一个自己的项目类型插件,扩展它支持的工作流。这个门槛偏高,需要理解服务端和客户端的通信协议,但作为学习 WebSocket 协作架构的案例,读一遍它的源码也很有收获。
最后再分享一个小技巧:如果你和我一样经常在云服务器上部署服务端,记得把启动服务做成一个 systemd 服务,这样服务器重启后服务能自动拉起来,不用每次手动跑命令。配置文件很简单,核心就三行:启动命令、工作目录、日志输出位置。这个小改动能省掉不少"服务器重启后项目失联"的尴尬。