mcpField
D
dedicated-server

dedicated-server

github.com/jame581开发889163 次安装4 天前更新
代码仓库

关于 dedicated-server

面向 Godot 4.3+ 的专用多人游戏服务器开发与部署技能,涵盖无头导出、服务端游戏流程、配置,以及通过 Docker 和 systemd 部署到 Linux。

主要功能

  1. 01无头服务器导出
  2. 02分离服务端与客户端逻辑
  3. 03大厅与比赛状态模式
  4. 04基于命令行参数的服务器配置
  5. 05通过 Docker 和 systemd 部署

适用场景

  1. 01准备无头多人游戏服务器构建
  2. 02将服务端系统与客户端场景分开
  3. 03实现权威式大厅和比赛流程
  4. 04在 Linux VPS 上部署游戏服务器

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jame581/godotprompter --skill dedicated-server

安装后在 Claude Code 中输入/dedicated-server确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
deployment.md
lobby-management.md
match-flow.md
server-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 导出预设:

  1. 打开 项目 → 导出。
  2. 添加一个 Linux 预设,并将其命名为 Linux Server。
  3. 在 资源 选项卡中,将 导出模式 设置为 导出为专用服务器。这会添加 dedicated_server 功能标签,让二进制文件即使未使用 --headless,也会以无头显示驱动程序和虚拟音频驱动程序启动;同时将 res:// 标记为 剥离视觉资源:视觉资源会导出为占位符(例如,纹理只保留其尺寸)。将服务器需要完整保留的文件夹设置为 保留。
  4. 使用同一选项卡中的 排除筛选器,从服务器 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专用服务器导出检测服务器二进制文件最可靠的方式
(无标签) --headlessCLI 标志不是功能标签——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

评论

支持 Markdown