Bắt đầu với Sprite Kit

1. Sprite kit là gì

Sprite kit là một framework game 2D của Apple xây dựng trên iOS 7, hỗ trợ các hiệu ứng như video, filter, masking, thư viện vật lý tích hợp,...

So với các game engine khác, Sprite kit có những điểm mạnh và điểm yếu sau:

Điểm mạnh
  • Sprite kit được xây dựng bởi Apple và tích hợp vào hệ điều hành iOS, vì vậy chúng ta không cần sử dụng các thư viện ngoài của bên thứ 3, chúng ta sẽ được nhận tốt nhất support và update từ Apple
  • Sprite kit cho phép chúng ta làm những việc rất khó khăn hoặc không thể làm được trên các framework khác, ví dụ như việc coi video là 1 sprite, hay là việc sử dụng các hiệu ứng tuyệt vời của ảnh
Điểm yếu
  • Khi sử dụng Sprite kit, chúng ta chỉ có thể sử dụng trên hệ điều hành iOS. Điều này có nghĩa là chúng ta không thể mang code Sprite kit sử dụng trên hệ điều hành khác ví dụ như Android,...
  • Sprite kit vẫn còn là một framework mới với ít tính năng hơn so với các framework khác. Thiếu sót lớn nhất ở đây là trên Sprite kit không có khả năng viết custom OpenGL code

2. Sử dụng Sprite kit, cocos2D, cocos2D-X hay Unity

  • Nếu bạn chưa có kinh nghiệm làm game, bạn chỉ làm game trên iOS và không cần viết code OpenGL, sprite kit là lựa chọn lý tưởng: dễ học, dễ viết, không cần thêm thư viện
  • Nếu bạn cần viết code OpenGL, cocos2D có thể là lựa chọn cho bạn. cocos2D chỉ được viết trên iOS
  • Nếu bạn cần viết game với nhiều nền tảng hệ điều hành khác nhau, bạn cần sử dụng cocos2D-X hoặc Unity. cocos2D-X có thể là lựa chọn tốt hơn khi bạn tạo game 2D, còn đối với game 3D, Unity có thể là lựa chọn tốt hơn cho bạn

3. Ví dụ với Sprite kit

Tạo project game: Xcode->New->Project->iOS->Application->Game chọn next. Đặt tên cho project, ngôn ngữ code là Swift, game technology là SpriteKit, devices là iPhone->done Screen Shot 2015-03-24 at 8.10.14 AM.png

Do game trong ví dụ này chỉ chạy trên màn hình xoay ngang, nên chúng ta vào target->general và bỏ tích chọn portrait, chỉ để landscape left và landscape right

Tiếp theo, các bạn tìm và xóa file GameScene.sks trong project. Đây là file giúp chúng ta bố trí các sprite và các thành phần khác của scene, tuy nhiên game này của chúng ta chỉ là một game đơn giản và không cần đến file này

Build chạy thử project, app sẽ chạy như sau

Screen Shot 2015-03-24 at 8.53.47 AM.png

Để làm việc với project này, chúng ta cần resource tại đây (lưu ý là bài viết này được viết dựa trên tutorial trên trang raywenderlich.com và resource cũng được lấy trực tiếp từ link của trang web này, các bạn có thể vào trang web này để học theo tutorial gốc bằng tiếng Anh tại đây)

Sau khi download resource về máy, các bạn giải nén và chuyển các file ảnh và âm thanh vào trong project. Vậy là chúng ta đã có đủ resource để bắt đầu code, Let's begin.

Thêm GameScene vào root view

Đầu tiên, chúng ta mở file GameViewController.swift, xóa bỏ toàn bộ code do xcode tạo sẵn cho chúng ta và thay vào bằng code sau

import UIKit
import SpriteKit

class GameViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let scene = GameScene(size: view.bounds.size)
        let skView = view as SKView
        skView.showsFPS = true
        skView.showsNodeCount = true
        skView.ignoresSiblingOrder = true
        scene.scaleMode = .ResizeFill
        skView.presentScene(scene)
    }

    override func prefersStatusBarHidden() -> Bool {
        return true
    }
}

Đoạn code trên khá đơn giản, chúng ta config các thông số của skView, khởi tạo và thêm màn GameScene. Cần lưu ý ở đây root view là skView chứ không phải một UIView thông thường

Thêm sprite node player vào GameScene

Tiếp theo, chúng ta mở file GameScene.swift và thay thế code bằng đoạn code sau

class GameScene: SKScene {
    // Tạo biến constant property
    let player = SKSpriteNode(imageNamed: "player")

    override func didMoveToView(view: SKView) {
        // Set background cho GameScene
        backgroundColor = SKColor.whiteColor()
        // Set position của sprite node player
        player.position = CGPoint(x: size.width * 0.1, y: size.height * 0.5)
        // add sprite node player vào GameScene
        addChild(player)
    }
}

Build và chạy thử game, nếu các bước các bạn thực hiện chính xác, game sẽ chạy như hình sau

Screen Shot 2015-03-24 at 9.45.57 AM.png

Thêm monster vào GameScene

Trong file GameScene.swift, thêm các hàm như sau

func random() -> CGFloat {
    return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}

func random(#min: CGFloat, max: CGFloat) -> CGFloat {
    return random() * (max - min) + min
}

func addMonster() {
    // Khởi tạo sprite node monster
    let monster = SKSpriteNode(imageNamed: "monster")

    // Vị trí theo chiều Y của monster
    let actualY = random(min: monster.size.height/2, max: size.height - monster.size.height/2)

    // Vị trí của monster
    monster.position = CGPoint(x: size.width + monster.size.width/2, y: actualY)

    // Thêm monster vào GameScene
    addChild(monster)

    // Tốc độ của monster
    let actualDuration = random(min: CGFloat(2.0), max: CGFloat(4.0))

    // Tạo các action
    let actionMove = SKAction.moveTo(CGPoint(x: -monster.size.width/2, y: actualY), duration: NSTimeInterval(actualDuration))
    let actionMoveDone = SKAction.removeFromParent()
    // Gán action cho monster
    monster.runAction(SKAction.sequence([actionMove, actionMoveDone]))
}

Trong đoạn code trên, hàm AddMonster tạo một sprite node monster, gán cho monster một vận tốc.

  • SKAction.moveTo(_:duration:): di chuyển sprite node từ vị trí hiện tại đến vị trí chỉ định với duration truyền vào

  • SKAction.removeFromParent(): remove sprite node khỏi view cha

  • SKAction.sequence(_😃: hàm gọi chuỗi các action cho sprite node. Các action trong sequence sẽ được chạy lần lượt từng action, như trong đoạn code trên, sprite node monster sẽ thực hiện xong actionMove rồi chuyển sang actionMoveDone

Cuối cùng, để gọi hàm addMonster, chúng ta thêm đoạn code sau vào cuối hàm didMoveToView():

runAction(SKAction.repeatActionForever(
    SKAction.sequence([
        SKAction.runBlock(addMonster),
        SKAction.waitForDuration(1.0)
    ])
))

Đoạn code trên sẽ gọi hàm addMonster, chờ 1 giây rồi lại gọi hàm addMonster, vòng lặp là vô hạn

Build và chạy project, game của chúng ta sẽ chạy như hình sau

Screen Shot 2015-03-24 at 10.54.22 AM.png

Tạo đối tượng phi tiêu

Chúng ta đã tạo được sprite node player và các sprite node monster, bây giờ chúng ta sẽ tạo sprite node phi tiêu của player để tiêu diệt monster

Đầu tiên, chúng ta cần sử dụng Swift Operator Functions để viết lại các phép tính cộng, trừ, nhân, chia để sử dụng cho đối tượng phi tiêu. Các bạn thêm đoạn code sau vào trên cùng của GameScene.swift, bên ngoài GameScene class

func + (left: CGPoint, right: CGPoint) -> CGPoint {
    return CGPoint(x: left.x + right.x, y: left.y + right.y)
}

func - (left: CGPoint, right: CGPoint) -> CGPoint {
    return CGPoint(x: left.x - right.x, y: left.y - right.y)
}

func * (point: CGPoint, scalar: CGFloat) -> CGPoint {
    return CGPoint(x: point.x * scalar, y: point.y * scalar)
}

func / (point: CGPoint, scalar: CGFloat) -> CGPoint {
    return CGPoint(x: point.x / scalar, y: point.y / scalar)
}

if !(arch(x86_64) || arch(arm64))

func sqrt(a: CGFloat) -> CGFloat {
    return CGFloat(sqrtf(Float(a)))
}

endif

extension CGPoint {
    func length() -> CGFloat {
        return sqrt(x*x + y*y)
    }

    func normalized() -> CGPoint {
        return self / length()
    }
}

Trong đoạn code trên, chúng ta overload các phép toán cộng, trừ, nhân, chia, căn bậc 2 và thêm 2 hàm vào class CGPoint extension

Tiếp theo, chúng ta thêm hàm sau vào cuối class GameScene

override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
    // 1 - Xác định vị trí người dùng chạm vào màn hình
    let touch = touches.anyObject() as UITouch
    let touchLocation = touch.locationInNode(self)

    // 2 - Tạo và xác định vị trí của đối tượng phi tiêu
    let projectile = SKSpriteNode(imageNamed: "projectile")
    projectile.position = player.position

    // 3 - Xác định khoảng cách các chiều X, Y của vị trí người dùng chạm và vị trí phi tiêu
    let offset = touchLocation - projectile.position

    // 4 - Không cho phi tiêu bay ngược ra sau player
    if (offset.x < 0) { return }

    // 5 - thêm đối tượng phi tiêu vào GameScene
    addChild(projectile)

    // 6 - Xác định hướng bay của phi tiêu
    let direction = offset.normalized()

    // 7 - x1000 Để đảm bảo phi tiêu bay hết màn hình
    let shootAmount = direction * 1000

    // 8 - Tạo điểm đến cho phi tiêu
    let realDest = shootAmount + projectile.position

    // 9 - tạo và gán các action cho phi tiêu
    let actionMove = SKAction.moveTo(realDest, duration: 2.0)
    let actionMoveDone = SKAction.removeFromParent()
    projectile.runAction(SKAction.sequence([actionMove, actionMoveDone]))
}

Build và chạy thử, game của chúng ta đã có thêm phi tiêu bay ra mỗi khi chúng ta chạm vào màn hình

Untitled.png

Làm phi tiêu hoạt động

Sau khi tạo đối tượng phi tiêu ở bước trên, khi chạy thử project, chúng ta thấy phi tiêu đã được bắn ra, nhưng khi chúng ta bắn monster, phi tiêu và monster đi xuyên qua nhau mà không chạm nhau. bây giờ chúng ta sẽ code để phi tiêu và monster chạm nhau, player có thể tiêu diệt monster

Trong SpriteKit framework, Apple đã tích hợp sẵn cho chúng ta engine vật lý, chúng ta có thể sử dụng engine này để phát hiện va chạm giữa phi tiêu và monster

Đầu tiên, thêm đoạn code sau vào đầu file GameScene.swift, bên ngoài class GameScene

struct PhysicsCategory {
    static let None      : UInt32 = 0
    static let All       : UInt32 = UInt32.max
    static let Monster   : UInt32 = 0b1
    static let Projectile: UInt32 = 0b10
}

Ở đây, chúng ta tạo 1 struct để tạo constant category cho monster và phi tiêu

Tiếp theo, chúng ta bổ xung SKPhysicsContactDelegate protocol cho GameScene

class GameScene: SKScene, SKPhysicsContactDelegate {

Tiếp đó, chúng ta thêm đoạn code sau vào hàm didMoveToView(_😃, sau đoạn code thêm player

physicsWorld.gravity = CGVectorMake(0, 0)
physicsWorld.contactDelegate = self

Trong đoạn code trên, chúng ta config không trọng lực cho physicsWorld và gán contactDelegate của physicsWorld để bắt và sử lý sự kiện khi phi tiêu va chạm với monster

Tiếp theo, chúng ta thêm đoạn code sau vào hàm addMonster, ngay sau đoạn code thêm monster vào GameScene

monster.physicsBody = SKPhysicsBody(rectangleOfSize: monster.size) // 1
monster.physicsBody?.dynamic = true // 2
monster.physicsBody?.categoryBitMask = PhysicsCategory.Monster // 3
monster.physicsBody?.contactTestBitMask = PhysicsCategory.Projectile // 4
monster.physicsBody?.collisionBitMask = PhysicsCategory.None // 5

Chúng ta sẽ lần lượt nói qua ý nghĩa của từng dòng code

  • 1 Chúng ta thêm physicsBody property cho sprite node monster để physicsWorld quản lý và xác định va chạm sau này
  • 2 Set dynamic cho monster để engine vật lý không quản lý sự di chuyển của monster. Trong game của chúng ta, chúng ta cho monster di chuyển ngang bằng hàm moveTo
  • 3 chúng ta set categoryBitMask cho monster bằng category chúng ta đã tạo bên trên
  • 4 Set contactTestBitMask cho monster là constant của phi tiêu chúng ta đã tạo trong category ở trên
  • 5 Set collisionBitMask là none cho monster để khi monster và phi tiêu va chạm với nhau, engine vật lý sẽ không xử lý sau va chạm

Vậy là chúng ta đã set physicsBody cho monster, tương tự, chúng ta sẽ set physicsBody cho phi tiêu. Các bạn thêm đoạn code sau vào hàm touchesEnded, ngay sau đoạn code gán position cho phi tiêu:

projectile.physicsBody = SKPhysicsBody(circleOfRadius: projectile.size.width/2)
projectile.physicsBody?.dynamic = true
projectile.physicsBody?.categoryBitMask = PhysicsCategory.Projectile
projectile.physicsBody?.contactTestBitMask = PhysicsCategory.Monster
projectile.physicsBody?.collisionBitMask = PhysicsCategory.None
projectile.physicsBody?.usesPreciseCollisionDetection = true

Trong đoạn code trên, chúng ta set thuộc tính usesPreciseCollisionDetection cho phi tiêu là true. Đối với các Sprite node di chuyển với vận tốc lớn, chúng ta nên set thuộc tính này để việc xác định va chạm được chính xác

OK, vậy là xong với physicsWorld, physicsBody để làm việc với physic engine. Bây giờ chúng ta chuyển sang bước xử lý sau khi va chạm giữa phi tiêu và monster sảy ra. thêm đoạn code sau vào cuối class GameScene

func projectileDidCollideWithMonster(projectile:SKSpriteNode, monster:SKSpriteNode) {
    projectile.removeFromParent()
    monster.removeFromParent()
}

func didBeginContact(contact: SKPhysicsContact) {
    // 1
    var firstBody: SKPhysicsBody
    var secondBody: SKPhysicsBody
    if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
        firstBody = contact.bodyA
        secondBody = contact.bodyB
    } else {
        firstBody = contact.bodyB
        secondBody = contact.bodyA
    }

    // 2
    if ((firstBody.categoryBitMask & PhysicsCategory.Monster != 0) &&
        (secondBody.categoryBitMask & PhysicsCategory.Projectile != 0)) {
            projectileDidCollideWithMonster(firstBody.node as SKSpriteNode, monster: secondBody.node as SKSpriteNode)
    }
}

Hàm didBeginContact() là hàm của SKPhysicsContactDelegate, mỗi khi phi tiêu và monster va chạm với nhau, hàm này sẽ tự động được gọi đến. Đoạn code trên cũng khá đơn giản

  • 1 Dùng hàm if để check và gán firstBody là monster, secondBody là phi tiêu
  • 2 Kiểm tra xem 2 vật va chạm với nhau có đúng là phi tiêu và monster không, nếu đúng thì gọi hàm để xóa monster và phi tiêu ra khỏi GameScene

Build và chạy game, bắn thử monster nào. Nếu không có sai sót gì thì player đã có khả năng bắn hạ monster rồi đấy.

Thêm âm thanh cho game

SpriteKit không hỗ trợ engine âm thanh, vì thế để thêm âm thanh vào game, chúng ta sẽ dùng AVFoundation framework

Đầu tiên, chúng ta sẽ thêm background music, thêm đoạn code sau vào đầu file GameScene.swift, ngay sau đoạn code import thư viện:

import AVFoundation

var backgroundMusicPlayer: AVAudioPlayer!

func playBackgroundMusic(filename: String) {
    let url = NSBundle.mainBundle().URLForResource(
    filename, withExtension: nil)
    if (url == nil) {
        println("Could not find file: \(filename)")
        return
    }

    var error: NSError? = nil
    backgroundMusicPlayer = AVAudioPlayer(contentsOfURL: url, error: &error)
    if backgroundMusicPlayer == nil {
        println("Could not create audio player: \(error!)")
        return
    }

    backgroundMusicPlayer.numberOfLoops = -1
    backgroundMusicPlayer.prepareToPlay()
    backgroundMusicPlayer.play()
}

Đoạn code trên khá là đơn giản đối với những bạn đã quen thuộc với iOS, chúng ta tạo hàm playBackgroundMusic để lấy file nhạc ra và chạy file nhạc với vòng lặp vô hạn

Tiếp theo, chúng ta thêm đoạn code sau vào đầu hàm didMoveToView của class GameScene để gọi hàm playBackgroundMusic chúng ta vừa viết

playBackgroundMusic("background-music-aac.caf")

background-music-aac.caf là tên file nhạc chúng ta đã download từ link resource và thêm vào project

Vậy là chúng ta đã hoàn thành việc thêm background music. bây giờ chúng ta sẽ thêm âm thanh mỗi khi chúng ta bắn phi tiêu. Thêm đoạn code sau vào đầu hàm touchesEnded

runAction(SKAction.playSoundFileNamed("pew-pew-lei.caf", waitForCompletion: false))

Build và chạy game, vậy là chúng ta đã có background music cho game và âm thanh mỗi khi phi tiêu được bắn ra. Lưu ý, nếu đã thêm file nhạc vào project nhưng gặp phải lỗi không tìm thấy file nhạc, các bạn hãy thử xem target membership của file nhạc đã được chọn ở trong project của chúng ta hay chưa.

Luật chơi cho game

Đến thời điểm hiện tại, game của chúng ta đã khá là ổn, tuy nhiên game của chúng ta vẫn chưa có luật chơi, chưa xác định khi nào thì người chơi thắng hay thua. Tại bước này, chúng ta sẽ đặt luật chơi cho game, luật chơi khá đơn giản như sau

  • nếu người chơi để bất kỳ 1 monster nào chạy qua, người chơi sẽ bị tính thua
  • nếu người chơi hạ gục đủ 1 số lượng monster, người chơi được tính là thắng

OK, luật chơi đã có, chúng ta bắt đầu code nào

Đầu tiên, chúng ta tạo thêm màn để hiển thị thông báo thắng hoặc thua: New->file->iOS->source->Swift file, đặt tên file là GameOverScene, bấm create để tạo file mới

Thêm đoạn code sau vào file GameOverScene.swift mới tạo:

import Foundation
import SpriteKit

class GameOverScene: SKScene {

    init(size: CGSize, won:Bool) {

        super.init(size: size)
        // Set màu cho background
        backgroundColor = SKColor.whiteColor()
        // Set nội dung cho message màn là thắng hoặc thua dựa vào kết quả của người chơi
        var message = won ? "You Won!" : "You Lose!"
        // config chữ, font và màu chữ cho message thông báo
        let label = SKLabelNode(fontNamed: "Chalkduster")
        label.text = message
        label.fontSize = 40
        label.fontColor = SKColor.blackColor()
        label.position = CGPoint(x: size.width/2, y: size.height/2)
        addChild(label)
        // 1
        runAction(SKAction.sequence([ SKAction.waitForDuration(3.0), SKAction.runBlock() {
            // 2
            let reveal = SKTransition.flipHorizontalWithDuration(0.5)
            let scene = GameScene(size: size)
            self.view?.presentScene(scene, transition:reveal)
        }
        ]))
    }

    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
  • 1 Ở đây chúng ta thực hiện các SKAction lần lượt bằng hàm sequence(giống với đoạn code cho monster bên trên)
  • 2 Trong SpriteKit, chúng ta dùng hàm presentScene để chuyển sang scene mới. Trong game của chúng ta, sau khi thông báo thắng/thua được hiện ra, người chơi sẽ được quay lại màn chơi game để chơi tiếp màn mới

Chúng ta đã tạo xong màn thông báo thắng/thua, bây giờ chúng ta sẽ thêm code trong GameScene để gọi đến màn thông báo vừa tạo

Đầu tiên, để xác định người chơi thua, trong hàm addMonster của GameScene, thay thế dòng code cuối cùng:

monster.runAction(SKAction.sequence([actionMove, actionMoveDone]))

bằng đoạn code sau:

let loseAction = SKAction.runBlock() {
    let reveal = SKTransition.flipHorizontalWithDuration(0.5)
    let gameOverScene = GameOverScene(size: self.size, won: false)
    self.view?.presentScene(gameOverScene, transition: reveal)
}
monster.runAction(SKAction.sequence([actionMove, loseAction, actionMoveDone]))

Nếu các bạn theo dõi từ đầu bài viết đến giờ thì đoạn code trên không có gì là khó hiểu, khi monster thực hiện xong actionMove tức là monster đã chạy được đến cuối màn hình, lúc này người chơi bị xử thua, thực hiện loseAction và cuối cùng là actionMoveDone

Vậy là xong đoạn code xử thua, bây giờ chúng ta code xử thắng. Đầu tiên, thêm dòng code sau để đếm số monster đã bắn hạ, ngay dưới dòng code khai báo player

var monstersDestroyed = 0

Tiếp theo, trong hàm projectileDidCollideWithMonster, thêm đoạn code sau vào cuối hàm:

monstersDestroyed++
if (monstersDestroyed >= 30) {
    let reveal = SKTransition.flipHorizontalWithDuration(0.5)
    let gameOverScene = GameOverScene(size: self.size, won: true)
    self.view?.presentScene(gameOverScene, transition: reveal)
}

Mỗi khi phi tiêu bắn trúng monster, chúng ta tăng monstersDestroyed lên 1, đến khi số lượng monster bắn hạ đạt 30 con thì người chơi được xác định là thắng, chuyển sang màn GameOverScene và hiện thông báo chiến thắng

Done, build và chơi thử game, mọi thứ đã hoàn thành, let's enjoy our game

Kết luận

Qua bài viết, người viết giới thiệu đến các bạn về SpriteKit framework và cách xây dựng một game đơn giản bằng framework này

Người viết bài cũng chỉ mới tìm hiểu về SpriteKit, trong bài viết có sai sót gì mong bạn đọc bỏ qua và góp ý cho người viết sửa chữa để nâng cao chất lượng bài viết

Bài viết này được viết dựa theo tutorial trên trang web http://www.raywenderlich.com, bạn đọc có thể thao khảo tại đây

Cuối cùng, người viết xin cảm ơn các bạn đã dành thời gian đọc bài viết này!


All Rights Reserved