关于 dedicated-server
面向 Godot 4.3+ 的专用多人游戏服务器开发与部署技能,涵盖无头导出、服务端游戏流程、配置,以及通过 Docker 和 systemd 部署到 Linux。
主要功能
- 01无头服务器导出
- 02分离服务端与客户端逻辑
- 03大厅与比赛状态模式
- 04基于命令行参数的服务器配置
- 05通过 Docker 和 systemd 部署
适用场景
- 01准备无头多人游戏服务器构建
- 02将服务端系统与客户端场景分开
- 03实现权威式大厅和比赛流程
- 04在 Linux VPS 上部署游戏服务器
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jame581/ godotprompter --skill dedicated-server安装后在 Claude Code 中输入/dedicated-server确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/deployment.mdlobby-management.mdmatch-flow.mdserver-config.md详细介绍
Godot 4.3+ 中的专用服务器
所有示例均适用于 Godot 4.3+,不使用任何已弃用的 API。先展示 GDScript,再展示 C#。
**相关技能:**关于 ENet 设置、RPC 和权限模型,请参阅 multiplayer-basics。关于状态同步和插值,请参阅 multiplayer-sync。
1. 无头导出
专用服务器运行时不需要显示设备、GPU 或音频设备。Godot 通过 --headless 标志和专用导出预设支持此功能。
--headless 标志
在命令行中传入 --headless,即可在运行时停用显示和音频驱动程序:
./my_game.x86_64 --headless
--headless 是一个运行时标志,可用于任何导出的二进制文件。Godot 4 没有单独的服务器平台或服务器导出模板:专用服务器导出使用常规模板,因此二进制文件仍包含其渲染代码(若要生成更小的二进制文件,需要自定义编译模板)。导出时实际更改的是 PCK 和功能标签,见下文。
服务器导出预设
在 Godot 编辑器中,创建一个专用的 Linux 导出预设:
- 打开 项目 → 导出。
- 添加一个 Linux 预设,并将其命名为
Linux Server。 - 在 资源 选项卡中,将 导出模式 设置为 导出为专用服务器。这会添加
dedicated_server功能标签,让二进制文件即使未使用--headless,也会以无头显示驱动程序和虚拟音频驱动程序启动;同时将res://标记为 剥离视觉资源:视觉资源会导出为占位符(例如,纹理只保留其尺寸)。将服务器需要完整保留的文件夹设置为 保留。 - 使用同一选项卡中的 排除筛选器,从服务器 PCK 中完全移除仅客户端使用的文件(音频、仅客户端场景)。
功能标签
使用 OS.has_feature() 在运行时区分服务器和客户端代码。可以在导出预设的 功能 选项卡中的 自定义(逗号分隔) 字段定义自定义的 server 功能,也可以依赖专用服务器导出自动设置的内置 dedicated_server 功能:
# boot.gd — autoload, runs before any scene loads
extends Node
func _ready() -> void:
if OS.has_feature("dedicated_server") or DisplayServer.get_name() == "headless":
# Disable rendering-dependent systems
RenderingServer.set_render_loop_enabled(false)
# Start server logic
ServerBootstrap.start()
else:
# Start client logic
ClientBootstrap.start()
// Boot.cs — autoload, runs before any scene loads.
using Godot;
public partial class Boot : Node
{
public override void _Ready()
{
if (OS.HasFeature("dedicated_server") || DisplayServer.GetName() == "headless")
{
// Disable the render loop. The window is invisible but the engine still ticks.
RenderingServer.SetRenderLoopEnabled(false);
ServerBootstrap.Start();
}
else
{
ClientBootstrap.Start();
}
}
}
**注意:**导出预设配置(自定义功能、排除筛选器、“导出为专用服务器”模式)无论使用哪种语言都相同;预设设置请参阅上面的 GDScript 部分。
功能标签摘要:
| 标签 | 设置方式 | 说明 |
|---|---|---|
dedicated_server | 专用服务器导出 | 检测服务器二进制文件最可靠的方式 |
(无标签) --headless | CLI 标志 | 不是功能标签——OS.has_feature("headless") 为 false;请检查 DisplayServer.get_name() == "headless" |
自定义 server | 导出预设的自定义功能 | 适用于在不同角色之间共用同一个二进制文件 |
2. 服务器架构
不进行渲染的游戏循环
在无头服务器上,_process 和 _physics_process 仍会正常运行,但不会渲染任何内容。将所有服务器逻辑放在 _physics_process 中,以实现确定性的固定频率更新。
GDScript
# server_main.gd — add as autoload named ServerMain
extends Node
## Physics frames per second — matches Project Settings → Physics → Common → Physics Ticks Per Second.
## Override via --tick-rate CLI argument (see Section 5).
var tick_rate: int = 60
## Current server tick counter.
var server_tick: int = 0
func _ready() -> void:
# Guard: this node does nothing on the client.
if not _is_server():
set_process(false)
set_physics_process(false)
return
Engine.physics_ticks_per_second = tick_rate
print("[Server] Started — tick rate: %d Hz" % tick_rate)
func _physics_process(_delta: float) -> void:
server_tick += 1
_tick_game_logic()
func _tick_game_logic() -> void:
# All authoritative game simulation goes here.
# Never reference Camera, CanvasLayer, or any rendering node from this path.
pass
## Returns true when this process is acting as the authoritative server.
func _is_server() -> bool:
# Covers both: dedicated binary and hosted listen-server.
return multiplayer.is_server()
将服务器专用逻辑与客户端分离
组织场景结构,让服务器专用节点位于独立分支,并在客户端上跳过该分支:
# world.gd
extends Node
@onready var server_systems: Node = $ServerSystems # physics, AI, scoring
@onready var client_systems: Node = $ClientSystems # camera, HUD, audio
func _ready() -> void:
# Disable server systems on clients and vice versa.
server_systems.set_process_mode(
PROCESS_MODE_ALWAYS if multiplayer.is_server() else PROCESS_MODE_DISABLED
)
client_systems.set_process_mode(
PROCESS_MODE_DISABLED if multiplayer.is_server() else PROCESS_MODE_ALWAYS
)
Engine.is_editor_hint() + is_server 检查
在必须针对编辑器、服务器和客户端采取不同操作的脚本开头使用这些守卫:
func _ready() -> void:
if Engine.is_editor_hint():
return # Skip all runtime setup in editor preview
if multiplayer.is_server():
_server_init()
else:
_client_init()
func _server_init() -> void:
print("[Server] Initializing authoritative state")
func _client_init() -> void:
print("[Client] Initializing local presentation layer")
C#
// ServerMain.cs — add as autoload named ServerMain
using Godot;
public partial class ServerMain : Node
{
/// <summary>Physics ticks per second. Override via --tick-rate CLI argument.</summary>
public int TickRate { get; set; } = 60;
/// <summary>Current server tick counter.</summary>
public int ServerTick { get; private set; }
public override void _Ready()
{
if (!IsServer())
{
SetProcess(false);
SetPhysicsProcess(false);
return;
}
Engine.PhysicsTicksPerSecond = TickRate;
GD.Print($"[Server] Started — tick rate: {TickRate} Hz");
}
public override void _PhysicsProcess(double delta)
{
ServerTick++;
TickGameLogic();
}
private void TickGameLogic()
{
// All authoritative game simulation goes here.
}
private bool IsServer() => Multiplayer.IsServer();
}
// World.cs
using Godot;
public partial class World : Node
{
[Export] private Node _serverSystems = null!;
[Export] private Node _clientSystems = null!;
public override void _Ready()
{
if (Engine.IsEditorHint()) return;
_serverSystems.ProcessMode = Multiplayer.IsServer()
? ProcessModeEnum.Always
: ProcessModeEnum.Disabled;
_clientSystems.ProcessMode = Multiplayer.IsServer()
? ProcessModeEnum.Disabled
: ProcessModeEnum.Always;
}
}
7. 检查清单
- 将导出预设导出为专用服务器(自动设置
dedicated_server功能) - 从服务器 PCK 中排除仅客户端使用的资源(着色器、音频、高分辨率纹理)
- 启动脚本检查
OS.has_feature("dedicated_server")或DisplayServer.get_name() == "headless",以区分服务器与客户端的启动流程 - 在服务器上调用
RenderingServer.set_render_loop_enabled(false),避免执行任何渲染工作 - 服务器专用节点在客户端上使用
PROCESS_MODE_DISABLED;客户端专用节点在服务器上使用PROCESS_MODE_DISABLED - 对每个有副作用的脚本,在
_ready()顶部添加Engine.is_editor_hint()检查 -
ServerConfig在其他自动加载项的_ready()执行前,从命令行参数解析--port、--max-players、--tick-rate -
server.cfg不存在时,配置文件加载流程能优雅地回退 - 环境变量(
SERVER_PORT、SERVER_MAX_PLAYERS、SERVER_TICK_RATE)在配置文件之后、命令行参数之前应用 - 接受新对等端前,检查
LobbyManager.player_list的大小是否已达到max_players - 大厅已满时,通过 RPC 踢出对等端,然后再关闭连接
- 当
MatchState返回LOBBY时重置准备状态,确保玩家每轮都必须重新确认 -
MatchManager仅在服务器上运行_physics_process(在客户端上调用SetPhysicsProcess(false)) - 倒计时和结果计时器由
_physics_process的 delta 驱动,而非Timer节点(避免依赖场景) - Dockerfile 将二进制文件和
.pck文件复制到镜像中(C# 项目还需复制data_<project>_<platform>_<arch>文件夹) - 首次测试运行前,在 VPS 防火墙中开放 UDP 端口
- systemd 服务设置
Restart=on-failure,以便服务器在崩溃后自动恢复 - 日志输出到 systemd 日志,并可使用
journalctl -u <service> -f查看
3. 大厅系统
按 peer_id 为键的每玩家状态字典是规范做法。服务器持有权威字典;客户端通过 RPC 接收更新。实现 --max-players 命令行上限和准备状态切换 RPC,以便所有对等端确认后再开始。
完整的 GDScript 和 C# 大厅实现(
player_list字典、玩家人数上限、准备状态切换 RPC、广播模式)请参阅 references/lobby-management.md。
4. 对局流程
使用状态机驱动大厅 → 倒计时 → 游戏中 → 结果的流程。服务器具有权威性,客户端只接收状态变更 RPC。常见状态包括:LOBBY、COUNTDOWN、IN_GAME、RESULTS。
包含倒计时/结果计时器以及 GDScript 和 C# 实现的完整状态机,请参阅 references/match-flow.md。
5. 服务器配置
使用 OS.get_cmdline_args() 解析 --port、--max-players、--tick-rate、--log-level 命令行标志。在第一个物理帧之前预先设置 Engine.physics_ticks_per_second;读取和设置其他参数只需使用简单的 match/switch 逻辑。
安全地在启动时读取全部四个标志的 GDScript 和 C# 参数解析辅助工具,请参阅 references/server-config.md。
6. 部署
Linux VPS 搭配 Dockerfile 和 systemd 服务文件是标准的生产部署方式。Dockerfile 会打包导出的二进制文件、PCK 文件;对于 C# 项目,还会打包包含程序集和捆绑 .NET 运行时的 data_* 文件夹。systemd 负责自动重启、通过 journald 轮换日志以及设置资源限制。
Dockerfile、Linux VPS 设置步骤、systemd 单元文件和日志轮换配置请参阅 references/deployment.md。
7. 检查清单
- 已创建无头导出预设,并添加
dedicated_server功能标签 - 当
OS.has_feature("dedicated_server")为 true 时,执行RenderingServer.set_render_loop_enabled(false)(并将音频总线静音) - 服务端自动加载项在初始化系统前检查
multiplayer.is_server() - 启动时解析 CLI 参数:port、max-players、tick-rate、log-level
- 大厅状态仅保存在服务端;客户端通过 RPC 接收更新
- 对局流程状态机具有明确的状态转换,且只有一个事实来源(服务端)
- 生产环境部署使用 systemd 或 Docker 实现自动重启和日志轮转
- 生产预设导出的无头构建已关闭 Export with Debug