浅谈游戏配置数据在erlang使用

ETS

在之前的手游项目(多服)的时候,我们的配置数据是从MySQL加载到ETS,然后从ETS里面lookup,加载的时候还用到string_to_term这种比较实用的方法,将配置里面的string转为erlang term,这样配置一些复杂的东西的时候,可以简化配置,算是DSL吧。比如任务系统,达成条件配成了,"{lv, 40}",表示这个任务40级的时候就可以完成了,在MySQL或者xsl里面就是字符串,但是在erlang里面就是个tuple,使用的时候,来个匹配就可以。然后有其他需求,爱怎么加就怎么加,比如我们还有强化1次,{equip_upgrade, 1},程序处理匹配就可以了。

使用心得

好处:支持全遍历和选择遍历,写个match_spec就可以了,查询也很快。

不足:每张表都需要写个load data函数,不过这个问题不大,封装下,基本就几行代码功夫,最初没封,都是复制粘帖,累。分布式多节点热更新不方便,要去每个节点触发reload data,有点不方便。

example

load_base_data(DbTable, EtsTable, HandleFun) ->
    L = db_base:select_all(DbTable, "*", []),
    hetsutil:lock(EtsTable),
    try
        hetsutil:truncate(EtsTable),
        lists:foreach(fun(Info) ->
                              RecInfo = HandleFun(Info),
                              hetsutil:insert(EtsTable, RecInfo)
                      end, L)
    catch _:R ->
            ?WARNING_MSG("load ~p failed R:~w Stack: ~p~n",[DbTable, R, erlang:get_stacktrace()])
    after
        hetsutil:unlock(EtsTable)
    end,
    ok.
 
init_base_dungeon() ->
    hmisc:load_base_data(base_dungeon, ?ETS_BASE_DUNGEON, fun handle_base_dungeon_from_db/1).
 
handle_base_dungeon_from_db(DbBase) ->
    RecBase = list_to_tuple([base_dungeon | DbBase]),
    RecBase#base_dungeon{
      reward = lib_reward:convert_common_reward(util:to_term_list(RecBase#base_dungeon.reward))
     }.

代码就不解释,大概意思就那样,不能直接用的。

生成代码

一开始,我觉得这个方式会有性能问题,而且麻烦,当时一开始同事的做法是用php写生成erlang代码的(其实就是字符串),有啥字段写啥,拼起来,很累。后来,我测试了ETS和生成代码的方式,ETS在高并发(瞬间几百或上千)的情况下,会稍微有点性能差,实际上,一般不会遇到这么BT的情况,其他情况两者差不多(印象中4倍),生成代码,编译之后内部估计也是hash,所以性能差不多。

这东西,最初我是比较反感用php写死那些字段,新加一张表,又得这样来一次,万一有100个字段,不是写数据生成的php代码,就得花上点时间。不过当时项目,我是后期加入,没写多少业务代码,没怎么被这php坑。后来,想通了,我就用erlang写了一套,上层只需要声明函数,每个字段是否需要特殊处理(转为term,或者record等),索引返回id等基础功能。这样新加表,写点定义即可,比以前有100倍+的生产力提高。

再后来,我了解有些团队是用VB生成erlang代码的(比如dcy),我面试一个主程,他的做法和dcy家一样,就是简单的get和all_id两个函数,不支持term的表示。有些东西,还是看团队需求吧,像dcy他们家那样确实简单,都不要写配置定义。

使用心得

好处:上线部署不需要外部依赖(如外部的csv,MySQL等),代码即配置。热更方便。 坏处:遍历比较麻烦,特别是像SELECT * FROM base WHERE type=1这种取某一类数据,再或者SELECT * FROM base WHERE lv>=10 ANDALSO lv=< 45,这种区间检索。不过,我后来有个比较赞的办法,就是对lv和type这个字段加人肉索引,这样可以减少遍历。

example

使用代码就不贴,写得比较搓,贴下生成的效果代码。

数据源大概就像下面这样。先简单的解释下,这是抄某个游戏的签到,reward字段在Excel里面是个字符串,格式是{BaseId, Num}。如果把它当成字符串的话,erlang代码就像reward = "{BaseId, Num}"了,但实际上,我们不加双引号,就是成了erlang的term了。其实,这里并不是转为term,而是term的更高一层,record,这样发奖励,我不用再写一层转化。还有个配置,player_card是跟策划约定好去另外一张表找BaseId,因为数量相同,只是每个月BaseId不一样,即data_base_login_reward_player_card:get(hmisctime:get_month())。

这个生成的代码,dcy他们家那种是做不到的,目测他们也不想要这种坑爹的feature。

id  reward  vip_double
1   {3,5000}    1
2   {1,20}  0
3   {26000003,1}    2
4   {3,7500}    0
5   {player_card,2} 3
%% Warning:本文件由data_generate自动生成,请不要手动修改
-module(data_base_login_reward).
 
-export([get/1]).
 
-include("common.hrl").
 
-include("define_reward.hrl").
 
-include("db_base_login_reward.hrl").
 
get(1) ->
    #base_login_reward{id = 1,
               reward = #reward_item{base_id = 3, num = 5000},
               vip_double = 1};
get(2) ->
    #base_login_reward{id = 2,
               reward = #reward_item{base_id = 1, num = 20},
               vip_double = 0};
get(3) ->
    #base_login_reward{id = 3,
               reward = #reward_item{base_id = 26000003, num = 1},
               vip_double = 2};
get(4) ->
    #base_login_reward{id = 4,
               reward = #reward_item{base_id = 3, num = 7500},
               vip_double = 0};
get(5) ->
    #base_login_reward{id = 5,
               reward =
               #reward_item{base_id =
                        data_base_login_reward_player_card:get(hmisctime:get_month()),
                    num = 2},
               vip_double = 3};
get(6) ->
    #base_login_reward{id = 6,
               reward = #reward_item{base_id = 3, num = 10000},
               vip_double = 4};
get(7) ->
    #base_login_reward{id = 7,
               reward = #reward_item{base_id = 1, num = 40},
               vip_double = 0};
get(8) ->
    #base_login_reward{id = 8,
               reward = #reward_item{base_id = 26000003, num = 2},
               vip_double = 5};
get(9) ->
    #base_login_reward{id = 9,
               reward = #reward_item{base_id = 3, num = 15000},
               vip_double = 0};
get(10) ->
    #base_login_reward{id = 10,
               reward =
               #reward_item{base_id =
                        data_base_login_reward_player_card:get(hmisctime:get_month()),
                    num = 3},
               vip_double = 6};
get(11) ->
    #base_login_reward{id = 11,
               reward = #reward_item{base_id = 3, num = 15000},
               vip_double = 7};
get(12) ->
    #base_login_reward{id = 12,
               reward = #reward_item{base_id = 1, num = 60},
               vip_double = 0};
get(13) ->
    #base_login_reward{id = 13,
               reward = #reward_item{base_id = 26000003, num = 3},
               vip_double = 8};
get(14) ->
    #base_login_reward{id = 14,
               reward = #reward_item{base_id = 3, num = 22500},
               vip_double = 0};
get(15) ->
    #base_login_reward{id = 15,
               reward =
               #reward_item{base_id =
                        data_base_login_reward_player_card:get(hmisctime:get_month()),
                    num = 4},
               vip_double = 9};
get(16) ->
    #base_login_reward{id = 16,
               reward = #reward_item{base_id = 3, num = 20000},
               vip_double = 10};
get(17) ->
    #base_login_reward{id = 17,
               reward = #reward_item{base_id = 1, num = 80},
               vip_double = 0};
get(18) ->
    #base_login_reward{id = 18,
               reward = #reward_item{base_id = 26000003, num = 4},
               vip_double = 11};
get(19) ->
    #base_login_reward{id = 19,
               reward = #reward_item{base_id = 3, num = 30000},
               vip_double = 0};
get(20) ->
    #base_login_reward{id = 20,
               reward =
               #reward_item{base_id =
                        data_base_login_reward_player_card:get(hmisctime:get_month()),
                    num = 5},
               vip_double = 12};
get(21) ->
    #base_login_reward{id = 21,
               reward = #reward_item{base_id = 3, num = 40000},
               vip_double = 13};
get(22) ->
    #base_login_reward{id = 22,
               reward = #reward_item{base_id = 1, num = 100},
               vip_double = 0};
get(23) ->
    #base_login_reward{id = 23,
               reward = #reward_item{base_id = 26000003, num = 5},
               vip_double = 14};
get(24) ->
    #base_login_reward{id = 24,
               reward = #reward_item{base_id = 3, num = 40000},
               vip_double = 0};
get(25) ->
    #base_login_reward{id = 25,
               reward =
               #reward_item{base_id =
                        data_base_login_reward_player_card:get(hmisctime:get_month()),
                    num = 7},
               vip_double = 15};
get(26) ->
    #base_login_reward{id = 26,
               reward = #reward_item{base_id = 3, num = 50000},
               vip_double = 15};
get(27) ->
    #base_login_reward{id = 27,
               reward = #reward_item{base_id = 1, num = 120},
               vip_double = 0};
get(28) ->
    #base_login_reward{id = 28,
               reward = #reward_item{base_id = 26000003, num = 5},
               vip_double = 15};
get(29) ->
    #base_login_reward{id = 29,
               reward = #reward_item{base_id = 3, num = 50000},
               vip_double = 0};
get(30) ->
    #base_login_reward{id = 30,
               reward =
               #reward_item{base_id =
                        data_base_login_reward_player_card:get(hmisctime:get_month()),
                    num = 9},
               vip_double = 15};
get(31) ->
    #base_login_reward{id = 31,
               reward = #reward_item{base_id = 3, num = 50000},
               vip_double = 15};
get(Var1) ->
    ?WARNING_MSG("get not find ~p", [{Var1}]), [].

最后还是贴下,我们生成这个的上层代码,生成的代码看似比较碉堡,不过确实写不了多少代码。

base_login_reward() ->
    RewardFun = fun(RewardStr) ->
                        {NewBaseId, NewNum} =
                            case bitstring_to_term(RewardStr) of
                                {player_card, Num} ->
                                    {"data_base_login_reward_player_card:get(hmisctime:get_month())", Num};
                                {BaseId, Num} ->
                                    {BaseId, Num}
                            end,                        
                        lists:concat(["#reward_item{base_id = ", NewBaseId ,
                                      ", num = ", NewNum, "}"])
                end,
    Fields = trans_to_fun(record_info(fields, base_login_reward), [{reward, RewardFun}]),
    [base_login_reward, base_login_reward, ["define_reward.hrl",
                                            "db_base_login_reward.hrl"],
     [default_get_generate_conf(Fields, id)]].

关于人肉索引,其实是这样的。在上层代码,检索出满足的lv之后,拿到lv去换出ids,然后这些就是ids就是满足条件的。type更是简化后的,一个type对应一群ids。

ids_by_lv(1) -> [6, 5, 4, 3, 2, 1];
ids_by_lv(5) -> [12, 11, 10, 9, 8, 7];
ids_by_lv(11) -> [18, 17, 16, 15, 14, 13];
ids_by_lv(15) -> [24, 23, 22, 21, 20, 19];
ids_by_lv(20) -> [29, 28, 27, 26, 25];
ids_by_lv(25) -> [34, 33, 32, 31, 30];
ids_by_lv(Var1) ->
    ?WARNING_MSG("ids_by_lv not find ~p", [{Var1}]), [].
 
all_lv() -> [1, 5, 11, 15, 20, 25].

6 comments — post a comment

cnDenis

以前我写的导表工具就有按指定列生成索引的功能, 呵呵…

Roowe

我这个也有–还支持多列索引,比如type和subtype

zhizhen

问一下,你们这是从csv里面读取吧?如果用erlang,有没有从excel读取比较好的办法?

Roowe

我们是从数据库MySQL读取的(历史原因,以前从MySQL加载到ETS的,同时非游戏服也需要操作,所以统一成一个库)。之前,都是策划使用Navicat将Excel导入到MySQL,最近,对于导表做了自动化,我们用xlsx2csv转csv,地址:https://github.com/dilshod/xlsx2csv ,然后将csv用MySQL load进去就可以了。

Roowe-Test

测试看看有没有邮件回复

Roowe

已经回复你的了

发表评论

电子邮件地址不会被公开。 必填项已用*标注