5-3 Làm Một Tiểu Thuyết Trực Quan - Viblo
Có thể bạn quan tâm
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 VisualNovelSceneChú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 ẹndTê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 << fontNế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 endTrướ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 paramsVì 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.shiftThờ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 paramsSau đó 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 endChươ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 VisualNovelSceneThê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 endTạ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 = !nDò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 endTiế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 = falseRồ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 endTrong 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 endMọ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 @pauseVậ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 VisualNovelSceneThê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..."] endNế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_BTừ 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 = [] endChắ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 VisualNovelSceneNhả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 VisualNovelScenesend
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 putsArgument 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 endLệ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 endChú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}", *paramsArgument 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 2Cũng như chúng ta viết như sau
p 0, 1, 2Theo 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 endKhi 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 < 256Cho đế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.sizeChú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 @pauseHiể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 endrồ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 VisualNovelSceneNhữ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] endCommand 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 endTạ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 VisualNovelSceneVoice 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 endTrong 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" endCá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ìnhNhư 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 endVì 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_aLệ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 VisualNovelScenePhầ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::BaseChươ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
Từ khóa » Trò Chơi Ra Lệnh
-
Cách Chơi Trò Sai Khiến Trên App Drunky Finger
-
Cách Chơi Trò Chơi Sai Khiến, Cách Chơi Drunky Finger
-
Chơi Trò Tuân Lệnh (*) - Tuổi Trẻ Online
-
30 Trò Chơi Tập Thể Trong Nhà Vui Và Bựa
-
Các Trò Chơi Sinh Hoạt Tập Thể | Sinh Viên
-
40+ Trò Chơi Tập Thể Hấp Dẫn Và Vui Nhộn - VietPower
-
Cách Chơi Trò Chơi Sai Khiến, Cách Chơi Drunky Finger - Vozz
-
Điểm Danh Các Trò Chơi Team Building Dễ Tổ Chức Và Vui Nhộn Nhất
-
TOP 50+ Trò Chơi Team Building [SIÊU BỰA] Theo Chủ Đề
-
[Che Tên] Bộ Bài Hừng Hực 57 Lá Gồm 52 Lá Tư Thế Mệnh Lệnh ...
-
2-7 Sử Dụng Cơ Bản Lập Trình Game - Viblo
-
Hỗ Trợ Steam :: CS:GO - Tạm Nghỉ Tranh đấu Và Lệnh Cấm
-
Cờ Tư Lệnh – Wikipedia Tiếng Việt
-
Cách Lệnh Trong Đế Chế, Mã Lệnh Chơi Age Of Empires - Thủ Thuật