MeteorCat / H5游戏服务端(六)

Created Sat, 27 Jan 2024 23:02:57 +0800 Modified Wed, 29 Oct 2025 23:24:59 +0800

H5游戏服务端(六)

对接完客户端协议之后, 基本上暂时可以没什么需要与客户端对接的; 接下来则是对接策划工具了, 更进一步说是 excel 表导出工具, 用来映射成类表格式, 表格的格式:

  • 第一行: 转化的字段名, 该字段只允许英文字母等特殊符号, 用来给静态类设定名称
  • 第二行: 转化的类型, 用来给强类型数据转化声明, 这里可以采用 JSON 类型风格处理

设定下表格风格如下, 这里以设定场景地图 场景配置#SceneTable.csv 方式:

id resource name award
标识 场景 名称 解锁奖励
0 res://Scenes/Login/Main.tscn 登录页面 []
1 res://Scenes/Home/Main.tscn 玩家首页 [“100_1”,“200_2”]
2 res://Scenes/CityLight/Main.tscn 游戏城镇A [“100_1”,“200_2”]
3 res://Scenes/CityMoon/Main.tscn 游戏城镇B [“100_1”,“200_2”]

这里就是比较简单的配置表工具, 预留前三列作为数据配置转化风格, 注意 id 是 int 类型且唯一存在, 最后转化成 Java 结果类:

/**
 * 场景配置
 */
public class SceneTable {

    /**
     * 场景配置 - 数据行
     */
    public static class Row {
        public int id;
        public String resource;
        public String[] award;
    }


    /**
     * 静态数据数据行
     */
    public static final Map<Integer, Row> Rows = new HashMap<>() {{
        put(1, new Row() {{
            id = 1;
            resource = "res://Scenes/Login/Main.tscn";
            award = new String[]{
                    "100_1",
                    "200_2"
            };
        }});
    }};
}

注意: 上面仅仅是渲染 Java 静态样例并不是最终样例, 如果要做到可用至少要实现 C#|Lua|Python 这几种静态实现.

策划对接

策划数据基本上是游戏用到最频繁的数据, 所以值得放在进程堆内存数据之中; 上面的 Java 样例其实是有问题的, 最核心的就是没办法动态获取数据, 要知道策划需求变动是极其频繁的, 所以考验服务不停机能力!

注意: 策划相关功能核心点除了效率之外, 更核心的要求就是策划配置需要 不停机热更新, 热更新策划配置是绝对需要的.

在公司中后期的时候, 策划对于游戏内部要做大量数值版本测试, 甚至会花费几周测试几十个版本确定最后上架版本.

这里可以思考下, 怎么将策划数据表转化成程序?

  • 首先策划配置表是要客户端和服务端共享的, 有跨平台需求(PC|Android|Web|Linux等)
  • 其次不可能让策划手写代码将数据表转化成常量表
  • 最后不可能让策划登录 Linux 复制配置之后重启服务

按照上面需求那么基本上能够满足的就是改成 JSON|XML 之类通用格式加载; 虽然类似 Lua 有热更新可以把静态元表( table )更新, 但是客户端可能采用 C#|Python 之类方案可能没办法直接使用.

这里推荐策划表数据采用 JSON 转化, 相对来数据量少且类型完备.

处理策划配置有好几种方案, 这里说下常见几种:

  • 本地目录加载: 检索目录内部所有转化过JSON策划配置启动时候加载, 运维后台推送信号服务端去本地加载文件
  • 数据库加载: 追加配置运维后台, 提供给策划自己去提交同步配置, 之后推送给游戏服务端进程信号让其同步配置

按照游戏规模和人员配置来选择, 最简单就是本地目录直接加载配置; 直接维护 SVN 版本库并设置 hook 更新的时候直接脚本做提交信号更新.

项目初期一般采用本地目录模式, 没有精力去维护专门策划后台管理, 所以只需要简单 svn+hook 处理下就行了.

这里编写 Python 脚本将 csv 处理成 json 数据( csv2json.py ):

# !/usr/bin/python
# -*- coding: UTF-8 -*-

import pathlib
import sys
import getopt
import json
import csv


# 查询目录所有csv文件
def search_csv_file(path):
    return [f for f in pathlib.Path(path).iterdir() if f.is_file() and f.glob(".csv")]


# 入口方法
def main(argv):
    input_dir = ''
    output_dir = ''
    encoding = 'utf-8'
    delimiter = ','

    try:
        opts, args = getopt.getopt(argv, "hi:o:e:d:", ["input_dir=", "output_dir=", "encode=", "delimiter="])
    except getopt.GetoptError:
        print('csv2json.py -i <input_dir> -o <output_dir> -e <encode:(default utf-8)> -d <delimiter:(default ,)>')
        sys.exit(2)

    for opt, arg in opts:
        if opt == '-h':
            print("csv2json.py -i <input_dir> -o <output_dir> -e <encode:(default utf-8)> -d <delimiter:(default ,)>")
            sys.exit()
        elif opt in ("-i", "--input_dir"):
            input_dir = arg
        elif opt in ("-o", "--output_dir"):
            output_dir = arg
        elif opt in ("-e", "--encode"):
            encoding = arg
        elif opt in ("-d", "--delimiter"):
            delimiter = arg

    if len(input_dir) <= 0 or len(output_dir) <= 0:
        print("找不到输入|输出目录信息")
        sys.exit(1)

    print("输入目录: ", input_dir)
    print("输出目录: ", output_dir)

    files = search_csv_file(input_dir)
    if len(files) <= 0:
        print("找不到策划文件")
        sys.exit()

    # 遍历数据
    csv_list = []
    for file in files:
        pos = file.name.find("#")
        if pos != -1:
            name = file.name[pos + 1:]
            name = name.replace(".csv", "")
            if len(name) > 0:
                name = name + ".json"
                print("加载策划文件: ", name)
                csv_list.append({
                    "file": file,
                    "name": name,
                })

    # 开始写入策划文件
    for table in csv_list:
        with open(table["file"], mode="r", encoding=encoding) as file:
            reader = csv.reader(file, delimiter=delimiter)

            # 获取首行类型数据
            headers = next(reader)
            if len(headers) <= 0:
                break

            # 遍历出类型数据
            names = []
            for header in headers:
                names.append(header)

            if len(names) <= 0:
                print("表格错误", table["name"])
                break

            # 检索出注释, 用于给策划看的内容, 可以跳过
            _descriptions = next(reader)

            # 检索出数据
            data = []
            for row in reader:
                # 没有数据就跳过
                if len(row) <= 0:
                    break

                # 筛选数据
                lines = {}
                for i, name in enumerate(names):
                    try:
                        value = row[i]
                        line = json.loads(value)
                    except IndexError:
                        value = None
                    except json.decoder.JSONDecodeError:
                        if isinstance(value, str):
                            line = str(value)
                        else:
                            line = None
                    lines[name] = line
                data.append(lines)

            # 写入文件
            if len(data) > 0:
                f = pathlib.Path(output_dir)
                f = f.joinpath(table["name"])
                fd = f.open(mode="w+", encoding="utf-8")
                json.dump(data, fd, ensure_ascii=False)


# 入口调用
if __name__ == "__main__":
    main(sys.argv[1:])

注意: csv 默认保存成 utf-8 格式, 不过也可以指定格式进行读取, 最后直接执行目录导出 Excel 数据:

py.exe .\tools\csv2json.py -i .\tables\ -o .\target\

最后会导出成 JSON 文件(SceneTable.json)来提供给客户端和服务端使用:

[
  {
    "id": 0,
    "resource": "res://Scenes/Login/Main.tscn",
    "name": "登录页面",
    "award": []
  },
  {
    "id": 1,
    "resource": "res://Scenes/Home/Main.tscn",
    "name": "玩家首页",
    "award": [
      "100_1",
      "200_2"
    ]
  },
  {
    "id": 2,
    "resource": "res://Scenes/CityLight/Main.tscn",
    "name": "游戏城镇A",
    "award": [
      "100_1",
      "200_2"
    ]
  },
  {
    "id": 3,
    "resource": "res://Scenes/CityMoon/Main.tscn",
    "name": "游戏城镇B",
    "award": [
      "100_1",
      "200_2"
    ]
  }
]

现在就拿到策划生成的源数据, 这里客户端和服务端拿到之后就不需要和策划, 而是直接在游戏需求里应用这些数据.