2011年6月2日星期四

又一个坑了的视觉小说,\n顺便说说Java与Scheme的混合/嵌入使用

拿Slick2d和Kawa包了一组给Scheme用的API出来,做了一个简单而拙劣的VisualNovel演示。话说第三方库有源代码和JavaDoc外加严格的类型系统和Eclipse的辅助,使用起来还是挺方便的。
下面的东西是个试验用的半成品,不过后来发觉又依赖JRE又依赖平台二进制包感觉挺恐怖的。想分发或者跨平台的话暂且都有问题,也就没动力继续去完善了这个自命名Slickawa的包了。
于是代码什么的在很久很久以前就不继续了,现在拿出来发觉可以用来写篇日志,相当于随手写写遇到的想法吧。

话说Slick的提供的库的API让自己挺欣赏的,至少是打消了再去包装它一层的看法(比如SDL就一定要先包一个主循环出来类出来供派生才觉得舒服)。不过要注意的是Slick是在OpenGL上的一层包装,如果觉得它提供了某个抽象合适的话,就拿来用,或者直接看它的代码。如果不合适的话,大可不必考虑某些类的存在,按照自己的需要直接从更底层的方法往上层建立。达到目的即可,而且目的肯定不是重复一个和Slick一样的抽象层(比如移植其他平台用)吧。
不过SDL1.2和OpenGL用起来还是有个很明显的区别(就像PyGame和PyGlet),它们负责图像混合的设备不同。前者主要靠CPU,后者主要靠GPU,如果不这样顺着它们的原理用的话,性能会大有问题(所以C或者Shader的使用最好是无限制)。如果要在多媒体程序里做一些视觉效果的话,真的还是很容易出现性能瓶颈的。

给Scheme的API分为四组,sk中是基本的过程,skex中是可以用Scm本身来实现的,sklab是试验用方法,sklib是依赖底层的方法。这分别对应四个Java的Class,要暴露的方法实现为静态的,这样在Kawa中就可以直接使用了。
算是语言间类型的自动绑定吧,其实呢Kawa是把Scheme代码编译为Java的字节码,这样在性能上和互操作上都很方便。不过想在Kawa完全使用Java库还是有些不便,所以还是提供为简单的一个个过程较方便。对象仍旧是对象,方法变为以对象为参数的过程。
现在让Scheme代码和Java代码结合的地方可以有两种,一种是用Kawa提供的机制直接在Scm中使用Java的包和对象,一种是在Java中用Kawa中提供的类型映射类创建实例交给自己管理的Scheme运行环境。不过这两种方式都不要指望Kawa能够是Scheme和Java部分能够完全的分离,必然会有一个中间层在Scheme中操作了Java的数组,或者是在Java中操作了Scm的列表。
如果需要添加什么功能的话,要么单独的写为一个包,要么就在那个中间层里放心大胆的写。不必在意是写为纯Java的还是纯Scheme的为好,被中间层所选用的语言所限,不然在使用所依赖的库的时候会遇到麻烦。如果担心会导致所暴露的API会紊乱的话,就放到不同的命名空间里去。也不必在意是否暴露的足够的API,只要要中间层有足够的活性(有废止的方法标记一下就可以了,反正只涉及静态方法和单方法的界面,单一元素的修改仅会带来很有限的影响。),混合语言所带来的将不是负担而是效率了。中间层即使Java和Scheme语言同时用也可,目的都是让纯Scm部分的代码可以用某个函数,所以还是倾向于在Java中写这个中间层同时Scheme也用。至于Java6的那个对脚本语言的框架,造成通常得首先包装为对象传给那个框架,然后在用脚本语言提供适合自身类型的包装。用来两次包装之后,想实现一些功能的话,放在哪个部件里都觉得不是很方便。
相比另一个Scheme的Java实现SISC,本来自己是觉得它更小巧一些,所以一开始的更偏爱一些。后来觉得Kawa做得更成熟一些,于是就从比较省事的角度选择的Kawa。之前在用tinyscheme做过一次绑定,得自己添加垃圾收集用的类型,再添加Unicode编码的支持,再针对类型的转换把提供的API函数一个个写一遍。而且用到的函数得去tinyscheme的头文件里找,然后再去长长的单个源代码里看看是怎么回事后才用,再传递指针的时候很容易就把类型信息丢掉。结果就是TinyScheme的代码也混杂在宿主程序里面一起修改了。Java的好处是可以把单独要改的类拿出来改一下然后用(只要不涉及包的可见性以及开源的许可协议的限定的话),原本引入的jar文件可以丝毫不变的放在那里。不过话说似乎好多Scm实现都是为单独使用设计的,想手工包装到C/C++里面都有点手疼,而更倾向于用FFI在Scm代码中用C库(于是对第三方库就可能是先用C包一层,然后再Scm针对类型转换包一层了。这样总比在C中连类似什么也考虑要手疼的好一些,虽然这里来说说感想也看不出多少可再用的价值)。
有个chibi-scheme时间不如TinyScheme,但是设计得更加完善一些。

Java和Scheme都有Steele的参与,这或许是让两者看起来很协调的一方面吧(只是或许而已)。两者都有各自的变量命名习惯,都喜欢使用长的n的单词组合。虽然一个用大小写来分割单词,一个用横线来分割单词,但是这两者对应起来还是相当直观。都是很少用语法糖,都是写起来手疼,都是值是有类型的对象的语言。所以两者混合用起来还是蛮协调一致的感觉。
Scheme的运行环境有两个地方,一个是让Scm代码自己成为一个Java类,一个是人工的管理一个Runtime对象。如果仅仅是把Scheme作为数据来看待的话,似乎后者会比较方便。Scm的一个优势是自身可以当作一种数据格式来使用,Kawa中提供了解析供的方法。而让Scm代码去实现某个interface然后传回Java也是可以的。此外还可以main中直接eval,提供一些API后控制权给脚本,让Scheme当胶水使用。
如果仅仅是想REPL地试验一下某个Java代码片段,有个BeanShell还是蛮方便用的。

关于下面的实现,还有些额外的说明。这里就列举一下,语句会不大通顺。首先呢,当类型不重要的时候,用Scm中的List来传递值了,这样比从对象里查询要方便一些(如果List是Lazy的话就更方便了,语言的执行就是 Graph Reduction)。其次来说,这里Java的主循环三种事件要调用Scheme提供的函数。Java部分只要只管要时去调用,而场景的切换放在Scm里面做了。至于场景切换要怎么修改提供给Java的过程,以及场景切换中还是否要做一些额外的事情,Java部分是不用管的。所以这里为了方便的在Scm中实现多个场景,做了一个简单的OO。注意逻辑和渲染和必须分割开来的,中间可以用全局(相对两者而言)来传递信息。还有呢,涉及RGBA混合的过程无论写在Scheme中还是写在Java中都是会性能不够的,正确做法是写在OpenGL里面,可惜自己还不会。动画效果这里参考了Qt的状态和变换框架,这里仅仅提供了一个用来进行y=kx线性变化的类。其实利用y=f(kx1)+g(kx2)的组合还是能做出很多效果的,比如以线性变化为时间轴映射到多个变量上,或者串联多个线性变换,这些都是可以发挥想象里的地方。然后作用坐标、透明度、颜色以及其他可以数值表示的地方,看起来还是挺生动的(这方面Flash是榜样,不过Flash性能限制)。

写的时候参照了KRKR2/KAG3和ONScripter和RenPy的部分代码,以及rpgchina.net和gamediy.net上的教程,以及Slick和PyGame和ClanLib的API:
;;;开始
(sk:make "VisualNovelDemo")
(sk:path ".;visualnovel")
(sk:display-mode 800 600 #f)
(sk:fps 60)
(sk:debug #t)
;;;OO
(define (send obj msg . arg)
  (cond ((obj msg)
         =>(lambda (x) (apply x arg)))))
(define (dispatch prototype method-alist)
  (lambda (mth)
    (cond
      ((assoc mth method-alist) => (lambda (x) (cadr x)))
      ((procedure? prototype) (prototype mth) )
      (else #f))))
;;;全局变量
(define *env* (interaction-environment))
(define *background* #f)
;(define *script* (sk:load-data "script.scm"))
(define *script* (skex:load-line-data "script.scm"))
(define *scripts-now* #f)
;;;全局过程
;(define *music* #f)
(define (state-clear)
  (set! *scene*
        (dispatch '()
                  (list
                   (list 'frame (lambda (delta) '()))
                   (list 'button (lambda (b) '()))
                   (list 'render (lambda (g) '()))
                   )))
  )
(define (dialog-jump! label)
  ;(sk:display "try")(sk:display label)
  (let ((s (member (list 'label label) *script*)))
    (sk:display s)
    (when s
      (state-clear)
      (set! *scripts-now* s)
      (script-next!)
      ;(sk:display (state))
      )))
;;;场景
(define *scene*
  (dispatch '()
            (list
             (list 'frame (lambda (delta) '()))
             (list 'button (lambda (b) '()))
             (list 'render (lambda (g) '()))
             )))
;;;对话场景
(define (scene-dialog text)
  (define dialog-font (sk:load-font "Simhei" 32))
  (define *dialog-char-count* (skex:make-linear 0 (sk:string-length text) 1))
  (define (dialog-end?)
    (skex:linear-end? *dialog-char-count*))
  (define (dialog-end!)
    (skex:linear-end! *dialog-char-count*))
  (define (on-frame delta)
    (skex:linear-next! *dialog-char-count* (* 0.03 delta)))
  (define (on-render g)
    (sk:draw-color 30 428 (sk:color 0 0 0 128) (- 800 60) 128 )
    (skex:draw-text
     34 432
     (sk:substring text 0 (skex:linear-now *dialog-char-count*))
     (- 800 64) dialog-font (sk:color 0 0 0 128))
    (skex:draw-text
     32 430
     (sk:substring text 0 (skex:linear-now *dialog-char-count*))
     (- 800 64) dialog-font (sk:color 255 255 255 255))
    (when (dialog-end?)
      (sk:draw-color 740 530 (sk:color 128 255 128 255) 8 8)
      )
    )
  
  (define (on-button b)
    (if (or (equal? b '(pressed A)) (and (pair? b)(eq? (car b) 'touch)))
        (if (dialog-end?)
            (script-next!)
            (dialog-end!))))
  (dispatch
   '()
   (list
    (list 'frame on-frame )
    (list 'button on-button )
    (list 'render on-render )
    ))
  )
;;;scene-effect 背景切换效果
;;;背景切换场景
;;;(bg "1.png" 0)
(define (scene-bg-change arg)
  (define *bg-future* (sk:load-image (cadr arg)))
  (define *bg-changing* (skex:make-linear 0 1 (/ 1 (caddr arg))))
  (define on-frame
    (lambda (delta)
      (skex:linear-next! *bg-changing* delta)
      (when (skex:linear-end? *bg-changing*)
        (set! *background* *bg-future*)
        (set! *bg-changing* #f)
        (script-next!))))
  (define on-button
    (lambda (b) '()))
  (define (on-render g)
    (sk:draw-image 0 0 *bg-future* (* 255 (skex:linear-now *bg-changing*))))
  (dispatch '()
            (list
             (list 'frame on-frame )
             (list 'button on-button )
             (list 'render on-render ))))
;;;scene-effect 背景遮片效果
;;;effect-mask
;;;(effect mask "1.png" "rule.png" 100)
;;;name = 'mask
;;;arg = '(mask "1.png" "rule.png" 100)
(define (scene-bg-effect arg)
  ;;to extend this lambda class
  (define (make-effect bg-future effect-render-with-p% time-last)
    (dispatch '()
              (list
               (list 'frame (lambda (delta) '()))
               (list 'button (lambda (b) '()))
               (list 'render (lambda (g) '()))
               )))
  (case (cadr arg)
    ((mask) (scene-bg-mask-change (cdr arg)))
    ((bg) (scene-bg-change (cdr arg)))
    (else (error "effect"))))
;;;'(mask "1.png" "rule.png" 100)
(define (scene-bg-mask-change arg)
  (define *bg-future* (sk:load-image (cadr arg)))
  (define *bg-mask* (sk:load-image (caddr arg)))
  (define *time-line* (skex:make-linear 0 1 (/ 1 (cadddr arg))))
  (define on-frame
    (lambda (delta)
      (skex:linear-next! *time-line* delta)
      (when (skex:linear-end? *time-line*)
        (set! *background* *bg-future*)
        (set! *time-line* #f)
        (script-next!))))
  (define on-button
    (lambda (b) '()))
  (define (on-render g)
    (sklib:draw-image-with-mask 0 0 *bg-future* *bg-mask* (* 255 (skex:linear-now *time-line*))))
  (dispatch '()
            (list
             (list 'frame on-frame )
             (list 'button on-button )
             (list 'render on-render ))))
;;;用户界面场景
(define (scene-ui arg)
  ;(ui ((text 116 500 "开始" fnt clr) start) ((text 516 500 "退出" fnt clr) end))
  (define *ui* '())
  (define (ui-draw x)
    (map (lambda (i)
           (apply
            (case (caar i)
              ((text) sk:draw-string)
              ((image) sk:draw-image)
              (else (error i)))
            (map (lambda (j) (eval j *env*)) (cdar i))))
         x))
  (define (ui-area x)
    (map
     (lambda (i)
       (list
        (let ((arg (map (lambda (j) (eval j *env*)) (cdar i))))
          (let ((x (car arg))
                (y (cadr arg))
                (s (case (caar i)
                     ((text) (sk:text-size (caddr arg) (cadddr arg)))
                     ((image) sk:draw-image)
                     (else (error i)))))
            (list x y (car s) (cadr s))))
        (cadr i)))
     x))
  (define *button-area* #f)
  (set!
   *scene*
   (dispatch
    '()
    (list
     (list
      'frame
      (lambda (delta) '()))
     (list
      'button
      (lambda (b)
        ;(sk:display (ui-area *ui*))
        (when (and (pair? b)(eq? (car b) 'touch))
          ;(touched (1 2))
          (let ((x (caadr b))(y (cadadr b)))
            (map (lambda (i)
                   (when (skex:contained? x y (car i))
                     ;(sk:display (cadr i))
                     (when (symbol? (cadr i))
                       (dialog-jump! (cadr i)))))
                 (ui-area *ui*))))
        (when (and (pair? b)(eq? (car b) 'cursor))
          ;(touched (1 2))
          (let ((x (caadr b))(y (cadadr b)))
            (set! *button-area* #f)
            (map (lambda (i)
                   (when (skex:contained? x y (car i))
                     (set! *button-area* (or *button-area* (car i))))
                   (when *button-area*
                     (set!
                      *button-area*
                      (apply
                       (lambda (a b c d) (list a b (+ 2 c) (+ 2 d)))
                       *button-area*))))
                 (ui-area *ui*))))))
     (list
      'render
      (lambda (g)
        (when *button-area*
          (apply sk:fill (append *button-area* (list (sk:color 0 0 0 128)))))
        (ui-draw *ui*)))
     )))
  (sk:display (cdr arg))
  (set! *ui* (cdr arg))
  *scene*)
;;;解析并执行脚本
(define (script-next!)
  (if (not (null? (cdr *scripts-now*)))
      (begin
        (set! *scripts-now* (cdr *scripts-now*))
        (state-clear)
        (cond ((string? (car *scripts-now*))
               ;;显示对话
               ;;"Hello World"
               (set! *scene* (scene-dialog (car *scripts-now*))))
              ((pair? (car *scripts-now*))
               (case (caar *scripts-now*)
                 ((bg)
                  ;;切换背景
                  ;;(bg "05_1_2.jpg" 100)
                  (set! *scene* (scene-bg-change (car *scripts-now*))))
                 ((effect)
                  ;;切换效果
                  ;;(effect mask "sdf.jpg" "rule/12.png" 2000)
                  ;;(effect bg "rule/12.png" 2000)
                  ;;(effect bg:mask "rule/12.png" 2000)
                  (set! *scene* (scene-bg-effect (car *scripts-now*))))
                 ((ui)
                  ;;显示界面
                  ;;(ui ((text 116 500 "开始" fnt clr) start) ((text 516 500 "退出" fnt clr) end))
                  (set! *scene*(scene-ui (car *scripts-now*))))
                 ((music)
                  ;;播放音乐
                  ;;(music "11.ogg" 100)
                  ;(sk:play-music (sk:load-music (cadr (car *scripts-now*))) #t)
(sk:play-music (cadr (car *scripts-now*)) #t)
                  (script-next!))
                 ((eval)
                  ;;(eval (set! fnt (sk:load-font "Simhei" 48)))
                  (eval (cadr (car *scripts-now*)) *env*)
                  (script-next!))
                 ((label)
                  (script-next!))
                 ((jump)
                  (dialog-jump! (cadr (car *scripts-now*))))
                 (else (error "未知指令"))))
              ((null? (car *scripts-now*)) (script-next!))
              (else (error "未知指令"))))
      (sk:exit)
      ))

(sk:on-init
 (lambda (e)
   ;;;载入剧本
   (set! *scripts-now* (cons '() *script*))
   ;(set! bgm (sk:load-music "music/bgm03.ogg"))
   (script-next!)))
(sk:on-frame
 (lambda (delta)
   (send *scene* 'frame delta)))
(sk:on-button
 (lambda (b)
   (send *scene* 'button b)))
(sk:on-render
 (lambda (g)
   (when *background*
     (sk:draw-image 0 0 *background* 255))
   (send *scene* 'render g)))
(sk:start)

所用的脚本格式中@开始的行是命令,读取是首尾加括号,不然则加引号。读取后是一个sexp的列表,这样可以在Scheme中直接操作。
话说IEEE标准里没有收eval语句,因为有点细节上的欠缺。虽然R5RS里有,个人还是觉得这里也不必要去用完整的eval过程,不过自己改写出来一个eval还是个重复劳动。
脚本就是纯文本,话说一开始的写法是每个字符串要加引号。虽然有正则这种东西,但是想重新分段什么的还是不方便。与其每次保存是要处理,不如直接扩充Reader部分。
上面的代码在重构的时候消灭了很多全局变量,顺便也抽象出来一些较直观的概念。这些修改不影响跑测试代码,但是这体现Scm代码的进化方式。
以上的OO是基于原型的消息传递,还缺少一个set-this消息,继承时会用到。不过添加这样一个消息的话,对已有代码影响很小,而且暂时没用到,所以还没有添加。

Java部分的代码或许以后传吧,没多少内容
不过还是暂且来简单的写一些说明吧——
首先呢,是最常用到的若干包和类(以及类的方法):
kawa.standard.Scheme 对应 Scheme的一个运行环境;
gnu.lists.LList 对应 Scheme中的类型,列表;
gnu.mapping.Symbol 对应 Scheme中的类型,符号;
至于整型就是Integer,字符串就是String
想把sexp字符转为Scheme的嵌套Pair的h话:
LispReader reader = new LispReader(new InPort(new StringReader(sexp)));
return ReaderParens.readList(reader, 0, 1, -1);
至于Number的话可能是gnu.math.Numeric也可能是java.lang.Number,
如要用instanceof判断之后再获取值,比如调用.floatValue();。
此外,现在绘制是一堆draw-*的过程,另一种实现方式是:
提供 <? extends Drawable & Posable ...>然后统一的用draw来调用。
这样就转变为考虑怎样在场景里放置物品,而让绘制操作可以自动进行。
这部分暂时还没用到,只是在Java中申明了Interface,还是放到Scm中需要的时候写这部分好了。
话说一般用对象的使用,以及界面的实现好了,类的派生什么的还是少用为好。
比如这里Java给Scheme的API就不包括类的派生,不过Silck2D的习惯本身也没有精灵类。
有时候嘛,精灵类还是需要的,用来管理场景里哪些东西要绘制,若仅仅是OO的话,还是放在Scm中为好。
在Scheme中可以用lambda来做参数传递时转化为实现了所需要的只有一个方法的界面,
此外Kawa提供的一些机制还是可以用虽然目前不够完善所以用着不舒服。
kawa中导入Java的类是使用(define-namespace sk ),
然后可以直接使用静态成员方法,创建和使用对象也有提供了方法的。
想开一个交互的调试窗口的话,可以执行如下的代码(可以绑定到按键上):
ReplDocument doc = new ReplDocument(scm, scm.getEnvironment(), true);
GuiConsole con = new GuiConsole(doc);
con.setVisible(true);
其中scm是Kawa提供的Scheme类的实例,也就是我们的scm代码编译后运行的环境。
像Gambit-C一类编译类的Scm实现,直接和C混合着用虽然看起来不够美观。
但总比让FFI的部分复杂要好,毕竟C在内存管理和类型转换上比Java的鸿沟大。
另一个编译器Chicken是可以即当编译器也可嵌入当解释器用,不过Chicken的内存管理方式比较异类。
值得庆幸的是,两者都可以定义变量在GC时执行的过程,这样可以省写一些Free(不要和Close混淆哦,open/close是成对等价于with使用的)。
是嵌入使用,还是有FFI调用外部类,不同的实现有不同的方式的。此外,用C库的话,还有字符串编码问题要自行解决一下。
关于语言标准,IEEE出于简化考虑比R5RS少了eval和卫生宏,不过R5RS还没有module和record-type这两个很有用的组织代码的手段。
再如果仅仅是拿sexp当数据格式来使用的话,那就和使用什么实现没有关系了,手工解决即可。



剧本部分是拿以前的日志重写的,现在有点反胃了。
所有@开始的行已删除,现在算是一个纯文本了。
说实话,废话比以前多了,而且人物感觉更加薄弱。
在遥远的被阻隔的异世界
记忆之海
埋藏着记忆力淡去的痕迹
虽然显得遥远而迷茫
可是……

《遥远的记忆之海》

【Chapter 0】
这里是死后的世界,而年轻的我是记忆之海上的渡船人。
每天都会有亡灵抵达这里,来在这片汹涌的波涛下探寻往日的回忆。
虽然只是一些被遗忘的影像,这恐怕是对被阻隔在异世界的亡灵们有效的抚慰。
我将用这片小舟载着他们,驶向掩埋于他们意识的幻境之中,去探寻这份被遗忘的记忆。
不知道自己是如何开始这项职业的,也不知道是否在遥远的未来会有某个终结点。
大概我便会这么一直以这样平淡而幸福的方式生活下去吧——每天都可以期待不同的故事。
而自己就做好一个船夫好了,这点好奇心尚不会为自己的记忆增加多余的烦恼。

【Chapter 1】
【我】 坐稳呦~要出发了~
我在船尾向着船头走上来的一位老奶奶说道。
然后熟练而悠闲地划动起身旁的桨,让船缓缓却高速地飞向某个预定的水域。
海水在炫目却冰凉的天空下闪着光芒,一眼望去只能看见浅浅的海面。
目标抵达后,我让船平稳地停下而随意地顺着波浪起伏,然后向着船上老人站立的那一头说到——
【我】 你的记忆便保存在这里,在这片浑浊的水面之下。不过还有若干事项要知道——
【我】 在波浪起来的时候,要努力地往海底望,凭自己的坚定的意志去努力地望。
【我】 让自己的身躯仿佛为回忆包围、洗涤、沉浸,直到某一刻海水骤然变得清晰。
【我】 这样你便能看见深深的海底,看见那片保存着你的记忆的地方。
【我】 那是只属于你的地方,只有凭自己的力量才能看见,而别人的帮助对此无能为力。
而老人听见了我的话,点了点头,然后将视线远远的望着海平面的尽头。
她用手触摸着船的边沿,却一直没有敢低下头去将视线触及身旁水面的光芒。
虽然犹豫了一会,老人随后还是放弃了努力,也没有去尝试,转身向内在船头坐下。
【我】 就真的这样放弃了吗,这可是一个难得的机会,不需要那个世界的记忆了吗。
每个人只有一次机会来这里探寻活着时候的记忆,老人的行为多少让我不解。
【老人】 确实呢……
老人轻轻地应了声,然后坦然地点了点头,颤动着的嘴唇似乎有话要说。
我也暂且安下心来,在这水天包裹的空间里躺在船的一角,听着老人传来的言语:
【老人】 随着人生漫长的时光,我已经是一个年迈的人了。记忆力在一天天流逝,身体也一天天薄弱。
【老人】 变得不再有力气去触摸身边的世界中的幸福,而对周围事物的意识也越渐模糊。
【老人】 活着,自己却越来越难以感受到自己是存在着的。于是生命的最终一刻,便在完全不知觉中抵达了。
【老人】 过完了一生,纵然有坎坷或遗憾,可终究还是幸福的一生,可以让我安心于死后了。
【老人】 可是衰老的记忆力依旧是留下的残缺,对于自己最后的时光,竟然不能在心里珍藏着这份幸福。
【老人】 所以我便来到了这片记忆之海,想弥补这份残缺,想为自己寻找一份安慰。
【老人】 至少不论生后或生后,我想让自己明白,自己确实是幸福的生活着的。
【老人】 不过啊,果然我还是有这份幸福感便足够了呢。
【老人】 这种感受纵然已经被遗忘,也肯定在那个世界里曾经存在过吧。
我打量着对面的老者,觉得她是在害怕由记忆去触及内心的真实,不禁叹气感到遗憾。
老人在船头沉默地坐着,渐渐平静了略带激动的言语,我随后起身撑起船驶上回程之路。
伴着海平面上柔和而巨大的夕阳,船的划痕消失在身后的那片先前抵达过的水域,
这也同时告别了那里那些对于老人将永远被掩埋于海底的对往日的记忆与情感。
这过去的,便是一个普通的工作日,随着一天天的过去,我不久也就会把这天淡忘的。
因为这毕竟只是我不断往复着的类似的日子中的一天,一个没有源头也没有终点的记忆所背负的时光。
接下来一天的客户是一位中年男子,身体健壮没有疾病的样子,看上去是个生活中积极的人。
他是意外而死去的吧,真是遗憾呢,我简单地这样判断着。然后撑起船,又一起踏上一个新的旅途。

【Chapter 2】
船平稳地向预定的某地行去,今天的海面的波浪依旧缓和,我在船头悠闲地把着方向。
而他在船的另一头显出好奇,往四周那片行船偶然路过的水天之痕,不断地张望。
【我】 这途中附近不知是埋藏着谁的记忆,在无尽的记忆之海上,恐怕连我也可能不再会抵达这里。
我静静地坐在船的末端,以船夫的身份这样解释道,然后视线依旧向着遥远的目光不能触及的何处。
【中年】 好了,我是有点兴奋,毕竟是第一次来到记忆之海。如果这样做对航船有影响的话,我会注意的。
我能够感受到他一旁的视线,那似乎是一种寻求着言语上的联系的期待,这恐怕是出于他待人的热情吧。
【我】 我想知道,你为什么要用这唯一的机会,到这里探寻自己的记忆呢?你还没有到一个健忘的年纪。
【中年】 我来寻找记忆,并不是因为它们被我遗忘,而是因为它们所承载的时光已经离我而远去。
【中年】 我是想寻找一丝安慰——即使将来记忆变得淡去,记忆也曾经凭借着自我的存在而存在过。
【中年】 这也是那个生者的世界所留给我的痕迹,告诉自己往日的努力的意义所在,没有遗憾也是一种遗憾吧。
【中年】 那些记忆纵然不能在物质上于现在现在的我建立起沟通,但是分明就是我在这里可以依旧延续下的一种财富。
【中年】 虽然记忆显得飘渺但却是中真实,这也就是我来到记忆之海缘由,想在这里挖掘到的一份心灵中的宝藏。
船速已经变缓,这是即将到达预定的海域了,我向他做着关于探寻记忆的说明,引着他往船外沿的地方走去。
然后留下他一人等待海风的气息掀起记忆之海翻滚的浪花,祝福他的思绪将能够触及到脚下海水的彼端世界。
风与浪下船头的挺拔地站立着,在海天间映出一个厚实的身影,轮廓仿佛是一扇凭空为世界展开的门。
他的声音在海风之间,在倾诉着言语,在一层层的展开着他的过去——
【中年】 这是我的出生,这是我的父母,这是我的学校,
【中年】 我的成年,我的工作,我的老婆,我的女儿,我的事业,我的生活……
男人在回忆着他活着的经历,眼前海水的蔚蓝色里翻滚着幸福的味道,一丝腥味,又一丝甘甜。
在随后回程的航线上,他一直在向我分享着他在这里获得的满足——
【中年】 美好的期待一直在延续着,直到有一天,那天的一场意外的事故,让我只能告别那一切。
【中年】 回忆是以痛苦终结的,我想把这最后的时刻忘掉,可是又害怕失去了想守护的东西。
【中年】 我便这样犹豫着在这个死后的世界,后来想法开始有些坦然——
【中年】 记忆的存在并不是因为它们被记住,而是它们被经历过。
【中年】 记忆的痕迹即使随着时光淡去而似乎不曾有过,它们也不会消失依然存在。
【中年】 不经意间看到自己身体上留下的微小残缺,知道这便是一种以不可弥补的痕迹来保存着记忆。
【中年】 我想我脑海中的记忆也是一样的,是因为我曾今为着他们的存在而做出过什么。
【中年】 这便足够了,它们就是我在记忆之海下埋藏的一份财富,将永远为海天守候。
【中年】 所以,记忆里的东西无法改变,也不需要改变了,在现在这个世界里依然可以有新的记忆。
他只是说着他想说出来的事情,似乎不在意我是怎样漫不经心地听着,这样能否记住他说出的话。
在傍晚的时候,打量着我们周围海面似乎永恒不变的夕阳景色,我想——
他说出了现在的感受,这样便是让这种感受以与人共同的记忆的形式确认着存在,让自己能够保留下去。
不过即使这样,着对于我,仍只是不知过去与未来的时光里的一天,一个在永恒的时间轴上的一个渺小痕迹。
而接下来一天的客人是位年轻的学生,年龄似乎与我接近而略小,我依然只是职业地将他引上船。
今天的天空似乎有些异样的阴沉,不过不会影响到航行,于是这又将开始一个新的旅程。

【Chapter 3】
我控着方向,让船往预定的地点驶去,在有些阴沉的天空下划过淡淡的线条。
他和来到记忆之海的大多数人一样,在船的前头打量周围,以未知抚慰着未知,显得有些无措。
回忆的味道就是这样的吧,我也顺着他的视线向远方望去,心中划过这样的想法。
其实的船行不需要过多的体力操作,在今天格外低的天空之下,航行显得有些沉闷。
不过如果天空晴朗,这就是个同往日一样的航行了,充满生活的平淡的温馨,平淡便足够了。
我沉浸在自己的思绪中,随后忽然感到船身的一个摇晃,我看见船对面的那个身影站起身子。
他向我走来,我赶忙先稳定下船的航行,然后放松下身子。此时听见他说话的声音——
【青年】 您比我大吧,我可以叫你哥哥吗?
【我】 我已经是不在意自己的年龄了。
【青年】 那我就叫你哥哥吧!
【我】 这随便你。
【青年】 嗯,哥哥好!
【我】 ……
他在我一旁坐下,没有把刚才打招呼的热情继续下去,而是将视线转向远方,有些顾虑的样子。
【我】 来说你为什么要来到这里,是想寻找到怎样的记忆吧。
虽然他的举动让我感到有些奇怪,我还是试图以平静的语气挑起对话。
【青年】 不知道。
他把头转向我,带着年轻人的稚气。
【青年】其实我确实不知道,我没有想寻找的东西。
即使这样也不必要有多余的犹豫吧。
【我】 这里有记忆的宝藏哦,会有让人感到幸福的东西,不要为让自己可以努力的机会而犹豫。
【青年】可是那些记忆是我主动抛开的,它们让我感到难受,我却矛盾地又为一种缺失感来到这里。
【我】 好了,既然来到这里了,即使意外,也会找到些东西吧。
【我】 它们都曾今属于你,今后也依然是。想着抛弃,就可能会让自己失去更多的东西。
他看着我的眼睛,像在寻找一种希望。而航行也在这不知觉中驶近了目的地。
【我】 去吧,相信自己心中小小的愿望,在海浪之间寻找能够寻找到的东西。
【青年】 恩,谢谢哥哥。
他答应了,然后走向船的那头。站立在船的边沿上,在阴沉的海天之间,显得波澜壮阔。
而我一人在船的这头放松下身子,视线漫无目的地望向天空,意识到居然对着客人说出自己的想法。
我这是说给谁听的呢。一个陌生人?一个在这死后世界里再也不会相遇的人?还是说……我自己?
我在怀疑着自己举动的意义,是否就代表着自己已经做出或者将会做出一些多余的事情呢。
可是此刻我异样的感觉到,我的思绪与情感在被他联系着,从未有过的被一个人所联系着。
想着在此刻能为别人的困难添上自己的帮助,这同时也是在敞开心扉地能够去接受别人的帮助。
看到船头转过头来望着我,在脸上露出一个微笑,而我看着也在自己脸上回应着一个微笑。
两个人的微笑,未曾在这里奢望过的微笑,我意识到这是一个第一次在这死后世界里自己希望永远记住的东西。
忽然平静的一切被打破了,一个浪打在了船身上,他刚刚侧转的身子没有站稳,摇晃着向记忆之海里跌落下去。
在这一瞬间里,海风也变得巨大了,天空更加阴沉下去,而且似乎要发展往一个糟糕的方向。
此刻顾不上太多,我想我得救他。赶忙跑向他所在的船头,往波浪翻滚的海水里跳了下去。
我在下沉,身边被黑暗包围着。那是一片无尽的黑暗,我感到瞬间失去了重力,努力划着水却寻找不到方向。
周围也异样的安静,这是一个海面下无声的世界,时间的流淌似乎漫长,甚至凝结。
渐渐地我的心也在水中平静下来,我能感受到自己的呼吸,呼吸在这个海底并无异样。
恐惧之后这我才意识到,这是在一个死后的世界,一个已经不会再死去而离开的世界,
一个欣慰却又孤独的世界,一个没有失去也没有保留的世界,一个让记忆永恒封存的世界。
在这个黑暗无声的世界里,我顺着水流漂浮着,让意识随着身体漂浮着。
在记忆的海洋中以心灵去触摸着曾今,以及那些孕育着现在和未来的往昔时刻。
我能体会到在深海之下闪烁的幽幽光芒,像一个指引者在前方对我轻轻的召唤着。
我凭借着意志向光芒游去,让光点在眼前接近在身边扩散,觉得这样才能够拯救到什么。
这样才能够实现自己对他人的一个守候,一个哪怕在这死后世界已经显得多余的举动。
一种是哪怕只是出于内心原始的善良的淳朴的冲动,也要去带着落海的少年顺利的回来。
光芒弥散于海底,如同思绪飘荡在无尽的水中接受着温柔而永恒的抚慰,包裹在我的周围。
渐渐地,这些光点构成的屏障上清晰出一个影像,在向我讲述着什么。
在向我呈现着一个“生”所在的世界,一个在海底无尽的黑暗里心所体会的“光”所组成的世界。

【Chapter 4】
【青年】 真是一个空旷的操场啊。
耳边响起了青年的言语,轻轻地感叹着,然后声音又远去了。我想这是少年的记忆传递给我的信息吧。
在他的视角上,我以第一人称在看到了他的双手,和他眼前的整片操场。
接下来将在这个记忆构建的世界里,以一天天的经历,去体会着他的过去。
可是这只是视觉,他的意识以及他的存在感,却无法通过薄弱的纽带感觉到。
身体开始活动起来,由机械变得自然地走动着,向着教学楼那里去寻找人的踪影。
我的意识仅仅是随着身体漂浮着,在这个世界里不能左右什么,也不会有想去左右什么的愿望。
在阳光下曝露的身体,接触着这里的空气与水分,仿佛在由陌生不断变得亲近着。
与空旷的操场相比,教学楼因为人的踪迹,而显出格外不同的氛围。
身体继续往教室里走去,人的气息的存在让它移动着的脚步学会了自信。
同时身体也是在渴望着阳光之外的什么,让自己来到这人群中去寻找着。
身体在经历着成长,可是这样似乎仍缺少着什么,它所见到的人群都在它面前散开了。
当身体带着疑惑去走进下一个教室,带着或许会有改观的期望,依旧得到的是一个失望的结果。
【青年】 果然人们是讨厌我的,这个世界里已经不需要我的存在了。
此刻身体举动正是表达着他的意志,这让我清晰的感受到意识的存在,但是明白这种体验的阻隔。
之后,身体便失去了他本身的驱动,松软下来。重量压在了我的身上,而我只是依然麻木地站在教室中央。
刚才跑去教室外的人群开始渐渐返回,他们仿佛无视了我的存在,又回到了他们的日常。
我就这样站在教室里,近距离的带着身体逝去的愿望。去接触人群的气息,去沉浸于海洋的梦境之中。
可是,梦境最终将迎来黎明。
【同学】 刚才真是一个讨厌的人啊,就这么像无视我们的跑近教室来。”
是“讨厌”吗?一句声音之后,周围的声音也纷纷附和起来,成为这个狭小的教室里共通的话题。
我能感受到其中的恐惧,这正是我能从身体中所感受到的。
让我出于内心的害怕,从这个空间里,从这个人群中向外跑去,跑向一个与此隔绝的世界中。
我在遥远的地方大口的喘着气。驻足后望,那个教室,那片学校已经变成渺小的身影。
这已经足够远了,这已经是喊声不能再被传递到的距离了。我的心开始变得空荡荡的。
这是一种安心吗?我看着自己缓慢起落的脚步,在坚硬的水泥地面没有留下任何的痕迹。
我再去望那片远去的世界,一个希望着阻隔开的,一个永远只保留着不起眼的存在的世界。
我漂浮在这个梦境的世界,和一个已经属于我的身体,因为躲避着什么而肆意游荡着。
只到有一天,也是偶然而特别的一天,我似乎是感到孤单,感到厌倦了。
也似乎是出于好奇,决定再去一次那所学校,再次重拾起曾今想寻找的什么。
校门在眼前清晰了,操场在眼前清晰了,教学楼在眼前清晰了,曾今去过的那所教室在眼前清晰了。
自己觉得心中的愿望,去寻找一个未知到重要的东西的心愿,也这路途上变得清晰了。
这或许就是在凭借着海的梦境的经历,来记忆里所未填的空缺吧。
在那间教室的前,我敲了敲门走了进去,心里已经做好了接受各种意外的准备。
这不是躲闪着旁人视线,而是在寻找他人的帮助,寻找着一种重拾遗失的启示。
他站在教室门前,鼓起勇气地努力睁开眼,去接受着人群的目光。
身体在人群中肆意的走动着,人们并没有做出异样的反应。
身体想在人群中挑起话题,人们的意志中已经淡忘了他的痕迹了。
这我才发现,他所做出决定已经无法改变,关于那个让自己消失于此的决定。
他已经不再属于这个世界了,这一个他曾想抛开却一直未割舍的世界。
那我又将飘向何方呢,向着一个新的永恒的世界吗?
在意识在这个身体已经在远离我而去的时候,我又似乎不愿回到那片记忆之海上。
因为在我海的梦境中,在这死后的世界里,我是第一次意识到了我所希望寻找的东西。
我的意识回到的我的身体上,我闭着眼漂浮在黑暗的无尽的记忆之海的深处。
随着水流飘向我一个不知为何方的地方,一个永远将背负着记忆的地方。
在漫长的飘荡中,终于,我决定,睁开我的眼睛,去看看周围的世界。
去看看身边的世界,哪怕看到的依旧只是黑暗,去依旧能在世界里发现着什么。

【Chapter 5】
这是一片天空柔和的色彩,西斜的阳光透过玻璃照射在我面前的床铺上。
双手是棉被的触觉,鼻尖是医药水的味道,身边有机器在运转的声响。
这是一个空荡荡的病房,我孤单的一个人躺在中央。身体不能动,只能望着粉白的屋顶。
在这狭小的容器里,我感到内心的一阵寂寞与痛苦,因为孤单而在对外界恐惧而害怕着。
害怕着我的记忆里所寄托的想象,会成为自己的一种负担,会让自己面对着未知却只会守护着丝丝迷茫。
我闭上眼,想象自己正被水包围着,想象就在海底,在那片无尽的黑暗中。
希望再次抵达那个世界每天航行在记忆之海上,一直地去陪同着别人寻找着别人在寻找的东西。
于是在这样的孤单中,我的意识又变得模糊开始轻浮,再向一个遥远的地方飘去了。
直到忽然间响起清脆的敲门声,这让我回过神来,可是我仍害怕着不敢睁开我的眼睛。
我听出来是同班的同学来了,他们的话语声在翻滚着我的记忆,清晰着我的心跳。
这让我想到,我忽视了一些自己没有察觉的细节——
那片无尽的大海中也有一份属于自己的记忆吗,是否他们一直埋藏在那里而我却没有在意呢。
我是否也有过一丝小小的心愿要实现——在那个异世界中,也去寻找属于自己的那一份记忆呢。
那些记忆是一份仍然需要自己继续延续下去的记忆,它们还尚未到一个应该被海水所掩埋的时刻。
处在这个生者的世界里的我,是否只有那个异世界里才能找到唯一的归宿,而该早早的就去呢。
可是活着不该是一个被轻易淡忘着的概念,而让记忆独自在无尽深海之下,去忍受着永恒的没有终结的孤单。
是啊,刚才心头居然有抛弃“生”的世界的想法,自己觉得可笑。
去寻找活着的的自己的意义所在,才是对那个死后世界无尽时光最好的终结。
况且一直有人在为着我的“生”而努力呢,让我能够再次的活过来,而我竟想拒绝他人的心愿。
它们的努力让我能够有机会去真正地体会到自己内心的希望,这同时也是他人给予我的希望。
不论是给予的关心或者是给予的治疗,都是由这一切的努力才让我又回到了这个世界中。
【同学】 真希望你能够早点醒过来哦,和我们一起去看班级篮球赛。
熟悉的声音在面前在病房中弥散开来。
而我的内心出于一种固执,依然让自己的双眼紧紧闭着,用拒绝去回避心头残余的害怕。
【同学】 今天我们先走了,明天放学的时候还会来看你的。
然后是一个轻轻的关门声。
病房里恢复了先前的安静,又只剩下我孤单的一个人在这狭小的房间里。
此刻安静得如同深海的气息,我心头的另一种害怕又让自己猛地设法睁开自己的眼睛。
可是我发现自己的眼睛没能睁开,只是忽然有泪水的热度在脸颊上流淌开来。
我努力地张开口,努力地说着——
【我】 只是一个旅途,这几天里我去了一个遥远的地方,现在我已经回来了……
破碎的言语没有出声,只化作苦涩与甘甜,混杂着口角泪水的味道。
而所告别的旅途呢,
将依然安静地在一个遥远的地方延续下去,延续往彼世界中所不存在的尽头。

话说这种质量不高的应急式产物果然连自己都讨厌,当然GalGame是不应该用这样东西做剧本的。
还是去看 阿加莎 克里斯蒂 的小说去吧。。。
顺便觉得小野不由美的也不错。。。
我这里质量拙劣,仅此而已。。。

----
后来试了试html5+Biwascheme或Processing+Kawa或Processing+自己解析脚本,just work。

没有评论: