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 NovelSuy 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