11/08/2018, 23:23

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 logo

7.PNG

Hã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ển

7.PNG

8.PNG

Dướ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 Title

10.PNG

Chú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]

12.png11.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 Menu

13.PNG

Hơ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

menu0.png

menu1.png

menu2.png

Khi không được lựa chọn

menu0_back.png

menu1_back.png

menu2_back.png

Có 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(
            
            
            
         
0