-
Notifications
You must be signed in to change notification settings - Fork 89
NovaScript
Nova的脚本格式称为NovaScript,主要由文本和Lua代码块组成。文件后缀名一般为.txt(需要被Unity识别为text asset),建议用支持Lua语法高亮的编辑器来编辑。
脚本文件一般放在Assets/Resources/Scenarios/文件夹下,文件位置在NovaController.prefab中由GameState.scriptPath设置。
一部作品的脚本由许多节点(node)组成。脚本可以被拆分为多个文件,一个文件中可以有多个节点。脚本解析的结果与读取文件的顺序无关。
每个节点的开头和结尾处各有一个提前代码块(eager execution block),语法为@<| ... |>。它用来定义节点的metadata,比如名称和下个节点,在游戏开始之前执行。
一个节点中可以有许多“条”对话(dialogue entry)。每条对话可以包括一个延迟代码块(lazy execution block),语法为<| ... |>,以及一些文本。延迟代码块用来实现演出效果,在游戏过程中执行。
文本中可以用::或::分隔角色名称和台词。(台词前后的引号“”不是NovaScript语法的一部分,游戏制作者可以选择其他排版习惯。)
角色名称中还可以用//分隔显示名称和内部名称,内部名称可以用于自动语音等功能。
文本中可以使用TextMeshPro提供的rich text XML标记。
两条对话之间由一个空行分隔。一条对话的文本可以有许多行。如果文本中需要空行,可以写一个空的XML标记<b></b>。
一条对话的延迟代码块和文本之间不应该有空行。(如果有空行,就会变成两条对话,点一下鼠标执行代码,再点一下鼠标播放文本。)
节点开头的提前代码块和第一条对话之间,以及最后一条对话与结尾的提前代码块之间,也不应该有空行。
建议在一个代码块中按以下顺序写代码:角色、背景、前景、时间轴、音乐、音效、对话框、语音、其他系统设置。
Tools/Scenarios/lint.py可以用于检查脚本的一些常见问题。
Nova提供的Lua文件放在Assets/Nova/Lua/文件夹下。
修改Lua文件之后,打包游戏之前,需要在Unity Editor的上面的菜单中运行Lua -> Copy Lua Files to Resources,详见游戏打包。Assets/Resources/Lua/文件夹下的*.lua.bytes文件是复制(或者编译)后的,不要手动修改。
我们目前使用的Lua运行库为ToLua#,它使用的Lua语言版本为LuaJIT,与Lua 5.1完全兼容,Lua 5.2的特性只有一部分兼容。
把C#接口绑定到Lua的方法详见Lua接口注册。
在Unity Editor中运行游戏时,修改Lua文件或脚本之后,按R可以重新加载脚本,而不用重新运行游戏。
但是由于一些缓存机制,重新加载的脚本可能会出现错误,这时需要在Unity Editor的上面的菜单中运行Nova -> Clear Save Data来清空存档。
如果一开始游戏,Unity Editor就闪退,也可以试试清空存档。
- 设置节点名称
label(name, display_name)-
name是一个字符串,表示程序内部使用的节点名称,设置跳转时用的是这个名称 - 如果
name以l_开头,这个节点名称就是定义在局部的,只能在同一个文件中使用,其他文件中可以有同名的节点 -
display_name是一个字符串,表示显示给玩家看的节点名称 -
display_name可省略,如果这个文件中还没有定义过display_name,则默认与name相同,否则默认为上一个display_name
-
- 设置跳转
jump_to(dest)-
dest是一个字符串,表示目标节点的名称
-
- 设置剧情分支
branch(branches)-
branches是一个array,每个元素是一个table,它的key有dest, text, mode, cond -
dest是一个字符串,表示目标节点的名称 -
text是一个字符串,表示显示在按钮上的文本 -
mode是一个字符串,可以为'normal'|'jump'|'show'|'enable'-
'normal'表示普通选项,此时cond必须省略 -
'jump'表示如果cond返回true,就会直接跳转到目标节点,否则不显示这个选项,此时text必须省略- 如果省略
cond,则一定会跳转 - 如果有多个选项是
'jump',则会按顺序测试每个cond,按照第一个返回true的来跳转
- 如果省略
-
'show'表示如果cond返回true,这个选项才会显示 -
'enable'表示如果cond返回true,这个选项才能点击
-
-
cond是一个函数,没有参数,返回一个boolean- 如果
cond是字符串,就会把它parse成表达式,然后转换成返回这个表达式的函数 -
cond必须返回boolean,我们不会进行类型转换,因为Lua会把任何数字和字符串转换为true
- 如果
- 具体的例子可以参考
Assets/Resources/Scenarios/test_branch.txt
-
- 设置章节
is_chapter()- 跳到上一个/下一个章节时会在这个节点停止,但是这个节点不一定显示在章节选择界面
- 设置开始节点
is_start()- 这个节点会显示在章节选择界面
- 设置一开始就解锁的开始节点
is_unlocked_start()- 同时会设置
is_chapter() - 在标题界面点击“开始游戏”时,如果只有一个开始节点已经解锁,就会跳过章节选择界面,直接进入游戏
- 同时会设置
- 设置调试节点
is_debug()- 调试节点正常情况下不会出现在章节选择界面中,当按Ctrl键解锁所有章节时才会出现
- 在模拟测试中也可以设置是否解锁调试节点
- 设置结局
is_end(name)-
name是一个字符串,表示程序内部使用的结局名称,目前没什么用
-
- 每个节点开头的提前代码块必须有
label,可以有is_start等;结尾的提前代码块必须有jump_to, branch, is_end之一 - 具体的例子可以参考
Assets/Resources/Scenarios/tut01.txt
- 显示图像
show(obj, image_name, coord, color, fade)-
obj可以为GameCharacterController(如ergong)或SpriteController(如bg)- Controller绑定到Lua的名称在Unity Editor中由
*Controller.luaGlobalName设置
- Controller绑定到Lua的名称在Unity Editor中由
-
image_name是一个字符串- 如果
obj是GameCharacterController,image_name就是所显示的pose的名称- 一个pose包括多个立绘部件,在
pose.lua中设置
- 一个pose包括多个立绘部件,在
- 如果
obj是SpriteController,image_name就是所显示的图片的名称,如'room'- 图片名称不需要后缀名,应避免名称相同而后缀名不同的素材
- 图片文件位置由
SpriteController.imageFolder设置
- 如果
-
coord是图像的坐标,格式为{x, y, scale, z, angle},元素均为数值- 坐标系:画面中心的
x, y, z为0,向右x增大,向上y增大,向里z增大 -
angle的正值为逆时针,范围为-180..180 -
scale, z, angle可省略,默认为这个controller之前的值 -
scale和angle可以改为三个数值的table,表示x, y, z分量 - 为了方便只有2D的演出,我们没有把
z和x, y放在一起
- 坐标系:画面中心的
-
color是与图像相乘的颜色,是一个table,包括1..4个数值,范围为0..1- 1个数值:
r, g, b均为该值,a为1 - 2个数值:
r, g, b均为第一个值,a为第二个值 - 3个数值:
r, g, b为这三个数值,a为1 - 4个数值:
r, g, b, a为这四个数值
- 1个数值:
-
fade是一个boolean,fade为true时,如果这个controller上有自动淡入淡出的脚本,就会进行淡入淡出 -
coord, color, fade可省略,默认coord, color为这个controller之前的值,fade为true - 可以在
animation_presets.lua中定义一些常用的coord和color - 可以先在Unity Editor中移动各种东西来尝试构图,再把数值写到脚本里
-
- 隐藏图像
hide(obj, fade) - 具体的例子可以参考
Assets/Resources/Scenarios/tut02.txt
- 播放音效
sound(audio_name, volume, pos, use_3d)-
audio_name是音效的名称,文件位置由SoundController.audioFolder设置 -
volume范围为0..1- 实际的音量为脚本中的音量与设置中的音量的乘积
-
pos是音效的位置,格式为{x, y, z}- 音效素材一般不需要做出“在左边还是在右边”的立体声效果,而是由Unity实现
- 但是音效一边播放一遍移动的效果可以做到素材里,比如一辆车从左开到右
-
use_3d是一个boolean,表示是否启用音源的3D效果,如多普勒效应和双耳相位差- 即使不启用3D效果,声音仍然会越近越响
-
volume, pos, use_3d可省略,默认volume为1,position为摄像机的位置,use_3d为false
-
- 播放语音
say(obj, audio_name, delay, override_auto_voice)-
obj为GameCharacterController -
audio_name是语音的名称,文件位置由GameCharacterController.voiceFolder设置 -
delay表示进入这条对话后延迟多少时间播放语音,以秒为单位 -
override_auto_voice是一个boolean,表示是否跳过这条对话的自动语音 -
delay, override_auto_voice可省略,默认delay为0,override_auto_voice为true
-
- 播放音乐
play(obj, audio_name, volume)-
obj为AudioController -
audio_name是音乐的名称,文件位置由AudioController.audioFolder设置 -
volume可省略,默认为0.5(与sound的默认volume不同) -
AudioController上面的AudioSource可以设为不循环播放 - 可以在
ViewManager.audiosToPause中设置切换界面时暂停某些AudioController,以保证声音和演出动画的同步
-
- 停止音乐
stop(obj) - 改变音量
volume(obj, value) - 动画改变音量
anim:volume(obj, value, duration)-
duration可省略,默认为1
-
- 动画淡入音乐
anim:fade_in(obj, audio_name, volume, duration)-
volume, duration可省略,默认volume为0.5,duration为1
-
- 动画淡出音乐
anim:fade_out(obj, duration)-
duration可省略,默认为1
-
- 具体的例子可以参考
Assets/Resources/Scenarios/tut03.txt
- 停止自动和快进模式
stop_auto_ff() - 停止快进模式
stop_ff() - 跳到下一条对话
force_step() - 设置对话框
set_box(pos_name, style_name, auto_new_page)-
pos_name是对话框位置预设的名称,style_name是对话框风格预设的名称,在dialogue_box.lua中设置 -
auto_new_page是一个boolean,auto_new_page为true时,如果对话框的mode是append,就会执行new_page() -
pos_name, style_name, auto_new_page可省略,pos_name, style_name的默认值在dialogue_box.lua中设置,auto_new_page默认为true - 要隐藏对话框,可以把对话框的位置移到画面外
- 预设的
pos_name中,'full'是全屏对话框,'hide'是隐藏对话框
-
- 清空对话框
new_page() - 延迟文本出现
text_delay(time) - 设置文本动画时长
text_duration(time) - 隐藏对话框一段时间后再出现
box_hide_show(duration, pos_name, style_name)-
duration, pos_name, style_name可省略,默认duration为1 - 如果使用自动语音,建议对语音设置相应的延时
-
- 具体的例子可以参考
Assets/Resources/Scenarios/tut04.txt
- 一“组”动画由许多“段”动画(animation entry)组成
- 每段对话可以接在动画的根或上一段动画后面,形成树状结构,用来定义动画的串行和并行播放
- 演出用到的动画分为对话内动画(per dialogue animation)和持续动画(holding animation)
- 对话内动画只能在一条对话之内播放,点击鼠标时动画会停止到最终状态
- 持续动画播放的过程可以跨越多条对话,点击鼠标时动画不会停止,必须用脚本停止
- 两种动画各有一个
NovaAnimation作为“根”,名称分别为anim和anim_hold
-
anim:_and()可以让下一段动画与上一段动画同时开始 -
anim:stop()可以停止一组动画 - 持续动画开始时要调用
anim_hold_begin(),结束时要调用anim_hold_end(),以处理动画系统和存档系统的一些状态- TODO:等我们的preload系统能更好地parse Lua的时候,就不用手动写
anim_hold_begin和anim_hold_end了
- TODO:等我们的preload系统能更好地parse Lua的时候,就不用手动写
- 可以把一些常用的动画写成函数放在
animation_presets.lua里 - 详见演出系统
- 动画等待
anim:wait(duration)-
duration以秒为单位
-
- 动画等待另一组动画播放结束
anim:wait_all(wrap_anim) - 动画执行代码
anim:action(func, ...)- 如果代码只有一个函数,就把函数名和参数当作
action的参数,比如anim:action(show, bg, 'room') - 如果代码有很多行,就把代码放在一个函数里,比如:
anim:action(function() show(bg, 'room') play(bgm, 'room') end)
- 如果代码只有一个函数,就把函数名和参数当作
- 动画无限循环
anim:loop(func)- func的输入为一段动画,输出为接在这段动画后面的一段动画,比如:
anim:loop(function(entry) return entry:wait(1 ):action(show, bg, 'room' ):wait(1 ):action(show, bg, 'corridor') end)
- func的输入为一段动画,输出为接在这段动画后面的一段动画,比如:
- 移动
move(obj, coord) - 动画移动
anim:move(obj, coord, duration, easing)-
move等函数如果不接在:后面,就不是动画,没有duration, easing参数 -
easing为{start_slope, target_slope},表示开始和结束时的速度- 0表示开始时从静止加速/结束时减速到静止,1表示匀速运动,可以小于0或大于1
- 如果两者相同,可以只写一个数
- 如果需要更复杂的动画曲线,可以写
{func_name, args...},并在animation_high_level.lua中的easing_func_name_map里绑定func_name对应的EasingFunction,EasingFunction在AnimationEntry.cs中定义
-
duration, easing可省略,默认duration为1,easing为{0, 0}
-
- 改变颜色
tint(obj, color) - 动画改变颜色
anim:tint(obj, color, duration, easing) - 改变环境颜色
env_tint(obj, color)-
tint一般用于短期改变颜色,env_tint一般用于黄昏、夜晚等效果,实际的颜色为tint与env_tint的乘积
-
- 动画改变环境颜色
anim:env_tint(obj, color, duration, easing) - 具体的例子可以参考
Assets/Resources/Scenarios/tut05.txt - 更低层的接口详见
animation.lua
- 第一类转场
anim:trans(obj, image_name, shader_layer, times, properties, color2)-
obj可以为SpriteController或CameraController -
image_name- 如果
obj是SpriteController,image_name就是转场后图片的名称 - 如果
obj是CameraController,则进行全局转场(把角色和背景等一起转场),image_name是一个函数,定义转场后的内容
- 如果
-
shader_layer- 如果
obj是SpriteController,则shader_layer就是shader_name,是一个字符串,表示shader绑定到Lua的名称- 如果shader在Unity中的名称是
Foo Bar,绑定到Lua之后会自动转换成foo_bar -
shader_alias_map可以自定义绑定的名称
- 如果shader在Unity中的名称是
- 如果
obj是CameraController,则shader_layer可以是shader_name或{shader_name, layer_id}- 摄像机上可以叠加多层shader,按
layer_id从小到大依次渲染 - 转场默认的
layer_id为1,特效默认的layer_id为0 - 目前不支持同一种特效叠加多次
- 摄像机上可以叠加多层shader,按
- 如果
-
times可以是duration或{duration, easing} -
properties为shader的属性- TODO:由于一些历史遗留问题,shader的名称会从Unity风格转换为Lua风格,但是property的名称不会,这里以后要统一
-
color2为转场后图片的颜色- 如果
obj是CameraController,则color2无效
- 如果
-
times, properties, color2可省略,默认times为{1, {1, 1}},properties为default_shader_properties_map提供的默认值,color2为之前的颜色 - 改变背景一般不会直接用
show,总是用转场;但是改变立绘时默认有一个渐变,一般不需要转场
-
- 第二类转场
anim:trans2(obj, image_name, shader_layer, times, properties, times2, properties2, color2)- 两类转场的区别:
- 第一类转场同时显示转场前后的两张图片;第二类转场一次只显示一张图片,先让前一张图片消失,再让后一张图片出现
- 第二类转场前半段和后半段的
times和properties可以分别设置
- 两类转场的区别:
- 特效
vfx(obj, shader_layer, t, properties)-
shader_name为nil或空字符串表示取消特效 -
t对应shader的属性_T,范围为0..1,0表示特效不出现,1表示特效完全出现 -
t, properties可省略,默认t为1
-
- 特效动画
anim:vfx(obj, shader_layer, start_target_t, times, properties)-
start_target_t为{start_t, target_t}- 如果
target_t为0,动画结束后会自动取消特效
- 如果
-
times, properties可省略,默认times为{1, {1, 1}}
-
- 具体的例子可以参考
Assets/Resources/Scenarios/tut06.txt
- 显示时间轴prefab
timeline(timelime_name, time)-
time为时间轴所处的时刻,以秒为单位 -
time可省略,默认为0 - 如果prefab中没有
playableDirector,则time必须省略 - 如果prefab中有
CameraController,main camera会切换到那个摄像机- 在时间轴没有移动那个摄像机时,仍然可以用脚本移动那个摄像机
-
- 隐藏时间轴prefab
timeline_hide() - 跳转时间轴
timeline_seek(time) - 动画播放时间轴
anim:timeline_play(to, duration, slope)
- 显示视频
video(video_name) - 隐藏视频
video_hide() - 开始播放视频
video_play() - 获取视频长度
video_duration() - 具体的例子可以参考
Assets/Resources/Scenarios/test_video.txt
- 在Lua脚本中,
v_开头的全局变量为Nova的变量,会保存在存档里 -
gv_开头的全局变量为Nova的全局变量,会保存在全局存档里,不会在读档或回跳时重置 - 变量的类型可以是boolean、number或string
- 把变量设为
nil就是删除这个变量 - 在文本中可以用
{{变量名}}显示number或string类型的变量 - 具体的例子可以参考
Assets/Resources/Scenarios/test_variables.txt和test_global_variable.txt
- 显示警告
alert(text) - 显示通知
notify(text) - 具体的例子可以参考
Assets/Resources/Scenarios/tut04.txt
- 开启自动语音
auto_voice_on(name, index)-
name是文本中的角色名称(如'王二宫'),而不是Lua变量名(如ergong) - 如果文本中设置了角色的显示名称和内部名称(如
ch2.txt中的???//张浅野),而name设为内部名称('张浅野'),这条对话也会播放自动语音 -
index是语音文件名中的编号
-
- 关闭自动语音
auto_voice_off(name) - 关闭所有角色的自动语音
auto_voice_off_all() - 给这条对话的自动语音设置延时
set_auto_voice_delay(value)-
value以秒为单位
-
- 跳过这条对话的自动语音
auto_voice_skip()-
say默认会跳过这条对话的自动语音
-
- 具体的例子可以参考
Assets/Resources/Scenarios/ch1.txt .. ch4.txt
TODO
- 具体的例子可以参考
Assets/Resources/Scenarios/test_avatar.txt