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

转载:https://blog.csdn.net/littleschemer/article/details/75194909

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

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

策划数据库的概念

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

用户数据库的概念

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

数据库ORM方案

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

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

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

/**
	 * 查询返回一个bean实体
	 * @param alias 数据库别名
	 * @param sql
	 * @param entity
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static <T> T queryOne(String alias, String sql, Class<?> entity){

}

配置数据库的设计

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

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

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


 
 
  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. – Records of configplayerlevel
  9. – ----------------------------
  10. INSERT INTO configplayerlevel VALUES ( ‘1’, ‘2345’, ‘100’);
  11. INSERT INTO configplayerlevel VALUES ( ‘2’, ‘23450’, ‘105’);
2. 定义数据实体


 
 
  1. /
  2. * 玩家等级配置表
  3. * @author kingston
  4. */
  5. @Entity(readOnly = true)
  6. public class ConfigPlayerLevel {
  7. /
  8. * 等级
  9. */
  10. @Column
  11. private int level;
  12. /
  13. * 升到下一级别需要的经验
  14. */
  15. @Column
  16. private long needExp;
  17. /
  18. * 最大体力
  19. */
  20. @Column
  21. private int vitality;
  22. public int getLevel() {
  23. return level;
  24. }
  25. public void setLevel(int level) {
  26. this.level = level;
  27. }
  28. public long getNeedExp() {
  29. return needExp;
  30. }
  31. public void setNeedExp(long needExp) {
  32. this.needExp = needExp;
  33. }
  34. public int getVitality() {
  35. return vitality;
  36. }
  37. public void setVitality(int vitality) {
  38. this.vitality = vitality;
  39. }
  40. }
3. 为了方便管理表数据,对于每一张表都定义一个容器


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

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


 
 
  1. public interface Reloadable {
  2. /
  3. * 重载数据
  4. */
  5. void reload();
  6. }
5. 为了方便管理所有表数据,我们再定义一个配置数据池,每一个配置容器都在这里进行申明。这样做可以很方便在生产环境进行热更新配置,关于热更新配置的做法,后面文章再详细介绍。该数据池还需要提供一个公有方法用于读取全部配置数据。


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

用户数据库设计

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


 
 
  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()方法。于是,有了下面的抽象类


 
 
  1. /
  2. * db实体基类
  3. * @author kingston
  4. */
  5. public abstract class BaseEntity<Id extends Comparable<Id>> extends AbstractCacheable
  6. implements Serializable {
  7. private static final long serialVersionUID = 5416347850924361417L;
  8. public abstract Id getId() ;
  9. @Override
  10. public int hashCode() {
  11. final int prime = 31;
  12. int result = 1;
  13. result = prime * result + ((getId()== null)? 0:getId().hashCode());
  14. return result;
  15. }
  16. @SuppressWarnings( “rawtypes”)
  17. @Override
  18. public boolean equals(Object obj) {
  19. if ( this == obj)
  20. return true;
  21. if (obj == null)
  22. return false;
  23. if (getClass() != obj.getClass())
  24. return false;
  25. BaseEntity other = (BaseEntity) obj;
  26. if (getId() != other.getId())
  27. return false;
  28. return true;
  29. }
  30. }
3. 定义用户模型Player类,该类只需要继承上面的BaseEntity抽象类即可。是不是很方便 _


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

用户数据异步持久化

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


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

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


文章预告:下一篇主要介绍如何借助谷歌的guanva工具来定制自己的缓存系统。
手游服务端开源框架系列完整的代码请移步github ->>game_server








评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值