11/08/2018, 23:32

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ỉ ...

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
        co            
0