引擎,脚本语言和视觉小说-45小时内

视觉小说,引擎和脚本语言长达45小时


问候。 碰巧的是,连续三年,作为给某些人的新年礼物,我一直在做游戏。 在2018年,这是一个带有谜题元素的平台游戏,我在集线器上写道 在2019年-两名球员的网络即时战略(RTS),对此我什么都没写。 最后,在2020年-一个视觉上的短故事,将在非常有限的时间内创建,稍后将进行讨论。


在本文中:


  • 视觉短篇小说引擎的设计和实现,
  • 在8小时内有非线性情节的游戏,
  • 以自己的语言删除脚本中的游戏逻辑。

有意思吗 然后欢迎猫。


小心:有很多文字和〜3.5mb图片


内容:


0.开发引擎的理由。


  1. 平台的选择。
  2. 引擎架构及其实现:
    2.1。 问题陈述。
    2.2。 体系结构和实现。
  3. 脚本语言:
    3.1。 语言
    3.2。 口译员
  4. 游戏开发:
    4.1。 游戏逻辑的历史和发展。
    4.2。 图形
  5. 统计和结果。

注意:如果由于某种原因您对技术细节不感兴趣,则可以跳至步骤4“游戏开发”,但是您将跳过大部分内容


0.开发引擎的理由


当然,有很多现成的用于视觉短篇小说的引擎,毫无疑问,它们比下面描述的解决方案要好。 但是,无论我是哪种类型的程序员,如果我没有编写另一种。 因此,让我们假装其开发是合理的。


1.平台选择


实际上,选择是很小的:Java还是C ++。 无需三思而后行,我决定用Java实现我的计划,因为 为了快速开发,它提供了所有可能性(即:与C ++相比,自动内存管理和更高的简便性,因为它隐藏了许多低级细节,因此,可以减少对语言本身的强调,而仅考虑业务逻辑),并提供开箱即用的窗口,图形和音频支持。


选择Swing来实现图形界面,因为我使用了Java 13,在Java 13中JavaFX不再是库的一部分,并且由于它过于懒惰而添加了数十兆的OpenJFX。 也许这不是最好的解决方案,但是。


问题可能出现了:它是哪种游戏引擎,但没有硬件加速? 答案在于缺乏处理OpenGL的时间,以及它的绝对毫无意义:FPS对于视觉小说并不重要(无论如何,动画和图形的数量与这种情况一样)。


2.引擎的架构及其实现


2.1问题陈述


为了决定如何做某事,您需要决定为什么。 这是我关于问题的陈述,因为 该体系结构不是通用的,但根据定义,“特定于域的”引擎直接取决于预期的游戏。


通过通用引擎,我了解支持相对较低级别概念(例如“游戏对象”,“场景”,“组件”)的引擎。 决定使其不是通用引擎,因为这将大大减少开发时间。


按照计划,游戏应包括以下部分:


视觉小说游戏的结构


也就是说,每个场景都有一个背景,主要文本以及一个供用户输入的文本字段(视觉小说是在任意用户输入的情况下精确想到的,而不是像通常那样从建议的选项中进行选择。稍后我将告诉您为什么它很糟糕决定)。 该图还显示游戏中可能有多个场景,因此可以在它们之间进行转换。


注意:场景是游戏的逻辑部分。 在这一部分中,场景的标准可以是相同的背景。


对引擎的要求还包括播放音频和显示消息的能力(具有可选的用户输入功能)。


也许最重要的愿望不是写游戏逻辑,而是用某种简单的声明性语言来编写游戏逻辑。


还希望实现过程动画的可能性,即图像的基本运动,以便在Java级别上可以确定考虑当前运动速度的函数(例如,速度图是直接的,正弦的或其他形式的)。


按照计划,所有用户交互都将通过对话系统完成。 在这种情况下,对话被认为不一定与NPC或类似的对话,而是通常是对注册了相应处理程序的任何用户输入的反应。 不清楚 它将很快变得更加清晰。


2.2。 架构与实施


鉴于以上所有内容,您可以将引擎分为三个相对较大的部分,分别对应于相同的Java包:


  1. display -包含所有与向用户输出任何信息(图形,文本和声音)以及从用户接收输入有关的内容。 一种(视图),如果我们谈论MVC / MVP /等。
  2. initializer -包含在其中初始化和启动引擎的类。
  3. sl包含用于使用脚本语言的工具(以下称为SL)。

在这一段中,我将考虑前两个部分。 我将从第二个开始。


初始值设定项类有两个主要方法: initialize()run() 。 最初,控制权来自启动器类,在该类initialize()调用initialize() 。 调用之后,初始化程序将分析传递给程序的参数(包含任务的目录路径和要运行的任务的名称),加载所选任务的清单(稍后再进行介绍),初始化显示,检查数据是否支持该任务所需的语言版本(SL)解释器,最后为开发人员控制台启动一个单独的线程。


之后,如果一切顺利,启动器将调用run()方法,该方法将启动任务的实际加载。 首先,所有与下载的任务相关的脚本(关于下面的任务文件结构)都被馈送到分析器,分析器的结果被提供给解释器。 然后,开始所有场景的初始化,并且初始化程序完成其流的执行,最后将Enter键处理程序挂在显示器上。 因此,当用户按Enter键时,将加载第一个场景,但稍后会加载更多场景。


任务的文件结构如下:


任务的文件结构


有一个单独的任务文件夹,其根目录是清单,还有三个其他文件夹: audio -声音, graphics -视觉部分和scenes -描述场景的脚本。


我想简要介绍一下宣言。 它包含以下字段:


  • sl_version_req启动任务所需的SL版本,
  • init_scene任务开始的场景名称,
  • quest_name出现在窗口标题中的漂亮任务名称,
  • resolution -任务所针对的屏幕的分辨率(稍后对此进行说明),
  • font_size所有文本的字体大小,
  • font_name是所有文本的字体名称。

值得注意的是,在显示初始化期间,除其他事项外,还执行了渲染分辨率的计算:也就是说,所需的分辨率是从清单中获取的,并挤入了可用于窗口的空间中,以便:


  • 长宽比与清单中的分辨率相同,
  • 所有可用空间在宽度或高度上都被占用。
    因此,任务的开发者可以确定他的图像(例如16:9)将以该比例显示在任何屏幕上。

同样,在初始化显示时,光标会隐藏,因为它不参与游戏。


简而言之,关于开发者控制台。 开发它的原因如下:


  1. 用于调试。
  2. 如果游戏过程中出现问题,则可以通过开发者控制台进行修复。
    它仅实现了一些命令,即:输出特定类型及其状态的描述符,输出工作线程,重新启动显示以及最重要的命令exec ,它们允许在当前场景中执行任何SL代码。

这样就结束了初始化程序和相关内容的描述,我们可以继续进行显示的描述。


其最终结构如下:


显示结构


从问题的陈述中,我们可以得出结论,要做的就是绘制图像,绘制文本,播放音频。


通常如何在通用引擎及其他引擎中绘制文本/图像? 有一种类型为update()的方法,该方法在每个刻度/步/帧/渲染/帧/等中都被调用,并且其中有一个对drawText() / drawImage()类型的方法的调用-这可确保文本/图像在此帧中的外观。 但是,一旦停止调用此类方法,相应事物的呈现就会停止。


就我而言,决定做一些不同的事情。 由于对于视觉小说而言,文本和图像是相对永久的,并且几乎也是用户看到的所有内容(也就是说,它们足够重要),因此它们是作为游戏对象制成的-也就是说,您只需要生成就不会消失的东西。直到你问他们。 另外,该解决方案简化了实施。


描述文本/图像的对象(从OOP的角度来看)称为描述符。 也就是说,对于API引擎的用户来说,只有可以添加到显示状态并从显示状态中删除的描述符。 因此,在显示的最终版本中,存在以下描述符(它们对应于相同名称的类):


描述符名称:描述符描述:
ImageDescriptor图像描述符:包含图像( BufferedImage ),屏幕上的位置以及带有高度的宽度。 但是,在创建时,仅指定宽度-高度是与原始高度成比例地计算的(并且无法手动不成比例地拉伸/压缩图像,因此这是一个错误的决定)。
TextDescriptor文本描述符:包含文本,其位置和大小。 此外,文本不会在宽度和高度上按比例缩放,但是在超出边界时会被截断。 可以通过音节和空白字符来传输文本,还可以按行滚动文本。
AudioDescriptor可以播放音频(一次或循环播放),暂停并删除音频的音频描述符。
AnimationDescriptor可以像音频一样循环播放动画手柄。 包含一个实现动画的对象,以及一个为其播放动画的图像描述符。 主要方法是update(long) ,它花费了自上次update(long)调用以来经过的毫秒数。 它们的编号用于计算动画的当前状态。
InputDescriptor输入描述符:是一个文本字段,其中光标始终位于文本的末尾。 还应注意的是,通过隐式创建的文本描述符执行来自输入描述符的文本的存储和渲染,以免重复逻辑。 有趣的是,我考虑了按Backspace键的可能性,但没有考虑Delete键。 当在游戏过程中仍然按Delete键时,由于没有对Delete进行任何处理且角色试图显示为文本,因此in出现在字段中。
KeyAwaitDescriptor传递了必要键的句柄(来自KeyEvent ),以及带有某些逻辑的回调,当按下相应的键时将启动该逻辑。
PostWorkDescriptor接受回调的句柄,该回调将在处理每个刻度后被调用。

该显示还包含用于当前输入接收器(输入描述符)的字段,以及一个字段,该字段指示当前哪个文本描述符具有焦点,以及哪个文本描述符将在用户方面的相应动作下滚动。


游戏周期如下所示:


  1. 音频处理-在音频描述符上调用update()方法,该方法检查音频的当前状态,释放内存(如有必要),并进行其他技术工作。
  2. 处理击键-将输入的字符传输到描述符以接收输入,处理滚动键的击键(上下箭头)和Backspace。
  3. 动画处理。
  4. 清除渲染缓冲区中的背景(以BufferedImage作为缓冲区)。
  5. 绘图图像。
  6. 文字渲染。
  7. 输入的绘图字段。
  8. 缓冲区的输出到屏幕。
  9. 处理PostWorkDescriptor
  10. 关于替换显示状态的一些工作,我将在后面(在SL解释器的部分中)进行讨论。
  11. 停止流以动态计算时间,以使FPS等于指定值(默认为30)。

注意:也许会出现一个问题,“如果为输入字段创建了适当的文本描述符,为什么会更早地渲染它们,为什么还要渲染输入字段?” 实际上,第7段中的呈现不会发生-只有InputDescriptor的参数与InputDescriptor的参数同步-例如屏幕可见性,位置,大小等。 如上所述,这样做是因为用户不直接使用文本描述符控制相应的输入描述符,并且通常对此一无所知。


值得注意的是,屏幕上元素的大小和位置不是以像素为单位,而是以相对大小设置-从0到1的数字(下图)。 也就是说,渲染的整个宽度为1,整个高度为1(它们不相等,我忘记了几次,后来感到遗憾)。 也应该使(0,0)为中心,并且宽度/高度应等于2,但是出于某种原因,我忘记了/没考虑过。 但是,即使宽度/高度等于1的选项也简化了任务开发人员的生命。


相对坐标系


关于释放内存的系统的几句话。


每个描述符都有一个setDoFree(boolean)方法,如果用户要销毁给定的描述符,则必须调用该方法。 处理某种类型的所有描述符之后,立即进行了某种类型的描述符的垃圾收集。 另外,播放一次后播放的音频会在播放结束后自动删除。 与非循环动画完全相同。


因此,目前您可以绘制任何想要的东西,但这不是上面的图片,上面只有背景,主要文本和输入字段。 接下来是显示上方的包装器,它对应于类DefaultDisplayToolkit


初始化后,它仅将背景,文本等的描述符添加到显示器,还知道如何显示带有可选图标,输入字段和回调的消息。


然后出现了一个小错误,要对其进行全面纠正,需要重做一半的渲染系统:如果您查看游戏循环中的渲染顺序,则可以看到首先绘制图像,然后才绘制文本。 同时,当工具箱显示图像时,它会将其宽度和高度放置在屏幕中间。 并且,如果消息中包含很多文本,则它应该与场景的主要文本部分重叠。 但是,由于消息背景是图像(完全是黑色,但仍然是黑色),并且图像是在文本之前绘制的,因此一个文本会叠加在另一个文本上(下面的屏幕截图)。 通过不在屏幕上而是在正文上方的区域中垂直居中,部分解决了该问题。 完整的解决方案包括引入深度参数,并从单词“完全”重做渲染器。


叠加演示

文字重叠


最后,也许这与显示器有关。 您可以继续使用该语言, sl软件包中包含使用该语言的整个API。


3.脚本语言


注意:如果尊敬的USERNAME%在这里读过它,那他做得很好,我要他不要停止这样做:现在,它会比以前有趣得多。


3.1。 语言能力


最初,我想使用一种声明性语言,只需要指示场景所需的所有必要参数即可,仅此而已。 引擎将采取所有逻辑。 但是,最后,我使用了过程语言,即使使用OOP元素(几乎没有区别),这也是一个很好的解决方案,因为与声明性选项相比,它在游戏逻辑上具有更大的灵活性。


考虑到语言语法,以便在给定可用时间量的情况下,语法分析尽可能简单。


因此,代码存储在扩展名为SSF的文本文件中。 每个文件都包含一个或多个场景的描述; 每个场景包含零个或多个动作; 每个动作包含零个或多个运算符。


有关条款的一些解释。 动作只是一个过程,不可能传递参数(决不会阻止游戏的发展)。 运算符显然不是普通单词(+,-,/,*)中该词的含义,但形式是相同的:运算符是其名称及其所有参数的总和。


也许您渴望最终看到SL的源代码,这里是:


 scene dungeon { action init { load_image "background" "dungeon/background.png" load_image "key" "dungeon/key.png" load_audio "background" "dungeon/background.wav" load_audio "got_key" "dungeon/got_key.wav" } action first_come { play "background" loop set_background "background" set_text "some text" add_dialog "(||(|) (||-))" "dial_look_around" dial_look_around on } //some comment action dial_look_around { play "got_key" once show "some text 2" "key" none tag "key" switch_dialog "dial_look_around" off } } 

现在可以清楚地知道操作员是什么。 还可以看到每个动作都是一个语句块(一个语句可以是一个语句块),以及支持单行注释的事实(输入多行注释没有任何意义,此外,我没有使用单行注释)。


为了简化起见,该语言未引入“变量”这样的概念; 结果,代码中使用的所有值都是文字。 根据类型,区分以下文字:


文字名称:注意事项:
字符串文字包括转义引号和斜杠(\\)的功能,也可以在文本中插入连字符
整数文字支持负数
浮点文字支持负数
没有文字该代码表示​​为none
布尔文字在代码中-分别为true / false的on / off
一般文字如果文字不属于上述任何一种类型,并且由英文字母,数字和下划线组成,则为一般文字。

关于语言解析的几句话。 有多个级别的“加载”代码(如下图):


  1. 标记器是用于将源代码分解为标记(语言的最小语义单元)的模块化类。 每种令牌都与一个数字相关联-它的类型。 为什么模块化? 因为令牌生成器中检查源代码的任何部分是否为某种类型的令牌的那些部分与令牌生成器隔离,并从外部下载(从第二段下载)。
  2. tokenizer附加组件是一个类,用于定义SL中每种类型的令牌的外观; 在较低级别使用标记器。 这里也是对空间令牌的筛选和对单行注释的省略。 输出给出了干净的令牌流,用于...
  3. ...解析器(它也是模块化的),在输出时生成抽象语法树。 模块化-因为它本身只能解析场景和动作,但不知道如何分析操作员。 因此,将模块加载到其中(实际上,他本人将其加载到构造函数中,这不是很好),它可以解析其每个运算符。

脚本语言解析管道


现在,简要介绍一下运算符,以便出现一种语言功能的想法。 最初有11个运营商,然后在思考游戏的过程中,其中一些合并为一个,进行了一些更改,然后添加了9个。 这是汇总表:


操作员姓名:注意事项:
load_image将图像加载到内存中
load_audio将音频加载到内存中
set_text设置场景的主要文本。
set_background设置场景的背景。
play播放音频(可以播放一次或循环播放)
show显示带有回调(在SL中)和可选图像的消息(在用户关闭消息后调用)
tag设置标签(标签)。 可以将其视为具有指定名称的全局变量,该名称不存储任何值。 这在不同的情况下很有用:例如,您可以通过这种方式标记玩家是否找到了门的钥匙,他是否已经在此位置等。
if_tag / if_tag_n分支运算符,使您可以根据是否安装了相应的标记来执行某些操作。 支持else分支。 如果设置了标签,则执行if_tag ;如果是if_tag则执行if_tag_n反之亦然
add_dialog允许您向场景添加对话。 稍后再介绍他们
goto过渡到另一个场景
call自定义动作通话
call_extern从另一个场景调用动作。
stop_all停止播放所有声音/音乐
show_motion在指定的时间(持续时间)内显示图像并将其从一个点移动到另一点(在移动结束时调用可选回调)
animate对先前通过show_motion显示的图像进行show_motion 。 从选项中:您可以指定动画的类型v_motion / h_motion (按功能进行垂直/水平移动) 2t33t2),循环播放的可能性,动画播放的时间(持续时间)的指示。 可以将一个数值(一个,因为无法传递可变数量的参数,因此部分是拐杖)传递给动画(对于每个动画,它意味着不同的东西)和可选的回调(在播放动画时调用)。

用于计数器的运算符-特定于场景的整数变量。


操作员姓名:注意事项:
counter_set创建一个计数器并用一些值初始化它
counter_add向计数器添加值
if_counter <modifier>能够比较两个计数器/数字/一个计数器的值。 <modifier>是通用文字,其形式为eq/gr/ls[_n] ,其中eq相等, gr大于, ls小于, _ngr_n (例如, gr_n不大于)。 如您所见,一切都已尽可能简化。

还曾考虑过引入return (甚至在解释器的核心级别上添加了相应的功能),但我忘了,它没有用。


, , : show_motion (, , 0.01) duration .


, (lookup) ( ): ///, load_audio / load_image / counter_set / add_dialog . , , , , — . . , . , : " scene_coast.dialog_1 " — dialog_1 scene_coast .


SL-, . , , , — . : (-, ), , lookup ', , , . , goto lookup ', .


- — - , , n ( ) . , , n . , .


. :


 add_dialog "regexp" "dialog_name" callback on/off 

, . , : , , , ( ).


, , ( ) ( ) , ( ). : , , , "" "".


dialogs system work


, ( , )


, "":


 (||((|||) ( )?(||)?)|(||)( )?|  ) 

***


, : — , , — .


, :


3.2.


: , — "" ( ). .


SL , - . :


  1. init — , ( , , , ).
  2. first_come — , . , , .
  3. , : come — , ( ).

: init first_come — , .


. : , , init -. , ( ) .


, n , first_come - ( - - ). . , : , , first_come come , come ( ). : , , , .


(, "", " ", " " . .). , , - - . , ( ), .


(, , ). : ? , , . provideState , ; , .


, , , , ( , ), (, , , ).


4.


. 2019- 2018-, , , .


4.1.


, , , — . , . ( ), , - , 9 (), - ( , ( , , ) .


, : , , , . , , .


, 25% (5) , : , ; ( animate ), ( call_extern ).


, - ( ), (, , — , "You won").


demo visual novel


4.2.


, :


stubs for graphics


, , - - " ". :


  • (4x2.23''), .
  • : , , — .
  • ////etc.

usage of art brushes


  • , , .

a character from the visual novel


  • , . , , , . .

5.


( 11 ) 30 40 . 9 4 55 . ( ) 7 41 . — ~4-6 ( 45 ).


: "Darkyen's Time Tracker" JetBrains ( ).
: 2 , — . 45 8 .


: 4777, ( ) — 637.


: cloc .


30 . ( ) : — ~8 , — ~24 , ( ) — ~8 . .


— 232 ( - , WAV).


WAV?

javax.sound.sampled.AudioSystem , WAV AU , WAV.


28 ( 3 ). — 17 /.


- : , . , , " ", " ". (, ), ( ""/"" - ).


?

— , . : . . , , "" : NPC, , (, — ..).


, : , .


— . , : , , , . . , , , , , .


. ( ), :


  • . . .
  • . , .
  • , .

, , .


GitHub .


(assets) "Releases" "v1.0" .

Source: https://habr.com/ru/post/zh-CN483818/


All Articles