游戏的业务开发主要是在《客户端——通信层——业务层——数据层——数据库》中进行。
知识点一:sleep()和wait()的区别
sleep(100L)是占用cpu,线程休眠100毫秒,其他进程不能再占用cpu资源,wait(100L)是进入等待池中等待,交出cpu等系统资源供其他进程使用,在这100毫秒中,该线程可以被其他线程notify,但不同的是其他在等待池中的线程不被notify不会出来,但这个线程在等待100毫秒后会自动进入就绪队列等待系统分配资源,换句话说,sleep(100)在100毫秒后肯定会运行,但wait在100毫秒后还有等待os调用分配资源,所以wait100的停止运行时间是不确定的,但至少是100毫秒。
知识点二:全职猎人游戏中playBean和PlayCache的区别。 playBean是在线玩家缓存,playerCache是离线玩家缓存。
playBean是玩家登陆时才加载,谁登陆加载谁。playBean是玩家的详细介绍,包含玩家所有的信息,所以playBean比较大,全部加载到内存中,不但会导致启动时比较慢,还会占用大量的内存。因为playBean是登录后加载,所以playBean一般存放的是在线玩家的数据。玩家登陆把玩家信息写入到内存中,若中间玩家退出登录,玩家数据依然存在内存,要经过一段时间才会被GC回收器回收。
playCache是服务器启动时加载,服务器启动时会把全服的玩家数据从数据库加载到内存中。 playCache是玩家的简要介绍,存储的玩家信息不全面。所以每个玩家的playCache不太大,加载到内存中不会占用太多的内存空间, 服务器启动时把所有在线和不在线的玩家信息加载到内存中,作为离线玩家数据缓存。
(1)如果要拿一个在线玩家的信息,直接用PlayerBean,因为用户少,而且在线。 同时玩家需要拿自己的信息用playBean,因为拿自己的信息说明你已经登录在线。
(2)玩家登陆在线,需要通过自己拿另外一个不在线玩家信息。也可以用PlayBean。有个懒加载机制:1、先从内存中拿,如果另外一个玩家退出不太久,说明其数据还存放在内存中,没有被回收。2、内存中没有的话,再从数据库中拿。
(3)如果要拿大量的不在线的玩家数据,则用playerCache。playCache就是用于存放大量的不在线玩家的简要数据。
知识点三:游戏服业务类service如何调用脚本handle类中的方法。
public static void addTongMercenary(long uuid, int heroId) {
ScriptHandler.execute(TONG_SCRIPT, "addTongMercenary", uuid, heroId);
}
上述业务调用tongscript脚本中的addTongMercenary()方法。
具体的实现原理是:private static Map<String, IScriptHandler> _datatable = new ConcurrentHashMap<>();
loadallscript()加载脚本的时候会定义一个集合_datatable,把所有的脚本经过加载,编译成二进制class文件,生成对象存放在集合中。key 为handlename,value为handle对象。调用脚本的时候传入handlename和method方法,通过脚本名在集合中找到对应的handle对象,然后用类通过方法名调用自己方法的函数 handler.execute(method, args);来执行方法。
知识点四:数据层到数据库的自动回写机制原理:由于数据层都是一些持久化的javaBean,全职游戏有封装好的数据回写机制定时把数据回写到数据库中。Mysql数据库中存储数据采用的是泛文本的方式类似于moddb数据库,这样的好处是在JavaBean中增加或者修改成员变量,不用修改数据库中对应表的字段。数据以key-value文本(如下图)的形式存在数据库中,启动从数据库中加载数据,把文本解析成Map,可以“=”号为分割字符,左边为key,右边为value,存放在Map中。
知识点五:通信层原理:全职项目主要有两种常用的通信方式。
一种是协议下发数据:客户端和服务器定好协议号,发送数据的格式,二进制的方式下发数据。 缺点:不利于修改。发送数据和解析数据的格式必须一样。
二种是封装好的JsonUI对象来发送数据。服务器定好发送的UI名和对应的Json格式的字段。客户端通过UI名把json数据填充到对应的UI页面中
重点:jsonUI的格式发送数据和用通信协议发二进制原理上是一样的,其实就是把数据封装成UIname+json格式的writer的格式,然后用固定的协议发送给客户端。
不管是用jsonUI,json格式或者用二进制来发数据,其最终都是用通信协议发送 编码后成字节码文件。只不过json格式的数据发送后,客户端接收到字节码文件会将其变成字符串,然后再封装成json对象。
知识点六:数据从配置表中加载到模板类
游戏中的数据主要分为两部分,一部分是NPC角色、场景规则的配置数据,另一部分是玩家的游戏数据。玩家的游戏数据从数据库中加载,场景规则的数据因为所有玩家都是一样的,所以通过启动时加载配置表加载到模板类上。每一张配置表对应一个模板类,表里的字段名对应模板类的成员变量。通过对表进行解析,读取表的每一行数据,以”set+字段名”
调用对应模板类的方法加载数据。
对配置表的解析程序:
public static <T> List<T> parserExlAuto(File file, Class<T> obj, int index,int colType, int fieldRow, int firstParserRow) throws Exception { InputStream is = new FileInputStream(file); Workbook wb = Workbook.getWorkbook(is); Sheet sheet = wb.getSheet(index); List<T> list = new ArrayList<T>(); try { //int fieldRow = 2; //int firstParserRow = 3; Cell[] firstRow = sheet.getRow(fieldRow); List<String> colname = new ArrayList<String>(); for (int i = 0; i < firstRow.length; i++) { String col = firstRow[i].getContents(); if (StringUtils.isNotBlank(col)) { col=col.substring(0, 1).toLowerCase()+col.substring(1, col.length()); colname.add(col); } else break; } firstRow = sheet.getRow(colType); List<Class> coltype = new ArrayList<Class>(); for (int i = 0; i < firstRow.length; i++) { String col = firstRow[i].getContents(); if (StringUtils.isNotBlank(col)) { if(col.equals("null")) coltype.add(null); else if (col.equals("string")) coltype.add(String.class); else if (col.equals("int")) coltype.add(int.class); else if (col.equals("bol")) coltype.add(boolean.class); } else coltype.add(null); } Method[] methods = new Method[colname.size()]; // Class[] types = new Class[coltype.size()]; // coltype.toArray(types); for (int r = firstParserRow; r < sheet.getRows(); r++) { T o = obj.newInstance(); Cell[] row = sheet.getRow(r); if (row.length == 0 || StringUtils.isEmpty(row[0].getContents())) break; for (int c = 0; c < colname.size(); c++) { try { String value; if (c >= row.length) value = ""; else value = row[c].getContents(); if (StringUtils.isEmpty(value)) continue; if (colname.get(c).equals("null")) continue; if(coltype.get(c)==null) continue; if (methods[c] == null) { String te = colname.get(c); if (coltype.get(c) == boolean.class) { if (te.startsWith("is")) te = te.substring(2); } methods[c] = obj.getMethod(StringUtils.toSetMethod(te), coltype.get(c)); } if (coltype.get(c) == int.class) { methods[c].invoke(o, Integer.parseInt(value)); } else if (coltype.get(c) == String.class) { methods[c].invoke(o, value); } else if (coltype.get(c) == boolean.class) { methods[c].invoke(o, value.equals("0") ? false : true); } } catch (Exception e) { e.printStackTrace(); System.out.println("row : " + (r + 1) + ", col:" + colname.get(c) + " error " + e.getMessage()); } } list.add(o); } } finally { } return list; }
模板类程序:
对配置表中字符串的解析:
任务一:公会第一期
公会主要包括玩家的公会信息以及工会本身的信息,玩家的公会信息存放在tongInfo类中,tongInfo绑定在PlayerBean上,把tongInfo编码成字符串的信息通过PlayerBean回写写入到数据库中。
公会本身的信息绑定在tongBean身上,通过tongBean的自动回写机制回写到数据库中。
(1)关于成员申请
公会tongBean身上存放公会申请容器TongApplyMemberContainer,公会申请容器里面包括公会Id、公会申请成员map。map由uuid为key,TongApplyMember为value。TongApplyMember包括成员变量uuid和申请时间applytime。
玩家PlayerBean身上存放玩家公会信息tongInfo,tongInfo身上有玩家申请了哪些工会的列表List<Long> applyTongIds。
对应关系:玩家可以同时申请三个公会,记录在applyTongIds列表中,对应的三个公会的公会申请容器中都有这个玩家的申请信息(uuid和applytime)。
(2)tongService公会业务缓存数据的容器
GConcurrentHashMap<Long, TongBean> tongId2Tong 集合存放所有的公会。
服务器启动时加载:由tongId得到tongBean,服务器启动时,从数据库加载所有的公会对象,存放在此Map中。这是工会的主要数据存放容器。
GConcurrentHashMap<String, Long> tongName2TongId
服务器启动时加载:由公会名tongName得到tongId,有了tongId则可以拿到tongBean。
GConcurrentHashMap<Long, List<TongBean>> uuid2Tongs
每一个玩家都对应一个他的可申请公会列表,这个列表是从tongId2Tong所有公会集合中遍历筛选掉<不可申请公会、已解散公会和玩家等级达不到申请的公会>得到的。
这个列表用于玩家没有公会时,打开公会界面 跳到公会申请列表时显示页面数据。同样的,其他公会列表和公会申请列表的不同之处在于其他公会列表里面还包含有玩家不可申请公会,所以其他公会列表只要从所有公会集合中筛选掉已解散公会即可得到。
GConcurrentHashMap<Long, Map<Long, Long>> uuid2TongsIndex
用于得到公会列表排序下标。 Map<uuid,Map<tongId,index>> 玩家对应的可申请公会列表里每个公会对应一个公会下标。
Collections.sort(tongList, TongComparators);
公会的可申请列表和其他公会列表用比较器进行排序后再显示。
(3)关于公会日志
公会tongBean身上有公会日志容器TongLogContainer logs。公会日志容器和公会申请容器一样,容器内存放tongId和Map,Map<logId,tongLog>,logId是原子类自动递增的,tongLog则包含logId、当前时间(毫秒)、日志日期,时间,内容和标题等信息。
日志按时间先后用比较器来排序。
日志编码成字符串的形式存放在tongBean对应的数据库表tong里,服务器启动时调用解码方法加载到内存中。
(4)tongBean身上绑定着tongPost公告、tongNote宣言,viceHosts副会长列表(list<uuid>)、elders精英列表以及公会货币、公会副本、公会建筑。
playerBean的tonginfo身上绑定玩家对应的tongId、公会代币tongToken和荣誉tongHonor,玩家上一次公会签到的时间lastSignInTime、离开公会时间leaveTongTime、已申请的公会列表、玩家的公会副本信息。
注意:每次对玩家的公会数据像公会代币 或者公会的数据像公会人数、宣言、资金等数据进行修改后,都必须要刷一遍对应显示的UI或者刷一遍下发数据的通信协议。
(5)国际化的语言包code_zh_CN.properties
以key-value 的形式存在文本中,服务器启动时对文本进行解析,将其存放在Map中。 程序中使用的时候传入key,则可以在map中找到对应的中文文本。
国际化可以保证程序中不出现中文,这样程序可以移植到其他国家语言,也可以运行,只需要修改语言包里的中文就行。
(6)公会第一期主要包含以下模块:
(玩家无工会)公会申请列表:所有公会集合里去掉已解散和不可加入的公会,如果有等级限制,还需要去除高于玩家等级才能加入的公会。
注意:本游戏是 每个玩家都有一个对应的公会申请列表
公会申请列表五种状态:立即加入、立即申请、已申请、立即加入公会满员、立即申请公会满员。
(玩家有工会)公会主界面: 主界面有公会名、公会公告、公会人数、公会建筑的入口、公会日志、公会管理、公会成员、其他公会的入口。
公会日志:按照时间顺序显示公会日志。 玩家进行公会操作时,生成日志放入公会容器中,存入数据库。从公会日志容器中拿日志进行显示时,用比较器按时间来排序。
公会管理:1、修改公会的属性,修改公会的徽章、宣言、公会的限制条件(任何人都可以加入、需要申请才能加入、任何人都不能加入)。 公会主界面修改公会的公告。
客户端协议上传修改后的内容,服务器端设置新的内容绑在JavaBean上,由JavaBean的自动回写机制修改到数据库中。2、提供公会建筑升级页面的入口。 目前只做了公会建设(签到)和公会商店的升级。 公会建设的等级就是公会的等级,公会建设升级,公会的人数上限增加,公会其他建筑上限增加。
公会成员:1、公会成员:显示公会所有的成员列表,列表也是用比较器来排序。VIP等级高的、战队等级高的、战队念力高的排在前面。 2、公会审核:公会会长或者公会副会长审核 申请加入公会的玩家,“接受或者拒绝”。
其他公会:和公会申请列表一样,只不过这里显示任何人都不可以加入的公会,从所有公会中剔除掉已解散的公会即可。
任务二:公会第二期
公会第二期主要包括公会商店、公会建设(祈祷)和公会建筑升级。
公会商店:公会商店是绑在玩家身上的。每个玩家都有属于自己的商店,这样某一样东西对于当前玩家显示销磬,其他玩家还可以购买。 玩家身上有普通商店、天空竞技场PVP商店、夙敌来袭商店、公会固定商店和普通商店。
公会商店是根据公会商店建筑的等级和类型(5为固定商店 7为随机商店)拿到从配置表里加载到的模板。固定商店物品不变,每天8点刷新。随机商店通过物品池随机物品,每天8,12,16,20点刷新。
公会建设:公会建设就是每天进行公会祈祷。通过祈祷获得 公会升级需要的公会资金、公会资材 以及 公会商店买物品的公会代币、 公会成员列表显示的公会荣誉。公会荣誉代表对公会做出的贡献度,只是起到展示的作用。
公会四大货币:公会资金、公会资材(绑在公会身上,公会建筑升级时用到)、公会代币(绑在玩家身上,在公会商店使用)、公会荣誉(展示贡献度作用)。
公会祈祷每天6点重置,用corn表达式来做的,没有使用定时器。记录上一次祈祷的时间和状态,存放在数据库中。用corn表达式和上一次祈祷时间(2015-10-11 9:00:00)算出重置的时间(2015-10-12 6:00:00)。如果当前时间大于重置时间,则重置。
公会升级:公会升级实际上是对公会建筑的升级,每一个公会建筑都有一个升级的页面。
公会升级配置表tongbuild.xls对应两个模板TongUpGradeTemplate和TongUpGradeLimitTemplate。表里配置每一个建筑Id,建筑升每一级升级需要消耗的资金和资材数以及升级以后的效果。同时还有公会每升一级对应的其他公会建筑的上限。
公会建设和公会商店升级:扣除对应的资金和资材数。公会建设升级相当于公会升级,公会升级提升公会的人数上限和其他建筑的等级。公会商店升级刷新公会商店里对应的物品。
公会副本: 副本战斗+公会鼓舞+副本排行榜+副本日志
公会副本战斗比较复杂,这部分是由其他人完成的。公会成员每天可以挑战两次公会副本战斗。当前公会副本战斗打完后解锁新的公会副本。公会副本战斗里面由一个大boss带几个小怪。小怪对大boss有Buff功能,比如说小怪可以给大怪加攻击50%或者防御60%。打死小怪,才能消除大boss身上的buff效果。
公会鼓舞是消耗钻石提升公会成员攻击60%或者生命力60%,每次鼓舞持续时间两小时。
副本排行榜和战力排行榜差不多。副本日志和公会日志原理上是一样的。
任务三:洗练系统
洗练系统主要是对英雄的生命、攻击和防御三个属性进行洗练。消耗洗练石,洗练石可以通过每日洗练事件和累计洗练成就来得到,也可以在商店里购买。洗练分为普通洗练、金币洗练和钻石洗练三种,后面两种需要消耗玩家的金钱和钻石。
洗练配置表forge.xls根据猎人的星级和职业对应不同的累计洗练上限,forge.xls三个页签对应三个模板,分别是ForgeTypeTemplate、ForgeLimitTemplate、ForgeRuleTemplate。根据猎人的职业、选择的洗练类型以及当前属性的累计洗练值来确定洗练的规则,也就是洗练出现下降或者上升的概率和区间。这样的规则设置也是为了保证不同职业的洗练值不一样,以及英雄累计洗练快达到上限时掉落的概率高。
洗练一次和洗练十次,就是从ForgeTypeTemplate中由职业+洗练类型确定生命,攻击,防御洗练规则池,然后在ForgeRuleTemplate中由当前洗练累计值确定洗练规则,得到洗练规则后通过权值的方式判断是落入属性下降的区间还是上升的区间,最后在区间里随机一个值发给客户端。洗练十次,也就是重复十次上面的操作。
保存,洗练一次或者洗练十次得到的结果只用于显示,点击保存才加到累计的洗练值上。相加之后得到的累计洗练值要进行判断是否超过上限和下限,上限是通过职业和星级从ForgeLimitTemplate中得到,下限就是0.
展示的是猎人 当前属性累计洗练值/洗练上限值 洗练一次或者十次的值。
洗练每天有次数限制,每个玩家所有英雄加起来每天有50次洗练机会,第二天6点重置。重置是在玩家登陆后处理的,用corn表达式来做的。记录上一次重置的日期,有上一次的日期得到下一次重置的时间6:00,看当前时间是不是在下一天,是否6:00以后,满足就重置。重置完后更新重置时间。
任务四:GMTool后台一键查询功能
如上图所示,即为GMService的后台一键查询功能。一键查询功能主要包括的业务有:
(查询各服务器、各渠道)注册人数、登录人数、充值人数、活跃人数(最近3天以内有登录)、付费率、付费ARPU和活跃ARPU,总充值。一键查询功能的数据来源于分析日志和数据库的Pay表。其中注册和登录人数主要分析35号注册日志和7号登录日志,付费充值主要分析Pay表。 以登录人数为例,将各个渠道Id和登录人数以key-value的形式存在map中,这样就可以通过渠道Id拿到对应的登录人数,遍历map,所有渠道相加,即可得到总人数。其他数据注册人数等也是一样,每一项数据对应一个map。存完后,将数据显示在浏览器的表格<td>中。
两个页面文件one_key_info_query.vm和one_key_info_result.vm,
one_key_info_query.vm页面是查询之前的页面,用户选择查询服务器、查询渠道列表,减数据后,checkbox会改变页面元素的属性。one_key_info_query.vm通过document拿到相应页面元素的属性,转化为js数据类型的格式,然后拼接成get参数请求one_key_info_result.vm结果页面
结果页面拿到对应的参数后,调用AccountQueryTool类getOneKeyQueryInfo()方法,在这个方法内根据传进来的查询服务器列表和渠道列表去分析日志和充值Pay,得到对应的数据,写入到浏览器的<td>表格中。