首先來解釋一下什麼是路由數據格式,這個呢,是一個非官方的描述詞。就是是我感覺應該叫這個比較合適,但是並不明白各位大佬怎麼叫這種數據格式。要用到這種數據格式的起因也比較簡單,是我們的的遊戲裏邊要有一個掉落系統。比如說,在某些場景下,某些東西死了或者被破壞掉了會按照某些規則掉落某些東西。當然路由還可以做其他很有意思的事情,我拿它來做的事情包含掉落總共有三個,不過其他兩個優點類似一會一塊說
先來說說,我們的掉落需求
需求的簡單描述
掉落的規則,是這樣的,我首先需要計算掉率比如說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
好像也沒什麼好繼續說的了,先這樣,好的,我懂我懂又有人要吐槽我了。哈哈哈