В этом посте мы создадим простую игру с нуля. Попутно мы коснемся некоторых наиболее важных аспектов библиотеки SpriteKit.
Этот пост основан на том, что мы узнали ранее в серии Основ SpriteKit. Если вы хотите обновить свои знания SpriteKit, взгляните на некоторые другие мои посты.
Новый проект
Откройте Xcode и запустите новый проект из меню File > New > Project. Убедитесь, что выбрана iOS и выберите Game в качестве шаблона.
Дайте имя вашему проекту и убедитесь, что для Language установлено значение Swift, для Game Technology — SpriteKit , а для Devices — iPad .
Планирование игровых сцен
Первое, что мне нравится делать при создании проекта, это определить, сколько сцен мне понадобится для проекта. У меня обычно будет по крайней мере три сцены: вступительная сцена, сцена основной игры и сцена, показывающая рекорды и т. Д.
Для этого примера нам просто нужна вступительная и основная сцена игрового процесса, поскольку мы не будем отслеживать жизни, результаты и т. Д. SpriteKit уже поставляется с одной сценой при создании нового проекта, поэтому нам просто необходима вступительная сцена.
В меню XCode выберите « Файл» > « Создать» > « Файл» . Убедитесь, что iOS выбрана, и выберите Cocoa Touch Class.
Назовите класс StartGameScene и убедитесь, что для Subclass установлено значение SKScene, а для языка установлено значение Swift .
Настройка GameViewController
Откройте 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
.
Создание класса Player
В меню 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 от А до Я.