『設計模式』中有一個模式能夠解釋特定的語法規則,它就是解釋器模式(Interpreter Pattern)。不一樣於常見的策略模式或者是工廠模式,解釋器模式在.NET或者JDK中並不常見,並且在業務上也不多會去解釋特定的語法,因此它並不被普遍使用。一個解釋器可大可小,大能夠是複雜的編譯器,小也能夠是一個簡單的字符串解析,但本質上它們都是對特定的語法作出合理的解釋。前端
雖然解釋器模式不多使用,但在在遊戲開發中,仍是很常見的。好比你在戰鬥時,普通攻擊和魔法攻擊必定會產生不一樣的傷害,遊戲設計者會爲技能設計不一樣的『公式』,簡單如我方的攻擊力-敵方的防護力,同時『公式』還能夠加入參數,如$critRate
表明一個爆發率。故遊戲的技能傷害以下圖所示:git
遊戲裏的『公式』本質上是字符串,很像數學表達式,但又比它更高級,能夠加入自定義的參數,因此『公式』更像是數學表達式的超集。既然談到了數學表達式,那麼有必要知道怎樣去解析一個數學表達式。github
千萬不要小看這個任務,實際上要作一個計算器是很是複雜的。假設輸入一個字符串:-(1+(2+3)x4-5),注意這是一個字符串。解決方案有兩種:算法
while
遍歷字符串,將括號、運算符、數字等取出來,根據運算符左結合以及優先級計算很顯然第一種方式簡單直白,但很繁重,代碼的易讀性也不佳,第二種是目前最好的解決方式,將表達式轉化爲二叉樹。因此難點在於怎樣將表達式轉化爲一棵二叉樹?express
這須要瞭解數據結構相關知識,表達式-(1+(2+3)x4-5)又被稱爲中序排序,中序排序不能生成一棵二叉樹,你須要將中序排序轉化爲前序排序或者後序排序,而後根據中序排序和前序排序生成二叉樹,相關算法自行搜索,不作累贅。設計模式
我在閱讀了《編譯原理》第1,2章以後,還有另一種方式將表達式生成二叉樹形式,這也是編譯的基本原理。數據結構
咱們以最簡單的算術表達式舉例,編譯器在分析階段把一個字符序列分爲各個組成部分,最終生成一棵抽象語法樹(abstract syntax tree),以下所示:ide
表達式語法定義學習
語法,顧名思義,是一種特定的描述方法。咱們學習的英語語法,又或者是程序語言的語法,都有嚴格的格式要求。對於算術表達式而言,好比9-5+2,3-2,語法是兩個數字之間必須出現+,-,若是出現9+-5,那麼這就是錯誤的語法。ui
那咱們怎麼來制定語法呢?在編譯原理領域,使用一個通用的表示方法來描述語法,這個方法就是上下文無關文法或BNF範式。
好比上述的算術(+和-)表達式:9-5+2,咱們能夠推導出以下BNF範式:
list->list+digit|list-digit|digit
digit->0|1|2|3|4|5|6|7|8|9
list表明一個表達式序列,digit表明數字,箭頭->能夠讀做「能夠具備以下形式」,而豎線|表明或的意思。
詞法分析器
詞法分析器讀入源程序中的字符序列,將他們組織爲具備詞法含義的詞素,生成並輸出表明這些詞素的詞法單元(Token)。
語法分析器
語法分析器根據詞法單元,以語法分析樹的形式構建表達式,最終造成一顆抽象的語法樹(abstract syntax tree),這是一種表示了層次化的結構。
語法分析樹
若是非終端節點A有一個產生式A->XYZ,那麼在語法分析樹中就可能有一個標號爲A的內部節點,該節點有三個子節點,從左向右標號爲X,Y,Z。內部節點對應於產生式的頭,它的子節點對應於產生式的體:
數學表達式的特色
運用編譯原理的知識,編寫一個自定義的解釋器,咱們須要以下三個步驟:
LexicalAnalyzer
Parser
我在一開始就提到過,遊戲裏的『公式』很像數學表達式,那麼數學表達式有什麼普遍和通用的特色?
首先數學表達式由數字和運算符構成,而且運算符有左結合性和優先性:
算術表達式的BNF構建
經過對數學表達式的瞭解,咱們知道一個數學表達式有數字、運算符等組成,而且運算符是左結合和有優先性,那怎樣去構建它的BNF範式呢?
咱們建立兩個非終結符號expr(表達式) 和 term(項) ,分別對應這兩個優先級層次,並使用另外一個非終結符號factor(因子)來生成表達式的基本單元。
那什麼是factor呢?
咱們能夠將因子(factor)理解成不能被任何運算符分開的表達式。『不能分開』的意思是說當咱們在任意因子的任意一邊放置一個運算符,都不會致使這個因子的任何部分分離出來,成爲這個運算符的運算份量。固然,因子自己做爲一個總體能夠成爲該運算符的一個運算份量。若是這個因子是由一個括號括起來的表達式,那麼這個括號將起到保護其不被分開的做用。
factor->digit|(expr)
digit->0|1|2|3|4|5|6|7|8|9
那什麼是term呢?
一個(不是因子)項(term)是一個可能被高優先級的運算符x和/分開,但不能被低優先級運算符分開的表達式。
term->term x factor|term / factor|factor
那什麼是expr呢?
一個(不是因子也不是項)的表達式可能被任何一個運算符分開。
expr->expr+term|expr-term|term
所以最終獲得的BNF範式是:
expr->expr+term|expr-term|term
term->term x factor|term/factor|factor
factor->digit|(expr)
使用這個BNF範式時,一個表達式就是一個由+或-分割開來的項(term)列表,而項是由x或者/分隔的因子(factor)列表。請注意,任何由括號括起來的表達式都是一個因子。
這個BNF範式的語法分析樹爲以下所示:
求值時,從root節點遍歷二叉樹,若是節點有子節點,遞歸的方式遍歷下去,直到是葉子節點爲止,接着將左子樹和右子樹取得的值放入它們的根節點,最後root節點的值就是表達式最終的值。
有了準備以後,接下來就是實現解釋器,它能夠解釋遊戲中的『公式』。
1.) 建立一個數學表達式類MathExpression
,根據面向對象思想,它封裝了數據和行爲,因爲篇幅有限,只展現其骨架:
public class MathExpression
{
private readonly string _expression;
public int CurrentIndex{}
public bool IsIndexOutOfRange{}
public bool IsEndOfString{}
public char CurrentChar{}
public char GetSpecificCharByIndex(int index){}
}複製代碼
2.) 建立一個詞法分析器LexicalAnalyzer
,獲取對應的詞法單元Token:
switch (_mathExpression.CurrentChar)
{
case '+':
token = Token.Add;
_mathExpression.CurrentIndex++;
break;
case '-':
token = Token.Sub;
_mathExpression.CurrentIndex++;
break;
case '*':
token=Token.Mul;
_mathExpression.CurrentIndex++;
break;
case '/':
token = Token.Div;
_mathExpression.CurrentIndex++;
break;
case '(':
token = Token.OParen;
_mathExpression.CurrentIndex++;
break;
case ')':
token = Token.CParen;
_mathExpression.CurrentIndex++;
break;
case '$':
if (_mathExpression.GetSpecificCharByIndex(_mathExpression.CurrentIndex + 1) =='c')
{
_mathExpression.CurrentIndex += 2;
token = Token.Param;
}
else
{
_mathExpression.CurrentIndex++;
token=Token.Illegal;
}
break;
default:
if (char.IsDigit(_mathExpression.CurrentChar))
{
token = GetDigitsFromString();
}else if (char.IsLetter(_mathExpression.CurrentChar))
{
token = GetSineCosineFromString();
}
else
{
throw new Exception("Illegal Token");
}
break;
}複製代碼
3.) 值得一提的事情,怎樣從字符串中獲取數字,數字有兩種形式:整數和小數點形式,經過有窮自動機在不一樣的狀態間跳轉並記錄下數字的索引下標,直到遇到非數字退出,有窮自動機以下所示:
一個有窮自動機的狀態判斷代碼以下:
do
{
isEndOfString = _mathExpression.IsEndOfString;
currentChar = _mathExpression.CurrentChar;
switch (_currentState)
{
case State.Init:
if (char.IsDigit(currentChar))
{
_currentState = State.Integer;
if (!isEndOfString)
{
_mathExpression.CurrentIndex++;
}
}
else
{
//Init狀態非數字則退出
_currentState= State.Quit;
}
break;
case State.Integer:
if (currentChar == '.')
{
_currentState = State.Float;//輸入小數點,狀態轉移到Float
if (!isEndOfString)
{
_mathExpression.CurrentIndex++;
}
}
else
{
if (!char.IsDigit(currentChar))//既不是數字也不是小數
{
_currentState = State.Quit;
}
else
{
if (!isEndOfString)
{
_mathExpression.CurrentIndex++;//讀取下一個字符
}
}
}
break;
case State.Float:
if (!char.IsDigit(currentChar))//非數字,退出
{
_currentState = State.Quit;
}
else
{
if (!isEndOfString)
{
_mathExpression.CurrentIndex++;
}
}
break;
case State.Quit:
break;
}
} while (_currentState != State.Quit && !isEndOfString);複製代碼
4.)經過語法解析器Parser
構建表達式樹,每一個節點都是一個抽象Expression
public abstract class Expression
{
public abstract double Evaluate(Context context);
}複製代碼
Expression
根據類型不一樣有常量表達式,二元表達式,一元表達式等,一個常見的二元表達式以下:
public class BinaryExpression:Expression
{
private Expression _leftExpression;
private Expression _rightExpression;
private Operator _operator;
public BinaryExpression(Expression leftExpression,Expression righExpression,Operator op) {
_leftExpression = leftExpression;
_rightExpression = righExpression;
_operator = op;
}
public override double Evaluate(Context context) {
switch (_operator)
{
case Operator.Plus:
return _leftExpression.Evaluate(context) + _rightExpression.Evaluate(context);
case Operator.Minus:
return _leftExpression.Evaluate(context) - _rightExpression.Evaluate(context);
case Operator.Mul:
return _leftExpression.Evaluate(context) * _rightExpression.Evaluate(context);
case Operator.Div:
return _leftExpression.Evaluate(context) / _rightExpression.Evaluate(context);
}
return Double.NaN;
}
}複製代碼
能夠看到左子樹和右子樹一樣是Expression
。
5.)到目前爲止,能夠說是萬事俱備,只欠東風了,這個『東風』就是怎麼樣去構建表達式樹。已知的是,一個 expr 就是一個由+或-分割開來的項( term )列表,而項是由x或者/分隔的因子( factor )列表。
expr->expr+term|expr-term|term
private Expression Expr() {
Token old;
Expression expression = Term();
while (_currentToken==Token.Add|| _currentToken==Token.Sub)
{
old = _currentToken;
_currentToken = _lexicalAnalyzer.GetToken();
Expression e1 = Expr();
expression=new BinaryExpression(expression,e1,old==Token.Add?Operator.Plus:Operator.Minus);
}
return expression;
}複製代碼
term->term x factor|term/factor|factor
private Expression Term() {
Token old;
Expression expression = Factor();
while (_currentToken==Token.Mul || _currentToken==Token.Div)
{
old = _currentToken;
_currentToken = _lexicalAnalyzer.GetToken();
Expression e1 = Term();
expression=new BinaryExpression(expression,e1,old==Token.Mul?Operator.Mul:Operator.Div);
}
return expression;
}複製代碼
factor->digit|(expr)
private Expression Factor() {
Token token;
Expression expression;
if (_currentToken==Token.Double)
{
expression=new NumericConstant(_lexicalAnalyzer.GetDigits());
_currentToken = _lexicalAnalyzer.GetToken();
}
else if (_currentToken == Token.Param)
{
expression=new Var();
_currentToken = _lexicalAnalyzer.GetToken();
}
else if (_currentToken==Token.OParen)
{
_currentToken = _lexicalAnalyzer.GetToken();
expression = Expr();
if (_currentToken!=Token.CParen)
{
throw new Exception("Missing Closing Parenthesis\n");
}
_currentToken = _lexicalAnalyzer.GetToken();
}
else if(_currentToken==Token.Add || _currentToken==Token.Sub)
{
var old = _currentToken;
_currentToken = _lexicalAnalyzer.GetToken();
expression = Factor();
expression=new UnaryExpression(expression,old==Token.Add?Operator.Plus:Operator.Minus);
}
else
{
throw new Exception("error");
}
return expression;
}複製代碼
最後生成的樹結構以下所示:
本文爲你們介紹了怎樣從編譯原理的角度來實現一個解釋器。在遊戲領域,須要解釋器來解釋自定義的『公式』。這個『公式』的語法每每是和上下文無關的,又被稱爲BNF範式。解釋器的核心就是怎樣構建一棵抽象的表達式樹,這須要詞法分析和語法分析的相關知識。
參考代碼以下:github.com/MEyes/uInte…
歡迎關注個人公衆號: