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

原创 2017年07月16日 20:56:19

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

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

策划数据库的概念

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

用户数据库的概念

以前玩街机游戏的时候,玩家的数据是无法保存的,一旦断电了,那么就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. 建立数据表结构(这个结果及即可以有程序制定,也可以由策划制定,看项目),并加入若干测试数据

DROP TABLE IF EXISTS `configplayerlevel`;
CREATE TABLE `configplayerlevel` (
  `level` int(11) DEFAULT NULL,
  `needExp` bigint(20) DEFAULT NULL,
  `vitality` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of configplayerlevel
-- ----------------------------
INSERT INTO `configplayerlevel` VALUES ('1', '2345', '100');
INSERT INTO `configplayerlevel` VALUES ('2', '23450', '105');
2. 定义数据实体

/**
 * 玩家等级配置表
 * @author kingston
 */
@Entity(readOnly = true)
public class ConfigPlayerLevel {

	/**
	 * 等级
	 */
	@Column
	private int level;
	
	/**
	 * 升到下一级别需要的经验
	 */
	@Column
	private long needExp;
	
	/**
	 * 最大体力
	 */
	@Column
	private int vitality;

	public int getLevel() {
		return level;
	}

	public void setLevel(int level) {
		this.level = level;
	}

	public long getNeedExp() {
		return needExp;
	}

	public void setNeedExp(long needExp) {
		this.needExp = needExp;
	}

	public int getVitality() {
		return vitality;
	}

	public void setVitality(int vitality) {
		this.vitality = vitality;
	}
	
}
3. 为了方便管理表数据,对于每一张表都定义一个容器

/**
 * 玩家等级配置表
 * @author kingston
 */
public class ConfigPlayerLevelContainer implements Reloadable{
	
	private Map<Integer, ConfigPlayerLevel> levels = new HashMap<>();

	@Override
	public void reload() {
		String sql = "SELECT * FROM ConfigPlayerLevel";
		List<ConfigPlayerLevel> datas = DbUtils.queryMany(DbUtils.DB_DATA, sql, ConfigPlayerLevel.class);
		//使用jdk8,将list转为map
		levels = datas.stream().collect(
				Collectors.toMap(ConfigPlayerLevel::getLevel, e -> e));
	}
	
	public ConfigPlayerLevel getConfigBy(int level) {
		return levels.get(level);
	}
	
}

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

public interface Reloadable {

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

/**
 * 所有策划配置的数据池
 * @author kingston
 */
public class ConfigDatasPool {
	
	private static ConfigDatasPool instance = new ConfigDatasPool(); 
	
	private ConfigDatasPool() {}
	
	public static ConfigDatasPool getInstance() {
		return instance;
	}
	
	public ConfigPlayerLevelContainer configPlayerLevelContainer = new ConfigPlayerLevelContainer();

	/**
	 * 起服读取所有的配置数据
	 */
	public void loadAllConfigs() {
		Field[] fields = ConfigDatasPool.class.getDeclaredFields();
		ConfigDatasPool instance = getInstance();
		for (Field f:fields) {
			try {
			if (Reloadable.class.isAssignableFrom(f.getType())) {
				Reloadable container = (Reloadable) f.getType().newInstance();
				System.err.println(f.getType());
				container.reload();
				f.set(instance, container);
			}
			}catch (Exception e) {
				LoggerUtils.error("策划配置数据有误,请检查", e);
				System.exit(0);
			}
		}
		
	}
	
	
}

用户数据库设计

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

DROP TABLE IF EXISTS `player`;
CREATE TABLE `player` (
  `id` bigint(20) NOT NULL,
  `name` varchar(255) DEFAULT NULL  COMMENT '昵称',
  `job` tinyint(4) DEFAULT NULL  COMMENT '职业',
  `level` int(11) DEFAULT '1' COMMENT '等级',
  `exp` bigint(20) DEFAULT 0  COMMENT '经验' ,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2. 用户数据是需要持久化的,所以我们需要借助orm框架的 AbstractCacheable类。同时为了能够将用户数据放入哈希容器,我们有必要重写object类的equals()和hashCode()方法。于是,有了下面的抽象类

/**
 * db实体基类
 * @author kingston
 */
public abstract class BaseEntity<Id extends Comparable<Id>> extends AbstractCacheable 
			implements Serializable {

	private static final long serialVersionUID = 5416347850924361417L;

	public abstract Id getId() ;

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((getId()==null)?0:getId().hashCode());
		return result;
	}

	@SuppressWarnings("rawtypes")
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		BaseEntity other = (BaseEntity) obj;
		if (getId() != other.getId())
			return false;
		return true;
	}

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

@Entity
public class Player extends BaseEntity<Long>{

	private static final long serialVersionUID = 8913056963732639062L;

	@Id
	@Column
	private long id;
	
	@Column
	private String name;
	
	/**
	 * 职业
	 */
	@Column 
	private int job;
	
	@Column
	private int level;
	
	@Column
	private long exp;
	
	public Player() {
		this.id = IdGenerator.getUid();
	}

	@Override
	public Long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getJob() {
		return job;
	}

	public void setJob(int job) {
		this.job = job;
	}

	public int getLevel() {
		return level;
	}

	public void setLevel(int level) {
		this.level = level;
	}

	public long getExp() {
		return exp;
	}

	public void setExp(long exp) {
		this.exp = exp;
	}

	@Override
	public String toString() {
		return "Player [id=" + id + ", name=" + name + ", job=" + job
				+ ", level=" + level + ", exp=" + exp + "]";
	}
	
}

用户数据异步持久化

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

/**
 * 用户数据异步持久化的服务
 * @author kingston
 */
public class DbService {
	
	private static volatile DbService instance;
	
	public static DbService getInstance() {
		if (instance ==  null) {
			synchronized (DbService.class) {
				if (instance ==  null) {
					instance = new DbService();
				}
			}
		}
		return instance;
	}
	
	/**
	 * 启动消费者线程
	 */
	public void init() {
		new Thread(new Worker()).start();
	}
	
	@SuppressWarnings("rawtypes")
	private BlockingQueue<BaseEntity> queue = new BlockingUniqueQueue<>();
	
	private final AtomicBoolean run = new AtomicBoolean(true);
	
	public void add2Queue(BaseEntity<?> entity) {
		this.queue.add(entity);
	}
	
	
	private class Worker implements Runnable {
		@Override
		public void run() {
			while(run.get()) {
				try {
					BaseEntity<?> entity = queue.take();
					saveToDb(entity);
				} catch (InterruptedException e) {
					LoggerUtils.error("", e);
				}
			}
		}
	}
	
	/**
	 * 数据真正持久化
	 * @param entity
	 */
	private void saveToDb(BaseEntity<?> entity) {
		entity.save();
	}

}

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


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








版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

数据库设计中的14个技巧

数据库设计技巧

MySQL主从复制架构搭建及读写分离测试

一、业务发展驱动数据发展 随着网站业务的不断发展,用户量的不断增加,数据量成倍地增长,数据库的访问量也呈线性地增长。特别是在用户访问高峰期间,并发访问量突然增大,数据库的负载压力也会增大,如果架构方...
  • luyaran
  • luyaran
  • 2016年12月27日 09:37
  • 967

手游服务端框架之模仿SpringMvc处理玩家请求

经典web开发项目通常采用三层架构来组织代码。典型的,第一层为表现层,通常使用MVC模式;第二层为业务逻辑层,该层主要是各种service业务操作类;第三层则为数据访问层。类似的,我们的游戏项目也可以...

游戏测试永乐大典——服务端架构,游戏服务器架构,游戏数据库设计

服务端结构概念简图                                                          《水桥月游戏测试技术- Webgame Technolog...

手游服务端框架之消息线程模型

玩家的消息请求如果放在mina的io线程池进行处理,当业务处理非常耗时,会严重影响io的吞吐量。所以,我们应该另起用于处理业务逻辑的线程池,采用生产者消费者模型,异步处理玩家请求。...

手游服务端框架之使用Guava构建缓存系统

缓存,在项目中的应用非常之广泛。诸如这样的场景,某些对象计算或者获取的代码比较昂贵,并且在程序里你不止一次要用到这些对象,那么,你就应该使用缓存。缓存,在项目中的应用非常之广泛。诸如这样的场景,某些对...

3D动作手游《末日战歌》全套源码分享 客户端+服务端+数据库

源码介绍:         今天我们外星人源码论坛给大家分享一套《末日战歌》3D动作手游源码。游戏添加了丰富多样的各类养成系统及社区交互系统,同时还有精彩的飞龙战、塔防战、推塔等玩法。    ...

TYPESDK手游聚合SDK服务端设计思路与架构之五:流程优化之特殊流程处理

在之前的几篇文字中,我们分析了从零开始搭建一个渠道聚合SDK服务端所需要应对的几个最重要的一般性流程。按照文中的内容,我们大可以自己最擅长的语言和工具开发出一套已经可以正常工作的服务端,这个服务端可以...

TYPESDK手游聚合SDK服务端设计思路与架构之一:应用场景分析

TYPESDK 服务端设计思路与架构之一:应用场景分析              作为一个渠道SDK统一接入框架,TYPESDK从一开始,所面对的需求场景就是多款游戏,通过一个统一的SDK服务端,能够...

unity手游《摩卡世界online》全套源码(服务端+客户端+数据库)

unity手游《摩卡世界online》全套源码(服务端+客户端+数据库),客户端用unity3d开发,服务端用java开发,包含服务端源码、客户端源码、工具源码、数据库、配置搭建文档说明、编译好的文件...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:手游服务端框架之配置与玩家数据库设计
举报原因:
原因补充:

(最多只允许输入30个字)