2011年3月16日星期三

Game & FP

随意记点东西。


* Game

在Linux下SDL是最知名游戏库了。它本是作为商业产品移植Windows下游戏而生,所以作用是游戏和本地平台间的代理。其功能主要来说就是图像、声音、事件三个部分,也包括格式、字体和一些系统相关的组件。

不过呢,通常称SDL是多媒体库,是指它提供的功能在娱乐之外也会用到。而只要能让代码执行,不论是否属于多媒体,也可以用于娱乐的目的。

Java中处理2D图像有通常有两种方式,自带的Java2D或者系统的OpenGL。它们和SDL一样,都可以用来绘制2D图形。

不过在游戏中,绘制的概念已经有些底层了。通过抽象出Spirit的概念,可以更好的描绘游戏世界。例如按钮或者人物、背景都可以作为精灵对象出现在屏幕上,并参与交互。

一种做法是建立抽象的Entity类,供完成具体实现,并提供实用方法。例如Qt的GraphicsView中的Item类,提供的接口包括绘制托管、事件实例、坐标变换、碰撞检测。

最简单的Spirit会有两个最重要的接口,是update和paint。在固定帧率的游戏中,以固定的方式被调用。

基于对象的方式,可以使场景的设计可视化进行。一个例子是Scratch,只需要添加属性和事件,而不用直接面对事件分派和如何绘制的问题。

采用上面的抽象方法的一个问题是,为了简化逻辑我们希望主循环负责全部的事件处理,Spirit的职责仅需是查询抽象事件。

如果要给已提供Spirit添加功能的话,可以派生子类或者以其他的方式重用其组件。而另一方面,对于游戏又希望逻辑部分和平台部分分割开来。

这样的直觉是想到了MVC模式,游戏数据代表模型,游戏的展示效果代表视图。

游戏逻辑可以看作模型的一部分,他的作用是发挥在控制器部分——由视图的上操作来修改模型。而模型上的修改又被视图展示出来。可以通过观察者模式连接组件。

这样的话上面的Spirit类就有了功能上的细分,Image对象负责显示效果,Item对象负责管理Image对象,它们是视图的部分。模型上可以有Shape对象,它可以用来检测碰撞,以作出回应。也可以用来施加变换,并引起对应的Item的改变。

一些类的用法也就得到了丰富,如Tile类不是指单个的Spirit类的实例。而是提供地图的数据和对应到图像的方式,就可以获取碰撞情况并让视图负责显示出来。作为比较可以看J2ME和Slick2D在该模块上封装的差异。

所得的结论是:用对象的组合(包括拥有、回调、通知、链式)要比用类的扩展具有更高的灵活度。当已有的组件越丰富时,重用也就更加方便。由于可以重构实现,两种方式可以提供一样的接口,就可以按需选择。

对于游戏需要一个存放全局状态的地方,可以是一个单实例类,或者让用户在使用库时自己创建,或者让别的对象的方法增加表示这个状态对象的参数。


* UI

CLI和GUI的体验差异不是在视觉的感官上,因为通常的程序可以划分为以下三个部分:
1.平台依赖,包括硬件设备和所用的库;
2.用户界面,是和用户产生交互的地方;
3.程序逻辑,从上面两方面获得的信息对它们产生影响。

程序的逻辑可以封装成lib*.so,而CLI和GUI是属于UI的部分都可以作为这个so文件的提供的函数的视图。如果以是以鼠标还是用命令来区分界面的话,也不严密。因为CLI可以用curses库,而GUI也可以只放一个文本框。

这里想说的是,似乎对CLI和GUI的差异的感觉,是出于对有有状态和无状态的交互的差异感。多数CLI程序是无状态的,至少程序内不含系统的部分是无状态的。由命令调用后返回结果后,程序立即退出。

如果一个CLI在进入后是可以进行用户命令的交互话,这样的CLI就可以认为是有状态的。它的状态是以程序的进入和退出为一个的周期,这是一个交互的回话存在的周期。前后命令间会有影响,同样的命令会在不用的状态下可以有不同的意义(特别是Y/N的时候)。这里的交互是程序与人双方的,人可以由程序不同的状态执行不同的操作。

相比而言GUI是有状态的,我们称当前界面对用户的回应方式为模式。一个模式对话框弹出,然后用户使用并提交,这个CLI中有状态的交互方式是一致的。关于不同界面的效率问题,可以参见《人本界面》这是模式之外的另一种衡量方式。


* Iter

在Scheme中为了提高程序效率,可以把递归改写为尾递归。利用尾递归优化将递归变为迭代,此时引入副作用可以减少在表达“反复”时临时状态所带来的消耗。

这里以求5的阶乘为例:
(letrec ((f (lambda (x) (if (zero? x) 1 (* x (f (- x 1))))))) (f 5))
其中f是一个递归函数,自变量x在特定的值上递归终止。
引入一个迭代变量后得到尾递归形式:
(letrec ((f (lambda (r x) (if (zero? x) r (f (* r x) (- x 1)))))) (f 1 5))
这个利于语法糖衣也能写为:
(let loop ((r 1) (x 5)) (if (zero? x) r (loop (* r x) (- x 1))))
(do ((x 5 (- x 1)) (r 1 (* r x))) ((zero? x) r))
其中的共同点是在使用跌倒变量保存中间结果,并在迭代终止是将结果返回出来。
也可以在引入一个变量i供迭代用,原本的x以原样传给它的连续:
(let loop ((r 1) (i 1) (x 5)) (if (> i x) r (loop (* r i) (+ i 1) x)))

另一种例子是不使用数而使用列表:
(letrec ((f (lambda (x) (if (zero? x) '() (cons x (f (- x 1))))))) (f 5))
按同样的方式改写,并引入副作用的话,就得到(set! r (cons ... r))。这的使用方式是一个Stack结构,引入有针对的实现可以提高代码的执行效率。
并且有时为了更好的查询迭代情况,可以引入专门结构冗余的存储信息。

迭代代表了从一个状态到下一个状态的映射,并且不同于递归,我们所求的信息是最终状态的一部分,而不需要综合所有的状态来得到结论。

(cons+delay可用于过程见间的协同,display的话就不用储存临时数据了。)

map函数可以看作是基于flod的,或直接改写为对cdr的递归直到null?


* IO

在纯函数语言中,副作用是被严格禁止的。而IO操作的副作用是无法避免的,这就需要了间接的处理方式。
先以对变量的副作用为例,方法是将它作为自变量引入所有和它有关的函数。如果需要改变该变量的值,则直接原样传给它的连续,否则传入修改后的值。
我们可以所有可以被复制的变量看作一个整体,代表副作用发生的范围,在跳转到连续时作为一直存在的参数。
在带类型的Lambda中IO的情况略有不同,它不是连续传递时额外的参数,而是代表了函数返回值的一种类型。
例如一个数学返回的数值和IO.read读取返回的数值,虽然都是数值却是不同的类型。就像代表梨子的数目和代表苹果的数目,只有在水果的意义上才能直接相加,此时已经从子类变为共同的父类。
同样是IO类型的连续,可以依次执行传递变量。通过IO类型和一般类型间的转换,可以将副作用限定在一定的区域内,从而简化了整个程序的逻辑。


* Overload

在C++中,函数的Overload和类的成员非virtual函数都在编译时确定所调用的函数的。它们根据调用的参数变量的类型,将同名的函数保存为不同的函数。
我们将一个结构和针对该结构的方法看作一个整体,在C语言中函数的命名是唯一的,习惯上会给函数的名称添加代表类型的前缀。当使用Overload或使用非virtual成员函数时,就不需要这个前缀了。
此时代表相同意义的操作符,可以使用相同的名称,并作为模板的参数时在编译时检查方法是否存在。
当方法名称和模板中代码调用的方式不一致时,可以引入一个代理类。但是在一致的时候,也可能只是巧合,不代表语义正确。
这里可以的方法是引入代表语义的Concept,需要显示的表明某类型可以的调用方式,并提供相应名称的一组函数。
静态的多态相当于该函数的参数的类型是它所有Overload了的类型的并集,这个并集是在编译时临时产生的。
而动态多态,也就是Override的方式,是以函数作为了结构的一个成员。该成员的存在是类型的一部分,而该成员是不同的函数所带来的不同的行为,不一定要产生一个新的类型。
至于static member则是和namespace一类的东西,可以include。

(CLOS是形式像这里不过是动态的,外加上模板和类型擦除都叫泛型来着。)


* Subclass

在Java中extend表示扩充某个类,可以是添加新的成员而不影响原有的东西。而implement表示提供特定作用的一组函数用于回调。
不过实际上extend包含的功能是多方面的:
1.类的扩充,把父类看作子类的一个成员,并提供方法的委托调用。
2.实现了父类隐含的一个界面,将自己提供给父类调用改变了父类的成员。
3.子类是父类的子类型,子类可以充当父类使用。
4.提供默认的访问控制,虽然说其实访问控制是可以认为改变的。(指protected,其他的可看作词法作用域)
5.给interface的方法提供了默认实现。

需要说明的是:
1.当需要类A以界面B的方式使用时,不是必须要类A派生自界面B,而可以用代理模式实现界面B,也可以实现其他的界面。这里可以看作一个类型转换,或者是Functor,用C++模板和Inline可以在编译期完成。
2.当把类继承看作是代码复用的话,一个类是可以使用其他多个类的对象的。一个类型可以是多个父类的子类,这和它复用其他类的代码是无关的。
3.当把函数也看作对象时,Override实际上是改变一个结构的成员,与类型无关。
4.结论是,单继承或者类的派生是一个混合产物。

再比较C++和Java中的Subclass的话,C++提供了一种灵活性更高的机制,而Java的优点是提供方面思考的模型。

还有一些OO是提供消息传递实现的,Class的作用是工厂,不同于这里的类型关系。


* OCaml & Haskell

现在对ML语言很有好感,其中代表性的实现是OCaml。该语言较明显的特征是可扩充的预处理和带类型的lambda演算。自己写起来比Scheme慢不过产物看起来更加严密一点,这样的缺点是藏丑陋但能用的代码要麻烦一些。

有个很显眼的特性叫Algebraic data type,例如通过Tuple定义List:
type 'a pair=Nil|Cons of 'a*'a pair
let null p=match p with
| Nil -> true
| _ -> false
let car p=match p with
| Cons(x,_) -> Some x
| Nil -> None
let cdr p=match p with
| Cons(_,x) -> Some x
| Nil -> None
List和Record都可以看作由Tuple定义,此外也可以这样来定义数和数的运算。
每个函数只接受一个参数,可以接收Tuple,或者返回可接受参数的函数再接收参数。

Haskell中的type class实现了静态多态和语义上的约束的表示,在Ocaml中是模块的Functor用于算法对于类型的复用,这里算一致的。不过默认不用静态多态是的Ocaml中整数除法和浮点数乘法得用不同的名称,或者显式转换为操作的。
静态类型的派生关系特点是子类可以映射到父类被使用,而不代表子类型可以充当父类存在。如果不涉及赋值或函数调用的话,这个细节不明显。

OCaml有个比较简单保守的core部分,好处是ML语言存在时间较长,使用起来简单一些。Haskell看起来有很多有趣的想法,不过比较零碎,以自己口味不大用得惯。它们的资料在各自官网上都有提供,包括教程、手册以及其他的文字。这里不往下说了。
它们都有源自数学的抽象的概念,这些还没有涉及过。

Miranda和Maxima和Axiom中的表达式都很有数学表达式的样子,视觉上充满了表达力。


* 相关链接

没有评论: