异步多人游戏参考体系结构

构建异步多人游戏的主干是将游戏状态保存到永久性数据库以及推送通知机制,前者的目的是使游戏会话中的所有玩家都可以在需要进行下一回合时进行检索。

在定义异步多人游戏时,有多个变量可供考虑:

  • 并行支持多个游戏 - 是、否。
  • 截止时间 - 玩家需要在特定的时间(6 小时、12 小时、24 小时等)内完成一个回合,否则游戏将作废。
  • 游戏自定义设置 - 考虑允许玩家使用游戏中的特定设置来创建新的游戏会话。

参考实现详细信息

这一特定参考体系结构展示了一个无服务器的简单井字游戏。 下面是同一用例的不同实现,可以帮助您抢占先机:

以下各部分将介绍适用于所记录的不同实现的常规设计注意事项。

玩家身份验证和标识

此参考体系结构不包含玩家身份验证和深入的玩家标识管理,这两者都留给读者作为练习。

PlayFab 提供多种身份验证形式,从而可以跨多台设备跟踪玩家:

  • 用于访客登录的设备 ID
  • 用户名/密码
  • Google 帐户
  • GameCenter 帐户
  • Facebook 帐户
  • Steam 帐户
  • Kongregate 帐户
  • Twitch 帐户
  • 其他 oAuth 提供程序
  • Android 设备 ID
  • 自定义玩家 ID

扩展到多个数据中心

此参考体系结构仅考虑在后端使用单个区域。 根据所构建的游戏,可能不需要利用流量管理器并部署到多个数据中心。 由于流量管理器用于 HTTP 调用,因此,如果所有资源都位于一个数据中心内且并未分布于全球各地,那么实际成本可能会更低。 此外,拥有多个数据中心也意味着应使用数据库同步机制在不同数据中心之间同步数据库数据。 话虽如此,如果您想要拥有多个数据中心,则应使用流量管理器

分步操作

异步多人游戏的一般流程如下:

  1. 玩家登录到后端。
  2. 从后端检索现有玩家的游戏列表。
  3. 玩家通过一组设置创建新的开放游戏,或者邀请特定对手。
    • 如果玩家创建新的开放游戏,它将添加到游戏会话列表中并等待对手加入。 当玩家创建新的开放游戏时,实际上将发送一个与对手建立连接的请求。 如果有其他与该设置匹配的等待玩家,游戏将直接匹配这两个玩家。
    • 如果玩家邀请另一个特定玩家,这就像是创建一个新的开放游戏,但有一个特定的对手。 在此参考体系结构中,未考虑此功能。
  4. 玩家选择游戏中支持的一个操作,并将其提交以完成游戏回合。
  5. 提交游戏回合后,此游戏会话的永久性数据库记录将显示更新的游戏状态。
  6. 系统向对手玩家发送通知,以便他们开始游戏回合。

数据库架构

将用于存储所需信息的 Azure Database for MySQL 永久性数据库有三个表:player 表,每个玩家对应一个条目,并且扩展到可靠的玩家标识系统;gamesession 表,每个游戏会话对应一个条目;以及gamesession_player 表,每个游戏的每个用户对应一个条目。

Player 表

字段 类型 说明
ID 序列主键 它是此表的主键
CreatedTime 非 Null 时间戳 添加玩家时存储
UpdatedTime 非 Null 时间戳 更新玩家时存储
Name VARCHAR(100) 存储玩家的姓名

游戏会话表

字段 类型 说明
ID 序列主键 此表的主键
GUID VARCHAR(36) 游戏会话的唯一标识符
CreatedTime 非 Null 时间戳 创建游戏会话时存储
UpdatedTime 非 Null 时间戳 存储游戏会话的更新时间
CreatedPlayer_ID 非 Null 无符号 BIGINT 创建游戏会话的玩家
GameStatus 非 Null TINYINT 正在匹配 (0),正在进行 (1) 或已完成 (2)
BoardState 非 Null VARCHAR(200) 序列化板信息(X 和 O 在板中的放置位置)
MovesLeft 非 Null TINYINT 确定游戏的结束时间
CurrentTurnPlayer_ID 无符号的 BIGINT 必须采取行动的玩家
WinningPlayer_ID 无符号的 BIGINT 获胜的玩家
CreatedPlayer_ID 外键 References player(ID)
CurrentTurnPlayer_ID 外键 References player(ID)
WinningPlayer_ID 外键 References player(ID)

游戏会话玩家表

字段 类型 说明
ID 序列主键 它是此表的主键
CreatedTime 非 Null 时间戳 添加玩家时存储
UpdatedTime 非 Null 时间戳 更新玩家时存储
GameSession_ID 非 Null 无符号 BIGINT 外键。 这是游戏会话表中的唯一游戏标识符
Player_ID 非 Null 无符号 BIGINT 外键。 这是玩家表中的唯一玩家标识符
GameSession_ID 外键 References gamesession(ID)
Player_ID 外键 References player(ID)

创建数据库表

请参阅本教程,了解如何获取数据库连接信息、配置防火墙以及与数据库建立连接,方法包括:

之后,运行以下命令以创建数据库:

CREATE DATABASE asyncmpdb;

运行以下命令以开始使用:

USE asyncmpdb;

然后,运行以下命令以创建 3 个表:

CREATE TABLE player (
    ID SERIAL PRIMARY KEY,
    CreatedTime TIMESTAMP NOT NULL,
    UpdatedTime TIMESTAMP NOT NULL,
    Name VARCHAR(100) NOT NULL
);

CREATE TABLE gamesession (
    ID SERIAL PRIMARY KEY,
    GUID VARCHAR(36) NOT NULL,
    CreatedTime TIMESTAMP NOT NULL,
    UpdatedTime TIMESTAMP NOT NULL,
    CreatedPlayer_ID BIGINT UNSIGNED NOT NULL,
    GameStatus TINYINT NOT NULL,
    BoardState VARCHAR(200) NOT NULL,
    MovesLeft TINYINT NOT NULL,
    CurrentTurnPlayer_ID BIGINT UNSIGNED,
    WinningPlayer_ID BIGINT UNSIGNED,
    FOREIGN KEY(CreatedPlayer_ID) REFERENCES player(ID),
    FOREIGN KEY(CurrentTurnPlayer_ID) REFERENCES player(ID),
    FOREIGN KEY(WinningPlayer_ID) REFERENCES player(ID)
);

CREATE TABLE gamesession_player (
    ID SERIAL PRIMARY KEY,
    CreatedTime TIMESTAMP NOT NULL,
    UpdatedTime TIMESTAMP NOT NULL,    
    GameSession_ID BIGINT UNSIGNED NOT NULL,
    Player_ID BIGINT UNSIGNED NOT NULL,
    FOREIGN KEY(GameSession_ID) REFERENCES gamesession(ID),
    FOREIGN KEY(Player_ID) REFERENCES player(ID)
);

使用此命令检查是否创建了 3 个表:

show tables;

创建表后,让我们创建一组存储过程。

此命令用于创建新的游戏会话,将在匹配尝试未返回任何合适的游戏会话时执行:

DELIMITER //
CREATE PROCEDURE `gamesession_INSERT`
   (IN param_guid VARCHAR(36),
    IN param_createdplayer_id BIGINT UNSIGNED,
    IN param_status TINYINT,
    IN param_boardstate VARCHAR(200),
    IN param_movesleft TINYINT,
    IN param_currentturnplayer_id BIGINT UNSIGNED,
    IN param_winningplayer_id BIGINT UNSIGNED)
BEGIN
    INSERT INTO gamesession
       (GUID,
        CreatedTime,
        UpdatedTime,
        CreatedPlayer_ID,
        GameStatus,
        BoardState,
        MovesLeft)
    VALUES 
       (param_guid,
        NOW(),
        NOW(),
        param_createdplayer_id,
        param_status,
        param_boardstate,
        param_movesleft);
    INSERT INTO gamesession_player
       (CreatedTime,
        UpdatedTime,
        GameSession_ID,
        Player_ID)
    VALUES
       (NOW(),
        NOW(),
        LAST_INSERT_ID(),
        param_createdplayer_id);
END //

通知服务

有两个主要服务可用于提交通知:通知中心和 SignalR。 下表展示了这 2 种服务之间的主要区别。 该实现将遵循级联模式,首先尝试 SignalR 通知;如果在一小段时间后未收到已收到通知,则会取消 SignalR 传递,并使用 Azure 通知中心作为回退措施。

SignalR 通知中心
消息源 可以来自游戏服务器(仅限广播或服务器推送)或者其他客户端(类似于聊天的双向传递),取决于具体情况 平台提供程序的现有基础结构(Microsoft、Google、Apple 等)
需要专用服务器 取决于具体情况。 该服务支持 REST API,因此消息源/应用程序可以直接通过 SignalR Service REST API 将消息发布到客户端,而无需服务器。 或者,它正式支持与 SignalR 服务绑定的 Azure Functions,因此它还可以容纳无服务器的情况
不依赖于代码 是。 SignalR 现已拥有基于 C#、JS 的客户端 SDK,还涵盖了 Xamarin、Unity 和 Java SDK。 即将推出适用于 C++ 和 Object-C/Swift 的版本。 REST API 支持任何支持 REST 的语言。 Azure Functions 绑定支持 Azure Function 支持的任何语言 是,您可以使用 REST API 和模板
消息传递 即时(通过 WebSocket) 不保证即时性