I am participating in the individual competition of the Nuggets Community Game Creativity Contest. For details, please see: Game Creativity Contest
foreword
I remember playing a very fun mobile game called Thunder Fighter many years ago, and it was crazy at that time. So today we use SpriteKit
to implement a simple version of the Thunder fighter
Install
1. It iOS
's a bit uncomfortable to install on the side. It's not like giving one directly to Android . If I put one apk
on my side TestFlight
, you can download an TestFlight
APP and use it to TestFlight
install it .
Use WeChat to open this address and follow the prompts to install:
It looks like this when opened:
2. You can run the code to install, the demo address is here
how to play
1. This game is a level break game. Players drag the plane to destroy the enemy to get points
. 2. At the beginning, the player has 3 lives, and each level will randomly drop a blood pack and increase the attack power of the plane. The enemy's health also increases with the level of the level, each level will have a boss, defeating the boss will get a lot of points
Let's start to implement it step by step.
accomplish
1. Create a new project and create a PlanScene
scene in the project
1.1 didMove(to view: SKView)
. Set the gravitational acceleration to 0,0 in the method
override func didMove(to view: SKView) {
super.didMove(to: view)
//设置重力加速度
physicsWorld.gravity = .zero
}
复制代码
2. Background loop scrolling
2.1. To make the background scroll circularly, we need to create two identical background sprites, bgNode1
set position
to , CGPoint(x: 0, y: 0)
set tobgNode2
position
CGPoint(x: 0, y: size.height)
private lazy var bgNode1: SKSpriteNode = {
let view = SKSpriteNode(imageNamed: "plan_bg")
view.position = CGPoint(x: 0, y: 0)
view.size = size
view.anchorPoint = CGPoint(x: 0, y: 0)
view.zPosition = 0
view.name = "bgNode"
return view
}()
private lazy var bgNode2: SKSpriteNode = {
let view = SKSpriteNode(imageNamed: "plan_bg")
view.position = CGPoint(x: 0, y: size.height)
view.size = size
view.anchorPoint = CGPoint(x: 0, y: 0)
view.zPosition = 0
view.name = "bgNode"
return view
}()
复制代码
2.2, override func update(_ currentTime: TimeInterval)
reset the bgNode1
sum in bgNode2
the methodposition
override func update(_ currentTime: TimeInterval) {
super.update(currentTime)
backgroudScrollUpdate()
}
private func backgroudScrollUpdate() {
bgNode1.position = CGPoint(x: bgNode1.position.x, y: bgNode1.position.y - 4)
bgNode2.position = CGPoint(x: bgNode2.position.x, y: bgNode2.position.y - 4)
if bgNode1.position.y <= -size.height {
bgNode1.position = CGPoint(x: 0, y: 0)
bgNode2.position = CGPoint(x: 0, y: size.height)
}
}
复制代码
This will allow the background to scroll infinitely.
3. Add plane and drag plane to move
3.1、如果想要拖动飞机,要给当前场景的view
添加pan
手势,因为SKSpriteNode
是不能添加手势的,所以,我们要把手势添加到当前场景的view
上,然后在touchesBegan
方法里判断当前触摸的是不是飞机,所以我们先定义两个变量
// 触摸的是否是飞机
private var isTouchPlan: Bool = false
// 当前飞机的point
private var planPoint: CGPoint = .zero
复制代码
3.2、创建飞机精灵
// 飞机
private lazy var planNode: SKSpriteNode = {
let view = SKSpriteNode(imageNamed: "plan02")
view.position = CGPoint(x: size.width / 2, y: size.height / 2)
view.anchorPoint = CGPoint(x: 0.5, y: 0.5)
view.size = CGSize(width: 70, height: 54)
view.zPosition = 2
view.name = "plan"
view.physicsBody = SKPhysicsBody(rectangleOf: view.size)
//物理体是否受力
view.physicsBody?.isDynamic = false
//设置物理体的标识符
view.physicsBody?.categoryBitMask = 1
//设置可与哪一类的物理体发生碰撞
view.physicsBody?.contactTestBitMask = 2
return view
}()
复制代码
3.3、在touchesBegan
方法里面判断触摸的是不是飞机
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
isTouchPlan = false
guard let touch = (touches as NSSet).anyObject() as? UITouch else { return }
let point = touch.location(in: self)
let node = atPoint(point)
switch node.name {
case "plan":
// 如果点击的是飞机,则将isTouchPlan置为true
isTouchPlan = true
default:
break
}
}
复制代码
3.4、给当前场景添加一个pan
手势来实现飞机拖动
private func addPanGestureRecognizer(_ view: SKView) {
let pan = UIPanGestureRecognizer(target: self, action: #selector(panAction(_:)))
view.addGestureRecognizer(pan)
}
@objc private func panAction(_ sender: UIPanGestureRecognizer) {
if isTouchPlan {
var position = sender.location(in: sender.view)
// 因为SpriteKit的坐标原点在左下角,所以需要转换一下
position = CGPoint(x: position.x, y: size.height - position.y)
planNode.position = position
planPoint = position
}
}
复制代码
4、飞机发射子弹
4.1、发射子弹的话我们创建一个定时器,在定时器方法里面生成子弹,由于我们是关卡制,所以间隔时间根据关卡来定,关卡越高时间越短
private func startBulletTimer() {
var ti = 0.2 - TimeInterval(leve) * 0.02
if ti <= 0.05 {
ti = 0.05
}
bulletTimer = Timer.scheduledTimer(timeInterval: ti, target: self, selector: #selector(createBullet), userInfo: nil, repeats: true)
}
// 创建子弹
@objc private func createBullet() {
let bulletNode = SKSpriteNode(imageNamed: "plan_bullet")
bulletNode.position = planPoint
bulletNode.anchorPoint = CGPoint(x: 0.5, y: 1)
bulletNode.size = CGSize(width: 10, height: 10)
bulletNode.zPosition = 1
bulletNode.name = "bullet"
addChild(bulletNode)
var ti = 3 - TimeInterval(leve) * 0.5
if ti <= 0.5 {
ti = 0.5
}
// 让子弹向上移动
bulletNode.run(SKAction.moveTo(y: size.height, duration: ti)) {
bulletNode.removeAllActions()
bulletNode.removeFromParent()
}
bulletNode.physicsBody = SKPhysicsBody(rectangleOf: bulletNode.size)
//物理体是否受力
bulletNode.physicsBody?.isDynamic = true
bulletNode.physicsBody?.allowsRotation = false
bulletNode.physicsBody?.collisionBitMask = 0
//设置物理体的标识符
bulletNode.physicsBody?.categoryBitMask = 1
//设置可与哪一类的物理体发生碰撞
bulletNode.physicsBody?.contactTestBitMask = 2
}
复制代码
5、分别添加返回按钮以及显示关卡、得分、生命的精灵,代码比较简单,就不贴出来了
6、创建敌机、血包和增加攻击力包
6.1、创建敌机我们还是使用定时器
// 敌机定时器
private func startEnemyTimer() {
var ti = 0.5 - TimeInterval(leve) * 0.05
if ti <= 0.1 {
ti = 0.1
}
enemyTimer = Timer.scheduledTimer(timeInterval: ti, target: self, selector: #selector(createEnemy), userInfo: nil, repeats: true)
}
// 创建敌机
@objc private func createEnemy() {
let enemyNode = SKSpriteNode(imageNamed: "plan01")
// 随机敌机出现的位置
let pointX = CGFloat(arc4random_uniform(UInt32(size.width - 40)))
enemyNode.position = CGPoint(x: pointX + 20, y: size.height)
enemyNode.anchorPoint = CGPoint(x: 0.5, y: 0.5)
enemyNode.size = CGSize(width: 40, height: 40)
enemyNode.zPosition = 1
enemyNode.name = "enemy"
addChild(enemyNode)
var ti = 6 - TimeInterval(leve) * 0.3
if ti <= 1 {
ti = 1
}
// 敌机向下移动,移动速度根据关卡来
enemyNode.run(SKAction.moveTo(y: 0, duration: ti)) {
enemyNode.removeAllActions()
enemyNode.removeFromParent()
}
enemyNode.physicsBody = SKPhysicsBody(rectangleOf: enemyNode.size)
//物理体是否受力
enemyNode.physicsBody?.isDynamic = true
enemyNode.physicsBody?.allowsRotation = false
//设置物理体的标识符
enemyNode.physicsBody?.categoryBitMask = 2
//设置可与哪一类的物理体发生碰撞
enemyNode.physicsBody?.contactTestBitMask = 1
enemyNode.physicsBody?.collisionBitMask = 0
enemyNode.physicsBody?.mass = CGFloat(enemyLife)
}
复制代码
6.2、创建血包和增加攻击力包,这两个每个关卡分别只会出现一次,每个关卡时长里随机出现
private func createPotionOrAttack() {
// 创建药水
DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(arc4random_uniform(5))) {
self.createNode("plan_potion")
}
// 增加攻击力
DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(arc4random_uniform(5))) {
self.createNode("plan_attack")
}
}
private func createNode(_ name: String) {
let node = SKSpriteNode(imageNamed: name)
let pointX = CGFloat(arc4random_uniform(UInt32(self.size.width - 40)))
node.position = CGPoint(x: pointX + 20, y: self.size.height)
node.anchorPoint = CGPoint(x: 0.5, y: 0.5)
node.size = CGSize(width: 40, height: 40)
node.zPosition = 1
node.name = name
self.addChild(node)
var ti = 6 - TimeInterval(leve) * 0.3
if ti <= 1 {
ti = 1
}
node.run(SKAction.moveTo(y: 0, duration: ti)) {
node.removeAllActions()
node.removeFromParent()
}
node.physicsBody = SKPhysicsBody(rectangleOf: node.size)
//物理体是否受力
node.physicsBody?.isDynamic = true
node.physicsBody?.allowsRotation = false
//设置物理体的标识符
node.physicsBody?.categoryBitMask = 2
//设置可与哪一类的物理体发生碰撞
node.physicsBody?.contactTestBitMask = 1
node.physicsBody?.collisionBitMask = 0
}
复制代码
7、创建Boss与Boss发射子弹
7.1、创建boss我们还是使用定时器,没错还是定时器。每个关卡多长时间,这个boss的定时器就设置多长时间,为了方便测试,暂时设定5s
// boss出现定时器
private func startBossTimer() {
bossTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(createBoss), userInfo: nil, repeats: true)
}
// 创建boss
@objc private func createBoss() {
bossNode = SKSpriteNode(imageNamed: "boss01")
bossNode?.position = CGPoint(x: size.width / 2, y: size.height - 50)
bossNode?.anchorPoint = CGPoint(x: 0.5, y: 1)
bossNode?.size = CGSize(width: 98, height: 127)
bossNode?.zPosition = 1
bossNode?.name = "boss"
addChild(bossNode!)
// 让boss左右移动
let wait = SKAction.wait(forDuration: 2)
let action1 = SKAction.moveTo(x: 64, duration: 1.5)
let action2 = SKAction.moveTo(x: size.width - 64, duration: 3)
bossNode?.run(wait) {
self.bossNode?.run(SKAction.repeatForever(SKAction.sequence([action1, action2])))
}
bossNode?.physicsBody = SKPhysicsBody(rectangleOf: bossNode?.size ?? .zero)
//物理体是否受力
bossNode?.physicsBody?.isDynamic = true
bossNode?.physicsBody?.allowsRotation = false
//设置物理体的标识符
bossNode?.physicsBody?.categoryBitMask = 2
//设置可与哪一类的物理体发生碰撞
bossNode?.physicsBody?.contactTestBitMask = 1
bossNode?.physicsBody?.collisionBitMask = 0
bossNode?.physicsBody?.mass = CGFloat(leve) * 1000
// 生命值
bossLife = leve * 1000
bossLifeNode = SKLabelNode(text: "\(bossLife)/\(bossLife)")
bossLifeNode?.fontColor = .green
bossLifeNode?.fontSize = 20
bossLifeNode?.position = bossNode?.position ?? .zero
bossLifeNode?.horizontalAlignmentMode = .center
bossLifeNode?.zPosition = 1
addChild(bossLifeNode!)
bossLifeNode?.run(SKAction.wait(forDuration: 2)) {
self.bossLifeNode?.run(SKAction.repeatForever(SKAction.sequence([
SKAction.moveTo(x: 64, duration: 1.5),
SKAction.moveTo(x: self.size.width - 64, duration: 3)
])))
}
// 停止敌机创建定时器
stopEnemyTimer()
// 停止Boss定时器
stopBossTimer()
// boss发射子弹
startBossBulletTimer()
}
复制代码
7.2、Boss发射子弹,这里我们开启一个boss发射子弹的定时器, 并在createBoss()
方法里调用
// boss发射子弹定时器
private func startBossBulletTimer() {
var ti = 1 - TimeInterval(leve) * 0.05
if ti <= 0.1 {
ti = 0.1
}
bossBulletTimer = Timer.scheduledTimer(timeInterval: ti, target: self, selector: #selector(createBossBullet), userInfo: nil, repeats: true)
}
// 创建boss Bullet
@objc private func createBossBullet() {
let bossBulletNode = SKSpriteNode(imageNamed: "boss_bullet")
bossBulletNode.position = bossNode?.position ?? CGPoint(x: size.width / 2, y: size.height)
bossBulletNode.anchorPoint = CGPoint(x: 0.5, y: 1)
bossBulletNode.size = CGSize(width: 12, height: 25)
bossBulletNode.zPosition = 1
bossBulletNode.name = "bossBullet"
addChild(bossBulletNode)
var ti = 6 - TimeInterval(leve) * 0.3
if ti <= 1 {
ti = 1
}
bossBulletNode.run(SKAction.moveTo(y: 0, duration: ti)) {
bossBulletNode.removeAllActions()
bossBulletNode.removeFromParent()
}
bossBulletNode.physicsBody = SKPhysicsBody(rectangleOf: bossBulletNode.size)
//物理体是否受力
bossBulletNode.physicsBody?.isDynamic = true
bossBulletNode.physicsBody?.allowsRotation = false
//设置物理体的标识符
bossBulletNode.physicsBody?.categoryBitMask = 2
//设置可与哪一类的物理体发生碰撞
bossBulletNode.physicsBody?.contactTestBitMask = 1
bossBulletNode.physicsBody?.collisionBitMask = 0
}
复制代码
该创建的创建好了,该动的动起来了,下面我们来实现各个节点的碰撞,来消灭敌机与boss
8、消灭敌机与boss
8.1、消灭敌机与boss我们就使用物理引擎的碰撞来实现,主要是使用SKPhysicsContactDelegate
中的didBegin(_ contact: SKPhysicsContact)
这个代理方法实现的。
8.2、首先,我们要对场景的physicsWorld
属性设置代理对象为场景自身
physicsWorld.contactDelegate = self
复制代码
8.3、遵守SKPhysicsContactDelegate
并实现didBegin(_ contact: SKPhysicsContact)
方法
extension PlanScene: SKPhysicsContactDelegate {
func didBegin(_ contact: SKPhysicsContact) {
}
}
复制代码
在didBegin(_ contact: SKPhysicsContact)
方法中返回了一个SKPhysicsContact
对象
open class SKPhysicsContact : NSObject {
// 碰撞体A
open var bodyA: SKPhysicsBody { get }
// 碰撞体B
open var bodyB: SKPhysicsBody { get }
// 在场景坐标中,两个物理体之间的接触点。
open var contactPoint: CGPoint { get }
// 指定碰撞方向的法向量。
open var contactNormal: CGVector { get }
// 这两个物体在牛顿秒内相互撞击的强度。
open var collisionImpulse: CGFloat { get }
}
复制代码
我们主要使用SKPhysicsContact
中的bodyA
和bodyB
来获取两个碰撞的节点。那么我们怎么知道bodyA
和bodyB
是敌机还是我们飞机呢?查看文档我们会发现有这么一段描述,我们可以通过给场景中的每个物理体,设置categoryBitMask(物理体的标识符)
和contactTestBitMask(标记可与哪一类的物理体发生碰撞)
属性
8.4、在整个游戏中,我们创建了有飞机
、机发射的子弹
、敌机
、boss
、boss发射的子弹
、攻击力包
和血包
这几个物理体
其中
飞机
、飞机发射的子弹
我们设置categoryBitMask
为1
,contactTestBitMask
为2
。
那么对应的敌机
、boss
、boss发射的子弹
、攻击力包
和血包
我们设置categoryBitMask
为2
,contactTestBitMask
为1
。
8.5、判断出bodyA
和bodyB
分别代表什么
extension PlanScene: SKPhysicsContactDelegate {
func didBegin(_ contact: SKPhysicsContact) {
// 飞机、机发射的子弹
var planeNode: SKSpriteNode?
// 敌机、boss、boss发射的子弹、攻击力包和血包
var enemyNode: SKSpriteNode?
if contact.bodyA.categoryBitMask == 1 && contact.bodyB.categoryBitMask == 2 {
planeNode = contact.bodyA.node as? SKSpriteNode
enemyNode = contact.bodyB.node as? SKSpriteNode
} else {
planeNode = contact.bodyB.node as? SKSpriteNode
enemyNode = contact.bodyA.node as? SKSpriteNode
}
}
}
复制代码
区分好bodyA
和bodyB
分别代表什么之后,我们就开始做相应处理
8.5、处理节点碰撞
extension PlanScene: SKPhysicsContactDelegate {
func didBegin(_ contact: SKPhysicsContact) {
var planeNode: SKSpriteNode?
var enemyNode: SKSpriteNode?
if contact.bodyA.categoryBitMask == 1 && contact.bodyB.categoryBitMask == 2 {
planeNode = contact.bodyA.node as? SKSpriteNode
enemyNode = contact.bodyB.node as? SKSpriteNode
} else {
planeNode = contact.bodyB.node as? SKSpriteNode
enemyNode = contact.bodyA.node as? SKSpriteNode
}
guard let planeNode = planeNode, let enemyNode = enemyNode else { return }
// 增加生命
if enemyNode.name == "plan_potion", planeNode.name == "plan" {
ownLife += 1
enemyNode.removeAllActions()
enemyNode.removeFromParent()
return
}
// 增加攻击力
if enemyNode.name == "plan_attack", planeNode.name == "plan" {
aggressivity += 10
enemyNode.removeAllActions()
enemyNode.removeFromParent()
return
}
// 处理飞机
switch planeNode.name {
case "bullet" where (enemyNode.name != "bossBullet" &&
enemyNode.name != "plan_potion" &&
enemyNode.name != "plan_attack"):
planeNode.removeAllActions()
planeNode.removeFromParent()
if enemyNode.name == "boss" {
bossLife -= aggressivity
bossLifeNode?.text = "\(bossLife)/\(leve*1000)"
}
case "plan":
ownLife -= 1
if ownLife <= 0 {
gameOver()
planeNode.removeAllActions()
planeNode.removeFromParent()
}
default:
break
}
// 如果是boss发射的子弹,则不做处理
if enemyNode.name == "bossBullet" {
return
}
if enemyNode.name == "plan_potion" {
return
}
if enemyNode.name == "plan_attack" {
return
}
enemyNode.physicsBody?.mass -= CGFloat(aggressivity)
if enemyNode.physicsBody?.mass ?? 0 <= CGFloat(aggressivity) {
enemyNode.removeAllActions()
enemyNode.removeFromParent()
switch enemyNode.name {
case "enemy":
score += enemyLife
case "boss":
score += leve * 1000
leve += 1
enemyLife += leve * 10
stopBossBulletTimer()
bossNode?.removeAllActions()
bossNode?.removeFromParent()
bossLifeNode?.removeAllActions()
bossLifeNode?.removeFromParent()
startBulletTimer()
startEnemyTimer()
startBossTimer()
default:
break
}
}
}
}
复制代码
基本功能已经完成了,下面我们就来添加碰撞效果
8.6、添加碰撞效果
碰撞效果我们使用SKEmitterNode
来做,然后在碰撞的节点处显示出来。我们选择SpriteKit Particle File
来新建一个目标销毁时的效果Blast.sks
和击打boss产生的效果Strike.sks
文件 然后分别选中两个文件,设置相应参数,我这边是随便设置的就不多说了。
根据这两个文件名称来初始化,并在相应的位置加载
private func blast(_ position: CGPoint, fileName: String) {
if let blast = SKEmitterNode(fileNamed: fileName) {
blast.zPosition = 2
blast.position = position
addChild(blast)
// 0.3秒后消失
blast.run(SKAction.sequence([
SKAction.wait(forDuration: 0.3),
SKAction.run {
blast.removeAllActions()
blast.removeFromParent()
}
]))
}
}
复制代码
The final effect is as shown at the top
finally
The whole game is over here, come and show your record