Stendhal的相关原理与知识解析
Stendhal的项目总体介绍
Stendhal 是一款以Marauroa为框架/引擎,基于 Internet 名为Stendhal的多人在线冒险游戏。其主要功能包括:
- 任务系统
- 副本(raid)
- 成就系统
- 交易系统(玩家和NPC的交易,玩家之间的交易)
- 保护区和监狱
- 装备系统
- 聊天系统
- 教程
- GM
- 人物外观自定义(捏脸)
- 简单的宠物
- 其他功能
Stendhal的实现原理
Stendhal的地图结构设计
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZLd2ztfQ-1655653343445)(stendhal_img/stendhal_map.png)]
对于Stendhal项目地图设计的解析:
- 最顶层是一个zones.xml配置文件,包含了所有的Region(地区)
- 第二层是Region(地区),包含了该Region(地区)下的所有Zone(区域)
- 第三层是Zone(区域),也是地图的基本单位,对应一个单独的tmx文件;区域可以分为室内和室外
- 第四层是每个区域的配置,传送门和实体,每一项都可以包括0个或多个
Stendhal的程序开发过程中引擎主要是利用的Marauroa引擎为server层的框架。
Marauroa 用来管理客户端服务器通信,并为游戏开发人员提供面向对象的世界视图。它进一步以透明的方式处理数据库访问,以存储玩家帐户、角色进度和世界状态。
Marauroa引擎原理
Marauroa 是一个开源的多人在线游戏框架和引擎,用于开发回合制和实时游戏。它提供了一种在可移植且强大的服务器架构上创建游戏的简单方法。该服务器采用 Java 编码,可以使用 Python 进行游戏描述,提供 MySQL 后端并使用 TCP 传输通道与数十名玩家进行通信。我们的参考客户端使用 Java 进行编码,以实现最大的可移植性并使用开放技术。
Stendhal的项目模块介绍
Server
1、Stendhal项目中的服务器层主要对应的是games.stendhal.server项目中包,games.stendhal.server 包中定义了stendhal游戏程序中的server层的代码设计。
games.stendhal.server中的包的详细作用简介
包名 | 对应包在服务器层所起的作用 |
---|---|
actions | 对玩家从客户端发送到服务器的命令进行反应处理 |
core | 主要存储负责stendhal项目的主要逻辑部分 |
entity | 定义游戏中与之交互的所有内容(玩家、怪物、物品、传送门等)的实体类 |
events | 将服务器接受到的信息进行处理后将事件发回stendhal客户端 |
extensions | 对于stendhal游戏项目的拓展,根据开发者不同而改变 |
maps | 对Stendhal项目中地图的所有互动内容(包括任务、NPC)信息的定义 |
script | 定义的项目中的服务器脚本,可以通过GM执行的其他脚本 |
uitl | 定义用于简化项目代码的方法 |
2、对于games.stendhal.server包的具体包进行一个简略的分析
1.games.stendhal.server.actions包
处理从客户端发送的命令。这包括可以在聊天行中输入的所有 / 命令,还包括四处移动、攻击生物等内容。
2.games.stendhal.server.core
core包主要包括对于 Stendhal 项目的基本逻辑设计以及程序算法等逻辑性设计。Stendhal项目启动后,对于数据的修改不会影响core包,core代码无需更改。
3.games.stendhal.server.entity
entity包主要是负责Stendhal项目中的实体类的保存与设置。下表为entity包中每个实体类的作用:
games.stendhal.server.entity中的包的详细作用简介
实体类名 | 实体类对应包在服务器层的数据结构 |
---|---|
items | 武器、金钱、卷轴、食物等物品的逻辑实体类 |
mapstuff | 对于地图信息的修改与管理,例如门户、胸部、标志、重生点等信息 |
npc | 对于游戏中nopc角色的的定义与代码实现 |
player | Stendhal程序中游戏用户的信息实体类 |
slots | 对于stendhal游戏项目的游戏道具等数据的实体类 |
spell | 对Stendhal项目中游戏玩家中数据信息的定义 |
4.games.stendhal.server.events
events包主要是在服务器层对于Stendhal项目产生的事件进行处理并将其由服务器端将事件发送到客户端。例如events包包含 RPEvent 的子类,这些子类由服务器发送到客户端,以通知他们有关私人消息等事件。
5.games.stendhal.server.maps
maps包主要负责服务器端对于Stendhal项目的地图信息相关文件的定义。其主要负责项目中地图信息的交互的逻辑管理。具体的实现依靠entity包中的实体类进行实现。
client层
games.stendhal.client中的包的详细作用简介
包名 | 对应包在服务器层所起的作用 |
---|---|
actions | 对玩家从客户端发送到服务器的命令进行反应处理(客户端触发的事件) |
entity | 定义游戏中与之交互的所有内容(玩家、怪物、物品、传送门等)的实体类 |
events | 将服务器接受到的信息进行处理后将事件发回stendhal客户端 |
gui | 用户界面,包括所有对话框、主游戏窗口和游戏实体的视图。 |
sound | 控制声音和音乐的数据信息 |
sprite | 处理世界对象和用户界面的图像 |
update | 定义软件自动更新程序 |
common层
Stendhal的server层优秀代码解析
1、对于Stendhal entity实体类层代码的解析
在stendhal中,Entity继承了RPObject,是所有实体类的基类。
1.服务器端的Entity类的构造
服务器端的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);
// 2 大小
x = 0;
y = 0;
// 3 抗性及可见性
setSize(1, 1);
area.setRect(x, y, 1, 1);
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;
}
if (object.has("walk_blocker")) {
walkBlocker = true;
}
/*
* 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;
}
2.Stendhal中客户端存在大量监听器的原因
stendhal的客户端实现中使用了大量的监听器,是因为其客户端实现是基于swing的窗口系统,这与专门为游戏而生的游戏引擎有很大不同,其需要监听器这种机制来监听对象属性的变化。而对游戏引擎来说,是一个更新-渲染的循环过程,因此不需要这些监听器。
2、对于服务器处理事件逻辑的优秀代码分析
以games.stendhal.server.actions.admin包中代码为例,项目将不同模块的功能逻辑分成对应的模块进行编写,最终通过CommandCenter类利用其 registerActions函数通过各个类中的函数register()对于各个功能逻辑的实现进行集成,实现代码如下:
private static void registerActions() {
AdministrationAction.registerActions();
AttackAction.register();
AwayAction.register();
BanAction.register();
BuddyAction.register();
CastSpellAction.register();
ChallengePlayerAction.register();
ChatAction.register();
ConditionalStopAction.register();
CStatusAction.register();
DisplaceAction.register();
DropAction.register();
EquipAction.register();
FaceAction.register();
ForsakeAction.register();
GroupManagementAction.register();
KnockAction.register();
LanguageAction.register();
ListProducersAction.register();
LookAction.register();
MoveAction.register();
MoveContinuousAction.register();
MoveToAction.register();
NameAction.register();
OutfitAction.register();
OwnAction.register();
ProgressStatusQueryAction.register();
PushAction.register();
QuestListAction.register();
RemoveDetailAction.register();
ReorderAction.register();
SentenceAction.register();
SetCombatKarmaAction.register();
StoreMessageAction.register();
StopAction.register();
TradeAction.register();
UseAction.register();
AutoWalkAction.register();
WhereAction.register();
WhoAction.register();
register("info", new InfoAction());
register("markscroll", new MarkScrollAction());
}
CommandCenter类中则通过Action监听ActionListener()来进行对于当前用户正在进行的操作进行而产生的Action进行监听,Action监听的逻辑代码实现如下:
private static ActionListener getAction(final RPAction action) {
if (action == null) {
return UNKNOWN_ACTION;
} else {
String type = action.getRPClass().getName();
if (type.equals("")) {
type = action.get("type");
}
return getAction(type);
}
}
private static ActionListener getAction(final String type) {
if (type == null) {
return UNKNOWN_ACTION;
}
ActionListener action = getActionsMap().get(type);
if (action == null) {
// Look up for close matches that can be suggested to the user.
List<String> suggestions = new ArrayList<String>();
for (String s : getActionsMap().keySet()) {
if (SimilarExprMatcher.isSimilar(type, s, 0.1)) {
suggestions.add(s);
}
}
if (suggestions.size() != 0) {
return new UnknownAction(suggestions);
}
return UNKNOWN_ACTION;
} else {
return action;
}
}
3、对于服务器处理事件后发回客户的优秀代码分析
该包中使用了大量单例模式,以games.stendhal.server.events.BestiaryEvent函数为例,该函数通过继承Marauroa引擎中common.game.RPEvent函数以及common.game.RPClass函数实现通过利用Marauroa引擎进行实现服务器端将事件发回客户端。实现代码如下:
games.stendhal.server.eventsbao中的各个功能的处理事件类都是通过注册logger实例来实现对于功能事件的接受与发送:
private static final Logger logger = Logger.getLogger(BestiaryEvent.class);
事件中通过创建rpclass类实现对于实体类的引用:
/**
* Creates the rpclass.
* 创建rpclass。
*/
public static void generateRPClass() {
try {
final RPClass rpclass = new RPClass(BESTIARY);
rpclass.addAttribute("enemies", Type.VERY_LONG_STRING, Definition.PRIVATE);
} catch (final SyntaxException e) {
logger.error("cannot generateRPClass", e);
}
}
BestiaryEvent类中设置了不同参数的数据的实现:
BestiaryEvent类的构建中只有玩家信息
public BestiaryEvent(final Player player) {
this(player, true, true);
}
BestiaryEvent类的构建中有玩家信息、以及对于怪物情况信息。并根据实际信息进行处理。
public BestiaryEvent(final Player player, final boolean includeRare, final boolean includeAbnormal) {
super(BESTIARY);
// lists of enemies to be shown in bestiary
//怪物书籍中列出的敌人名单
standardEnemies = new ArrayList<>();
rareEnemies = new ArrayList<>();
abnormalEnemies = new ArrayList<>();
soloKills = new ArrayList<String>();
sharedKills = new ArrayList<String>();
final StringBuilder formatted = new StringBuilder();
if (player.hasSlot("!kills")) {
String killString = player.getSlot("!kills").getFirst().toAttributeString();
final char firstChar = killString.charAt(0);
final char lastChar = killString.charAt(killString.length() - 1);
// remove leading & trailing brackets
//移除前尾括号
if (firstChar == '[') {
killString = killString.substring(1);
}
if (lastChar == ']') {
killString = killString.substring(0, killString.length() - 1);
}
final EntityManager em = SingletonRepository.getEntityManager();
for (String k: killString.split("\\]\\[")) {
boolean shared = false;
if (k.startsWith("solo.")) {
k = k.replace("solo.", "");
} else if (k.startsWith("shared.")) {
shared = true;
k = k.replace("shared.", "");
}
if (!k.contains("=")) {
logger.warn("Invalid !kill format: " + k);
continue;
}
final String[] count = k.split("=");
try {
if (Integer.parseInt(count[1]) > 0) {
if (shared) {
sharedKills.add(count[0]);
} else {
soloKills.add(count[0]);
}
}
} catch (final NumberFormatException e) {
logger.warn("Kill count value for creature \"" + count[1] + "\" not numeric");
}
}
// place rare & abnormal enemies in separate lists
//将罕见和异常的敌人放在单独的列表中
for (final Creature e: em.getCreatures()) {
if (e.isRare()) {
rareEnemies.add(e);
} else if (e.isAbnormal()) {
abnormalEnemies.add(e);
} else {
standardEnemies.add(e);
}
}
// sort alphabetically
//按字母顺序排序
final Comparator<Creature> sorter = new Comparator<Creature>() {
@Override
public int compare(final Creature c1, final Creature c2) {
return (c1.getName().toLowerCase().compareTo(c2.getName().toLowerCase()));
}
};
Collections.sort(standardEnemies, sorter);
Collections.sort(rareEnemies, sorter);
Collections.sort(abnormalEnemies, sorter);
formatted.append(getFormattedString(standardEnemies));
if (includeRare) {
formatted.append(";" + getFormattedString(rareEnemies));
}
if (includeAbnormal) {
formatted.append(";" + getFormattedString(abnormalEnemies));
}
}
put("enemies", formatted.toString());
}
对于事件处理完成后由服务器端发回客户端时采用了将结果转化成一个字符串,并以字符串的形式将事件发回给客户端。
private String getFormattedString(final List<Creature> enemies) {
final boolean rare = enemies.equals(rareEnemies);
final boolean abnormal = enemies.equals(abnormalEnemies);
final StringBuilder sb = new StringBuilder();
final int creatureCount = enemies.size();
int idx = 0;
for (final Creature enemy: enemies) {
String name = enemy.getName();
Boolean solo = false;
Boolean shared = false;
if (soloKills.contains(name)) {
solo = true;
}
if (sharedKills.contains(name)) {
shared = true;
}
// hide the names of creatures not killed by player
//隐藏呗玩家杀死的怪物名称
if (!solo && !shared) {
name = "???";
}
if (rare) {
name += " (rare)";
} else if (abnormal) {
name += " (abnormal)";
}
sb.append(name + "," + solo.toString() + "," + shared.toString());
if (idx != creatureCount - 1) {
sb.append(";");
}
idx++;
}
return sb.toString();
}
对于Stendhal继承Marauroa引擎中RPrule、RPword等函数的实现
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分析
- 设计RPClass这个类背后的想法是,不定义OOP语言的成员,而是使用短数字来代替,达到节省带宽的目的。
- 每次创建一个新的RPClass,只要给了它一个名字(例如:entity,player…),它就会被加入到系统class列表中。注意在例子中属性默认时可见的,隐藏的属性必须单独指定。
- 当定义RPClass时,可以跳过id和type属性,这两个属性会由RPClass自动填充。 可以不使用RPClass,不是用RPClass会增加网络带宽。
RPClass的代码实现
RPObject对象作为基类的保护成员变量,需要在子类中调用public void setRPClass(String rpclass)重新设置对象的RPClass。
public class Dot {
protected RPObject myObject;
}
public class Superdot extends Dot {
}
Stendhal使用Marauroa引擎的基础原理分析
- 模型类用于保存数据,被服务端和客户端公用,包括一个RPObject对象及模型类自身的属性,同时要定义一个与其RPObject对象对应的RPClass。
- 模型类包括两个重载的构造函数,其中一个构造函数以模型类自身属性为参数,另一个构造函数以一个RPObject作为参数。
- RPObject对象是模型类自身属性的网络化表示,用于网络传输和对象重建。
- 客户端向服务端发送RPAction,通过感知监听器接收服务端发送的感知。
服务器端在每回合处理过程中,如果对某对象进行了修改,要在最后(对象最后一次被修改之后)调用RPWorld.modify(RPObject object)以通知区域对象被改变:
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());
}
}
服务器是基于回合来驱动的。回合起点在IRPRuleProcessor.beginTurn(),回合终点在IRPRuleProcessor.endTurn()
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();
}
在RPWorld的子类中实现beginTrun方法,并依次调用所属区域中的beginTurn方法,并且在区域的实现类中,包含了区域所有的变量(在区域的),并实现区域的beginTurn方法:
/**
* 开始回合
*/
public void beginTurn() {
// 遍历本世界所有的区域,并开始回合
for (IRPZone RPzone : this) {
MaPacmanZone zone = (MaPacmanZone) RPzone;
zone.beginTurn();
}
}
在IRPRuleProcessor的实现类中,重载其beginTurn方法,并调用world.beginTurn()方法,以便以此调用区域的beginTurn()方法,以此推动服务端不断向前移动:
public void beginTurn() {
// ...
world.beginTurn();
}
设计RPClass这个类想法是,不定义OOP语言的成员,而是使用短数字来代替,达到节省带宽的目的。
RPClass是marauroa的关键概念。它定义了属性的类型(字符串,整数,布尔型…)和可见性(隐藏、私有或可见)。而属性用来构造对象(对象是属性的集合,例如对于人来说,年龄、高度就是他的属性)。
对于Stendhal的server解析与总结
Stendhal的server层主要是对于Stendhal游戏程序的服务器端的程序的编写。Stendhal层主要包括实体类层的实现、客户端与服务端事件的定义以及对于客户端的请求进行处理并对事件进行处理、对于游戏数据信息的定义以及脚本的编写。其主要负责的是启动Stendhal程序以及对于Stendhal游戏项目中的使用过程中产生的事件进行处理。