Marauroa开发笔记一
2019-04-12
模型类用于保存数据,被服务端和客户端公用,包括一个RPObject对象及模型类自身的属性,同时要定义一个与其RPObject对象对应的RPClass。
模型类包括两个重载的构造函数,其中一个构造函数以模型类自身属性为参数,另一个构造函数以一个RPObject作为参数。
RPObject对象是模型类自身属性的网络化表示,用于网络传输和对象重建。
客户端向服务端发送RPAction,通过感知监听器接收服务端发送的感知。
2019-04-15
服务器端在每回合处理过程中,如果对某对象进行了修改,要在最后(对象最后一次被修改之后)调用RPWorld.modify(RPObject object)
以通知区域对象被改变
/**
* This method notify zone that object has been modified. Used in Delta^2
* 通知区域对象被修改
*
* @param object the object that has been modified.
*/
public void modify(RPObject object) {
IRPZone zone = zones.get(new IRPZone.ID(object.get("zoneid")));
if (zone != null) {
zone.modify(object);
} else {
logger.warn("calling RPWorld.modify on a zoneless object: " + object + " parent: " + object.getContainerBaseOwner(), new Throwable());
}
}
另外发现程序中有时候直接使用RPObject,有时候将RPObject作为模型类的一个属性;目前还不清楚这两种使用方式分别在那种情况下使用。
服务器是基于回合来驱动的。回合起点在IRPRuleProcessor.beginTurn(),回合终点在IRPRuleProcessor.endTurn()
/**
* Interface for the class that is in charge of executing actions. Implement it
* to personalize the game
* <p>
* <b> Important: you must implement the method<br>
* <i>public static IRPRuleProcessor get().</i> </b>
* <p>
* This interface is the key to extend Marauroa to match your game needs. First
* we have setContext that allows us complete access to RP Server Manager, so we
* can control some things like disconnect players or send them stuff. It is not
* possible to access GameServerManager or NetworkServerManager directly to
* avoid incorrect manipulation of data structures that could place the server
* in a bad state.
* <p>
* onActionAdd, execute, beginTurn and endTurn allow you to code behaviour for
* each of these events. You can control whenever to allow player to add an
* action to system, what system should do when it receive an action and what to
* do at the begin and end of each turn.<br>
* Perceptions are delivered just after endTurn is called.
* <p>
* Also your game can handle what to do at player entering, player leaving and
* decide what to do when player timeouts because connection has been dropped
* for example.
*
*/
public interface IRPRuleProcessor {
/**
* Notify it when a begin of actual turn happens.
*
*/
public void beginTurn();
/**
* Notify it when a end of actual turn happens.
*/
public void endTurn();
}
一种服务端写法(当然还有其他写法,Marauroa并没有强制)是,在RPWorld的子类中实现beginTrun方法,并依次调用所属区域中的beginTurn方法
/**
* 开始回合
*/
public void beginTurn() {
// 遍历本世界所有的区域,并开始回合
for (IRPZone RPzone : this) {
MaPacmanZone zone = (MaPacmanZone) RPzone;
zone.beginTurn();
}
}
而在区域的实现类中,包含了区域所有的变量(在区域的),并实现区域的beginTurn方法,在该方法中完成每回合所需的游戏逻辑。
最后在IRPRuleProcessor的实现类中,重载其beginTurn方法,并调用world.beginTurn()方法,以便以此调用区域的beginTurn()方法,以此推动服务端不断向前移动
// Notify it when a begin of actual turn happens.
synchronized public void beginTurn() {
// ...
world.beginTurn();
}
2019-04-16
在Marauroa中使用了大量的单例模式,甚至给出了SingletonRepository
这样的类,来消除单例过多带来的影响,下面举几个其中的例子(只显示主要相关代码)
public class StendhalRPRuleProcessor implements IRPRuleProcessor {
/**
* The Singleton instance.
*/
protected static StendhalRPRuleProcessor instance;
/**
* creates a new StendhalRPRuleProcessor
*/
protected StendhalRPRuleProcessor() {
onlinePlayers = new PlayerList();
playersRmText = new LinkedList<Player>();
entityToKill = new LinkedList<Pair<RPEntity, Entity>>();
}
/**
* gets the singleton instance of StendhalRPRuleProcessor
*
* @return StendhalRPRuleProcessor
*/
public static StendhalRPRuleProcessor get() {
synchronized (StendhalRPRuleProcessor.class) {
if (instance == null) {
StendhalRPRuleProcessor instance = new StendhalRPRuleProcessor();
instance.init();
StendhalRPRuleProcessor.instance = instance;
}
}
return instance;
}
}
/**
* Manages gags.
*/
public class GagManager implements LoginListener {
/**
* The Singleton instance.
*/
private static GagManager instance;
/**
* returns the GagManager object (Singleton Pattern).
*
* @return GagManager
*/
public static GagManager get() {
if (instance == null) {
instance = new GagManager();
}
return instance;
}
// singleton
private GagManager() {
SingletonRepository.getLoginNotifier().addListener(this);
}
}
/**
* Singleton class that contains inventory and prices of NPC stores.
*/
public final class ShopList {
private static ShopList instance;
/**
* Returns the Singleton instance.
*
* @return The instance
*/
public static ShopList get() {
if (instance == null) {
instance = new ShopList();
}
return instance;
}
private final Map<String, Map<String, Integer>> contents;
private ShopList() {
contents = new HashMap<String, Map<String, Integer>>();
}
}
关于单例模式,网上随便找了篇文章https://blog.csdn.net/goodlixueyong/article/details/51935526,上面的实现有几个特点:
- 获得单例对象的方法一般统一命名为get(), 简短易懂
The idea behind RPClass is not define members as a OOP language but to save bandwidth usage by replacing these members text definitions with a short integer.
设计RPClass这个类背后的想法是,不定义OOP语言的成员,而是使用短数字来代替,达到节省带宽的目的。
RPClass是marauroa的关键概念。它定义了属性的类型(字符串,整数,布尔型...)和可见性(隐藏、私有或可见)。而属性用来构造对象(对象是属性的集合,例如对于人来说,年龄、高度就是他的属性)。
2019-04-17
在jmapacman-0.3中,幽灵对象(Ghost)有一个隐藏属性!changedir,其初值为10,每回合该属性递减,直至为0后,幽灵随机选择一个方向(dir),且!changedir获得一个1~6的随机值。!changedir代表了幽灵改变方向的频率:所有幽灵初始都会在10个回合后改变方向,后续则随机1~6个回合改变方向。
2019-04-19
在stendhal中,Entity继承了RPObject,是所有实体类的基类。
2019-04-20
在IRPRuleProcessor接口的实现中,必须实现一个静态方法public static IRPRuleProcessor get()
,很明显这是要作为一个单例实现了。在marauroa.server.game.rp.RPRuleProcessorImpl中:
/** *************************************************************************
* (C) Copyright 2011-2012 - Marauroa *
***************************************************************************
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
************************************************************************** */
package marauroa.server.game.rp;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.List;
import marauroa.common.Log4J;
import marauroa.common.Logger;
import marauroa.common.crypto.Hash;
import marauroa.common.game.AccountResult;
import marauroa.common.game.CharacterResult;
import marauroa.common.game.IRPZone;
import marauroa.common.game.RPAction;
import marauroa.common.game.RPObject;
import marauroa.common.game.RPObjectInvalidException;
import marauroa.common.game.RPObjectNotFoundException;
import marauroa.common.game.Result;
import marauroa.server.db.DBTransaction;
import marauroa.server.db.TransactionPool;
import marauroa.server.game.db.AccountDAO;
import marauroa.server.game.db.CharacterDAO;
import marauroa.server.game.db.DAORegister;
/**
* a default implementation of RPRuleProcessor
* 一个默认的RPRuleProcessor的实现
*/
public class RPRuleProcessorImpl implements IRPRuleProcessor {
// 单例
private static RPRuleProcessorImpl instance;
/**
* RPServerManager
*/
protected RPServerManager manager;
private static Logger logger = Log4J.getLogger(RPRuleProcessorImpl.class);
/**
* gets the Rule singleton object
*
* @return Rule
*/
public static IRPRuleProcessor get() {
if (instance == null) {
instance = new RPRuleProcessorImpl();
}
return instance;
}
/**
* Set the context where the actions are executed.
* 设置action执行的上下文
*
* @param rpman the RPServerManager object that is running our actions
*/
@Override
public void setContext(RPServerManager rpman) {
manager = rpman;
}
/**
* Returns true if the version of the game is compatible
* 若游戏版本兼容则返回true
*
* @param game the game name
* @param version the game version
* @return true, if the game and version is compatible
*/
@Override
public boolean checkGameVersion(String game, String version) {
return true;
}
/**
* Callback method called when a new player time out. This method MUST
* logout the player
* 玩家超时回调函数。该方法必须注销玩家。
*
* @param object the new player that timeouts.
* @throws RPObjectNotFoundException if the object was not found
*/
@Override
public synchronized void onTimeout(RPObject object) {
onExit(object);
}
/**
* Callback method called when a player exits the game
* 玩家离开游戏回调函数
*
* @param object the new player that exits the game.
* @return true to allow player to exit
* @throws RPObjectNotFoundException if the object was not found
*/
@Override
public synchronized boolean onExit(RPObject object) {
RPWorld.get().remove(object.getID());
return true;
}
/**
* Callback method called when a new player enters in the game
* 玩家进入游戏回调函数
*
* @param object the new player that enters in the game.
* @return true if object has been added.
* @throws RPObjectInvalidException if the object was not accepted
*/
@Override
public synchronized boolean onInit(RPObject object) {
IRPZone zone = RPWorld.get().getDefaultZone();
zone.add(object);
return true;
}
/**
* Notify it when a begin of actual turn happens.
* 回合开始
*/
@Override
public synchronized void beginTurn() {
// do nothing
}
/**
* This method is called *before* adding an action by RPScheduler so you can
* choose not to allow the action to be added by returning false
* 该方法在将action加入调度器之前被调用,这样可以选择是否允许该action加入(返回false则不加入)
*
* @param caster the object that casted the action
* @param action the action that is going to be added.
* @param actionList the actions that this player already owns.
* @return true if we approve the action to be added.
*/
@Override
public boolean onActionAdd(RPObject caster, RPAction action, List<RPAction> actionList) {
// accept all actions
return true;
}
/**
* Notify it when a end of actual turn happens.
* 回合结束
*/
@Override
public synchronized void endTurn() {
// do nothing
}
/**
* Execute an action in the name of a player.
* 以玩家的名义执行action
*
* @param caster the object that executes
* @param action the action to execute
*/
@Override
public void execute(RPObject caster, RPAction action) {
logger.info(caster + " executed action " + action);
}
/**
* Creates an account for the game
* 为游戏创建账号
*
* @param username the username who is going to be added.
* @param password the password for our username.
* @param email the email of the player for notifications or password
* reminders.
* @return the Result of creating the account.
*/
@Override
public AccountResult createAccount(String username, String password, String email) {
TransactionPool transactionPool = TransactionPool.get();
DBTransaction trans = transactionPool.beginWork();
AccountDAO accountDAO = DAORegister.get().get(AccountDAO.class);
try {
if (accountDAO.hasPlayer(trans, username)) {
return new AccountResult(Result.FAILED_PLAYER_EXISTS, username);
}
accountDAO.addPlayer(trans, username, Hash.hash(password), email);
transactionPool.commit(trans);
return new AccountResult(Result.OK_CREATED, username);
} catch (SQLException e) {
logger.error(e, e);
transactionPool.rollback(trans);
return new AccountResult(Result.FAILED_EXCEPTION, username);
}
}
/**
* Creates an new character for an account already logged into the game
* 为已经登录游戏的账号创建一个新角色
*
* @param username the username who owns the account of the character to be added.
* @param character the character to create
* @param template the desired values of the avatar representing the character.
* @return the Result of creating the character.
*/
@Override
public CharacterResult createCharacter(String username, String character, RPObject template) {
TransactionPool transactionPool = TransactionPool.get();
DBTransaction trans = transactionPool.beginWork();
CharacterDAO characterDAO = DAORegister.get().get(CharacterDAO.class);
try {
if (characterDAO.hasCharacter(trans, username, character)) {
return new CharacterResult(Result.FAILED_CHARACTER_EXISTS, character, template);
}
RPObject object = createCharacterObject(username, character, template);
IRPZone zone = RPWorld.get().getDefaultZone();
zone.assignRPObjectID(object);
characterDAO.addCharacter(trans, username, character, object);
transactionPool.commit(trans);
return new CharacterResult(Result.OK_CREATED, character, object);
} catch (IOException | SQLException e) {
logger.error(e, e);
transactionPool.rollback(trans);
return new CharacterResult(Result.FAILED_EXCEPTION, character, template);
}
}
/**
* Creates an new character object that will used by createCharacter
* 创建一个新的角色对象供createCharacter使用
*
* @param username the username who owns the account of the character to be
* added.
* @param character the character to create
* @param template the desired values of the avatar representing the
* character.
* @return RPObject
*/
@SuppressWarnings("unused")
protected RPObject createCharacterObject(String username, String character, RPObject template) {
RPObject object = new RPObject(template);
object.put("name", character);
return object;
}
/**
* gets the content type for the requested resource
* 获得请求资源的Content-Type
*
* @param resource name of resource
* @return mime content/type or <code>null</code>
*/
@Override
public String getMimeTypeForResource(String resource) {
if (resource.endsWith(".tmx")) {
return "text/xml";
} else if (resource.endsWith(".tmx")) { // bug: tmx may be ogg
return "audio/ogg";
} else if (resource.endsWith(".png")) {
return "image/png";
} else if (resource.endsWith(".js")) {
return "text/javascript";
} else if (resource.endsWith(".css")) {
return "text/css";
}
return null;
}
/**
* gets an input stream to the requested resource
*
* @param resource name of resource
* @return InputStream or <code>null</code>
*/
@Override
public InputStream getResource(String resource) {
return null;
}
}
在server.ini文件中有如下配置项
# World and RP configuration. Don't edit.
world=com.acuigame.pacman.server.core.engine.PacmanRPWorld
ruleprocessor=com.acuigame.pacman.server.core.engine.PacmanRPRuleProcessor
说明RPWorld也是只有一个实例,可以实现为单例(但不像RPRuleProcessor要求强制实现)。
关于RPObject和RPClass
RPObject共有6个构造函数
package marauroa.common.game;
public class RPObject extends SlotOwner {
/**
* Constructor1
*/
public RPObject() {
super(RPClass.getBaseRPObjectDefault());
clear();
}
/**
* Constructor2
*
* @param rpclass of this object
*/
public RPObject(RPClass rpclass) {
super(rpclass);
clear();
}
/**
* Constructor3
*
* @param rpclass of this object
*/
public RPObject(String rpclass) {
super(RPClass.getRPClass(rpclass));
clear();
}
/**
* Constructor4
*
* @param initialize initialize attributes
*/
RPObject(boolean initialize) {
super(RPClass.getBaseRPObjectDefault());
if (initialize) {
clear();
}
}
/**
* Copy constructor5
*
* @param object the object that is going to be copied.
*/
public RPObject(RPObject object) {
this();
fill(object);
}
/**
* Constructor6
*
* @param id the id of the object
*/
RPObject(ID id) {
this();
setID(id);
}
}
其中4号和6号构造函数是package访问权限,外部无法调用
5号构造函数是拷贝构造函数,用于将一个对象直接赋值给本对象
2号和3号构造函数本质上都是通过指定一个RPClass实例化对象
1号是无参构造函数,不推荐使用,使用一个默认的RPClass实例化,会增加带宽,不享受Marauroa框架带来的好处。
因此实际使用的构造函数包括2号、3号和5号。
RPObject的强制属性包括id(Type.INT),type(Type.STRING)和zoneid(Type.STRING)
id是Object的唯一标识,zoneid是对象所在区域的标识,type是对象类的类(的名字),因此您可以共享该类的所有实例的属性。
id在包含该对象的区域内唯一。
设计RPClass这个类背后的想法是,不定义OOP语言的成员,而是使用短数字来代替,达到节省带宽的目的。
每次创建一个新的RPClass,只要给了它一个名字(例如:entity,player…),它就会被加入到系统class列表中。注意在例子中属性默认时可见的,隐藏的属性必须单独指定。
当定义RPClass时,可以跳过id和type属性,这两个属性会由RPClass自动填充。 可以不使用RPClass,不是用RPClass会增加网络带宽。
RPClass可以继承自另一个RPClass
RPObject对象作为基类的保护成员变量,需要在子类中调用public void setRPClass(String rpclass)
重新设置对象的RPClass。
public class Dot {
protected RPObject myObject;
}
public class Superdot extends Dot {
}
2019-04-22
游戏UI=一张漂亮的背景图片+菜单+动态效果,首先关注功能的实现,其次再着重界面的设计。
客户端通过PerceptionDispatcher类对感知监听器进行分派,这样可以在若干个客户端UI界面进行监听。
2019-05-02
服务器端的Entity类从RPObject继承,有两个构造函数,一个是无参构造函数,一个以RPObject作为参数的构造函数
/**
* 构造函数
* @param object The template object.
* 模板对象
*/
public Entity(final RPObject object) {
super(object);
if (!has("x")) {
put("x", 0);
}
if (!has("y")) {
put("y", 0);
}
if (!has("width")) {
put("width", 1);
}
if (!has("height")) {
put("height", 1);
}
if (!has("resistance")) {
put("resistance", 100);
}
if (!has("visibility")) {
put("visibility", 100);
}
update();
}
// 无参构造函数
public Entity() {
// 1 横纵坐标
put("x", 0);
put("y", 0);
x = 0;
y = 0;
// 2 大小
setSize(1, 1);
area.setRect(x, y, 1, 1);
// 3 抗性及可见性
setResistance(100);
setVisibility(100);
}
客户端的Entity对象,有一个RPObject对象的成员变量和一个无参构造函数:
public Entity() {
clazz = null;
name = null;
subclazz = null;
title = null;
type = null;
x = 0.0;
y = 0.0;
}
其initialize方法用于将服务器传送过来的RPObject对象转换为Entity实例:
/* (non-Javadoc)
* @see games.stendhal.client.entity.IEntity#initialize(marauroa.common.game.RPObject)
*/
@Override
public void initialize(final RPObject object) {
rpObject = object;
/*
* Class
*/
if (object.has("class")) {
clazz = object.get("class");
} else {
clazz = null;
}
/*
* Name
*/
if (object.has("name")) {
name = object.get("name");
} else {
name = null;
}
/*
* Sub-Class
*/
if (object.has("subclass")) {
subclazz = object.get("subclass");
} else {
subclazz = null;
}
/*
* Size
*/
if (object.has("height")) {
height = object.getDouble("height");
} else {
height = 1.0;
}
if (object.has("width")) {
width = object.getDouble("width");
} else {
width = 1.0;
}
/*
* Title
*/
if (object.has("title")) {
title = object.get("title");
} else {
title = null;
}
/*
* Resistance
*/
if (object.has("resistance")) {
resistance = object.getInt("resistance");
} else {
resistance = 0;
}
/*
* Visibility
*/
if (object.has("visibility")) {
visibility = object.getInt("visibility");
} else {
visibility = 100;
}
/*
* Coordinates
*/
if (object.has("x")) {
x = object.getInt("x");
} else {
x = 0.0;
}
if (object.has("y")) {
y = object.getInt("y");
} else {
y = 0.0;
}
/*
* Notify placement
*/
onPosition(x, y);
/*
* Type
*/
type = object.getRPClass().getName();
inAdd = true;
onChangedAdded(new RPObject(), object);
inAdd = false;
}
stendhal的客户端实现中使用了大量的监听器,是因为其客户端实现是基于swing的窗口系统,这与专门为游戏而生的游戏引擎有很大不同,其需要监听器这种机制来监听对象属性的变化。而对游戏引擎来说,是一个更新-渲染的循环过程,因此不需要这些监听器。
2019-05-05
Stendhal是一个比较完整的mmorpg的实现,包括了mmorpg的常见功能:
- 任务系统
- 副本(raid)
- 成就系统
- 交易系统(玩家和NPC的交易,玩家之间的交易)
- 保护区和监狱
- 装备系统
- 聊天系统
- 教程
- GM
- 人物外观自定义(捏脸)
- 简单的宠物
- 其他。。。
对于一个小游戏来说,这些功能有些并不需要,有些只需要其中的部分,有些则完全不可少。现在的思路是以最新的1.29版为基础,按照增量添加的原则,生成一个基础项目作为项目模板,包含大多数游戏都需要的基础框架代码(直接使用Marauroa的话,相当于完全重头做起,需要耗费大量的时间,而Stendhal基本上是唯一的基于Marauroa可参考的项目,且代码质量有保证),这些基础代码的原则是:
- 足够基础(大多数游戏都能用的到,例如一整套Entity结构)
- 能够完全理解(stendhal源码中目前有一些还不能理解的暂缓加入)
- 起一个通用的与具体项目无关的名称,在做具体项目时减少代码修改量,例如simple-marauroa或acuigame-marauroa
- 一边增加基础代码,一边验证,增加对Marauroa框架的理解
- 非基础功能可以在需要的时候参考stendhal增加
- 客户端使用libgdx实现,以支持手机(Android、iOS)
20190510
Stendhal地图设计结构:
- 最顶层是一个zones.xml配置文件,包含了所有的Region(地区)
- 第二层是Region(地区),包含了该Region(地区)下的所有Zone(区域)
- 第三层是Zone(区域),也是地图的基本单位,对应一个单独的tmx文件;区域可以分为室内和室外
- 第四层是每个区域的配置,传送门和实体,每一项都可以包括0个或多个
这种多层结构对Stendhal这种大型的mmorpg来说是必要的,但对于一般的游戏来说,有些过于复杂了,可以进行适当简化,去掉第二层:
- 去掉室内和室外interior
- 去掉层级level
- 去掉危险等级danger level
- ...
20190516
再次改变一下思路:游戏类型多种多样,想要一劳永逸做出一个适用于所有游戏类型的大一统模板这个想法还是有点坑(可能是能力还不够),有些很简单的小游戏根本就不需要那么多的功能;而Stendhal毕竟是针对mmorpg写的,对其他类型的游戏很多功能都不适用。所以在读源码的基础上,从小项目着手,边读源码边实践,逐渐加深理解,提高能力。每次项目从头搭建就好,后续需要的功能再参考stendhal。
20190517
关于用户登录时的默认区域:
/**
* gets the default zone.
*
* @return default zone
*/
public IRPZone getDefaultZone() {
if (defaultZone != null) {
return defaultZone;
}
if (zones.isEmpty()) {
IRPZone zone = new MarauroaRPZone("lobby");
addRPZone(zone);
}
return zones.values().iterator().next();
}
/**
* sets the default zone
*
* @param defaultZone default zone
*/
public void setDefaultZone(IRPZone defaultZone) {
this.defaultZone = defaultZone;
}
如果已经通过setDefaultZone设置过默认区域,则用户登录后所在区域就是这个默认区域;否则若区域列表zones为空,则增加一个id为lobby的区域(一般不会出现这种情况),此时遍历zones,将其中第一个区域作为默认区域返回(由于map结构的相对无序性,具体哪一个区域是第一个与添加顺序无关)。
2019-05-23
先努力实现一个游戏大厅吧!!!
@see http://acuilab.com/articles/2019/05/23/1558598079893.html
2019-05-24
Morauroa只是按照一个游戏来设计的,要支持多个游戏需要修改数据库相关代码和数据库表结构。
首先所有游戏服务器都访问同一个Morauroa数据库。算了,感觉还是有些问题无法很好的处理, 这个问题以后再考虑吧。