TypeScript图形渲染实战2D架构设计与实现:第2章 使用TypeScript实现Doom3词法解析器(2:Token与Tokenizer)

TypeScript图形渲染实战2D架构设计与实现:第2章 使用TypeScript实现Doom3词法解析器(2:Token与Tokenizer)_第1张图片
基于Canvas2D的2D篇:京东有售

正文:

  本章的目的是想让大家了解TypeScript中常用的一些语法以及编程方式(例如面向对象编程,面向接口编程,泛型编程以及常用的设计模式等),因此特别以面向接口的方式编写了一个Doom3(原id Software公司毁灭战士3游戏引擎)词法解析器,并且在此基础上实现了工厂模式和迭代器两种设计模式,使其支持接口的生成以及使用迭代方式进行Token解析输出。

  Doom3引擎中所有的资源都存储在后缀名为.pk4的资源文件包中。该资源文件包实际就是一个zip压缩文件,因此我们可以将.pk4后缀名更改为.zip,然后就可以直接使用例如winzip程序进行解压并浏览该文件。

  当我们打开pk4文件浏览后会发现一个事实:Doom3引擎中大部分的资源都是基于文本描述的(除了图片,视频,或音频等资源外),并且基于一套简单的、统一的词法规则。如何将文本按照词法规则描述解析成有意义的标记(Token)是本章的另外一个重要目的。

  最后我们对XMLHttpRequest对象进行二次封装用于向服务器请求资源。这样我们就能够利用第一章中部署的lite-server服务器,将所有的存储在lite-server服务器上的文本文件,二进制文件,视频,音频传输到TypeScript(网页客户端)中进行处理。

  实际上我们实现的词法解析器不单单用于Doom3引擎相关资源的解析,通过些许扩展,我们还可以支持解析各种不同格式的ASCII编码文本文件,例如Wavefront的obj模型文件以及mtl材质文件等等。

2.1 Token与Tokenizer

  简单起见,我们使用js代码作为示例,来了解一下js的Token相关内容,以加深对Token的理解,代码如下:

if ( b === true )
    alert ( "true" ) ;

  在浏览器中输入网址:http://esprima.org/demo/parse.html后,并将上述代码黏贴到左侧文本编辑框,然后选择Tokens Tab选项,我们就会获得如下结果:

[
    {
        "type" : "Keyword" ,
        "value" : "if"
    } ,
    {
        "type" : "Punctuator" ,
        "value" : "("
    } ,
    {
        "type" : "Identifier" ,
        "value" : "b"
    } ,
    {
        "type" : "Punctuator" ,
        "value" :  ==="
    } ,
    {
        "type" : "Boolean" ,
        "value" : "true"
    } ,
    {
        "type" : "Punctuator" ,
        "value" : ")"
    } ,
    {
        "type" : "Identifier" ,
        "value" : "alert"
    } ,
    {
        "type" : "Punctuator" ,
        "value" : "("
    } ,
    {
        "type" : "String" ,
        "value": "\"true\""
    } ,
    {
        "type" : "Punctuator" ,
        "value" : ")"
    } ,
    {
        "type" : "Punctuator" ,
        "value" : ";"
    }
]

  通过上述代码,我们可以看到,esprima(ECMAScript词法语法解析器)会将js源码解析成Token的集合表示,每个Token具有type属性,表示该Token的类型分类,并且具有value属性,表示该Token的值是什么。

  由此可见,Token(标记或记号)就是指一组不可分割的字符或字符串,它能唯一的、没有歧义的标记出一种状态。本质上来说,就是特殊的字符或字符串(例如if、=== 等)。而esprima则是Tokenizer,其作用是将字符串表示的js源码数据读取进来,按照预先设定的标准进行分类处理,处理的结果就是Token。

  事实上esprima是一个ECMAScript解析器,包含词法解析和语法解析,最终会将js源码解析成抽象语法树(Abstract Syntax Tree,简称AST),而在这里,为了演示的原因,我们仅仅使用了esprima的词法解析功能,并没有使用到语法解析功能。

2.1.1 Doom3文本文件格式

  如果要实现一个特定文件格式的词法解析器,一定要了解该文件的词法特征,根据文件的词法特征抽象出分类规则,然后才能编码实现词法解析功能。因此本小节来了解一下Doom3引擎中的文本文件的相关规则,使用如下一段具有普遍性的文本字符串:

numMeshes  5

/*
* joints关键字定义了骨骼动画的bindPose
*/
joints {
    "origin"    -1  ( 0 0 0 )  ( -0.5  -0.5  -0.5 )     
    "Body"  0  ( -12.1038131714  0  79.004776001 )  ( -0.5 -0.5 -0.5 )  // origin
}

  例如numMeshes、joints等没有双引号的单词,作为关键字处理,也就是Doom3引擎预先定义好的,具有特定含义的一些词,它们具有唯一性,不可更改性。

  例如 "origin"、"Body"这些具有双引号的单词,作为标识符处理,这些标识符并非由Doom3引擎预先定义,而是美术设计等相关人员或者模型制作动画师定义的名称。

  在/*和*/之间的文字被Doom3引擎的词法解析器视为注释,和TypeScript一样,表示多行注释。斜杠//后的文字则被视为单行注释,这也和TypeScript的单行注释保持一样性。

  大括号对{ }表示一个块状模块,你可以将其视为一个区块分组,和TypeScript作用域类似。小括号对( )内部是使用浮点数表示的矢量或矩阵数据,你可以将其看成数组表示,需要注意的一点是,数组元素之间不是使用逗号分隔,而是使用空格符号进行分隔。

  Doom3文本文件中的数据类型其实就两种:字符串和数字,其中关键字和标识符都可以看成字符串类型,而数字可以分为整数和浮点数两种类型。

  上述几条,基本囊括了Doom3文本文件格式的关键之处,还有一些隐藏在深处的规则,则由源码实现的过程中进行描述。

2.1.2 使用IDoom3Token与IDoom3Tokenizer接口

  先来看一下如何调用Doom3的词法解析器,然后再去了解如何实现过程。首先创建一个名为doom3TokenizerTest . ts的文件,并导入如下四个结构:

import { IDoom3Token , IDoom3Tokenizer , Doom3Factory , ETokenType } from " ./src/doom3Tokenizer" ;

  然后将要解析的字符串赋值给一个string类型的变量,需要注意的是我们使用了ES6中的模板字符串(使用了开单引号`xxx`,而不是双引号"xxx"或单引号'xxx'的方式来定义字符串字面值):

let str : string = ` // 注意:这是开单引号`,不是单引号'
numMeshes  5
/*
*  joints 关键字定义了骨骼动画的 bindPose
*/

joints  {
        "origin"    -1  ( 0 0 0 )  ( -0.5  -0.5  -0.5 )     
        "Body"  0  ( -12.1038131714  0  79.004776001 )  ( -0.5 -0.5 -0.5 )  // origin
}
` ;  // 注意:这是开单引号`,不是单引号'

  最后来看一下如何使用IDoom3Token和IDoom3Tokenizer的属性和方法,具体代码如下:

// 从Doom3Factory工厂创建IDoom3Tokenizer接口
let tokenizer : IDoom3Tokenizer = Doom3Factory . createDoom3Tokenizer ( ) ;
// IDoom3Tokenizer接口创建IDoomToken接口
let token : IDoom3Token = tokenizer . createDoom3Token ( ) ;

//设置IDoom3Tokenizer要解析的数据源
tokenizer . setSource ( str ) ;

// getNextToken函数返回ture,说明没有到达字符串的结尾,仍有token需要解析
// 解析的结果以传引用的方式从参数token传出来
// 如果getNextToken返回false,说明已经到达字符串结尾,则停止循环
while ( tokenizer . getNextToken ( token ) ) {
    //如果当前的token的type是Number类型
    if ( token . type === ETokenType . NUMBER ) {
        console . log ( " NUMBER :  " + token . getFloat ( ) ) ; //输出该数字的浮点值
    } else if ( token . isString ( "joints" ) ) {
        //如果当前token是字符串类型,并且其值为joints,则输出
        console . log ( " 开始解析joints数据 " ) ;
    }
    else { //否则获取当前token的字符串值
        console . log( " STRING :  " + token . getString ( ) ) ;
    }
}

  我们使用F5快捷键启动VS Code的调试器,会看到在浏览器的控制台中输出如图2.1所示的内容。

TypeScript图形渲染实战2D架构设计与实现:第2章 使用TypeScript实现Doom3词法解析器(2:Token与Tokenizer)_第2张图片
图2.1 词法解析后Chrome输出结果

  我们会看到每个关键字(例如numMeshes),标识符(例如origin)以及数字(浮点数,整数以及负数)被正确的输出,并且跳过多行注释和单行注释中的内容。代码中对于joints关键字则进行了特殊处理,因此并没有输出。除此之外,还将左右小括号和左右大括号都作为单独一个Token输出到控制台。

  如果我们想重新解析整个字符串,那么我们可以使用IDoom3Tokenizer接口的reset方法,该方法会将当前索引设置到字符串的首位,这样继续循环调用getNextToken就可以重新解析整个字符串。

  我们也可以使用IDoom3Tokenizer的setSource方法重设要解析的字符串(另外一个字符串),setSource方法内部也会将当前索引重置到字符串的首位。

2.1.3 ES6中的模板字符串

  在此强调一下,使用ES6中的模板字符串是很不错的一种体验。笔者认为ES6模板字符串一个最大的优点是:可以一次定义多行字符串,并且保证空格和缩进,而单引号或双引号只能定义单行字符串。让我们来看一下笔者以前经常使用的定义多行字符串的三种方式。

1、在一行字符串中添加\n转义换行符,这种方式耗时又容易出错,具体代码如下所示:

let str1 : string = " numMeshes 5 \n /** \n * joints关键词定义了骨骼动画的bindPose \n */ \n joints { \n 'origin'-1 ( 0 0 0 ) ( -0.5 -0.5 -0.5 ) \n 'Body'0 ( -12.1038131714 0 79.004776001 ) ( -0.5 -0.5 -0.5 ) // origin \n } " ;

2、使用+=符号拼接字符串,代码如下所示:

let str2: string = " numMeshes  5 " ;
            str2 += " /* " ;
            str2 += " * joints关键词定义了骨骼动画的bindPose " ;
            str2 += " */ " ;
            str2 += " joints { " ;
            str2 += "  'origin'  -1  ( 0 0 0 )  ( -0.5  -0.5  -0.5 ) " ;
            str2 += "  'Body'     0  ( -12.1038131714  0  79.004776001 )  ( -0.5 -0.5 -0.5 )    // origin " ;
            str2 += " } " ;

3、使用数组方式并调用join函数将数组拼接成字符串,这种方式相对来说比较清晰,代码如下所示:

let str3: string = [
    " numMeshes  5 " ,
    " /* " ,
    " * joints关键词定义了骨骼动画的bindPose " ,
    " */ " ,
    " joints { " ,
        " 'origin'  -1  ( 0 0 0 )  ( -0.5  -0.5  -0.5 ) " ,
        " 'Body'    0  ( -12.1038131714  0  79.004776001 )  ( -0.5 -0.5 -0.5 )  // origin " ,
    " } "
] . join( " \n " ) ;    

  需要注意的一点是,这三种方式中处理字符串中的子字符串(例如orgin和Body),需要使用单引号引起来。

  如果不考虑兼容性,并且你当前的JavaScrit引擎支持ES6标准的话,那么能使用模板字符串的话就尽量使用模板字符串吧。当然模板字符串还支持以${ }方式定义变量,具体的用法请自行查阅TypeScript或ES6官方手册。

2.1.4 IDoom3Token与IDoom3Tokenizer接口的定义

  可以使用TypeScript的interface关键字来定义接口,具体代码如下所示:

export interface IDoom3Token {
    reset ( ) : void ;
    isString ( str : string ) : boolean ;
    readonly  type : ETokenType ;
    getString ( ) : string ;
    getFloat ( ) : number ;
    getInt ( ) : number ;
}

  关于IDoom3Token接口中大部分方法的应用,在上一节的代码中有演示,还是比较简单的。这里看一下type这个只读属性,该属性使用了readonly声明,意味着其值只能被读取,不能被更改。同时type的数据类型为ETokenType,是一个枚举类型。在TypeScript中,我们可以使用enum关键字来定义枚举类型,具体代码如下所示:

export enum ETokenType {
    NONE ,    // 0 default情况下,enum定义的枚举值是以0开始的数字类型
    STRING ,  // 1 表示字符串类型
    NUMBER  // 2 表示数字类型 
}

  最后看一下IDoom3Tokenizer的接口定义,代码如下所示:

export interface IDoom3Tokenizer {
    setSource ( source : string ) : void ;  //设置要解析的字符串
    reset ( ) : void ;  // 重置当前索引为0
    getNextToken ( token : IDoom3Token ) : boolean ;  // 获取下一个token
}

今天的内容到此结束,明天我们来解析如何以面向接口的方式实现IDoom3Tokenizer相关的接口方法。

TypeScript图形渲染实战2D架构设计与实现:第2章 使用TypeScript实现Doom3词法解析器(2:Token与Tokenizer)_第3张图片
基于WebGL的3D篇:目前在出版社三审三校中

3D篇:使用安德斯海尔斯伯格的TypeScript语言来演示约翰卡马克的Quake3/Doom3引擎相关内容!

决定弄个2D/3D书籍相关的群,群名和群号如下所示:

TypeScript图形渲染实战2D/3D架构设计与实现(689569128)

本人有一游戏行业与招聘方面的群,现有群友1700多群友,坑位不多了,若想找工作或HR及猎头,可以加入如下群:

游戏研发招聘群(232183160)

本人微信公众号如下所示:


TypeScript图形渲染实战2D架构设计与实现:第2章 使用TypeScript实现Doom3词法解析器(2:Token与Tokenizer)_第4张图片
微信公众号二维码.jpg

你可能感兴趣的:(TypeScript图形渲染实战2D架构设计与实现:第2章 使用TypeScript实现Doom3词法解析器(2:Token与Tokenizer))