Ogre源代码浅析——脚本及其解析(三)
在完成了对脚本源文件的“词法分析”后,Ogre将进入到parse也就是“语义分析”阶段。这两个阶段的作用,从各自定义的数据结构中就可以一窥端倪,看下相应的结点定义:
1 struct ScriptToken
2 {
3 /// This is the lexeme for this token
4 String lexeme, file;
5 /// This is the id associated with the lexeme, which comes from a lexeme-token id mapping
6 uint32 type;
7 /// This holds the line number of the input stream where the token was found.
8 uint32 line;
9 };
10 typedef SharedPtr<ScriptToken> ScriptTokenPtr;
11 typedef vector<ScriptTokenPtr>::type ScriptTokenList;
12 typedef SharedPtr<ScriptTokenList> ScriptTokenListPtr;
13
14 --------------------------------------------------------------------
15
16 struct ConcreteNode;
17 typedef SharedPtr<ConcreteNode> ConcreteNodePtr;
18 typedef list<ConcreteNodePtr>::type ConcreteNodeList;
19 typedef SharedPtr<ConcreteNodeList> ConcreteNodeListPtr;
20 struct ConcreteNode : public ScriptCompilerAlloc
21 {
22 String token, file;
23 unsigned int line;
24 ConcreteNodeType type;
25 ConcreteNodeList children;
26 ConcreteNode *parent;
27 };
以上代码中,上半部分(1-12行)是“词法分析”的输出数据的数据结构;下半部分(16-27行)是“语义分析”阶段输出数据的数据结构。从定义可以看出“词法分析”的结果,要以ScriptToken为单位来保存脚本中的各“词素(lexeme)”信息,然后将所有ScriptToken对象存放到vector中。vector中各ScriptToken之间是一种并列的关系,它只一一列举了相应源脚本中所有的“词素(lexeme)”的数据,而并未反应出这些lexeme之间的逻辑关系。那么,Ogre的脚本文件在编写时是否都要遵循相同的规则?它们有统一的结构吗?为解决此问题,先列出“.material”、“.program”、“.particle”和“.compositor”四种脚本的片段,来探究一下它们在结构上的共同规律:
1 SdkTrays.overlay
2
3 template container Panel(SdkTrays/Cursor)
4 {
5 metrics_mode pixels
6 transparent true
7
8 // You can offset the image to change the cursor "hotspot"
9 element Panel(CursorImage)
10 {
11 metrics_mode pixels
12 material SdkTrays/Cursor
13 width 32
14 height 32
15 }
16 }
17 ...
18
19 ---------------------------------------------------------
20
21 SdyTrays.material
22
23 material SdkTrays/Base
24 {
25 technique
26 {
27 pass
28 {
29 lighting off
30 scene_blend alpha_blend
31 depth_check off
32
33 texture_unit
34 {
35 tex_address_mode clamp
36 filtering linear linear none
37 }
38 }
39 }
40 }
41 ...
42 ----------------------------------------------------------------
43
44 smoke.particle
45
46 particle_system Examples/Smoke
47 {
48 material Examples/Smoke
49 particle_width 35
50 particle_height 35
51 cull_each true
52 quota 500
53 billboard_type point
54 sorted true
55
56 // Area emitter
57 emitter Point
58 {
59 position 0 15 -15
60 angle 35
61 emission_rate 15
62 time_to_live 4
63 direction 0 1 0
64 velocity_min 50
65 velocity_max 80
66 }
67
68 affector ColourImage
69 {
70 image smokecolors.png
71 }
72
73 affector Rotator
74 {
75 rotation_range_start 0
76 rotation_range_end 360
77 rotation_speed_range_start -60
78 rotation_speed_range_end 200
79 }
80
81 affector Scaler
82 {
83 rate 50
84 }
85
86 }
87
88 --------------------------------------------------------
89
90 Example.compositor
91
92 compositor Glass
93 {
94 technique
95 {
96 texture rt0 target_width target_height PF_R8G8B8
97
98 target rt0 { input previous }
99
100 target_output
101 {
102 // Start with clear output
103 input none
104
105 pass render_quad
106 {
107 material Ogre/Compositor/GlassPass
108 input 0 rt0
109 }
110 }
111 }
112 }
可以看到,在这些脚本中,真正的数据单位是以左右大括号为限定符的数据单元。如果把一个数据单元看作一个结点,那么其中一些结点可以嵌套在另一些结点中,且所有的结点将形成一个树形结构;而对于每一个结点来说,“词素”又是构建它的最小单位。因此,如果把“词法分析”的结果vector中的数据作为输入的话,那么“语义分析”的输出结果就应该是——以数据单元为结点构建成的“结点树”。ScriptParser::parse()函数就是用来完成语义分析任务的函数,看一下相关的代码:
1 ConcreteNodeListPtr ScriptParser::parse(const ScriptTokenListPtr &tokens)
2 {
3 // MEMCATEGORY_GENERAL because SharedPtr can only free using that category
4 ConcreteNodeListPtr nodes(OGRE_NEW_T(ConcreteNodeList, MEMCATEGORY_GENERAL)(), SPFM_DELETE_T);
5
6 enum{READY, OBJECT};
7 uint32 state = READY;
8
9 ConcreteNode *parent = 0;
10 ConcreteNodePtr node;
11 ScriptToken *token = 0;
12 ScriptTokenList::iterator i = tokens->begin(), end = tokens->end();
13 while(i != end)
14 {
15 token = (*i).get();
16
17 switch(state)
18 {
19 case READY:
20 if(token->type == TID_WORD)
21 {
22 if(token->lexeme == "import")
23 {
24 node = ConcreteNodePtr(OGRE_NEW ConcreteNode());
25 node->token = token->lexeme;
26 node->file = token->file;
27 node->line = token->line;
28 node->type = CNT_IMPORT;
29
30 // The next token is the target
31 ++i;
32 if(i == end || ((*i)->type != TID_WORD && (*i)->type != TID_QUOTE))
33 OGRE_EXCEPT(Exception::ERR_INVALID_STATE,
34 Ogre::String("expected import target at line ") +
35 Ogre::StringConverter::toString(node->line),
36 "ScriptParser::parse");
37 ConcreteNodePtr temp(OGRE_NEW ConcreteNode());
38 temp->parent = node.get();
39 temp->file = (*i)->file;
40 temp->line = (*i)->line;
41 temp->type = (*i)->type == TID_WORD ? CNT_WORD : CNT_QUOTE;
42 if(temp->type == CNT_QUOTE)
43 temp->token = (*i)->lexeme.substr(1, token->lexeme.size() - 2);
44 else
45 temp->token = (*i)->lexeme;
46 node->children.push_back(temp);
47
48 // The second-next token is the source
49 ++i;
50 ++i;
51 if(i == end || ((*i)->type != TID_WORD && (*i)->type != TID_QUOTE))
52 OGRE_EXCEPT(Exception::ERR_INVALID_STATE,
53 Ogre::String("expected import source at line ") +
54 Ogre::StringConverter::toString(node->line),
55 "ScriptParser::parse");
56 temp = ConcreteNodePtr(OGRE_NEW ConcreteNode());
57 temp->parent = node.get();
58 temp->file = (*i)->file;
59 temp->line = (*i)->line;
60 temp->type = (*i)->type == TID_WORD ? CNT_WORD : CNT_QUOTE;
61 if(temp->type == CNT_QUOTE)
62 temp->token = (*i)->lexeme.substr(1, (*i)->lexeme.size() - 2);
63 else
64 temp->token = (*i)->lexeme;
65 node->children.push_back(temp);
66
67 // Consume all the newlines
68 i = skipNewlines(i, end);
69
70 // Insert the node
71 if(parent)
72 {
73 node->parent = parent;
74 parent->children.push_back(node);
75 }
76 else
77 {
78 node->parent = 0;
79 nodes->push_back(node);
80 }
81 node = ConcreteNodePtr();
82 }
83 else if(token->lexeme == "set")
84 {
85 node = ConcreteNodePtr(OGRE_NEW ConcreteNode());
86 node->token = token->lexeme;
87 node->file = token->file;
88 node->line = token->line;
89 node->type = CNT_VARIABLE_ASSIGN;
90
91 // The next token is the variable
92 ++i;
93 if(i == end || (*i)->type != TID_VARIABLE)
94 OGRE_EXCEPT(Exception::ERR_INVALID_STATE,
95 Ogre::String("expected variable name at line ") +
96 Ogre::StringConverter::toString(node->line),
97 "ScriptParser::parse");
98 ConcreteNodePtr temp(OGRE_NEW ConcreteNode());
99 temp->parent = node.get();
100 temp->file = (*i)->file;
101 temp->line = (*i)->line;
102 temp->type = CNT_VARIABLE;
103 temp->token = (*i)->lexeme;
104 node->children.push_back(temp);
105
106 // The next token is the assignment
107 ++i;
108 if(i == end || ((*i)->type != TID_WORD && (*i)->type != TID_QUOTE))
109 OGRE_EXCEPT(Exception::ERR_INVALID_STATE,
110 Ogre::String("expected variable value at line ") +
111 Ogre::StringConverter::toString(node->line),
112 "ScriptParser::parse");
113 temp = ConcreteNodePtr(OGRE_NEW ConcreteNode());
114 temp->parent = node.get();
115 temp->file = (*i)->file;
116 temp->line = (*i)->line;
117 temp->type = (*i)->type == TID_WORD ? CNT_WORD : CNT_QUOTE;
118 if(temp->type == CNT_QUOTE)
119 temp->token = (*i)->lexeme.substr(1, (*i)->lexeme.size() - 2);
120 else
121 temp->token = (*i)->lexeme;
122 node->children.push_back(temp);
123
124 // Consume all the newlines
125 i = skipNewlines(i, end);
126
127 // Insert the node
128 if(parent)
129 {
130 node->parent = parent;
131 parent->children.push_back(node);
132 }
133 else
134 {
135 node->parent = 0;
136 nodes->push_back(node);
137 }
138 node = ConcreteNodePtr();
139 }
140 else
141 {
142 node = ConcreteNodePtr(OGRE_NEW ConcreteNode());
143 node->file = token->file;
144 node->line = token->line;
145 node->type = token->type == TID_WORD ? CNT_WORD : CNT_QUOTE;
146 if(node->type == CNT_QUOTE)
147 node->token = token->lexeme.substr(1, token->lexeme.size() - 2);
148 else
149 node->token = token->lexeme;
150
151 // Insert the node
152 if(parent)
153 {
154 node->parent = parent;
155 parent->children.push_back(node);
156 }
157 else
158 {
159 node->parent = 0;
160 nodes->push_back(node);
161 }
162
163 // Set the parent
164 parent = node.get();
165
166 // Switch states
167 state = OBJECT;
168
169 node = ConcreteNodePtr();
170 }
171 }
172 else if(token->type == TID_RBRACKET)
173 {
174 // Go up one level if we can
175 if(parent)
176 parent = parent->parent;
177
178 node = ConcreteNodePtr(OGRE_NEW ConcreteNode());
179 node->token = token->lexeme;
180 node->file = token->file;
181 node->line = token->line;
182 node->type = CNT_RBRACE;
183
184 // Consume all the newlines
185 i = skipNewlines(i, end);
186
187 // Insert the node
188 if(parent)
189 {
190 node->parent = parent;
191 parent->children.push_back(node);
192 }
193 else
194 {
195 node->parent = 0;
196 nodes->push_back(node);
197 }
198
199 // Move up another level
200 if(parent)
201 parent = parent->parent;
202
203 node = ConcreteNodePtr();
204 }
205 break;
206 case OBJECT:
207 if(token->type == TID_NEWLINE)
208 {
209 // Look ahead to the next non-newline token and if it isn't an {, this was a property
210 ScriptTokenList::iterator next = skipNewlines(i, end);
211 if(next == end || (*next)->type != TID_LBRACKET)
212 {
213 // Ended a property here
214 if(parent)
215 parent = parent->parent;
216 state = READY;
217 }
218 }
219 else if(token->type == TID_COLON)
220 {
221 node = ConcreteNodePtr(OGRE_NEW ConcreteNode());
222 node->token = token->lexeme;
223 node->file = token->file;
224 node->line = token->line;
225 node->type = CNT_COLON;
226
227 // The following token are the parent objects (base classes).
228 // Require at least one of them.
229
230 ScriptTokenList::iterator j = i + 1;
231 j = skipNewlines(j, end);
232 if(j == end || ((*j)->type != TID_WORD && (*j)->type != TID_QUOTE)) {
233 OGRE_EXCEPT(Exception::ERR_INVALID_STATE,
234 Ogre::String("expected object identifier at line ") +
235 Ogre::StringConverter::toString(node->line),
236 "ScriptParser::parse");
237 }
238
239 while(j != end && ((*j)->type == TID_WORD || (*j)->type == TID_QUOTE))
240 {
241 ConcreteNodePtr tempNode = ConcreteNodePtr(OGRE_NEW ConcreteNode());
242 tempNode->token = (*j)->lexeme;
243 tempNode->file = (*j)->file;
244 tempNode->line = (*j)->line;
245 tempNode->type = (*j)->type == TID_WORD ? CNT_WORD : CNT_QUOTE;
246 tempNode->parent = node.get();
247 node->children.push_back(tempNode);
248 ++j;
249 }
250
251 // Move it backwards once, since the end of the loop moves it forwards again anyway
252 j--;
253 i = j;
254
255 // Insert the node
256 if(parent)
257 {
258 node->parent = parent;
259 parent->children.push_back(node);
260 }
261 else
262 {
263 node->parent = 0;
264 nodes->push_back(node);
265 }
266 node = ConcreteNodePtr();
267 }
268 else if(token->type == TID_LBRACKET)
269 {
270 node = ConcreteNodePtr(OGRE_NEW ConcreteNode());
271 node->token = token->lexeme;
272 node->file = token->file;
273 node->line = token->line;
274 node->type = CNT_LBRACE;
275
276 // Consume all the newlines
277 i = skipNewlines(i, end);
278
279 // Insert the node
280 if(parent)
281 {
282 node->parent = parent;
283 parent->children.push_back(node);
284 }
285 else
286 {
287 node->parent = 0;
288 nodes->push_back(node);
289 }
290
291 // Set the parent
292 parent = node.get();
293
294 // Change the state
295 state = READY;
296
297 node = ConcreteNodePtr();
298 }
299 else if(token->type == TID_RBRACKET)
300 {
301 // Go up one level if we can
302 if(parent)
303 parent = parent->parent;
304
305 // If the parent is currently a { then go up again
306 if(parent && parent->type == CNT_LBRACE && parent->parent)
307 parent = parent->parent;
308
309 node = ConcreteNodePtr(OGRE_NEW ConcreteNode());
310 node->token = token->lexeme;
311 node->file = token->file;
312 node->line = token->line;
313 node->type = CNT_RBRACE;
314
315 // Consume all the newlines
316 i = skipNewlines(i, end);
317
318 // Insert the node
319 if(parent)
320 {
321 node->parent = parent;
322 parent->children.push_back(node);
323 }
324 else
325 {
326 node->parent = 0;
327 nodes->push_back(node);
328 }
329
330 // Move up another level
331 if(parent)
332 parent = parent->parent;
333
334 node = ConcreteNodePtr();
335 state = READY;
336 }
337 else if(token->type == TID_VARIABLE)
338 {
339 node = ConcreteNodePtr(OGRE_NEW ConcreteNode());
340 node->token = token->lexeme;
341 node->file = token->file;
342 node->line = token->line;
343 node->type = CNT_VARIABLE;
344
345 // Insert the node
346 if(parent)
347 {
348 node->parent = parent;
349 parent->children.push_back(node);
350 }
351 else
352 {
353 node->parent = 0;
354 nodes->push_back(node);
355 }
356 node = ConcreteNodePtr();
357 }
358 else if(token->type == TID_QUOTE)
359 {
360 node = ConcreteNodePtr(OGRE_NEW ConcreteNode());
361 node->token = token->lexeme.substr(1, token->lexeme.size() - 2);
362 node->file = token->file;
363 node->line = token->line;
364 node->type = CNT_QUOTE;
365
366 // Insert the node
367 if(parent)
368 {
369 node->parent = parent;
370 parent->children.push_back(node);
371 }
372 else
373 {
374 node->parent = 0;
375 nodes->push_back(node);
376 }
377 node = ConcreteNodePtr();
378 }
379 else if(token->type == TID_WORD)
380 {
381 node = ConcreteNodePtr(OGRE_NEW ConcreteNode());
382 node->token = token->lexeme;
383 node->file = token->file;
384 node->line = token->line;
385 node->type = CNT_WORD;
386
387 // Insert the node
388 if(parent)
389 {
390 node->parent = parent;
391 parent->children.push_back(node);
392 }
393 else
394 {
395 node->parent = 0;
396 nodes->push_back(node);
397 }
398 node = ConcreteNodePtr();
399 }
400 break;
401 }
402
403 ++i;
404 }
405
406 return nodes;
407 }
可以看出,“语义分析”的输入数据是ScriptTokenList,其输出是ConcreteNodeList;整个分析过程用的是,“逐词素分析+状态机”的实现机制,这与“词法分析”机制类似(参见之前的讨论:http://www.cnblogs.com/yzwalkman/archive/2013/01/02/2841607.html)。需要说明的是,按照脚本文件的固有结构,我们一般会自然地想到:如果把左右大括号为限定符的数据作为一个数据单元的话,那么,上面定义中出现的ConcreteNode结构就应该直接对应此数据单元;而ConcreteNodeList就应该是用来保存同一文件中所有数据单元的列表。但从代码中我们可以看到,情况并不是这样的。
实际上,每一个ConcreteNode对象对应的仍旧是一个词素结点(ScriptToken)对象。parse函数在生成一个ConcreteNode对象后,会直接把相应的ScriptToken中的数据赋值给它(参见24-28行、85-89行等)。然后parse()函数会在此基础上再做两步工作:1.确定此ConcreteNode的子结点;2.确定此ConcreteNode对象的父结点。而父结点与子结点的判定依据就是上面说的——确定了脚本文件固有结构的数据单元间的关系。以材质脚本为例,假设一个脚本文件中定义且只定义了10个材质,那么此脚本在进过语义分析后,相应的ConcreteNodeList就会保存这10个ConcreteNode对象的指针,此容器中每个ConcreteNode对象实际上是相应材质的第一个词素;同时,每个材质内部的数据单元也会按此方法保存。为更清楚说明,可以上面的脚本SdkTrays.material中的材质material SdkTrays/Base为例,SdkTrays.material脚本被解析后,material SdkTrays/Base将被作为第一个数据单元被保存在ConcreteNodeList中(因为它是脚本中第一个被定义的数据单元),而作为此单元根结点的ConcreteNode对应着词素——material,接下来的三个词素SdkTrays/Base和左、右大括号所对应的ConcreteNode对象,将作为三个并列的子结点保存在material结点的子结点列表(结点定义的第25行)中,相应的,这三个子结点的父结点指针(见结点定义的第26行)将指向material结点对象。而紧随其后的technique词素所对应生成的ConcreteNode结点对象,将被作为左括号的第一个子结点被保存,technique结点的父结点指针将指向此左括号对象,其余结点的操作以此类推。
从代码中可以看到,整个分析过程只在两种READY和OBJECT两种状态中转换(第6行)。在处理完左括号(268-298行)或右括号(299-336行)后,分析会从OBJECT状态转换到READY状态,也就是说,OBJECT状态是用来处理包含在左右括号内的数据单元内部信息的,而READY状态是用来处理,分析完一个数据单元或开始一个新的数据单元之后要做的相关工作的。
最后比较一下ScriptToken和ConcreteNode的各自可取的type的定义:
1 enum ConcreteNodeType 2 { 3 CNT_VARIABLE, 4 CNT_VARIABLE_ASSIGN, 5 CNT_WORD, 6 CNT_IMPORT, 7 CNT_QUOTE, 8 CNT_LBRACE, 9 CNT_RBRACE, 10 CNT_COLON 11 }; 12 13 enum{ 14 TID_LBRACKET = 0, // { 15 TID_RBRACKET, // } 16 TID_COLON, // : 17 TID_VARIABLE, // $... 18 TID_WORD, // * 19 TID_QUOTE, // "*" 20 TID_NEWLINE, // \n 21 TID_UNKNOWN, 22 TID_END 23 };
可以看到它们有对应的项,也有各自独立的项。这是由这两个结点自身所承担的任务来决定的。
浙公网安备 33010602011771号