配置表解析
前言
经过一阵研究,我终于是把关卡、干员配置设置进Excel中。不过目前是存在在本地Excel表,可能以后会进MySQL,虽然不一定。
protobuf设置
我使用proto整合了干员配置、关卡配置和配置总表
syntax = "proto3";
package api;
option go_package="./;api";
// ============ 配置表(由 cmd/configtool 从 Excel 生成,服务端启动时加载) ============
// 干员配置,对应 operator.xlsx 一行
message OperatorCfg {
uint32 id = 1; // 主键
string name = 2; // 名称
int32 cost = 3; // 部署费用
int32 attack = 4; // 每帧攻击伤害
float range = 5; // 攻击半径(格)
int32 hp = 6; // 干员血量(仅展示)
}
// 关卡配置,对应 level.xlsx 一行;波次按 waves 数量重复同一套敌人参数
message LevelCfg {
uint32 id = 1; // 主键
int32 waves = 2; // 波数
int32 enemy_hp = 3; // 每只敌人血量
float enemy_speed = 4; // 移动速度(格/秒)
int32 enemy_count = 5; // 每波敌人数
float wave_interval = 6; // 同波内相邻两只的出场间隔(秒)
}
// 配置总表,configtool 序列化后写入 config/config.pb
message ConfigSet {
repeated OperatorCfg operators = 1;
repeated LevelCfg levels = 2;
}
然后用命令执行一下就i好了。
配置表工具
主要工具就两个readOperators 解析 operator.xlsx和readLevels 解析 level.xlsx然后将解析的内容序列化,再写回。
func main() {
flag.Parse()
operators, err := readOperators(filepath.Join(*dir, "operator.xlsx"))
if err != nil {
fmt.Println("解析干员表失败:", err)
os.Exit(1)
}
levels, err := readLevels(filepath.Join(*dir, "level.xlsx"))
if err != nil {
fmt.Println("解析关卡表失败:", err)
os.Exit(1)
}
data, err := proto.Marshal(&api.ConfigSet{Operators: operators, Levels: levels})
if err != nil {
fmt.Println("序列化配置失败:", err)
os.Exit(1)
}
if err := os.WriteFile(*out, data, 0o644); err != nil {
fmt.Println("写入配置文件失败:", err)
os.Exit(1)
}
fmt.Printf("已生成 %s:干员 %d 条,关卡 %d 条\n", *out, len(operators), len(levels))
}
readOperators函数:
func readOperators(path string) ([]*api.OperatorCfg, error) {
rows, header, err := readSheet(path)
//读取行和第一行表头(字段名小写)到列下标的映射
if err != nil {
return nil, err
}
var list []*api.OperatorCfg
for i := 1; i < len(rows); i++ {
if isEmptyRow(rows[i]) {
continue
}
id, err := intCell(rows[i], header, "id")
if err != nil {
return nil, rowErr(path, i, err)
}
if id <= 0 {
return nil, rowErr(path, i, fmt.Errorf("id 必须大于 0"))
}
op := &api.OperatorCfg{Id: uint32(id), Name: strCell(rows[i], header, "name")}
if op.Cost, err = int32Cell(rows[i], header, "cost"); err != nil {
return nil, rowErr(path, i, err)
}
if op.Attack, err = int32Cell(rows[i], header, "attack"); err != nil {
return nil, rowErr(path, i, err)
}
if op.Range, err = floatCell(rows[i], header, "range"); err != nil {
return nil, rowErr(path, i, err)
}
if op.Hp, err = int32Cell(rows[i], header, "hp"); err != nil {
return nil, rowErr(path, i, err)
}
list = append(list, op)
}
return list, nil
}
其实这是一个盒子,装进了很多小函数。readLevels函数也和上面差不多,就不多讲了。
然后我们讲其他的小函数。
readSheet 读取第一个工作表的全部行,并返回第一行表头(字段名小写)到列下标的映射
func readSheet(path string) ([][]string, map[string]int, error) {
f, err := excelize.OpenFile(path)
if err != nil {
return nil, nil, fmt.Errorf("打开 %s 失败: %w", path, err)
}
defer f.Close()
rows, err := f.GetRows(f.GetSheetName(0))
if err != nil {
return nil, nil, fmt.Errorf("读取 %s 失败: %w", path, err)
}
if len(rows) < 2 {
return nil, nil, fmt.Errorf("%s 没有数据行", path)
}
header := make(map[string]int, len(rows[0]))
for i, name := range rows[0] {
header[strings.ToLower(strings.TrimSpace(name))] = i
}
return rows, header, nil
}
cell 取指定列的值;列不存在时返回空串
func cell(row []string, header map[string]int, name string) string {
i, ok := header[name]
if !ok || i >= len(row) {
return ""
}
return strings.TrimSpace(row[i])
}
剩下几个都是套用cell的函数,不多赘述。
bettle相关文件读取
我在internal的config文件夹,中增加了config用于配置表的加载与查询。
Load 读取并解析配置文件;失败时保留原有数据,调用方决定是否回落默认配置
func Load(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("读取配置文件失败: %w", err)
}
var set api.ConfigSet//配置总表
if err := proto.Unmarshal(data, &set); err != nil {
return fmt.Errorf("解析配置文件失败: %w", err)
}
ops := make(map[uint32]*api.OperatorCfg, len(set.Operators))
for _, op := range set.Operators {
ops[op.Id] = op
}
lvs := make(map[uint32]*api.LevelCfg, len(set.Levels))
for _, lv := range set.Levels {
lvs[lv.Id] = lv
}
mu.Lock()
operators = ops
levels = lvs
mu.Unlock()
return nil
}
GetOperator 按主键查询干员配置;ok=false 表示配置表中没有该 id
func GetOperator(id uint32) (*api.OperatorCfg, bool) {
mu.RLock()
defer mu.RUnlock()
op, ok := operators[id]
return op, ok
}
GetLevel 按主键查询关卡配置;ok=false 表示配置表中没有该 id
func GetLevel(id uint32) (*api.LevelCfg, bool) {
mu.RLock()
defer mu.RUnlock()
lv, ok := levels[id]
return lv, ok
}
Counts 返回已加载的干员数与关卡数,用于启动日志
func Counts() (int, int) {
mu.RLock()
defer mu.RUnlock()
return len(operators), len(levels)
}
来到正片:wavesForLevel 按关卡配置生成波次表;配置缺失时回落默认 waves
func wavesForLevel(levelID uint32) []Wave {
lv, ok := config.GetLevel(levelID)
if !ok || lv.Waves <= 0 {
return waves
}
out := make([]Wave, 0, lv.Waves)
for i := int32(0); i < lv.Waves; i++ {
out = append(out, Wave{
Count: int(lv.EnemyCount),
HP: lv.EnemyHp,
Speed: float64(lv.EnemySpeed),
IntervalSec: float64(lv.WaveInterval),
})
}
return out
}
干员数值来自配置表
if cfg, ok := config.GetOperator(opID); ok {
if cfg.Cost > 0 {
cost = cfg.Cost
}
attack = cfg.Attack
if cfg.Range > 0 {
rng = float64(cfg.Range)
}
hp = cfg.Hp
}
if r.cost < cost {
return codeBadParam, "费用不足", r.cost
}
r.cost -= cost
r.operators = append(r.operators, &Operator{
ID: opID, X: x, Y: y, Direction: dir,
Cost: cost, Attack: attack, Range: rng, HP: hp,
})
最后是启动时加载配置表;失败则回落硬编码默认值,不阻断服务启动
if err := config.Load(configPath); err != nil {
logger.Printf("加载配置表失败,使用默认数值: %v", err)
} else {
ops, lvs := config.Counts()
logger.Printf("配置表加载成功:干员%d 条,关卡%d 条", ops, lvs)
}
我是Strandingwhale,也可以叫我bridges,计算机专业,专注游戏后端与Unreal引擎开发。在git pages和博客园上发布文章,其他网站上的都不是本人。
目前还在上大学,希望能做出很牛逼的项目,然后去到我喜欢的岗位。
本人是冯·诺伊曼派,不是图灵学派。
古法编程,AI提供一点思路,少用AI。
喜欢玩明日方舟
喜欢的游戏还有 死亡搁浅 灵魂摆渡人 异星工厂 等
浙公网安备 33010602011771号