acuigame-snake
20200410
生成项目模板
- 执行gdx-setup.jar

- 增加server子项目
服务器和客户端有部分代码共享,服务器和客户端的代码都放到core子项目中,而server子项目中放入服务器的启动代码,与desktop和android子项目(包含客户端的启动代码)类似。
首先打开项目所在目录D:\GitHub\acuigame-snake,将desktop目录拷贝一份并重命名为server目录
project(":server") {
apply plugin: "java-library"
dependencies {
implementation project(":core")
}
}
在settings.gradle中,增加server
include 'desktop', 'server', 'android', 'core'
将server子项目中的DesktopLauncher重命名为ServerLauncher作为服务端的启动类,同时删掉main方法中的代码,将包结构重构为 com.acuigame.pacman.server
package com.acuigame.snake.server;
public class DesktopLauncher {
public static void main (String[] arg) {
// TODO: 增加服务端的启动代码
}
}
修改server子项目下的build.gradle文件
// 修改前
project.ext.mainClassName = "com.acuigame.pacman.desktop.DesktopLauncher"
// 修改后
project.ext.mainClassName = "com.acuigame.pacman.server.ServerLauncher"
// 修改前
name = appName + "-desktop"
// 修改后
name = appName + "-server"
至此,几个子项目都可以正常编译执行。下面的工作是集成Marauroa相关lib。
在core目录下创建libs目录,并将Marauroa相关jar包拷入其中。
在build.gradle中,增加 compile fileTree(dir:"libs",include:["*.jar"]):
project(":core") {
apply plugin: "java-library"
dependencies {
implementation fileTree(dir:"libs",include:["*.jar"])
api "com.badlogicgames.gdx:gdx:$gdxVersion"
}
}
基本完成!!
20200411


服务端驱动过程
RPServerManager线程周期性循环调用IRPRuleProcessor.beginTurn()
RPRuleProcessor是服务器的大脑,它决定发生什么事情,何时发生以及对谁发生。
public class SnakeRPRuleProcessor implements IRPRuleProcessor {
/**
* 新的回合开始
* @see [https://img.hacpai.com/file/2020/04/image-32ef9d0f.png]
*/
@Override
public void beginTurn() {
SnakeRPWorld.get().beginTurn();
}
}
SnakeRPWorld类实现beginTurn()方法
SnakeRPWorld负责管所有的区域以及区域的创建
public class SnakeRPWorld extends RPWorld {
/**
* 回合开始
*/
public void beginTurn() {
// 遍历区域依次执行其beginTurn()方法
for (IRPZone RPzone : this) {
if(RPzone instanceof SnakeRPZone) {
SnakeRPZone zone = (SnakeRPZone) RPzone;
zone.beginTurn();
}
}
}
}
SnakeRPZone类实现beginTurn()方法
public class SnakeRPZone extends MarauroaRPZone {
/**
* 回合开始
*/
public void beginTurn() {
// TODO: 区域逻辑实现部分
}
}
玩家登录的服务端处理
marauroa.server.game.rp.IRPRuleProcessor的一个默认实现 marauroa.server.game.rp.RPRuleProcessorImpl
首先是玩家进入游戏的回调函数 IRPRuleProcessor.onInit()
/**
* a default implementation of RPRuleProcessor
* 一个默认的RPRuleProcessor的实现
*/
public class RPRuleProcessorImpl implements IRPRuleProcessor {
/**
* 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;
}
}
上面是默认实现,我们可以稍微改动一下
public class MaPacmanRPRuleProcessor implements IRPRuleProcessor {
// Callback method called when a new player enters in the game
// 新玩家进入游戏的回调方法
synchronized public boolean onInit(RPObject object) throws RPObjectInvalidException {
object.put("zoneid", "start");
if (object.has("text")) {
object.remove("text");
}
object.put("dir", consts.DIR_NONE);
object.put("power", 0);
object.put("nextdir", consts.DIR_NONE);
world.addPlayer(object);
world.modify(object);
return true;
}
}
下面是RPWorld中的addPlayer()实现,功能是遍历区域,并调用指定区域的addPlayer()实现
public class MaPacmanRPWorld extends RPWorld {
/**
* 增加一个玩家
* @param object
* @return
*/
public boolean addPlayer(RPObject object) {
// set GhostMoveInt, that Ghosts move 100turns after last player has left
// 设置幽灵移动回合数,最后一个玩家离开游戏后幽灵移动100回合
GhostMoveInt = 100;
// 遍历本世界所有的区域
for (IRPZone RPzone : this) {
MaPacmanZone zone = (MaPacmanZone) RPzone;
if (zone.getName().equals(object.get("zoneid"))) {
// 找到玩家所在区域
this.add(object); // 在世界中增加
zone.addPlayer(object); // 在区域中增加
object.put("type", consts.TYPE_PLAYER); // 设置RPObject类型字段为"玩家"
return Players.add(object); // 在玩家列表中增加RPObject并返回
}
}
return false;
}
}
下面是区域中的addPlayer()方法的实现
public class MaPacmanZone extends MarauroaRPZone {
public boolean addPlayer(RPObject object) {
boolean res = respawnMovable(object);
if (res) {
Players.add(object);
}
return res;
}
}
从上面的beginTurn()到这里的addPlayer()可以看出:
- IRPRuleProcessor的实现类是服务器端的分派中心
- 重要的业务逻辑要在各自的区域类(IRPZone)中实现,由IRPRuleProcessor通过RPWorld分派
- RPWorld只是区域的容器,区域之间相互独立,可以在RPWorld中保存整个游戏世界的全局属性或执行全局操作。
RPSlot
RPSlot是RPObject拥有的插槽,其他RPObject可以放入其中(如背包中的项目)
RPSlot适合存放RPObject中的对象属性;对于列表类的也适用,RPSlot有一个容量(capacity)属性,可以表示存储在该插槽中的对象数量,底层是用链表 java.util.LinkedList实现的
感知在服务器端的构建并发送到客户端是框架自动处理的;感知包含对客户端RPObject的操作,在客户端该操作也是框架自动处理的,在处理的过程中,可以注册监听器IPerceptionListener
/**
* The PerceptionHandler class is in charge of applying correctly the
* perceptions to the world. You should always use this class because it is a
* complex task that is easy to do in the wrong way.
* PerceptionHandler类负责向世界应用正确的感知。 你应该总是使用这个类,因为这是一个复杂的任务,很容易以错误的方式去做。
*/
public class PerceptionHandler {
/**
* Apply a perception to a world instance.
* 对一个世界实例应用感知
*
* @param message the perception message
* @param world_instance a map representing objects stored in a zone.
* @throws Exception
*/
public void apply(MessageS2CPerception message, Map<RPObject.ID, RPObject> world_instance) throws Exception {
listener.onPerceptionBegin(message.getPerceptionType(), message.getPerceptionTimestamp());
/*
* We want to clear previous delta^2 info in the objects.
* Delta^2 is only useful in server for getting changes done to the object.
* 我们要清除对象中以前的delta ^ 2信息。 Delta ^ 2仅在服务器中对完成对象更改有用。
*/
for (RPObject obj : world_instance.values()) {
obj.resetAddedAndDeleted();
}
/*
* When we get a sync perception, we set sync flag to true and clear the stored data to renew it.
* 当我们获得同步感知时,将sync标志设置为true并清除存储的数据以更新它。
*/
if (message.getPerceptionType() == Perception.SYNC) {
try {
/**
* OnSync: Keep processing
*/
previousPerceptions.clear();
applyPerceptionAddedRPObjects(message, world_instance);
applyPerceptionMyRPObject(message, world_instance);
if (!synced) {
synced = true;
listener.onSynced();
}
} catch (Exception e) {
listener.onException(e, message);
}
// Since we are using TCP, we don't have to check the order of perceptions anymore
// 由于我们使用的是TCP,因此我们不再需要检查感知的顺序
} else if (message.getPerceptionType() == Perception.DELTA) {
try {
/**
* OnSync: Keep processing
*/
applyPerceptionDeletedRPObjects(message, world_instance);
applyPerceptionModifiedRPObjects(message, world_instance);
applyPerceptionAddedRPObjects(message, world_instance);
applyPerceptionMyRPObject(message, world_instance);
} catch (Exception e) {
listener.onException(e, message);
}
/*
* In any other case, store the perception and check if it helps
* applying any of the still to be applied perceptions.
* 在任何其他情况下,请存储该感知并检查它是否有助于应用尚待应用的感知。
*/
} else {
previousPerceptions.add(message);
for (Iterator<MessageS2CPerception> it = previousPerceptions.iterator(); it.hasNext();) {
MessageS2CPerception previousmessage = it.next();
try {
/**
* OnSync: Keep processing
*/
applyPerceptionDeletedRPObjects(previousmessage, world_instance);
applyPerceptionModifiedRPObjects(previousmessage, world_instance);
applyPerceptionAddedRPObjects(previousmessage, world_instance);
applyPerceptionMyRPObject(previousmessage, world_instance);
} catch (Exception e) {
listener.onException(e, message);
}
it.remove();
}
/* If there are no preceptions that means we are synced */
// 如果没有感知,那就意味着我们已经同步
if (previousPerceptions.isEmpty()) {
synced = true;
listener.onSynced();
} else {
synced = false;
listener.onUnsynced();
}
}
/* Notify the listener that the perception is applied */
// 通知监听器已应用感知
listener.onPerceptionEnd(message.getPerceptionType(), message.getPerceptionTimestamp());
}
}
作为客户端的代表,marauroa.client.ClientFramework中有几个方法要熟练掌握
/**
* It is a wrapper over all the things that the client should do. You should extend this class at your game.
*
* @author miguel
*
*/
public abstract class ClientFramework {
/**
* Call this method to connect to server. This method just configure the connection, it doesn't send anything
* 调用此方法以连接到服务器。 此方法仅配置连接,不发送任何内容
*/
public void connect(String host, int port) throws IOException {
// ...
}
/**
* discovers a SOCKS proxy, ignores all other proxies
* 发现一个SOCKS代理,忽略所有其他代理
*/
private Proxy discoverProxy(String host, int port) throws IOException {
// ...
}
/**
* Call this method to connect to server using a proxy-server inbetween. This method just configure the connection, it doesn't send anything.
* 调用此方法以使用之间的代理服务器连接到服务器。 此方法仅配置连接,不发送任何内容。
*/
public void connect(Proxy proxy, InetSocketAddress serverAddress) throws IOException {
// ...
}
/**
* Login to server using the given username and password.
* 使用给定的用户名和密码登录服务器。
*/
public synchronized void login(String username, String password) throws InvalidVersionException, TimeoutException, LoginFailedException, BannedAddressException {
// ...
}
/**
* Login to server using the given username and password.
* 使用给定的用户名和密码登录服务器。
*/
public synchronized void login(String username, String password, String seed) throws InvalidVersionException, TimeoutException, LoginFailedException, BannedAddressException {
// ...
}
/**
* After login allows you to choose a character to play
* 登录后,您可以选择要扮演的角色
*/
public synchronized boolean chooseCharacter(String character) throws TimeoutException, InvalidVersionException, BannedAddressException {
// ...
}
/**
* Request server to create an account on server.
* 请求服务器创建一个帐户。
*/
public synchronized AccountResult createAccount(String username, String password, String email) throws TimeoutException, InvalidVersionException, BannedAddressException {
// ...
}
/**
* Request server to create a character on server. You must have successfully logged into server before invoking this method.
* 请求服务器创建一个角色。 调用此方法之前,您必须已成功登录服务器。
*/
public synchronized CharacterResult createCharacter(String character, RPObject template) throws TimeoutException, InvalidVersionException, BannedAddressException {
// ...
}
/**
* Sends a RPAction to server
* 将RPAction发送到服务器
*/
public void send(RPAction action) {
// ...
}
/**
* Request logout of server
* 请求注销服务器
*/
public synchronized boolean logout() throws InvalidVersionException, TimeoutException, BannedAddressException {
// ...
}
/**
* Disconnect the socket and finish the network communications.
* 断开套接字并完成网络通信。
*/
public void close() {
// ...
}
/**
* Call this method to get and apply messages
* 调用此方法获取并应用消息
*/
public synchronized boolean loop(@SuppressWarnings("unused") int delta) {
// ...
}
/**
* sends a KeepAliveMessage, this is automatically done in game, but you may be required to call this method very five minutes in pre game.
* 发送一个KeepAliveMessage,这会在游戏中自动完成,但是您可能需要在游戏前五分钟调用此方法。
*/
public void sendKeepAlive() {
// ...
}
/**
* Are we connected to the server?
* 我们连接到服务器了吗?
*/
public boolean getConnectionState() {
// ...
}
/**
* It is called when a perception arrives so you can choose how to apply the perception.
* 它在感知到达时被调用,因此您可以选择如何应用感知。
*/
abstract protected void onPerception(MessageS2CPerception message);
/**
* is called before a content transfer is started.
* 在内容传输开始之前调用。
*/
abstract protected List<TransferContent> onTransferREQ(List<TransferContent> items);
/**
* It is called when we get a transfer of content
* 当我们获得内容转移时称为
*/
abstract protected void onTransfer(List<TransferContent> items);
/**
* It is called when we get the list of characters
* 当我们得到角色列表时调用它
*/
abstract protected void onAvailableCharacters(String[] characters);
/**
* It is called when we get the list of characters
*
*/
protected void onAvailableCharacterDetails(@SuppressWarnings("unused") Map<String, RPObject> characters) {
// stub
}
/**
* It is called when we get the list of server information strings
*
*/
abstract protected void onServerInfo(String[] info);
/**
* Returns the name of the game that this client implements
*
*/
abstract protected String getGameName();
/**
* Returns the version number of the game
*
*/
abstract protected String getVersionNumber();
/**
* Call the client with a list of previous logins.
*/
abstract protected void onPreviousLogins(List<String> previousLogins);
}
20200413
发现morauroa不同项目之间可以共用同一个数据库了;目前moraroa数据库包含13张表
| col1 | col2 | col3 |
| - | - | - |
| account | 账号 | 身份验证信息存储在表帐户中。 它由用户名,密码哈希,电子邮件地址和帐户创建的时间戳组成。 |
| accountban | 账号禁用 | 不幸的是,有一些不友好的人,你可能需要远离。该表是基于每个帐户的。 尝试登录的人显示原因。 帐户禁令可以自动失效。 |
| accountlink | | |
| banlist | 地址禁用 | 不幸的是,有一些不友好的人,你可能需要远离。该表基于ip-address和ip-address-ranges的禁令。 掩码255.255.255.255表示禁用一个单独的ip-address。 |
| characters | 角色表 | |
| email | | |
| gameevents | 游戏事件 | 存储游戏世界中发生的事件(杀死怪物,交易,传送等)带有时间戳和引发事件的玩家。 参数param1和param2取决于事件。 |
| loginevent | 登录事件 | 记录每次登录(成功与否)。 |
| loginseed | | |
| passwordchange | | |
| rpobject | rp对象 | |
| rpzone | rp区域 | |
| statistics | 统计信息 | 每分钟记录统计信息。 这包括网络流量数据和在线玩家数量。 |

- account表支持多个项目的登录,如果其中账号被禁用的话,那么所有项目都被禁用
- characters表,只能是所有项目共用同一账号下的所有角色(或者可以用项目id作为角色名的一部分来区分不同项目下的角色)
- rpobject是跟角色关联的,如果能区分不同项目的角色,那么rpobject可以很自然地区分
- rpzone是单独一张表,与其他表没有任何关联,可以用该项目id作为zoneid的一部分来区分不同项目下的区域。
- statistics和gameevents,都是单独一张表,那么只能存储所有项目的统计信息和游戏事件了,gameevents貌似可以用source字段来区分。
- loginEvent和passwordchange与account相关联。由于账号是所有项目共用的,因此也不能区分。
总之,多个项目共用一个morauroa是可行的,但是会有一些副作用(毕竟morauroa是为单一项目设计的,没有特别考虑共用的情况)。
对于需要持久化的游戏,可以在characters中以项目id来区分(返回给客户端的是某个账号下的所有角色,但客户端可以根据角色名过滤掉其他项目的角色,只显示本项目的角色给用户选择);对于区域可以类似处理(每个项目都会设置自己的默认区域,应该是用户登录时最先进入的那个区域)。
对于不需要持久化的简单游戏,不考虑角色,也没必要持久化rpzone、rpobject,貌似除了账号,完全没必要考虑数据库的设计了(当然上面提到的那些副作用还是有的)。
下一步任务:
- LibGdx2DGameStarterTemplate客户端开发
- simple marauroa
https://github.com/javydreamercsw/simple-marauroa/blob/master/Simple+Marauroa+Parent/Simple+Marauroa/content/developer/start.md
RPEvents:服务器上发生的事件会通知客户端,以便客户端可以做出相应的反应。 对于所有需要了解事件的实体,将事件添加到RPClass生成代码中非常重要。 否则会出现运行时错误。
RPAction:通常,服务器中的RPEvent反应由一系列操作(可能是一个,很多或没有)和由于每个操作或一组操作而可能产生的新RPEvent组成。 可以使用客户端send(RPAction)方法从客户端自行调用RPAction。
20200417
去掉MTX中AbstractAssets抽象类,资源类用单例实现。
20200419
Table
Root table
在libgdx中,可以使用setFillParent方法轻松地将Root table的大小调整到舞台(但通常只应在Root table上使用)
Table table = new Table();
table.setFillParent(true);
stage.addActor(table);
Logical table
单元格构成一个逻辑表,但是它的大小不适合表窗口小部件,逻辑表默认位于表格的中心。
因此要让控件居中的简便方法是设置其所在table的fillParent属性为true。
MTX推荐的分辨率是960x540