一款網絡遊戲的設計,至少需要兩種數據庫。策劃數據庫是表示遊戲玩法規則的數據庫;用戶數據庫是保存玩家個人信息的數據庫。除了這兩類基本的數據庫以外,還有其他數據庫。例如有些跨服玩法需要配置數據庫來尋找其他服務節點的鏈路地址;有些架構把日誌放到獨立的日誌數據庫進行統一管理,等等。
本文主要介紹玩法配置數據庫與玩家用戶數據庫。
策劃數據庫的概念
策劃數據庫,顧名思義,是策劃童鞋用於描述他心目中理想遊戲世界的手段,是遊戲的規則。例如,玩家當前級別可擁有的最大體力值是多少,長到下一級別需要獲得多少經驗,各種遊戲規則的設定都是通過該數據庫裏的各種數據表進行控制。也就是說,策劃配置表是遊戲的玩法,因此,除了策劃童鞋之外,絕不允許開發人員亂修改表內容。我曾呆過的一家遊戲項目,經常看到開發新手不小心在代碼裏修改了策劃數值,導致遊戲規則被修改了。這可是要扣績效的啊!!
用戶數據庫的概念
以前玩街機遊戲的時候,玩家的數據是無法保存的,一旦斷電了,那麼就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();
}
}
到這裏,關於配置數據庫和用戶數據庫的概念及實現就介紹完畢了。