Project1
标题: 剧本系统-剧情内容写在游戏外 [打印本页]
作者: guoxiaomi 时间: 2018-7-25 20:19
标题: 剧本系统-剧情内容写在游戏外 本帖最后由 guoxiaomi 于 2018-7-26 01:29 编辑
摘要
RM系列 制作的游戏,其数据是以ruby marshal的格式保存。此类数据不能直观的阅读。本文提供了一个“剧情系统”的初步实现:即将剧情文件放在游戏工程之外,在游戏运行的时候读取剧情文件生成对应的事件页并执行。此系统分离开游戏的核心和剧情,在游戏的制作、调试、校对和翻译上会带来优势。最后,考虑到标记语言markdown的流行,在这个实现里,剧情文件使用类似markdown的语法。
引言
传统rpg对数值总是有所苛求,这使得作者不愿意把游戏的核心数据公开,并认为玩家查看数据文件=作弊=毁掉游戏体验。但是有些游戏会把一些数值设定暴露给玩家,这会带来以下优势:
开发方面并行合作进行开发、调试,剧情文本文件和游戏系统的改动互不影响 所有剧情文本文件统一管理,剧情流程直观,可以高效的比对、修改 游戏的多语言版本只需要将剧情文本文件翻译校对,无需提取剧情翻译后再复制回去 用git做版本管理时,可以直接追踪剧情文本文件的改动 剧情文本文件的格式不依赖于制作工具,方便迁移到其他平台 游戏性方面 下面是游戏Geneforge 5 中的一段剧情文本:
// dlg.txt
begintalkscript;
variables;
begintalknode 1 ;
state = -1 ;
nextstate = -1 ;
condition = get_sdf( 1 ,1 ) == 0 ;
question = "special" ;
text1 = "You enter the settlement of Minallah, the only town in these frigid mountains. It is also the gateway to the Foundry, where creations are checked in and taken out." ;
text2 = "Like most cities in western Terrestia, it is centuries old. Unlike most of them, it isn't very impressive. While the Shapers tend to build massive and intimidating settlements, Minallah is mainly a collection of burrows in the rock." ;
text3 = "It is as if the cold and wind wore away all determination to bring any of the standard Shaper glory to Minallah. The only impressive structure is the spire to the west." ;
text4 = "You manage to recall that it is Isenwood's Spire. A huge, natural pinnacle of stone, honeycombed with tunnels. Dozens of narrow windows dot its sides, little pinpricks of light in the gray and the snow." ;
text5 = "Shaper Rawal commanded that you go see him. That is probably where he is." ;
action = SET_SDF 1 1 1 ;
code =
toggle_quest( 3 ,1 ) ;
break ;
// dlg.txt
begintalkscript;
variables;
begintalknode 1 ;
state = -1 ;
nextstate = -1 ;
condition = get_sdf( 1 ,1 ) == 0 ;
question = "special" ;
text1 = "You enter the settlement of Minallah, the only town in these frigid mountains. It is also the gateway to the Foundry, where creations are checked in and taken out." ;
text2 = "Like most cities in western Terrestia, it is centuries old. Unlike most of them, it isn't very impressive. While the Shapers tend to build massive and intimidating settlements, Minallah is mainly a collection of burrows in the rock." ;
text3 = "It is as if the cold and wind wore away all determination to bring any of the standard Shaper glory to Minallah. The only impressive structure is the spire to the west." ;
text4 = "You manage to recall that it is Isenwood's Spire. A huge, natural pinnacle of stone, honeycombed with tunnels. Dozens of narrow windows dot its sides, little pinpricks of light in the gray and the snow." ;
text5 = "Shaper Rawal commanded that you go see him. That is probably where he is." ;
action = SET_SDF 1 1 1 ;
code =
toggle_quest( 3 ,1 ) ;
break ;
从上面的例子中可以看出,剧情文本文件里面不仅有对话的内容,还有其他的一些标记符号。而游戏系统所需要做的,就是解析这个文档并生成对应的事件触发。
方法
首先,需要在RGSS3中找到生成事件页的位置,然后需要写一个解释器来解析markdown格式的剧情文本文件。
替换事件页
同样,从RMVA的RGSS3源码中可以看到,RMVA的事件解释器(Game_Interpreter)所做的,正是解析地图文件(比如Map001.rvdata2)中保存的事件指令(RPG::EventCommand)数组,并从开头按照流程执行到数组末尾。解析的代码在Game_Event中(部分内容省略):
class Game_Event < Game_Character
#--------------------------------------------------------------------------
# * Set Up Event Page Settings
#--------------------------------------------------------------------------
def setup_page_settings
...
@list = @page .list
@interpreter = @trigger == 4 ? Game_Interpreter.new : nil
end
end
class Game_Event < Game_Character
#--------------------------------------------------------------------------
# * Set Up Event Page Settings
#--------------------------------------------------------------------------
def setup_page_settings
...
@list = @page .list
@interpreter = @trigger == 4 ? Game_Interpreter.new : nil
end
end
只需要用钩子方法,在此方法之后替换@list即可:
class Game_Event < Game_Character
alias _new_list_setup_page_settings setup_page_settings
def setup_page_settings
_new_list_setup_page_settings
@list .replace new_list
end
def new_list
...
end
end
class Game_Event < Game_Character
alias _new_list_setup_page_settings setup_page_settings
def setup_page_settings
_new_list_setup_page_settings
@list .replace new_list
end
def new_list
...
end
end
new_list即生成新的@list的方法,里面包含了解析markdown文件的内容。
markdown主要格式
markdown是一种纯文本格式的标记语言。通过简单的标记语法,它可以使普通文本内容具有一定的格式。这里只简单的介绍本文用到的语法:
多级标题是以多个'#'号开头的行:
有序列表是以数字和小数点开头的行
链接和图片是用连续的中括号和小括号括起的内容:
[ 显示的文本] ( 链接地址 "alt text" )
![ 显示的文本] ( 图片地址 "alt text" )
[ 显示的文本] ( 链接地址 "alt text" )
![ 显示的文本] ( 图片地址 "alt text" )
代码是以'`'括起的内容,并且支持多行代码:
这是`内嵌表达式`。
```
这是多行代码
```
剧情文本
利用好这些格式,就可以很自然的写一段初始的剧情。下面的剧情中首先设置了开关和变量,然后增加了金钱和部分道具。为了对比,后面给出这个剧情生成的事件页图片以供参考。
## 初始剧情
[ Switches] ( 1 "ON" )
[ Variables] ( 1 "100" )
[ Party.Gold ] ( 200 )
[ Party.Item ] ( 3 "恢复剂" )
[ Party.Weapon ] ( 1 "手斧" )
[ Party.Armor ] ( 1 "布衣" )
神秘的声音:
醒来吧,勇者!
![ 主角] ( Actor1.png "1" )
我在哪里?
神秘的声音:
你已经是第 `$data_retrytimes` 次尝试这个迷宫了,还需要继续吗?
![ 主角] ( Actor1.png "1" )
我以前尝试过吗?那这次?
1 . 继续尝试
2 . 放弃治疗
### 继续尝试
![ 主角] ( Actor1.png "1" )
虽然不知道为什么,但是选这个就没错了。
### 放弃治疗
```
SceneManager.goto ( Scene_Title)
```
## 初始剧情
[ Switches] ( 1 "ON" )
[ Variables] ( 1 "100" )
[ Party.Gold ] ( 200 )
[ Party.Item ] ( 3 "恢复剂" )
[ Party.Weapon ] ( 1 "手斧" )
[ Party.Armor ] ( 1 "布衣" )
神秘的声音:
醒来吧,勇者!
![ 主角] ( Actor1.png "1" )
我在哪里?
神秘的声音:
你已经是第 `$data_retrytimes` 次尝试这个迷宫了,还需要继续吗?
![ 主角] ( Actor1.png "1" )
我以前尝试过吗?那这次?
1 . 继续尝试
2 . 放弃治疗
### 继续尝试
![ 主角] ( Actor1.png "1" )
虽然不知道为什么,但是选这个就没错了。
### 放弃治疗
```
SceneManager.goto ( Scene_Title)
```
读取此文件后生成的事件页如下图所示,需要注意的是`$data_retrytimes` 内嵌表达式已经求值完毕。当然书写的剧情文本格式并不强求。由于这个解释器和markdown的格式是自定义的,只是借用了markdown的格式,其实现非常简单,可扩展性强。
更多功能
由于事件页是在游戏内解析生成的,合适的解析方法可以根据场合生成不同的事件页,对原来系统的进行扩展。
第一个例子就是上面提到的的内嵌表达式。此功能是文本里插入指示符\v[n] 的增强。第二个例子是控制选择项出现的条件。由于选择项是解析生成的,可以在生成的时候控制选择项是否出现。比如上一节提到的例子中,要求尝试次数超过3次才能选择放弃,否则“放弃治疗”的选项不出现。可以参考下面的语法格式:
![ 主角] ( Actor1.png "1" )
我以前尝试过吗?那这次?
1 . 继续尝试
2 . 放弃治疗
- `$data_retrytimes >= 3 `
![ 主角] ( Actor1.png "1" )
我以前尝试过吗?那这次?
1 . 继续尝试
2 . 放弃治疗
- `$data_retrytimes >= 3 `
第三个例子是设置移动路线,在剧情复杂的地方,移动路线的设置往往非常冗长而且麻烦,需要在新打开的窗口里用鼠标点击。这里也给一个语法参考,以简化对移动路线的设置,这里用wasd表示四方向的移动,WASD表示四方向的转身:
从markdown格式的剧本文本文件解析到事件页的方案能够自定义,给了开发者极大的自由去书写剧本。而不同的使用者可能钟爱于不同的标记文本的书写格式,这里暂无一个统一的标准给出。
总结
本文提供了一个简单的方案将剧情文本写在游戏数据库之外,并且在游戏内解析成事件指令。由于只是解释成事件指令,在与原来的系统可以做到完全兼容的同时,还带来了开发上的诸多便利。此外,利用自定义的解析方式,甚至可以超出原来的事件页的功能。
附录
附录是当前所完成的剧本系统的语法格式和对应的代码,仅供参考。
作者: guoxiaomi 时间: 2018-7-25 20:19
本帖最后由 guoxiaomi 于 2018-7-26 01:40 编辑
剧本系统
把游戏的剧情文本写在游戏外部,方便后续的更改。可能增加一些新功能,比如,特定剧情的出现条件等。
剧本系统尽量使用markdown的语法,会读取#开头的段落并将后续的文本导入到一个hash里缓存。目前有以下功能:
使用>表示注释,引用的内容会被忽略 使用```括起的内容,会转换成事件脚本 使用![name](pic_name "1") 表示“显示文章”的头像
普通的文本会转化为“显示文章”的内容,
如果普通文本之后出现了空行,会转化为下一个显示文章 普通文本末尾的<br> 会自动忽略
使用`作为内嵌表达式,会在生成事件 的时候求值
即载入地图前 仅对显示文章有效 调用事件的 auto_reload 方法会重新求值
使用有序列表(比如1. xxx )表示选择项,选择项只有跳转到对应标签的功能,取消的话会继续执行
选择项之间不要空开,选择项和后面的文本要空开 在列表项后面插入深一层的无序列表,以控制选项出现的条件(见后面) 调用事件的 auto_reload 方法会重新计算条件
使用<email@com> 的格式表示一些特殊的“可见”指令
<CommonEvent@16> 调用16号公共事件,注意调用公共事件后会返回<GetItem@16> 选择物品,ID保存在16号变量里<JumpTo@chapter> 标签跳转<AutoEvent@reload> 重新载入本事件
使用 [sth]: para1 "para2" 表示一些特殊的“不可见”指令
这个格式的引用在markdown中不显示 [Balloon]: event "surprise" 显示气泡[SelfSwitch]: A "ON" 打开独立开关[Dialog]: setting "ON" 对话加强脚本设置[Move]: -1 "w3 S" 设置移动路线[PlaySE]: file "80" 播放SE
使用表格表示商店
商店已经完成,后续其他的想法再说 表格前后都要有空行
使用[chapter](condition) 表示有条件的标签跳转,支持短跳
转换成条件分歧-脚本 默认会在条件分歧后创建标签:#chapter,从而允许执行跳转后再回来
(高级) 使用[cmd] [paras] 的格式输入任意事件指令
中间必须有一个空格 后面的[paras] 是一个数组,eval求值后作为参数 indent = 0
选项出现的条件
如下所示,多个条件的话必须全部满足。
我是文本:
1. 选项1
2. 金币大于 100 才会出现的选项
- `$game_party.gold > 100` 复制代码
商店格式
商店格式,可以支持多个物品。
第1列是道具种类,第2列是id,最后一列是价格,如果价格为'-',则用默认的价格 第二个参数 P = Purchase Only 表示不可卖出。只识别第一个字母
其他指令说明
# 这个脚本会重新载入事件页,从而对内嵌表达式和选项出现的条件重新求值
$game_map.events[@event_id].auto_reload
# 可以使用 <AutoEvent@reload> 替代 复制代码 设置移动路线
设置移动路线的格式如下:[Move]: -1 "w2 X1" :
第1项是角色的id:-1表示Player,0表示本事件,1以上是事件的id。 如果第一个参数是字符串,会搜索同名的事件原理是在“设置移动路线”的指令前面加了一段脚本修改了后续事件的内容 @list[@index + 1].parameters[1] = ... 使用 [Moves] 使得事件设置为:不等待移动结束 第2项的参数用空格隔开,每一项对应成移动指令,具体规则见下表。区分大小写 。 代码 说明 移动 - w1 向上移动 1 步 a2 向左移动 2 步 s3 向下移动 3 步 d4 向右移动 4 步 f5 向前 5 步 b6 向后 6 步 j7.-8 跳跃 7, -8 J9.10 传送到 9, 10 转向 - W 脸朝上 A 脸朝左 S 脸朝下 D 脸朝右 L 左转90度 R 右转90度 B 转180度 FIX 面向固定 ON fix 面向固定 OFF 设置 - PASS 穿透 ON pass 穿透 OFF v1 设置移动速度 1 V2 设置移动频率 2 STEP 踏步动画 ON step 踏步动画 OFF 透明 - T 透明 ON t 透明 OFF o128 调整透明度为 128 控制 - W60 等待 60 帧 X1 打开 1 号开关 x2 关闭 2 号开关
解析用代码:
# functions:
# auto load event list from the txt file with same name.
class Game_Event < Game_Character
alias _auto_load_setup_page_settings setup_page_settings
def setup_page_settings
_auto_load_setup_page_settings
if @list [ 0 ] .code == 355 && @list [ 0 ] .parameters [ 0 ] =~ /AUTO:( .+) /
auto_load( @event.name , $1 .strip )
elsif @auto_load_keys
auto_load *@auto_load_keys
end
end
def auto_load( name, key)
@auto_load_keys = [ name, key]
@list .replace Auto_Event.load ( name) .list ( key)
end
def name
@event .name
end
def auto_reload
@page = nil
refresh
end
end
module Auto_Event
module_function
Event_List = { }
def reload # only after .md file changes
Event_List.keys .each do |name|
Event_List[ name] = load_list_txt( name)
end
end
def load ( name)
Event_List[ name] ||= load_list_txt( name)
@data = Event_List[ name]
@list = [ ]
@keys = [ ]
self
end
def load_list_txt( name)
data, key, value = { } , '' , [ ]
File .open ( 'Events/' + name + '.md' , 'r' ) do |f|
f.readlines .each do |line|
# start with # key
if line =~ /^#+\s*(.+)/
data[ key] = value.join ( "\n " ) if key != ''
key, value = $1 , [ ]
next
else
value << line.strip
next
end
end
# last key
data[ key] = value.join ( "\n " ) if key != ''
end
data
end
def list( key)
@keys << key
event_keys = [ ]
while @keys .size != event_keys.size
@keys .uniq !
@keys .each do |key|
next if event_keys.include ?( key)
event_keys << key
next if !@data[ key]
add( 118 , 0 , [ key] )
set_text( @data[ key] )
add( 115 , 0 , [ ] )
end
end
add( 0 , 0 , [ ] )
end
def set_text( text)
msg_tag, script_tag, choice_tag, table_tag = *[ false ] * 4
choices = [ ]
choice_ignore = [ ]
table_rows = [ ]
text.split ( "\n " ) .each do |line|
# ------------------------------------------------------------
# 优先判定是否为脚本
# ------------------------------------------------------------
if script_tag
if line =~ /^`` `/
script_tag = false
else
add(655, 0, [line])
msg_tag = false
end
next
end
# ------------------------------------------------------------
# 判定注释
# ------------------------------------------------------------
if line =~ /^>/
next
end
# ------------------------------------------------------------
# 继续剩下的判定
# ------------------------------------------------------------
case line
# ------------------------------------------------------------
# ` `` 识别为 脚本
# ------------------------------------------------------------
when /^`` `/
add(355, 0, '#~ event scripts')
script_tag = true
# ------------------------------------------------------------
# 空行
# ------------------------------------------------------------
when /^\s *$/
if msg_tag
msg_tag = false
end
if choice_tag
choice_tag = false
add_choices(choices, choice_ignore)
end
if table_tag
table_tag = false
add_table(table_rows)
table_rows.clear
end
# ------------------------------------------------------------
# 数字列表:选择项
# ------------------------------------------------------------
when /^\s *\d +\. \s *(.+)/
text = $1
if choice_tag
choices << text
else
choice_tag = true
choice_ignore = []
choices = [text]
end
when /^-\s +` ( .+) `/
if choice_tag
if !eval($1)
choice_ignore << choices.size - 1
end
end
# ------------------------------------------------------------
# 图片:对话开始,自动名称和头像
# ------------------------------------------------------------
when /^!\[ (.+)\] \( (.+)\) /
name, fig = $1, $2
if fig =~ /"(\d +)"/
num = $1.to_i
fig = fig.sub(/"\d +"/, '').strip
else
num = 0
end
msg_tag = true
add(101, 0, [fig, num, 0, 2])
add(401, 0, [name])
# ------------------------------------------------------------
# 邮件<>:可见指令
# ------------------------------------------------------------
when /^<(.+)@(.+)>/ #
cmd, para = $1, $2
case cmd
when 'CommonEvent'
if para =~ /\d +/
add(117, 0, [para.to_i])
else
id = $data_common_events.find{|e| e && e.name == para}.id
add(117, 0, [id])
end
when 'GetItem'
add(104, 0, [para.to_i])
when 'JumpTo'
add(119, 0, [para])
@keys << para
when 'AutoEvent'
if para == 'reload'
add(355, 0, ['$game_map.events[@event_id].auto_reload'])
end
end
# ------------------------------------------------------------
# [sth]: site "description" 不可见指令
# ------------------------------------------------------------
when /^\[ (.+)\] \s *:\s *(.+)\s +"(.+)"/
cmd, para1, para2 = $1, $2, $3
case cmd
when 'SelfSwitch' # 独立开关
_v = (para2 == 'ON')? 0 : 1
add(123, 0, [para1, _v])
when 'Move', 'Moves' # 移动事件,等待移动结束
if para1 =~ /\- *\d +/
id = para1.to_i
else
add(355, 0,
["s = $game_map.events.select{|id, e| e.name == '#{para1}'}"]
)
add(655, 0, ["@list[@index + 1].parameters[0] = s.keys[0]"])
end
route = add_moveroute(para2, cmd == 'Move')
add(205, 0, [id, route])
when 'PlaySE'
add(250, 0, [RPG::SE.new(para1, para2.to_i, 100)])
when 'Dialog'
case para1
when 'change-skin'
id = Window_Message::Skin[:var]
add(122, 0, [id, id, 0, 4, para2])
end
end
# ------------------------------------------------------------
# [label](script) 条件跳转
# ------------------------------------------------------------
when /^\[ (.+)\] \( (.+)\) /
label, script = $1, $2
add(111, 0, [12, script])
add(119, 1, [label])
add(0, 1, [])
add(412, 0, [])
add(118, 0, ['#' + label])
@keys << label
# ------------------------------------------------------------
# [code][paras] 直接执行命令:
# [201] [0, map, x, y, 0, 0] 切换地图
# [214] [] 暂时消除事件
# [213] [-1, 1, true] Balloon
# ...
# ------------------------------------------------------------
when /^\[ (\d +)\] (\[ .+\] )/
add($1.to_i, 0, eval($2))
# ------------------------------------------------------------
# 表格的情况
# ------------------------------------------------------------
when /^.+\| /
ary = line.split('|')
if table_tag
table_rows << ary
else
if table_rows.empty?
table_rows << ary
elsif ['-'] * table_rows[0].size == ary
table_tag = true
else
table_rows.clear
end
end
# ------------------------------------------------------------
# 其他的情况
# ------------------------------------------------------------
else
# 移除行尾的 <br/> 或者 <br>
line.sub!(/<br\/ *>$/, '')
# 转义 ` ` 中的内容
while line =~ /` ( .* ?) `/
line.sub! /` .* ?`/, eval($1).to_s
end
# 视为普通的对话
if msg_tag
add(401, 0, [line])
else
msg_tag = true
add(101, 0, ['', 0, 1, 2])
add(401, 0, [line])
end
end
end
# 如果以选择项结尾
add_choices(choices, choice_ignore) if choice_tag
add_table(table_rows) if table_tag
end
def add(*parameters)
@list << RPG::EventCommand.new(*parameters)
end
def add_choices(choices, choice_ignore)
@keys.concat(choices)
choice_ignore.each do |i|
choices[i] = nil
end
choices.compact!
add(102, 0, [choices, 5])
choices.each_index do |i|
add(402, 0, [i, choices[i]])
add(119, 1, [choices[i]])
add(0, 1, [])
end
add(403, 0, [])
add(0, 1, [])
add(404, 0, [])
end
def add_table(rows)
case rows[0][0]
when 'Shop'
add(302, 0, [-1, 0, 0, 0, rows[0][1] =~ /^P/])
rows.each do |paras|
paras[2] = (paras[3] == '-') ? 0 : 1
add(605, 0, paras.map{|x| x.to_i})
end
end
end
def add_moveroute(string, wait = false)
route = RPG::MoveRoute.new
route.repeat = false
route.skippable = true
route.wait = wait
route.list.clear
list = []
string.split(' ').each do |c|
case c
# move
when /w(\d +)/ then list.concat([4, []] * $1.to_i) # move up
when /a(\d +)/ then list.concat([2, []] * $1.to_i) # move left
when /s(\d +)/ then list.concat([1, []] * $1.to_i) # move down
when /d(\d +)/ then list.concat([3, []] * $1.to_i) # move right
when /f(\d +)/ then list.concat([12, []] * $1.to_i) # move forward
when /b(\d +)/ then list.concat([13, []] * $1.to_i) # move backward
when /j(.+)/ then list << [14, $1.split('.').map{|i| i.to_i}] # jump
when /J(.+)/ then list << [45, ["moveto #{$1.sub('.', ',')}"]] # move to
# turn
when 'W' then list << [19, []] # turn up
when 'A' then list << [17, []] # turn left
when 'S' then list << [16, []] # turn down
when 'D' then list << [18, []] # turn right
when 'L' then list << [21, []] # turn left 90
when 'R' then list << [20, []] # turn right 90
when 'B' then list << [22, []] # turn 180
when 'FIX' then list << [35, []] # fix direction on
when 'fix' then list << [36, []] # fix direction off
# move setting
when 'PASS' then list << [37, []] # pass through on
when 'pass' then list << [38, []] # pass through off
when /v(\d +)/ then list << [29, [$1.to_i]] # change speed
when /V(\d +)/ then list << [30, [$1.to_i]] # change frequency
when 'STEP' then list << [33, []] # step animation on
when 'step' then list << [33, []] # step animation off
# opacity
when 'T' then list << [39, []] # transparent on
when 't' then list << [40, []] # transparent off
when /o(\d +)/ then list << [42, [$1.to_i]] # opacity
# other
when /W(\d +)/ then list << [15, [$1.to_i]] # wait
when /X(\d +)/ then list << [27, [$1.to_i]] # switch on
when /x(\d +)/ then list << [28, [$1.to_i]] # switch off
end
end
list << [0, []]
list.each do |code, paras|
route.list << RPG::MoveCommand.new(code, paras)
end
route
end
end
# functions:
# auto load event list from the txt file with same name.
class Game_Event < Game_Character
alias _auto_load_setup_page_settings setup_page_settings
def setup_page_settings
_auto_load_setup_page_settings
if @list [ 0 ] .code == 355 && @list [ 0 ] .parameters [ 0 ] =~ /AUTO:( .+) /
auto_load( @event.name , $1 .strip )
elsif @auto_load_keys
auto_load *@auto_load_keys
end
end
def auto_load( name, key)
@auto_load_keys = [ name, key]
@list .replace Auto_Event.load ( name) .list ( key)
end
def name
@event .name
end
def auto_reload
@page = nil
refresh
end
end
module Auto_Event
module_function
Event_List = { }
def reload # only after .md file changes
Event_List.keys .each do |name|
Event_List[ name] = load_list_txt( name)
end
end
def load ( name)
Event_List[ name] ||= load_list_txt( name)
@data = Event_List[ name]
@list = [ ]
@keys = [ ]
self
end
def load_list_txt( name)
data, key, value = { } , '' , [ ]
File .open ( 'Events/' + name + '.md' , 'r' ) do |f|
f.readlines .each do |line|
# start with # key
if line =~ /^#+\s*(.+)/
data[ key] = value.join ( "\n " ) if key != ''
key, value = $1 , [ ]
next
else
value << line.strip
next
end
end
# last key
data[ key] = value.join ( "\n " ) if key != ''
end
data
end
def list( key)
@keys << key
event_keys = [ ]
while @keys .size != event_keys.size
@keys .uniq !
@keys .each do |key|
next if event_keys.include ?( key)
event_keys << key
next if !@data[ key]
add( 118 , 0 , [ key] )
set_text( @data[ key] )
add( 115 , 0 , [ ] )
end
end
add( 0 , 0 , [ ] )
end
def set_text( text)
msg_tag, script_tag, choice_tag, table_tag = *[ false ] * 4
choices = [ ]
choice_ignore = [ ]
table_rows = [ ]
text.split ( "\n " ) .each do |line|
# ------------------------------------------------------------
# 优先判定是否为脚本
# ------------------------------------------------------------
if script_tag
if line =~ /^`` `/
script_tag = false
else
add(655, 0, [line])
msg_tag = false
end
next
end
# ------------------------------------------------------------
# 判定注释
# ------------------------------------------------------------
if line =~ /^>/
next
end
# ------------------------------------------------------------
# 继续剩下的判定
# ------------------------------------------------------------
case line
# ------------------------------------------------------------
# ` `` 识别为 脚本
# ------------------------------------------------------------
when /^`` `/
add(355, 0, '#~ event scripts')
script_tag = true
# ------------------------------------------------------------
# 空行
# ------------------------------------------------------------
when /^\s *$/
if msg_tag
msg_tag = false
end
if choice_tag
choice_tag = false
add_choices(choices, choice_ignore)
end
if table_tag
table_tag = false
add_table(table_rows)
table_rows.clear
end
# ------------------------------------------------------------
# 数字列表:选择项
# ------------------------------------------------------------
when /^\s *\d +\. \s *(.+)/
text = $1
if choice_tag
choices << text
else
choice_tag = true
choice_ignore = []
choices = [text]
end
when /^-\s +` ( .+) `/
if choice_tag
if !eval($1)
choice_ignore << choices.size - 1
end
end
# ------------------------------------------------------------
# 图片:对话开始,自动名称和头像
# ------------------------------------------------------------
when /^!\[ (.+)\] \( (.+)\) /
name, fig = $1, $2
if fig =~ /"(\d +)"/
num = $1.to_i
fig = fig.sub(/"\d +"/, '').strip
else
num = 0
end
msg_tag = true
add(101, 0, [fig, num, 0, 2])
add(401, 0, [name])
# ------------------------------------------------------------
# 邮件<>:可见指令
# ------------------------------------------------------------
when /^<(.+)@(.+)>/ #
cmd, para = $1, $2
case cmd
when 'CommonEvent'
if para =~ /\d +/
add(117, 0, [para.to_i])
else
id = $data_common_events.find{|e| e && e.name == para}.id
add(117, 0, [id])
end
when 'GetItem'
add(104, 0, [para.to_i])
when 'JumpTo'
add(119, 0, [para])
@keys << para
when 'AutoEvent'
if para == 'reload'
add(355, 0, ['$game_map.events[@event_id].auto_reload'])
end
end
# ------------------------------------------------------------
# [sth]: site "description" 不可见指令
# ------------------------------------------------------------
when /^\[ (.+)\] \s *:\s *(.+)\s +"(.+)"/
cmd, para1, para2 = $1, $2, $3
case cmd
when 'SelfSwitch' # 独立开关
_v = (para2 == 'ON')? 0 : 1
add(123, 0, [para1, _v])
when 'Move', 'Moves' # 移动事件,等待移动结束
if para1 =~ /\- *\d +/
id = para1.to_i
else
add(355, 0,
["s = $game_map.events.select{|id, e| e.name == '#{para1}'}"]
)
add(655, 0, ["@list[@index + 1].parameters[0] = s.keys[0]"])
end
route = add_moveroute(para2, cmd == 'Move')
add(205, 0, [id, route])
when 'PlaySE'
add(250, 0, [RPG::SE.new(para1, para2.to_i, 100)])
when 'Dialog'
case para1
when 'change-skin'
id = Window_Message::Skin[:var]
add(122, 0, [id, id, 0, 4, para2])
end
end
# ------------------------------------------------------------
# [label](script) 条件跳转
# ------------------------------------------------------------
when /^\[ (.+)\] \( (.+)\) /
label, script = $1, $2
add(111, 0, [12, script])
add(119, 1, [label])
add(0, 1, [])
add(412, 0, [])
add(118, 0, ['#' + label])
@keys << label
# ------------------------------------------------------------
# [code][paras] 直接执行命令:
# [201] [0, map, x, y, 0, 0] 切换地图
# [214] [] 暂时消除事件
# [213] [-1, 1, true] Balloon
# ...
# ------------------------------------------------------------
when /^\[ (\d +)\] (\[ .+\] )/
add($1.to_i, 0, eval($2))
# ------------------------------------------------------------
# 表格的情况
# ------------------------------------------------------------
when /^.+\| /
ary = line.split('|')
if table_tag
table_rows << ary
else
if table_rows.empty?
table_rows << ary
elsif ['-'] * table_rows[0].size == ary
table_tag = true
else
table_rows.clear
end
end
# ------------------------------------------------------------
# 其他的情况
# ------------------------------------------------------------
else
# 移除行尾的 <br/> 或者 <br>
line.sub!(/<br\/ *>$/, '')
# 转义 ` ` 中的内容
while line =~ /` ( .* ?) `/
line.sub! /` .* ?`/, eval($1).to_s
end
# 视为普通的对话
if msg_tag
add(401, 0, [line])
else
msg_tag = true
add(101, 0, ['', 0, 1, 2])
add(401, 0, [line])
end
end
end
# 如果以选择项结尾
add_choices(choices, choice_ignore) if choice_tag
add_table(table_rows) if table_tag
end
def add(*parameters)
@list << RPG::EventCommand.new(*parameters)
end
def add_choices(choices, choice_ignore)
@keys.concat(choices)
choice_ignore.each do |i|
choices[i] = nil
end
choices.compact!
add(102, 0, [choices, 5])
choices.each_index do |i|
add(402, 0, [i, choices[i]])
add(119, 1, [choices[i]])
add(0, 1, [])
end
add(403, 0, [])
add(0, 1, [])
add(404, 0, [])
end
def add_table(rows)
case rows[0][0]
when 'Shop'
add(302, 0, [-1, 0, 0, 0, rows[0][1] =~ /^P/])
rows.each do |paras|
paras[2] = (paras[3] == '-') ? 0 : 1
add(605, 0, paras.map{|x| x.to_i})
end
end
end
def add_moveroute(string, wait = false)
route = RPG::MoveRoute.new
route.repeat = false
route.skippable = true
route.wait = wait
route.list.clear
list = []
string.split(' ').each do |c|
case c
# move
when /w(\d +)/ then list.concat([4, []] * $1.to_i) # move up
when /a(\d +)/ then list.concat([2, []] * $1.to_i) # move left
when /s(\d +)/ then list.concat([1, []] * $1.to_i) # move down
when /d(\d +)/ then list.concat([3, []] * $1.to_i) # move right
when /f(\d +)/ then list.concat([12, []] * $1.to_i) # move forward
when /b(\d +)/ then list.concat([13, []] * $1.to_i) # move backward
when /j(.+)/ then list << [14, $1.split('.').map{|i| i.to_i}] # jump
when /J(.+)/ then list << [45, ["moveto #{$1.sub('.', ',')}"]] # move to
# turn
when 'W' then list << [19, []] # turn up
when 'A' then list << [17, []] # turn left
when 'S' then list << [16, []] # turn down
when 'D' then list << [18, []] # turn right
when 'L' then list << [21, []] # turn left 90
when 'R' then list << [20, []] # turn right 90
when 'B' then list << [22, []] # turn 180
when 'FIX' then list << [35, []] # fix direction on
when 'fix' then list << [36, []] # fix direction off
# move setting
when 'PASS' then list << [37, []] # pass through on
when 'pass' then list << [38, []] # pass through off
when /v(\d +)/ then list << [29, [$1.to_i]] # change speed
when /V(\d +)/ then list << [30, [$1.to_i]] # change frequency
when 'STEP' then list << [33, []] # step animation on
when 'step' then list << [33, []] # step animation off
# opacity
when 'T' then list << [39, []] # transparent on
when 't' then list << [40, []] # transparent off
when /o(\d +)/ then list << [42, [$1.to_i]] # opacity
# other
when /W(\d +)/ then list << [15, [$1.to_i]] # wait
when /X(\d +)/ then list << [27, [$1.to_i]] # switch on
when /x(\d +)/ then list << [28, [$1.to_i]] # switch off
end
end
list << [0, []]
list.each do |code, paras|
route.list << RPG::MoveCommand.new(code, paras)
end
route
end
end
作者: 七重 时间: 2018-7-25 21:23
类似的脚本有用过下,hime的TES。
我觉得学起来是挺麻烦的,主要是新的编辑法则。。。
但是这个脚本本身是带有加密的,我就看到过隔壁的汉化组在这个的解包上焦头烂额。。。
虽然现在已经有专用的提取工具了。
作者: 百里_飞柳 时间: 2018-7-25 21:37
之前就一直觉得在编辑器里定义事件真的蜜汁心累,尤其是移动路径(虽然之后接入了一个自动寻路+等待结束偷懒)
而且默认的事件编辑器对于文本的翻译也非常不友好
不过这一套Markdown类似语法的编辑,其实有时候也有点容易懵……?
如果支持把事件页再导出成Markdown就更好了(做梦.jpg)
对于那个显示选项并跳转标签,之后就直接事件中断了??
大概可以改成在每个选项后加一个跳转到所有选项分歧结束的地方?
可以加个特定 0. 序号用于处理所有选项的结束位置?不过还一个取消时的默认分支没办法啊
作者: guoxiaomi 时间: 2018-7-26 01:21
本帖最后由 guoxiaomi 于 2018-7-26 01:26 编辑
把事件转化成markdown指令也是可行的,但是格式如果不是按照约定好的话,会有很多麻烦。我这里的设定是markdown里的每一个小节都是以标签开始,以中断事件结束。这是为了结构简单,故意做成小节之间无嵌套关系,但是选择项就不好实现。我这里只好是选项默认跟一个同名的标签跳转。
其实主要还是看个人的约定了~
关于取消,在选项后面还是可以添加内容的,如果取消就执行这之后的内容。也可以在这之后跳转到别的标签。类似下面这样~
## 主线
对话1
1. 跳转1
2. 跳转2
<JumpTo@跳转3>
### 跳转1
<JumpTo@跳转3>
### 跳转2
<JumpTo@跳转3>
### 跳转3 复制代码
作者: 七重 时间: 2018-7-26 07:51
本帖最后由 七重 于 2018-7-26 09:36 编辑
不过说到这样类似的需求。我是觉得如果有个更加易用的事件编辑器就好了。
比如自己用rm的时候,有时候就会出现变量的编号定好了之后,后面才发现又要加新的,最后东++,西++,结果越来越混乱。。又不能排列整理。
因为编号是固定的,如果要改又要把事件里面的引用修正。
tes算是把事件写在了外面,要调用的时候直接调用相关名字。
如果楼主是做的话,能再做个在外部安排开关与变量就好了。
追记:
过了一会之后。。我才忽然发现这个问题可以靠自力解决。。
作者: guoxiaomi 时间: 2018-7-26 16:56
其实写一个脚本把事件编辑器里的指令数组导出来也很容易的,我做的时候就写了一个,要不然搞不清楚它是怎么实现的。但是并不是很赞同把数据分开保存在事件编辑器和剧情文本文件里。
一开始确实想着,外挂的文本只是动态替代事件页中的一部分。事件在生成@list时,原有的内容保持不变,外挂的文本所占的位置被替换插入到@list中,如此可做到最大兼容。
做了一点后,觉得这里其实是不需要考虑兼容性的。事件指令都比较简单,复杂的部分主要是循环和分歧。实际操作的时候:
1. 循环和分歧用到的时候比较少,尤其是循环
2. 有时候,循环和分歧不是“最优解”,而是rm事件页这个框架下的无奈之举
3. 利用标签和跳转可以完全替代循环和分歧,但管理混乱+事件页太长
所以未必一定要坚持照着rm的事件框架去写,把常用的几种命令和rm事件框架恰当的结合更好。所以我选择了标签跳转。
全部的内容写到外挂文本里还有一个好处是内容统一管理,工程里放一点,外挂文本里放一点不太好。
我还提供了一个方案是随意书写指令,仍然限定了indent = 0:
用的eval执行后面的中括号的内容生成参数数组。
欢迎光临 Project1 (https://rpg.blue/)
Powered by Discuz! X3.1