玩过单机游戏的朋友,应该对金山游侠这个软件很熟悉把。当初我经常嫌刷怪升级非常辛苦,很多时候都是直接用金山游侠来修改游戏的经验或者等级内存,直接把角色调得很牛逼。

游戏开发也非常需要这些可以修改玩家数据的金手指。在游戏里,它有个更加专业的名称,叫GM(GameMaster)命令。

有了GM命令,我们就看好很方便让角色快速升级或者直接获取极品道具。当然,只要是数据,都可以有对应的GM命令,只要项目有需要。

下面,我们就来看一下GM系统的一种实现方式。

假设我们需要有一种指令,比如输入playerLv加一个数字表示要达到的等级,只要服务端受到这条指令,就直接将当前角色升到目标等级。不同的GM命令,指令的前缀和参数都是不同的,也就是说,每个指令都有自己固定的格式。在Java里,我们可以用正则表达式来定义这样的格式。

1.首先来看一些我们的gm命令抽象类,该类有几个作用,例如,定制具体的gm命令参数的格式,解析参数的方法,以及执行逻辑的方法。

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.kingston.game.database.user.player.Player;
import com.kingston.game.gm.message.ResGmResultMessage;

/**
 * 抽象gm命令
 * @author kingston
 */
public abstract class AbstractGmCommand {

	/**
	 * 正则表达式模式
	 * @return
	 */
	public abstract String getPattern();

	/**
	 * 帮助文档
	 * @return
	 */
	public abstract String help();

	/**
	 * 是否匹配
	 * @param pattern
	 * @param content
	 * @return
	 */
	public boolean isMatch(Pattern pattern, Matcher matcher, String content) {
		return matcher.matches();
	}

	/**
	 * 返回正则表达式解析的一系列参数
	 * @param matcher
	 * @param message
	 * @return
	 */
	public List<String> params(Matcher matcher, String message) {
		List<String> params = new ArrayList<>();
		for (int i=1; i<matcher.groupCount()+1; i++) {
			params.add(matcher.group(i));
		}
		return params;
	}

	/**
	 * 执行逻辑
	 * @param player
	 * @param params
	 * @return
	 */
	public abstract ResGmResultMessage execute(Player player, List<String> params);

}

2.定义一个gm命令的管理工具,负责缓存所有的gm命令。当收到玩家的gm指令后,如果判断当前玩家的IP地址或者角色有执行的权限,那么再通过枚举的方式,找到能够处理请求的具体gm命令。从而执行对应的逻辑。

public class GmManager {

	private static volatile GmManager instance;

	private GmManager() {}

	/** 缓存gm指令的模式与对应的逻辑处理者 */
	private Map<Pattern, AbstractGmCommand> commands = new HashMap<>();

	private final String SCAN_PATH = "com.kingston.game.gm.command";

	public static GmManager getInstance() {
		if (instance == null) {
			synchronized(GmManager.class) {
				if (instance == null) {
					instance = new GmManager();
					instance.init();
				}
			}
		}
		return instance;
	}

	private void init() {
		Set<Class<?>> clazzs = ClassScanner.getClasses(SCAN_PATH, new ClassFilter() {
			@Override
			public boolean accept(Class<?> clazz) {
				return AbstractGmCommand.class.isAssignableFrom(clazz)
						&& !Modifier.isAbstract(clazz.getModifiers());
			}
		});

		for (Class<?> clazz:clazzs) {
			try{
				AbstractGmCommand command = (AbstractGmCommand) clazz.newInstance();
				String regex = command.getPattern();
				commands.put(Pattern.compile(regex), command);
			}catch(Exception e) {
				e.printStackTrace();
			}
		}

	}

	/**
	 * 处理gm入口
	 * @param playerId
	 * @param content
	 * @return
	 */
	public void receiveCommand(long playerId, String content) {
		Player player = PlayerManager.getInstance().get(playerId);
		//判断权限
		if (!hasExecPower(player)) {
			return;
		}
		for (Map.Entry<Pattern, AbstractGmCommand> entry:commands.entrySet()) {
			Pattern pattern = entry.getKey();
			AbstractGmCommand command = entry.getValue();

			Matcher matcher = pattern.matcher(content);
			if (command.isMatch(pattern, matcher, content)) {
				List<String> params = command.params(matcher, content);
				ResGmResultMessage result =  command.execute(player, params);
				MessagePusher.pushMessage(playerId, result);
				return;
			}
		}

		ResGmResultMessage failedMessage = ResGmResultMessage.buildFailResult("找不到对应的gm命令");
		MessagePusher.pushMessage(playerId, failedMessage);
	}

	/**
	 * 是否有执行权限
	 * @param player
	 * @return
	 */
	private boolean hasExecPower(Player player) {
		//这里根据具体业务进行拦截
		return true;
	}

	public static void main(String[] args) {
		Pattern pattern = Pattern.compile("^reloadConfig\\s+([a-zA-Z_]+)");
		String expr = "reloadConfig CofingPlayer_Level";
		Matcher matcher = pattern.matcher(expr);
		if (matcher.matches()) {
			List<String> params = new ArrayList<>();
			for (int i=1; i<matcher.groupCount()+1; i++) {
				params.add(matcher.group(i));
			}
			System.err.println(params);
		}

	}

}

3.举个例子,上面的修改玩家等级的gm命令。格式为:playerLv [level],对应的正则表达式模式为

^playerLv\\s+(\\d+)

该命令的完整代码

/**
 * 修改玩家等级的gm
 * @author kingston
 */
public class GmPlayerLevelCommand extends AbstractGmCommand {

	@Override
	public String getPattern() {
		return "^playerLv\\s+(\\d+)";
	}

	@Override
	public String help() {
		return "修改玩家等级(playerLv [level])";
	}

	@Override
	public ResGmResultMessage execute(Player player, List<String> params) {
		int newLevel = Integer.parseInt(params.get(0));
		ConfigPlayerLevel configLevel = ConfigDatasPool.getInstance()
								.configPlayerLevelContainer.getConfigBy(newLevel);
		if (configLevel == null) {
			return ResGmResultMessage.buildFailResult("目标等级参数无效");
		}
		player.setLevel(newLevel);
		player.setUpdate();

		DbService.getInstance().add2Queue(player);
		return ResGmResultMessage.buildSuccResult("修改玩家等级成功");
	}

}

4.还有一个指令也很常见。比如游戏运行过程中,策划童鞋发现数值填错了(他们经常这么干)。我们不想重启服务但又希望内存里的策划配置能刷新。所以,我们需要有热更新配置的gm命令。回顾一下我们前一篇文章,关于配置数据库的设计。里面将策划的所有配置统一由ConfigDatasPool管理。如此,我们很容易通过反射的方式,动态实例化新的配置容器,读取新的配置并替换原有的container。

重载配置的gm指令格式为 

^reloadConfig [tableName]

对应的正则表达式为

^reloadConfig\\s+([a-zA-Z_]+)

完整代码如下:

/**
 * 修改配置表的gm
 * @author kingston
 */
public class GmReloadConfigCommand extends AbstractGmCommand {

	@Override
	public String getPattern() {
		return "^reloadConfig\\s+([a-zA-Z_]+)";
	}

	@Override
	public String help() {
		return "修改配置表(^reloadConfig [tableName])";
	}

	@Override
	public ResGmResultMessage execute(Player player, List<String> params) {
		String tableName = params.get(0);
		String containerName = tableName + "Container";
		try {
			Field field = ConfigDatasPool.class.getDeclaredField(containerName);
			Class<?> type = field.getType();
			Reloadable newContainer = (Reloadable) type.newInstance();
			newContainer.reload();
			field.set(ConfigDatasPool.getInstance(), newContainer);

			return ResGmResultMessage.buildSuccResult("重载["+tableName+"]表成功");
		} catch (Exception e) {
			e.printStackTrace();
		}
		return ResGmResultMessage.buildFailResult("找不到目标配置表");
	}

}

5.GM命令也属于玩家请求,那么也是需要对应的Controller,Message,RequestMapper。都是很简单的申明,这里就不贴代码了。


文章预告:下一篇主要介绍使用观察者模式实现事件驱动
该系列完整的代码请移步github ->>game_server

手游服务端框架之GM金手指的设计的更多相关文章

  1. Pomelo:网易开源基于 Node.js 的游戏服务端框架

    Pomelo:网易开源基于 Node.js 的游戏服务端框架 https://github.com/NetEase/pomelo/wiki/Home-in-Chinese

  2. XGoServer 一个基础性、模块完整且安全可靠的服务端框架

    作者:林冠宏 / 指尖下的幽灵 掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8 博客:http://www.cnblogs.com/linguan ...

  3. 用“MEAN”技术栈开发web应用(二)express搭建服务端框架

    上一篇我们讲了如何使用angular搭建起项目的前端框架,前端抽象出一个service层来向后端发送请求,后端则返回相应的json数据.本篇我们来介绍一下,如何在nodejs环境下利用express来 ...

  4. 手游client思考框架

    手游新公司新项目client我不太同意框架.虽然我也终于让步,当他居然问老板,使这个幼稚的行为而悔恨. 然而,就在最近我写了一些代码视图,我更坚定了自己的想法和思想.和思路不一定适合其它人,所以我并不 ...

  5. 基于 xorm 的服务端框架 XGoServer

    作者:林冠宏 / 指尖下的幽灵 掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8 博客:http://www.cnblogs.com/linguan ...

  6. Mina学习+手写服务端+通过telnet连接服务端

    1. 2. 3. 4.MinaServer.java package com.mina; import java.io.IOException;import java.net.InetSocketAd ...

  7. 我要多开梦幻手游PC端(梦幻手游PC端多开的简单分析及实现办法)(二)

    上一篇,多开方法,适用于一年前的版本 http://www.cnblogs.com/suanguade/p/5646776.html 前言: 一转眼一年过去了,日子越来越无聊了,于是,准备再玩一玩梦幻 ...

  8. 手游服务端框架之使用Guava构建缓存系统

    缓存的作用与应用场景 缓存,在项目中的应用非常之广泛.诸如这样的场景,某些对象计算或者获取的代码比较昂贵,并且在程序里你不止一次要用到这些对象,那么,你就应该使用缓存. 缓存跟java的Coucurr ...

  9. 用.Net打造一个移动客户端(Android/IOS)的服务端框架NHM(四)——Android端Http访问类(转)

    本章目的 在上一章中,我们利用Hibernate Tools完成了Android Model层的建立,依赖Hibernate Tools的强大功能,自动生成了Model层.在本章,我们将继续我们的项目 ...

随机推荐

  1. kali安装机场v2ray客户端

    为了方便查找资料,之前安装的ssr在kali上面,感觉速度不怎么快,相比windows和android上慢很多,所以打算在kali上面装个机场试试,看官方介绍说机场比ssr速度更快,下面是安装步骤: ...

  2. 20145103《JAVA程序设计》课程总结

    每周读书笔记链接汇总 假期笔记 http://www.cnblogs.com/20145103fwh/p/5248170.html 第一周读书笔记 http://www.cnblogs.com/201 ...

  3. unsigned short A = 10; printf("~A = %u\n", ~A); char c=128; printf("c=%d\n",c); 输出多少?

    这是题目给出的答案:第一题,-A =0xfffffff5,int值 为-11,但输出的是uint.所以输出4294967285 第二题,c=0x10,输出的是int,最高位为1,是负数,所以它的值就是 ...

  4. Python解析JSON数据的基本方法

    转自:http://www.jb51.net/article/73450.htm JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式.它基于ECMAScri ...

  5. mybatis关联配置(一对多配置)

    敲代码也有不少日子了,今天碰到个需求,就是定时器生成一张表,但是这个表的某些数据是从另外两张表中拿到的,定外两张表又是一对多的关系,想着咋在一个接口就能敲出来,大概结构如下 然后需要a表的数据(比如张 ...

  6. 使用Spring注解注入属性

    本文介绍了使用Spring注解注入属性的方法.使用注解以前,注入属性通过类以及配置文件来实现.现在,注入属性可以通过引入@Autowired注解,或者@Resource,@Qualifier,@Pos ...

  7. Windows窗体应用程序常用的几个类的属性、方法以及事件

    System.Diagnostics.Process 属性 public bool EnableRaisingEvents { get; set; }//获取或设置在进程终止时是否应激发 Exited ...

  8. servlet初始化参数

    使用<context-param>标签初始化的参数是被应用程序中所有的servlet所共享.但是有时候我们需要为某一个特定的servlet配置参数,这个时候我们就需要使用servlet初始 ...

  9. 阿里云centOS7.4 ftp连接不上的问题

    首先查看是开启21端口 选择ECS-->安全组-->配置规则 增加21端口配置 配置如下

  10. 最优化问题 Optimization Problems & 动态规划 Dynamic Programming

    2018-01-12 22:50:06 一.优化问题 优化问题用数学的角度来分析就是去求一个函数或者说方程的极大值或者极小值,通常这种优化问题是有约束条件的,所以也被称为约束优化问题. 约束优化问题( ...