Game Studio
Liên kế mạng xã hội

Game Studio


Hướng dẫn: Chuyển/Tạo ứng dụng cho desktop và tvOS với Corona SDK

 

Mặc dù desktop có thể mô phỏng touch và tap bằng cách sử dụng chuột, nhưng khi nói đến game desktop, người dùng muốn chơi với một keyboard hoặc một game controller, hoặc các ứng dụng TV thì chỉ muốn chơi game với một game controller, do đó game của bạn cần phải thích nghi với những nền tảng mới.

Trong thực tế, bạn có thể phải tưởng tượng lại toàn bộ ứng dụng của bạn và làm thế nào nó vẫn có thể hoạt động trên một nền tảng desktop hoặc TV. Hãy xem xét:

TV và hầu hết các desktop là theo định hướng landscape. Ứng dụng theo định hướng Portrait sẽ không cho một cái nhìn tốt cũng như không tạo được sự thú vị.

Màn hình máy tính có nhiều tỉ lệ màn hình khác nhau, nhưng thường rơi vào một trong thể loại sau: 4:3, 3:2, hoặc 16:9. TV thông thường là 4:3 hoặc 16:9 và hầu hết mọi người sử dụng TV Android hoặc tvOS rất có khả năng sẽ dùng màn hình 16:9 do sự phụ thuộc vào cổng HDMI.

Nhập văn bản thông thường là một trải nghiệm phiền toái với người dùng cuối.

Người dùng trên thực tế sẽ sử dụng ứng dụng của bạn trên TV/desktop? Ví dụ, là họ sẽ phải bật tv của họ, khởi động ứng dụng của bạn, và post một tweet? Hoặc họ sẽ chỉ cần nhấc điện thoại và tweet ngay tại đó? Nói cách khác, bạn có muốn triển khai một ứng dụng trên TV/desktop?

Các button mà thay đổi khi bị touch sẽ không làm việc. Thay vào đó, các button cần phải thay đổi khi chúng được lựa chọn, chứ không phải khi chúng được kích hoạt. Tương tự như vậy, bạn sẽ zoom vào button được lựa chọn? Hay làm mờ các button không được chọn? Hay thêm hình động bổ sung cho các button?

Trước khi bạn cố gắng để xây dựng ứng dụng của bạn cho một nền tảng TV/desktop, bạn cần phải đảm bảo với tính hiệu đầu vào của controller - keyboard. Hướng dẫn khác nhau về các chủ đề này có thể được tìm thấy ở đây.

Giả sử bạn đã xem xét tất cả các điểm bên trên và bạn đã sẵn sàng để bắt tay vào con đường desktop/TV, hướng dẫn này sẽ tập trung vào việc xây dựng một màn hình menu và màn hình này sẽ làm việc với một tính hiệu đầu vào (input) từ remote như trên Apple TV hoặc keyboard cơ bản của máy tính.

Remote của Apple TV

Thiết bị này cung cấp một số input events mà ứng dụng Corona của bạn có thể tận dụng. Trong thực tế, Apple coi nó như là một "microgamepad" hơn là một remote. Với Remote Apple TV, bạn có quyền truy cập vào các input sau. Nó thậm chí còn hỗ trợ các event trùng khớp với các event gia tốc (accelerometer) trong Corona.

 

Các inputs là rất giống với những gì bạn nhận được trên một game controller. Ví dụ, một cái click trên touch pad tạo ra một key "buttonA". Điều này cũng giống như button "A" trên Xbox controllers hoặc button "X" trên PlayStation controllers. Các button Play/Pause tạo ra một "buttonX" event mà đượ mapping thành button "X" trên Xbox controllers hoặc button "Square" trên PlayStation controllers. Key events từ việc swiping ngón tay của bạn trên touch pad sẽ bắt chước các “D-pad” inputs trên game controllers hoặc các phím mũi tên trên bàn phím máy tính.

Bây giờ chúng ta hãy xem làm thế nào những event này có thể được sử dụng để điều khiển menu chính của ứng dụng.

Xây dựng một menu hoạt động

Nếu bạn đang porting một game di động sang nền tảng desktop hoặc TV, bạn có thể sẽ có một số loại menu game. Menu game này có thể có các tùy chọn như “Play”, “Help”, “Settings”, “Credits”,vv... Trong thế giới di động, bạn có thể xây dựng các đối tượng này với với touch handlers hoặc có thể sử dụng một loạt các widget button. Hãy xem xét bố trí này trong một scene Composer điển hình:

 

display.setDefault( "background", 1, 1, 1 )

-- Called when the scene's view does not exist
function scene:create( event )
    local sceneGroup = self.view

    local playButton = display.newImageRect("images/newgame.png", 160, 31)
    sceneGroup:insert(playButton)

    local function playGame(event)
        composer.gotoScene( "loading", {effect="slideLeft", time=500}  )
        return true
    end

    playButton.x = display.contentWidth / 2
    playButton.y = display.contentHeight - 32
    playButton:addEventListener("tap", playGame)

    local resumeButton = display.newImageRect("images/resume.png", 120, 31)
    sceneGroup:insert(resumeButton)

    local function resumeGame(event)
        composer.gotoScene( "loading", {effect="slideLeft", time=500}  )
        return true
    end

    resumeButton.x = display.contentWidth / 2
    resumeButton.y = display.contentHeight - 32 - 50
    resumeButton:addEventListener("tap", resumeGame)

    local settingsButton = display.newImageRect("images/settingsButton.png", 133, 31)
    sceneGroup:insert(settingsButton)

    local function goSettings(event)
        composer.gotoScene( "gamesettings", {effect="slideLeft", time=500} )
        return true
    end

    settingsButton.x = 25
    settingsButton.y = display.contentHeight - 32 - 100
    settingsButton:addEventListener("tap", goSettings)

    local helpButton = display.newImageRect("images/helpButton.png", 78, 32)
    sceneGroup:insert(helpButton)

    local function goHelp(event)
        composer.gotoScene( "help", {effect="slideLeft", time=500} )
        return true
    end

    helpButton.x = 25
    helpButton.y = display.contentHeight - 32 - 50
    helpButton:addEventListener("tap", goHelp)

    local creditsButton = display.newImageRect("images/creditsButton.png", 116, 31)
    sceneGroup:insert(creditsButton)

    local function goCredits(event)
        composer.gotoScene( "gamecredits" , {effect="slideLeft", time=500})
        return true
    end

    creditsButton.x = 25
    creditsButton.y = display.contentHeight - 32
    creditsButton:addEventListener("tap", goCredits)
end

Trong trường hợp này, có tất cả 5 button, nhưng tất cả chúng đều là local trong hàm scene:create() bởi vì chúng ta không cần truy cập vào chúng nữa. Điều này có nghĩa là chúng ta đã không thực hiện bất kỳ cách nào để biết button đó đang được chọn và button nào có thể sẽ được chọn tiếp theo. Mặc dù touch hoặc tap handler vẫn có thể được sử dụng để đáp ứng với các button, chúng ta sẽ cần phải thiết lập một hệ thống đẻ "select” một button trên desktop/TV.

Với suy nghĩ này, hãy xem cách chúng ta có thể quản lý các button của chúng ta để sau này chúng ta có thể truy cập vào chúng. Ở phía trên của scene, tạo ra một table để giữ các tham chiếu cho các button. Đây sẽ là một array table cơ bản với các indexes. Ngoài ra, chúng ta sẽ cần một vài biến số để quy mô (scoped) cho toàn bộ scene:

 display.setDefault( "background", 1, 1, 1 )

local buttons = {}
local selectedButton = 0
local previousButton = 0

Bây giờ, sửa đổi hàm scene:create() như thế này:

 function scene:create( event )
    local sceneGroup = self.view

    local playButton = display.newImageRect("images/newgame.png", 160, 31)
    sceneGroup:insert(playButton)

    local function playGame(event)
        composer.gotoScene( "loading", {effect="slideLeft", time=500}  )
        return true
    end

    playButton.x = display.contentWidth / 2
    playButton.y = display.contentHeight - 32
    playButton:addEventListener("tap", playGame)

    buttons[5] = playButton

    local resumeButton = display.newImageRect("images/resume.png", 120, 31)
    sceneGroup:insert(resumeButton)

    local function resumeGame(event)
        composer.gotoScene( "loading", {effect="slideLeft", time=500}  )
        return true
    end

    resumeButton.x = display.contentWidth / 2
    resumeButton.y = display.contentHeight - 32 - 50
    resumeButton:addEventListener("tap", resumeGame)

    buttons[4] = resumeButton

    local settingsButton = display.newImageRect("images/settingsButton.png", 133, 31)
    sceneGroup:insert(settingsButton)

    local function goSettings(event)
        composer.gotoScene( "gamesettings", {effect="slideLeft", time=500} )
        return true
    end

    settingsButton.x = 25
    settingsButton.y = display.contentHeight - 32 - 100
    settingsButton:addEventListener("tap", goSettings)

    buttons[1] = settingsButton

    local helpButton = display.newImageRect("images/helpButton.png", 78, 32)
    sceneGroup:insert(helpButton)

    local function goHelp(event)
        composer.gotoScene( "help", {effect="slideLeft", time=500} )
        return true
    end

    helpButton.x = 25
    helpButton.y = display.contentHeight - 32 - 50
    helpButton:addEventListener("tap", goHelp)

    buttons[2] = helpButton

    local creditsButton = display.newImageRect("images/creditsButton.png", 116, 31)
    sceneGroup:insert(creditsButton)

    local function goCredits(event)
        composer.gotoScene( "gamecredits" , {effect="slideLeft", time=500})
        return true
    end

    creditsButton.x = 25
    creditsButton.y = display.contentHeight - 32
    creditsButton:addEventListener("tap", goCredits)

    buttons[3] = creditsButton
end

Các bổ sung chỉ đơn giản là chỉ định các local buttons thành các buttons array. Khi scene được thiết kế ban đầu, thứ tự của các button không quan trọng, tuy nhiên khi porting ứng dụng sang bất kỳ desktop/TV, chúng ta phải cân nhắc cẩn thận các trải nghiệm người dùng. Trong đó, các button trên cùng bên trái phải là những button đầu tiên, nhưng nó là button thứ 3 so với khởi tạo ban đầu. Nói cách khác, chúng ta nên gán các button trong mảng theo thứ tự mà chúng ta muốn "Up" và "Down" sẽ lướt qua.

Sử dụng input events

Tiếp theo, chúng ta sẽ cần phải thiết lập một hàm để xử lý các key event. Hàm này sẽ quản lý việc lựa chọn các buttons, bao gồm việc hiển thị lựa chọn này cho người dùng. Chúng ta cũng cần có một key event để kích hoạt button. Kiểm tra code này:

local function onKeyEvent( event )

    local phase = event.phase
    local keyName = event.keyName

    -- 1
    local previousButton = selectedButton

    -- 2
    if ( keyName == "enter" or keyName == "buttonA" ) and phase == "down" and selectedButton ~= 0 then
        local mappedEvent = { name = "tap" }
        buttons[selectedButton]:dispatchEvent( mappedEvent )
        return true
    end

    -- 3
    if keyName == "up" and phase == "down" then
        if selectedButton == 0 then
            selectedButton = #buttons
        else
            selectedButton = selectedButton - 1
            if selectedButton < 1 then
                selectedButon = #buttons
            end
        end
    elseif keyName == "down" and phase == "down" then
        if selectedButton == 0 then
            selectedButton = 1
        else
            selectedButton = selectedButton + 1
            if selectedButton > #buttons then
                selectedButton = 1
            end
        end
    elseif keyName == "right" and phase == "down" then
        if selectedButton == 0 then
            selectedButton = 1
        else
            if selectedButton == 1 or selectedButton == 2 then
                selectedButton = 4
            elseif selectedButton == 3 then
                selectedButton = 5
            end
        end
    elseif keyName == "left" and phase == "down" then
        if selectedButton == 0 then
            selectedButton = #buttons
        else
            if selectedButton == 4 then
                selectedButton = 2
            elseif selectedButton == 5 then
                selectedButton = 3
            end
        end

    -- 4
    elseif ( keyName == "back" or keyName == "buttonStart" or keyName == "escape" ) and phase == "down" then
        native.requestExit()
        return true
    end

    -- 5
    transition.to( buttons[previousButton], { time=500, xScale=0.909, yScale=0.909 } )
    transition.to( buttons[selectedButton], { time=500, xScale=1.10, yScale=1.10 } )
    return false
end

Hãy kiểm tra toàn bộ hàm này từng bước một:

Bước 1

Lưu button đang được chọn, trong trường hợp chúng ta thay đổi button được chọn. Chúng tôi sẽ sử dụng điều này sau.

Bước 2

Trong khối code này, chúng tôi xử lý việc đi đến scene được chọn. Trên máy tính, hãy sử dụng phím "Enter" để chọn scene. Trên Remote của Apple, Apple Human Interface Guidelines đề nghị lựa chọn button bằng cách nhấn vào touch pad - điều này tạo ra một "buttonA" event.

Vì các button đang chờ đợi một tap event, chúng ta thiết lập một table mà có các thông tin cần thiết cho các tap (trong trường hợp này chỉ là thuộc tính name như các hàm xử lý chứ không quan tâm về các thuộc tính cảm ứng khác).

Cuối cùng, chúng ta gửi một even cho các đối tượng với các touch handler. Điều này sẽ kích hoạt các hàm listener chúng ta đã gán và thay đổi scene đến nơi mà chúng ta muốn đến.

Bước 3

Đây là câu lệnh if-then-else được sử dụng để chọn một button. Nếu menu của chúng ta là chỉ trong một cột hoặc một hàng, chúng ta có thể map "Up" và "Left" để làm cùng một điều, cũng như "Down" và "Right" theo ý muốn của chúng ta. Tuy nhiên trong ví dụ này, chúng ta có 2 cột và 3 hàng.

Về cơ bản, chúng ta cần phải quyết định hành vi của chuyển động sẽ như thế nào. Trong trường hợp này, chúng ta hãy giả định rằng "Up" và "Down" sẽ chỉ đơn giản là lướt qua các nút theo thứ tự. Nói cách khác, nếu button [1] được chọn và người dùng nhấn "Down", nó sẽ di chuyển đến button [2], sau đó button [3]. Vì không có button nào dưới button [3], theo logic thì button [4] nên là button tiếp theo được lựa chọn. Cuối cùng, khi chúng tôita đến button [5], chúng tôi quay vòng trở lại button [1]. Theo cùng một phương thức, button "Up" sẽ làm điều tương tự, nhưng ngược lại (lướt qua các button từ sau ra trước).

Trong cả hai trường hợp của "Up" và "Down", nếu chúng ta không chọn trước một button, chúng ta sẽ chọn một cái (nếu chúng ta nhấn “Up”, chọn button cuối cùng, và nếu chúng ta nhấn “Down”, chọn button đầu tiên). Nếu không, thêm hoặc trừ đi một giá trị và selectedButton, nếu chúng ta cẩn thận hơn, điều chỉnh button được chọn là button đầu tiên hay cuối cùng.

Di chuyển sang trái hoặc phải tạo ra một tình huống đầy thách thức, đặc biệt là bởi vì các số lẻ của các button. Trong trường hợp này, nếu nút [1] hoặc nút [2] được chọn và "Right" event xảy ra, chúng ta nhảy sang button [4]. Nếu button [3] được chọn, chúng ta chuyển đến button [5]. Button [5] sẽ quay trở lại button [3], và button [4] sẽ quay về [2]. Chúng ta xây dựng logic như thế cho các "Left" và "Right" key events.

Bước 4

Trong thế giới iOS, ứng dụng thường không "thoát ra". Điều này cũng có thể áp dụng cho tvOS. Ứng dụng Android có thể thoát khỏi/exit, cũng như các ứng dụng desktop. Block code này sẽ yêu cầu một “exit” nếu hệ điều hành hỗ trợ cho nó. Chúng ta map điều này vào phím "back" (Android), "buttonStart"(button "Menu" của Apple TV Remote hoặc button "logo" của controller), hoặc phím "Esc" từ bàn phím.

Bước 5

Cuối cùng, trong ví dụ này, chúng tôi phóng to button được lựa chọn lên 110% bằng cách sử dụng một transition. Ngoài ra, chúng ta thu nhỏ button đã được chọn trước đó. Vì chúng ta zoom lên 110%, chúng tôi thu nhỏ 100/110 (90,9%) để chúng được trở lại kích thước ban đầu. Tất nhiên, có những cách khác để hiển thị nếu một button được chọn - cân nhắc việc thêm một hình ảnh cho button được chọn, hay làm mờ button không được chọn, hoặc đảo ngược màu sắc button được chọn...

Kích hoạt key event handler

Để kích hoạt key handling, chúng ta phải thêm một event handler vào Runtime. Trong Composer, điều này thường được thêm vào "did" phase của hàm scene:show():

function scene:show( event )
    local sceneGroup = self.view

    if event.phase == "did" then
        Runtime:addEventListener( "key", onKeyEvent )
    end
end

Điều này sẽ cho phép event handler bắt đầu đúng nơi của nó, nhưng chúng ta cũng sẽ cần phải loại bỏ event listener trước khi chúng ta đi đến bất kỳ scene nào khác. Đặt nó vào "will" phase của hàm scene:hide() là một vị trí thuận tiện, mặc dù có thể xem xét loại bỏ nó ngay trước khi gọi composer.gotoScene() để ngăn chặn bất kỳ key events nào được kích hoạt khi quá trình transition đang diễn ra.

function scene:hide( event )
    local sceneGroup = self.view

    if event.phase == "will" then
        Runtime:removeEventListener( "key", onKeyEvent )
    end
end

Lời kết

Đây là một thời điểm thú vị dành cho các nhà phát triển Corona, bây giờ chúng ta có thể xây dựng cho nền tảng desktop và TV. Porting các ứng dụng dựa trên cảm ứng hiện có sang các nền tảng này đòi hỏi một số nỗ lực và sự cân nhắc về giao diện người dùng/hiển thị mới, nhưng có thể thấy quá trình này cũng khá... đơn giản.

Xem thêm: