目录

我的学习分享

记录精彩的程序人生

X

acuigame-snake

20200410

生成项目模板

  • 执行gdx-setup.jar
    image.png
  • 增加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

image.png

image.png

服务端驱动过程

  • 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 | 统计信息 | 每分钟记录统计信息。 这包括网络流量数据和在线玩家数量。 |

imageView2

  • account表支持多个项目的登录,如果其中账号被禁用的话,那么所有项目都被禁用
  • characters表,只能是所有项目共用同一账号下的所有角色(或者可以用项目id作为角色名的一部分来区分不同项目下的角色)
  • rpobject是跟角色关联的,如果能区分不同项目的角色,那么rpobject可以很自然地区分
  • rpzone是单独一张表,与其他表没有任何关联,可以用该项目id作为zoneid的一部分来区分不同项目下的区域。
  • statistics和gameevents,都是单独一张表,那么只能存储所有项目的统计信息和游戏事件了,gameevents貌似可以用source字段来区分。
  • loginEvent和passwordchange与account相关联。由于账号是所有项目共用的,因此也不能区分。

总之,多个项目共用一个morauroa是可行的,但是会有一些副作用(毕竟morauroa是为单一项目设计的,没有特别考虑共用的情况)。
对于需要持久化的游戏,可以在characters中以项目id来区分(返回给客户端的是某个账号下的所有角色,但客户端可以根据角色名过滤掉其他项目的角色,只显示本项目的角色给用户选择);对于区域可以类似处理(每个项目都会设置自己的默认区域,应该是用户登录时最先进入的那个区域)。
对于不需要持久化的简单游戏,不考虑角色,也没必要持久化rpzone、rpobject,貌似除了账号,完全没必要考虑数据库的设计了(当然上面提到的那些副作用还是有的)。

下一步任务:

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

20200420