最近刚看完了Ruby的《元编程》,收获颇丰。
想趁着印象深刻的时候多写点东西练练手,于是想到了这么一个功能。
在辉光翼战记中,玩家在迷宫中移动时,角色偶尔会在屏幕下方进行一些小对话。虽然对剧情本身没有什么影响,
但是这个功能不但削弱了在迷宫中移动时的烦躁感,而且还可以加深对人物性格的刻画。
RPG Maker VX的屏幕默认只有544 * 416大小,在冒险中专门取出一块地方来进行小对话会让游戏画面显得更加狭窄,由此选择了使用漂浮气泡对话。
本脚本主要使用了RPG Maker VX的跟随和村人のつぶやき两个脚本,以这两个脚本为基础,设计了一个小型DSL,这两个脚本的内容附在文章最后。
使用方法如下:
story_event "First Event" do message [-1, 'First Message'] message [0, 'Second Message'] scope [1, 2, 3, 5] end
每个小对话使用 story_event来定义,并使用一个字符串对其命名,便于区别其他的小对话
message用来声明一条信息,其中第一个参数表示说话角色,第二个参数表示信息内容。
scope用来声明作用范围,其内容为一个包含了剧情可发生地图的id数组,上例中该剧情可以在map_id为1、2、3、5的地图中发生。
通过提前设置好对话和作用范围,就可以实现玩家在地图移动时的随机对话了。
作为1.0版本,只实现了一个大致上的功能。还有一些设想中的功能没有制作,内容包括:
1. 添加一个run_switch, 内容是一个开关id数组,当run_switch中的所有开关打开时该剧情才可能发生。
2. 添加一个close_switch,其功能和run_switch相反,用来终止一个剧情的发生。
3. 每条message除了可以声明角色和信息内容以外,还可以指定气泡滞留时间,使得句子长的信息滞留更久,句子短的信息消失更快。
4. 可以通过给地图增加特殊声明来修改该地图中随机剧情的出现频度
大概会在后续版本中添加上去。
附录
地图随机剧情脚本
#============================================================================== # # 通过扩展Kernel的功能实现的小型DSL # 由于RMVX的问题,class_eval内的代码是被迫实现的 # 原版代码如下 # Kernel.send :define_method, :story_event do |name, &block| # now_event = event_list[name] = Move_Talking::Story_Event.new(name) # block.call # end # 在RMVX中,由于|name, &block|处会报语法错误,所以变成了现在的丑陋样子。 # 如果有人知道在RMVX中怎么通过define_method定义方法并可以传递block请告诉我。 # #============================================================================= lambda { event_list = {} #用以储存所有小剧情事件 now_event = nil #用以构造小剧情事件 #-------------------------------------------------------------------------- # ● 定义story_event方法 #-------------------------------------------------------------------------- Kernel.class_eval('def story_event(name, &block) event_list = get_event_list event_list[name] = Move_Talking::Story_Event.new(name) now_event = event_list[name] set_now_event(now_event) block.call end') #-------------------------------------------------------------------------- # ● 获取event_list # story_event方法专用 # 如果可以将story_event改回使用define_method实现,本方法可作废 #-------------------------------------------------------------------------- Kernel.send :define_method, :get_event_list do return event_list end #-------------------------------------------------------------------------- # ● 设置now_event # story_event方法专用 # 如果可以将story_event改回使用define_method实现,本方法可作废 #-------------------------------------------------------------------------- Kernel.send :define_method, :set_now_event do |event| now_event = event end #-------------------------------------------------------------------------- # ● 定义message方法 # 接收一个数组作为message内容 #-------------------------------------------------------------------------- Kernel.send :define_method, :message do |param| now_event.msg_data << param end #-------------------------------------------------------------------------- # ● 定义scope方法 # 接收一个数组作为scope内容 #-------------------------------------------------------------------------- Kernel.send :define_method, :scope do |scope| now_event.scope = scope end #-------------------------------------------------------------------------- # ● 定义run_switch方法 # 接收一个数组作为run_switch内容 #-------------------------------------------------------------------------- Kernel.send :define_method, :run_switch do |run_switch| now_event.run_switch = run_switch end #-------------------------------------------------------------------------- # ● 获取当前地图适用的小剧情 # 此处只考虑map_id其他操作在scene_map中具体判别 #-------------------------------------------------------------------------- Kernel.send :define_method, :get_story_event do |map_id| result = [] event_list.each do |key, val| if val.scope.nil? then result.push val elsif val.scope.include? map_id then result.push val end end return result end }.call #============================================================================== # ■ Move_Talking #------------------------------------------------------------------------------ # 移动中对话的主要模块 #============================================================================== module Move_Talking TRIGGER_COOLDOWN = 1500 AWP = 500 #-------------------------------------------------------------------------- # ●Story_Event # # 用来装载小对话的类 #-------------------------------------------------------------------------- class Story_Event attr_accessor :scope attr_accessor :run_switch attr_accessor :name attr_accessor :msg_data attr_accessor :cooldown #-------------------------------------------------------------------------- # ● 初始化 #-------------------------------------------------------------------------- def initialize(name) @name = name @msg_data = [] @scope = nil @run_switch = nil @index = 0 @cooldown = 200 end #-------------------------------------------------------------------------- # ● 运行 # 每次显示一条信息 #-------------------------------------------------------------------------- def run player = @msg_data[@index][0] message = @msg_data[@index][1].dup if player == -1 $game_player.message = message else $game_party.characters[player].message = message end @index += 1 end #-------------------------------------------------------------------------- # ● 判断是否全部显示完毕 #-------------------------------------------------------------------------- def over? @index == @msg_data.size end #-------------------------------------------------------------------------- # ● 重置 # 将index复位,以便重复播放 #-------------------------------------------------------------------------- def reset @index = 0 end end end #============================================================================== # ■ Scene_Map #------------------------------------------------------------------------------ # 处理地图画面的类。 #============================================================================== class Scene_Map < Scene_Base #-------------------------------------------------------------------------- # 初始化 #-------------------------------------------------------------------------- alias story_event_initialize initialize def initialize story_event_initialize @story_count = 0 @message_count = 0 @story_event = nil end #-------------------------------------------------------------------------- # 更新 #-------------------------------------------------------------------------- alias story_event_update update def update @story_count += 1 if @story_count >= 1500 && @story_event.nil? events = get_story_event($game_map.map_id) @story_event = events[rand(events.size)] @story_event.reset @story_event.run @message_count = 0 elsif @story_event != nil @message_count += 1 if @message_count == 200 @story_event.run @message_count = 0 end if @story_event.over? @story_event = nil @story_count = 0 end end story_event_update end end
村人のつぶやき
#============================================================================== # ★ RGSS2_村人のつぶやき # tomoaky (http://hikimoki.hp.infoseek.co.jp/) #============================================================================== #============================================================================== # ■ 設定項目 #============================================================================== module TMRBT SW_MESSAGE_STOP = 2 # 機能無効化スイッチのID MESSAGE_DURATION = 180 # メッセージの表示時間(フレーム) MESSAGE = {} # おじさんタイプのメッセージリスト MESSAGE["Test"] = [ 'Test message' ] end #============================================================================== # ■ コマンド #============================================================================== module TMRBT module Commands module_function #-------------------------------------------------------------------------- # ● 指定したIDのイベントにメッセージを強制 #-------------------------------------------------------------------------- def mes(id, text, count = 300) event = $game_map.interpreter.get_character(id) if event != nil event.message = text event.message_count = count end end #-------------------------------------------------------------------------- # ● 指定したIDのイベントの村人タイプを変更 #-------------------------------------------------------------------------- def mes_type(id, type = nil) return if id < 0 # プレイヤーには無効 event = $game_map.interpreter.get_character(id) if event != nil event.murabito = type end end end end class Game_Interpreter include TMRBT::Commands end #============================================================================== # ■ Game_Character #============================================================================== class Game_Character #-------------------------------------------------------------------------- # ● 公開インスタンス変数 #-------------------------------------------------------------------------- attr_accessor :message # フキダシメッセージ attr_accessor :message_count # メッセージ待機カウント #-------------------------------------------------------------------------- # ● オブジェクト初期化 #-------------------------------------------------------------------------- alias tmrbt_game_character_initialize initialize def initialize tmrbt_game_character_initialize @message = "" @message_count = 60 + rand(240) end #-------------------------------------------------------------------------- # ○ メッセージを表示できる状態かどうかを返す #-------------------------------------------------------------------------- def message_can_speak? return false if @character_name == "" and @tile_id == 0 return false if $game_switches[TMRBT::SW_MESSAGE_STOP] return true end end #============================================================================== # ■ Game_Event #============================================================================== class Game_Event < Game_Character #-------------------------------------------------------------------------- # ● 公開インスタンス変数 #-------------------------------------------------------------------------- attr_accessor :murabito # 村人タイプ #-------------------------------------------------------------------------- # ● オブジェクト初期化 # map_id : マップ ID # event : イベント (RPG::Event) #-------------------------------------------------------------------------- alias tmrbt_game_event_initialize initialize def initialize(map_id, event) tmrbt_game_event_initialize(map_id, event) @murabito = event.name =~ /<村人:(S+?)>/i ? $1 : nil # 村人判定 end #-------------------------------------------------------------------------- # ● フレーム更新 #-------------------------------------------------------------------------- alias tmrbt_game_event_update update def update tmrbt_game_event_update if @murabito != nil if @message_count > 0 @message_count -= 1 if @message_count == 0 if message_can_speak? n = TMRBT::MESSAGE[@murabito].size @message = TMRBT::MESSAGE[@murabito][rand(n)].clone end @message_count = 270 + rand(300) end end end end #-------------------------------------------------------------------------- # ○ 指定したIDのイベントにメッセージを強制(カスタム移動用) #-------------------------------------------------------------------------- def mes(id, text, count = 300) TMRBT::Commands.mes(id == 0 ? @id : id, text, count) end #-------------------------------------------------------------------------- # ○ 指定したIDのイベントの村人タイプを変更(カスタム移動用) #-------------------------------------------------------------------------- def mes_type(id, type = nil) TMRBT::Commands.mes_type(id == 0 ? @id : id, type) end end #============================================================================== # ■ Sprite_Character #============================================================================== class Sprite_Character < Sprite_Base @@bitmap_murawin = Cache.system("murawin") @@windowskin = Cache.system("Window") #-------------------------------------------------------------------------- # ● オブジェクト初期化 # viewport : ビューポート # character : キャラクター (Game_Character) #-------------------------------------------------------------------------- alias tmrbt_sprite_character_initialize initialize def initialize(viewport, character = nil) @message_duration = 0 tmrbt_sprite_character_initialize(viewport, character) end #-------------------------------------------------------------------------- # ● 解放 #-------------------------------------------------------------------------- alias tmrbt_sprite_character_dispose dispose def dispose dispose_message tmrbt_sprite_character_dispose end #-------------------------------------------------------------------------- # ● フレーム更新 #-------------------------------------------------------------------------- alias tmrbt_sprite_character_update update def update tmrbt_sprite_character_update update_message if @character.message != "" @message = @character.message start_message @character.message = "" end end #-------------------------------------------------------------------------- # ○ 文字色取得 # n : 文字色番号 (0~31) #-------------------------------------------------------------------------- def text_color(n) x = 64 + (n % 8) * 8 y = 96 + (n / 8) * 8 return @@windowskin.get_pixel(x, y) end #-------------------------------------------------------------------------- # ○ 特殊文字の変換 #-------------------------------------------------------------------------- def convert_special_characters if @message == nil @message = "" end @message.gsub!(/\L/) { "x00" } @message.gsub!(/\V[([0-9]+)]/i) { $game_variables[$1.to_i] } @message.gsub!(/\N[([0-9]+)]/i) { $game_actors[$1.to_i].name } @message.gsub!(/\C[([0-9]+)]/i) { "x01[#{$1}]" } @message.gsub!(/\\/) { "\" } end #-------------------------------------------------------------------------- # ○ フキダシメッセージ表示の開始 #-------------------------------------------------------------------------- def start_message dispose_message convert_special_characters @message_duration = TMRBT::MESSAGE_DURATION bitmap = Bitmap.new(160, 160) bitmap.font.name = "Verdana" bitmap.font.size = 14 w = 0 contents_x = 4 contents_y = 0 loop do c = @message.slice!(/./m) # 次の文字を取得 case c when nil break # 描画すべき文字がなければ終了 when "x00" # 改行 w = contents_x if w < contents_x contents_x = 4 contents_y += 16 next when "x01" # C[n] (文字色変更) @message.sub!(/[([0-9]+)]/, "") bitmap.font.color = text_color($1.to_i) next else bitmap.font.color = Color.new(255, 255, 255) bitmap.draw_edge_text(contents_x, contents_y, 40, 16, c, 0, Color.new(94, 24, 24)) c_width = bitmap.text_size(c).width contents_x += c_width if contents_x >= 140 # 右端にきていれば改行 w = contents_x if w < contents_x contents_x = 4 contents_y += 16 end end end h = contents_y + (contents_x == 4 ? 0 : 16) w = (w < contents_x ? contents_x + 4 : w + 4) @message_sprite = ::Sprite.new(viewport) @message_sprite.ox = w / 2 @message_sprite.oy = h + 4 @message_sprite.bitmap = Bitmap.new(w, h + 8) rect = Rect.new(0, 0, 8, 8) @message_sprite.bitmap.blt(0, 0, @@bitmap_murawin, rect, 192) rect.x += 8 @message_sprite.bitmap.blt(w - 8, 0, @@bitmap_murawin, rect, 192) rect.y += 8 @message_sprite.bitmap.blt(w - 8, h - 8, @@bitmap_murawin, rect, 192) rect.x -= 8 @message_sprite.bitmap.blt(0, h - 8, @@bitmap_murawin, rect, 192) rect.set(16, 0, 8, 8) @message_sprite.bitmap.blt(@message_sprite.ox - 4, h, @@bitmap_murawin, rect, 192) color = @@bitmap_murawin.get_pixel(8, 8) color.alpha = 192 @message_sprite.bitmap.fill_rect(8, 0, w - 16, h, color) @message_sprite.bitmap.fill_rect(0, 8, 8, h - 16, color) @message_sprite.bitmap.fill_rect(w - 8, 8, 8, h - 16, color) @message_sprite.bitmap.blt(0, 0, bitmap, bitmap.rect) bitmap.dispose update_message end #-------------------------------------------------------------------------- # ○ フキダシメッセージの更新 #-------------------------------------------------------------------------- def update_message if @message_duration > 0 @message_duration -= 1 if @message_duration == 0 or not @character.message_can_speak? @message_duration = 0 dispose_message else @message_sprite.x = x @message_sprite.y = y - height @message_sprite.z = z + 200 @message_sprite.opacity = @message_duration * 24 end end end #-------------------------------------------------------------------------- # ○ フキダシメッセージの解放 #-------------------------------------------------------------------------- def dispose_message if @message_sprite != nil @message_sprite.dispose @message_sprite = nil end end end
跟随脚本