5-3 Làm một tiểu thuyết trực quan

5-3 Làm một tiểu thuyết trực quan

Phần này chúng ta sẽ tạo một tiểu thuyết trực quan đơn giản. Là game hiển thị hình ảnh và đoạn văn để triển khai một câu chuyện

Nhập môn

Tiểu thuyết trực quan so với những game chân thực phải thao tác với nhân vật như game hành động thì việc viết ra dễ hơn nhiều. Chương này, chúng ta sẽ chỉ làm một [visualnovel.rb] nhỏ chỉ gồm 110 dòng. Chúng ta dãy thử xem [visualnovel.rb] một lần xem sao. Nếu chỉ xem qua thì có lẽ chúng ta cũng không hiểu nó đang làm cái gì nhưng về nội dung chi tiết thì sẽ có giải thích ở phần sau nên mọi người cũng không cần lo lắng đâu. Nếu xem [visualnovel.rb] thì chúng ta cũng có thể hiểu qua class [VisualNovelScene] được định nghĩa như thế nào. [VisualNovelScene] class được định nghĩa như một màn hình game thông thường. Vì vậy, có những lệnh cơ bản sẽ được định nghĩa trong class sẽ là [init] [update] [render]. Nếu xem qua những lệnh khác thì ta thấy lệnh [command_] có rất nhiều. Đây chính là lệnh định nghĩa như để command những hình ảnh hay những lời thoại. Ngoài những lệnh đó thì chúng ta chỉ có thêm 3 lệnh khác.

Vậy các bạn đã hiểu tại sao tôi nói chương trình game tập hợp những lệnh trên là chương trình game không lớn chưa?

Hình 5-23 Visual Novel

17.PNG

Suy nghĩ cấu trúc của game

Có những thành tố dưới đây cấu tạo nên màn hình game


  • Hình ảnh nền (Background)
  • Hình ảnh mặt trước (foreground)
  • Text

Bên trên chính là những hiển thị cơ bản của game. Nếu chúng ta kết hợp với tiến trình của game, thay đổi các hiển thị thì ta sẽ tạo ra được một Visual Game.

Chương trình chỉ để vẽ những hình ảnh sẽ hiển thị

Chúng ta hãy thử viết chương trình chỉ để vẽ những hình ảnh hiển thị.

visualnovel01.rb

require 'mygame/boot'

class VisualNovelScene < Scene::Base
	FONT_SIZE   = 24
	TEXT_MARGIN = 16
	LINE_HEIGHT = 32
	def init
		Font.default_size = FONT_SIZE
		@bg = Image.new("images/bg.jpg")
		@fg = TransparentImage.new("images/fg_girl.png")
		@text = []
		@texts << ShadowFont.new("Dòng một là Text")
		@texts << ShadowFont.new("Dòng hai là Text")
		@texts << ShadowFont.new("Dòng ba là Text")
	end

	def render
		@bg.render
		@fg.render
		@texts.each_with_index do |e,i|
			e.x = TEXT_MARGIN
			e.y = TEXT_MARGIN + LINE_HEIGHT*i
			e.render
	    end
	end
end

Scene.main_loop VisualNovelScene

Chúng ta tạo [ViisualNovelScene] theo [Scene::Base] và định nghĩa màn hình như một class. Tạo object hình ảnh bằng lệnh [init] trong [VisualNovelScene] và vẽ object đã tạo ra đó lên màn hình bằng lệnh [render].


  • Hình ảnh background: thay bằng object hình ảnh background @bg
  • Hình ảnh foregroung: thay bằng object hình ảnh foregroung @fg
  • Text: thay bằng font object được vẽ trong @texts

Cấu tạo nên dữ liệu lời thoại

Nói chung những hình ảnh cần thiết đã được hiển thị. Tuy nhiên, chỉ như thế này thì mỗi lần hiển thị chúng chỉ hiện những tin nhắn giống nhau. Trong game thực tế cần nhiều lời thoại hơn thế. Hơn nữa nếu chúng ta không kết hợp lời thoại và hình ảnh với nhau thì cũng không thể hình thành nên game trên thực tế được. Những trường hợp như thế này, thì những thông tin muốn hiển thị chúng ta sẽ dữ liệu hóa. Nếu chúng ta thiết lập được chương trình sao cho dữ liệu đọc vào khác nhau thì sẽ thay đổi hiển thị khác nhau thì chỉ cần gắn đủ dữ liệu thì chỉ cần thêm vào thông tin là có thể thêm vào những lời thoại mới. Dữ liệu lời thoại sẽ được viết sử dụng dãy như sau.

[
[command, option],
[command, option],
[command, option],
]

Tại command thì chuẩn bị 3 loại sau.


  • Command vẽ hình ảnh background
  • Command vẽ hình ảnh foreground
  • Command hiển thị text

Sau này sẽ cần thiết thêm một số command khác nhưng trước hết chúng ta sẽ tiếp tục câu chuyện về 3 command này. Những command khác sẽ tùy vào độ cần thiết mà chúng ta sẽ thêm vào sau. Dữ liệu lệnh, về cơ bản chúng ta viết như sau.

# Dữ liệu lời thoại
[
[:bg, "images/bg.jpg"],
[:fg, "images/fg_girl.png"],
[:text, "Dòng 1 là text"],
[:text, "Dòng 2 là text"],
[:text, "Dòng 3 là text"],
]

[:bg] là command hiển thị hình ảnh background, [:fg] là command hiển thị hình ảnh foreground. Sau đó từng cái một bằng giá trị phía sau sẽ chỉ định tên hình ảnh hiển thị như một object. [:text] là command biểu thị text và sẽ chỉ định hiển thị dòng chữ text như một object. Từ những dòng command này mà sẽ tiến hành hiển thị. Câu chuyện sẽ tùy vào dữ liệu mà được quyết định dần dần. Cso nghĩa là dữ liệu chính là lời thoại cho câu chuyện.

Đọc dữ liệu lời thoại

Những dữ liệu lời thoại mà ta nghĩ ra từ trước chính là nơi tập trung của những command. Chúng ta thử tạo nên bộ phận xử lý những command này nhé. Đầu tiên, lệnh [init] sẽ được biến đổi như sau

def init
  Font.default_size = FONT_SIZE
  @bg = nil
  @fg = nil
  @texts = []
  @commands = []
  @commands << [:bg, "images/bg.jpg"]
  @commands << [:fg, "images/fg_girl.png"]
  @commands << [:text, "Dòng 1 là text."]
  @commands << [:text, "Dòng 2 là text."]
  @commands << [:text, "Dòng 3 là text."]
end

[@command] là dãy họ command chỉ dữ liệu lời thoại. Tại đây, sau khi chúng ta nhập dữ liệu ban đầu cho dãy [@commands] thì chúng ta thêm dữ liệu lệnh vào dãy đó. Ở [@bg] và [@fg] thì đã cho sẵn giá trị [nil]. Hơn nữa, lệnh [render] thì chúng ta sử như sau.

def render
  @bg.render if @bg
  @fg.render if @fg
  (lược)

Nếu làm thế này thì khi giá trị [@bg] và [@fg] thì [@bg.render] sẽ không được thực hiện và [fg] cũng vậy. Chúng ta cùng cho những lệnh xử lý đã viết ra vào class [VisualNovelScene]. 3 lệnh tiếp theo đây sẽ có những command tương ứng như bên đưới

def command_bg(fname)
  @bg = Image.new(fname)
end

def command_fg(name)
  @fg = TransparentImage.new(fname)
end

def command_text(text = " ")
  font = ShadowFonet.new(text)
  @texts << font
ẹnd

Tên những lệnh thực hiện command thì chúng ta đang để là [command_~]. Lệnh [command_bg] tạo object hình ảnh và thay thế nó vào [@bg]. Tên của file sẽ được tiếp nhận như một argument của lệnh. [command_fg] cũng giống như vậy, cũng thay thế vào hình ảnh foreground cho hình ảnh. Lênh [command_text] sẽ viết ra dãy chữ được nhập vào như một argument và nhập vào [@text]. Vì giá trị mặc định của dãy chữ là không có gì nên nếu chúng ta giản lược argument thì dòng viết sẽ trở thành không có gì.

Với dòng code như dưới đây

def command_text(text = " ")
  font = ShadowFonet.new(text)
  @texts << font

Nếu để dãy chữ là rỗng như command_text(text= "") thì chương trình sẽ lỗi nên chúng ta cần để dấu cách ở giữa để chương trình có thể chạy bình thường là command_text(text=" ").

Từ dòng [@commands] được nhập dữ liệu ban đầu và sẽ xử lý đọc vào dữ kiệu lệnh thì chúng ta cần định nghĩa thêm lệnh [run_command.]

def run_command
  if params = @commands.shift
    commans = params.shif
    case command
    when :bg
      command_bg params[0]
    when :fg
       command_fg params[0]
    when :text
     command_text params[0]
    end
  end
end

Trước khi chạy lệnh [run_command] thì chúng chúng ta hãy nghĩ rằng [@commands] đang được xử lý như sau.

[
[Command, option],	# Command được đọc từ trên xuống
[Command, option],
[Command, option],
]

Lệnh [run_command], đầu tiên thực hiện [@commands.shift] và các thành tố được đọc từ trên trở xuống, biến số [params] được đưa vào giá trị. Nếu nội dung phía trong của [@commands] mà rỗng thì [params] được thay giá trị [nil], nội dung trong câu if không được chayh. Trong trường hợp [params] không phải là [nil] thì trong params dãy dữ liệu sau chắc chắn sẽ được thay vào.

[command, option]	# nội dung bên trong params

Vì command đã được thêm vào từ đầu của params nên ta lấy cái đó ra và thay vào biến [command]. Chạy xử lý này chính là bộ phận tiếp theo đây.

command = params.shift

Thời khắc này, biến số [command] được thay lệnh vào và tại params chỉ còn lại nội dung sau.

[option] # nội dung bên trong params

Sau đó dùng [case] để gọi lệnh đối với từng command. Argument của command method chính là thành tố đầu của params (Option). Sau đó, nếu gọi [run_command] bằng lệnh [update] thì thông qua nội dung trong [@command] được nhập từ [init] thì trên thực tế command method sẽ được chạy.

  def update
    run_command
  end

Chương trình cho đến phần này sẽ là,

require 'mygame/boot'

class VisualNovelScene < Scene::Base
  FONT_SIZE   = 24
  TEXT_MARGIN = 16
  LINE_HEIGHT = 32
  def init
    Font.default_size = FONT_SIZE
    @bg = nil
    @fg = nil
    @texts = []
    @commands = []
    @commands << [:bg, "images/bg.jpg"]
    @commands << [:fg, "images/fg_girl.png"]
    @commands << [:text, "Dòng 1 là text."]
    @commands << [:text, "Dòng 2 là text."]
    @commands << [:text, "Dòng 3 là text."]
  end

  def run_command
    if params = @commands.shift
      command = params.shift
      case command
      when :bg
        command_bg params[0]
      when :fg
        command_fg params[0]
      when :text
        command_text params[0]
      end
    end
  end

  def command_bg(fname)
    @bg = Image.new(fname)
  end

  def command_fg(fname)
    @fg = TransparentImage.new(fname)
  end

  def command_text(text = "")
    font = ShadowFont.new(text)
    @texts << font
  end

  def update
    run_command
  end

  def render
    @bg.render if @bg
    @fg.render if @fg
    @texts.each_with_index do |e, i|
      e.x = TEXT_MARGIN
      e.y = TEXT_MARGIN + LINE_HEIGHT * i
      e.render
    end
  end
end

Scene.main_loop VisualNovelScene

Thêm command chờ

Các bạn chạy thử chương trình này thì có thể hiểu, tất cả các command được thự hiện cùng một lúc và gần như đồng thời những hình ảnh hiển thị hiển thị ra hết màn hình. Tại đây thì chúng ta có thể thêm tính năng chờ để thêm thời gian điều tiết hiển thị.

Dữ liệu lời thoại sẽ được viết như dưới đây. [:wait] là bộ phận command và bộ phận option, chúng ta sẽ viết thời gian chờ. Đơn vị của thời gian chính là đơn vị FPS của loop chính, theo mặc định sẽ là 1/60 giây, tức 1 giây có 60 hình ảnh.

@commands = []
@commands << [:bg, "images/bg.jpg"]
@commands << [:wait, 30]
@commands << [:fg, "images/fg_girl.png"]
@commands << [:wait, 30]
@commands << [:text, "Đang chờ. Bạn hãy bấm phím Space đi nhé!"]
@commands << [:wait]
@commands << [:text, "Thời gian chờ đã kết thúc"]

Những command được thêm vào dãy [@commands] sẽ được đọc theo thứ tự từ trên xuống dưới và được thực hiện cũng theo thứ tự trên. Đầu tiên, command [:bg] được thực hiện, sau đó [:wait] được thực hiện. Option của [:wait] là 30 nên sau khi chờ [30] tức là [0.5 giây] thì lệnh tiếp theo là [:fg] được thực hiện. Đó chính là trình tự thực hiện. Hơn nữa, nếu bộ phận option của command [:wait] bị giản lược, bỏ trống thì tại đó coi như đang thực hiện chế độ pause, cho đến khi người chơi nhập một cái gì đó thì trò chơi cũng không được tiếp tục.

Lệnh thực hiện [wait command]

def commmand_wait(n = nil)
  @wait_counter = n.to_i
  @pause = !n
end

Tại khu vực option của [@wait_counter] chúng ta sẽ nhập thời gian. Chúng ta để [n.to_i] để khi n có giá trị [nil] thì [@wait_counter] sẽ đọc để thêm vào giá trị 0. [@wait_counter] sẽ chạy tốt hơn khi đọc số nên các bạn hãy cố gắng nhất định hãy nhập số cho lệnh này. [@pause] chính là biến số để ghi nhớ trạng thái pause. Trạng thái pause mà là true thì những trạng thái ngoài ra sẽ là false.

@pause = !n

Dòng code có ý nghĩa giống như dòng code trên của chúng ta chính là

if @pause
  @pause = false
else
  @pause = true
end

Tiếp theo thêm 2 dòng sau vào phần lệnh [init] để nhập những giá trị ban đầu cho xử lý chờ sử dụng những biến instance.

@wait_counter = 0
@pause = false

Rồi trong dãy câu [case] của [run_command] thì chúng ta cũng thêm dòng sau để có thể gọi được lệnh.

when :wait
  command_wait params[0]

Tuy nhiên, kể cả command_wait có được thực hiện đi chăng nữa thì xử lý chờ vẫn chưa phát sinh. Trong lệnh [update] thì chúng ta phải viết thêm.

def update
  if @wait_counter > 0
    @wait_counter -= 1
  else
    run_command
  end
end

Trong trường hợp [@wait_counter] lớn hơn 0 thì giá trị trong [@wait_counter] sẽ bị trừ dần và [run_command] vẫn chưa dduwwocj thực hiện. Chỉ trong trường hợp [@wait_counter] bằng [0] thì lệnh [run_command] mới được gọi ra. Tức là, nếu ta điền một giá trị lớn hơn 0 tại[@wait_counter] thì [run_command] sẽ không được chạy và xử lý chờ sẽ được thực hiện. Hơn nữa, để thêm xử lý pause thì chúng ta cần sửa thêm ở lệnh [update] như dưới đây.

def update
  if @wait_counter > 0
    @wait_counter -= 1
  else
    run_command unless @pause
  end
  if @pause
    if new_key_pressed?(Key::SPACE)
      @wait_counter = 0
      @pause = false
     end
   end
end

Mọi người hãy chú ý dòng tiếp theo. Trong trường hợp [@pause] là đúng thì [run_command] sẽ không được thực hiện

run_command unless @pause

Vậy nên nếu [@pause] và true thì [run_command] sẽ mãi mãi không được gọi ra nên chúng ta thêm vào nếu nhấn dấu cách thì [@pause] sẽ trở về giá trị [false] và trạng thái dừng bị hóa giải. Chương trình cho đến phần này là

visualnovel02.rb

require 'mygame/boot'

class VisualNovelScene < Scene::Base
  FONT_SIZE   = 24
  TEXT_MARGIN = 16
  LINE_HEIGHT = 32
  def init
    Font.default_size = FONT_SIZE
    @wait_counter = 0
    @pause = false
    @bg = nil
    @fg = nil
    @texts = []
    @commands = []
    @commands << [:bg, "images/bg.jpg"]
    @commands << [:wait, 30]
    @commands << [:fg, "images/fg_girl.png"]
    @commands << [:wait, 30]
    @commands << [:text, "Đang chờ. Bạn hãy bấm phím Space đi nhé!"]
    @commands << [:wait]
    @commands << [:text, "Thời gian chờ đã kết thúc"]
  end

  def run_command
    if params = @commands.shift
      command = params.shift
      case command
      when :wait
        command_wait params[0]
      when :bg
        command_bg params[0]
      when :fg
        command_fg params[0]
      when :text
        command_text params[0]
      end
    end
  end

  def command_wait(n = nil)
    @wait_counter = n.to_i
    @pause = !n
  end

  def command_bg(fname)
    @bg = Image.new(fname)
  end

  def command_fg(fname)
    @fg = TransparentImage.new(fname)
  end

  def command_text(text = "")
    font = ShadowFont.new(text)
    @texts << font
  end

  def update
    if @wait_counter > 0
      @wait_counter -= 1
    else
      run_command unless @pause
    end
    if @pause
      if new_key_pressed?(Key::SPACE)
        @wait_counter = 0
        @pause = false
      end
    end
  end

  def render
    @bg.render if @bg
    @fg.render if @fg
    @texts.each_with_index do |e, i|
      e.x = TEXT_MARGIN
      e.y = TEXT_MARGIN + LINE_HEIGHT * i
      e.render
    end
  end
end

Scene.main_loop VisualNovelScene

Thêm command để clear những vật thông tin đã hiển thị

Chúng ta viết thêm một lời thoại dài hơn nữa nhé. Tại đây tôi sẽ chia ra là lời thoại [A] và [B].

def scenario_A
  @commands << [:bg, "images/bg.jpg"]
  @commands << [:wait, 30]
  @commands << [:fg, "images,fg_girl.png"]
  @commands << [:text, "Xin chào!"]
  @commands << [:wait, 30]
  @commands << [:text, "."]
  @commands << [:wait, 30]
  @commands << [:text, "."]
  @commands << [:wait, 30]
  @commands << [:text, "."]
  @commands << [:text, "Tôi đang đợi đó"]
  @commanđs << [:wait]
  @commands << [:text, "Vâng"]
  @commands << [:text, "Thời gian chờ kết thúc"]
  @commands << [:wait, 30]
end

def scenario B
  @commands << [:clear]  #xóa hết những commands đã hiển thị
  @commands << [:wait, 30]
  @commands << [:fg, "images/fg_girl.png"]
  @commands << [:wait, 30]
  @commands << [:text, "Chào buổi tối..."]
end

Nếu chúng ta cứ thêm những dòng commands vào lệnh [init] thì lệnh [init] sẽ ngày càng dài ra. Chúng ta tạo lệnh thêm vào những commands mới vào [@commands]. Đó chính là lệnh được ghi nhớ bằng [scenario_A] và [scenario_B].

@commands = []
scenario_A
scenario_B

Từ trong [scenario_B] thì chúng ta dùng những command mới ở đầu. Đó chính là [:clear], [:clear] chính là để xóa đi những gì đã hirn thị từ trước đó. Để định nghĩa command_clear thì chúng ta làm như sau

def command_clear
  @bg = nil
  @fg = nil
  @texts = []
end

Chắc là tại đây không cần thiết phải giải thích chi tiết. Chúng ta cũng cần thêm vào câu [case] của [run_command] là [command_clear] để có thể gọi ra.

when :clear
  command_clear

[command_clear] không cần argument nên chúng ta cũng nên chú ý không cần thêm gì vào phần options. Nội dung chương trình cho đến phần này được viết dưới đây.

visualnovel03.rb

require 'mygame/boot'

def scenario_A
  @commands << [:bg, "images/bg.jpg"]
  @commands << [:wait, 30]
  @commands << [:fg, "images,fg_girl.png"]
  @commands << [:text, "Xin chào!"]
  @commands << [:wait, 30]
  @commands << [:text, "."]
  @commands << [:wait, 30]
  @commands << [:text, "."]
  @commands << [:wait, 30]
  @commands << [:text, "."]
  @commands << [:text, "Tôi đang đợi đó"]
  @commanđs << [:wait]
  @commands << [:text, "Vâng"]
  @commands << [:text, "Thời gian chờ kết thúc"]
  @commands << [:wait, 30]
end

def scenario_B
  @commands << [:clear]  #xóa hết những commands đã hiển thị
  @commands << [:wait, 30]
  @commands << [:fg, "images/fg_girl.png"]
  @commands << [:wait, 30]
  @commands << [:text, "Chào buổi tối..."]
end

class VisualNovelScene < Scene::Base
  FONT_SIZE   = 24
  TEXT_MARGIN = 16
  LINE_HEIGHT = 32
  def init
    Font.default_size = FONT_SIZE
    @wait_counter = 0
    @pause = false
    @bg = nil
    @fg = nil
    @texts = []
    @commands = []
    scenario_A
    scenario_B
  end

  def run_command
    if params = @commands.shift
      command = params.shift
      case command
      when :clear
        command_clear
      when :wait
        command_wait params[0]
      when :bg
        command_bg params[0]
      when :fg
        command_fg params[0]
      when :text
        command_text params[0]
      end
    end
  end

  def command_clear
    @bg = nil
    @fg = nil
    @texts = []
  end

  def command_wait(n = nil)
    @wait_counter = n.to_i
    @pause = !n
  end

  def command_bg(fname)
    @bg = Image.new(fname)
  end

  def command_fg(fname)
    @fg = TransparentImage.new(fname)
  end

  def command_text(text = "")
    font = ShadowFont.new(text)
    @texts << font
  end

  def update
    if @wait_counter > 0
      @wait_counter -= 1
    else
      run_command unless @pause
    end
    if @pause
      if new_key_pressed?(Key::SPACE)
        @wait_counter = 0
        @pause = false
      end
    end
  end

  def render
    @bg.render if @bg
    @fg.render if @fg
    @texts.each_with_index do |e, i|
      e.x = TEXT_MARGIN
      e.y = TEXT_MARGIN + LINE_HEIGHT * i
      e.render
    end
  end
end

Scene.main_loop VisualNovelScene

Nhảy đến cuộc hội thoại khác

Cho đến đây nếu chúng ta thực hiện chương trình thì ngay sau khi [scenario_A] được thực hiện thì [scenario_B] cũng được thực hiện. Nếu chúng ta làm thêm [scenario_C] và [scenario_D] thì chúng ta dần dần có thể thêm được những cảnh mới.

Tuy nhiên, nếu là phương pháp này thì chỉ có thể thực hiện theo thứ tự đã lưu. Ví dụ [scenario_A -> scenario_B -> scenario_A -> ...] và nó sẽ trở thành một dãy vô tận không di chuyển đi đâu được. Tại đây nếu chúng ta tạo được lệnh nhày để có thể đi tới bất cử đâu mình thích , có thể di chuyển đến cuộc hội thoại nào cũng được.

def scenario_A
  (lược)
  @commands << [:jump, :B]
end

def scenario_B
  (lược)
  @commands << [:jump, :A]
end

[:jump] là command để di chuyển đến một cuộc hội thoại khác. Về khu vực option thì chúng ta sẽ đưa nơi muốn di chuyển đến.

Jump Command chính là điểm quan trọng nhất trong chương trình Visual Novel này. Nếu chúng ta có thể làm được điều này thì có nghĩa chúng ta đã hoàn thành xong cơ bản để có thể tự điều chỉnh được dòng chảy của câu chuyện.

Lệnh để thực hiện jump command là nội dung dưới đây.

def command_jump(name)
  @command = []
  send "scenario_#{name}"
end

Đầu tiên, khi lệnh này được gọi ra thì dãy command sẽ trở nên trống. Kể cả những command khác có đang được lưu nhưng khi [jump command] được thực hiện thì ngay tại điểm đó sẽ nhảy tới cuộc hội thoại mới, cuộc hội thoại cũ sẽ là không cần nữa. Tiếp theo ta gọi đến lệnh [send] [send] là lệnh mà Ruby trang bị ngay từ đầu, sẽ thực hiện lệnh có tên được ghi trong phần argument. (Về chi tiết thì ngay phần sau đây sẽ có chi tiết về lệnh này). Argument của [send] được ghi như sau

"scenario_#{name}"

Tại [name] chính là argument trao cho option của [:jump] Option của [:jump] chính là tên của cuộc hội thoại, ở đây chúng ta trao cho những giá trị như [:A] [:B] Trong trường hợp argument được trao là [:B], thì tùy vào cách thức triển khai thì argument của [send] sẽ là dãy chữ sau

"scenario_B"

Cái này được trao cho [send] và lệnh [scenario_B] được chạy. Nếu lệnh [scenario_B] được chạy thì những command trong cuộc hội thoại B sẽ được lưu trên [@commands]. Tức là, nếu [scenario_B] được gọi ra thì cuộc hội thoại B sẽ được bắt đầu. Như vậy ta cũng phải thêm vào [case] của [run_command] để có thể gọi và thực hiện lệnh

when :jump
  comman_jump params [0]

Như vậy chúng ta đã hoàn thành lệnh [:jump]. Trong lệnh [init] thì chúng ta phải sửa như sau.

def init
  Font.default_size = FONT_SIZE
  @wait_counter = 0
  @pause = false
  command_clear
  command_jump(:start)
end

Đầu tiên, nhất định chúng ta chú ý để thực hiện [command_jump(:start)]. Dựa vào đây chúng ta có thể thực hiện lệnh [scenario_start] đầu tiên. Kết hợ với điều đó, chúng ta để tên lệnh đọc vào cuộc hội thoại đọc vào đầu tiên là [scenario_start] luôn.

Code của chương trình cho đến đoạn này là

visualnovel04.rb

require 'mygame/boot'

def scenario_start
  @commands << [:jump, :A]
end

def scenario_A
  @commands << [:bg, "images/bg.jpg"]
  @commands << [:wait, 30]
  @commands << [:fg, "images,fg_girl.png"]
  @commands << [:text, "Xin chào!"]
  @commands << [:wait, 30]
  @commands << [:text, "."]
  @commands << [:wait, 30]
  @commands << [:text, "."]
  @commands << [:wait, 30]
  @commands << [:text, "."]
  @commands << [:text, "Tôi đang đợi đó"]
  @commanđs << [:wait]
  @commands << [:text, "Vâng"]
  @commands << [:text, "Thời gian chờ kết thúc"]
  @commands << [:wait, 30]
end

def scenario_B
  @commands << [:clear]  #xóa hết những commands đã hiển thị
  @commands << [:wait, 30]
  @commands << [:fg, "images/fg_girl.png"]
  @commands << [:wait, 30]
  @commands << [:text, "Chào buổi tối..."]
end

class VisualNovelScene < Scene::Base
  FONT_SIZE   = 24
  TEXT_MARGIN = 16
  LINE_HEIGHT = 32
  def init
    Font.default_size = FONT_SIZE
    @wait_counter = 0
    @pause = false
    command_clear
    command_jump(:start)
  end

  def run_command
    if params = @commands.shift
      command = params.shift
      case command
      when :clear
        command_clear
      when :jump
        command_jump params[0]
      when :wait
        command_wait params[0]
      when :bg
        command_bg params[0]
      when :fg
        command_fg params[0]
      when :text
        command_text params[0]
      else
        raise
      end
    end
  end

  def command_clear
    @bg = nil
    @fg = nil
    @texts = []
  end

  def command_jump(name)
    @commands = []
    send "scenario_#{name}"
  end

  def command_wait(n = nil)
    @wait_counter = n.to_i
    @pause = !n
  end

  def command_bg(fname)
    @bg = Image.new(fname)
  end

  def command_fg(fname)
    @fg = TransparentImage.new(fname)
  end

  def command_text(text = "")
    font = ShadowFont.new(text)
    @texts << font
  end

  def update
    if @wait_counter > 0
      @wait_counter -= 1
    else
      run_command unless @pause
    end
    if @pause
      if new_key_pressed?(Key::SPACE)
        @wait_counter = 0
        @pause = false
      end
    end
  end

  def render
    @bg.render if @bg
    @fg.render if @fg
    @texts.each_with_index do |e, i|
      e.x = TEXT_MARGIN
      e.y = TEXT_MARGIN + LINE_HEIGHT * i
      e.render
    end
  end
end

Scene.main_loop VisualNovelScene

send

Lệnh [send] gọi ra lệnh có tên là dãy chữ đứng làm argument đầu tiên của lệnh. Lênh cũng có thể chỉ định bằng kí hiệu.

send "puts"	# gọi lệnh puts
send :puts	# gọi lệnh puts

Argument thứ 2 trao cho send chính là argument dành cho lệnh mà send gọi ra.

send :puts, "Hello"	#cũng giống như viết puts "Hello"

Để send gọi command method

Định nghĩa về [run_command] thì chúng ta đã viết như sau

def run_command
    if params = @commands.shift
      command = params.shift
      case command
      when :clear
        command_clear
      when :jump
        command_jump params[0]
      when :wait
        command_wait params[0]
      when :bg
        command_bg params[0]
      when :fg
        command_fg params[0]
      when :text
        command_text params[0]
      else
        raise
      end
    end

Lệnh [run_command] này nếu sử dụng [send] thì có thể viết viết lại như sau.

def run_command
  if params = @commands.shift
    command = params.shift
    send "command_#{command}", *params
  end
end

Chúng da đã có một câu [case] rất dài nhưng bây giờ chỉ còn lại trong 1 dòng. Chúng ta xem kĩ hơn một chút về câu lệnh này.

send "command_#{command}", *params

Argument thứ 1 đó chusnh là tên lệnh command muốn chạy. Đây cũng giống như lúc [command_jump]. Argument thứ 2 là [params], vậy [] chính là cái gì vậy?

Nếu argument cuối trao cho một lệnh mà có thêm [*] thì có nghĩa là sẽ triển khai nội dung trong dãy và để cho nó trở thành argument. Ở đây thì ngoài send chúng ta cũng có thể dùng trong nhiều lệnh khác nhau.

a = [0,1,2]
p *a

[Kết quả hiển thị]

0
1
2

Cũng như chúng ta viết như sau

p 0, 1, 2

Theo như cách kết hợp này thì argument thứ 2 trao cho send chính là trao cho nội dung khi triển khai triển khai [params].

Fade-in hình ảnh

Chúng ta có thể hiển thị fade-in hình nền background và hình nổi foreground như dưới đây.

def command_bg(fname)
  @bg = Image.new(fname)
  @bg.alpha = 0
end

def command_fg(fname)
  @fg = TransparentImage.new(fname)
  @fg.alpha = 0
end

Khi hình ảnh được hình thành thì ta sẽ nhập thông tin ban đầu cho hình ảnh có giá trị alpha = 0. Tiếp theo ở phần [update] thì chúng ta thêm tiếp dòng code sau

@bg.alpha += 8 if @bg and @bg.alpha < 256
@fg.alpha += 8 if @fg and @fg.alpha < 256

Cho đến khi [@bg.alpha] và [@fg.alpha] tiến đến giá trị 256 thì bằng lệnh [update], giá trị alpha sẽ được cộng liên tục một giá trị.

Hiển thị con chuột

Vào lệnh [init], chúng ta tạo hình ảnh con chuột và thay nó bằng biến [@cursor]

@cursor = TransparentImage.new("images/carsor.png")

Rồi thêm vào lệnh [update] dòng code như dưới đây.

@cursor.x = TEXT_MARGIN
@cursor.y = TEXT_MARGIN + LINE_HEIGHT * @text.size

Chúng ta thiết kế tọa độ của hình ảnh con trỏ chuột. Tọa độ x của con trỏ chuột hiển thị kết hợp với tọa độ hiển thị cảu text. Tọa độ y của con trỏ chuột sẽ hiển thị ngay dưới dòng text. Trong lệnh [render] chúng ta thêm dòng code dưới đây để vẽ hình ảnh con trỏ chuột trên màn hình.

@cursor.render if @pause

Hiển thị con trỏ chuột là khi game trong trạng thái pause và đợi sự nhập từ bàn phím từ người chơi. Chỉ khi [@pause] ở trạng thái pause thì sẽ được hiển thị. Hơn nữa chúng ta chúng ta xử lý công phu hơn nữa như sau

@cursor.render if @pause and frame_counter / 12%3 != 0

[frame_counter] là lệnh được định nghĩa trong Scene::Base, từ khi hình ảnh này được khởi động thì số hình ảnh hiện lên trong một giây được đếm. Làm như vậy thì chúng ta có thể tạo thời gian mà hình ảnh con trỏ không được hiển thị. Tức là chúng ta có thể nhìn thấy hình ảnh con trỏ biến mất rồi hiện lên nhấp nháy.

Cử chỉ của chuột để giải phóng trạng thái chờ

Chúng ta sẽ thêm vào [VisualNovelScene] dòng dưới đây.

def restart
  init_events
  @wait_counter = 0
  @pause = false
end

rồi thêm một dòng nữa vào [command_wait]

add_event(:mouse_button_down){restart}

Trong khi wait_command được chạy thì event này được lưu. Event này là lệnh khi nút trên con trỏ chuột được nhấn thì sẽ thực hiện [restart]. Để gọi [init_events] thì chúng ta phải khởi tạo sự kiện, cho đến khi khởi tạo thì sự kiên được lưu là không sử dụng được.

Như vậy, trong khi pause mà chuột được nhấn thì trạng thái chờ và trạng thái tạm dừng pause sẽ kết thúc. Cho đến đây chúng ta đã để phím cách để kết thúc trạng thái chờ và dừng nhưng thao tác bằng chuột sẽ tiện lợi hơn nwn chúng ta sẽ xóa xử lý này đi. Lệnh [restart] màu chúng ta sẽ để nó sau lệnh [command_clear]. Hơn nữa, chúng ta để khi [@wait_counter] trong lệnh [update] trở vể 0, có nghĩa là trạng thái chờ kết thúc thì lệnh lệnh gọi ra [restart] cũng clear. Chương trình cho đến đây sẽ được viết như sau.

visualnovel05.rb

require 'mygame/boot'

def scenario_start
  @commands << [:jump, :A]
end

def scenario_A
  @commands << [:bg, "images/bg.jpg"]
  @commands << [:wait, 30]
  @commands << [:fg, "images,fg_girl.png"]
  @commands << [:text, "Xin chào!"]
  @commands << [:wait, 30]
  @commands << [:text, "."]
  @commands << [:wait, 30]
  @commands << [:text, "."]
  @commands << [:wait, 30]
  @commands << [:text, "."]
  @commands << [:text, "Tôi đang đợi đó"]
  @commanđs << [:wait]
  @commands << [:text, "Vâng"]
  @commands << [:text, "Thời gian chờ kết thúc"]
  @commands << [:wait, 30]
end

def scenario_B
  @commands << [:clear]  #xóa hết những commands đã hiển thị
  @commands << [:wait, 30]
  @commands << [:fg, "images/fg_girl.png"]
  @commands << [:wait, 30]
  @commands << [:text, "Chào buổi tối..."]
end

class VisualNovelScene < Scene::Base
  FONT_SIZE   = 24
  TEXT_MARGIN = 16
  LINE_HEIGHT = 32
  def init
    Font.default_size = FONT_SIZE
    @cursor = TransparentImage.new("images/carsor.png")
    command_clear
    command_jump(:start)
  end

  def restart
    init_events
    @wait_counter = 0
    @pause = false
  end

  def run_command
    if params = @commands.shift
      command = params.shift
      send "command_#{command}", *params
    end
  end

  def command_clear
    restart
    @bg = nil
    @fg = nil
    @texts = []
  end

  def command_jump(name)
    @commands = []
    send "scenario_#{name}"
  end

  def command_wait(n = nil)
    @wait_counter = n.to_i
    @pause = !n
    add_event(:mouse_button_down) { restart }
  end

  def command_bg(fname)
    @bg = Image.new(fname)
    @bg.alpha = 0
  end

  def command_fg(fname)
    @fg = TransparentImage.new(fname)
    @fg.alpha = 0
  end

  def command_text(text = "")
    font = ShadowFont.new(text)
    @texts << font
  end

  def update
    if @wait_counter > 0
      @wait_counter -= 1
      restart if @wait_counter == 0
    else
      run_command unless @pause
    end
    @bg.alpha += 8 if @bg and @bg.alpha < 256
    @fg.alpha += 8 if @fg and @fg.alpha < 256
    @cursor.x = TEXT_MARGIN
    @cursor.y = TEXT_MARGIN + LINE_HEIGHT * @texts.size
  end

  def render
    @bg.render if @bg
    @fg.render if @fg
    @cursor.render if @pause and frame_counter / 12 % 3 != 0
    @texts.each_with_index do |e, i|
      e.x = TEXT_MARGIN
      e.y = TEXT_MARGIN + LINE_HEIGHT * i
      e.render
    end
  end
end

Scene.main_loop VisualNovelScene

Những lựa chọn

Dần dần thì chương trình đang gần giống với visual novel thật sự rồi. Tuy nhiên, còn một chức năng cơ bản quan trọng nữa vẫn chưa có. Đó chính là những lực chọn, bằng lựa chọn thì chúng ta nhảy sang những chuỗi hội thoại khác. Nếu chúng ta thêm xong được chức năng lựa chọn thì chúng ta sẽ gần đến hoàn thành chương trình.

Để hiển thị lựa chọn bằng lệnh thì chúng ta thêm [:choice] vào. Hãy nhìn dữ liệu hội thoại dưới đây.

def scenario_start
  @commands << [:text, "Hội thoại bắt đầu."]
  @commands << [:text, "Hãy lựa chọn phương án."]
  @commands << [:choice, :A, "Phương án A"]
  @commands << [:choice, :B, "Phương án B"]
end

def scenario_A
  @commands << [:text, "Đây là hội thoại A"]
  @commands << [:wait]
  @commands << [:jump, :start]
end

def scenario_B
  @commands << [:text, "Đây là hội thoại B"]
  @commands << [:wait]
  @commands << [:jump, :start]
end

Command hiển thị [:choice] mang 2 option. Đầu tiên là khi lựa chọn xong đối tượng thì sẽ có tên cuộc hội thoại mà :jump sẽ nhảy tới. Thứ 2 là phần [text] hiển thị khi chọn lựa chọn đó.

Để định nghĩa [command_choice] thì chúng ta định nghĩa như sau

  def command_choice(name, text)
    font = ShadowFont.new(text)
    font.color = [255,255,128]
    @choices << font
    @cursor_index = 0
    idx = @choices.size - 1
      add_event(:mouse_motion) {|e| @cursor_idx = idx if font.hit?(e)}
      add_event(:mouse_button_down) {|e| command_jummp(name) if font.hit?(e)}
  end

Đầu tiên, để hiển thị được các phương án lựa chọn thì hcunsg ta phải tạo một text object. Màu của font phương án lựa chọn thì để phân biệt với text bình thường thì ta để màu vàng ([255,255,128]). Sau đó font object này ta thêm vào dãy [@choices].

Hãy nhìn dòng cuối cùng.

add_event(:mouse_button_down) {|e| command_jummp(name) if font.hit?(e)}

Với lệnh này thì khi nút của chuột được nhấn xuống thì sự kiện phát sinh sẽ được ghi lại. Khi nứt chuột được nhấn thì nếu tọa độ cnon trỏ chuột ở trên phần font text, có nghĩa nếu phần font text được nhấn thì lệnh [command_jump(name)] sẽ được thực hiện. [name] là tên của đoạn hội thoại sẽ nhảy đến. Từ đó nếu phương án lựa chọn được click thì options sẽ nhảy đến đoạn hội thoại đã được chỉ định.

[@cursor_idx] chính là biến số để lấy chỉ số hiệu, chỉ số của phương án lựa chọn đã được lựa chọn. Bằng bộ phận tiếp theo đây thì sẽ xảy ra sự kiện viết lại [@cursor_idx]

idx = @choice.size - 1
add_event(:mouse_motion){|e| @cursor_idx = idx if font.hit?(e)}

[@choices.size] chính là số lượng phương án lựa chọn lúc đó đang tồn tại. Đầu tiên, khi [command_choice] được gọi ra thì ở [@choices] vẫn chưa có một object nào được thêm vào nên [@choice.size] sẽ thành 1. Có nghĩa [idx] bằng 0.

[:mouse_motion] là sự kiện phát sinh khi di chuyển con trỏ chuột. Khi cử động con chuột thì nếu con chuột đi đến trên vùng font các phương án lựa chọn thì ở [@cursor_ind] sẽ có [idx] được thay vào. Tức là những chuyện sau sẽ xảy ra.


  • Nếu con trỏ chuột đi đến phần trên của phương án dầu tiên thì @cursor_ind được điền 0
  • Nếu con trỏ chuột đi đến phần trên của phương án thứ hai thì @cursor_ind được điền 1
  • Nếu con trỏ chuột đi đến phần trên của phương án thú ba thì @cursor_ind được điền 2

Vị trí con trỏ chuột được hiển thị ở phần [update] thì chúng ta cũng kết hợp với hiển thị lựa chọn như sau.

@cursor.y = TEXT_MARGIN + LINE_HEIGHT* (@text.size + @cursor_idx.to_i)

Vì phương án lựa chọn sẽ hiển thị cuối của dòng, nên nếu làm như thế này thì tại vị trí của phương án lựa chọn trong khi lựa chọn thì con trỏ chuột sẽ được hiển thị.

Rồi lệnh [render] chúng ta cũng thay đổi như sau.

@cursor.render if (@pause or @cursor_idx) and frame_counter / 12%3 != 0
(@text + @choice).each_with_index do |e,i|
  dx = 0
  dx = FONT_SIZE if @choice.include?(e)
  e.x = TEXT_MARGIN + dx
  e.y = TEXT_MARGIN + LINE_HEIGHT * 1
  e.render
end
end

Tại điểu kiện chạy [@cursor.render] thì chúng ta thêm vào[@cursor.idx] là đúng . Mặt khác, dãy [@text] được cất trong font object liên kết với [@choices] và sẽ hiên thị ở hai.

Biến số [dx] có trong loop là sẽ điền kích cỡ của 1 chữ trong trường hợp object chính là những phương án lựa chọn. Đây là để vẽ lên để bỏ qua 1 chữ phía bên phải của object những phương án lựa chọn. Tại đây, chúng ta dùng [@choice.include?(e)] đẻ phán đoán, phân biệt những phương án lựa chọn bằng biến số [e]được điển vào font object. [include?] là lệnh được định nghĩa trong class dãy, nếu ở argument có giá trị điền cào thì kết quả '"true" sẽ được trả về.

Như vậy command [:choice] đã có thể dùng được. Chương trình cho đến hiện tạo là.

require 'mygame/boot'

def scenario_start
  @commands << [:text, "Hội thoại bắt đầu."]
  @commands << [:text, "Hãy lựa chọn phương án."]
  @commands << [:choice, :A, "Phương án A"]
  @commands << [:choice, :B, "Phương án B"]
end

def scenario_A
  @commands << [:text, "Đây là hội thoại A"]
  @commands << [:wait]
  @commands << [:jump, :start]
end

def scenario_B
  @commands << [:text, "Đây là hội thoại B"]
  @commands << [:wait]
  @commands << [:jump, :start]
end

class VisualNovelScene < Scene::Base
  FONT_SIZE   = 24
  TEXT_MARGIN = 16
  LINE_HEIGHT = 32
  def init
    Font.default_size = FONT_SIZE
    @cursor = TransparentImage.new("images/carsor.png")
    command_clear
    command_jump(:start)
  end

  def restart
    init_events
    @wait_counter = 0
    @pause = false
  end

  def run_command
    if params = @commands.shift
      command = params.shift
      send "command_#{command}", *params
    end
  end

  def command_clear
    restart
    @bg = nil
    @fg = nil
    @texts = []
    @choices = []
    @cursor_idx = nil
  end

  def command_jump(name)
    command_clear
    @commands = []
    send "scenario_#{name}"
  end

  def command_wait(n = nil)
    @wait_counter = n.to_i
    @pause = !n
    add_event(:mouse_button_down) { restart }
  end

  def command_bg(fname)
    @bg = Image.new(fname)
    @bg.alpha = 0
  end

  def command_fg(fname)
    @fg = TransparentImage.new(fname)
    @fg.alpha = 0
  end

  def command_text(text = "")
    font = ShadowFont.new(text)
    @texts << font
  end

  def command_choice(name, text)
    font = ShadowFont.new(text)
    font.color = [255, 255, 128]
    @choices << font
    @cursor_idx = 0
    idx = @choices.size - 1
    add_event(:mouse_motion) {|e| @cursor_idx = idx if font.hit?(e) }
    add_event(:mouse_button_down) {|e| command_jump(name) if font.hit?(e) }
  end

  def update
    if @wait_counter > 0
      @wait_counter -= 1
      restart if @wait_counter == 0
    else
      run_command unless @pause
    end
    @bg.alpha += 8 if @bg and @bg.alpha < 256
    @fg.alpha += 8 if @fg and @fg.alpha < 256
    @cursor.x = TEXT_MARGIN
    @cursor.y = TEXT_MARGIN + LINE_HEIGHT * (@texts.size + @cursor_idx.to_i)
  end

  def render
    @bg.render if @bg
    @fg.render if @fg
    @cursor.render if (@pause or @cursor_idx) and frame_counter / 12 % 3 != 0
    (@texts + @choices).each_with_index do |e, i|
      dx = 0
      dx = FONT_SIZE if @choices.include?(e)
      e.x = TEXT_MARGIN + dx
      e.y = TEXT_MARGIN + LINE_HEIGHT * i
      e.render
    end
  end
end

Scene.main_loop VisualNovelScene

Voice command và exit command

Chúng ta đã hoàn thành đến 90% chương trình game khi hoàn thành xong chức năng lựa chọn. Tại đây thì chúng ta hoàn thành luôn chương trình thôi. Đó chính là thêm [:voice] và [:exit].

def command_voice(fname)
  Wave.play fname
end

def command_exit
  self.next_scene = Scene::Exit
end

[:voice] dùng để chạy file âm thanh của được gọi ra trong option. [:exit] là command dùng để kết thúc chương trình.

Tạo lệnh lưu command

Cho đến nay, việc nhập command đều là chúng ta nhập trực tiếp [@commands] vào loop.

@commands << [:text, "Cuộc hội thoại bắt đầu."]

Tại [VisualNovelScene], chúng ta chuẩn bị lệnh dưới đây để có thể lưu command để gọi các lệnh.

def add_command(*args)
  @commands << args
end

Trong phần cài đặt hội thoại dùng [add_command] sẽ trở thành như sau.

def scenario_start
  add_command :clear
  add_command :text, "Hãy lựa chọn phương án."
  add_command :choice, :A, "Phương án A"
  add_command :choice, :B, "Phương án B"
end

Các bạn có nhìn thấy tự nhiên mọi thứ trông đã gọn gàng hơn chưa? Hãy nhìn vào định nghĩa argument cho lệnh [add_command]

def add_command(*args)

Trước argument[args] có dấu [*] đúng không? Có ý nghĩa argument sẽ được lưu trữ trong [args] dưới dạng một dãy. Ví dụ, nếu chúng ta gọi lệnh [add_command] như dưới đây.

add_command :choice, :B, "Phương án B"

Ngay sau đó thì tại phần argument[args] của [add_command] sẽ lưu dãy sau như một giá trị trong dãy.

[:choice, :B, "Phương án B"]

Dãy này chính là dữ liệu command.

Cường hóa command clear

Command [clear] hiện tại sẽ xóa tất cả những gì hiển thị trên màn hình. Tuy nhiên, nếu chúng ta có thể xóa riêng biệt từng bộ phận trên màn hình thì sẽ tiện lợi hơn. Như vậy, chúng ta nên thêm vào [clear] như sau.

[:clear]	#xóa tất cả hiển thị trên màn hình
[:clear, :bg]	#xóa background trên màn hình
[:clear, :fg, :text]	#xóa foreground và text trên màn hình

Như vậy chúng ta có thể chỉ định được phần cần xóa. Hãy nhìn phần định nghĩa mới của [command_clear]

def command_clear(*args)
  restart
  args = args.to_a
  @bg = nil if args.empty? or args.include?(:bg)
  @fg = nil if args.empty? or args.include?(:fg)
  @texts = [] if args.empty? or args.include?(:texts)
  @choices = [] if args.empty? or args.include?(:choices)
  @cursor_idx = nil
end

Vì số lượng argument sẽ thay đổi nên tại đây nếu chúng ta thêm[*] thi có thể nhận được argument

Dòng tiếp theo là dòng xử lý biến đổi dãy [args]

args = args.to_a

Lệnh [to_a] là lệnh biến đổi object thành dãy, nên nếu vốn [args] là dãy thì sẽ không có gì xảy ra cả, và kết quả trả về vẫn là dãy. Lý do để chuyển [args] thành dãy là vì sau đó [args] được sử dụng như dãy.

Dòng tiếp theo là xử lý thay [@bg] bằng [nil]

@bg = nil if args.empty? or args.include?(:bg)

Cho đến nay thì chúng ta vô điều kiện để [@bg] là [nil] nhưng đây đã thêm vào sử dụng điều kiện câu [if] [args.empty?] sẽ điều tra trong dãy [args] có rỗng hay không. Nếu không có option thì [args.empty?] sẽ trả về kết quả [true]. [args.include?(:bg)] là nếu trong dãy [args] có [:bg] thì sẽ trả về kết quả [True], [@bg] sẽ làm trở thành [nil] Đối với những hiển thị khác trên màn hình thì chúng ta cũng để xử lý tương tự.

Hoàn thành

Đến đây, chúng ta đã hoàn thành [Visual Novel]. Chương trình hoàn thành sẽ là

visualnovel.rb

require 'mygame/boot'
require_relative 'scenario.rb'

class VisualNovelScene < Scene::Base
  FONT_SIZE   = 24
  TEXT_MARGIN = 16
  LINE_HEIGHT = 32
  def init
    Font.default_size = FONT_SIZE
    @cursor = TransparentImage.new("images/carsor.png")
    command_clear
    command_jump(:start)
  end

  def restart
    init_events
    @wait_counter = 0
    @pause = false
  end

  def add_command(*args)
    @commands << args
  end

  def run_command
    if params = @commands.shift
      command = params.shift
      send "command_#{command}", *params
    end
  end

  def command_clear(*args)
    restart
    @bg = nil     if args.empty? or args.include?(:bg)
    @fg = nil     if args.empty? or args.include?(:fg)
    @texts = []   if args.empty? or args.include?(:texts)
    @choices = [] if args.empty? or args.include?(:choices)
    @cursor_idx = nil
  end

  def command_jump(name)
    @commands = []
    command_clear(:texts, :choices) unless @choices.empty?
    send "scenario_#{name}"
  end

  def command_wait(n = nil)
    return if key_pressed?(Key::SPACE) # DEBUG
    @wait_counter = n.to_i
    @pause = !n
    add_event(:mouse_button_down) { restart }
  end

  def command_bg(fname)
    @bg = Image.new(fname)
    @bg.alpha = 0
  end

  def command_fg(fname)
    @fg = TransparentImage.new(fname)
    @fg.alpha = 0
  end

  def command_text(text = "")
    font = ShadowFont.new(text)
    @texts << font
  end

  def command_choice(name, text)
    font = ShadowFont.new(text)
    font.color = [255, 255, 128]
    @choices << font
    @cursor_idx = 0
    idx = @choices.size - 1
    add_event(:mouse_motion) {|e| @cursor_idx = idx if font.hit?(e) }
    add_event(:mouse_button_down) {|e| command_jump(name) if font.hit?(e) }
  end

  def command_voice(fname)
    Wave.play fname
  end

  def command_exit
    self.next_scene = Scene::Exit
  end

  def update
    if @wait_counter > 0
      @wait_counter -= 1
      restart if @wait_counter == 0
    else
      run_command unless @pause
    end
    @bg.alpha += 8 if @bg and @bg.alpha < 256
    @fg.alpha += 8 if @fg and @fg.alpha < 256
    @cursor.x = TEXT_MARGIN
    @cursor.y = TEXT_MARGIN + LINE_HEIGHT * (@texts.size + @cursor_idx.to_i)
  end

  def render
    @bg.render if @bg
    @fg.render if @fg
    @cursor.render if (@pause or @cursor_idx) and frame_counter / 12 % 3 != 0
    (@texts + @choices).each_with_index do |e, i|
      dx = 0
      dx = FONT_SIZE if @choices.include?(e)
      e.x = TEXT_MARGIN + dx
      e.y = TEXT_MARGIN + LINE_HEIGHT * i
      e.render
    end
  end
end

Scene.main_loop VisualNovelScene

Phần định nghĩa hội thoại [scenario.rb] được thêm trên đầu để load.

require 'mygame/boot'
require_relative 'scenario.rb'

class VisualNovelScene < Scene::Base

Chương trình tiểu thuyết giả tưởng trong này được viết là một chương trình rất nhỏ, nhưng nó đã có tất cả những chức năng cơ bản nhất của một chương trình tiểu thuyết trực quan. Chúng ta có thể viết chương trình một cách gọn như thế này là do tính mềm dẻo của Ruby. Nhất là lệnh [send] gọi lệnh để xử lý command nên thực sự rất tiện lợi.

Hình 5-35 Tiểu thuyết trực quan hoàn thành

18.PNG

18.PNG


All Rights Reserved