转载: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. 建立数据表结构(这个结果及即可以有程序制定,也可以由策划制定,看项目),并加入若干测试数据
-
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();
-
}
-
-
}
到这里,关于配置数据库和用户数据库的概念及实现就介绍完毕了。