手游服务端框架之配置与玩家数据库设计

一款网络游戏的设计,至少需要两种数据库。策划数据库是表示游戏玩法规则的数据库;用户数据库是表示玩家个人信息的数据库。除了这两类基本的数据库以外,还有其他数据库。例如有些跨服玩法需要配置数据库来寻找其他服务节点的链路地址;有些架构把日志放到独立的日志数据库进行统一管理,等等。

本文主要介绍玩法配置数据库与玩家用户数据库。

策划数据库的概念

策划数据库,顾名思义,是策划童鞋用于描述他心目中理想游戏世界的手段,是游戏的规则。例如,玩家当前级别可拥有的最大体力值是多少,长到下一级别需要获得多少经验,各种游戏规则的设定都是通过该数据库里的各种数据表进行控制。也就是说,策划配置表是游戏的玩法,因此,除了策划童鞋之外,绝不允许开发人员乱修改表内容。我曾呆过的一家游戏项目,经常看到开发新手不小心在代码里修改了策划数值,导致游戏规则被修改了。这可是要扣绩效的啊!!

用户数据库的概念

以前玩街机游戏的时候,玩家的数据是无法保存的,一旦断电了,那么就GameOver了。在网络游戏时代,游戏数据三是长时间保存的,那么就需要数据库来保存玩家的个人信息。打个比方,今天运气非常好,打野怪刷到了一把极品装备,如果没有持久化机制,那么玩家下线后再来玩,装备就不见了。玩家数据是玩家的私有财产,如果代码不小心把玩家的数据弄脏了,那么就一定要想方设法来帮助玩家恢复数据或进行游戏道具补偿。玩家数据库除了保存个人数据之外,还会保存一些公共数据,比如帮派数据是整个帮派成员共有的。

数据库ORM方案

不管是什么数据库,都会涉及到数据的增删查改操作。ORM(对象关系映射)是解决这些繁琐重复工作的利器。需要注意的是,策划配置表属于游戏规则,开发人员一般只有读取的权限,而没有修改的权限。

本文所采用的ORM框架在之前的文章 自定义orm框架解决玩家数据持久化问题 已有详细介绍,这里不作详细介绍。

需要说明的是,orm工具这里采用的数据库连接池改为Proxool库;为了统一处理策划库与用户库,DbUtils工具类的多个方法增加一个参数,表示对应的数据库别名。例如:

 

[plain] view plain copy
 
  1. /**  
  2.      * 查询返回一个bean实体  
  3.      * @param alias 数据库别名  
  4.      * @param sql  
  5.      * @param entity  
  6.      * @return  
  7.      */  
  8.     @SuppressWarnings("unchecked")  
  9.     public static <T> T queryOne(String alias, String sql, Class<?> entity){  
  10.                
  11. }  

配置数据库的设计

从策划童鞋的角度上看,配置数据就是一张一张的excel表格。开发人员根据策划的表设计,转化成对应的数据库表格式。程序启动的时候,就会将所有的数据库表读取到缓存里,这样程序的逻辑就会按给定的数值进行运行。当然,策划表格不一样只能从数据库读取,有些项目连数据库都取消了。策划配置的表格,通过一种导表程序,转换为xml文件或者csv文件,程序一样可以读取到内存。但个人感觉,还是采用数据库处理配置比较方便,毕竟数据库对开发人员来说比较友好。

下边说明一下建立一张配置表的步骤:

1. 建立数据表结构(这个结果及即可以有程序制定,也可以由策划制定,看项目),并加入若干测试数据

 

[sql] view plain copy
 
  1. DROP TABLE IF EXISTS `configplayerlevel`;  
  2. CREATE TABLE `configplayerlevel` (  
  3.   `level` int(11) DEFAULT NULL,  
  4.   `needExp` bigint(20) DEFAULT NULL,  
  5.   `vitality` int(11) DEFAULT NULL  
  6. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;  
  7.   
  8. -- ----------------------------  
  9. -- Records of configplayerlevel  
  10. -- ----------------------------  
  11. INSERT INTO `configplayerlevel` VALUES ('1', '2345', '100');  
  12. INSERT INTO `configplayerlevel` VALUES ('2', '23450', '105');  

2. 定义数据实体

 

[java] view plain copy
 
  1. /** 
  2.  * 玩家等级配置表 
  3.  * @author kingston 
  4.  */  
  5. @Entity(readOnly = true)  
  6. public class ConfigPlayerLevel {  
  7.   
  8.     /** 
  9.      * 等级 
  10.      */  
  11.     @Column  
  12.     private int level;  
  13.       
  14.     /** 
  15.      * 升到下一级别需要的经验 
  16.      */  
  17.     @Column  
  18.     private long needExp;  
  19.       
  20.     /** 
  21.      * 最大体力 
  22.      */  
  23.     @Column  
  24.     private int vitality;  
  25.   
  26.     public int getLevel() {  
  27.         return level;  
  28.     }  
  29.   
  30.     public void setLevel(int level) {  
  31.         this.level = level;  
  32.     }  
  33.   
  34.     public long getNeedExp() {  
  35.         return needExp;  
  36.     }  
  37.   
  38.     public void setNeedExp(long needExp) {  
  39.         this.needExp = needExp;  
  40.     }  
  41.   
  42.     public int getVitality() {  
  43.         return vitality;  
  44.     }  
  45.   
  46.     public void setVitality(int vitality) {  
  47.         this.vitality = vitality;  
  48.     }  
  49.       
  50. }  

3. 为了方便管理表数据,对于每一张表都定义一个容器

 

 

[java] view plain copy
 
  1. /** 
  2.  * 玩家等级配置表 
  3.  * @author kingston 
  4.  */  
  5. public class ConfigPlayerLevelContainer implements Reloadable{  
  6.       
  7.     private Map<Integer, ConfigPlayerLevel> levels = new HashMap<>();  
  8.   
  9.     @Override  
  10.     public void reload() {  
  11.         String sql = "SELECT * FROM ConfigPlayerLevel";  
  12.         List<ConfigPlayerLevel> datas = DbUtils.queryMany(DbUtils.DB_DATA, sql, ConfigPlayerLevel.class);  
  13.         //使用jdk8,将list转为map  
  14.         levels = datas.stream().collect(  
  15.                 Collectors.toMap(ConfigPlayerLevel::getLevel, e -> e));  
  16.     }  
  17.       
  18.     public ConfigPlayerLevel getConfigBy(int level) {  
  19.         return levels.get(level);  
  20.     }  
  21.       
  22. }  

 

4. 容器表都实现Reloadable接口,该接口只有一个抽象方法,这样方便服务启动的时候能统一管理

 

[java] view plain copy
 
  1. public interface Reloadable {  
  2.   
  3.     /** 
  4.      * 重载数据 
  5.      */  
  6.     void reload();  
  7.       
  8. }  

5. 为了方便管理所有表数据,我们再定义一个配置数据池,每一个配置容器都在这里进行申明。这样做可以很方便在生产环境进行热更新配置,关于热更新配置的做法,后面文章再详细介绍。该数据池还需要提供一个公有方法用于读取全部配置数据。

 

 

[java] view plain copy
 
  1. /** 
  2.  * 所有策划配置的数据池 
  3.  * @author kingston 
  4.  */  
  5. public class ConfigDatasPool {  
  6.       
  7.     private static ConfigDatasPool instance = new ConfigDatasPool();   
  8.       
  9.     private ConfigDatasPool() {}  
  10.       
  11.     public static ConfigDatasPool getInstance() {  
  12.         return instance;  
  13.     }  
  14.       
  15.     public ConfigPlayerLevelContainer configPlayerLevelContainer = new ConfigPlayerLevelContainer();  
  16.   
  17.     /** 
  18.      * 起服读取所有的配置数据 
  19.      */  
  20.     public void loadAllConfigs() {  
  21.         Field[] fields = ConfigDatasPool.class.getDeclaredFields();  
  22.         ConfigDatasPool instance = getInstance();  
  23.         for (Field f:fields) {  
  24.             try {  
  25.             if (Reloadable.class.isAssignableFrom(f.getType())) {  
  26.                 Reloadable container = (Reloadable) f.getType().newInstance();  
  27.                 System.err.println(f.getType());  
  28.                 container.reload();  
  29.                 f.set(instance, container);  
  30.             }  
  31.             }catch (Exception e) {  
  32.                 LoggerUtils.error("策划配置数据有误,请检查", e);  
  33.                 System.exit(0);  
  34.             }  
  35.         }  
  36.           
  37.     }  
  38.       
  39.       
  40. }  

用户数据库设计

 

1. 用户数据表的设计是由开发人员在实现业务需求时自行设计的。以前的一篇文章游戏服务器关于玩家数据的解决方案 详细说明了两种用户数据设计策略。由于当前涉及的用户信息非常少,作为演示,我们只用到一张数据表。(针对不同业务所需要的用户信息保存方式,以后再作详细展开)。用户表的设计如下

 

[sql] view plain copy
 
  1. DROP TABLE IF EXISTS `player`;  
  2. CREATE TABLE `player` (  
  3.   `id` bigint(20) NOT NULL,  
  4.   `name` varchar(255) DEFAULT NULL  COMMENT '昵称',  
  5.   `job` tinyint(4) DEFAULT NULL  COMMENT '职业',  
  6.   `level` int(11) DEFAULT '1' COMMENT '等级',  
  7.   `exp` bigint(20) DEFAULT 0  COMMENT '经验' ,  
  8.   PRIMARY KEY (`id`)  
  9. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;  

2. 用户数据是需要持久化的,所以我们需要借助orm框架的 AbstractCacheable类。同时为了能够将用户数据放入哈希容器,我们有必要重写object类的equals()和hashCode()方法。于是,有了下面的抽象类

 

 

[java] view plain copy
 
  1. /** 
  2.  * db实体基类 
  3.  * @author kingston 
  4.  */  
  5. public abstract class BaseEntity<Id extends Comparable<Id>> extends AbstractCacheable   
  6.             implements Serializable {  
  7.   
  8.     private static final long serialVersionUID = 5416347850924361417L;  
  9.   
  10.     public abstract Id getId() ;  
  11.   
  12.     @Override  
  13.     public int hashCode() {  
  14.         final int prime = 31;  
  15.         int result = 1;  
  16.         result = prime * result + ((getId()==null)?0:getId().hashCode());  
  17.         return result;  
  18.     }  
  19.   
  20.     @SuppressWarnings("rawtypes")  
  21.     @Override  
  22.     public boolean equals(Object obj) {  
  23.         if (this == obj)  
  24.             return true;  
  25.         if (obj == null)  
  26.             return false;  
  27.         if (getClass() != obj.getClass())  
  28.             return false;  
  29.         BaseEntity other = (BaseEntity) obj;  
  30.         if (getId() != other.getId())  
  31.             return false;  
  32.         return true;  
  33.     }  
  34.   
  35. }  

3. 定义用户模型Player类,该类只需要继承上面的BaseEntity抽象类即可。是不是很方便 ^_^

 

 

[java] view plain copy
 
  1. @Entity  
  2. public class Player extends BaseEntity<Long>{  
  3.   
  4.     private static final long serialVersionUID = 8913056963732639062L;  
  5.   
  6.     @Id  
  7.     @Column  
  8.     private long id;  
  9.       
  10.     @Column  
  11.     private String name;  
  12.       
  13.     /** 
  14.      * 职业 
  15.      */  
  16.     @Column   
  17.     private int job;  
  18.       
  19.     @Column  
  20.     private int level;  
  21.       
  22.     @Column  
  23.     private long exp;  
  24.       
  25.     public Player() {  
  26.         this.id = IdGenerator.getUid();  
  27.     }  
  28.   
  29.     @Override  
  30.     public Long getId() {  
  31.         return id;  
  32.     }  
  33.   
  34.     public void setId(long id) {  
  35.         this.id = id;  
  36.     }  
  37.   
  38.     public String getName() {  
  39.         return name;  
  40.     }  
  41.   
  42.     public void setName(String name) {  
  43.         this.name = name;  
  44.     }  
  45.   
  46.     public int getJob() {  
  47.         return job;  
  48.     }  
  49.   
  50.     public void setJob(int job) {  
  51.         this.job = job;  
  52.     }  
  53.   
  54.     public int getLevel() {  
  55.         return level;  
  56.     }  
  57.   
  58.     public void setLevel(int level) {  
  59.         this.level = level;  
  60.     }  
  61.   
  62.     public long getExp() {  
  63.         return exp;  
  64.     }  
  65.   
  66.     public void setExp(long exp) {  
  67.         this.exp = exp;  
  68.     }  
  69.   
  70.     @Override  
  71.     public String toString() {  
  72.         return "Player [id=" + id + ", name=" + name + ", job=" + job  
  73.                 + ", level=" + level + ", exp=" + exp + "]";  
  74.     }  
  75.       
  76. }  

用户数据异步持久化

 

当玩家的数据发生变动时,我们需要将最新的数据保存到数据库。这里有一个问题,当玩家数据有部分变动的时候,我们不可能即使保存到数据库的,这样对数据库的压力太大。所以,我们需要有独立线程来完成数据的异步保存。这里又要搬出我们可爱的生产者消费者模型啦。

[java] view plain copy
 
  1. /** 
  2.  * 用户数据异步持久化的服务 
  3.  * @author kingston 
  4.  */  
  5. public class DbService {  
  6.       
  7.     private static volatile DbService instance;  
  8.       
  9.     public static DbService getInstance() {  
  10.         if (instance ==  null) {  
  11.             synchronized (DbService.class) {  
  12.                 if (instance ==  null) {  
  13.                     instance = new DbService();  
  14.                 }  
  15.             }  
  16.         }  
  17.         return instance;  
  18.     }  
  19.       
  20.     /** 
  21.      * 启动消费者线程 
  22.      */  
  23.     public void init() {  
  24.         new Thread(new Worker()).start();  
  25.     }  
  26.       
  27.     @SuppressWarnings("rawtypes")  
  28.     private BlockingQueue<BaseEntity> queue = new BlockingUniqueQueue<>();  
  29.       
  30.     private final AtomicBoolean run = new AtomicBoolean(true);  
  31.       
  32.     public void add2Queue(BaseEntity<?> entity) {  
  33.         this.queue.add(entity);  
  34.     }  
  35.       
  36.       
  37.     private class Worker implements Runnable {  
  38.         @Override  
  39.         public void run() {  
  40.             while(run.get()) {  
  41.                 try {  
  42.                     BaseEntity<?> entity = queue.take();  
  43.                     saveToDb(entity);  
  44.                 } catch (InterruptedException e) {  
  45.                     LoggerUtils.error("", e);  
  46.                 }  
  47.             }  
  48.         }  
  49.     }  
  50.       
  51.     /** 
  52.      * 数据真正持久化 
  53.      * @param entity 
  54.      */  
  55.     private void saveToDb(BaseEntity<?> entity) {  
  56.         entity.save();  
  57.     }  
  58.   
  59. }  

 

到这里,关于配置数据库和用户数据库的概念及实现就介绍完毕了。

posted @ 2017-07-17 20:22  热血江湖  阅读(1344)  评论(0编辑  收藏  举报
友情链接:博客园  微群相册  百度音乐