Corona SDK tutorial: game Flappy Bat phần 1

I - Giới thiệu

Tiếp tục trong mạch bài viết về Corona SDK, người viết sẽ trình bày những kiến thức căn bản về corona engine qua 1 game phổ biến mỗi khi bạn start engine mới hiện nay, đó là game Flappy Bird.

Flappy Bird là 1 game thuộc thể loại casual được Nguyễn Hà Đông tạo ra, đã trở thành 1 cơn sốt trong suốt 1 thời gian dài ở việt nam và quốc tế, cách chơi của game này rất đơn giản, bạn chỉ việc chạm màn hình để giữ chú chim không bị rơi xuống đất và vượt qua các chướng ngại vật nhiều nhất có thể.

Với tài nguyên được cung cấp sẵn, chúng ta có thể sử dụng Corona dể tạo ra 1 game tương tự đó là game Flappy Bat Nếu sử dụng điện thoại Android bạn đọc có thể tải bản build về chạy thử:

https://drive.google.com/file/d/0B5ZgM3GuNcA1akFubXNkY3I0RHM/view?usp=sharing

Việc làm quen với Corona và công cụ Simulator, bạn đọc có thể tham khảo ở bài viết trước:

https://viblo.asia/TienNM87/posts/1l0rvmByRyqA

Bài viết này sẽ chỉ ra cách sử dụng Corona để giải quyết các vấn đề trong game Flappy Bat, các kiến thức cần thiết sẽ được cung cấp để bạn đọc tham khảo thêm

II - Project

Bạn tải các assets của game tại đây: https://drive.google.com/file/d/0B5ZgM3GuNcA1dlJDM1Z2S2F5VDA/view?usp=sharing Bao gồm các ảnh sẽ sử dụng trong trò chơi, bạn cần copy tất cả ảnh vào thư mục project đã tạo.

Như bạn đọc đã biết, Corona sử dụng 1 số các file đặc biệt để cấu hình project, một số thông tin như orientation, with height của content area, scaling mode .. trong 1 số file đặc biệt việc đầu tiên khi tạo project đó là phải tạo ra các file đặc biệt này đó là

--build.settings
settings = {
   orientation = {
      default = "portrait",
      supported = {
         "portrait",
      },
   },
}

--config.lua
local ratio = display.pixelHeight/display.pixelWidth

application = {
	content = {
		width = ratio > 1.5 and 800 or math.ceil(1200/ratio),
		height = ratio < 1.5 and 1200 or math.ceil(800*ratio),
		scale = 'letterbox',
		fps = 30,

		imageSuffix = {
			['@2x'] = 1.3,
		},--end suffix
	},--end content
} -- end application

--main.lua
display.setStatusBar( display.HiddenStatusBar )

với file build.settings đó là nơi lưu trữ các thông tin để corona build thành game, ở đây chỉ cần thông tin về orientation nên chúng ta set orientation là 'portrait'

với file config.lua là nơi lưu trữ những thuộc tính về content area (vùng nội dung) và scaling mode (chế độ hiển thị) những khái niệm này người đọc có thể tham khảo ở đây

https://docs.coronalabs.com/daily/guide/basics/configSettings/index.html

Ở đây chúng ta sử dụng content area tương ứng với 2 loại màn hình đó là ratio (h/w) > 1.5 (có vẻ dài) < 1.5 (có vẻ ngắn)

với tỉ lệ > 1.5 thì content area là 800 x (800*ratio)

với tỉ lệ < 1.5 thì content area là 1200/ratio x 1200

scaling mode là 'letterbox' cho chúng ta giữ nguyên tỉ lệ ratio trên các màn hình khác nhau, do vậy kết hợp với điều kiện trên ta sẽ được content are full screen với tất cả các màn hình.

Với file main.lua, đây chính là entry của chương trình vì vậy chúng ta khởi chạy một số tác vụ cần thiết khi start game ở đây đơn giản chúng ta chỉ ấn status bar cho đẹp.

Trong quá trình phân tích game flappy bird ta nhận thấy:

game có 3 screen chính

  • screen start game: có background, nền chạy, nút start, nhân vật chúng ta sẽ tạo ra màn hình start game như sau:

Screen Shot 2015-06-29 at 12.11.27 AM.png

  • screen game chính: có background, nền chạy, nhân vật, chướng ngại vật như sau

Screen Shot 2015-06-29 at 12.12.38 AM.png

  • screen restart game: có background, bảng thông báo điểm, nút restart như sau:

Screen Shot 2015-06-29 at 12.12.50 AM.png

Với việc phân tích ban đầu như vậy, chúng ta sẽ tạo ra 3 scene(cảnh) tương ứng cho mỗi màn hình, lần lượt là start, game và restart

Trong corona, mỗi scene(cảnh) được tạo bởi 1 file .lua và được quản lý bởi 1 đối tượng được gọi là composer, đối tượng này chịu trách nhiệm với các tác vụ liên quan đến scene như add scene, remove scene ..

Chúng ta sẽ tạo ra 3 empty file là start.lua, game.lua và restart.lua

Khi bắt đầu trò chơi thì chuyển đến scene start do vậy chúng ta cần sửa file main.lua như sau

display.setStatusBar( display.HiddenStatusBar )

local composer = require "composer"
composer.gotoScene( "start" )

chỉ việc gọi composer để thực hiện việc chuyển scene

Tuy nhiên đến đây chương trình không thể chạy được, nguyên do là file start.lua cần phải được định nghĩa như 1 scene chứ không phải giống như 1 file .lua bình thường, chúng ta cần sửa lại file start.lua như sau

local composer = require( "composer" )
local scene = composer.newScene()

function scene:create( event )
end

function scene:show( event )
end

function scene:hide( event )
end

function scene:destroy( event )
end

-- Listener setup
scene:addEventListener( "create", scene )
scene:addEventListener( "show", scene )
scene:addEventListener( "hide", scene )
scene:addEventListener( "destroy", scene )

return scene

Tác vụ ở đây đó là đăng kí sử lí các sự kiện 'create' - tạo scene, 'show' - hiện scene, 'hide' - ẩn scene, 'destroy' - ẩn scene

khi gọi câu lện composer.newScene() sẽ dẫn tới các sự kiện lần lươt là create => show

Như vậy các tác vụ về khởi tạo và draw scene chúng ta nên viết trong method create. Các công việc đó là : draw background, draw nền, draw player và draw nút start Chúng ta thực hiện các công việc này như sau (trong method create)


local physics = require 'physics'
physics.start()

-- Called when the scene's view does not exist:
function scene:create( event )

    local sceneGroup = self.view

    drawScene(sceneGroup)

end

function drawScene( sceneGroup )
	--draw the background
    background = display.newImageRect('bg.png', 900, 1425)
    background.anchorX = 0.5
    background.anchorY = 1
    background.x = display.contentCenterX
    background.y = display.contentHeight
    sceneGroup:insert(background)

	--draw the platform
	platform = display.newImageRect('platform.png',900,53)
	platform.anchorX = 0
	platform.anchorY = 1
	platform.x = 0
	platform.y = display.viewableContentHeight - 110
	physics.addBody(platform, "static", {density=.1, bounce=0.1, friction=.2})
	platform.speed = 4
	sceneGroup:insert(platform)

	platform2 = display.newImageRect('platform.png',900,53)
	platform2.anchorX = 0
	platform2.anchorY = 1
	platform2.x = platform.width
	platform2.y = display.viewableContentHeight - 110
	physics.addBody(platform2, "static", {density=.1, bounce=0.1, friction=.2})
	platform2.speed = 4
	sceneGroup:insert(platform2)

	--draw the start button
	startBtn = display.newImageRect("start_btn.png",300,65)
	startBtn.anchorX = 0.5
	startBtn.anchorY = 1
	startBtn.x = display.contentCenterX
	startBtn.y = display.contentHeight - 400
	sceneGroup:insert(startBtn)

	--draw the title group

	--draw the title
    title = display.newImageRect("title.png", 500, 100)
	title.anchorX = 0.5
	title.anchorY = 0.5
	title.x = display.contentCenterX - 80
	--title.y = display.contentCenterY
	sceneGroup:insert(title)

	p_options = {

		width = 80,
		height = 42,
		numFrames = 2,

		sheetContentWidth = 160,
		sheetContentHeight = 42,

	}

	--draw player
	playerSheet = graphics.newImageSheet('bat.png', p_options)
	sequenceData = {

		name = 'player',
		start = 1,
		count = 2,
		time = 500,
	}
	player = display.newSprite(playerSheet, sequenceData)
	player.anchorX = 0.5
	player.anchorY = 0.5
	player.x = display.contentCenterX + 240
	--player.y = display.contentCenterY
	player:play()
	sceneGroup:insert(player)

	titleGroup = display.newGroup()
	titleGroup.anchorChildren = true
	titleGroup.anchorX = 0.5
	titleGroup.anchorY = 0.5
	titleGroup.x = display.contentCenterX
	titleGroup.y = display.contentCenterY - 250

	titleGroup:insert(title)
	titleGroup:insert(player)

	sceneGroup:insert(titleGroup)
	titleAnimation()

end

Một số điểm chú ý trong đoạn code trên:

  • Muốn draw image trong Corona chúng ta thực hiện lệnh display.newImageRect() - với 2 tham số with, height, bức ảnh sẽ được stretch (kéo giãn) để full với hình chữ nhật, trong đa số trường hợp width height chính là size của bức ảnh.
  • Thiết lập anchorX & anchorY để xác định pivot của đối tượng hình ảnh, kiến thức này bạn đọc có thể tham khảo ở đây https://docs.coronalabs.com/guide/graphics/transform-anchor.html
  • Thết lập x và y để xác định vị trí đối tượng trên content Area
  • Muốn draw 1 sprite trong Corona chúng ta thực hiện lệnh display.newSprite() - với tham số truyền vào là spriteSheet và các options chi tiết về method này bạn đọc có thể tham khảo ở đây: https://docs.coronalabs.com/api/library/display/newSprite.html
  • Các đối tượng có thể được group lại, việc group giúp ich cho việc thao tác trên nhiều đối tượng ví dụ như thay đổi vị trí, ẩn hiện ...

với các công cụ trên ta dễ dàng xây dựng được một scene 2D tuỳ ý. Việc draw scene coi như là được thực hiện xong.

Công việc tiếp theo ở scene này là chạy platform ở dưới, thực chất thì platform được tạo ra bởi 2 image rect:

   --draw the platform
    platform = display.newImageRect('platform.png',900,53)
    platform.anchorX = 0
    platform.anchorY = 1
    platform.x = 0
    platform.y = display.viewableContentHeight - 110
    physics.addBody(platform, "static", {density=.1, bounce=0.1, friction=.2})
    platform.speed = 4
    sceneGroup:insert(platform)

    platform2 = display.newImageRect('platform.png',900,53)
    platform2.anchorX = 0
    platform2.anchorY = 1
    platform2.x = platform.width
    platform2.y = display.viewableContentHeight - 110
    physics.addBody(platform2, "static", {density=.1, bounce=0.1, friction=.2})
    platform2.speed = 4
    sceneGroup:insert(platform2)

với position là x1 = 0, x2 = platform.with nghĩa là được xếp cạnh nhau, mục đích của việc này là khi platform1 di chuyển sang trái, platform2 cũng di chuyển và khi platform1 di chuyển hết chiều dài của nó, nó sẽ được move về phía cuối của platform2 mà trong lúc đó người chơi vẫn nhìn thấy platform2 đang di chuyển, khi platform2 di chuyển hết chiều dài của nó thì nó lại được move đến cuối platform1 do vậy sẽ tạo ra cảm giác platform dài vô tận chạy mãi không hết! Để thực hiện việc này chúng ta cần sửa lại như sau:

-- Called immediately after scene has moved onscreen:
function scene:show( event )
    local group = self.view
    phase = event.phase
    if phase == 'will' then
    elseif phase == 'did' then
    	--move the platform
    	platform.enterFrame = scrollGround
    	Runtime:addEventListener( 'enterFrame', platform )
    	platform2.enterFrame = scrollGround
    	Runtime:addEventListener( 'enterFrame', platform2 )
    	startBtn:addEventListener( 'touch', startGame )
    end
    print("entered")

end

function scrollGround( self, event )

	if self.x - self.speed < -900 then
		self.x = 900 - (-900 - self.x + self.speed)
	else
		self.x = self.x - self.speed
	end
end

công việc chạy background cần thực hiện trong mỗi frame update do vậy chúng ta cần sử dụng sự kiện 'enterFrame', mõi khi vào 1 frame mới, thì thực hiện method scrollGround. Ở đây chiều rộng mỗi platform là 900 nên khi platform 1 di chuyển hết (self.x - self.speed < -900) thì nó được move tới vị trí cuối platform2 (900 - (-900 - self.x + self.speed)) và ngược lại với platform2.

Công việc còn lại ở scene này là add xử lí sự kiện click button start, chúng ta thực hiện như sau:

--method scene:show
startBtn:addEventListener( 'touch', startGame )

function startGame( event )
	-- body
	if event.phase == 'ended' then
		composer.gotoScene( 'game' )
	end
end

chúng ta cần đăng kí xử lí cho sự kiện 'touch' sử dụng method addEventListener(). Tác vụ ở đây đơn giản là chuyển scene sử dụng composer.

OK, như vậy màn hình start game đã được tạo xong, bạn đọc có thể sử dụng simulator để test thử hoạt động. Trong các phần tiếp theo mình sẽ trình bày cách tạo scene game và scene restart game. Thanks!