5-3 Làm một tiểu thuyết trực quan
Bài đăng này đã không được cập nhật trong 3 năm
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
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
All rights reserved