配置表解析

前言

经过一阵研究,我终于是把关卡、干员配置设置进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)
	}
posted @ 2026-11-07 00:20  布里吉斯  阅读(2)  评论(0)    收藏  举报