路由数据格式 可以用来做有趣的事情

首先来解释一下什么是路由数据格式,这个呢,是一个非官方的描述词。就是是我感觉应该叫这个比较合适,但是并不明白各位大佬怎么叫这种数据格式。要用到这种数据格式的起因也比较简单,是我们的的游戏里边要有一个掉落系统。比如说,在某些场景下,某些东西死了或者被破坏掉了会按照某些规则掉落某些东西。当然路由还可以做其他很有意思的事情,我拿它来做的事情包含掉落总共有三个,不过其他两个优点类似一会一块说

先来说说,我们的掉落需求

需求的简单描述

掉落的规则,是这样的,我首先需要计算掉率比如说50%,那么就会按照这个掉率计算一下,看掉还是不掉;如果掉了,那么就计算数量,可能是一个范围,然后随机一下具体掉落几个;确定了个数,然后找到对应条件下的可掉落列表,然后从可掉落列表中筛选出具体都要掉落什么东西。这三个相关数据,都是相同的规则,就是通过其他条件获取的。其他条件的数据中间的可变化数据总共有五个,地图名称,对象类型,对象名称,对象品质,权重值。需要根据这五个具体传入的值来匹配最合适的那条数据。或许你觉得这个可以通过各种if else来实现,但是这种实现方式需求一旦添加或者变化的时候,会带来很大的问题,具体的问题我不做赘述能懂的人自然能动是什么问题。

需求的简单分析

其实已经说得比较明白了,但是没有接触过这个的看到这个东西可能是一脸懵逼,不知道痛点在什么地方。所以我还是再分析一下可能出现的情况就能把握痛点了。

  • 掉落数量的规则跟地图有关、对象名称
    • 这个数量可能跟关卡有关系,比如说前10关都是1-2个掉落;后面的10管2-3个掉落类似
    • 某个障碍物(弹药宝箱)固定掉落3-4个
    • 组合的关系。
      • 掉落数量可能跟地图、对象类型或对象名称或对象品质、权重值有关系。在某些具体的情况下可能会掉落不同的数量(比如某些游戏中的彩蛋)
  • 掉落概率可能跟对象类型、对象名称、对象品质有关
    • 掉落可能跟类型有关系,比如说,怪物这个类别可能掉率就是10%,比如障碍的掉率就是5%
    • 掉落可能跟对象名称有关,比如说某一个障碍物就是100%掉落的,比如说武器宝箱,弹药宝箱一类的
    • 掉落可能跟对象品质有关, 比如说普通怪物掉率是10%,精英怪物的掉率是50%,Boss掉率是100%
    • 组合关系
      • 掉落数量可能跟对象类型、对象品质有关。高级的障碍物或者精英级别的怪物可能有不同的掉落概率
  • 掉落筛选列表可能跟对象名称、对象品质、权重值有关系
    • 掉落对象可能跟对象名称有关系。武器宝箱会掉落某些武器而不会收到其他因素的影响
    • 掉落对象可能跟对象品质有关系。Boss可能会固定掉落2-3把武器,5-6个补给......
    • 掉落对象可能跟权重值有关系。在这个权重值下,可能会掉落这些东西,在其他权重值下可能会掉落其他的东西。
    • 组合关系
      • 对象名称,权重值。同一个对象可能在不同的权重下可能有不同掉落内容。
      • 对象品质,权重值。再一个品质的对象,在不同的权重下可能有不同的掉落。

简单点说就是可能会出现各种各样的组合关系,具体的掉落的所需的参数可能跟不同的参数都有关系,也可能都没有关系。比如说,什么都没有匹配到数据的话系统可能会提供一个默认的数据,比如说掉率的0,掉落数量的0,空的掉落对象列表。

我们来看看路由数据格式吧

可能你看到上边的需求并不能直接考虑到怎么处理才能达到这种效果,我们还是先来认识一下路由数据格式(当然这个名字是我随便取的,不要拿来直接跟别人说这个,别人应该听不懂)吧。这个数据格式呢,是我参照了ASP.NET中的路由表来做的。ASP的路由数据的添加是这样色的。

routes.MapRoute(
	name: "Default",
	url: "{controller}/{action}/{id}",
	defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);		

这一小段代码的意思其实非常简单,就是注册了一个路由数据,名称是Default,然后URL的格式是{controller}/{action}/{id},然后还附带了部分的默认数据,如果Url格式中需要的数据没有的时候,就会使用默认数据填充起来,当然真正的项目中不会只有一条路由规则,而是会有很多很多的路由规则,然后URL会自动进行最匹配的一条。通过一组Url然后定位到某个Controller中的的某个函数,这个就是这个路由的主要作用。其实这个场景跟我们要解决的问题还挺相似的,都是通过一组数据匹配到一个最合适的结果,然后使用这个结果完成后续的逻辑。所以让我们来整理一下路由数据格式的相关需求,然后来实现一个简单的路由格式的数据吧。

路由格式的需求分析

  • 通过某些参数来匹配最合适的一条数据
  • 数据要求有顺序,需要可以根据注册的顺序来匹配优先级最高的数据

其实上边这两个需求看上去真的很简单。总结起来就是,路由数据就是一个一个带有自己数据格式化规则与数据优先级的大字典。首先通过格式化功能部分讲原本的数据格式化内部可能需要的数据,然后在将格式化后的数据作为Key依次向后数据匹配,当匹配到第一条数据之后则作为结果返回。

让我们在Lua中实现一个路由数据的版本吧

不废话直接上代码,我的代码中写的已经比较明确了,每一个函数都是用来干什么的。懂Lua代码的应该非常容易就能看懂了,不懂Lua的仔细看看应该也能看懂毕竟都是C语系的都大差不差


local RouteData = class("RouteData")

function RouteData:ctor()
	self._provider = {}
	self._route_formats = {}
end

-- 插入路由格式
function RouteData:addRouteFormat(sformat, index)
	table.insert(self._route_formats, sformat, index)
end

-- 设置路由格式
function RouteData:setRouteFormat(sformat)
	self._route_formats = sformat
end

-- 获取路由格式
function RouteData:getRouteFormats()
	return self._route_formats
end

-- 根据之前注入的路由规则获取路由名称列表
function RouteData:getRouteNames(change_list)
	local namelist = {}

	for _,v in ipairs(self._route_formats) do
		local name = v
		for _, change in ipairs(change_list) do
			if change.Befor and change.Change then
				name = string.gsub(name, change.Befor, change.Change)
			end
		end

		table.insert(namelist, name)
	end

	return namelist
end

function RouteData:setData(name, value)
	self._provider[name] = value
end

-- 查找数据
function RouteData:findValue(change_list)
	local namelist = self:getRouteNames(change_list)

	local value = nil

	for _, name in ipairs(namelist) do
		local v = self._provider[name]
		if v then
			value = v
			break
		end
	end

	return value
end

kunpo.RouteData = kunpo.RouteData or RouteData

return RouteData

然后回到我们的掉落需求上来

之前已经说过了需求,跟路由,现在需要做的就是怎么样才能结合路由的代码来实现我们的掉落需求了。当然了,我所说的路由格式数据并不是指像是像是ASP那种注册和使用一样的数据,而是符合我前边提到的总结的格式化数据模式的数据格式。所以在使用的时候也跟ASP的那种数据有很大的不同。

首先是根据业务需求准备格式化数据,下面这些事我们项目中使用的格式化数据。

	-- 注入查找顺序
	local Obstacle_routeformat =
	{
		"MapName::ObjType::ObjName::Default::Default",

		"Default::ObjType::ObjName::Default::TC",
		"MapName::ObjType::Default::Ability::TC",
		"MapName::Default::ObjName::Ability::TC",

		"MapName::ObjType::Default::Default::TC",
		"MapName::Default::ObjName::Default::TC",

		"Default::ObjType::ObjName::Default::Default",

		"MapName::ObjType::Default::Ability::Default",
		"MapName::Default::ObjName::Ability::Default",
		"Default::ObjType::Default::Ability::TC",
		"Default::Default::ObjName::Ability::TC",
		"MapName::Default::Default::Ability::TC",

		"MapName::ObjType::Default::Default::Default",
		"MapName::Default::ObjName::Default::Default",
		"Default::Default::ObjName::Ability::Default",
		"Default::ObjType::Default::Ability::Default",
		"MapName::Default::Default::Ability::Default",

		"Default::Default::ObjName::Default::Default",
		"Default::ObjType::Default::Default::Default",
		"Default::Default::Default::Ability::Default",
		"Default::Default::Default::Default::TC",
		"MapName::Default::Default::Default::Default",

		-- 基础默认值

		"Default::Default::Default::Default::Default",
	}

然后将这些数据注入到某一个路由中。当然也少不了部分代码初始化的数据。因为所有的数据一般都得有个初始化默认值。当然了这些默认数据最后都会被覆盖掉,因为我们真正的数据都是走策划提供的数值表。那个地方跟业务耦合的太紧密了。代码贴上来也没有什么用。

	-- 注入实际数据
	local default_data =
	{
		Bullet =
		{
			Probability =
			{
				["Default::Enemy::Default::Common::Default"] = 0.2,					-- 普通怪物在任何场景下的掉落几率为0.2(从关卡表获取)
				["Default::Enemy::Default::Boss::Default"] = 1,
				["Default::Enemy::Default::Npc::Default"] = 1,
				["Default::Obstacle::ComprehensiveBox::Default::Default"] = 1,
			},
			Count =
			{
				-- ["Default::Enemy::Default::Common::Default"] = 1,
				["Default::Enemy::xianjing_monster6_1::Common::Default"] = 2,
				["Default::Enemy::Default::Boss::Default"] = {3, 5},
				["Default::Obstacle::ComprehensiveBox::Default::Default"] = {4, 6},
			},
			DataPool =
			{
				["Default::Default::Default::Default::Default"] = default_bullet_pool,
			},
		},
		Weapon =
		{
			Probability =
			{
				["Default::Enemy::Default::Boss::Default"] = 1,
				["Default::Obstacle::ComprehensiveBox::Default::Default"] = 1,
			},
			Count =
			{
				["Default::Enemy::Default::Boss::Default"] = {1, 2},
				["Default::Obstacle::ComprehensiveBox::Default::Default"] = 1,
			},
			DataPool =
			{

			},
		},
		Skill =
		{
			Probability =
			{
				["Default::Obstacle::ComprehensiveBox::Default::Default"] = 1,
			},
			Count =
			{
				["Default::Obstacle::ComprehensiveBox::Default::Default"] = 1,
			},
			DataPool =
			{

			},
		},
		-- Else =
		-- {
		-- 	Probability =
		-- 	{
		-- 		["Default::Obstacle::Default::Default::Default"] = 1,
		-- 	},
		-- 	Count =
		-- 	{
		-- 		["Default::Obstacle::Default::Default::Default"] = 4,
		-- 	},
		-- 	DataPool =
		-- 	{

		-- 	},
		-- },
	}

然后调用的时候,将条件数据通过前面的格式化数据。真的变成对应的关键Key,然后通过这些关键Key按照顺序依次匹配路由数据就可以了。这样就可以实现掉落数据的提前注入与规则匹配啦。有新的掉落需求也可以直接满足。这个掉落规则自我实现以来,就没有大框架上的修改。当然实现的细节还是修改过两三次的。

让我们聊聊路由数据在其他地方的使用吧

因为我们游戏中的另一个组件模式挺有意思的,所以我就试着将这个模式放到了C#编程中。但是我最初没有想法具体要实现一个什么东西。所以就在考虑原本的C#实现的WebAPI还需要借助IIS用起来忒费劲,因为IIS有自己的问题,比如说IIS的多实例的问题,IIS自动休眠的问题,所以我就实现了一个不依托与IIS的简单的API项目取名TheHost,中间实现了一个HTTP的组件,需要做类似ASP的那种路由转发功能,所以我也将路由数据在C#中实现了一份。其实也是类似的逻辑,我就不多做赘述了。不过这个文件所在的项目我已经将他开源了。顺便贴一下地址

https://git.oschina.net/anxin1225/TheHost

using System;
using System.Collections.Generic;
namespace Host
{
	public class RouteData<T> where T : class
	{
		public RouteData()
		{
		}

		private List<KeyValuePair<string, T>> _route_list = new List<KeyValuePair<string, T>>();
		private Dictionary<Dictionary<string, string>, T> _route_value_list = new Dictionary<Dictionary<string, string>, T>();
		private List<string> _route_format_list = new List<string>();

		/// <summary>
		/// 从路由数据中查找对应数据
		/// </summary>
		/// <returns>The value.</returns>
		/// <param name="key">Key.</param>
		public T FindValue(string key)
		{
			foreach (var item in _route_list)
			{
				if (item.Key == key)
				{
					return item.Value;
				}
			}

			return null;
		}

		/// <summary>
		/// 从路由数据中查找对应数据
		/// </summary>
		/// <returns>The value.</returns>
		/// <param name="keys">Keys.</param>
		public object FindValue(IEnumerable<string> keys) 
		{
			foreach (var item in keys)
			{
				var obj = FindValue(item);
				if (obj != null)
					return obj;
			}

			return null;
		}

		/// <summary>
		/// 注册路由列表
		/// </summary>
		/// <param name="key">Key.</param>
		/// <param name="obj">Object.</param>
		public void RegisterRouteData(string key, T obj)
		{
			_route_list.Add(new KeyValuePair<string, T>(key, obj));
		}

		/// <summary>
		/// 获取当前的路由列表
		/// </summary>
		/// <returns>The route list.</returns>
		public List<KeyValuePair<string, T>> GetRouteList()
		{
			return _route_list;
		}

		/// <summary>
		/// 注册路由格式化数据
		/// </summary>
		/// <param name="route">Route.</param>
		public void RegisterRouteFormat(string route)
		{
			if (!string.IsNullOrEmpty(route))
			{
				_route_format_list.Add(route);
			}
		}

		/// <summary>
		/// 注册路由列表数据
		/// </summary>
		/// <param name="routevalue">Routevalue.</param>
		/// <param name="obj">Object.</param>
		/// <param name="reset_list">If set to <c>true</c> reset list.</param>
		public void RegisterRouteData(Dictionary<string, string> routevalue, T obj, bool reset_list = true)
		{
			_route_value_list.Add(routevalue, obj);

			if (reset_list)
				RestRouteList();
		}

		/// <summary>
		/// 重新计算路由列表
		/// </summary>
		public void RestRouteList()
		{
			_route_list.Clear();

			foreach (var item in _route_format_list)
			{
				foreach (var route in _route_value_list)
				{
					var key = item;
					foreach (var kv in route.Key)
					{
						key = key.Replace("{" + kv.Key + "}", kv.Value);
					}

					if (key.IndexOf('{') == -1 && key.IndexOf('}') == -1)
					{
						RegisterRouteData(key, route.Value);
					}
				}
			}

		}

		/// <summary>
		/// 根据变换列表和注册的格式获取路由名称列表
		/// </summary>
		/// <returns>The route key.</returns>
		/// <param name="routevalue">Routevalue.</param>
		public List<string> GetRouteKey(Dictionary<string, string> routevalue)
		{
			List<string> list = new List<string>();

			foreach (var item in _route_format_list)
			{
				var key = item;
				foreach (var kv in routevalue)
				{
					key = key.Replace("{" + kv.Key + "}", kv.Value);
				}

				if (key.IndexOf('{') == -1 && key.IndexOf('}') == -1)
				{
					list.Add(key);
				}
			}

			return list;
		}
	}
}

另一个项目

我很久之前写过一个聊天的项目,不过当时因为工作的变动,那个项目就没有继续维护下去了,因为我的离开所以那个项目也死掉了。不过聊天项目还真是有意思,所以我准备再次重新造这个轮子,不为别的,就是造着玩。中间已经不是使用HTTP协议了,准备使用UDP协议,然后用来转发消息,当然中间也要用到路由转发相关的功能,就直接使用TheHost中实现的那个了,毕竟那个路由一进挂在HTTP实现的时候验证过了,代码应该还算OK

贴一下地址:
https://git.oschina.net/anxin1225/aximserver

好像也没什么好继续说的了,先这样,好的,我懂我懂又有人要吐槽我了。哈哈哈

posted @ 2017-05-03 11:49  连程  阅读(1137)  评论(0编辑  收藏  举报