我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛
前言
记得很多年以前玩过一款很好玩的手游叫雷霆战机,那时候玩的很疯狂。那么今天我们使用SpriteKit
来实现一个简易版的雷霆战机
安装
1、iOS
端安装有点难受,不像安卓直接给一个apk
就好了,我这边的话上了一个TestFlight
,可以下载一个TestFlight
APP,使用TestFlight
安装
使用微信打开这个地址按提示安装就可以了:
打开后大概是这样的:
2、可以运行代码安装,demo地址在这
玩法
1、该游戏为闯关游戏,玩家拖动飞机消灭敌人来获取分数
2、初始的时候玩家有3条生命,每一关会随机掉落一个血包和增加飞机的攻击力。敌人的生命值也是随着闯关级数逐级增加的,每一关都会有一个boss,打败boss会获得大量分数
下面我们就开始一步一步的实现吧。
实现
1、新建项目,在项目里创建一个PlanScene
场景
1.1、在didMove(to view: SKView)
方法里设置重力加速度为0,0
override func didMove(to view: SKView) {
super.didMove(to: view)
//设置重力加速度
physicsWorld.gravity = .zero
}
2、背景循环滚动
2.1、要让背景循环滚动的话,我们需要创建两个一样的背景精灵,bgNode1
的position
设置成CGPoint(x: 0, y: 0)
,bgNode2
的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)
方法里重新设置bgNode1
和bgNode2
的position
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)
}
}
这样的话背景就可以无限滚动起来了
3、添加飞机并拖动飞机移动
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()
}
]))
}
}
最后效果就入顶部展示的那样
最后
整个游戏到此就结束了,快来秀出你的战绩吧