Статьи

Основы SpriteKit: все вместе

Конечный продукт
Что вы будете создавать

В этом посте мы создадим простую игру с нуля. Попутно мы коснемся некоторых наиболее важных аспектов библиотеки SpriteKit.

Этот пост основан на том, что мы узнали ранее в серии Основ SpriteKit. Если вы хотите обновить свои знания SpriteKit, взгляните на некоторые другие мои посты.

Откройте Xcode и запустите новый проект из меню File > New > Project. Убедитесь, что выбрана iOS и выберите Game в качестве шаблона.

new project

Дайте имя вашему проекту и убедитесь, что для Language установлено значение Swift, для Game TechnologySpriteKit , а для DevicesiPad .

варианты проекта

Первое, что мне нравится делать при создании проекта, это определить, сколько сцен мне понадобится для проекта. У меня обычно будет по крайней мере три сцены: вступительная сцена, сцена основной игры и сцена, показывающая рекорды и т. Д.

Для этого примера нам просто нужна вступительная и основная сцена игрового процесса, поскольку мы не будем отслеживать жизни, результаты и т. Д. SpriteKit уже поставляется с одной сценой при создании нового проекта, поэтому нам просто необходима вступительная сцена.

В меню XCode выберите « Файл» > « Создать» > « Файл» . Убедитесь, что iOS выбрана, и выберите Cocoa Touch Class.

новый класс касания какао

Назовите класс StartGameScene и убедитесь, что для Subclass установлено значение SKScene, а для языка установлено значение Swift .

класс начальной игры

Откройте GameViewController.swift . Удалите все в этом файле и замените его следующим.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
import UIKit
import SpriteKit
import GameplayKit
 
class GameViewController: UIViewController {
 
    override func viewDidLoad() {
        super.viewDidLoad()
         
        let scene = StartGameScene(size: view.bounds.size)
        let skView = self.view as!
        skView.showsFPS = false
        skView.showsNodeCount = false
        skView.ignoresSiblingOrder = false
        scene.scaleMode = .aspectFill
        skView.presentScene(scene)
    }
 
    override var prefersStatusBarHidden: Bool {
        return true
    }
}

Когда вы создаете новый проект, GameViewController.swift настроен для загрузки GameScene.sks с диска. GameScene.sks используется вместе со встроенным редактором сцен SpriteKit, который позволяет визуально планировать свои проекты. Мы не будем использовать GameScene.sks, а вместо этого создадим все из кода, поэтому здесь мы инициируем новый экземпляр StartGameScene и представляем его.

Добавьте следующее во вновь созданный StartGameScene.swift .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import UIKit
import SpriteKit
 
class StartGameScene: SKScene {
 
    override func didMove(to view: SKView){
        scene?.backgroundColor = .blue
        let logo = SKSpriteNode(imageNamed: «bigplane»)
        logo.position = CGPoint(x: size.width/2, y: size.height/2)
        addChild(logo)
         
        let newGameBtn = SKSpriteNode(imageNamed: «newgamebutton»)
        newGameBtn.position = CGPoint(x: size.width/2, y: size.height/2 — 350)
        newGameBtn.name = «newgame»
        addChild(newGameBtn)
    }
     
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else {
            return
        }
         
        let touchLocation = touch.location(in: self)
        let touchedNode = self.atPoint(touchLocation)
         
        if(touchedNode.name == «newgame»){
            let newScene = GameScene(size: size)
            newScene.scaleMode = scaleMode
            view?.presentScene(newScene)
        }
    }}

Эта сцена довольно проста. В методе didMove мы добавляем логотип и кнопку. Затем в touchesBegan мы обнаруживаем прикосновения к новой кнопке игры и отвечаем, загружая основную сцену GameScene .

Следующее, что мне нравится делать при создании новой игры, это решить, какие классы мне понадобятся. Я могу сразу сказать, что мне понадобится класс Player класс Enemy . Оба эти класса будут расширять SKSpriteNode . Я думаю, что для этого проекта мы просто создадим пули игрока и врага прямо из их соответствующих классов. Если хотите, вы можете создавать отдельные классы пули игрока и вражеской пули, и я предлагаю вам попробовать сделать это самостоятельно.

Наконец, есть острова. Они не имеют каких-либо конкретных функций, кроме как перемещаться вниз по экрану. В этом случае, поскольку они просто украшения, я думаю, что не стоит создавать класс, а просто создавать их в основной GameScene .

В меню XCode выберите « Файл» > « Создать» > « Файл». Убедитесь, что выбрана iOS и выберите Cocoa Touch Class .

новый класс касания какао

Убедитесь, что для Class задано Player , для Subclass of : SKSpriteNode и для Language установлен Swift .

класс игрока

Теперь добавьте следующее в Player.swift .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import UIKit
import SpriteKit
 
class Player: SKSpriteNode {
    private var canFire = true
    private var invincible = false
    private var lives:Int = 3 {
        didSet {
            if(lives < 0){
                kill()
            }else{
                respawn()
            }
        }
    }
    init() {
        let texture = SKTexture(imageNamed: «player»)
        super.init(texture: texture, color: .clear, size: texture.size())
        self.physicsBody = SKPhysicsBody(texture: self.texture!,size:self.size)
        self.physicsBody?.isDynamic = true
        self.physicsBody?.categoryBitMask = PhysicsCategories.Player
        self.physicsBody?.contactTestBitMask = PhysicsCategories.Enemy |
        self.physicsBody?.collisionBitMask = PhysicsCategories.EdgeBody
        self.physicsBody?.allowsRotation = false
        generateBullets()
    }
     
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    func die (){
        if(invincible == false){
            lives -= 1
        }
    }
         
    func kill(){
        let newScene = StartGameScene(size: self.scene!.size)
        newScene.scaleMode = self.scene!.scaleMode
        let doorsClose = SKTransition.doorsCloseVertical(withDuration: 2.0)
        self.scene!.view?.presentScene(newScene, transition: doorsClose)
    }
     
    func respawn(){
        invincible = true
        let fadeOutAction = SKAction.fadeOut(withDuration: 0.4)
        let fadeInAction = SKAction.fadeIn(withDuration: 0.4)
        let fadeOutIn = SKAction.sequence([fadeOutAction,fadeInAction])
        let fadeOutInAction = SKAction.repeat(fadeOutIn, count: 5)
        let setInvicibleFalse = SKAction.run {
            self.invincible = false
        }
        run(SKAction.sequence([fadeOutInAction,setInvicibleFalse]))
    }
     
    func generateBullets(){
        let fireBulletAction = SKAction.run{ [weak self] in
            self?.fireBullet()
        }
        let waitToFire = SKAction.wait(forDuration: 0.8)
        let fireBulletSequence = SKAction.sequence([fireBulletAction,waitToFire])
        let fire = SKAction.repeatForever(fireBulletSequence)
        run(fire)
    }
     
    func fireBullet(){
        let bullet = SKSpriteNode(imageNamed: «bullet»)
        bullet.position.x = self.position.x
        bullet.position.y = self.position.y + self.size.height/2
        bullet.physicsBody = SKPhysicsBody(rectangleOf: bullet.size)
        bullet.physicsBody?.categoryBitMask = PhysicsCategories.PlayerBullet
        bullet.physicsBody?.allowsRotation = false
        scene?.addChild(bullet)
        let moveBulletAction = SKAction.move(to: CGPoint(x:self.position.x,y:(scene?.size.height)! + bullet.size.height), duration: 1.0)
        let removeBulletAction = SKAction.removeFromParent()
        bullet.run(SKAction.sequence([moveBulletAction,removeBulletAction]))
    }
}

В методе init() мы устанавливаем physicsBody и вызываем generateBullets() . Метод generateBullets неоднократно вызывает fireBullet() , который создает пулю, устанавливает ее physicsBody и перемещает ее вниз по экрану.

Когда игрок теряет жизнь, вызывается метод respawn() . В рамках метода respawn мы пять раз respawn и исчезаем с самолета, за это время игрок будет непобедим. Когда игрок исчерпал все жизни, вызывается метод kill() . Метод kill просто загружает StartGameScene .

Выберите File > New > File из меню Xcode. Убедитесь, что выбрана iOS и выберите Cocoa Touch Class .

новый класс касания какао

Убедитесь, что для Class установлен Enemy , для Subclass of : SKSpriteNode и для Language установлен Swift .

Добавьте следующее в Enemy.swift .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import UIKit
import SpriteKit
 
class Enemy: SKSpriteNode {
 
    init() {
        let texture = SKTexture(imageNamed: «enemy1»)
        super.init(texture: texture, color: .clear, size: texture.size())
        self.name = «enemy»
        self.physicsBody = SKPhysicsBody(texture: self.texture!, size: self.size)
        self.physicsBody?.isDynamic = true
        self.physicsBody?.categoryBitMask = PhysicsCategories.Enemy
        self.physicsBody?.contactTestBitMask = PhysicsCategories.Player |
        self.physicsBody?.allowsRotation = false
        move()
        generateBullets()
    }
     
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
     
    func fireBullet(){
        let bullet = SKSpriteNode(imageNamed: «bullet»)
        bullet.position.x = self.position.x
        bullet.position.y = self.position.y — bullet.size.height * 2
        bullet.physicsBody = SKPhysicsBody(rectangleOf: bullet.size)
        bullet.physicsBody?.categoryBitMask = PhysicsCategories.EnemyBullet
        bullet.physicsBody?.allowsRotation = false
        scene?.addChild(bullet)
        let moveBulletAction = SKAction.move(to: CGPoint(x:self.position.x,y: 0 — bullet.size.height), duration: 2.0)
        let removeBulletAction = SKAction.removeFromParent()
        bullet.run(SKAction.sequence([moveBulletAction,removeBulletAction])
        )
    }
     
    func move(){
        let moveEnemyAction = SKAction.moveTo(y: 0 — self.size.height, duration: 12.0)
        let removeEnemyAction = SKAction.removeFromParent()
        let moveEnemySequence = SKAction.sequence([moveEnemyAction, removeEnemyAction])
        run(moveEnemySequence)
    }
     
    func generateBullets(){
        let fireBulletAction = SKAction.run{ [weak self] in
            self?.fireBullet()
        }
        let waitToFire = SKAction.wait(forDuration: 1.5)
        let fireBulletSequence = SKAction.sequence([fireBulletAction,waitToFire])
        let fire = SKAction.repeatForever(fireBulletSequence)
        run(fire)
    }
}

Этот класс очень похож на класс Player . Мы устанавливаем его physicsBody и вызываем generateBullets() . move() просто перемещает противника вниз по экрану.

Удалите все внутри GameScene.swift и добавьте следующее.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import SpriteKit
import GameplayKit
import CoreMotion
 
class GameScene: SKScene, SKPhysicsContactDelegate {
    let player = Player()
    let motionManager = CMMotionManager()
    var accelerationX: CGFloat = 0.0
     
    override func didMove(to view: SKView) {
        physicsWorld.gravity = CGVector(dx:0.0, dy:0.0)
        self.physicsWorld.contactDelegate = self
        scene?.backgroundColor = .blue
        physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
        physicsBody?.categoryBitMask = PhysicsCategories.EdgeBody
        player.position = CGPoint(x: size.width/2, y: player.size.height)
        addChild(player)
        setupAccelerometer()
        addEnemies()
        generateIslands()
    }
         
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    }
     
    func addEnemies(){
        let generateEnemyAction = SKAction.run{ [weak self] in
            self?.generateEnemy()
        }
        let waitToGenerateEnemy = SKAction.wait(forDuration: 3.0)
        let generateEnemySequence = SKAction.sequence([generateEnemyAction,waitToGenerateEnemy])
        run(SKAction.repeatForever(generateEnemySequence))
    }
     
    func generateEnemy(){
        let enemy = Enemy()
        addChild(enemy)
        enemy.position = CGPoint(x: CGFloat(arc4random_uniform(UInt32(size.width — enemy.size.width))), y: size.height — enemy.size.height)
    }
     
    func didBegin(_ contact: SKPhysicsContact) {
        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
        }
         
        if((firstBody.categoryBitMask & PhysicsCategories.Player != 0) && (secondBody.categoryBitMask & PhysicsCategories.Enemy != 0)){
            player.die()
            secondBody.node?.removeFromParent()
            createExplosion(position: player.position)
        }
                 
        if((firstBody.categoryBitMask & PhysicsCategories.Player != 0) && (secondBody.categoryBitMask & PhysicsCategories.EnemyBullet != 0)){
            player.die()
            secondBody.node?.removeFromParent()
        }
        if((firstBody.categoryBitMask & PhysicsCategories.Enemy != 0) && (secondBody.categoryBitMask & PhysicsCategories.PlayerBullet != 0)){
            if(firstBody.node != nil){
                createExplosion(position: (firstBody.node?.position)!)
            }
            firstBody.node?.removeFromParent()
            secondBody.node?.removeFromParent()
        }
    }
     
    func createExplosion(position: CGPoint){
        let explosion = SKSpriteNode(imageNamed: «explosion1»)
        explosion.position = position
        addChild(explosion)
        var explosionTextures:[SKTexture] = []
         
        for i in 1…6 {
            explosionTextures.append(SKTexture(imageNamed: «explosion\(i)»))
        }
         
        let explosionAnimation = SKAction.animate(with: explosionTextures,
                                                  timePerFrame: 0.3)
        explosion.run(SKAction.sequence([explosionAnimation, SKAction.removeFromParent()]))
    }
     
    func createIsland() {
        let island = SKSpriteNode(imageNamed: «island1»)
 
        island.position = CGPoint(x: CGFloat(arc4random_uniform(UInt32(size.width — island.size.width))), y: size.height — island.size.height — 50)
        island.zPosition = -1
        addChild(island)
        let moveAction = SKAction.moveTo(y: 0 — island.size.height, duration: 15)
        island.run(SKAction.sequence([moveAction, SKAction.removeFromParent()]))
    }
     
    func generateIslands(){
        let generateIslandAction = SKAction.run { [weak self] in
            self?.createIsland()
        }
        let waitToGenerateIslandAction = SKAction.wait(forDuration: 9)
        run(SKAction.repeatForever(SKAction.sequence([generateIslandAction, waitToGenerateIslandAction])))
    }
         
    func setupAccelerometer(){
        motionManager.accelerometerUpdateInterval = 0.2
        motionManager.startAccelerometerUpdates(to: OperationQueue(), withHandler: { accelerometerData, error in
        guard let accelerometerData = accelerometerData else {
            return
        }
            let acceleration = accelerometerData.acceleration
            self.accelerationX = CGFloat(acceleration.x)
        })
    }
     
    override func didSimulatePhysics() {
        player.physicsBody?.velocity = CGVector(dx: accelerationX * 600, dy: 0)
    }
}

Мы создаем экземпляр Player и экземпляр CMMotionManager . Мы используем акселерометр для перемещения игрока в этой игре.

В didMove(to:) мы отключаем гравитацию, настраиваем contactDelegate , добавляем контур края и устанавливаем положение player перед добавлением его в сцену. Затем мы вызываем setupAccelerometer() , который устанавливает акселерометр, и вызываем addEnemies() и generateIslands() .

Метод addEnemies() неоднократно вызывает метод generateEnemy() , который создаст экземпляр Enemy и добавит его в сцену.

Метод generateIslands() работает аналогично addEnemies() в том, что он неоднократно вызывает createIsland() который создает SKSpriteNode и добавляет его в сцену. В createIsland() мы также создаем SKAction который перемещает остров вниз по сцене.

В рамках didBegin(_:) мы проверяем, какие узлы вступают в контакт, и отвечаем, удаляя соответствующий узел со сцены и вызывая player.die() при необходимости. Метод createExplosion() создает анимацию взрыва и добавляет ее на сцену. Как только взрыв закончен, он удаляется со сцены.

В ходе этой серии мы изучили некоторые из наиболее важных концепций, используемых почти во всех играх SpriteKit. Мы завершили серию, показав, как просто запустить и запустить основную игру. Есть еще некоторые улучшения, которые могут быть сделаны, такие как HUB, высокие оценки и звуки (я включил пару файлов MP3, которые вы можете использовать для этого в репо). Я надеюсь, что вы узнали что-то полезное в этой серии, и спасибо за чтение!

Если вы хотите узнать больше о программировании игр с помощью SpriteKit, ознакомьтесь с одним из наших подробных видеокурсов! Вы узнаете, как построить игру SpriteKit от А до Я.

  • стриж
    Запрограммируйте игру с боковой прокруткой с помощью Swift и SpriteKit
    Дерек Дженсен
  • Разработка игр
    Разработка игр со Swift и SpriteKit
    Дерек Дженсен