Swift_Tetris game - part 5
Các phần trước ta đã thiết lập gần như đầy đủ các thuộc tính và phương thức về giao diện cho game, đồng thời đã tạo chuyển động cho các khối hình. Và chuyển động như thế nào cho hợp lý và điều khiển các chuyến động ra sao thì ta sẽ thực hiện trong bài này. Có thể coi phần này là phần phức tạp nhất ...
Các phần trước ta đã thiết lập gần như đầy đủ các thuộc tính và phương thức về giao diện cho game, đồng thời đã tạo chuyển động cho các khối hình. Và chuyển động như thế nào cho hợp lý và điều khiển các chuyến động ra sao thì ta sẽ thực hiện trong bài này. Có thể coi phần này là phần phức tạp nhất trong việc xây dựng game. Ta phải liệt kê tất cả những khả năng sẽ xảy ra trong các chuyển động của khối block: ví dụ như các khối block không được va chạm nhau, block không được roai vượt quá khung hình, điểm sẽ được ghi chỉ khi mỗi hàng được lấp đầy bởi các khôi block, các khối block đã rơi phải tự động rơi xuống lấp lấy vị trí mà các hàng đã ghi điểm. Chúng ta sẽ bắt đầu thiết lập các rule này bằng cách tạo các protocol.
Bài trước ta đã đề cập đến Hashable và Printable là 2 protocol mà một số class ta đã sử dụng đến. Bây giờ ta cần thiết lập 1 giao thức để tiếp nhận những cập nhật thay đổi từ lớp Swiftris
let PreviewRow = 1 protocol SwiftrisDelegate { // Được gọi khi game kết thúc func gameDidEnd(swiftris: Swiftris) // Được gọi khi 1 ván game mới bắt đầu func gameDidBegin(swiftris: Swiftris) // Được gọi khi khối hình bắt đầu xuất hiện trên khung trò chơi func gameShapeDidLand(swiftris: Swiftris) // Được gọi mỗi khi khối hình thay đổi vị trí func gameShapeDidMove(swiftris: Swiftris) // Được gọi mỗi khi khối hình thay đổi vị trí sau khi nó đã rơi xuống func gameShapeDidDrop(swiftris: Swiftris) // Được gọi khi game chuyển sang level mới func gameDidLevelUp(swiftris: Swiftris) }
Trong class Swiftris ta khai báo biến delegate với kiểu là protocol tiết lập bên trên. Biến delegate này ta sẽ sử dụng trong suốt quá trình chơi game. GameViewController sẽ sử dụng delegate để cập nhật giao diện và thay đổi ứng với mỗi thay đổi từ lớp Swiftris.
var delegate:SwiftrisDelegate?
Trên giao diện, GameViewController sẽ gửi yêu cầu đến lớp Swiftris để hiện thì khối hình rơi xuống, sang trái hay sang phải. Swiftris sẽ chập nhận request này và di chuyển các khôi hình nếu như chuyển động đó hợp lệ.
Tiếp theo ta thêm 1 function để kiểm tra và phát hiện mỗi khi có khối hình nào di chuyển không hợp lệ. Trước hết, trong function beginGame() của class Swiftris ta khai báo thêm
delegate?.gameDidBegin(self)
và khai báo thêm function detectIllegalPlacement
func detectIllegalPlacement() -> Bool { if let shape = fallingShape { for block in shape.blocks { if block.column < 0 || block.column >= NumColumns || block.row < 0 || block.row >= NumRows { return true } else if blockArray[block.column, block.row] != nil { return true } } } return false }
Trong function trên ta đã check điều kiện sao cho các khôi block không được vượt quá phạm vi khung hình kèm theo điều kiện cho các khối hình rơi xuống không được chiếm vị trí của các hình đã rơi xuống trước nó. Swiftris sẽ định nghĩa các function hoạt động theo cơ chế trial-and-error, cụ thể ở đây, ta sẽ để khối block cập nhật vị trí đến bất cứ vị trí nào trên khung hình rồi kiểm tra xem vị trí đó có hợp lệ hay không.
Sau khi định nghĩa xong hàm trên, ta sẽ khai báo 1 toán tử điều kiện trong function newShape() dựa trên hàm kiểm tra ta vừa khai báo ở trên
if detectIllegalPlacement() { nextShape = fallingShape nextShape!.moveTo(PreviewColumn, row: PreviewRow) endGame() return (nil, nil) }
khối lệnh điều kiện trên giúp ta kiểm tra xem game đã kết thúc hay chưa, cụ thể game sẽ kết thúc khi khối hình cuối cùng rơi xuống có đỉnh chạm hoặc bị che khuất bởi cạnh trên của khung hình.
Tiếp theo ta sẽ khai báo thêm 1 số hàm hepler giúp lớp Swiftris có khả năng xoay và di chuyển các khối hình
final func rotateClockwise() { let newOrientation = Orientation.rotate(orientation, clockwise: true) rotateBlocks(newOrientation) orientation = newOrientation } final func rotateCounterClockwise() { let newOrientation = Orientation.rotate(orientation, clockwise: false) rotateBlocks(newOrientation) orientation = newOrientation }
2 function này giúp các khối hình quay theo chiều và ngược chiều kim đồng hồ.
final func raiseShapeByOneRow() { shiftBy(0, rows:-1) } final func shiftRightByOneColumn() { shiftBy(1, rows:0) } final func shiftLeftByOneColumn() { shiftBy(-1, rows:0) }
3 hàm này thực hiện việc điều khiển các khối hình rơi xuống, sang trái hay sang phải.
Tiếp theo ta sẽ khai báo thêm các function để cho phép nguwoif chơi thao tác thông qua giao diện.
func dropShape() { if let shape = fallingShape { while detectIllegalPlacement() == false { shape.lowerShapeByOneRow() } shape.raiseShapeByOneRow() delegate?.gameShapeDidDrop(self) } }
Khi 1 hình rơi xuống, nó sẽ rơi từng bước 1 cho đến đáy khung hình. và hàm này giúp khối hình từ từ rơi xuống và nguwoif chơi sẽ chờ đợi đến khi nó rơi xuống đáy khung hình hoặc chạm vào các khối hình đã rơi xuống trước đó. CHú ý là tất cả các hàm này đều dùng toán tử điều kiện để check trước khi thực hiện nhằm đảm bảo các khối hình luôn di chuyển hợp lệ.
Tiếp theo ta khai báo function letShapeFall được gọi trong mỗi buwocs di chuyển của khối hình.Khi các khối hình rơi xuống theo từng hàng, nó sẽ liên tục check xem vị trí mới có hợp lệ ko, và game kết thúc khi nó ko tìm được vị trí tiếp theo hợp lệ để rơi.
func letShapeFall() { if let shape = fallingShape { shape.lowerShapeByOneRow() if detectIllegalPlacement() { shape.raiseShapeByOneRow() if detectIllegalPlacement() { endGame() } else { settleShape() } } else { delegate?.gameShapeDidMove(self) if detectTouch() { settleShape() } } } }
Bây giờ ta sẽ khai báo 1 function để cho phép người dùng có thể thao tác xoay các khối hình và xiay theo chiều kim đồng hồ. Nếu mỗi vị trí mới của khối hình vượt quá đường bao khung hình hoặc đè lên các khối đã rơi trước đó thì nó sẽ lập tức trở lại góc như trước khi xoay.
func rotateShape() { if let shape = fallingShape { shape.rotateClockwise() if detectIllegalPlacement() { shape.rotateCounterClockwise() } else { delegate?.gameShapeDidMove(self) } } }
Và 2 function dưới đây sẽ giúp người chơi thao tác và di chuyển khối hình sang trái hay sang phải
func moveShapeLeft() { if let shape = fallingShape { shape.shiftLeftByOneColumn() if detectIllegalPlacement() { shape.shiftRightByOneColumn() return } delegate?.gameShapeDidMove(self) } } func moveShapeRight() { if let shape = fallingShape { shape.shiftRightByOneColumn() if detectIllegalPlacement() { shape.shiftLeftByOneColumn() return } delegate?.gameShapeDidMove(self) } }
Chạy thử ứng dụng, ta sẽ thấy Xcode báo lỗi missing function. Bây giờ ta sẽ bổ sung những function này. Trước tiên, ta khai báo hàm settleShape() để tạo thêm khối block sẽ rơi và khi nó xuất hiện trong khung hình thì fallingShape sẽ bị reset và nó sẽ tạo 1 khối block mới khác chờ đến lượt và rơi.
func settleShape() { if let shape = fallingShape { for block in shape.blocks { blockArray[block.column, block.row] = block } fallingShape = nil delegate?.gameShapeDidLand(self) } }
Tiếp đó ta cần khai báo 1 function để thông báo mỗi khi 1 block kết thúc việc rơi xuống. Điều này xảy ra khi thỏa mãn 1 trong 2 điều kiện: nó rơi xuống tiếp xúc với 1 block khác hoặc nó rơi xuống đáy khung hình. Đoạn code dưới đây sẽ trả về true khi thoả mãn 1 trong 2 điều kiện trên
func detectTouch() -> Bool { if let shape = fallingShape { for bottomBlock in shape.bottomBlocks { if bottomBlock.row == NumRows - 1 || blockArray[bottomBlock.column, bottomBlock.row + 1] != nil { return true } } } return false }
Thiết lập ghi điểm
Có thể nói hầu hết các game đều quấn hút người chơi bởi các cơ chế ghi điểm riêng của nó Ta hãy cùng thực hiện việc tính điểm cho người chơi và nâng level. việc này cũng được thực hiện bởi lớp Swiftris.
Đầu tiên ta khai báo 2 biến để tính điểm và level cho người chơi. ở đây sẽ là mỗi hàng ta sẽ ghi đc 10 điểm và khi được 1000 điểm ta sẽ nâng level.
let PointsPerLine = 10 let LevelThreshold = 1000
Tiếp đó là cặp biến score và level để ghi lại điểm và level hiện tại của người chơi
var score:Int var level:Int
thêm vào bộ khởi tạo
init() { score = 0 level = 1 fallingShape = nil nextShape = nil blockArray = Array2D<Block>(columns: NumColumns, rows: NumRows) }
Bây giờ ta sẽ khai báo một hàm để có thể nhận biết được khi nào thì 1 hàng được xếp đầy và ghi điểm. Ta định nghĩa 1 hàm mà sẽ trả về kiểu tuple ( đã đề cập trong bài trước) và nó bao gồm 2 array: linesRemoved và fallenBlocks. Trong đó linesRemoved là số hàng đã được user lấp đầy.
func removeCompletedLines() -> (linesRemoved: Array<Array<Block>>, fallenBlocks: Array<Array<Block>>) { var removedLines = Array<Array<Block>>() for var row = NumRows - 1; row > 0; row-- { var rowOfBlocks = Array<Block>()
for column in 0..<NumColumns { if let block = blockArray[column, row] { rowOfBlocks.append(block) } } if rowOfBlocks.count == NumColumns { removedLines.append(rowOfBlocks) for block in rowOfBlocks { blockArray[block.column, block.row] = nil } }
tiếp đó, ta check xem mỗi hàng đã được lấp đầy hay chưa, nếu không có hàng nào bị lấp đầy thì sẽ trả về 1 mảng rỗng
if removedLines.count == 0 { return ([], []) }
Bây giờ ta thực hiện cộng điểm cho user dựa trên số hàng mà user lấp đầy được và dựa trên level của user hiện tại. Khi ghi được trên 1000 điểm thì level sẽ được cập nhật.
let pointsEarned = removedLines.count * PointsPerLine * level score += pointsEarned if score >= level * LevelThreshold { level += 1 delegate?.gameDidLevelUp(self) }
Về cơ bản thì logic của game đã gần hoàn thiện. Giờ ta sẽ thêm 1 function để xoá tất cả những block trên khung hình khi game kết thúc để chuẩn bị cho 1 game mới
func removeAllBlocks() -> Array<Array<Block>> { var allBlocks = Array<Array<Block>>() for row in 0..<NumRows { var rowOfBlocks = Array<Block>() for column in 0..<NumColumns { if let block = blockArray[column, row] { rowOfBlocks.append(block) blockArray[column, row] = nil } } allBlocks.append(rowOfBlocks) } return allBlocks }
Bây giờ ta hãy chạy thử và xem kết quả