5-2 Cuộc đua của Punya (2)
Ở phần này, chúng ta sẽ thêm vào trò chơi trước những tính năng như hình chủ đề, màn hình menu. Hình chủ đề tên game và màn hình menu là những thứ không thể thiếu trong game. Phần này sẽ giới thiệu những khái niệm về màn hình, những thuật di chuyển màn hình và vừa làm màn hình menu Phân biệt ...
Ở phần này, chúng ta sẽ thêm vào trò chơi trước những tính năng như hình chủ đề, màn hình menu. Hình chủ đề tên game và màn hình menu là những thứ không thể thiếu trong game. Phần này sẽ giới thiệu những khái niệm về màn hình, những thuật di chuyển màn hình và vừa làm màn hình menu
Phân biệt các loại màn hình và Scene class
Trong game, chúng ta có thể chia ra nhiều cảnh. Ví dụ như cảnh menu, cảnh trong game. Sắp xếp cấu trúc của chương trình, sắp xếp chương trình theo từng cảnh sẽ làm cho cả chương trình dễ nhìn hơn và cũng dễ quản lý hơn. Trong MyGame chúng ta được chuẩn bị Scene class rất tiện lợi để tạo nên các cảnh là [Scene::Base]. Khi sử dụng [Scene::Base] này chúng ta có thể dễ dàng tạo nên những cảnh phức tạp và có thể quản lý các cảnh một cách rất đơn giản. Dưới đây tôi sẽ giới thiệu phương pháp tạp Scene sử dụng [Scene::Base]
Sử dụng Scene Class
Scene Class sử dụng [Scene::Base] sẽ được viết như dưới đây.
require 'mygame/boot' class tên class<Scene::Base def init #xử lý dữ liệu ban đầu end def quit # kết thúc xử lý end def update # làm mới xử lý end def render # xử lý hình ảnh end end #Chạy Scene Class Scene.main_loop [Tên class]
Đây là chương trình sử dụng scene class. Có 4 lệnh để định nghĩa Scene Class [init][quit][update][render] được viết ra để tạo chương trình.
Phần dưới đây sẽ vừa tạo một số cảnh, vừa giải thích cách sử dụng Scene Class.
Quyết định cấu trúc của các scene
Trước khi chúng ta bắt tay vào lập trình thì hãy cùng nghĩ cấu trúc của các cảnh trong game thôi. Dưới đây là hình vẽ đại khái những hình ảnh sẽ có trong game.
Hình logo -> Hình tên game -> Game menu -> Cuộc đưa của các con punya. Trong màn hình menu thì chúng ta sẽ thiết kế hai nút là "Quay lại" và "Giới thiệu nhân vật". Kể cả một hình đơn giản như thế nào thì khi chúng ta vẽ thử ra giấy, chi tiết hóa hình ảnh thì lúc đó chusg ta mới có thể bắt đầu công việc lập trình.
Màn hình logo
Chúng ta cùng thử tạo màn hình hiển thị hình logo nhé. Để tạo hình, chúng ta dùng [Scene::Base] Vì màn hình này hiển thị logo nên chúng ta đặt tên là [LogoScene].
punyamenu00.rb
require 'mygame/boot' class LogoScene < Scene::Base def init @logo = Image.new("image/logo.png") end def render @logo.render end end Scene.main_loop do LogoScene
Như vậy chúng ta đã vẽ xong màn hình Logo.
Hình 5-11 Vẽ hình logoHãy nhìn lệnh [init]. Đây là lệnh được thực hiện chỉ một lần để viết những xử lsy ban đầu đầu vào của hình ảnh. Tiếp theo hãy nhìn lệnh [render]. Tại đây, những xử lý vẽ hình được viết vào. Như vậy tại đây có nghĩa là hình ảnh được [init] tạo ra sẽ được vẽ lên.
Cuối cùng, chúng ta cũng có dòng này
Scene.main_loop LogoScene
Tại dòng này là xử lý cần thiết để thực hiện Scene Class. Argument của [Scene.main_loop] được trao cho chính là hình ảnh chúng ta đẫ định nghĩa tại Scene Class. Tại đây, Scene Class sẽ được chạy. Trong trường hợp sử dụng Scene Class thì chúng ta không cần thieetsghi vào những lệnh như [main_loop] như từ trước đến nay.
Khi định nghĩa 4 lệnh [init][quit][update][render] của Scene Class thì những lệnh đó được tự động gọi ra. Chúng ta không cần định nghĩa những lệnh không cần thiết. Tại [LogoScene] thì chỉ có lệnh [init] và [render] là được định nghĩa.
Thay đổi cảnh
Tiếp theo chúng ta sẽ thêm [Hình ảnh title] như một hình ảnh mới vào. Để biểu thị [Hình ảnh Title] thì chúng ta định nghĩa [Title Scene].
class TitleScene<Scene::Base end
Hiện tại thì nội dung trong TitleScene là trống rỗng. Trước khi viết chế tạo nội dung của TitleScene thì chúng ta cần phải hiểu cách xử lý để chuyển từ [LogoScene] sang [TitleScene]. Để phát sinh sự chuyển cảnh thì trong chương trình chúng ta cần phải có lệnh dưới đây.
self.next_scene = Tên Scene Class
Dùng [self.next_scene] rồi đưa thêm tên class thì chúng ta sẽ chuyển được sang màn hình khác.
Chúng ta cùng thêm vào xử lý di chuyển từ màn hình "LoGoScene" đến "TitleScene".
class LogoScene < Scene::Base def init @logo = Image.new("Image/logo.png") add_event(:key_down){go_next_scene} add_event(mouse_button_down){so_nexr_scene} end def go_next_scene self. next_scene = TitleScene. end (lược)
Khi chúng ta thêm tính năng mới [go_next_scene] thì công việc chuyển đổi cảnh được diễn ra. Việc gọi [go_next_scene] được lưu như một sự kiện. Một phím nào được nhấn trên bàn phím hay con trỏ chuột được nhấn xuống thì [go_next_scene] sẽ được gọi và màn hình sẽ chuyển sang [TitleScene]. Tại [TitleScene] thì chưa có gì được thực hiện. Khi chyển sang màn hình của tên game thì hình ảnh Logo biến mất và trên màn hình không có gì được vẽ cả.
Hình 5-12 Màn hình logo sau khi click chuột hay nhấn phím trên bàn phím thì màn hình sẽ được chuyểnDưới đây là chương trình để chuyển màn hình.
punya01.rb
require 'mygame/boot' class LogoScene < Scene::Base def init @logo = Image.new("logo.png") add_event(:key_down) { go_next_scene } add_event(:mouse_button_down) { go_next_scene } end def go_next_scene self.next_scene = TitleScene end def render @logo.render end end class TitleScene < Scene::Base end Scene.main_loop LogoScene
Màn hình tên game
Hình 5-13 Màn hình TitleChúng ta cùng làm lên nội dung của màn hình Title bằng chương trình dưới đây.
class TitleScene<Scene::Base def init @bg = Image.new("images/bg_title.png") @press = TransparentImage.new("images/press_any_key.png", :x => 80, :y => 270) add_event(:key_down) { go_next_scene } add_event(:mouse_button_down) { go_next_scene} end def go_next_scene self.next_scene = MenuScene end def render @bg.render @press.render if frame_counter/16%4!=0 end end class MenuScene<Scene::Base end
Lệnh [init] đã tạo hình ảnh hiển thị hình ảnh Title. Hiển ảnh vẽ bằng hình ảnh Title có 2 thứ
- Images/bg_title.png ...... Hình ảnh phông nền cho hình ảnh Title
- images/bg/press_any_button.png ..... Hình ảnh [PRESS ANY BUTTON]
[PRESS ANY BUTTON] được thay thế vào biến số [@press] và là biến số instance. Để xử lsy vẽ nên hình ảnh [@press] thì chúng ta thực hiện như chương trình dưới đây.
@press.render if frame_counter / 16%4 != 0
[frame_counter] là lệnh mà Scene Class mang theo khi hình ảnh đó được trả lại theo giá trị số frame(số lần loop) từ khi khởi động cho đến khi được thực hiện. Có nghĩa là [frame_counter] sẽ trả lại giá trị [0,1,2,...] giá trị đó được cộng vào trong mỗi lần thực hiện loop. Tại đây, [frame_counter / 16%4] khi không phải là [0] thì hình ảnh sẽ hiện ra nên chúng ta có thể nhìn thấy hình ảnh nhấp nháy. Nếu khi nhấn phím nào đó trên bàn phím hay nhấn và con chuột thì màn hình sẽ được chuyển sang màn hình [MenuScene] (hiện đang rỗng). Chương trình cho đến phần này là
punya02.rb
require 'mygame/boot' class LogoScene < Scene::Base def init @logo = Image.new("images/logo.png") add_event(:key_down) { go_next_scene } add_event(:mouse_button_down) { go_next_scene } end def go_next_scene self.next_scene = TitleScene end def render @logo.render end end class TitleScene < Scene::Base def init @bg = Image.new("images/bg_title.png") @press = TransparentImage.new("images/press_any_key.png", :x => 80, :y => 270) add_event(:key_down) { go_next_scene } add_event(:mouse_button_down) { go_next_scene } end def go_next_scene self.next_scene = MenuScene end def render @bg.render @press.render if frame_counter / 16 % 4 != 0 end end class MenuScene < Scene::Base end Scene.main_loop LogoScene
Fade-in và Fade-out
Khi chuyển màn hình mà chúng ta cho xử lý fade-in hoặc fade-out thì việc di chuyển màn hình sẽ trở nên nhẹ nhàng hơn.
Chúng ta thử Fade-in hình ảnh phông nền đằng sau của màn hình Title. Chúng ta thêm vào lệnh [render] ở [TitleScene] những xử lý như sau
def render alpha = frame_counter * 8 alpha = 255 if alpha > 255 @bg.alpha = alpha @bg.render (lược)
[@bg] là object chỉ hình ảnh phông nền đằng sau. Ta cho giá trị [alpha] của hình ảnh thay đổi để thực hiện xử lý fade-in.
@bg.alpha = alpha
[alpha] thì ta sử dụng [frame_counter] để thay giá trị từ 0~255. Đầu tiên giá trị [alpha] là 0 nên màn hình tối đên hoàn toàn. Khi giá trị [alpha] đần đến [255] thì hình ảnh dần hiện lên và chúng ta có thể nhìn thấy rõ hình ảnh được.
Như vậy chúng ta cũng thêm xử lý fade-in với fade-out với hình ảnh Logo ban đầu. Tại [LogoScene] chúng ta thêm xử lý.
class LogoScene < Scene::Base FADE_IN_TIME = 64 (lược) def render alpha = 0 if frame_counter <= FADE_IN_TIME alpha = frame_counter * 255 / FADE_IN_TIME elsif frame_counter <= FADE_IN_TIME * 4 alpha = 255 elsif frame_counter <= FADE_IN_TIME * 5 ct = frame_counter - FADE_IN_TIME * 4 alpha = 255 - ct * 255 / FADE_IN_TIME end @logo.alpha = alpha @logo.render end end
[FADE_IN_TIME] là một định số và được thêm giá trị 64. Tại đây giá trị alpha được thay thế bởi biến [@logo.alpha] sẽ biến đổi như sau.
0->255 .... fade-in, hình ảnh dần được nhìn thấy rõ(trong khoảng frame 64) | 255 ... chờ trong khoảng thời gian nhất định, hình ảnh được hiển thị (trong | khoảng 64*3) 255->0 .... fade-out. Hình ảnh sẽ dần dần biến mất (trong khoảng 64)
Hơn nữa chúng ta cũng có thể cho thêm tính năng nếu hình ảnh Logo biến mất hoàn toàn thì chương trình sẽ tự động chuyển sang hình ảnh tiếp bằng cách định nghĩa lệnh [update] trong [LogoScene] class như sau
def updater go_next_scene if frame_counter >= FADE_IN_TIME *6 end
Tại đây nếu [fram_counter] đến giá trị trên (FADE_IN_TIME)*6 thì lệnh [go_next_scene] được gọi ra về màn hình Title sẽ được thay thế vào. Chương trình cho đến phần này là.
punyamenu03.rb
require 'mygame/boot' class LogoScene < Scene::Base FADE_IN_TIME = 64 def init @logo = Image.new("images/logo.png") add_event(:key_down) { go_next_scene } add_event(:mouse_button_down) { go_next_scene } end def go_next_scene self.next_scene = TitleScene end def update go_next_scene if frame_counter >= FADE_IN_TIME * 6 end def render alpha = 0 if frame_counter <= FADE_IN_TIME alpha = frame_counter * 255 / FADE_IN_TIME elsif frame_counter <= FADE_IN_TIME * 4 alpha = 255 elsif frame_counter <= FADE_IN_TIME * 5 ct = frame_counter - FADE_IN_TIME * 4 alpha = 255 - ct * 255 / FADE_IN_TIME end @logo.alpha = alpha @logo.render end end class TitleScene < Scene::Base def init @bg = Image.new("images/bg_title.png") @press = TransparentImage.new("images/press_any_key.png", :x => 80, :y => 270) add_event(:key_down) { go_next_scene } add_event(:mouse_button_down) { go_next_scene } end def go_next_scene self.next_scene = MenuScene end def render alpha = frame_counter * 8 alpha = 255 if alpha > 255 @bg.alpha = alpha @bg.render @press.render if frame_counter / 16 % 4 != 0 end end class MenuScene < Scene::Base end Scene.main_loop LogoScene
Định số
Định số là biến số mà tên bắt đầu từ chữ Alphabet được viết hoa. Định số khác với những biến số khác đó chính là chúng có thể được dùng trong trường hợp giá trị thay thế không thay đổi. Giá trị sẽ được thay đổi nếu chúng ta nhập lại một lần nữa nhưng lúc đó sẽ có tin nhắn thông báo hiện ra. Định số không được định nghĩa trong lệnh nên chúng ta nên chú ý.
class A HELLO = "hello" def foo puts HELLO end end
Định số được định nghĩa trong class thì chúng ta có thể sử dụng trong class, nhưng nếu ta muốn sử dụng ngoài class thì phải có thêm [tên class::] đằng trước.
puts A::HELLO
Vẽ hình sử dụng Title
Tiếp theo chúng ta sẽ tạo nên màn hình menu. Chúng ta sẽ sử dụng [menu_tile.png] và [menu_title.png] để hiển thị màn hình menu có phông nền đằng sau.
Hình 5-16 [menu_tile.png] và [menu_title.png][menu_tile.png] có kích thước 128*128 pixel. Chúng ta sẽ trải hình ảnh này lên toàn màn hình, và trên đó sẽ là 1 hình ảnh [menu_title.png].
Hình 5-17 Màn hình MenuHơn nữa, chúng ta sẽ làm màn hình đằng sau chuyển động. Làm màn hình đằng sau chuyển động sẽ làm màn hình cũng thay đổi liên tục, dễ bắt mắt hơn. Đây là thủ thuật rất hay được sử dụng trên thị trường game.
Để dịch chuyển được hình ảnh phông đằng sau thì chúng ta phải chạy chương trình sau trong [MenuScene] class.
class MenuScene<Scene::Base def init @bg_tiles = Array.new(30) {Image.new("images/menu_tile.png")} @title = TransparentImage.new("image/menu_title.png", :x => 192, :y => 16) end def update @bg_tiles.each_with_index do |e,i| e.x = e.w * (i%6) - frame_counter % e.w e.y = e.h *(i/6) - frame_counter % e.h end end def render @bg_tiles.each {|e|e.render} @title.render end end
Trong lệnh [init] thì sẽ sinh ra cùng 1 lúc 30 tấm dùng để làm nền cho hình ảnh menu, dãy [@bg_tiles] được đưa vào.
Lệnh [update] tiến hành xử lý cuộn từng hình ảnh hình nền. Xử lý cuộn thực hiện bằng cách làm chệch từng chút tọa độ của hình ảnh. Nếu chỉ lấy hình ảnh phủ lên thì đoạn code sau cũng có thể làm được.
def update @bg_tiles.each_with_index do |e,i| e.x = e.w *(i%6) e.y = e.h *(i/6) end end
[e] là object mà hình ảnh được điền vào. [e.h][e.w] chính là kích thước của hình ảnh, sử dụng [i] đề tính toán tọa độ cần đặt theo thứ tự từ phía bên trên bên trái. Hơn nữa sử dụng [frame_counter] để làm chệch tất cả các hình ảnh nền đằng sau từng 1 pixel theo thứ tự từ phía bên trên bên trái.
def update @bg_tiles.each_with_index do |e,i| e.x = e.w*(i%6) - frame_counter % e.w e.y = e.h*(i/6) - frame_counter % e.h end end
Chương trình cho đến phần này sẽ là
punyamenu04.rb
require 'mygame/boot' class LogoScene < Scene::Base FADE_IN_TIME = 64 def init @logo = Image.new("images/logo.png") add_event(:key_down) { go_next_scene } add_event(:mouse_button_down) { go_next_scene } end def go_next_scene self.next_scene = TitleScene end def update go_next_scene if frame_counter >= FADE_IN_TIME * 6 end def render alpha = 0 if frame_counter <= FADE_IN_TIME alpha = frame_counter * 255 / FADE_IN_TIME elsif frame_counter <= FADE_IN_TIME * 4 alpha = 255 elsif frame_counter <= FADE_IN_TIME * 5 ct = frame_counter - FADE_IN_TIME * 4 alpha = 255 - ct * 255 / FADE_IN_TIME end @logo.alpha = alpha @logo.render end end class TitleScene < Scene::Base def init @bg = Image.new("images/bg_title.png") @press = TransparentImage.new("images/press_any_key.png", :x => 80, :y => 270) add_event(:key_down) { go_next_scene } add_event(:mouse_button_down) { go_next_scene } end def go_next_scene self.next_scene = MenuScene end def render alpha = frame_counter * 8 alpha = 255 if alpha > 255 @bg.alpha = alpha @bg.render @press.render if frame_counter / 16 % 4 != 0 end end
Skip xử lý hình ảnh
Tại máy tính có cấu hình thấp, khi tổ chức xử lý hình ảnh thì có nguy cơ phát sinh drop xử lý giữa chừng. Những Scene Class tiếp nhận bằng [Scene::Base] có tính năng skip hình ảnh để không làm drop chương trình giữa chừng.
Nếu nhấn phím [Page Down] thì hình ảnh sẽ được skip và FPS sẽ hạ xuống. Trong trường hợp FPS được cài mặc định 60, thì khi nhấn phím [Page Down] 1 lần thì FPS hạ xuống giá tị 30, nhấn lần thì 2 thì FPS được tái cài đặt với gái trị 20, nhấn phím [Page Up] thì giá trị FPS lại trở về giá trị ban đầu.
Mặt khác, với lệnh [render] của Scene Class, nếu ta thêm xử lý dưới đây thì giá trị FPS được cài đặt và giá trị FPS thực tế sẽ được hiển thị trên màn hình một cách dễ dàng.
def render (lược) ShadowFont.render("FPS:#{real_fps()}/#{fps()}") end
Vẽ màn hình menu
Để vẽ màn hình menu thì chúng ta sử dụng những hình ảnh sau.
Để lựa chọn Khi không được lựa chọnCó 3 loại menu những mỗi loại lại được chuẩn bị những hình ảnh sáng và tối của chúng. Menu được chọn mà active thì sẽ hiển thị hình ảnh sáng, còn những menu còn lại thì hiển thị hình ảnh màu tối.
Để hình thành xử lý hình ảnh như trên thì chúng ta thêm những lệnh sau vào [MenuScene].
def init (lược) @menu = [] @back_menu = [] 3.times do |i| x = 70 y = 128 + i*100 img = TransparentImage.new("images/menu#{i}.png)", :x => x, :y => y) back_img = TransparentImage.new("images/menu#{i}_back.png", :x => x, :y => y) @menus << img @back_menu << back_img end end (lược) def render (@bg_tiles + @back_menus + @menus).each{|e| e.render} @title.render end
[@menus] và [@back_menus] là những dãy Object hình ảnh. [@menu] là nhận những hình ảnh sáng màu còn [@back_menus] nhận những hình ảnh tối màu. Xử lý hình ảnh [@menus] và [@back_menus] được thêm vào lệnh [render]. Tại đây có sự liên kết giữa 3 dãy hình ảnh và được gọi ra và thực hiện bằng lệnh [each].
(@bg_tiles + @back_menus + @menus).each{|e| e.render}
Như trên, chúng ta dùng [+] để liên kết giữa các dãy rồi thực hiện lệnh [render] đối với những object chứa 3 dãy trên. Thứ tự kết nối này đều có ý nghĩa. Vì lệnh [each] sẽ thực hiện lệnh đối với object theo thứ tự từ đầu dãy đến cuối nên theo từng thứ tự chúng ta sắp xếp trong dãy mà thứ tự hiển thị hình ảnh cũng khã nhau. Object chứa[@menu]được viết ra ở cuối cùng nên chúng ta nhìn thấy cả 3 hình ảnh sáng. Chương trình cho đến đây là
require 'mygame/boot' class LogoScene < Scene::Base FADE_IN_TIME = 64 def init @logo = Image.new("images/logo.png") add_event(:key_down) { go_next_scene } add_event(:mouse_button_down) { go_next_scene } end def go_next_scene self.next_scene = TitleScene end def update go_next_scene if frame_counter >= FADE_IN_TIME * 6 end def render alpha = 0 if frame_counter <= FADE_IN_TIME alpha = frame_counter * 255 / FADE_IN_TIME elsif frame_counter <= FADE_IN_TIME * 4 alpha = 255 elsif frame_counter <= FADE_IN_TIME * 5 ct = frame_counter - FADE_IN_TIME * 4 alpha = 255 - ct * 255 / FADE_IN_TIME end @logo.alpha = alpha @logo.render end end class TitleScene < Scene::Base def init @bg = Image.new(