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

Game Studio


Hướng dẫn tạo một dòng nước đẹp mắt với LiquidFun, snapshot và filter trong Corona SDK

Hướng dẫn này được viết bởi Andreas von Lepel từ Frozen Gun Studios. Tiếp theo thành công trên toàn thế giới của "Freeze! - The Escape" (iOS / Android) với hơn 11 triệu lượt download, Frozen Gun Studios vừa phát hành phần tiếp theo," Freeze! 2 - Brothers" (iOS / Android), được xây dựng hoàn toàn bằng Corona SDK và đặt trưng bởi hiệu ứng nước dựa trên LiquidFun.

Tổng quan

Ngay từ lúc đầu khi phát triển trò chơi, chúng tôi quyết định rằng nó sẽ là một trò chơi hấp dẫn xung quanh những hiệu ứng chuyển động của dòng nước và các chất độc. Bạn có thể xem trailer để thấy những gì chúng tôi đã làm:

 

 

Như bạn có thể thấy, cả 2 nhân vật của chúng tôi có thể bơi trong nước, nhưng trong các phần sau, nhiệm vụ của người chơi là thu thập tất cả nhiên liệu trong những cái thùng và sau đó tìm lối thoát ra khỏi level.

Project code

Thiết lập một scene với những hạt nước bằng cách sử dụng LiquidFun là khá dễ dàng và đã có một số ví dụ trong kho Corona SampleCode. Tuy nhiên, việc tạo ra những hạt nước trong suốt để có thể nhìn thấy một "bề mặt" sẽ khó khăn hơn một chút nên bạn cần phải sử dụng snapshots và filters. May mắn thay, Corona SDK có thể giúp bạn về điều này.

Khi chúng ta tiếp tục, tôi khuyến cáo các bạn download project LiquidFun-Transparency trên kho GitHub, hay các hướng dẫn trong kho ví dụ của các bản build gần đây:

CoronaSDK-XXXX → SampleCode → Physics → LiquidFun-Transparency

Trong thư project folder, mở file main.lua. Chúng ta sẽ bắt đầu với những thiết lập cơ bản:

Liquid “container”

  1. -- Add 3 đối tượng vật lý như là ranh giới cho nguồn nước, chúng sẽnằm ngoài màn hình hiển thị
  2. local leftSide = display.newRect( worldGroup, -54-letterboxWidth, display.contentHeight-180, 600, 70 )
  3. physics.addBody( leftSide, "static" )
  4. leftSide.rotation = 86
  5. local centerPiece = display.newRect( worldGroup, display.contentCenterX, display.contentHeight+60+letterboxHeight, 440, 120 )
  6. physics.addBody( centerPiece, "static" )
  7. local rightSide = display.newRect( worldGroup, display.contentWidth+54+letterboxWidth, display.contentHeight-180, 600, 70 )
  8. physics.addBody( rightSide, "static" )
  9. rightSide.rotation = -86

Trong block code này, chúng ta sử dụng 3 hình chữ nhật static để xây dựng một “container” bên ngoài ranh giới màn hình. Container sẽ được mở ở trên và để giữ nước bên trong màn hình.

Scrolling background

Tôi muốn rằng ngay cả khi chúng ta tạo một bản demo thì nó cũng phải trông thật đẹp, vì vậy trong block code tiếp theo, chúng ta bổ sung một scrolling background vô tận bằng cách đặt hai hình ảnh giống hệt nền liền kề với nhau, một hình ảnh đầy đủ có thể nhìn thấy ở giữa màn hình, một cái khác ở bên phải của nó.

  1. -- Tạo một scrolling background vô tận.
  2. local background1 = display.newImageRect( worldGroup, "background.png", 320, 480 )
  3. background1.x = 160
  4. background1.y = 240
  5. background1.xScale = 1.202
  6. background1.yScale = 1.200
  7. transition.to( background1, { time=12000, x=-224, iterations=0 } )
  8. local background2 = display.newImageRect( worldGroup, "background.png", 320, 480 )
  9. background2.x = 544
  10. background2.y = 240
  11. background2.xScale = 1.202
  12. background2.yScale = 1.200
  13. transition.to( background2, { time=12000, x=160, iterations=0 } )

Lưu ý rằng cả 2 hình ảnh sẽ được di chuyển từ từ sang bên trái và sau đó, thông qua sự lặp lại vô tận, chúng sẽ được thiết lập trở lại vị trí ban đầu và di chuyển một lần nữa, tất cả đều dùng cuộc gọi transition.to() rất đơn giản.

Nhân vật

Tiếp theo, chúng ta bổ sung thêm những nhân vật (đại diện là một con mắt) vào scene như là một đối tượng vật lý dynamic và sẽ bơi trên mặt nước. Nó cũng có thể được touched và dragged xung quanh bởi người dùng. Tôi sẽ không đi sâu vào các code để touch-drag, một điều mà trước đây đã được viết và demo rất nhiều.

  1. -- Tạo nhân vật
  2. local hero = display.newImageRect( worldGroup, "hero.png", 64, 64 )
  3. hero.x = 160
  4. hero.y = -400
  5. physics.addBody( hero, { density=0.7, friction=0.3, bounce=0.2, radius=30 } )
  6. -- Làm cho nhân vật có khả năng drag thông qua touch handler và physics touch joint
  7. local function dragBody( event )
  8. local body = event.target
  9. local phase = event.phase
  10. if ( "began" == phase ) then
  11. display.getCurrentStage():setFocus( body, event.id )
  12. body.isFocus = true
  13. body.tempJoint = physics.newJoint( "touch", body, event.x, event.y )
  14. body.isFixedRotation = true
  15. elseif ( body.isFocus ) then
  16. if ( "moved" == phase ) then
  17. body.tempJoint:setTarget( event.x, event.y )
  18. elseif ( "ended" == phase or "cancelled" == phase ) then
  19. display.getCurrentStage():setFocus( body, nil )
  20. body.isFocus = false
  21. event.target:setLinearVelocity( 0,0 )
  22. event.target.angularVelocity = 0
  23. body.tempJoint:removeSelf()
  24. body.isFixedRotation = false
  25. end
  26. end
  27. return true
  28. end
  29. hero:addEventListener( "touch", dragBody )

Hệ thống hạt LiquidFun và dòng nước

Trong các block code tiếp theo, hệ thống hạt cho dòng nước sẽ được tạo ra, và một hình chữ nhật lớn của các hạt nước sẽ được đặt vào scene. Nếu cần thiết, xin vui lòng đọc hướng dẫn mô tả quá trình này để biết chi tiết hơn.

  1. -- Tạo LiquidFun particle system cho dòng nước
  2. local particleSystem = physics.newParticleSystem{
  3. filename = "liquidParticle.png",
  4. radius = 3,
  5. imageRadius = 5,
  6. gravityScale = 1.0,
  7. strictContactCheck = true
  8. }
  9. -- Tạo một "block" của dòng nước (LiquidFun group)
  10. particleSystem:createGroup(
  11. {
  12. flags = { "tensile" },
  13. x = 160,
  14. y = 0,
  15. color = { 0.1, 0.1, 0.1, 1 },
  16. halfWidth = 128,
  17. halfHeight = 256
  18. }
  19. )

Và đây là kết quả từ đầu cho đến bây giờ:

Dòng nước đang chảy, nhưng nó trông giống như là dầu hơn vì nó vẫn chưa trong suốt.

Add transparency (tạo sự trong suốt cho dòng nước)

Bây giờ mọi thứ sẽ phức tạp hơn một chút. Chúng ta sẽ làm cho dòng nước trở nên bán trong suốt. Ví dụ với một giá trị alpha 0,3 (30%), ta có thể nhìn thấy background ở đằng sau dòng nước.

Nếu chúng ta thay đổi giá trị cuối cùng (alpha) của các thuộc tính màu sắc trên line thứ 111, nó sẽ trong suốt, nhưng đây không phải là điều chúng ta muốn, hãy nhìn xem:

Mỗi một hạt bây giờ sẽ có một giá trị alpha là 30%.

Nếu bạn nhìn kỹ vào hình ảnh, bạn có thể thấy rằng mỗi hạt bây giờ có một giá trị alpha là 0.3, và điều này sẽ render ra một dòng nước hỗn độn bởi vì tất cả các hạt đều chồng lên nhau một chút. Mặc dù điều này có thể có lợi cho một số trường hợp, nhưng nó chắc chắn không phải là những gì chúng ta đang cố gắng để đạt được.

Thêm độ trong suốt bằng cách sử dụng một snapshot

Giải pháp là render tất cả các hạt vào một snapshot texture trên mỗi khung hình và sau đó áp dụng transparency cho toàn bộ texture. Dưới đây là các code có liên quan:

  1. -- Khởi tạo snapshot
  2. local snapshot = display.newSnapshot( worldGroup, 320+letterboxWidth+letterboxWidth, 480+letterboxHeight+letterboxHeight )
  3. local snapshotGroup = snapshot.group
  4. snapshot.x = 160
  5. snapshot.y = 240
  6. snapshot.canvasMode = "discard"
  7. snapshot.alpha = 0.3

---

  1. -- Chèn particle system vào snapshot
  2. snapshotGroup:insert( particleSystem )
  3. snapshotGroup.x = -160
  4. snapshotGroup.y = -240

---

  1. -- Cập nhật (vô hiệu) snapshot từng khung hình
  2. local function onEnterFrame( event )
  3. snapshot:invalidate()
  4. end
  5. Runtime:addEventListener( "enterFrame", onEnterFrame )

Về cơ bản, một snapshot và group của nó sẽ được tạo ra, vị trí là ở trung tâm của khu vực nội dung. Lưu ý rằng chiều rộng và chiều cao của snapshot được điều chỉnh bởi các biến letterboxWidthletterboxHeight đã được tính toán trước trong main.lua - điều này đảm bảo rằng, khi chạy chế độ scale: "letterbox", snapshot sẽ chiếm toàn bộ màn hình trên các thiết bị có các tỉ lệ khác nhau.

Trên line 123, chúng ta thiết lập giá trị alpha của toàn bộ snapshot về 0.3, thiết lập này sẽ đưa opacity của toàn bộ snapshot texture về 30%. Sau đó, các hệ thống hạt được đưa vào snapshot group và cuối cùng, với sự giúp đỡ của "enterFrame" listener, snapshot sẽ bị vô hiệu và được render một lần nữa ở mỗi frame.

Với sự bổ sung này, bạn có thể thấy rằng dòng nước hiện tại đã có sự trong suốt:

Cuối cùng thì nó đã trong suốt

Làm cho nó đẹp mắt hơn bằng cách bổ sung một filter

Khi tôi đến được đây, tôi đã rất hạnh phúc. Nhưng các kỹ sư ở Corona Labs đã hỗ trợ thêm bộ lọc - filter, nên tôi bắt đầu thử nghiệm rất nhiều với tất cả các filter và các tùy chọn của filter để bổ sung một bề mặt có thể nhìn thấy của dòng nước. Phải mất một thời gian để đạt được một cái nhìn chính xác, nhưng cuối cùng thì tôi cũng đã tìm ra nó:

  1. -- Apply một "sobel" filter để miêu tả bề mặt nhìn thấy được của dòng nước
  2. snapshot.fill.effect = "filter.sobel"

Và đây, bây giờ chúng ta đã có một dòng nước trong suốt và một bề mặt khá đẹp!

"Bề mặt" dòng nước khá đẹp mắt và nó được rendered bởi sobel filter.

Những vấn đề tiếp theo

Từ đây, có rất nhiều thí nghiệm bạn có thể thực hiện, ví dụ như tạo các phần chơi thú vị cho nhân vật của chúng ta (ở đây bạn có thể điều chỉnh nó phải rất nhẹ để luôn luôn nổi trên mặt nước, hoặc nó nó sẽ nặng và chìm xuống dưới).

Một lựa chọn khác là sử dụng các filter khác cho dòng nước và điều chỉnh các thiết lập cho các filter tương ứng của chúng:

  • snapshot.fill.effect = "filter.emboss"
  • snapshot.fill.effect = "filter.frostedGlass"
  • snapshot.fill.effect = "filter.crystallize"
  • snapshot.fill.effect = "filter.scatter"

Cuối cùng, đừng quên là sẽ có nhiều "flag" khác nhau để bạn có thể sử dụng cho các group của LiquidFun - ví dụ: thêm "staticPressure" và các hạt nước ở dưới cùng của container sẽ không bị nén....

Vấn đề hiệu năng

Không phải tất cả các thiết bị đều đủ mạnh để hiển thị dòng nước với các hiệu ứng đặc biệt mà vẫn duy trì một tỷ lệ khung hình chấp nhận được. Vì thế, tôi có những biện pháp phòng ngừa sau đây:

  • Trên iOS, tôi sử dụng snapshot trên tất cả các thiết bị được hỗ trợ (iPhone 4S và cao hơn; iPad 2 và cao hơn). Tuy nhiên, sobel filter chỉ được đưa vào trên các thiết bị mạnh hơn như các iPad Air hay iPad mini 2 (iPad4, *), iPhone 5S (iPhone6,1), hoặc iPod Touch 5G (iPod5,1) và cao hơn.
  • Đối với Android, tôi thiết lặp minSdkVersion là "16" (Android 4.1) để loại trừ nhiều thiết bị cũ. Ngoài ra, tôi sử dụng sobel filter chỉ trên các thiết bị có hỗ trợ shader độ chính xác cao, được xác định bằng cách này: system.getInfo( "gpuSupportsHighPrecisionFragmentShaders" )
  • Cuối cùng, tôi thử nghiệm system.getInfo( "androidDisplayDensityName" ) đối với "xhdpi", "xxhdpi", và "xxxhdpi" - chỉ những thiết bị này mới sử dụng sobel filter, vì chúng là khá hiện đại và hy vọng sẽ đủ mạnh.

Kết luận

Như bạn có thể thấy, khá dễ dàng để tạo ra dòng nước đẹp và các vật chất lỏng khác. Đây là một công nghệ rất tuyệt và nó có thể sử dụng cho các thiết bị hiện đại ngày nay. Tôi hi vọng rằng bạn sẽ có thể tận dụng được điều này cho dự án tiếp theo của bạn.

J.Black

 


Đăng sự kiện cho developer